نظرات مطالب
پلاگین DataTables کتابخانه jQuery - قسمت سوم
با سلام  و عرض ادب
زمانیکه من با Ajax , Jquery   سطرهای دیتای جدول مورد نظر برای DataTable  شدن را از سمت سرور ایجاد  می‌کنم متاسفانه بار اول دیتا‌ها رو نشون میده  ولی Search  نمیکنه و صفحه بندی هم نمیکنه و ... در ضمن کدهای مربوطه رو هم می‌گذارم . لطفا راهنمایی کنید که اگه خواستیم دیتا‌ها را از سمت سرور بیاریم و کار بده باید چه کار کرد؟  مرسی
   $(document).ready(function () {
                dataparam2 = "cmd=FillScope";
                $.ajax({
                    url: "Default2.aspx",
                    type: "POST",
                    data: dataparam2,
                    async: true,
                    success: function (msg) {
                        if (msg != '') {
                            var data = eval("(" + msg + ")");
                            $("#tbodytblMain").html('');
                            for (var i = 0; i < data.length; i++) {
                                $("#tbodytblMain").append(
                                         "<tr class='odd gradeX'>"
                                         + "<td style='width:200px'>" + data[i].T + "</td>"
                                         + "<td style='width:150px'>" + data[i].P + "</td>"
                                          + "<td>" + data[i].S + "</td>"
                                         + "<td>" + data[i].TP + "</td>"
                                         + "<td>" + data[i].Sp + "</td>" + "</tr>");

                            }
                        }
                    },
                    error: function (msg) {

                    }
                });



                $('#tblMain').dataTable();
            });
:کد سمت سرور
    if (Request["cmd"] == "FillScope")
        {
            string Val = "برخوار";
            JavaScriptSerializer js = new JavaScriptSerializer();
            string serText = "";
            MUIDataClassesDataContext db = new MUIDataClassesDataContext();
            var LST = (from x in db.tblProjectInfos
                       where x.tblScope.xScopeName.Contains(Val)
                       orderby x.tblScope.xScopeName
                       select new
                       {

                           P = x.xPlace,
                           S = x.tblScope.xScopeName,
                           TP = x.tblProjectType.xProjectTypeName,
                           Sp = x.tblStatus.xStatusName
                       });
            serText = js.Serialize(LST);
            Response.Write(serText);
            Response.End();

        }


مطالب
الگوی استخر اشیاء Object Pool Pattern
الگوی استخر اشیاء، جزو الگوهای تکوینی است و کار آن جلوگیری از ایجاد اشیاء تکراری و محافظت از به هدر رفتن حافظه است. نحوه کار این الگو بدین شکل است که وقتی کاربر درخواست نمونه‌ای از یک شیء را می‌دهد، بعد از اتمام کار، شیء نابود نمی‌شود؛ بلکه به استخر بازگشت داده می‌شود تا در درخواست‌های آینده، مجددا مورد استفاده قرار گیرد. این کار موجب عدم هدر رفتن حافظه و همچنین بالا رفتن کارآیی برنامه می‌گردد. این الگو به خصوص برای اشیایی که مدت کوتاهی مورد استفاده قرار میگیرند و آماده سازی آن‌ها هزینه بر است بسیار کارآمد می‌باشد. از آنجا که هزینه‌ی ساخت و نابود سازی در حد بالایی قرار دارد و آن شیء به طور مکرر مورد استفاده قرار می‌گیرد، زمان زیادی صرفه جویی می‌شود.

در این الگو ابتدا شیء از استخر درخواست می‌شود. اگر قبلا ایجاد شده باشد، مجددا مورد استفاده قرار می‌گیرد. در غیر این صورت نمونه جدیدی ایجاد می‌شود و وقتی که کار آن پایان یافت به استخر بازگشت داده شده و نگهداری می‌شود، تا زمانی که مجددا درخواست استفاده از آن برسد.
 یکی از نکات مهم و کلیدی این است که وقتی کار شیء مربوطه تمام شد، باید اطلاعات شیء قبلی و داده‌های آن نیز پاکسازی شوند؛ در غیر این صورت احتمال آسیب و خطا در برنامه بالا می‌رود و این الگو را به یک ضد الگو تبدیل خواهد کرد.
نمونه‌ای از پیاده سازی این الگو را در دات نت، می‌توانید در data provider‌های مخصوص SQL Server نیز ببینید. از آنجا که ایجاد اتصال به دیتابیس، هزینه بر بوده و دستورات در کوتاه‌ترین زمان ممکن اجرا می‌شوند، این کانکشن‌ها یا اتصالات بعد از بسته شدن از بین نمی‌روند، بلکه در استخر مانده تا زمانیکه مجددا نمونه مشابهی از آن‌ها درخواست شود تا دوباره مورد استفاده قرار بگیرند تا از هزینه اولیه برای ایجاد آن‌ها پرهیز شود. استفاده از این الگو در برنامه نویسی با سوکت ها، تردها و کار با اشیاء گرافیکی بزرگ مثل فونت‌ها و تصاویر Bitmap کمک شایانی می‌کند. ولی در عوض برای اشیای ساده می‌تواند کارآیی را به شدت کاهش دهد.

الگوی بالا را در سی شارپ بررسی می‌کنیم:
ابتدا کلاس PooledObject را به عنوان یک شیء بزرگ و پر استفاده ایجاد می‌کنیم:
 public class PooledObject
    {
        DateTime _createdAt = DateTime.Now;

        public DateTime CreatedAt
        {
            get { return _createdAt; }
        }

        public string TempData { get; set; }

        public void DoSomething(string name)
        {
            Console.WriteLine($"{name} : {TempData} is written on {CreatedAt}");
        }
    }
tempdata ویژگی است که باید برای هر شیء، مجزا باشد. پس باید دقت داشت برای استفاده‌های بعدی نیاز است پاک سازی شود.
بعد از آن کلاس پول را پیاده سازی می‌کنیم:
public static class Pool
{
    private static List<PooledObject> _available = new List<PooledObject>();
    private static List<PooledObject> _inUse = new List<PooledObject>();
 
    public static PooledObject GetObject()
    {
        lock(_available)
        {
            if (_available.Count != 0)
            {
                PooledObject po = _available[0];
                _inUse.Add(po);
                _available.RemoveAt(0);
                return po;
            }
            else
            {
                PooledObject po = new PooledObject();
                _inUse.Add(po);
                return po;
            }
        }
    }
 
    public static void ReleaseObject(PooledObject po)
    {
        CleanUp(po);
 
        lock (_available)
        {
            _available.Add(po);
            _inUse.Remove(po);
        }
    }
 
    private static void CleanUp(PooledObject po)
    {
        po.TempData = null;
    }
}
در کلاس بالا، کاربر با متد GetObject، درخواست شی‌ءایی را می‌کند و متد نگاه می‌کند که اگر لیست موجودی، پر باشد، اولین نمونه‌ی در دسترس را ارسال می‌کند. ولی در صورتیکه نمونه‌ای از آن نباشد، یک نمونه‌ی جدید را ایجاد کرده و آن را در لیست مورد استفاده‌ها قرار می‌دهد و به کاربر باز می‌گرداند.
متد Release Object وظیفه‌ی بازگرداندن شیء را به استخر، دارد که از لیست در حال استفاده‌ها آن را حذف کرده و به لیست موجودی اضافه می‌کند. متد Cleanup در این بین وظیفه ریست کردن شیء را دارد تا مشکلی که در بالا بیان کردیم رخ ندهد.
کد زیر را اجرا می‌کنیم:
            var obj1 = Pool.GetObject();
            obj1.DoSomething("obj1");

            Thread.Sleep(2000);
            var obj2 = Pool.GetObject();
            obj2.DoSomething("obj2");
            Pool.ReleaseObject(obj1);

            Thread.Sleep(2000);
            var obj3 = Pool.GetObject();
            obj3.DoSomething("obj3");
نتیجه به شکل زیر است:
obj1 :  is written on 4/21/2016 11:25:26 AM
obj2 :  is written on 4/21/2016 11:25:28 AM
obj3 :  is written on 4/21/2016 11:25:26 AM
ابتدا شیء obj1 ایجاد می‌شود و سپس obj2 و بعد از آن obj1 به لیست موجودی‌ها بازگشته و برای obj3 همان شیء را بازگشت می‌هد که برای obj1 ساخته بود. توقف تردها در این مثال برای مشاهده‌ی بهتر زمان است.
مطالب
طراحی گزارش در Stimulsoft Reports.Net – بخش 1

برای طراحی گزارش شما میتوانید به سه روش این کار را انجام دهید.

1- طراحی در برنامه طراح گزارش

2- طراحی از داخل ویژوال استودیو

3- طراحی گزارش در زمان اجرا

برای شروع شما میتوانید نسخه آزمایشی این گزارش‌ساز را دریافت کنید. تنها محدودیت این نسخه نمایش عبارت Demo در چاپ میباشد.

برنامه Designer  را اجرا کنید. در صورتی که برای اولین بار است این برنامه را اجرا میکنید ابتدا باید رابط کاربری خود را انتخاب نمایید. نوار ابزار سمت چپ تمامی ابزارهای پر‌کاربرد طراحی گزارش را در اختیارتان قرار میدهد. ابزارهایی که در این بخش درباره آنها توضیح داده خواهد شد عبارتند از:

Header, Footer, Data, Page Header, Page Footer, Report Title, Report Summery

*به ابزارهای بالا Band گفته میشود.

Header , Footer :

همانطور که از نامشان پیداست در قسمت بالا و پایین بخشی از گزارش قرار میگیرند که برای استفاده در بالا و پایین بند Data میباشد. به عنوان مثال بند Header مناسب طراحی سرستونهای یک جدول میباشد و بند Footer  هم جهت نمایش اطلاعات انتهایی یک جدول. ولی شما میتوانید با تنظیم خصوصیات هر بند رفتار و نمایش آنها را به طور کل تغییر دهید. نکته مثبت این گزارش‌ساز این است که شما میتوانید بیش از یک واحد از هر بند را بر روی صفحه طراح خود قرار دهید، به عنوان مثال شما میتوانید دو بند Header داشته باشید که یکی در صفحات زوج و دیگری در صفحات فرد نمایش داده شود.

Data :

این بند جهت نمایش اطلاعات از منبع داده‌ها میباشد. به این معنا که به ازای هر سطر از داده‌ها یک بار این بخش نمایش داده میشود. تعداد دفعات نمایش این بند محدود به تعداد سطرهای منبع داده و یا اندازه صفحه و همچنین خصوصیت محدوده نمایش سطرها در یک صفحه میباشد.

Page Header , Page Footer :

این دو بند با توجه به نامشان جهت نمایش در بالا و پایین هر صفحه از گزارش میباشد. البته باز هم یادآور میشوم که با تغییر در خصوصیات‌شان میتوانید رفتار و نحوه نمایش آنها را تغییر دهید.

Report Title :

این بند فقط در ابتدای گزارش نمایش داده خواهد شد.

Report Summery :

این بند بلافاصله بعد از اتمام گزارش نمایش داده خواهد شد.

مثال :

برای شروع در Designer یک گزارش جدید از نوع Blank Report ایجاد نمایید. سپس در پنل Dictionary بر روی New Item کلیک کرده و گزینه XML Data را انتخاب نمایید. با توجه به محل نصب گزارش‌ساز وارد مسیر …\Bin\Data شده و فایلهای Demo.xsd و Demo.xml را برای قسمتهای مربوطه انتخاب نمایید. یک بار دیگر بر رو New Item کلیک کرده و گزینه New Data Source را انتخاب نمایید، از لیست ظاهر شده کانکشنی را که ایجاد کرده‌اید را انتخاب نمایید؛ نتیجه کار تا اینجا باید به صورت زبر باشد.

جدول Product را دراگ کرده و بر روی صفحه طراحی گزارش رها کنید. فرم Data ظاهر میشود این فرم را مطابق تصویر زیر تنظیم نمایید.

حال بر روی صفحه طراحی گزارش بندهای Header, Data, Footer مشاهده میشود؛ حال شما میتوانید با کلیک بر روی سربرگ Preview خروجی گزارش را ببینید.

توابع :

این گزارش‌ساز دارای توابع بسیاری است که اکثر نیازهای شما را برطرف می‌کند به عنوان مثال تابع تبدیل عدد به حروف به زبان فارسی. همچنین شما میتوانید توابع خاص خود را ساخته و به صورت رفرنس به گزارش اضافه نمایید.

در این بخش ما از توابع موجود در گزارش استفاده خواهیم کرد. برای شروع بر روی کامپوننت Text در بند Footer  زیر ستون UnitPrice دابل کلیک کرده تا فرم TextEditor ظاهر شود. سربرگ Summery را انتخاب نمایید. مطابق اطلاعات زیر بخشها را تنظیم نمایید.

Summery Function: Sum

Data Band: DataProducts

Data Column: Products.UnitPrice

حال بر روی سربرگ Preview کلیک نمایید تا خروجی گزارش را ببینید. جمع ستون UnitPrice فقط در صفحه آخر نمایش داده خواهد شد. اگر بخواهید جمع ستون در پایین هر صفحه نمایش داده شود ابتدا باید خصوصیت Print on All Pages بند Footer به True ست شود. سپس بر روی کامپوننت Text در بند Footer، دابل کلیک نمایید و در فرم TextEditor سربرگ Summery تیک Running Total را به حالت انتخاب شده در بیاورید، حال خروجی گزارش را ببینید، جمع در انتهای هر صفحه ظاهر میشود.

متغیرها :

در این گزارش ساز دو نوع متغیر وجود دارد؛ نرمال و سیستمی. نوع سیستمی شامل متغیرهایی میشود که کاربرد مشخصی در تهیه گزارش دارند، مثل شماره صفحه، شماره ردیف، عنوان گزارش و ...

برای مثال شما میتوانید متغیر سیستمی Line را برای روی صفحه طراحی دراگ کنید. دو کامپوننت Text بر روی صفحه ایجاد میشود. اولی با محتوای Line و دومی با محتوای {Line}. اولی را به بند Header و دومی را به بند Data منتقل کنید و سپس خروجی گزارش را مشاهده نمایید، حال گزارش شما دارای شماره ردیف است.

متغیرهای نرمال تقریبا همانند متغیرهایی هستند که همه روزه شما در برنامه‌های خود از آنها استفاده می‌کنید. با کلیک بر روی New Item گزینه New Variable را انتخاب نمایید و نوع متغیر را Decimal انتخاب نمایید، سپس متغیر ایجاد شده را دراگ کرده و بروی صفحه طراحی قرار دهید و مشابه متغیر Line عمل کرده و کامپوننتهای Text را در بندهای مناسب قرار دهید. سپس بند Data بر روی صفحه طراحی را انتخاب نمایید، در پنل Properties بر روی Eventes کلیک کرده سپس در رویداد Rendering کد زیر را وارد نمایید.

Variable1 += Products.UnitPrice

حال در خروجی گزارش میتوانید مقادیر محاسبه شده را ببینید. توجه داشته باشید که شما میتوانید در رویدادهای این گزارش‌ساز به زبان VB و C# برنامه نویسی کنید و محدود به یک خط کد نمی‌باشید.

شما میتوانید گزارش ساخته شده را به صورتهای مختلف ذخیره کنید از جمله کد C# و یا یک اپلیکیشن قابل اجرا. 

Report.mrt 

مطالب
Blazor 5x - قسمت 30 - برنامه‌ی Blazor WASM - افزودن پرداخت آنلاین توسط درگاه مجازی پرباد
در ادامه‌ی تمرین قسمت قبل که مقدمات ثبت درخواست رزرو یک اتاق را فراهم کردیم، اکنون می‌خواهیم اگر کاربری بر روی دکمه‌ی checkout now یک اتاق کلیک کرد، به درگاه مجازی پرباد منتقل شده، پرداخت را تکمیل کند، به برنامه هدایت شود و در آخر درخواست او در سیستم ثبت گردد. مزیت کار کردن با درگاه مجازی پرباد، امکان آزمایش محلی برنامه، بدون نیاز به یک درگاه بانکی واقعی است و زمانیکه قرار است با یک درگاه بانکی واقعی کار شود، فقط قسمت معرفی و تنظیمات ابتدایی مشخصات درگاه بانکی آن باید تغییر کند و نه هیچ قسمت دیگری از کدهای برنامه.


نصب پرباد و انجام تنظیمات اولیه‌ی آن

بسته‌های نیوگت پرباد را در دو پروژه‌ی زیر نصب خواهیم کرد:
الف) پروژه‌ی Web API (و یا همان BlazorWasm.WebApi در مثال این سری):
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Parbad.AspNetCore" Version="1.1.0" />
    <PackageReference Include="Parbad.Storage.EntityFrameworkCore" Version="1.2.0" />
  </ItemGroup>
</Project>
که شامل بسته‌ها‌ی ASP.NET Core آن و همچنین محل ذخیره سازی مبتنی بر EF-Core آن است.

ب) پروژه‌ای که محل قرارگیری فایل‌های Migration است (و یا همان BlazorServer.DataAccess) در این مثال:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Parbad.Storage.EntityFrameworkCore" Version="1.2.0" />
  </ItemGroup>
</Project>
که در اینجا فقط نیاز به بسته‌ی EF-Core آن است تا بتوان Context مخصوص پرباد را در حین اعمال مهاجرت‌ها شناسایی کرد.

پس از نصب این بسته‌ها، به کلاس آغازین پروژه‌ی Web API مراجعه کرده و تنظیمات سرویس‌ها و همچنین میان‌افزار پرباد را انجام می‌دهیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
           // ...

            var connectionString = Configuration.GetConnectionString("DefaultConnection");

            services.AddParbad()
                    .ConfigureHttpContext(httpContextBuilder => httpContextBuilder.UseDefaultAspNetCore())
                    .ConfigureGateways(gatewayBuilder =>
                    {
                        gatewayBuilder
                            .AddParbadVirtual()
                            .WithOptions(gatewayOptions => gatewayOptions.GatewayPath = "/MyVirtualGateway");
                    })
                    .ConfigureStorage(storageBuilder =>
                    {
                        storageBuilder.UseEfCore(efCoreOptions =>
                            {
                                var assemblyName = typeof(ApplicationDbContext).Assembly.GetName().Name;
                                efCoreOptions.ConfigureDbContext = db =>
                                    db.UseSqlServer(
                                        connectionString,
                                        sqlServerOptionsAction: sqlOptions => sqlOptions.MigrationsAssembly(assemblyName)
                                    );
                            });
                    })
                    .ConfigureAutoTrackingNumber(opt => opt.MinimumValue = 1)
                    .ConfigureOptions(parbadOptions =>
                    {
                        // parbadOptions.Messages.PaymentSucceed = "YOUR MESSAGE";
                    });

           // ...
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
           // ...

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

            if (env.IsDevelopment())
            {
                app.UseParbadVirtualGatewayWhenDeveloping();
            }
            else
            {
                app.UseParbadVirtualGateway();
            }
        }
    }
}
چند نکته:
- در متد ConfigureGateways می‌توان چندین درگاه را معرفی کرد که برای مثال در اینجا از درگاه مجازی و محلی آن استفاده شده‌است.
- در متد ConfigureStorage، تنظیمات EF-Core آن‌را مشاهده می‌کنید. پرباد به همراه DbContext خاص خودش است. یعنی در این حالت برنامه‌ی شما حداقل دو DbContext خواهد داشت؛ یکی ApplicationDbContext و دیگری ParbadDataContext.
- می‌خواهیم شماره‌ی تراکنش‌ها را به صورت خودکار توسط پرباد مدیریت کنیم. به همین جهت می‌توان عدد ابتدای آن‌را توسط متد ConfigureAutoTrackingNumber مشخص کرد.
- در پایان هم تعاریف مسیریابی میان‌افزار آن‌را مشاهده می‌کنید که می‌تواند برای حالت توسعه و ارائه‌ی نهایی متفاوت باشد.


تکمیل خواص موجودیت RoomOrderDetail جهت کار با پرباد

موجودیت RoomOrderDetail را در قسمت قبل معرفی کردیم. پرباد به ازای هر تراکنش بانکی که صورت می‌گیرد، یا نیاز به یک TrackingNumber خودکار را دارد و یا دستی. یعنی یا می‌توانیم شماره تراکنش خاص خودمان را تولید کنیم و در اختیار آن قرار دهیم و یا از آن درخواست کنیم تا این شماره را مدیریت کرده و به صورت خودکار تولید کند. در هر دو حالت نیاز است این شماره را به ردیف‌های جدول جزئیات سفارشات اتاق‌های هتل اضافه کرد که در این مثال ParbadTrackingNumber نام دارد:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BlazorServer.Entities
{
    public class RoomOrderDetail
    {
        // ...

        [Required]
        public long ParbadTrackingNumber { get; set; }

        public bool IsPaymentSuccessful { get; set; }

        public string Status { get; set; }
    }
}
همچنین در پایان عملیات هم فیلدهای IsPaymentSuccessful و وضعیت اتاق را تکمیل می‌کنیم.


ایجاد جداول متناظر با ParbadDataContext

همانطور که عنوان شد، اکنون برنامه به همراه دو DbContext است. بنابراین در این حالت در حین اجرای مهاجرت‌ها، ذکر نام Context مدنظر اجباری است.
برای ایجاد مهاجرت‌های متناظر با ParbadDataContext، از طریق خط فرمان به پوشه‌ی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا می‌کنیم:
dotnet tool update --global dotnet-ef --version 5.0.4
dotnet build

dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddParbadFields --context ApplicationDbContext
dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddParbad --context Parbad.Storage.EntityFrameworkCore.Context.ParbadDataContext

dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext
dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context Parbad.Storage.EntityFrameworkCore.Context.ParbadDataContext
چون برنامه دو Context ای است، نیاز است دوبار دستور تولید مهاجرت‌ها و دوبار دستور اعمال آن‌ها را به بانک اطلاعاتی صادر کرد که روش آن‌را در دستورات فوق مشاهده می‌کنید. پس از این دستورات، بانک اطلاعاتی برنامه شامل دو جدول جدید مخصوص پرباد خواهد بود:



روش یکپارچه سازی پرباد با یک برنامه‌ی SPA

روش متداول کار با پرباد، بر اساس طراحی مخصوص ASP.NET Core آن است. ابتدا درخواستی را به آن ارسال می‌کنید. سپس پرباد شماره تراکنشی را تولید کرده و شروع تراکنش را در بانک اطلاعاتی ثبت می‌کند. در ادامه به صورت خودکار، کار ارسال اطلاعات به درگاه بانکی (برای مثال ارسال تمام فیلدهای یک فرم ویژه‌ی آن بانک، بر اساس مستندات آن) و هدایت به درگاه بانکی را انجام می‌دهد. پس از پایان کار پرداخت، کار هدایت به اکشن متد دریافت تائیدیه‌ی نهایی صورت می‌گیرد و همینجا کار به پایان می‌رسد. این روش هرچند برای برنامه‌های سمت سرور ASP.NET Core کار می‌کند، اما ... به همین نحو با برنامه‌های تک صفحه‌ای وب مانند Blazor WASM قابل استفاده نیست. در اینجا روش تبادل اطلاعات با اکشن متدهای وب سرویس‌های برنامه از طریق یک HttpClient است و در این حالت دیگر نمی‌توان از مزایای Post و Redirect خودکار پرباد که در سمت سرور صورت می‌گیرد استفاده کرد. با استفاده از HttpClient، یک شیء را به سمت Web API ارسال می‌کنیم و در پاسخ، فقط یک شیء را دریافت می‌کنیم. در اینجا دیگر خبری از Redirect به درگاه اصلی بانکی و Post اطلاعات به آن نیست. بنابراین روش کار با پرباد در اینجا به صورت زیر خواهد بود:
الف) شماره Id سفارش و مبلغ نهایی آن‌را از طریق یک درخواست Get معمولی به اکشن متدی در سمت سرور ارسال می‌کنیم. یعنی نیاز است ابتدا Url زیر را تشکیل داد که شماره سفارش و مبلغ آن، به صورت کوئری استرینگ‌هایی به اکشن متد PayRoomOrder ارسال می‌شوند:
https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000
برنامه‌ی کلاینت برای اینکه بتواند این هدایت را انجام دهد، نیاز به نکته‌ی خاصی را دارد که در ادامه توضیح داده خواهد شد.
ب) اکنون چون یک redirect سمت سرور صورت گرفته، به صورت معمولی در اکشن متد PayRoomOrder با پرباد پردازش صورت گرفته و به سمت درگاه هدایت می‌شویم. پس از پرداخت نهایی، باز هم به صورت خودکار به اکشن متد دیگری جهت تائید عملیات هدایت خواهیم شد.
ج) در پایان کار، اکشن متد سمت سرور، ما را به سمت کامپوننتی در برنامه‌ی کلاینت Redirect خواهد کرد:
https://localhost:5002/payment-result/OrderId/TrackingNumber/Message
در اینجا شماره سفارش ابتدایی که مشخص است. همان شماره‌ای است که کار را با آن از سمت کلاینت آغاز کردیم. نکته‌ی مهم، TrackingNumber تراکنش است که بر اساس آن رکورد متناظری یافت شده و وضعیت نهایی آن‌را به کاربر نمایش می‌دهیم.

بنابراین روش یکپارچه سازی پربابد با برنامه‌های SPA، بر اساس Redirect‌های کامل است که سبب بارگذاری مجدد کل صفحه و آدرس‌ها می‌شوند و در اینجا از HttpClient برای کار با پرباد استفاده نخواهیم کرد؛ چون تمام اعمال خودکار آن‌را از دست خواهیم داد و مجبور به بازنویسی آن‌ها خواهیم شد که در دراز مدت با تغییرات این کتابخانه، قابل نگهداری نخواهند بود. بنابراین بهتر است خود پرباد کار Redirect‌ها و ارسال اطلاعات به درگاه‌های بانکی را مدیریت کند و نه ما از طریق کار با یک HttpClient.


آشنایی با گردش کار برنامه

در این مثال، مراحل زیر را طی خواهیم کرد:

1- شروع به انتخاب یک بازه‌ی زمانی و تعداد شب اقامت


2- انتخاب یک اتاق از لیست اتاق‌ها با کلیک بر روی دکمه‌ی Book آن


3- کلیک بر روی دکمه‌ی checkout، در صفحه‌ی مشاهده‌ی جزئیات اتاق و شروع به پرداخت


4- هدایت به درگاه مجازی پرباد در سمت برنامه‌ی Web API


5- پرداخت و هدایت خودکار به سمت برنامه‌ی Web API، جهت تائید نهایی


6- هدایت نهایی به سمت برنامه‌ی کلاینت، جهت نمایش اطلاعات پرداخت



ایجاد کنترلر پرداخت، توسط درگاه مجازی پرباد

پس از آشنایی با گردش کاری اطلاعات در اینجا، نیاز است بتوان لینک زیر را در برنامه‌ی کلاینت تولید کرد و سپس کاربر را به سمت اکشن متد PayRoomOrder هدایت نمود:
https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000
این اکشن متد و کنترلر آن به صورت زیر تهیه می‌شود:
namespace BlazorWasm.WebApi.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class ParbadPaymentController : Controller
    {
        private readonly IConfiguration _configuration;
        private readonly IOnlinePayment _onlinePayment;
        private readonly IRoomOrderDetailsService _roomOrderService;

        public ParbadPaymentController(
            IConfiguration configuration,
            IOnlinePayment onlinePayment,
            IRoomOrderDetailsService roomOrderService)
        {
            _configuration = configuration;
            _onlinePayment = onlinePayment ?? throw new ArgumentNullException(nameof(onlinePayment));
            _roomOrderService = roomOrderService ?? throw new ArgumentNullException(nameof(roomOrderService));
        }

        [HttpGet]
        public async Task<IActionResult> PayRoomOrder(int orderId, long amount)
        {
            var verifyUrl = Url.Action(
                    action: nameof(ParbadPaymentController.VerifyRoomOrderPayment),
                    controller: nameof(ParbadPaymentController).Replace("Controller", string.Empty),
                    values: null, protocol: Request.Scheme);

            var result = await _onlinePayment.RequestAsync(invoiceBuilder =>
                invoiceBuilder.UseAutoIncrementTrackingNumber()
                            .SetAmount(amount)
                            .SetCallbackUrl(verifyUrl)
                            .UseParbadVirtual()
            );

            if (result.IsSucceed)
            {
                await _roomOrderService.UpdateRoomOrderTrackingNumberAsync(orderId, result.TrackingNumber);

                // It will redirect the client to the gateway.
                return result.GatewayTransporter.TransportToGateway();
            }
            else
            {
                return Redirect(getClientReturnUrl(orderId, result.TrackingNumber, result.Message));
            }
        }

        [HttpGet, HttpPost]
        public async Task<IActionResult> VerifyRoomOrderPayment()
        {
            var invoice = await _onlinePayment.FetchAsync();
            var orderDetail = await _roomOrderService.GetOrderDetailByTrackingNumberAsync(invoice.TrackingNumber);
            if (invoice.Status == PaymentFetchResultStatus.AlreadyProcessed)
            {
                return Redirect(getClientReturnUrl(orderDetail.Id, invoice.TrackingNumber, "The payment is already processed."));
            }

            var verifyResult = await _onlinePayment.VerifyAsync(invoice);
            if (verifyResult.Status == PaymentVerifyResultStatus.Succeed)
            {
                var result = await _roomOrderService.MarkPaymentSuccessfulAsync(verifyResult.TrackingNumber, verifyResult.Amount);
                if (result == null)
                {
                    return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, "Can not mark payment as successful"));
                }
                return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, verifyResult.Message));
            }
            return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, verifyResult.Message));
        }

        private string getClientReturnUrl(int orderId, long trackingNumber, string errorMessage)
        {
            var clientBaseUrl = _configuration.GetValue<string>("Client_URL");
            return new Uri(new Uri(clientBaseUrl),
                $"/payment-result/{orderId}/{trackingNumber}/{WebUtility.UrlEncode(errorMessage)}").ToString();
        }
    }
}
توضیحات:
در اینجا کدهای کامل ParbadPaymentController مشاهده می‌کنید.

- گردش کاری پرداخت، با فراخوانی اکشن متد PayRoomOrder شروع می‌شود که دو پارامتر شماره سفارش و مبلغ آن‌را دریافت می‌کند.
[HttpGet]
public async Task<IActionResult> PayRoomOrder(int orderId, long amount)
نوع آن هم عمدا، HttpGet درنظر گرفته شده‌است تا دقیقا مشخص باشد که فقط با Redirect کامل به آن (هدایت کامل از سمت کلاینت به سمت سرور)، کار خواهد کرد و هدف دیگری را دنبال نمی‌کند.

- در اکشن متد PayRoomOrder، نیاز است لینک بازگشت از درگاه بانکی را مشخص کنیم. پس از اینکه کاربر پرداختی را انجام داد، مجددا به صورت خودکار، به سمت آدرسی در همین Web API و نه برنامه‌ی سمت کلاینت هدایت می‌شود؛ چون هنوز کار پرباد به پایان نرسیده و باید عملیات انجام شده را تصدیق کند. به همین جهت ابتدا آدرس اکشن متدی که کار تائید نهایی را انجام می‌دهد، تولید کرده و به متد RequestAsync آن به همراه مبلغ نهایی و نوع درگاه، ارسال می‌کنیم.

- استفاده از UseAutoIncrementTrackingNumber سبب می‌شود تا پرباد خودش مدیریت TrackingNumber را انجام دهد که پس از پایان عملیات، توسط خاصیت result.TrackingNumber در دسترس خواهد بود.

- پس از پایان عملیات ابتدایی RequestAsync که سشن پرباد را ایجاد کرده و همچنین رکوردی را در بانک اطلاعاتی نیز ثبت می‌کند (در جداول درونی خود پرباد)، نیاز است رکورد سفارشی را که با آن کار را شروع کردیم یافته و TrackingNumber آن‌را با مقدار واقعی دریافتی از پرباد، به روز رسانی کنیم. اینکار توسط متد UpdateRoomOrderTrackingNumberAsync انجام می‌شود:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        public async Task UpdateRoomOrderTrackingNumberAsync(int roomOrderId, long trackingNumber)
        {
            var order = await _dbContext.RoomOrderDetails.FindAsync(roomOrderId);
            if (order == null)
            {
                return;
            }

            order.ParbadTrackingNumber = trackingNumber;
            _dbContext.RoomOrderDetails.Update(order);
            await _dbContext.SaveChangesAsync();
        }
    }
}
بر اساس شماره سفارشی که داریم، رکورد متناظر با آن‌را یافته و سپس trackingNumber تولیدی را در آن به روز رسانی می‌کنیم.

- اکنون با فراخوانی متد ()result.GatewayTransporter.TransportToGateway، دو کار مهم رخ می‌دهند:
الف) ارسال خودکار اطلاعات به سمت درگاه بانکی
ب) Redirect خودکار به سمت درگاه بانگی
به همین جهت است که علاقمند نبودیم تا این مراحل را توسط HttpClient برنامه‌ی Blazor WASM مدیریت و بازنویسی کنیم.

- پس از هدایت به سمت درگاه بانکی و تکمیل پرداخت، اکنون مجددا به همان verifyUrl هدایت می‌شویم. یعنی اکنون به مرحله‌ی پردازش اکشن متد VerifyRoomOrderPayment در سمت Web API رسیده‌ایم.
[HttpGet, HttpPost]
public async Task<IActionResult> VerifyRoomOrderPayment()
در اینجا ابتدا invoice.TrackingNumber در حال پردازش را دریافت می‌کنیم. به کمک این عدد می‌توان رکورد سفارش متناظر با آن‌را یافت. به همین جهت است که آن‌را به لیست فیلدهای جدول سفارشات اضافه کردیم. اینکار هم توسط متد GetOrderDetailByTrackingNumberAsync صورت می‌گیرد:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        public async Task<RoomOrderDetailsDTO> GetOrderDetailByTrackingNumberAsync(long trackingNumber)
        {
            var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
                                            .Include(u => u.HotelRoom)
                                                .ThenInclude(x => x.HotelRoomImages)
                                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                                            .FirstOrDefaultAsync(u => u.ParbadTrackingNumber == trackingNumber);

            roomOrderDetailsDTO.HotelRoomDTO.TotalDays =
                roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days;
            return roomOrderDetailsDTO;
        }
    }
}
- در ادامه پرباد کار تصدیق اطلاعات دریافتی از درگاه بانکی را انجام می‌دهد. دراینجا اگر عملیات با موفقیت مواجه شود، سه فیلدی را که در ابتدای بحث در مورد ثبت اطلاعات تراکنش اضافه کردیم، به روز رسانی می‌کنیم:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        public async Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(long trackingNumber, long amount)
        {
            var order = await _dbContext.RoomOrderDetails.FirstOrDefaultAsync(x => x.ParbadTrackingNumber == trackingNumber);
            if (order?.IsPaymentSuccessful != false || order.TotalCost != amount)
            {
                return null;
            }

            order.IsPaymentSuccessful = true;
            order.Status = BookingStatus.Booked;
            var markPaymentSuccessful = _dbContext.RoomOrderDetails.Update(order);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<RoomOrderDetailsDTO>(markPaymentSuccessful.Entity);
        }
    }
}
- در اینجا بر اساس trackingNumber، سند متناظری را یافته و سپس بررسی می‌کنیم که آیا مبلغ سند، با مبلغ تائید شده، یکی هست یا خیر؟ اگر خیر، نیاز هست پرداخت را برگشت بزنیم که اینکار توسط متد کنسل پرباد قابل انجام است.

- در تمام این مراحل، کار Redirect به سمت کلاینت و کامپوننت payment-result آن، با فراخوانی متد return Redirect اکشن متدها صورت می‌گیرد که Url آن به صورت زیر تامین می‌شود:
        private string getClientReturnUrl(int orderId, long trackingNumber, string errorMessage)
        {
            var clientBaseUrl = _configuration.GetValue<string>("Client_URL");
            return new Uri(new Uri(clientBaseUrl),
                $"/payment-result/{orderId}/{trackingNumber}/{WebUtility.UrlEncode(errorMessage)}").ToString();
        }
در این متد Client_URL را از فایل appsettings.json برنامه‌ی Web API دریافت می‌کنیم که به آدرس ریشه‌ی برنامه‌ی کلاینت اشاره می‌کند:
{
   "Client_URL": "https://localhost:5002/"
}


تکمیل قسمت سمت کلاینت عملیات پرداخت بانکی، توسط درگاه مجازی پرباد

تا اینجا کنترلری که کار پرداخت آنلاین را مدیریت می‌کند، پیاده سازی کردیم. قسمت آخر این بحث به تکمیل جزئیات این گردش کاری که شامل شروع عملیات سفارش و پرداخت از سمت کلاینت و نمایش پیام خطا یا موفقیت پرداخت به کاربر است، اختصاص دارد.

الف) تکمیل کامپوننت RoomDetails.razor جهت شروع به پرداخت آنلاین
کامپوننت RoomDetails.razor را در قسمت قبل آغاز کردیم و توسعه‌ی آن‌را تا جائی پیش بردیم که اعتبارسنجی‌های آن‌را به علت استفاده‌ی از خواص تو در تو، به صورت دستی انجام دادیم. پس از مرحله‌ی اعتبارسنجی، اکنون می‌خواهیم کاربر را به سمت درگاه بانکی جهت پرداخت، هدایت کنیم:
@page "/hotel-room-details/{Id:int}"

@inject IJSRuntime JsRuntime
@inject ILocalStorageService LocalStorage
@inject IClientHotelRoomService HotelRoomService
@inject IClientRoomOrderDetailsService RoomOrderDetailsService
@inject NavigationManager NavigationManager
@inject HttpClient HttpClient

// ...

@code {

    // ...

    private async Task HandleCheckout()
    {
        if (!await HandleValidation())
        {
            return;
        }

        try
        {
            HotelBooking.OrderDetails.ParbadTrackingNumber = -1;
            HotelBooking.OrderDetails.RoomId = HotelBooking.OrderDetails.HotelRoomDTO.Id;
            HotelBooking.OrderDetails.TotalCost = HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount;
            var roomOrderDetailsSaved = await RoomOrderDetailsService.SaveRoomOrderDetailsAsync(HotelBooking.OrderDetails);

            await LocalStorage.SetItemAsync(ConstantKeys.LocalRoomOrderDetails, roomOrderDetailsSaved);

            var paymentUri = new UriBuilderExt(new Uri(HttpClient.BaseAddress, $"/api/ParbadPayment/PayRoomOrder"))
                            .AddParameter("orderId", roomOrderDetailsSaved.Id.ToString())
                            .AddParameter("amount", roomOrderDetailsSaved.TotalCost.ToString())
                            .Uri;
            NavigationManager.NavigateTo(paymentUri.ToString(), forceLoad: true);
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }

    // ...
}
متد HandleValidation را در انتهای قسمت قبل تکمیل کردیم. اکنون OrderDetails را بر اساس اطلاعات فرم و انتخاب‌های کاربر، تکمیل کرده و به متد SaveRoomOrderDetailsAsync ارسال می‌کنیم تا Id سفارش را تولید کنیم. این همان Id ای است که قرار است به سمت سرور و Web API ارسال کنیم تا بر اساس آن تراکنش و Tracking Number ای را بتوان به رکورد جاری انتساب داد. بنابراین نیاز به کنترلر سمت Web API ای را داریم که بتواند این‌کار را انجام دهد:
namespace BlazorWasm.WebApi.Controllers
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class RoomOrderController : Controller
    {
        private readonly IRoomOrderDetailsService _roomOrderService;

        public RoomOrderController(IRoomOrderDetailsService roomOrderService)
        {
            _roomOrderService = roomOrderService ?? throw new ArgumentNullException(nameof(roomOrderService));
        }

        [HttpPost]
        public async Task<IActionResult> Create([FromBody] RoomOrderDetailsDTO details)
        {
            var result = await _roomOrderService.CreateAsync(details);
            return Ok(result);
        }

        [HttpGet]
        public async Task<IActionResult> GetOrderDetail(int trackingNumber)
        {
            var result = await _roomOrderService.GetOrderDetailByTrackingNumberAsync(trackingNumber);
            return Ok(result);
        }
    }
}
- متد Create، بر اساس اطلاعات وارد شده‌ی توسط کاربر، آن‌ها را تبدیل به یک رکورد سفارش جدید می‌کند و به سمت کلاینت بازگشت می‌دهد.
- متد GetOrderDetail، بر اساس trackingNumber دریافتی از پرباد، کار بازگشت رکورد متناظری را انجام می‌دهد. از آن در پایان کار، جهت نمایش وضعیت پرداخت، استفاده می‌کنیم.
این دو متد در سرویس سمت سرور RoomOrderDetailsService، به صورت زیر تامین شده‌اند:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        public async Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details)
        {
            var roomOrder = _mapper.Map<RoomOrderDetail>(details);
            roomOrder.Status = BookingStatus.Pending;
            var result = await _dbContext.RoomOrderDetails.AddAsync(roomOrder);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<RoomOrderDetailsDTO>(result.Entity);
        }


        public async Task<RoomOrderDetailsDTO> GetOrderDetailByTrackingNumberAsync(long trackingNumber)
        {
            var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
                                            .Include(u => u.HotelRoom)
                                                .ThenInclude(x => x.HotelRoomImages)
                                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                                            .FirstOrDefaultAsync(u => u.ParbadTrackingNumber == trackingNumber);

            roomOrderDetailsDTO.HotelRoomDTO.TotalDays =
                roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days;
            return roomOrderDetailsDTO;
        }

       // ...
    }
}
اکنون که Web API Endpoint مدنظر را ایجاد کردیم، نیاز است سرویس سمت کلاینتی را نیز جهت تعامل با آن تهیه کنیم:
namespace BlazorWasm.Client.Services
{
    public interface IClientRoomOrderDetailsService
    {
        Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details);
        Task<RoomOrderDetailsDTO> GetOrderDetailAsync(long trackingNumber);
    }
}

namespace BlazorWasm.Client.Services
{
    public class ClientRoomOrderDetailsService : IClientRoomOrderDetailsService
    {
        private readonly HttpClient _httpClient;

        public ClientRoomOrderDetailsService(HttpClient httpClient)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        }

        public Task<RoomOrderDetailsDTO> GetOrderDetailAsync(long trackingNumber)
        {
            // How to url-encode query-string parameters properly
            var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, $"/api/roomorder/GetOrderDetail"))
                            .AddParameter("trackingNumber", trackingNumber.ToString())
                            .Uri;
            return _httpClient.GetFromJsonAsync<RoomOrderDetailsDTO>(uri);

        }

        public async Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details)
        {
            details.UserId = "unknown user!";
            var response = await _httpClient.PostAsJsonAsync("api/roomorder/create", details);
            var responseContent = await response.Content.ReadAsStringAsync();
            if (response.IsSuccessStatusCode)
            {
                return JsonSerializer.Deserialize<RoomOrderDetailsDTO>(responseContent);
            }
            else
            {
                //var errorModel = JsonSerializer.Deserialize<ErrorModel>(responseContent);
                throw new InvalidOperationException(responseContent);
            }
        }
    }
}
- متد GetOrderDetailAsync بر اساس trackingNumber دریافتی پس از عملیات تصدیق پرداخت، کار بازگشت جزئیات رکورد متناظری را انجام می‌دهد.
- متد SaveRoomOrderDetailsAsync، یک رکورد سفارش جدید را ایجاد می‌کند. در اینجا با روش مشاهده‌ی خطای کامل بازگشتی از سمت سرور (در صورت وجود) هم آشنا شده‌ایم که در مواقع لزوم می‌تواند راه‌گشا باشد.
- در متد SaveRoomOrderDetailsAsync فعلا مقدار UserId اجباری را به عبارتی دلخواه، تنظیم کرده‌ایم. این مورد را در قسمت‌های بعدی با معرفی اعتبارسنجی و احراز هویت سمت کلاینت، تکمیل خواهیم کرد.

این سرویس جدید را هم باید به سیستم تزریق وابستگی‌های برنامه‌ی کلاینت معرفی کرد تا قابل استفاده شود:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            // ...
            builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>();
بنابراین در متد HandleCheckout ای که در حال بررسی آن هستیم، ابتدا متد SaveRoomOrderDetailsAsync فوق فراخوانی می‌شود تا توسط Web API Endpoint متناظری، یک رکورد سفارش ابتدایی را ایجاد کرده و Id آن‌را در اختیار ما قرار دهد.
سپس به قطعه کد مهم زیر می‌رسیم:
var paymentUri = new UriBuilderExt(new Uri(HttpClient.BaseAddress, $"/api/ParbadPayment/PayRoomOrder"))
    .AddParameter("orderId", roomOrderDetailsSaved.Id.ToString())
    .AddParameter("amount", roomOrderDetailsSaved.TotalCost.ToString())
    .Uri;
NavigationManager.NavigateTo(paymentUri.ToString(), forceLoad: true);
اینجا است که برای نمونه آدرس https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000 ساخته شده و توسط متد NavigateTo فراخوانی می‌شود. فراخوانی متداول متد NavigateTo در اینجا کارساز نیست؛ چون سبب reload آدرس درخواستی نمی‌شود. یعنی هدایت‌های صورت گرفته‌ی توسط آن، در همان داخل مرورگر رخ می‌دهند و سبب ارسال درخواستی به سمت سرور نخواهند شد. می‌توان این رفتار را با ذکر پارامتر دوم آن تغییر داد. در اینجا اگر پارامتر forceLoad را به true تنظیم کنیم، ابتدا سبب هدایت به آدرس درخواستی و سپس reload کامل صفحه می‌شود (دقیقا مثل اینکه شخصی، آدرسی را در نوار آدرس مرورگر وارد کند و سپس دکمه‌ی enter را بفشارد). این reload است که برنامه‌ی کلاینت را اکنون به سمت برنامه‌ی Web API هدایت می‌کند.


نمایش وضعیت پرداخت، به کاربر در پایان گردش کاری آن

پس از این مراحل، مرحله‌ی آخر کار باقی مانده‌است؛ یعنی بازگشت از اکشن متد VerifyRoomOrderPayment سمت سرور، به کامپوننت PaymentResult سمت کلاینت، برای نمایش نتیجه‌ی عملیات. به همین جهت کامپوننت جدید Pages\HotelRooms\PaymentResult.razor را ایجاد کرده و به صورت زیر تکمیل می‌کنیم:
@page "/payment-result/{OrderId:int}/{TrackingNumber:long}/{Message}"
@inject ILocalStorageService LocalStorage
@inject IClientRoomOrderDetailsService RoomOrderDetailService
@inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager

@if (IsLoading)
{
    <div style="position:fixed;top:50%;left:50%;margin-top:-50px;margin-left:-100px;">
        <img src="images/ajax-loader.gif" />
    </div>
}
else
{
    <div class="container">
        <div class="row mt-4 pt-4">
            <div class="col-10 offset-1 text-center">
            @if(IsPaymentSuccessful)
            {
                <h2 class="text-success">Booking Confirmed!</h2>
                <p>Your room has been booked successfully with order id @OrderId & tracking number @TrackingNumber .</p>
            }
            else
            {
                <h2 class="text-warning">Booking Failed!</h2>
                <p>@Message</p>
            }
            <a class="btn btn-primary" href="hotel-rooms">Back to rooms</a>
            </div>
        </div>
    </div>
}

@code
{
    private bool IsLoading;
    private bool IsPaymentSuccessful;

    [Parameter] public int OrderId { set; get; }
    [Parameter] public long TrackingNumber { set; get; }
    [Parameter] public string Message { set; get; }

    protected override async Task OnInitializedAsync()
    {
        IsLoading = true;
        try
        {
            var finalOrderDetail = await RoomOrderDetailService.GetOrderDetailAsync(TrackingNumber);
            var localOrderDetail = await LocalStorage.GetItemAsync<RoomOrderDetailsDTO>(ConstantKeys.LocalRoomOrderDetails);
            if(finalOrderDetail is not null &&
                finalOrderDetail.IsPaymentSuccessful &&
                finalOrderDetail.Status == BookingStatus.Booked &&
                localOrderDetail is not null &&
                localOrderDetail.TotalCost == finalOrderDetail.TotalCost)
            {
                IsPaymentSuccessful = true;
                await LocalStorage.RemoveItemAsync(ConstantKeys.LocalRoomOrderDetails);
                await LocalStorage.RemoveItemAsync(ConstantKeys.LocalInitialBooking);
            }
            else
            {
                IsPaymentSuccessful = false;
            }
        }
        catch(Exception ex)
        {
            await JsRuntime.ToastrError(ex.Message);
        }
        finally
        {
            IsLoading = false;
        }
    }
}
این کامپوننت بر اساس مسیریابی که دارد:
@page "/payment-result/{OrderId:int}/{TrackingNumber:long}/{Message}"
سه پارامتر شماره سفارش، شماره تراکنش و پیامی را پس از پایان عملیات تصدیق پرداخت، از Web API، در طی یک redirect کامل دریافت می‌کند. در ادامه به کمک متد RoomOrderDetailService.GetOrderDetailAsync که آن‌را پیشتر توسعه دادیم، اصل رکورد متناظر با این سفارش را بازیابی کرده و فیلدهای IsPaymentSuccessful و Status آن‌را بررسی می‌کنیم (این فیلدها در زمان تصدیق پرداخت، در همان سمت سرور مقدار دهی می‌شوند). همچنین جهت محکم‌کاری، قسمتی از این اطلاعات را با Local Storage نیز انطباق داده‌ایم. اگر پرداخت، موفقیت آمیز باشد، شماره سفارش و همچنین شماره تراکنش را به کاربر نمایش می‌دهیم و یا پیام دریافتی از سرور را در صفحه درج می‌کنیم.


جلوگیری از ثبت سفارش اتاقی که رزرو شده‌است


پس از پایان عملیات سفارش یک اتاق، بهتر است امکان سفارش اتاقی را که دیگر در دسترس نیست، غیرفعال کنیم (تصویر فوق) که اینکار را می‌توان توسط خاصیت IsBooked مدل UI کامپوننت نمایش لیست اتاق‌ها انجام داد:
    public class HotelRoomDTO
    {
        public bool IsBooked { get; set; }

        // ...
    }
این خاصیت را در متدهای بازگشت لیست تمام اتاق‌ها و یا بازگشت اطلاعات یک اتاق، به صورت زیر محاسبه و مقدار دهی می‌کنیم:
namespace BlazorServer.Services
{
    public class HotelRoomService : IHotelRoomService
    {
       // ...

        public async Task<List<HotelRoomDTO>> GetAllHotelRoomsAsync(DateTime? checkInDateStr, DateTime? checkOutDatestr)
        {
            var hotelRooms = await _dbContext.HotelRooms
                        .Include(x => x.HotelRoomImages)
                        .Include(x => x.RoomOrderDetails)
                        .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                        .ToListAsync();

            foreach (var hotelRoom in hotelRooms)
            {
                hotelRoom.IsBooked = isRoomBooked(hotelRoom, checkInDateStr, checkOutDatestr);
            }

            return hotelRooms;
        }

        public async Task<HotelRoomDTO> GetHotelRoomAsync(int roomId, DateTime? checkInDate, DateTime? checkOutDate)
        {
            var hotelRoom = await _dbContext.HotelRooms
                            .Include(x => x.HotelRoomImages)
                            .Include(x => x.RoomOrderDetails)
                            .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                            .FirstOrDefaultAsync(x => x.Id == roomId);
            hotelRoom.IsBooked = isRoomBooked(hotelRoom, checkInDate, checkOutDate);
            return hotelRoom;
        }

        private bool isRoomBooked(HotelRoomDTO hotelRoom, DateTime? checkInDate, DateTime? checkOutDate)
        {
            if (checkInDate == null || checkOutDate == null)
            {
                return false;
            }

            return hotelRoom.RoomOrderDetails.Any(x => x.IsPaymentSuccessful &&
                        //check if checkin date that user wants does not fall in between any dates for room that is booked
                        ((checkInDate < x.CheckOutDate && checkInDate.Value.Date >= x.CheckInDate)
                        //check if checkout date that user wants does not fall in between any dates for room that is booked
                        || (checkOutDate.Value.Date > x.CheckInDate.Date && checkInDate.Value.Date <= x.CheckInDate.Date))
                    );
        }
    }
}
متد isRoomBooked، یک محاسبه‌ی سمت سرور محسوب نمی‌شود؛ چون با استفاده از Include‌های نوشته شده، اطلاعات کامل اتاق و وابستگی‌های آن (سرهای دیگر رابطه‌ی تشکیل شده) را داریم و این محاسبات سبب رفت و برگشتی به سمت سرور نمی‌شوند.

اکنون که خاصیت IsBooked مقدار دهی شده‌است، در دو قسمت از آن استفاده خواهیم کرد:
الف) در کامپوننت نمایش لیست اتاق‌ها
@if (room.IsBooked)
{
    <button disabled class="btn btn-secondary btn-block">Sold Out</button>
}
else
{
    <a href="@($"hotel-room-details/{room.Id}")" class="btn btn-success btn-block">Book</a>
}
ب) در کامپوننت نمایش جزئیات یک اتاق
@if (HotelBooking.OrderDetails.HotelRoomDTO.IsBooked)
{
    <button disabled class="btn btn-secondary btn-block">Sold Out</button>
}
else
{
    <button type="submit" class="btn btn-success form-control">Checkout Now</button>
}


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-30.zip
مطالب
الگوی مشاهده‌گر Observer Pattern
الگوی مشاهده‌گر یکی از محبوبترین و معروفترین الگوهای برنامه نویسی است که پیاده سازی آن در بسیاری از زبان‌ها رواج یافته است. برای نمونه پیاده سازی این الگو را می‌توانید در بسیاری از کتابخانه‌ها (به خصوص GUI) مانند این مطالب (+ + + ) مشاهده کنید. برای اینکه بتوانیم این الگو را خودمان برای اشیاء برنامه خودمان پیاده کنیم، بهتر است که بیشتر با خود این الگو آشنا شویم. برای شروع بهتر است که با یک مثال به تعریف این الگو بپردازیم. مثال زیر نقل قولی از یکی از مطالب این سایت است که به خوبی کارکرد این الگو را برای شما نشان میدهد.

یک لامپ و سوئیچ برق را درنظر بگیرید. زمانیکه لامپ مشاهده می‌کند سوئیچ برق در حالت روشن قرار گرفته‌است، روشن خواهد شد و برعکس. در اینجا به سوئیچ، subject و به لامپ، observer گفته می‌شود. هر زمان که حالت سوئیچ تغییر می‌کند، از طریق یک callback، وضعیت خود را به observer اعلام خواهد کرد. علت استفاده از callbackها، ارائه راه‌حل‌های عمومی است تا بتواند با انواع و اقسام اشیاء کار کند. به این ترتیب هر بار که شیء observer از نوع متفاوتی تعریف می‌شود (مثلا بجای لامپ یک خودرو قرار گیرد)، نیازی نخواهد بود تا subject را تغییر داد.

عموما به شیءایی که قرار است وضعیت را مشاهده یا رصد کند، Observer گفته می‌شود و به شیءایی که قرار است وضعیت آن رصد شود Observable یا Subject گفته می‌شود.
 بد نیست بدانید این الگو یکی از کلیدی‌ترین بخش‌های معماری لایه بندی MVC نیز می‌باشد.
 همچنین این نکته حائز اهمیت است که این الگو ممکن است باعث نشتی حافظه هم شود و به این مشکل Lapsed Listener Problem می‌گویند. یعنی یک listener وجود دارد که تاریخ آن منقضی شده، ولی هنوز در حافظه جا خوش کرده‌است. این مشکل برای زبان‌های شیءگرایی که با سیستمی مشابه GC پیاده سازی می‌شوند، رخ میدهد. برای جلوگیری از این حالت، برنامه نویس باید  این مشکل را با رجیستر کردن‌ها و عدم رجیستر یک شنوده، در مواقع لزوم حل کند. در غیر این صورت این شنونده بی جهت، یک ارتباط را زنده نگه داشته و حافظه‌ی منبع را به هدر میدهد.

مثال: ما یک کلید داریم که سه کلاس  RedLED،GreenLED و BlueLED قرار است آن را مشاهده و وضعیت کلید را رصد کنند.
برای پیاده سازی این الگو، ابتدا یک کلاس انتزاعی را با نام Observer که دارای متدی به نام Update است، ایجاد می‌کنیم. متغیر از نوع کلاس Observable را بعدا ایجاد می‌کنیم:
public abstract class Observer
    {
        protected Observable Observable;
        public abstract void Update();
    }
سپس یک کلاس پدر را به نام Observable می‌سازیم تا آن را به شیء سوئیچ نسبت دهیم:
 public  class Observable
    {
        private readonly List<Observer> _observers = new List<Observer>();

        public void Attach(Observer observer)
        {
            _observers.Add(observer);
        }
        public void Dettach(Observer observer)
        {
            _observers.Remove(observer);
        }

        public void NotifyAllObservers()
        {
            foreach (var observer in _observers)
            {
                observer.Update();
            }
        }
    }
در کلاس بالا یک لیست از نوع Observer‌ها داریم که در آن، کلید با تغییر وضعیت خود، لیست رصد کنندگانش را مطلع می‌سازد و دیگر چراغ‌های LED نیازی نیست تا مرتب وضعیت کلید را چک کنند. متدهای attach و Detach در واقع همان رجیستر‌ها هستند که باید مدیریت خوبی روی آن‌ها داشته باشید تا نشتی حافظه پیش نیاید. در نهایت متد NotifyAllObservers هم متدی است که با مرور لیست رصدکنندگانش، رویداد Update آن‌ها را صدا میزند تا تغییر وضعیت کلید به آن‌ها گزارش داده شود.

حال کلاس Switch را با ارث بری از کلاس Observable می‌نویسیم:
 public  class Switch:Observable
    {
        private bool _state;

        public bool ChangeState
        {
            set
            {
                _state = value;
                NotifyAllObservers();
            }
            get { return _state; }
        }
    }
در کلاس بالا هرجایی که وضعیت کلید تغییر می‌یابد، متد NotifyAllObservers صدا زده میشود.
برای هر سه چراغ، رنگی هم داریم:
   public class RedLED:Observer
    {
        private bool _on = false;
        public override void Update()
        {
            _on = !_on;
            Console.WriteLine($"Red LED is {((_on) ? "On" : "Off")}");
        }
    }

  public class GreenLED:Observer
    {
        private bool _on = false;
        public override void Update()
        {
            _on = !_on;
            Console.WriteLine($"Green LED is {((_on) ? "On" : "Off")}");
        }
    }

  public  class BlueLED:Observer
    {
        private bool _on = false;
        public override void Update()
        {
            _on = !_on;
            Console.WriteLine($"Blue LED is {((_on) ? "On" : "Off")}");
        }
    }
سپس در Main اینگونه می‌نویسیم:
var greenLed=new GreenLED();
            var redLed=new RedLED();
            var blueLed=new BlueLED();

            var switchKey=new Switch();
            switchKey.Attach(greenLed);
            switchKey.Attach(redLed);
            switchKey.Attach(blueLed);

            switchKey.ChangeState = true;
            switchKey.ChangeState = false;
به طور خلاصه هر سه چراغ به شیء کلید attach شده و با هر بار عوض شدن وضعیت کلید، متدهای Update هر سه چراغ صدا زده خواهند شد. نتیجه‌ی کد بالا به شکل زیر در کنسول نمایش می‌یابد:
Green LED is On
Red LED is On
Blue LED is On
Green LED is Off
Red LED is Off
Blue LED is Off
مطالب
شروع به کار با DNTFrameworkCore - قسمت 2 - طراحی موجودیت‌های سیستم
در قسمت قبل، امکانات این زیرساخت را ملاحظه کردیم. در این مطلب و مطالب آینده، روش طراحی بخش‌های مختلف یکسری سیستم فرضی را با استفاده از امکانات مذکور و با جزئیات بیشتر، بررسی خواهیم کرد.
به منظور اعمال خودکار یکسری مفاهیم توسط زیرساخت، نیاز است موجودیت‌های شما قراردادهای مورد نظر را پیاده سازی کرده باشند یا اینکه از موجودیت‌های پایه که آن قراردادها را پیاده سازی کرده‌اند، به عنوان میانبر، از آنها ارث بری کنید. برای دسترسی به این موجودیت‌های پایه و یکسری واسط که به عنوان قراردادهایی در بخش‌های مختلف این زیرساخت استفاده می‌شوند، نیاز است تا ابتدا بسته نیوگت زیر را نصب کنید:
PM> Install-Package DNTFrameworkCore -Version 1.0.0

مثال اول: یک موجودیت ساده بدون نیاز به مباحث ردیابی تغییرات

public class MeasurementUnit : Entity<int>, IAggregateRoot
{
   public const int MaxTitleLength = 50;
   public const int MaxSymbolLength = 50;

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Symbol { get; set; }
    public byte[] RowVersion { get; set; }
}

‎کلاس جنریک Entity، در برگیرنده یکسری اعضای مشترک بین سایر موجودیت‌های سیستم از جمله Id و TrackingState (به منظور سناریوهای Master-Detail)، می‌باشد. 

‎نکته: در این زیرساخت برای پیاده سازی CrudService برای یک موجودیت خاص، نیاز است تا واسط IAggregateRoot را نیز پیاده سازی کرده باشد. برای پیاده سازی واسط مذکور نیاز است تا خصوصیت RowVersion را به منظور مدیریت Optimistic مباحث همزمانی، به کلاس بالا اضافه کنیم. این موضوع برای موجودیت‌های وابسته به یک Aggregate ضروری نیست، چرا که آنها با AggregateRoot ذخیره خواهند شد و تراکنش جدایی برای ثبت، ویرایش و یا حذف آنها وجود ندارد.

مثال دوم: یک موجودیت به همراه مباحث ردیابی تغییرات ثبت و آخرین ویرایش

public class Blog : TrackableEntity<long>, IAggregateRoot
{
    public const int MaxTitleLength = 50;
    public const int MaxUrlLength = 50;

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Url { get; set; }
    public byte[] RowVersion { get; set; }
}

کلاس جنریک TrackableEntity علاوه بر خصوصیات Id و TrackingState، یکسری خصوصیت دیگر از جمله زمان ثبت، زمان آخرین ویرایش، شناسه کاربر ثبت کننده، شناسه آخرین کاربر ویرایش کننده، اطلاعات مرورگرهای آنها و ... را نیز دارا می‌باشد. این خصوصیات به صورت خودکار توسط زیرساخت مقداردهی خواهند شد.


مثال سوم: یک موجودیت به همراه مباحث ردیابی تغییرات ثبت، آخرین ویرایش و حذف نرم

public class Blog : FullTrackableEntity<long>, IAggregateRoot
{
    public const int MaxTitleLength = 50;
    public const int MaxUrlLength = 50;

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Url { get; set; }
    public byte[] RowVersion { get; set; }
}

کلاس جنریک FullTrackableEntity علاوه بر خصوصیات ذکر شده در مثال دوم، یکسری خصوصیت دیگر از جمله IsDeleted، شناسه کاربر حذف کننده، زمان حذف و ... را نیز دارا می‌باشد. همچنین مباحث فیلتر خودکار رکوردهای حذف شده، به صورت خودکار توسط زیرساخت انجام می‌گیرد که امکان غیرفعال کردن آن در شرایط مورد نیاز نیز وجود دارد.

مثال چهارم: یک موجودیت با پشتیبانی از چند مستاجری

public class Blog : Entity<long>, IAggregateRoot, ITenantEntity
{
    public const int MaxTitleLength = 50;
    public const int MaxUrlLength = 50;
    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Url { get; set; }
    public byte[] RowVersion { get; set; }
    public long TenantId { get; set; }
}

با پیاده سازی واسط ITenantEntity، به صورت خودکار خصوصیت TenantId آن با توجه به اطلاعات مستاجر جاری سیستم مقداردهی خواهد شد و همچنین فیلتر خودکار بر روی رکوردهای مستاجرهای مختلف، توسط زیرساخت انجام می‌شود که این مکانیزم هم قابلیت غیرفعال شدن در شرایط خاص را دارد.

مثال پنجم: یک موجودیت به همراه تعدادی موجودیت جزئی (سناریوهای Master-Detail)

public class Invoice : TrackableEntity<long>, IAggregateRoot
{
    public InvoiceStatus Status { get; set; }
    public decimal TotalNet { get; set; }
    public decimal Total { get; set; }
    public decimal PayableTotal { get; set; }
    public decimal Debit { get; set; }
    public decimal Credit { get; set; }
    public decimal Gratuity { get; set; }
    public byte[] RowVersion { get; set; }

    public ICollection<InvoiceItem> Items { get; set; }
}

public class InvoiceItem : TrackableEntity
{
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal Price { get; set; }
    public decimal UnitPriceDiscount { get; set; }

    public long ItemId { get; set; }
    public Item Item { get; set; }
    public long InvoiceId { get; set; }
    public Invoice Invoice { get; set; }
}

همانطور که مشخص می‌باشد، موجودیت وابسته یا همان Detail، نیاز به پیاده سازی IAggregateRoot را نخواهد داشت. همانطور که اشاره شد، تراکنش مجزایی برای این موجودیت‌ها نخواهیم داشت و درون تراکنش AggregateRoot، عملیات CRUD آنها انجام خواهد شد و برای انجام عملیات ویرایش، به همراه Root متناظر با خود، واکشی خواهند شد. این موضوع یکی از نقاط قوت زیرساخت محسوب می‌شود که در مقالات آینده و در قسمت طراحی سرویس‌های متناظر با موجودیت‌های سیستم، با جزئیات بیشتری بررسی خواهد شد.

مثال ششم: یک موجودیت با امکان شماره گذاری خودکار

public class Task : TrackableEntity, IAggregateRoot, INumberedEntity
{
    public const int MaxTitleLength = 256;
    public const int MaxDescriptionLength = 1024;

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Number { get; set; }
    public string Description { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;
    public byte[] RowVersion { get; set; }
}

همانطور که در مطلب «طراحی و پیاده سازی زیرساختی برای تولید خودکار کد منحصر به فرد در زمان ثبت رکورد جدید» ملاحظه کردید، نیاز است تا موجودیت مورد نظر، پیاده ساز واسط INumberedEntity نیز باشد. این واسط دارای خصوصیت رشته‌ای Number می‌باشد و همچنین زیرساخت به صورت خودکار در زمان ثبت، این خصوصیت را برای موجودیت‌هایی از این نوع، با رعایت مباحث همزمانی مقداردهی می‌کند.

مثال هفتم: یک موجودیت با امکان ذخیره سازی اطلاعات اضافی در قالب فیلد JSON

public class Task : TrackableEntity, IAggregateRoot, INumberedEntity, IExtendableEntity
{
    public const int MaxTitleLength = 256;
    public const int MaxDescriptionLength = 1024;

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Number { get; set; }
    public string Description { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;
    public byte[] RowVersion { get; set; }

    public string ExtensionJson { get; set; }
}

با پیاده سازی واسط IExtendableEntity، یکسری متد الحاقی برروی اشیاء موجودیت مورد نظر فعال خواهند شد که امکان مقداردهی یا خواندن این اطلاعات اضافی را خواهید داشت. به عنوان مثال:

var task = new Task();
task.SetExtensionValue("Name","Value");
var value = task.ReadExtensionValue("Name");
//or any complex object as string json

با دو متد الحاقی استفاده شده در بالا، امکان مقداردهی، تغییر و خواندن مقدار خصوصیت‌های اضافی را خواهیم داشت که نیاز است موجودیت مورد نظر در دل خود نگهداری کند ولی ارزش و اهمیت زیادی در Domain ندارند.


مثال هشتم: طراحی یک نوع شمارشی (Enum)

public class OrderStatus : Enumeration
{
    public static OrderStatus Submitted = new OrderStatus(1, nameof(Submitted).ToLowerInvariant());
    public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant());
    public static OrderStatus StockConfirmed = new OrderStatus(3, nameof(StockConfirmed).ToLowerInvariant());
    public static OrderStatus Paid = new OrderStatus(4, nameof(Paid).ToLowerInvariant());
    public static OrderStatus Shipped = new OrderStatus(5, nameof(Shipped).ToLowerInvariant());
    public static OrderStatus Cancelled = new OrderStatus(6, nameof(Cancelled).ToLowerInvariant());

    protected OrderStatus()
    {
    }

    public OrderStatus(int id, string name)
        : base(id, name)
    {
    }
}

برای سناریوهایی که صرفا قصد انتخاب یک یا چند (حالت enum flags) مورد از بین یک لیست مشخص و سپس ذخیره سازی آنها را دارید، استفاده از نوع داده enum کفایت می‌کند؛ ولی اگر قصد استفاده از آنها برای flow control را دارید، در این صورت به طراحی شکننده‌ای خواهید رسید که پر شده است از if/else هایی که مقادیر مختلف enum مورد نظر را بررسی می‌کنند. با استفاده از کلاس Enumeration امکان مدل کردن انوع شمارشی که مرتبط هستند با منطق تجاری سیستم را با راه حل شیء گرا خواهید داشت. در این صورت رفتارهای متناظر با هریک از فیلدهای یک نوع شمارشی می‌تواند به عنوان رفتاری در دل خود کپسوله شده باشد و اینبار داده و رفتار کنار هم خواهند بود. 

نکته: برای مطالعه بیشتر می‌توانید به مطالب ^ و ^ مراجعه کنید.

در نهایت می‌توانید برای سناریوهای خاص خودتان از سایر واسط های موجود در زیرساخت، نیز به شکل زیر استفاده کنید:

نیاز به حذف نرم بدون نگهداری اطلاعات ردیابی تغییرات

public interface ISoftDeleteEntity
{
    bool IsDeleted { get; set; }
}

.با پیاده سازی واسط بالا این امکان را خواهید داشت که صرفا از مکانیزم حذف نرم استفاده کنید؛ بدون نیاز به نگهداری سایر اطلاعات

نیاز به مقداردهی خودکار زمان ثبت یک موجودیت خاص 

این امر با پیاده سازی واسط زیر امکان پذیر خواهد بود.

public interface IHasCreationDateTime
{
    DateTimeOffset CreationDateTime { get; set; }
}

با توجه به اعمال اصل ISP در مباحث مطرح شده در مطلب جاری، بنا به نیاز خود از این واسط‌ها و کلاس‌های پایه پیاده ساز آنها می‌توانید استفاده کنید.

مطالب
پیاده‌سازی میکروسرویس‌ها توسط Seneca
همانطور که در مطلب آشنایی با معماری Microservices گفته شد، Seneca یک فریم‌ورک مبتنی بر Node.js برای ساخت برنامه‌های سمت سرور بر مبنای معماری Microservices با هسته Monolithic است. در این مطلب قصد ارائه یک مثال عملی را بر اساس این فریم‌ورک ندارم. هدف، آشنایی با اجزای اصلی Seneca و چهارچوب کاری آن است.


فواید استفاده از فریم‌ورک Seneca  

  • فریم‌ورک Seneca کدنویسی برای ایجاد درخواست‌ها، ارسال پاسخ به درخواست‌های رسیده و تبدیل داده‌ها را که از روتین‌های هر سرویسی میتوانند باشند، ساده‌تر میکند.
  • فریم‌ورک Seneca با معرفی ایده Actionها و Pluginها که جلوتر توضیح داده خواهند شد، روند تبدیل کامپوننت‌های یک برنامه Monolithic را به نوع سرویسی، تسهیل میکند. روندی که به صورت عادی میتواند کاری طاقت فرسا باشد و نیاز به Refactoring زیادی دارد. 
  • سرویس‌های نوشته شده با زبانهای برنامه‌نویسی مختلف یا فریم‌ورک‌های متفاوت میتوانند با سرویس‌های نوشته شده توسط فریم‌ورک Seneca در ارتباط و تبادل باشند.


نصب فریم‌ورک Seneca

نصب Seneca تفاوتی با نصب سایر فریم‌ورک‌ها توسط npm ندارد. مثلا میشود فایلی با نام package.json را با تنظیماتی که در پی خواهد آمد داشت و با اجرای دستور npm install در خط فرمان، Seneca را نصب کرد. البته دستور نصب را باید در پوشه‌ای که فایل package.json در آن  ایجاد شده‌است، اجرا کرد.
{
    "name": "seneca-example",
    "dependencies": {
        "seneca": "0.6.5",
        "express": "latest"
    }
}
بعد برای استفاده از فریم‌ورک نصب شده باید نمونه‌ای از آن را ایجاد کنیم. 
var seneca = require("seneca")();

 Actionها

‌Actionها عملکرد بخصوصی را ارائه می‌دهند که باید به نمونه‌ای از فریم‌ورک Seneca که ایجاد کرده‌ایم، اضافه شوند. اینکه یک Action چه عملکرد بخصوصی را ارائه می‌هد، در زمان اضافه کردن آن به نمونه‌ای ایجاد شده، مشخص می‌شود. در مطلب گذشته گفته شد که برای تغییر یک برنامه Monolithic به نوع سرویسی، می‌شود فقط کامپوننت‌های دردسر ساز را تغییر داد. Actionها عملکردهای آن کامپوننت‌های مشکل ساز را به عهده خواهند گرفت. زمان اضافه کردن یک Action به نمونه‌ای از Seneca، از متد add استفاده میکنیم که دو آرگومان را دریافت میکند. اولین آرگومان، یک رشته JSON یا یک object است که هویت عملکرد Action را نشان می‌دهد و دومی در واقع یک متد Callback است و زمانیکه Action فراخوانی میشود، اجرا خواهد شد.  
seneca.add({role: "accountManagement", cmd: "login"}, function(args, respond){
});
seneca.add({role: "accountManagement", cmd: "register"}, function(args, respond){
});
در مثال بالا دو Action را به نمونه‌ای از Seneca اضافه کرده‌ایم. خواص role و cmd موارد بخصوصی نیستند. می‌شود آن‌ها را با  موارد دلخواه، بسته به عملکردی که از Action انتظار داریم، جایگزین کنیم. برای فراخوانی Actionهایی که اضافه کرده‌ایم، باید از متد act استفاده کنیم. البته متد act میتواند Actionهایی را که در سایر سرویس‌ها قرار دارند هم فراخوانی کند. اولین آرگومان act، برای تطبیق با الگوهایی که در زمان اضافه کردن Actionها به نمونه تنظیم شده‌اند، استفاده می‌شود. دومین آرگومان آن هم باز یک Callback است و در زمانیکه Action فراخوانی شده‌است اجرا میشود. 
seneca.act({role: "accountManagement", cmd: "register", username:
"parham", password: "12345!"}, function(error, response){
});

seneca.act({role: "accountManagement", cmd: "login", username:
"parham", password: "12345!"}, function(error, response){
});
در مثال بالا و در پارامتر اول، مشخصه‌های بیشتری نسبت به الگوی Actionهایی که اضافه کرده‌ایم، دیده می‌شود. این مسئله مشکلی ایجاد نمیکند و به هر حال Seneca تمامی الگوهای مشابه را شناسایی میکند و آن موردی که بیشترین تطبیق را دارد، فرا میخواند. 


Pluginها

  Pluginها در Seneca در واقع بسته بندیهایی از Actionهایی هستند که کاربرد مشابهی دارند. در مثال بالا دو Action داشتیم که یکی عملکرد ورود را برعهده دارد و دیگری ثبت‌نام. از آنجایی که هر دو برای بخش مدیریت کاربران تشابهاتی دارند، می‌شود آنها را در یک Plugin بسته بندی کرد. ارزش Pluginها زمانی است که می‌خواهیم آنها را توزیع کنیم. با توزیع Plugin، تمام Actionهای زیرمجموعه آن همزمان توزیع می‌شوند. Pluginها را میشود در قالب توابع یا ماژول‌ها ایجاد کرد.
function account(options){
    this.add({init: "account"}, function(pluginInfo, respond){
        console.log(options.message);
        respond();
    })  

    this.add({role: "accountManagement", cmd: "login"}, function(args, respond){
     }); 

    this.add({role: "accountManagement", cmd: "register"}, function(args, respond){
    });
}

seneca.use(account, {message: "Plugin Added"});
در مثال بالا از روش تابع برای ایجاد Plugin استفاده شده‌است. Plugin را ایجاد می‌کنیم و Actionهایی را که میخواهیم به Plugin اضافه شوند، توسط this اضافه می‌کنیم و در نهایت با متد use به نمونه Seneca میگوییم که از Plugin ساخته شده، استفاده کند. کلمه کلیدی This درون Plugin به نمونه‌ای از Seneca که ایجاد کرده‌ایم، ارجاع دارد. یعنی هر this.add برابر با seneca.add می‌باشد. نکته باقی مانده، Action اضافه‌ای است با مشخصه {init: "account"} که چه نقشی دارد. این Action نقش یک سازنده را برای مقداردهی اولیه، دارد. برای نمونه میشود از آن برای ارتباط با پایگاه داده، پیش از اجرا شدن سایر Actionها که نیاز به آن ارتباط دارند، استفاده کرد. مثال بالا را می‌شود با یک ماژول هم پیاده‌سازی کرد.
module.exports = function(options){
    this.add({init: "account"}, function(pluginInfo, respond){
        console.log(options.message);
        respond();
    })

    this.add({role: "accountManagement", cmd: "login"}, function(args, respond){
    });

    this.add({role: "accountManagement", cmd: "register"},function(args, respond){
    });

    return "account";
}  

seneca.use("./account.js", {message: "Plugin Added"});
 ماژول، نام Plugin را برگشت میدهد که با فرض بر اینکه ماژول در فایل account.js قرار دارد، با متد use به نمونه Seneca میگوییم که از Plugin ساخته شده استفاده کند.


Serviceها  

یک سرویس، یک نمونه از فریم‌ورک Seneca است که Actionهایی را برای استفاده بیرونی، در معرض شبکه قرار می‌دهد. 
var seneca = require("seneca")();
seneca.use("./account.js", {message: "Plugin Added"});
seneca.listen({port: "9090", pin: {role: "accountManagement"}});
در مثال بالا، نمونه‌ای از فریم‌ورک Seneca ایجاد شده، مجموعه‌ای از Actionها تحت یک Plugin به آن اضافه شده و در نهایت Actionهایی که در زمان ساخت و اضافه کردن آنها نقش accountManagement گرفته‌اند، برای استفاده در معرض شبکه و در بستر HTTP قرار می‌گیرند. هر دو بخش port و pin، اختیاری هستند و با صرفنظر از این دو بخش، اولا نمونه‌ی Seneca برای دریافت درخواست‌های ورودی، به درگاه پیش فرض 10101 گوش می‌دهد و ثانیا تمامی Actionهایی را که به نمونه اضافه کرده‌ایم، در معرض شبکه قرار میگیرند.
در سمت دیگر، برای اینکه سایر سرویس‌ها و برنامه‌های Monolithic قادر باشند Actionهای در معرض شکبه قرار داده شده را فراخوانی کنند، باید آنچه را که در معرض قرار داده شده، در خود ثبت کنند. برای مثال سرویس دیگری از Seneca می‌تواند از قطعه کد زیر استفاده کند.
seneca.client({port: "9090", pin: {role: "accountManagement"}});

در اینجا هم موارد port و pin اختیاری هستند. اگر سرویسی که میخواهیم ثبت کنیم در سرور دیگری باشد، بایستی خاصیت host و با مقدار آدرس IP سرور مورد نظر در زمان ثبت، اعمال شود. سوالی که باقی می‌ماند این است که یک سرویس چطور Actionی از یک سرویس دیگر را فراخوانی می‌کند؟

در بخش Actionها آمد که برای فراخوانی یک Action از متد act،  از نمونه‌ی Seneca استفاده میکنیم. رفتار Seneca به این صورت است که ابتدا بر اساس امضای Action درخواست شده، Actionهای محلی را که به سرویس جاری اضافه شده‌اند، جستجو میکند. اگر تطبیقی نیافت به سراغ Actionهای ثبت شده خارجی که دارای خاصیت pin هستند، خواهد رفت و در نهایت اگر آنجا هم موردی نیافت، برای تک تک سرویس‌هایی که آنها را ثبت کرده، اما خاصیت pin را ندارند، درخواستی را ارسال میکند.


برای اطلاعات بیشتر به بخش مستندات  فریم‌ورک Seneca رجوع کنید.

مطالب
کنترل شرایط تاثیرگذار بر روی یک نقش در ASP.NET MVC
در سایت جاری، مباحث زیادی در مورد دسترسی یک نقش به اکشن متدها مطرح شده است. در این مقالات یاد گرفته‌ایم اگر اکشن متدی به ویژگی Authorization مزین گردد، دسترسی این اکشن متد تنها به کاربران لاگین شده خلاصه شده و اگر پارامتر Roles را با نام نقش‌ها مقداردهی کنیم، تنها کاربرانی که آن نقش را دارند، به این اکشن متد دسترسی خواهند داشت. ولی گاهی اوقات شرایطی ایجاد میشود که مشخص نیست این نقش در حال حاضر باید دسترسی به اکشن متد مورد نظر را داشته باشد یا خیر؛ این مثال را در نظر بگیرید:
در یک سیستم حسابداری یا هر سیستم مشابهی، موقعی که یک سند در سیستم درج میشود، نقش‌های زیادی میتوانند درگیر این عمل باشند:
Create می تواند یک سند حسابداری را ایجاد کند.
Edit میتواند یک سند حسابداری را ویرایش کند.

کاربری که نقش edit را دارد توانایی ویرایش اسناد درج شده در سیستم را دارد ولی اسناد حسابداری تنها در همان روز ثبت، امکان ویرایش دارند و پس از پایان ساعاتی کاری یا از روز بعد دیگر قابل ویرایش نبوده و نباید کسی توانایی ویرایش آن را داشته باشد. به همین دلیل، در این نقطه زمانی، دیگر کاربر Edit نباید بتواند این اکشن متد را صدا بزند. به همین علت باید دسترسی او را بر روی سندهای ماقبل امروز بست.

با نگاهی بر روی بسیاری از کدهای نرم افزارهای مختلف که نوشته شده‌اند میتواند دریافت بسیاری از افراد در اکشن مربوطه یا هر دو اکشن Get و Post یک شرط قرار داده و با بررسی اینکه سند متعلق به امروز نیست پیامی را به کاربر نمایش میدهند؛ ولی این نکته باعث میشود که اکشن متد مربوطه تا حدی اولین اصل از اصول Solid یعنی اصل تک مسئولیتی را زیر سوال ببرد. ما در اینجا قصد داریم این موضوع بررسی را به خارج از اکشن متد مربوطه برده و قبل از رسیدن به اکشن متد مورد نظر جلوی آن را بگیریم.

با گردآوری و جمع بندی مطالب سایت تاکنون می‌توان تکه کد زیر را نوشت: 
در سایت جاری مباحث فیلترها (^ و ^) به طور مکرر مورد بررسی قرار گرفته‌است. طبق منابع ذکر شده یکی از این اکشن متدها ActionFilter بوده که دو متد زیر را در اختیار ما قرار می‌دهد:
// Called after the action method executes
void OnActionExecuted(ActionExecutedContext filterContext)

// Called before an action method executes
void OnActionExecuting(ActionExecutingContext filterContext)
اولین متد بعد از اجرای اکشن متد و دومی قبل از آن صدا زده میشود. از آنجا که ما نیاز داریم کاربر قبل از رسیدن به اکشن متد مسدود گردد؛ متد OnActionExecuting را بازنویسی میکنیم:
    public class BlockDocument : ActionFilterAttribute
    {
        public Func<IDocumentServices> _documentServices { get; set; };

        public string ParamName;
      
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {           
            var id = 0;
            if (filterContext.ActionParameters.ContainsKey(ParamName))
            {
                id =(int) (filterContext.ActionParameters[ParamName]??0);
            }
            if (IsInvalid(id))
            {
                return;
            } 
            var document = _documentServices().GetDocument(id);
            if (document.InserTime.IsPassed())
            {
                filterContext.Result = new HttpStatusCodeResult(403);
            }
        }
    }
در اینجا با استفاده از تزریق وابستگی‌ها و یک FilterProvider سفارشی، سرویس DocumentServices را در یک Func که باعث تاخیر در بارگذاری سرویس تا زمان استفاده از آن می‌گردد قرار میدهیم. سپس پارامتری که کد یا مشخصه‌ای از سند را داراست از ورودی دریافت کرده و بررسی می‌کنیم که آدرس مربوطه شامل این مشخصه میباشد یا خیر. در صورتیکه مشخصه صحت داشته باشد، سند را از طریق سرویس واکشی کرده و اگر زمان درج سند از محدوده زمانی خارج شده باشد، کاربر را به صفحه 403 هدایت میکنیم.
[BlockDocument(ParamName = "id")]
public ActionResult EditDocument(int id)
{
            //...
}
در نهایت اکشن متد مربوطه را به فیلتر نوشته شده مزین میکنیم.
مطالب
React 16x - قسمت 9 - ترکیب کامپوننت‌ها - بخش 3 - Lifecycle Hooks
کامپوننت‌ها در طول چرخه‌ی عمر خود، از چندین مرحله عبور می‌کنند. اولین مرحله، mount نام دارد و زمانی رخ می‌دهد که وهله‌ای از یک کامپوننت، ایجاد و به DOM افزوده شده‌است. در این حالت تعدادی متد خاص را می‌توان به کامپوننت خود اضافه کرد که به صورت خودکار توسط React فراخوانی می‌شوند. به این متدها Lifecycle Hooks می‌گویند. در طی مرحله‌ی mount، سه متد Lifecycle Hooks مخصوص constructor، render و componentDidMount قابل تعریف هستند. React این متدها را به ترتیب فراخوانی می‌کند. دومین مرحله، update نام دارد و زمانی رخ می‌دهد که state و یا props کامپوننت تغییر می‌کنند. در طی مرحله‌ی update، دو متد Lifecycle Hooks مخصوص render و componentDidUpdate قابل تعریف هستند. آخرین فاز یا مرحله، unmount نام دارد و زمانی رخ می‌دهد که کامپوننتی از DOM حذف می‌شود، مانند حذف کامپوننت Counter در قسمت‌های قبل. در طی مرحله‌ی unmount، یک متد Lifecycle Hooks مخصوص componentWillUnmount قابل تعریف است.
البته این Lifecycle Hooks ای که در اینجا نام برده شدند، بیشترین استفاده را دارند. در مستندات React مواردی دیگری نیز ذکر شده‌اند که در عمل آنچنان مورد استفاده قرار نمی‌گیرند.


مرحله‌ی Mount

در کامپوننت App، یک constructor را اضافه می‌کنیم تا بتوان مرحله‌ی Mount را بررسی کرد. این سازنده تنها یکبار در زمان وهله سازی این کامپوننت فراخوانی می‌شود. یکی از کاربردهای آن می‌تواند مقدار دهی اولیه‌ی خواص این وهله باشد. برای مثال یکی از کاربردهای آن می‌تواند مقدار دهی اولیه‌ی state بر اساس مقادیر props رسیده باشد.  در اینجا است که می‌توان خاصیت state را مستقیما مقدار دهی کرد (مانند this.state = this.props.something) و در این حالت نیازی به فراخوانی متد this.setState نیست و اگر فراخوانی شود، یک خطا را دریافت می‌کنیم. از این جهت که this.setState را تنها زمانیکه کامپوننتی رندر شده و در DOM قرار گرفته باشد، می‌توان فراخوانی کرد.

یک نکته: فراخوانی this.state = this.props.something در سازنده‌ی کلاس میسر نیست، مگر اینکه props را به صورت پارامتر به سازنده‌ی کلاس و سازنده‌ی base class توسط متد super ارسال کنیم:
  constructor(props) {
    super(props);
    console.log("App - constructor");
    this.state = this.props.something;
  }
در غیراینصورت this.props، مقدار undefined را بازگشت می‌دهد.

دومین متد lifecycle hooks ای که بررسی می‌کنیم، componentDidMount است:
class App extends Component {
  constructor() {
    super();
    console.log("App - constructor");
  }

  componentDidMount() {
    // Ajax calls
    console.log("App - mounted");
  }
این متد پس از رندر کامپوننت در DOM فراخوانی می‌شود و بهترین محلی است که از آن می‌توان برای ارسال درخواست‌های Ajaxای به سمت سرور و دریافت اطلاعات از backend، استفاده کرد و سپس setState را با اطلاعات جدید فراخوانی نمود.

سومین lifecycle hooks در مرحله‌ی mounting، متد رندر است که در اینجا به ابتدای آن، یک  console.logرا جهت بررسی بیشتر اضافه می‌کنیم:
  render() {
    console.log("App - rendered");
در این حالت اگر برنامه را اجرا کنیم، چنین خروجی را می‌توان در کنسول توسعه دهندگان مرورگر مشاهده کرد:


در اینجا ترتیب فراخوانی این متدها را مشاهده می‌کنید. ابتدا سازنده‌ی کلاس فراخوانی شده‌است. سپس در مرحله‌ی رندر، یک المان React که در DOM مجازی React قرار می‌گیرد، بازگشت داده می‌شود. سپس React این DOM مجازی را با DOM اصلی هماهنگ می‌کند. پس از آن مرحله‌ی Mount فرا می‌رسد. یعنی در این مرحله، کامپوننت در DOM اصلی قرار دارد. اینجا است که اعمال Ajax ای دریافت اطلاعات از سرور باید انجام شوند.

یک نکته: در مرحله‌ی رندر، تمام فرزندان یک کامپوننت نیز به صورت بازگشتی رندر می‌شوند. برای نمایش این ویژگی، به متد Render کامپوننت‌های NavBar، Counters و Counter، متد  console.log ای را جهت درج این مرحله در کنسول، اضافه می‌کنیم:
class Counter extends Component {
  render() {
    console.log("Counter - rendered");
//...

class Counters extends Component {
  render() {
    console.log("Counters - rendered");
//...

const NavBar = ({ totalCounters }) => {
  console.log("NavBar - rendered");
//...

یک نکته: نمی‌توان از lifecycle hooks در کامپوننت‌های بدون حالت تابعی استفاده کرد.

پس از این تغییرات و ذخیره سازی برنامه، با بارگذاری مجدد آن در مرورگر، چنین خروجی در کنسول توسعه دهندگان مرورگر ظاهر می‌شود:


همانطور که مشاهده می‌کنید، پس از فراخوانی App - rendered، تمام فرزندان کامپوننت App رندر شده‌اند و در آخر به App - mounted رسیده‌ایم.


مرحله‌ی Update

مرحله‌ی Update زمانی رخ می‌دهد که state و یا props یک کامپوننت تغییر می‌کنند. برای مثال با کلیک بر روی دکمه‌ی Increment، وضعیت کامپوننت به روز رسانی می‌شود. پس از آن فراخوانی خودکار متد رندر در صف قرار می‌گیرد. به این معنا که تمام فرزندان آن نیز قرار است مجددا رندر شوند. برای آزمایش آن، یکبار لاگ‌های کنسول توسعه دهندگان مرورگر را پاک کنید. سپس بر روی دکمه‌ی Increment کلیک کنید:


همانطور که ملاحظه می‌کنید با کلیک بر روی دکمه‌ی Increment، کل Component tree برنامه مجددا رندر شده‌است. البته این مورد به معنای به روز رسانی کل DOM اصلی در مرورگر نیست. زمانیکه کامپوننتی رندر می‌شود، فقط یک React element حاصل آن خواهد بود که در نتیجه‌ی آن DOM مجازی React به روز رسانی خواهد شد. سپس React، کپی DOM مجازی قبلی را با نمونه‌ی جدید آن مقایسه می‌کند. در آخر، محاسبه‌ی تغییرات صورت گرفته و تنها بر اساس موارد تغییر یافته‌است که DOM اصلی را به روز رسانی می‌کند. به همین جهت زمانیکه بر روی دکمه‌ی Increment کلیک می‌شود، فقط span کنار آن در DOM اصلی به روز رسانی می‌شود. برای اثبات آن در مرورگر بر روی المان span که شماره‌ها را نمایش می‌دهد، کلیک راست کرده و گزینه‌ی inspect را انتخاب کنید. سپس بر روی دکمه‌ی Increment کلیک نمائید. مرورگر قسمتی را که به روز می‌شود، با رنگی مشخص و متمایز، به صورت لحظه‌ای نمایش می‌دهد.

متد componentDidUpdate، پس از به روز رسانی کامپوننت فراخوانی می‌شود. به این معنا که در این حالت وضعیت و یا props جدیدی را داریم. در این حالت می‌توان این اشیاء به روز شده را با نمونه‌های قبلی آن‌ها مقایسه کرد و در صورت وجود تغییری، برای مثال یک درخواست Ajax ای را به سمت سرور برای دریافت اطلاعات تکمیلی ارسال کرد و در غیراینصورت خیر. بنابراین می‌توان به آن به عنوان یک روش بهینه سازی نگاه کرد. برای نمایش این قابلیت می‌توان متد componentDidUpdate را که مقادیر قبلی props و state را دریافت می‌کند، لاگ کرد:
class Counter extends Component {
  componentDidUpdate(prevProps, prevState) {
    console.log("Counter - updated", { prevProps, prevState });
    if (prevProps.counter.value !== this.props.counter.value) {
      // Ajax call and get new data
    }
  }
برای آزمایش آن، یکبار لاگ‌های کنسول توسعه دهندگان مرورگر را پاک کنید. سپس بر روی دکمه‌ی Increment اولین شمارشگر کلیک کنید:


همانطور که مشاهده می‌کنید، مقدار شیء counter، پیش از کلیک بر روی دکمه‌ی Increment، مساوی 4 بوده‌است. در یک چنین حالتی می‌توان مقدار قبلی prevProps.counter.value را با مقدار جدید this.props.counter.value مقایسه کرد و در صورت نیاز یک درخواست Ajax ای را برای دریافت اطلاعات به روز، صادر کرد.


مرحله‌ی Unmount

در این مرحله تنها یک lifecycle hook به نام componentWillUnmount قابل تعریف است که درست پیش از حذف یک کامپوننت از DOM فراخونی می‌شود.
class Counter extends Component {
  componentWillUnmount(){
    console.log("Counter - Unmount");
  }
پس از افزودن متد فوق و بارگذاری مجدد برنامه در مرورگر، یکبار دیگر لاگ‌های کنسول توسعه دهندگان مرورگر را پاک کنید. سپس اولین Counter رندر شده را حذف کنید.


در اینجا پس از حذف یک کامپوننت، state کامپوننت App تغییر کرده‌است. به همین جهت کل Component tree رندر مجدد شده‌است. اینبار یک DOM مجازی جدید را داریم که تعداد Counterهای آن 3 مورد است. سپس React این DOM مجازی جدید را با نمونه‌ی قبلی خود مقایسه کرده و متوجه می‌شود که یکی از Counterها حذف شده‌است. در ادامه متد componentWillUnmount را پیش از حذف این ‍Counter از DOM، فراخوانی می‌کند. به این ترتیب فرصت خواهیم یافت تا رهاسازی منابع را در صورت نیاز انجام دهیم تا برنامه دچار نشتی حافظه نشود.


یک مثال: افزودن دکمه‌ی Decrement به کامپوننت Counter

در ادامه می‌خواهیم دکمه‌ای را برای کاهش مقدار یک شمارشگر، به کامپوننت Counter اضافه کنیم. همچنین اگر مقدار value شمارشگر مساوی صفر بود، دکمه‌ی کاهش مقدار آن باید غیرفعال شود و برعکس. به علاوه از سیستم طرحبندی بوت استرپ نیز برای تعریف دو ستون، یکی برای نمایش مقدار شمارشگرها و دیگری برای نمایش دکمه‌ها استفاده خواهیم کرد.


برای پیاده سازی آن ابتدا متد رندر کامپوننت Counter را به صورت زیر تغییر می‌دهیم:
class Counter extends Component {

  render() {
    console.log("Counter - rendered");
    return (
      <div className="row">
        <div className="col-1">
          <span className={this.getBadgeClasses()}>{this.formatCount()}</span>
        </div>
        <div className="col">
          <button
            onClick={() => this.props.onIncrement(this.props.counter)}
            className="btn btn-secondary btn-sm"
          >
            +
          </button>
          <button
            onClick={() => this.props.onDecrement(this.props.counter)}
            className="btn btn-secondary btn-sm m-2"
            disabled={this.props.counter.value === 0 ? "disabled" : ""}
          >
            -
          </button>
          <button
            onClick={() => this.props.onDelete(this.props.counter.id)}
            className="btn btn-danger btn-sm"
          >
            Delete
          </button>
        </div>
      </div>
    );
  }
در اینجا یک row تعریف شده و سپس دو div، با کلاس‌های تعیین عرض ستون‌ها. در ادامه span نمایش شمارشگر، به div اول و دکمه‌ها به div دوم منتقل شده‌اند. همچنین marginها را هم اصلاح کرده‌ایم تا بین دکمه‌ها فضای مناسبی ایجاد شود.
در این بین، دکمه‌ی جدید کاهش مقدار را که با یک - مشخص شده‌است نیز مشاهده می‌کنید. رویدادگردان onClick آن به this.props.onDecrement اشاره می‌کند. همچنین ویژگی disabled نیز به آن اضافه شده‌است تا بر اساس مقدار value شیء counter، در مورد فعال یا غیرفعالسازی دکمه تصمیم گیری کند.
پس از آن نیاز است این this.props.onDecrement را تعریف کنیم. به همین جهت به والد آن که کامپوننت Counters است مراجعه کرده و آن‌را به صورت زیر تغییر می‌دهیم:
<Counter
  key={counter.id}
  counter={counter}
  onDelete={this.props.onDelete}
  onIncrement={this.props.onIncrement}
  onDecrement={this.props.onDecrement}
/>
در اینجا onDecrement اضافه شده‌است تا شیء this.props ارسالی به کامپوننت Counter را مقدار دهی کند. اکنون باید ارجاع به this.props.onDecrement این کامپوننت را نیز تکمیل کرد. این ارجاع نیز به والد Counters که در اینجا کامپوننت App است اشاره می‌کند:
<Counters
  counters={this.state.counters}
  onReset={this.handleReset}
  onIncrement={this.handleIncrement}
  onDecrement={this.handleDecrement}
  onDelete={this.handleDelete}
/>
در کامپوننت App، ویژگی onDecrement ارسالی به کامپوننت Counters، به صورت props مقدار دهی شده‌است. این ویژگی به متد this.handleDecrement اشاره می‌کند که به صورت زیر در کامپوننت App تعریف می‌شود:
  handleDecrement = counter => {
    console.log("handleDecrement", counter);
    const counters = [...this.state.counters]; // cloning an array
    const index = counters.indexOf(counter);
    counters[index] = { ...counter }; // cloning an object
    counters[index].value--;
    console.log("this.state.counters", this.state.counters[index]);
    this.setState({ counters });
  };
که کدهای آن با کدهای handleIncrement بحث شده‌ی در قسمت قبل یکی است. اکنون اگر برنامه را اجرا کنید، به تصویر ابتدای توضیحات این مثال خواهید رسید.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-09.zip
مطالب
امکان اجرای خودکار کدها در زمان بارگذاری اولیه‌ی یک اسمبلی در C# 9.0
C# 9.0 به همراه قابلیت جدیدی است به نام «module initializer» که در اصل متدی است که در زمان بارگذاری اولیه‌ی یک اسمبلی، به صورت خودکار اجرا می‌شود. عملکرد آن شبیه به سازنده‌های static کلاس‌ها است؛ اما بجای اعمال به یک کلاس، اینبار به کل اسمبلی اعمال می‌شود. این قابلیت از روزهای ابتدایی طراحی CLR وجود خارجی داشته‌، اما در C# 9.0، امکان استفاده‌ی عمومی از آن فراهم شده‌است.


روش تعریف یک module initializer

در مثال زیر، قالب ابتدایی یک ModuleInitializer را مشاهده می‌کنید:
namespace CS9Features
{
    using System.Runtime.CompilerServices;

    internal static class TestModuleInitializer
    {
        [ModuleInitializer]
        public static void MyModuleInitializer()
        {
            // put your module initializer here
        }
    }
}
متدی که قرار است به عنوان module initializer معرفی شود، باید مزین به ویژگی [ModuleInitializer] باشد و همچنین این متد باید دارای ویژگی‌های زیر نیز باشد:
- باید استاتیک باشد.
- باید بدون پارامتر باشد.
- باید خروجی آن void باشد.
- نباید به صورت جنریک تعریف شود.
- این متد باید در همان اسمبلی، قابل دسترسی باشد؛ یعنی سطح دسترسی آن باید یا public و یا internal باشد.
- نباید local function باشد.


می‌توان بیش از یک ModuleInitializer را در یک اسمبلی تعریف کرد

به مثال زیر دقت کنید:
namespace CS9Features
{
    using System.Runtime.CompilerServices;

    internal static class TestModuleInitializer
    {
        [ModuleInitializer]
        public static void MyModuleInitializer1()
        {
            // put your module initializer here
        }

        [ModuleInitializer]
        public static void MyModuleInitializer2()
        {
            // put your module initializer here
        }
    }
}
در اینجا بیش از یک متد ModuleInitializer تعریف شده‌اند. اگر بر روی ابتدای هر کدام از این متدها یک break-point را قرار دهید، مشاهده خواهید کرد که کدهای آن‌ها پیش از شروع متد Main برنامه اجرا می‌شوند. همچنین نحوه‌ی اجرای این متدها همواره مشخص و ترتیبی است؛ از بالا به پایین.
این مورد یکی از مهم‌ترین تفاوت‌های module initializer‌ها با سازنده‌های static است. ترتیب اجرای سازنده‌های static مشخص نیست و بر اساس کدهای کلاینت و زمان دسترسی به کلاس‌های مختلف، سازنده‌ی استاتیک کلاس A می‌تواند پس از سازنده‌ی استاتیک کلاس B اجرا شود و یا برعکس. اما همواره نحوه‌ی اجرای module initializer‌ها مشخص و ترتیبی است و همچنین نیازی به فراخوانی آن‌ها توسط هیچ کلاینتی نیست.


موارد کاربرد module initializer‌ها

نمونه‌ی بسیار پرکاربرد module initializer ها، اجرای کدهایی پیش از شروع به اجرای آزمون‌های خودکار یک برنامه‌است؛ مانند کدهایی که یک بانک اطلاعاتی را ایجاد و مقدار دهی اولیه می‌کنند و پس از آن قرار است آزمایش‌های برنامه بر روی این بانک اطلاعاتی مشخص، اجرا شوند.