var student=context.Students.FirstOrDefault();
var student=context.Students.LastOrDefault();
var student=context.Students.ToList().LastOrDefault();
var student=context.Students.OrderByDescending(s=>s.Id).FirstOrDefault();
var student=context.Students.FirstOrDefault();
var student=context.Students.LastOrDefault();
var student=context.Students.ToList().LastOrDefault();
var student=context.Students.OrderByDescending(s=>s.Id).FirstOrDefault();
هر کامپوننتی در React یک چرخه زندگی دارد. زمانیکه یک کامپوننت را به روش React.createClass یا React.Component تعریف میکنیم و در ReactDOM.render نمونهای از کامپوننت را برای نمایش در مرورگر میسازیم، چرخه حیات آن شروع میشود.
کتابخانه ReactDOMServer جهت ساخت یا render کردن کامپوننتها در سمت سرور استفاده میشود. توسط این کتابخانه میتوانیم کامپوننتها را در سمت سرور ایجاد کنیم و نتیجه آن را که تگهای HTML هستند به مرورگر ارسال کنیم. این روش جهت داشتن صفحههای وب سریعتر و اهداف SEO مفید است. جهت اطلاعات بیشتر و روشهای استفاده به مستندات آن رجوع کنید. در مثال زیر روش استفاده از این کتابخانه به اختصار آمده.
var persons = [ { id: 1, personName: "Parham", personContact: "parhamda@gmail.com" }, { id: 2, personName: "Roham", personContact: "roham@yahoo.com" }, { id: 3, personName: "Raha", personContact: "raha@live.com" } ]; class Person extends React.Component{ render(){ return ( <div> <p>{this.props.personName}</p> <p>{this.props.personContact}</p> </div> ) } } let person1 = persons[0]; let personElement = <Person personName={person1.personName} personContact={person1.personContact}/> console.log(ReactDOMServer.renderToStaticMarkup(personElement));
در کد بالا مواردی که جدید هستند، یکی ساخت یک نمونه از کامپوننت Person است و دیگری ساخت آن در سمت سرور، بدون آن که فعلا نمایشی در مرورگر داشته باشیم. در کنسول میتوانیم خروجی کتابخانه را که تگهای HTML هستند ببینیم. ReactDOMServer دو متد را فراهم کرده که کارکردی مشابه دارند؛ اما در جزئیات متفاوت هستند.
در نهایت خروجی از هر نوع که بود، برای اینکه در سمت کاربر قابل مشاهده باشد باید از همان متد ReactDOM.render استفاده کنیم. از آنجایی که این مجموعه جهت معرفی و بررسی ابزارهای اصلی React به صورت مختصر است، از آوردن مثالهای زیاد و پیچیده پرهیز میکنم. در اینجا میتوانید یک نمونه ساده برای استفاده از ReactDOMServer به صورت استاندارد و با جزئیات را بررسی کنید.
React چند متد را برای زمانهای قبل و بعد از ساخت شدن یک کامپوننت در DOM دارد که میشود رفتارهایی را برای کامپوننت، در این متدها در نظر گرفت تا در زمان مناسب اجرا شوند. در ادامه این متدها معرفی و کاربرد هر یک بیان میشود.
componentWillMount: این متد قبل از اینکه کامپوننت، تگهای متد render را بسازد اجرا میشود. این متد هم در سمت کلاینت کاربرد دارد و هم در سمت سرور. به همین جهت برای گرفتن log از دادههای کامپوننت و کار با پایگاه داده مکان مناسبی است. به عنوان مثال در قطعه کد زیر دادههای کامپوننت، توسط Ajax ارسال شدهاند.
componentWillMount() { Ajax.post("/componentLog", { name: this.constructor.name, props: this.props }); }
componentDidMount: این متد بعد از اینکه بخش render اجرا شد فراخوانی میشود. همچنین فقط در سمت کلاینت و زمانیکه از ReactDOM.render استفاده میکنیم کاربرد دارد. این متد مناسب برای تعامل کامپوننت با افزونهها و APIها است؛ مانند دریافت اطلاعات مورد نیاز کامپوننت از سایتی دیگر توسط یک API. از این متد در قسمت چهارم مثالی آورده شده.
(componentWillReciveProps(nextProps: این متد زمانی اجرا میشود که دادههای ورودی کامپوننت با مقادیری جدید تغییر کنند.
componentWillReceiveProps(nextProps) { // Do something with new received data and change the state. } ReactDOM.render( <TestComponent someData={newDataEveryFiveSecond()}/>, document.getElementById("divTest") );
در مثال بالا یک کامپوننت داریم که دادههای ورودی خود را از یک تابع میگیرد. این تابع هر پنج ثانیه یک بار یک داده تازه ایجاد میکند و به کامپوننت ارسال میکند. میتوانیم داخل کامپوننت، از متد componentWillReceiveProps جهت دستکاری دادههای رسیده و تغییر وضعیت کامپوننت توسط setState استفاده کنیم.
(shouldComponentUpdate(nextProps, nextState: این متد شبیه به متد componentWillReceiveProps است، البته با تفاوتهایی. این متد هم مقدار ورودی جدید برای پارامترهای کامپوننت میگیرد و هم مقداری برای وضعیتی که کامپوننت دارد. این متد باید یک مقدار بازگشتی false یا true داشته باشد. با این مقدار بازگشتی میتوان کنترل کرد که آیا کامپوننت بر اساس دادههای جدید بروز بشود یا نه.
class ComponentExample extends React.Component { shouldComponentUpdate(nextProps, nextState) { return notEqual(this.props, nextProps) || notEqual(this.state, nextState); } }
در مثال بالا پارامترها و وضعیت جاری کامپوننت، با مقدارهای تازه تغییر یافته و وضعیت جدید مقایسه میشوند. اگر مقادیر مقایسه شده برابر نباشند (یعنی داده تکراری وارد نشده) مقدار بازگشتی true خواهد بود و React کامپوننت را بر اساس وضعیت جدید و دادههای تازه دوباره میسازد.
(componentWillUpdate(nextProps, nextState: این متد زمانیکه کامپوننت ساخته شده، دادههای جدیدی را دریافت کند و یا وضعیت آن تغییر کند و دقیقا قبل از اجرای render فراخوانی میشود. اگر از متد shouldComponentUpdate مقدار false بازگشت داده شود، این متد دیگر اجرا نخواهد شد. باید توجه داشته باشیم که setState را نمیشود در این متد پیادهسازی کرده. به این علت که، زمانیکه وضعیت کامپوننت تغییر میکند، React متد componentWillUpdate و بلافاصله بعد از آن render را اجرا میکند و برای تغییر وضعیت دیگر دیر شده! تفاوت componentWillUpdate با componentWillMount این است که Will Mount در اولین وهله سازی از کامپوننت اجرا میشود، ولی Will Update بعد از هر دوباره سازی (rerender).
(componentDidUpdate(prevProps, prevStat: احتمالا میشود به راحتی حدس زد که این متد دقیقا بعد از دوباره سازی کامپوننتی که ساخته شده فراخوانی میشود.
componentWillUnmount: این متد زمانی اجرا میشود که یک کامپوننت از DOM پاک شود. برای پاک کردن نمونهای از یک کامپوننت که در DOM در حال نمایش است میتوانیم از دستور زیر استفاده کنیم.
ReactDOM.unmountComponentAtNode(document.getElementById("react"));
تابع ()parseInt در جاوااسکریپت، در صورتیکه یک مقدار رشتهای را به آن ارسال کنید و پایهی مناسب آن را تعیین نکنید، نتایج غیره منتظرهای (unexpected) را باز میگرداند . برای مثال:
# Returns 8, not 10! parseInt('010') is 8
# Use base 10 for the correct result parseInt('010', 10) is 10
با این حال، Strict mode به طور کامل با مرورگرهای قدیمی سازگار است.
تغییرات Strict mode
با این حال، برخی از رفتارهای زمان اجرای Strict mode نیز تغییر کرده است:
CoffeeScript در حال حاضر بسیاری از الزامات Strict mode را پیاده سازی کردهاست مانند: همیشه از کلمه کلیدی var برای تعریف متغیر استفاده میکند؛ اما فعال کردن Strict mode در برنامههای CoffeeScript نیز بسیار مفید خواهد بود. در واقع CoffeeScript بر روی انطباق برنامهها با Strict mode در زمان کامپایل را، در برنامههای آینده خود دارد.
استفاده از Strict mode
برای فعال کردن بررسی محدودیت، کد و توابع خود را با این رشته شروع کنید:-> "use strict" # ... your code ...
do -> "use strict" console.log(arguments.callee)
do -> "use strict" class @Spine
do -> "use strict" class window.Spine
JavaScript Lint یک ابزار بررسی کیفیت کدهای جاوااسکریپت است و اجرای برنامهی شما از طریق این راه عالی باعث بهبود کیفیت و بهترین شیوهی کد نویسی میشود. این پروژه براساس ابزار JSLint است. شما میتوانید چک لیست سایت JSLint را که شامل موضوعاتی است که باید آنها در نظر داشته باشید، مانند متغیرهای سراسری، فراموش کردن نوشتن سمی کالن، کیفیت ضعیف عمل مقایسه را نام برد.
خبر خوب این است که CoffeeScript تمام موارد گفته شدهی در چک لیست را انجام میدهد. بنابراین کد تولیدی CoffeeScript با JavaScript Lint کاملا سازگار است. در واقع ابزار coffee از lint ،option پشتیبانی میکند.
coffee --lint index.coffee index.coffee: 0 error(s), 0 warning(s)
import RegisterForm from "./components/registerForm";
<Route path="/register" component={RegisterForm} />
<NavLink className="nav-item nav-link" to="/register"> Register </NavLink>
import Joi from "@hapi/joi"; import React from "react"; import Form from "./common/form"; class RegisterForm extends Form { state = { data: { username: "", password: "", name: "" }, errors: {} }; schema = { username: Joi.string() .required() .email({ minDomainSegments: 2, tlds: { allow: ["com", "net"] } }) .label("Username"), password: Joi.string() .required() .min(5) .label("Password"), name: Joi.string() .required() .label("Name") }; doSubmit = () => { // Call the server console.log("Submitted"); }; render() { return ( <div> <h1>Register</h1> <form onSubmit={this.handleSubmit}> {this.renderInput("username", "Username")} {this.renderInput("password", "Password", "password")} {this.renderInput("name", "Name")} {this.renderButton("Register")} </form> </div> ); } } export default RegisterForm;
import { Link } from "react-router-dom"; // ... <div className="col"> <Link to="/movies/new" className="btn btn-primary" style={{ marginBottom: 20 }} > New Movie </Link> <p>Showing {totalCount} movies in the database.</p>
import Joi from "@hapi/joi"; import React from "react"; import { getGenres } from "../services/fakeGenreService"; import { getMovie, saveMovie } from "../services/fakeMovieService"; import Form from "./common/form"; class MovieForm extends Form { state = { data: { title: "", genreId: "", numberInStock: "", dailyRentalRate: "" }, genres: [], errors: {} };
schema = { _id: Joi.string(), title: Joi.string() .required() .label("Title"), genreId: Joi.string() .required() .label("Genre"), numberInStock: Joi.number() .required() .min(0) .max(100) .label("Number in Stock"), dailyRentalRate: Joi.number() .required() .min(0) .max(10) .label("Daily Rental Rate") };
componentDidMount() { const genres = getGenres(); this.setState({ genres }); const movieId = this.props.match.params.id; if (movieId === "new") return; const movie = getMovie(movieId); if (!movie) return this.props.history.replace("/not-found"); this.setState({ data: this.mapToViewModel(movie) }); }
mapToViewModel(movie) { return { _id: movie._id, title: movie.title, genreId: movie.genre._id, numberInStock: movie.numberInStock, dailyRentalRate: movie.dailyRentalRate }; }
doSubmit = () => { saveMovie(this.state.data); this.props.history.push("/movies"); };
render() { return ( <div> <h1>Movie Form</h1> <form onSubmit={this.handleSubmit}> {this.renderInput("title", "Title")} {this.renderSelect("genreId", "Genre", this.state.genres)} {this.renderInput("numberInStock", "Number in Stock", "number")} {this.renderInput("dailyRentalRate", "Rate")} {this.renderButton("Save")} </form> </div> ); }
import React from "react"; const Select = ({ name, label, options, error, ...rest }) => { return ( <div className="form-group"> <label htmlFor={name}>{label}</label> <select name={name} id={name} {...rest} className="form-control"> <option value="" /> {options.map(option => ( <option key={option._id} value={option._id}> {option.name} </option> ))} </select> {error && <div className="alert alert-danger">{error}</div>} </div> ); }; export default Select;
import Select from "./select"; // ... class Form extends Component { // ... renderSelect(name, label, options) { const { data, errors } = this.state; return ( <Select name={name} value={data[name]} label={label} options={options} onChange={this.handleChange} error={errors[name]} /> ); } }
import React from "react"; const SearchBox = ({ value, onChange }) => { return ( <input type="text" name="query" className="form-control my-3" placeholder="Search..." value={value} onChange={e => onChange(e.currentTarget.value)} /> ); }; export default SearchBox;
<p>Showing {totalCount} movies in the database.</p> <SearchBox value={searchQuery} onChange={this.handleSearch} />
import SearchBox from "./searchBox"; //... class Movies extends Component { state = { //... selectedGenre: {}, searchQuery: "" }; handleSearch = query => { this.setState({ searchQuery: query, selectedGenre: null, currentPage: 1 }); }; handleGenreSelect = genre => { console.log("handleGenreSelect", genre); this.setState({ selectedGenre: genre, searchQuery: "", currentPage: 1 }); };
getPagedData() { const { pageSize, currentPage, selectedGenre, movies: allMovies, sortColumn, searchQuery } = this.state; let filteredMovies = allMovies; if (searchQuery) { filteredMovies = allMovies.filter(m => m.title.toLowerCase().startsWith(searchQuery.toLowerCase()) ); } else if (selectedGenre && selectedGenre._id) { filteredMovies = allMovies.filter(m => m.genre._id === selectedGenre._id); }
<button data-perm="true" data-controller="c" data-action="r" data-method="post" data-type="disable">Test 1</button> <button data-perm="true" data-controller="c" data-action="t" data-method="post">Test 2</button> <button data-perm="false" data-controller="c" data-action="r" data-method="post">Test 3</button> <a data-perm="true" data-controller="c" data-action="m" data-method="post">Test 4</a> <a data-perm="false" data-controller="c" data-action="m" data-method="post">Test 5</a> <a data-perm="true" data-controller="c" data-action="t" data-method="post">Test 6</a>
ویژگی | توضیحات |
perm | آیا نیاز به اعتبارسنجی دارد یا خیر؟ در صورتی که المانی مقدار Perm آن با مقدار true پر گردد، اعتبارسنجی روی آن اعمال خواهد شد. |
controller | نام کنترلری که به آن دسترسی دارد. |
action | نام اکشنی که به آن در کنترلر ذکر شده دسترسی دارد. |
method | در صورتیکه دسترسی get و post و ... هر یک متفاوت باشد. |
type | نحوه برخورد با المان غیرمجاز. در صورتیکه با disable مقداردهی شود، المان غیرفعال و در غیر اینصورت، از روی صفحه حذف میشود. |
سپس یک کلاس جدید ساخته و با ارث بری از ActionFilterAttribute، کار ساخت فیلتر را آغاز میکنیم:
public class AuthorizePage: ActionFilterAttribute { private HtmlTextWriter _htmlTextWriter; private StringWriter _stringWriter; private StringBuilder _stringBuilder; private HttpWriter _output; IAuthorization _auth; public override void OnActionExecuting(ActionExecutingContext filterContext) { _stringBuilder = new StringBuilder(); _stringWriter = new StringWriter(_stringBuilder); _htmlTextWriter = new HtmlTextWriter(_stringWriter); _output = (HttpWriter) filterContext.RequestContext.HttpContext.Response.Output; filterContext.RequestContext.HttpContext.Response.Output = _htmlTextWriter; _auth = new Auth(); } public override void OnResultExecuted(ResultExecutedContext filterContext) { var response = _stringBuilder.ToString(); response = AuthorizeTags(response); _output.Write(response); } public string AuthorizeTags(string response) { var doc = GetHtmlDocument(response); var nodes=doc.DocumentNode.SelectNodes("//*[@data-perm]"); if (nodes == null) return response; foreach(var node in nodes) { var dataPermission = node.Attributes["data-perm"]; if(!dataPermission.Value.TryBooleanParse()) { continue; } var controller = node.Attributes["data-controller"].Value; var action = node.Attributes["data-action"].Value; var method = node.Attributes["data-method"].Value; var access=_auth.Authorize(HttpContext.Current.User.Identity.Name , controller, action, method); if (access) continue; var removeElm = true; var type = node.Attributes["data-type"]?.Value; if (type!=null && type.ToLower()== "disable") { removeElm = false; } if(removeElm) { node.Remove(); continue; } node.Attributes.Add("disabled", "true"); } return doc.DocumentNode.OuterHtml; } private HtmlDocument GetHtmlDocument(string htmlContent) { var doc = new HtmlDocument { OptionOutputAsXml = true, OptionDefaultStreamEncoding = Encoding.UTF8 }; doc.LoadHtml(htmlContent); return doc; } }
public interface IAuthorization { bool Authorize(string userId, string controller, string action, string method); }
protected void Application_Start() { GlobalFilters.Filters.Add(new AuthorizePage()); }
ng g c contact-manager/components/notes --no-spec
<mat-tab-group> <mat-tab label="Bio"> <p> {{user.bio}} </p> </mat-tab> <mat-tab label="Notes"> <app-notes [notes]="user.userNotes"></app-notes> </mat-tab> </mat-tab-group>
import { Component, Input, OnInit } from "@angular/core"; import { UserNote } from "../../models/user-note"; @Component({ selector: "app-notes", templateUrl: "./notes.component.html", styleUrls: ["./notes.component.css"] }) export class NotesComponent implements OnInit { @Input() notes: UserNote[];
<p> {{notes | json}} </p>
import { Component, Input, OnInit } from "@angular/core"; import { MatTableDataSource } from "@angular/material"; import { UserNote } from "../../models/user-note"; @Component({ selector: "app-notes", templateUrl: "./notes.component.html", styleUrls: ["./notes.component.css"] }) export class NotesComponent implements OnInit { @Input() notes: UserNote[]; displayedColumns = ["position", "title", "date"]; dataSource: MatTableDataSource<UserNote>; constructor() { } ngOnInit() { this.dataSource = new MatTableDataSource<UserNote>(this.notes); } }
<div class="example-container mat-elevation-z8" fxLayout="column"> <mat-table #table [dataSource]="dataSource"> <ng-container matColumnDef="position"> <mat-header-cell *matHeaderCellDef> No. </mat-header-cell> <mat-cell *matCellDef="let note"> {{note.id}} </mat-cell> </ng-container> <ng-container matColumnDef="title"> <mat-header-cell *matHeaderCellDef> Title </mat-header-cell> <mat-cell *matCellDef="let note"> {{note.title}} </mat-cell> </ng-container> <ng-container matColumnDef="date"> <mat-header-cell *matHeaderCellDef> Date </mat-header-cell> <mat-cell *matCellDef="let note"> {{note.date | date:'yyyy-MM-dd'}} </mat-cell> </ng-container> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row> </mat-table> </div>
</mat-table> <mat-paginator #paginator [pageSize]="2" [pageSizeOptions]="[2, 4, 6]"> </mat-paginator> </div>
export class NotesComponent implements OnInit, AfterViewInit { dataSource: MatTableDataSource<UserNote>; @ViewChild(MatPaginator) paginator: MatPaginator; ngAfterViewInit() { this.dataSource.paginator = this.paginator; } }
<div class="example-container mat-elevation-z8" fxLayout="column"> <div class="example-header"> <mat-form-field> <input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter"> </mat-form-field> </div>
applyFilter(filterValue: string) { this.dataSource.filter = filterValue.trim().toLowerCase(); // MatTableDataSource defaults to lowercase matches }
<mat-table #table [dataSource]="dataSource" matSort> <ng-container matColumnDef="position"> <mat-header-cell *matHeaderCellDef mat-sort-header> No. </mat-header-cell>
export class NotesComponent implements OnInit, AfterViewInit { dataSource: MatTableDataSource<UserNote>; @ViewChild(MatSort) sort: MatSort; ngAfterViewInit() { this.dataSource.sort = this.sort; } }
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="BlogUrl" value="https://www.blogger.com/feeds/blogId/posts/default" />
<add key="UserName" value="name@gmail.com" />
<add key="Password" value="myPass..." />
<add key="PostAt" value="00:05" />
<add key="FeedToParse" value="http://feeds2.feedburner.com/VahidsSharedItemsInGoogleReader" />
<add key="DBName" value="D:\Prog\db.xml"/>
<add key="Tag" value="News, daily news" />
<add key="Title" value="خلاصه اشتراکهای روز " />
<add key="UsePersianDate" value="true" />
<add key="ErrorsLogFile" value="D:\Prog\errors.log"/>
<add key="ReadSitesDataIntervalMin" value="15"/>
<add key="GooglePlusUserId" value="105013528531611201860"/>
<!--proxy settings-->
<add key="IsProxyEnabled" value="false"/>
<add key="ProxyServerAddress" value="127.0.0.1"/>
<add key="ProxyServerPort" value="8080"/>
<add key="ProxyServerUserName" value="user1"/>
<add key="ProxyServerPassword" value="pass1"/>
</appSettings>
</configuration>
if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); }
//if (env.IsDevelopment()) //{ //app.UseDeveloperExceptionPage(); //} //else //{ app.UseExceptionHandler("/Error"); //}
@page "/counter" <PageTitle>Counter</PageTitle> <h1>Counter</h1> <p role="status">Current count: @currentCount</p> <button @onclick="IncrementCount">Click me</button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; throw new Exception("This is my Exception !!"); } }
using System; using Stateless; namespace StatelessTests { class Program { static void Main(string[] args) { try { string on = "On", off = "Off"; var space = ' '; var onOffSwitch = new StateMachine<string, char>(initialState: off); onOffSwitch.Configure(state: off).Permit(trigger: space, destinationState: on); onOffSwitch.Configure(state: on).Permit(trigger: space, destinationState: off); Console.WriteLine("Press <space> to toggle the switch. Any other key will raise an error."); while (true) { Console.WriteLine("Switch is in state: " + onOffSwitch.State); var pressed = Console.ReadKey(true).KeyChar; onOffSwitch.Fire(trigger: pressed); } } catch (Exception ex) { Console.WriteLine("Exception: " + ex.Message); Console.WriteLine("Press any key to continue..."); Console.ReadKey(true); } } } }
public class StateMachine<TState, TTrigger>
<statemachine xmlns="http://statelessdesigner.codeplex.com/Schema"> <settings> <itemname>BlogPostStateMachine</itemname> <namespace>StatelessTests</namespace> <class>public</class> </settings> <triggers> <trigger>Save</trigger> <trigger>RequireEdit</trigger> <trigger>Accept</trigger> <trigger>Reject</trigger> </triggers> <states> <state start="yes">Begin</state> <state>InProgress</state> <state>Published</state> <state>Rejected</state> </states> <transitions> <transition trigger="Save" from="Begin" to="InProgress" /> <transition trigger="Accept" from="InProgress" to="Published" /> <transition trigger="Reject" from="InProgress" to="Rejected" /> <transition trigger="Save" from="InProgress" to="InProgress" /> <transition trigger="RequireEdit" from="Published" to="InProgress" /> <transition trigger="RequireEdit" from="Rejected" to="InProgress" /> </transitions> </statemachine>
using Stateless; namespace StatelessTests { public class BlogPostStateMachine { public delegate void UnhandledTriggerDelegate(State state, Trigger trigger); public delegate void EntryExitDelegate(); public delegate bool GuardClauseDelegate(); public enum Trigger { Save, RequireEdit, Accept, Reject, } public enum State { Begin, InProgress, Published, Rejected, } private readonly StateMachine<State, Trigger> stateMachine = null; public EntryExitDelegate OnBeginEntry = null; public EntryExitDelegate OnBeginExit = null; public EntryExitDelegate OnInProgressEntry = null; public EntryExitDelegate OnInProgressExit = null; public EntryExitDelegate OnPublishedEntry = null; public EntryExitDelegate OnPublishedExit = null; public EntryExitDelegate OnRejectedEntry = null; public EntryExitDelegate OnRejectedExit = null; public GuardClauseDelegate GuardClauseFromBeginToInProgressUsingTriggerSave = null; public GuardClauseDelegate GuardClauseFromInProgressToPublishedUsingTriggerAccept = null; public GuardClauseDelegate GuardClauseFromInProgressToRejectedUsingTriggerReject = null; public GuardClauseDelegate GuardClauseFromInProgressToInProgressUsingTriggerSave = null; public GuardClauseDelegate GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = null; public GuardClauseDelegate GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit = null; public UnhandledTriggerDelegate OnUnhandledTrigger = null; public BlogPost() { stateMachine = new StateMachine<State, Trigger>(State.Begin); stateMachine.Configure(State.Begin) .OnEntry(() => { if (OnBeginEntry != null) OnBeginEntry(); }) .OnExit(() => { if (OnBeginExit != null) OnBeginExit(); }) .PermitIf(Trigger.Save, State.InProgress , () => { if (GuardClauseFromBeginToInProgressUsingTriggerSave != null) return GuardClauseFromBeginToInProgressUsingTriggerSave(); return true; } ) ; stateMachine.Configure(State.InProgress) .OnEntry(() => { if (OnInProgressEntry != null) OnInProgressEntry(); }) .OnExit(() => { if (OnInProgressExit != null) OnInProgressExit(); }) .PermitIf(Trigger.Accept, State.Published , () => { if (GuardClauseFromInProgressToPublishedUsingTriggerAccept != null) return GuardClauseFromInProgressToPublishedUsingTriggerAccept(); return true; } ) .PermitIf(Trigger.Reject, State.Rejected , () => { if (GuardClauseFromInProgressToRejectedUsingTriggerReject != null) return GuardClauseFromInProgressToRejectedUsingTriggerReject(); return true; } ) .PermitReentryIf(Trigger.Save , () => { if (GuardClauseFromInProgressToInProgressUsingTriggerSave != null) return GuardClauseFromInProgressToInProgressUsingTriggerSave(); return true; } ) ; stateMachine.Configure(State.Published) .OnEntry(() => { if (OnPublishedEntry != null) OnPublishedEntry(); }) .OnExit(() => { if (OnPublishedExit != null) OnPublishedExit(); }) .PermitIf(Trigger.RequireEdit, State.InProgress , () => { if (GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit != null) return GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit(); return true; } ) ; stateMachine.Configure(State.Rejected) .OnEntry(() => { if (OnRejectedEntry != null) OnRejectedEntry(); }) .OnExit(() => { if (OnRejectedExit != null) OnRejectedExit(); }) .PermitIf(Trigger.RequireEdit, State.InProgress , () => { if (GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit != null) return GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit(); return true; } ) ; stateMachine.OnUnhandledTrigger((state, trigger) => { if (OnUnhandledTrigger != null) OnUnhandledTrigger(state, trigger); }); } public bool TryFireTrigger(Trigger trigger) { if (!stateMachine.CanFire(trigger)) { return false; } stateMachine.Fire(trigger); return true; } public State GetState { get { return stateMachine.State; } } } }
namespace StatelessTests { public class BlogPostManager { private BlogPostStateMachine _stateMachine; public BlogPostManager() { configureWorkflow(); } private void configureWorkflow() { _stateMachine = new BlogPostStateMachine(); _stateMachine.GuardClauseFromBeginToInProgressUsingTriggerSave = () => { return UserCanPost; }; _stateMachine.OnBeginExit = () => { /* save data + save state + send an email to admin */ }; _stateMachine.GuardClauseFromInProgressToPublishedUsingTriggerAccept = () => { return UserIsAdmin; }; _stateMachine.GuardClauseFromInProgressToRejectedUsingTriggerReject = () => { return UserIsAdmin; }; _stateMachine.GuardClauseFromInProgressToInProgressUsingTriggerSave = () => { return UserHasEditRights; }; _stateMachine.OnInProgressExit = () => { /* save data + save state + send an email to user */ }; _stateMachine.OnPublishedExit = () => { /* save data + save state + send an email to admin */ }; _stateMachine.GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights; }; _stateMachine.OnRejectedExit = () => { /* save data + save state + send an email to admin */ }; _stateMachine.GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights; }; } public bool UserIsAdmin { get { return true; // TODO: Evaluate if user is an admin. } } public bool UserCanPost { get { return true; // TODO: Evaluate if user is authenticated } } public bool UserHasEditRights { get { return true; // TODO: Evaluate if user is owner or admin } } // User actions public void Save() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Save); } public void RequireEdit() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.RequireEdit); } // Admin actions public void Accept() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Accept); } public void Reject() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Reject); } } }