پَرباد چیست؟
آنچه که شما در این مطلب یاد خواهید گرفت:
- طریقه نصب
- ایجاد صورتحساب و ارسال کاربر به درگاه پرداخت
- تایید صورتحساب
- مردود کردن صورتحساب قبل از انتقال وجه از مشتری به فروشنده
- برگشت وجه به حساب مشتری پس از تأیید صورتحساب
- درگاه مجازی پرداخت (برای تست وب اپلیکیشن، بدون داشتن حساب واقعی در درگاههای بانکی)
- تنظیمات
- ذخیره سازی اطلاعات پرداخت
طریقه نصب
PM> Install-Package Parbad
PM> Install-Package Parbad.MVC5
ایجاد صورتحساب و ارسال کاربر به درگاه پرداخت
var invoice = new Invoice( [Order Number], [Amount], [Verify URL]);
var invoice = new Invoice(1, 30000, "http://www.mywebsite.com/payment/verify" );
var result = Payment.Request(Gateways.Mellat, invoice);
if (result.Status == RequestResultStatus.Success) { // این متد، کاربر را به سمت وب سایت درگاه پرداخت هدایت میکند result.Process(Context); } else { // در صورت تمایل میتوانید پیغام مورد نظر از درگاه پرداخت را نمایش دهید var msg = result.Message; }
در وب سایتهای MVC میتوانید به روش زیر عمل کنید
if (result.Status == RequestResultStatus.Success) { // کاربر را به سمت وب سایت درگاه پرداخت هدایت میکند return new RequestActionResult(result); } else { return View("Error"); }
تأیید صورتحساب
var result = Payment.Verify(System.Web.HttpContext.Current);
if(result.Status == VerifyResultStatus.Success) { // کاربر، صورتحساب را پرداخت کرده است و شما میتوانید ادامه عملیات خرید را انجام دهید } else { // کاربر بنا به دلایلی صورتحساب را پرداخت نکرده است // شما همچنین میتوانید علت را در قالب یک پیام از پراپرتی پیام مشاهده کنید // بنابراین شما میتوانید این صورتحساب را در پایگاه داده خود مردود اعلام کنید }
مردود کردن صورتحساب قبل از انتقال وجه از مشتری به فروشنده
همانطور که در تصویر میبینید، در هنگام بازگشت مشتری به وب سایت شما و تأیید کردن صورتحساب، شما میتوانید اطلاعات تراکنش مورد نظر را که شامل، درگاه پرداخت بانکی، شماره سفارش و شماره رجوع است را دریافت کنید و سپس با استفاده از این اطلاعات، پایگاه داده خود را بررسی کرده و در صورت لزوم، متد Cancel را فراخوانی کنید. به این ترتیب به درگاه بانکی، هیچگونه تأییدیه ای اعلام نمیشود و این بدان معناست که اگر وجهی به حساب فروشگاه واریز شده باشد، پس از چند دقیقه (معمولا ۱۵ دقیقه) به حساب مشتری برگشت داده خواهد شد.
برگشت وجه به حساب مشتری پس از تأیید صورتحساب
var refundResult = Payment.Refund(new RefundInvoice([Order Number], [Amount]));
درگاه مجازی پرداخت
var result = Payment.Request(Gateways.ParbadVirtualGateway, invoice);
در نتیجه در هنگام هدایت کاربر به درگاه پرداخت، کاربر به درگاه مجازی هدایت خواهد شد.
<system.webServer> <handlers> <add name="ParbadGatewayPage" verb="*" path="Parbad.axd" type="Parbad.Web.Gateway.ParbadVirtualGatewayHandler" /> </handlers> </system.webServer>
ParbadConfiguration.Gateways.ConfigureParbadVirtualGateway(new ParbadVirtualGatewayConfiguration("Parbad.axd"));
تنظیمات پَرباد
public class Global : HttpApplication { void Application_Start(object sender, EventArgs e) { // configurations } }
public class Startup { public void Configuration(IAppBuilder app) { // configurations } }
تنظیمات درگاههای پرداخت
تنظیمات ذخیره سازی اطلاعات پرداخت
ParbadConfiguration.Storage = new SqlServerStorage("Connection String", "MyPaymentTableName");
public class MyStorage : Storage { // Implement methods here... }
ParbadConfiguration.Storage = new MyStorage();
در صورتیکه هر گونه پیشنهاد یا انتقاد نسبت به کارکرد این سیستم دارید، صمیمانه منتظر شنیدن آن در راستای توسعه این سیستم هستم.همچنین در صورت تمایل به توسعه آن، میتوانید آن را در گیت هاب دنبال کنید و یا لطفا پیشنهادات، بحثها و نظرات خود را در صفحه مخصوص این پروژه ارسال کنید.با تشکر.
// webpack.config.js file module.exports = { entry:'./main.js' ,output:{ filename:'bundle.js' } }
//for when webpack is installed globally webpack --watch //for when webpack is installed locally in project npm run webpack -- --watch
//webpack.config.js file module.exports = { entry:'./main.js' ,output:{ filename:'bundle.js' } ,watch :true }
// to install globally : npm install -g webpack-dev-server //to install locally in project : npm install -D webpack-dev-server
//package.json file { "name": "dntwebpack", "version": "1.0.0", "description": "", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "webpack": "webpack", "webpackserver": "webpack-dev-server" }, "author": "mehdi", "license": "ISC", "devDependencies": { "webpack": "^1.13.1", "webpack-dev-server": "^1.14.1" } }
npm run webpackserver
// user.js file function userLog() { console.log("ahooy from user module file"); } module.exports={ userLog:userLog }
//main.js file var user = require("./user"); user.userLog(); console.log(`i'm bundled by webpack`);
// shared.js file console.log('log message from shared module !');
//webpack.config.js file module.exports = { entry:['./shared.js','./main.js'] ,output:{ filename:'bundle.js' } ,watch :true }
npm install -D ts-loader
// main.ts file let user = require("./user"); user.userLog(); let mainlogger = () => { console.log(`i'm bundled by webpack in an arrow function`); } mainlogger();
//webpack.config.js module.exports = { entry:['./shared.js','./main.js'] ,output:{ filename:'bundle.js' } ,watch :true ,module:{ loaders:[ { test:/\.ts$/ ,exclude:/node_modules/ ,loader:'ts-loader' } ] } }
CoffeeScript #11
کامپایل خودکار CoffeeScript
همانطور که گفته شده CoffeeScript یک لایه میان شما و جاوااسکریپت است و هر زمان که فایل CoffeeScript تغییر کرد، باید به صورت دستی آن را کامپایل کرد. خوشبختانه CoffeeScript روشهای دیگری را برای کامپایل کردن دارد که به وسیله آن میتوان چرخهی توسعه را بسیار سادهتر نمود.
در قسمت اول گفته شد، برای کامپایل فایل CoffeeScript با استفاده از coffee به صورت زیر عمل میکردیم:
coffee --compile --output lib src
حال به کامپایل خودکار CoffeeScript توجه کنید.
Cake
Cake یک سیستم فوق العاده ساده برای کامپایل خودکار است که مانند Make و Rake عمل میکند. این کتابخانه همراه پکیج coffee-script npm نصب میشود و برای استفاده با فراخوانی cake اجرا میشود.
برای ایجاد فایل tasks در cake که Cakefile نامیده میشود، میتوان از خود CoffeeScript استفاده کرد. برای اجرای cake با استفاده از دستور [cake [task] [options میتوان عمل کرد. برای اطلاع از لیست امکانات cake کافی است دستور cake را به تنهایی اجرا کنید.
وظایف را میتوان با استفاده از تابع task، با ارسال نام و توضیحات (اختیاری) و تابع callback، تعریف کرد. به مثال زیر توجه کنید:
fs = require 'fs' {print} = require 'sys' {spawn} = require 'child_process' build = (callback) -> coffee = spawn 'coffee', ['-c', '-o', 'lib', 'src'] coffee.stderr.on 'data', (data) -> process.stderr.write data.toString() coffee.stdout.on 'data', (data) -> print data.toString() coffee.on 'exit', (code) -> callback?() if code is 0 task 'build', 'Build lib/ from src/', -> build()
همان طور که مشاهده میکنید پس از تغییر در فایل CoffeeScript باید به صورت دستی cake build را فراخوانی کنیم که این دور از حالت ایده آل است.
خوشبختانه دستور coffee پارامتر دیگری به نام watch-- دارد که به وسیله آن میتوان تمامی تغییرات یک پوشه را زیر نظر گرفت و در صورت نیاز دوباره کامپایل انجام شود. به مثال زیر توجه کنید:
task 'watch', 'Watch src/ for changes', -> coffee = spawn 'coffee', ['-w', '-c', '-o', 'lib', 'src'] coffee.stderr.on 'data', (data) -> process.stderr.write data.toString() coffee.stdout.on 'data', (data) -> print data.toString()
task 'open', 'Open index.html', -> # First open, then watch spawn 'open', 'index.html' invoke 'watch'
option '-o', '--output [DIR]', 'output dir' task 'build', 'Build lib/ from src/', -> # Now we have access to a `options` object coffee = spawn 'coffee', ['-c', '-o', options.output or 'lib', 'src'] coffee.stderr.on 'data', (data) -> process.stderr.write data.toString() coffee.stdout.on 'data', (data) -> print data.toString()
Cake یک روش عالی برای انجام وظایف معمول به صورت خودکار است، مانند کامپایل فایلهای CoffeeScript است. همچنین برای آشنایی بیشتر میتوانید به سورس cake نگاهی کنید.
میانافزار چندسکویی فشرده سازی صفحات در ASP.NET Core
پیشتر مطلب «استفاده از GZip توکار IISهای جدید و تنظیمات مرتبط با آنها» را در سایت جاری مطالعه کردهاید. این قابلیت صرفا وابستهاست به IIS و همچنین در صورت نصب بودن ماژول httpCompression آن کار میکند. بنابراین قابلیت انتقال به سایر سیستم عاملها را نخواهد داشت و هرچند تنظیمات فایل web.config آن هنوز هم در برنامههای ASP.NET Core معتبر هستند، اما چندسکویی نیستند. برای رفع این مشکل، تیم ASP.NET Core، میانافزار توکاری را برای فشرده سازی صفحات ارائه دادهاست که جزئی از تازههای ASP.NET Core 1.1 نیز بهشمار میرود.
برای نصب آن دستور ذیل را در کنسول پاورشل نیوگت، اجرا کنید:
PM> Install-Package Microsoft.AspNetCore.ResponseCompression
{ "dependencies": { "Microsoft.AspNetCore.ResponseCompression": "1.0.0" } }
مرحلهی بعد، افزودن سرویسهای و میان افزار مرتبط، به کلاس آغازین برنامه هستند. همیشه متدهای Add کار ثبت سرویسهای میانافزار را انجام میدهند و متدهای Use کار افزودن خود میانافزار را به مجموعهی موجود تکمیل میکنند.
public void ConfigureServices(IServiceCollection services) { services.AddResponseCompression(options => { options.MimeTypes = Microsoft.AspNetCore.ResponseCompression.ResponseCompressionDefaults.MimeTypes; }); }
namespace Microsoft.AspNetCore.ResponseCompression { /// <summary> /// Defaults for the ResponseCompressionMiddleware /// </summary> public class ResponseCompressionDefaults { /// <summary> /// Default MIME types to compress responses for. /// </summary> // This list is not intended to be exhaustive, it's a baseline for the 90% case. public static readonly IEnumerable<string> MimeTypes = new[] { // General "text/plain", // Static files "text/css", "application/javascript", // MVC "text/html", "application/xml", "text/xml", "application/json", "text/json", }; } }
services.AddResponseCompression(options => { options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "image/svg+xml", "application/font-woff2" }); });
به علاوه options ذکر شدهی در اینجا دارای خاصیت options.Providers نیز میباشد که نوع و الگوریتم فشرده سازی را مشخص میکند. در صورتیکه مقدار دهی نشود، مقدار پیش فرض آن Gzip خواهد بود:
services.AddResponseCompression(options => { //If no compression providers are specified then GZip is used by default. //options.Providers.Add<GzipCompressionProvider>();
همچنین اگر علاقمند بودید تا میزان فشرده سازی تامین کنندهی Gzip را تغییر دهید، نحوهی تنظیمات آن به صورت ذیل است:
services.Configure<GzipCompressionProviderOptions>(options => { options.Level = System.IO.Compression.CompressionLevel.Optimal; });
به صورت پیشفرض، فشرده سازی صفحات Https انجام نمیشود. برای فعال سازی آن تنظیم ذیل را نیز باید قید کرد:
options.EnableForHttps = true;
مرحلهی آخر این تنظیمات، افزودن میان افزار فشرده سازی خروجی به لیست میان افزارهای موجود است:
public void Configure(IApplicationBuilder app) { app.UseResponseCompression() // Adds the response compression to the request pipeline .UseStaticFiles(); // Adds the static middleware to the request pipeline }
تنظیمات کش کردن چندسکویی فایلهای ایستا در ASP.NET Core
تنظیمات کش کردن فایلهای ایستا در web.config مخصوص IIS به صورت ذیل است :
<staticContent> <clientCache httpExpires="Sun, 29 Mar 2020 00:00:00 GMT" cacheControlMode="UseExpires" /> </staticContent>
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { app.UseResponseCompression() .UseStaticFiles( new StaticFileOptions { OnPrepareResponse = _ => _.Context.Response.Headers[HeaderNames.CacheControl] = "public,max-age=604800" // A week in seconds }) .UseMvc(routes => routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}")); }
معادل چندسکویی ماژول URL Rewrite در ASP.NET Core
مثالهایی از ماژول URL Rewrite را در مباحث بهینه سازی سایت برای بهبود SEO پیشتر بررسی کردهایم (^ و ^ و ^). این ماژول نیز همچنان در ASP.NET Core هاست شدهی در ویندوز و IIS قابل استفاده است (البته به شرطی که ماژول مخصوص آن در IIS نصب و فعال شده باشد). معادل چندسکویی این ماژول به صورت یک میانافزار توکار به ASP.NET Core 1.1 اضافه شدهاست.
برای استفادهی از آن، ابتدا نیاز است بستهی نیوگت آنرا به نحو ذیل نصب کرد:
PM> Install-Package Microsoft.AspNetCore.Rewrite
{ "dependencies": { "Microsoft.AspNetCore.Rewrite": "1.0.0" } }
پس از نصب آن، نمونهای از نحوهی تعریف و استفادهی آن در کلاس آغازین برنامه به صورت ذیل خواهد بود:
public void Configure(IApplicationBuilder app) { app.UseRewriter(new RewriteOptions() .AddRedirectToHttps() .AddRewrite(@"app/(\d+)", "app?id=$1", skipRemainingRules: false) // Rewrite based on a Regular expression //.AddRedirectToHttps(302, 5001) // Redirect to a different port and use HTTPS .AddRedirect("(.*)/$", "$1") // remove trailing slash, Redirect using a regular expression .AddRedirect(@"^section1/(.*)", "new/$1", (int)HttpStatusCode.Redirect) .AddRedirect(@"^section2/(\\d+)/(.*)", "new/$1/$2", (int)HttpStatusCode.MovedPermanently) .AddRewrite("^feed$", "/?format=rss", skipRemainingRules: false));
در اینجا مثالهایی را از اجبار به استفادهی از HTTPS، تا حذف / از انتهای مسیرهای وب سایت و یا هدایت آدرس قدیمی فید سایت، به آدرسی جدید واقع در مسیر format=rss، توسط عبارات باقاعده مشاهده میکنید.
در این تنظیمات اگر پارامتر skipRemainingRules به true تنظیم شود، به محض برآورده شدن شرط انطباق مسیر (پارامتر اول ذکر شده)، بازنویسی مسیر بر اساس پارامتر دوم، صورت گرفته و دیگر شرطهای ذکر شده، پردازش نخواهند شد.
این میانافزار قابلیت دریافت تعاریف خود را از فایلهای web.config و یا htaccess (لینوکسی) نیز دارد:
app.UseRewriter(new RewriteOptions() .AddIISUrlRewrite(env.ContentRootFileProvider, "web.config") .AddApacheModRewrite(env.ContentRootFileProvider, ".htaccess"));
و یا اگر خواستید منطق پیچیدهتری را نسبت به عبارات باقاعده اعمال کنید، میتوان یک IRule سفارشی را نیز به نحو ذیل تدارک دید:
public class RedirectWwwRule : Microsoft.AspNetCore.Rewrite.IRule { public int StatusCode { get; } = (int)HttpStatusCode.MovedPermanently; public bool ExcludeLocalhost { get; set; } = true; public void ApplyRule(RewriteContext context) { var request = context.HttpContext.Request; var host = request.Host; if (host.Host.StartsWith("www", StringComparison.OrdinalIgnoreCase)) { context.Result = RuleResult.ContinueRules; return; } if (ExcludeLocalhost && string.Equals(host.Host, "localhost", StringComparison.OrdinalIgnoreCase)) { context.Result = RuleResult.ContinueRules; return; } string newPath = request.Scheme + "://www." + host.Value + request.PathBase + request.Path + request.QueryString; var response = context.HttpContext.Response; response.StatusCode = StatusCode; response.Headers[HeaderNames.Location] = newPath; context.Result = RuleResult.EndResponse; // Do not continue processing the request } }
و سپس میتوان آنرا به عنوان یک گزینهی جدید Rewriter معرفی نمود:
app.UseRewriter(new RewriteOptions().Add(new RedirectWwwRule()));
یک نکته: در اینجا در صورت نیاز میتوان از تزریق وابستگیهای در سازندهی کلاس Rule جدید تعریف شده نیز استفاده کرد. برای اینکار باید RedirectWwwRule را به لیست سرویسهای متد ConfigureServices معرفی کرد و سپس نحوهی دریافت وهلهای از آن جهت معرفی به میانافزار بازنویسی مسیرهای وب به صورت ذیل درخواهد آمد:
var options = new RewriteOptions().Add(app.ApplicationServices.GetService<RedirectWwwRule>());
آشنایی با NHibernate - قسمت سوم
سورس کامل قابل دریافت این موارد در پایان قسمت چهارم ارائه شده. به آن مراجعه کنید.
کتابخانه سورس باز Stateless، برای طراحی و پیاده سازی «ماشینهای حالت گردش کاری مانند» تهیه شده و مزایای زیر را نسبت به Windows workflow foundation دارا است:
- جمعا 30 کیلوبایت است!
- تمام اجزای آن سورس باز است.
- دارای API روان و سادهای است.
- امکان تبدیل UML state diagrams، به نمونه معادل Stateless بسیار ساده و سریع است.
- به دلیل code first بودن، کار کردن با آن برای برنامه نویسها سادهتر بوده و افزودن یا تغییر اجزای آن با کدنویسی به سادگی میسر است.
دریافت کتابخانه Stateless از Google code و یا از NuGet
پیاده سازی مثال کلید برق با Stateless
در ادامه همان مثال ساده کلید برق قسمت قبل را با Stateless پیاده سازی خواهیم کرد:
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); } } } }
امضای کلاس StateMachine را در ذیل مشاهده میکنید؛ جهت توضیح آرگومانهای جنریک string و char معرفی شده در مثال:
public class StateMachine<TState, TTrigger>
برای مثال در اینجا حالات روشن و خاموش، با رشتههای on و off مشخص شدهاند و رویداد قابل قبول دریافتی، کاراکتر فاصله است.
سپس نیاز است این ماشین حالت را برای معرفی رویدادهایی (trigger در اینجا) که سبب تغییر حالت آن میشوند، تنظیم کنیم. اینکار توسط متدهای Configure و Permit انجام خواهد شد. متد Configure، یکی از حالات از پیش تعیین شده را جهت تنظیم، مشخص میکند و سپس در متد Permit تعیین خواهیم کرد که بر اساس رخدادی مشخص (برای مثال در اینجا فشرده شدن کلید space) وضعیت حالت جاری، به وضعیت جدیدی (destinationState) منتقل شود.
نهایتا این ماشین حالت در یک حلقه بینهایت مشغول به کار خواهد شد. برای نمونه یک Thread پس زمینه (BackgroundWorker) نیز میتواند همین کار را در برنامههای ویندوزی انجام دهد.
یک نکته
علاوه بر روشهای یاد شدهی تشخیص الگوی ماشین حالت که در قسمت قبل بررسی شدند، مورد refactoring انبوهی از if و elseها و یا switchهای بسیار طولانی را نیز میتوان به این لیست افزود.
استفاده از Stateless Designer برای تولید کدهای ماشین حالت
کتابخانه Stateless دارای یک طراح و Code generator بصری سورس باز است که آنرا به شکل افزونهای برای VS.NET میتوانید در سایت Codeplex دریافت کنید. این طراح از کتابخانه GLEE برای رسم گراف استفاده میکند.
کار مقدماتی با آن به نحو زیر است:
الف) فایل StatelessDesignerPackage.vsix را از سایت کدپلکس دریافت و نصب کنید. البته نگارش فعلی آن فقط با VS 2012 سازگار است.
ب) ارجاعی را به اسمبلی stateless به پروژه خود اضافه نمائید (به یک پروژه جدید یا از پیش موجود).
ج) از منوی پروژه، گزینه Add new item را انتخاب کرده و سپس در صفحه ظاهر شده، گزینه جدید Stateless state machine را انتخاب و به پروژه اضافه نمائید.
کار با این طراح، با ادیت XML آن شروع میشود. برای مثال گردش کاری ارسال و تائید یک مطلب جدید را در بلاگی فرضی، به نحو زیر وارد نمائید:
<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; } } } }
ماشین حالت فوق دارای چهار حالت شروع، در حال بررسی، منتشر شده و رد شده است. معمول است که این چهار حالت را به شکل یک enum معرفی کنند که در کدهای تولیدی فوق نیز به همین نحو عمل گردیده و public enum State معرف چهار حالت ذکر شده است. همچنین رویدادهای ذخیره، نیاز به ویرایش، ویرایش، تائید و رد نیز توسط public enum Trigger معرفی شدهاند.
در قسمت Transitions، بر اساس یک رویداد (Trigger در اینجا)، انتقال از یک حالت به حالتی دیگر را سبب خواهیم شد.
تعاریف اصلی تنظیمات ماشین حالت، در سازنده کلاس BlogPostStateMachine انجام شده است. این تعاریف نیز بسیار ساده هستند. به ازای هر حالت، یک Configure داریم. در متدهای OnEntry و OnExit هر حالت، یک سری callback function فراخوانی خواهند شد. برای مثال در حالت Rejected یا Approved میتوان ایمیلی را به ارسال کننده مطلب جهت یادآوری وضعیت رخ داده، ارسال نمود.
متدهای PermitIf سبب انتقال شرطی، به حالتی دیگر خواهند شد. برای مثال رد یا تائید یک مطلب نیاز به دسترسی مدیریتی خواهد داشت. این نوع موارد را توسط delgateهای Guard ایی که برای مدیریت شرطها ایجاد کرده است، میتوان تنظیم کرد. PermitReentryIf سبب بازگشت مجدد به همان حالت میگردد. برای مثال ویرایش و ذخیره یک مطلب در حال انتشار، سبب تائید یا رد آن نخواهد شد؛ صرفا عملیات ذخیره صورت گرفته و ماشین حالت مجددا در همان مرحله باقی خواهد ماند.
نحوه استفاده از ماشین حالت تولیدی:
همانطور که عنوان شد، حداقل استفاده از ماشینهای حالت، refactoing انبوهی از if و elseها است که در حالت مدیریت یک چنین گردشهای کاری باید تدارک دید.
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); } } }
برای به حرکت درآوردن این ماشین، نیاز به یک سری اکشن متد نیز میباشد. تعدادی از این موارد را در انتهای کلاس فوق ملاحظه میکنید. کد نویسی آنها در حد فراخوانی متد TryFireTrigger ماشین حالت است.
یک نکته:
ماشین حالت تولیدی به صورت پیش فرض در حالت State.Begin قرار دارد. میتوان این مورد را از بانک اطلاعاتی خواند و سپس مقدار دهی نمود تا با هر بار وهله سازی ماشین حالت دقیقا مشخص باشد که در چه مرحلهای قرار داریم و TryFireTrigger بتواند بر این اساس تصمیمگیری کند که آیا مجاز است عملیاتی را انجام دهد یا خیر.
در ادامه نحوه سازگار سازی این مجموعه را با ASP.NET MVC مرور خواهیم کرد:
الف) سورسهای اصلی Flash کنترل ارسال فایلها
اگر علاقمند به تغییر اطلاعاتی در فایل فلش نهایی هستید به پوشه OriginalFlashSource پروژه پیوست شده مراجعه کنید. در اینجا برای مثال یک سری از برچسبهای آن فارسی شدهاند و کامپایل مجدد.
ب) مزیت استفاده از Flash uploader
با استفاده از Flash uploader امکان انتخاب چندین فایل با هم وجود دارد. همچنین در صفحه دیالوگ انتخاب فایلها دقیقا میتوان پسوند فایلهای مورد نظر را نیز تعیین کرد. این دو مورد در حالت ارسال معمولی فایلها به سرور و استفاده از امکانات معمولی HTML وجود ندارند. به علاوه امکان نمایش درصد پیشرفت آپلود فایلها و همچنین حذف کلی لیست و حذف یک آیتم از لیست را هم درنظر بگیرید.
ج) معادل کنترل Web forms را در ASP.NET MVC به شکل زیر میتوان تهیه کرد:
@helper AddFlashUploader( string uploadUrl, string queryParameters, string flashUrl, int totalUploadSizeLimit = 0, int uploadFileSizeLimit = 0, string fileTypes = "", string fileTypeDescription = "", string onUploadComplete = "") { onUploadComplete = string.IsNullOrEmpty(onUploadComplete) ? "" : "completeFunction=" + onUploadComplete; queryParameters = Server.UrlEncode(queryParameters); fileTypes = string.IsNullOrEmpty(fileTypes) ? "" : "&fileTypes=" + Server.UrlEncode(fileTypes); fileTypeDescription = string.IsNullOrEmpty(fileTypeDescription) ? "" : "&fileTypeDescription=" + Server.UrlEncode(fileTypeDescription); var totalUploadSizeLimitData = totalUploadSizeLimit > 0 ? "&totalUploadSize=" + totalUploadSizeLimit : ""; var uploadFileSizeLimitData = uploadFileSizeLimit > 0 ? "&fileSizeLimit=" + uploadFileSizeLimit : ""; var flashVars = onUploadComplete + fileTypes + fileTypeDescription + totalUploadSizeLimitData + uploadFileSizeLimitData + "&uploadPage=" + uploadUrl + "?" + queryParameters; <object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0" width="575" height="375" id="fileUpload" align="middle"> <param name="allowScriptAccess" value="sameDomain" /> <param name="movie" value="@flashUrl" /> <param name="quality" value="high" /> <param name="wmode" value="transparent"> <param name=FlashVars value="@flashVars"> <embed src="@flashUrl" FlashVars="@flashVars" quality="high" wmode="transparent" width="575" height="375" name="fileUpload" align="middle" allowScriptAccess="sameDomain" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" /> </object> }
د) نحوه استفاده از HTML helper فوق:
@{ ViewBag.Title = "Index"; var uploadUrl = Url.Action("Uploader", "Home"); var flashUrl = Url.Content("~/Content/FlashUpload/FlashFileUpload.swf"); } <h2> Flash Uploader</h2> <div style="background: #E0EBEF;"> @FlashUploadHelper.AddFlashUploader( uploadUrl: uploadUrl, queryParameters: "User=Vahid&Id=تست", flashUrl: flashUrl, fileTypeDescription: "Images", fileTypes: "*.gif; *.png; *.jpg; *.jpeg", uploadFileSizeLimit: 0, totalUploadSizeLimit: 0, onUploadComplete: "alert('انجام شد');") </div>
using System.Collections.Generic; using System.IO; using System.Web; using System.Web.Mvc; namespace MvcFlashUpload.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } public ActionResult Uploader(string User, string Id, IEnumerable<HttpPostedFileBase> FileData) { var queryParameter1 = User; var queryParameter2 = Id; // ... foreach (var file in FileData) { if (file.ContentLength > 0) { var fileName = Path.GetFileName(file.FileName); var path = Path.Combine(Server.MapPath("~/App_Data/Uploads"), fileName); file.SaveAs(path); } } return Content(" "); } } }
توضیحات:
در اینجا uploadUrl، مسیر اکشن متدی است که قرار است اطلاعات فایلها را دریافت کند. queryParameters اختیاری است. اگر تعریف شود تعدادی کوئری استرینگ دلخواه را میتواند به متد Uploader ارسال کند. برای نمونه در اینجا User و Id ارسال شدهاند یا هر نوع کوئری استرینگ دیگری که مدنظر است.
flashUrl مسیر فایل SWF را مشخص میکند. در اینجا فایل FlashFileUpload.swfدر پوشه Content/FlashUpload قرار گرفته است.
fileTypeDescription برچسبی است که نوع فایلهای قابل انتخاب را به کاربر نمایش میدهد و fileTypes نوعهای مجاز قابل ارسال را دقیقا مشخص میکند.
پارامترهای uploadFileSizeLimit و totalUploadSizeLimit در صورتیکه مساوی صفر وارد شوند، به معنای عدم محدودیت اندازه در فایلها و جمع حجم ارسالی در هر بار است.
استفاده از پارامتر onUploadComplete اختیاری است. در اینجا میتوان پس از پایان عملیات از طریق جاوا اسکریپت عملیاتی را انجام داد. برای مثال اگر خواستید کاربر را به صفحه خاصی هدایت کنید، window.locationرا مقدار دهی نمائید.
در متد Uploader کنترلر فوق، پارامترهای User و id اختیاری بوده و بر اساس queryParameters متد FlashUploadHelper.AddFlashUploader مشخص میشوند. اما نام FileData نباید تغییری کند؛ از این لحاظ که دقیقا همین نام در فایل فلش، مورد استفاده قرار گرفته است.
در اکشن متد دریافت فایلها، لیستی از فایلهای ارسالی به سرور دریافت شده و سپس بر این اساس میتوان آنها را در مکانی مشخص ذخیره نمود.
دریافت پروژه
MvcFlashUploader.zip
فرق بین TFS ،SVN و GIT در چیست؟
اما کسانیکه مثلا با SVN کار میکنند یا Git، مابقی کارها رو توسط مثلا Jira که با اینها به خوبی یکپارچه میشود مدیریت میکنند. البته Jira فقط یک نمونه است. عدهای دیگر از ترکیب SVN، TeamCity و YouTrack استفاده میکنند و الی آخر. در این حالت دست بازتر است برای انتخاب.
<compilation debug="true" targetFramework="4.5" />