بازخوردهای دوره
بررسی سیستم جدید گرید بوت استرپ 3
با تشکر و سپاس .من کد ایجاد فاصله بین ستون‌ها رو اجرا می‌کنم مثل کد شما :
<div class="container">
        <h4 class="alert alert-info">ایجاد فاصله بین ستون‌ها</h4>
        <div class="row">
            <div class="col-lg-3 col-sm-4 alert alert-danger">
             ستون اول
            </div>
            <div class="col-lg-8 col-lg-offset-1 col-sm-7 col-sm-offset-1 alert alert-success">
             ستون دوم
            </div>
        </div>   <!-- end row -->
</div>
اما خروجی از نظر ظاهری متفاوت هست :(در حالت lg)

ممکنه راهنمائی بفرمائید
مطالب
چگونگی استفاده از افزونه Isotope در AngularJS

حتما تا به حال در وب سایت‌های زیادی قسمت هایی را دیده اید که چیدمان عناصر آن به شکل زیر است:

این گونه چیدمان را حتما در منوی Start ویندوز 8 بار‌ها دیده‌اید! عناصر تشکیل دهنده‌ی این شکل از چیدمان، می‌توانند یک سری عکس باشند که تشکیل یک گالری عکس را داده‌اند و یا یک سری div که محتوای پست‌های یک وبلاگ را در خود جای داده‌اند. چیزی که این شکل از چیدمان عناصر را نسبت به چیدمان‌های معمول متمایز می‌کند این است که طول و عرض هر یک از این عناصر با یکدیگر متفاوت است و هدف از این گونه چیدمان آن است که این عناصر در فضایی که به آن‌ها اختصاص داده شده است، به صورت بهینه قرار گیرند تا کمترین فضا هدر رود.

برای اعمال این شکل از چیدمان در دنیای وب افزونه‌های زیادی بر فراز کتاب خانه‌ی jQuery تدارک دیده شده است که از جمله مطرح‌ترین آن‌ها می‌توان به افزونه های Isotope ، Masonry و Gridster  اشاره کرد.

افزونه‌ی Isotope مزایایی را برای من در پی داشت و این افزونه را برای انجام کارهای خود، مناسب دیدم. نکته‌ی مهم اینجا است که هدف من بررسی Isotope نیست، چرا که اگر به وب سایت آن مراجعه کنید، با کوهی از مستندات مواجه می‌شوید که چگونه از آن در وب سایت‌های معمولی استفاده کنید.

در این مقاله قصد من این است که نشان دهم چگونه از افزونه‌ی Isotope در AngularJS استفاده کنیم؛ چگونه چیدمان آن را راست به چپ کنیم و چگونه آن را با محیط‌های واکنش گرا (Responsive) سازگار کنیم.

فرض کنید در یک وب سایت قصد داریم اطلاعات یک سری مطلب خبری را از سرور، به فرمت JSON دریافت کرده و نمایش دهیم. در AngularJS شیوه‌ی کار بدین صورت است که اطلاعاتی که به فرمت JSON هستند را با استفاده از directive ایی به نام ng-repeat پیمایش کرده و آن‌ها را نمایش دهیم.  حال اگر بخواهیم چیدمان مطالب را با استفاده از Isotope تغییر دهیم، می‌بینیم که هیچ چیزی نمایش داده نمی‌شود. دلیل آن بر می‌گردد به مراحل کامپایل کردن AngularJS و نامشخص بودن زمان اعمال چیدمان Isotope به عناصر است.

در AngularJS هنگامیکه با دستکاری DOM سر و کار پیدا می‌کنیم، معمولا باید به سراغ Directive‌ها رفت و یک Directive سفارشی برای کار با Isotope تعریف کرد تا با مکانیزم‌های Angular سازگار باشد. خوشبختانه Directive Isotope برای Angular موجود می‌باشد. نکته‌ی مهم این است که این Directive برای نگارش 1 افزونه‌ی Isotope نوشته شده است. البته با نگارش 2 هم کار می‌کند که من برای انجام کار خود نسخه‌ی 1 را ترجیح دادم استفاده کنم.

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

برای اینکه در هنگام جابه جا شدن عناصر، انیمیشن‌ها نیز از راست به چپ انجام شوند، باید css‌های زیر را نیز اعمال نمود:

.isotope .isotope-item {
  -webkit-transition-property: right, top, -webkit-transform, opacity;
     -moz-transition-property: right, top, -moz-transform, opacity;
      -ms-transition-property: right, top, -ms-transform, opacity;
       -o-transition-property: right, top, -o-transform, opacity;
          transition-property: right, top, transform, opacity;
}

Responsive بودن این عناصر مسئله‌ی دیگری است که باید حل گردد. امروزه اکثر فریم ورک‌های مطرح css، واکنشگرا نیز هستند و برای پشتیبانی از سایز‌های متفاوت صفحه نمایش، تدابیری در نظر گرفته‌اند. اساس کار واکنش گرا بودن این فریم ورک‌ها در تعیین ابعاد عناصر، بیان ابعاد به صورت درصدی است. مثلا فلان عرض div برابر 50% باشد بدین معناست که همیشه عرض این div نصف عرض عنصر والد آن باشد.

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

  برای حل این مشکل می‌توان از امکانات CSS به مانند دستورات زیر استفاده کرد: 
@media (min-width: 768px) and (max-width: 980px) {
    .card {
        width: 320px;
    }
}

@media (min-width: 980px) and (max-width: 1200px) {
    .card {
        width: 260px;
    }
}

@media (min-width: 1200px) {
    .card {
        width: 340px;
    }
}
بدین صورت می‌توان در ابعاد مختلف نمایشگر تعیین کرد که عرض عناصر ما چقدر باشد.
اکنون یک گالری عکس را در نظر بگیرید که در زیر هر عکس توضیحی نیز نوشته شده است و ساختار HTML آن به این صورت است که داخل هر div عکسی نیز موجود است. اگر به شیوه‌ی ذکر شده عمل کنید با یک اشکال مواجه می‌شوید و عناصر روی هم قرار گرفته و اصطلاحا overlapping اتفاق می‌افتد. دلیل این امر این است که لود شدن عکس‌ها عملی زمان گیر است و Isotope قبل از این که عکس لود شود، سایز آن عنصر را محاسبه کرده که در حقیقت این سایز بدون احتساب سایز عکس است و ابعاد واقعی عنصر ما نیست؛ در نتیجه وقتی عکس لود می‌شود آن div فضای بیشتری احتیاج دارد و به همین دلیل به زیر div‌های دیگر می‌رود.
برای حل این مشکل باید به این صورت عمل کرد که وقتی عکس‌ها کامل لود شدند، Isotope وارد عمل شده و سایز عناصر را به دست آورده و آن‌ها را بچیند. برای این کار معمولا از افزونه‌ی  imagesLoaded استفاده می‌کنند که با کمک این افزونه می‌توان مشخص کرد که وقتی تمام عکس‌های موجود در فلان div کامل لود شدند، Isotope وارد عمل شده و عناصر را چیدمان کند.
البته بدون استفاده از افزونه‌ی imagesLoaded و به کمک امکانات AngularJS و تعریف یک Directive سفارشی می‌توان زمان لود شدن عکس‌ها را کنترل کرد.
app.directive('imageOnload', function () {
            return {
                restrict: 'A',
                link: function (scope, element, attrs) {
                    element.bind('load', function () {
                        scope.$emit('iso-method', { name: 'reLayout', params: null }); // call reLayout isotope methode prevent overlaaping the items
                    });
                }
            };
        });
کار این directive این است که به ازای بارگذاری هر عکس، متد reLayout را از Isotope، فراخوانی می‌کند. از این جهت فراخوانی reLayout به ازای لود شدن هر عکس بهتر است که لود شدن تمامی عکس‌ها ممکن است مدت زمان زیادی طول بکشد و کاربر برای مدتی با یک ساختار بهم ریخته مواجه شود.
    
اگر در نمونه کدی که قرار داده‌ام، به انتهای کدهای کنترلر ListController دقت کنید، برای رویداد resize شی window، تابعی تعریف شده است تا به هنگام تغییر سایز صفحه فراخوانی شود. در این رویداد هر بار که سایز پنجره تغییر کرد، پس از یک ثانیه تابع reLayout  افزونه‌ی Isotope را فراخوانی می‌کنیم تا مجددا المنت‌های صفحه چیده شوند. البته ضرورتی وجود نداشته ولی در بعضی مواقع عناصر خوب چیده نمی‌شدند که با فراخوانی reLayout از چیدمان صحیح عناصر مطابق با سایز جدید صفحه اطمینان حاصل پیدا می‌کنیم. دلیل یک ثانیه تاخیر این است که اگر به ساز و کار تعاریف متد‌ها در directive Isotope دقت کنید، از سرویس timeout$  به وفور استفاده شده است. ظاهرا اگر برای فراخوانی reLayout زودتر عمل کنیم با فراخوانی هایی این متد در ساختار خودش تداخل پیدا می‌کند.
$(window).resize(function () {
                $timeout(function myfunction() {
                    $scope.$broadcast('iso-method', { name: 'reLayout', params: null }); // call reLayout isotope methode prevent overlaaping the items
                },1000);
                
            });
   
در نهایت تمامی نکات گفته شده را به صورت یک نمونه کد آماده کردم:
   

نظرات مطالب
طرحبندی صفحات وب با بوت استرپ 4 - قسمت سوم
یک نکته‌ی تکمیلی: معادل‌های pull-left و pull-right بوت استرپ 3 در نگارش 4 آن

بجای pull-right می‌توان از float-right و بجای pull-left از float-left استفاده کرد.
<div class="clearfix">
  <span class="float-left">Float left</span>
  <span class="float-right">Float right</span>
</div>
اگر پاسخ نگرفتید، روش flexbox توضیح داده شده مانند d-flex justify-content-end را آزمایش کنید.
<div class="col-lg-6 d-flex justify-content-end">
روش دیگر آن استفاده از auto margins است؛ مانند ml-auto:
<ul class="nav">
   <li><a href class="nav-link">Link</a></li>
   <li><a href class="nav-link">Link</a></li>
   <li class="ml-auto"><a href class="nav-link">Right</a></li>
</ul>
مطالب
شروع به کار با DNTFrameworkCore - قسمت 6 - پیاده‌سازی عملیات CRUD موجودیت‌ها با استفاده از ASP.NET Core MVC
به عنوان آخرین قسمت مرتبط با تفکر مبتنی‌بر CRUD‏ ‎(‎CRUD-based thinking)‎، روش پیاده‌سازی عملیات CRUD موجودیت‌ها را با استفاده از ASP.NET Core MVC و افزونه jquery-unobtrusive-ajax بررسی خواهیم کرد. 


برای شروع لازم است بسته نیوگت زیر را نصب کنید: 
PM> Install-Package DNTFrameworkCore.Web

قراردادها، مفاهیم و نکات اولیه 
  1. Refactor کردن فرم‌های ثبت و ویرایش مرتبط با یک Aggregate، به یک PartialView که با یک ViewModel کار می‌کند. برای موجودیت‌های ساده و پایه، همان Model/DTO، به عنوان Model متناظر با یک ویو یا به اصطلاح ViewModel استفاده می‌شود؛ ولی برای سایر موارد، از مدلی که نام آن با نام موجودیت + کلمه ModalViewModel یا FormViewModel تشکیل می‌شود، استفاده خواهیم کرد.
  2. یک فرم، در قالب یک پارشال‌ویو، به صورت Ajaxای با استفاده از افزونه  jquery-unobtrusive-ajax بارگذاری شده و به سرور ارسال خواهد شد.
  3. یک فرم براساس طراحی خود می‌تواند در قالب یک مودال باز شود، یا به منظور inline-editing آن را بارگذاری و به قسمتی از صفحه که مدنظرتان می‌باشد اضافه شود.
  4. وجود ویو Index به همراه پارشال‌ویو ‎_List برای نمایش لیستی و یک پارشال‌ویو برای عملیات ثبت و ویرایش الزامی ‌می‌باشد. البته اگر از مکانیزمی که در مطلب « طراحی یک گرید با jQuery Ajax و ASP.NET MVC به همراه پیاده سازی عملیات CRUD»  مطرح شد، استفاده نمی‌کنید و نیاز دارید تا اطلاعات صفحه‌بندی شده، مرتب شده و فیلتر شده‌ای را در قالب JSON دریافت کنید، از اکشن‌متد ReadPagedList کنترلر پایه استفاده کنید.

مثال اول: پیاده‌سازی BlogsController و طراحی فرم ثبت و ویرایش
کار با ارث‌بری از کنترلر پایه طراحی شده در زیرساخت به شکل زیر آغاز می‌شود:
public class BlogsController : CrudController<IBlogService, int, BlogModel>
{
    public BlogsController(IBlogService service) : base(service)
    {
    }

    protected override string CreatePermissionName => PermissionNames.Blogs_Create;
    protected override string EditPermissionName => PermissionNames.Blogs_Edit;
    protected override string ViewPermissionName => PermissionNames.Blogs_View;
    protected override string DeletePermissionName => PermissionNames.Blogs_Delete;
    protected override string ViewName => "_BlogModal";
}
در اینجا مشخص کردن ViewName متناظر با موجودیت جاری، الزامی می‌باشد. همانطور که عنوان شد، یک پارشال‌ویو برای عملیات ثبت و ویرایش کفایت می‌کند و طراحی پشت این کنترلر پایه نیز بر این اساس می‌باشد. گام بعدی، طراحی پارشال‌ویو مذکور می‌باشد. 

<div>
    <h4 asp-if="Model.IsNew()">Create New Blog</h4>
    <h4 asp-if="!Model.IsNew()">Edit Blog</h4>
    <button type="button" data-dismiss="modal">&times;</button>
</div>
در اینجا با استفاده از تگ‌هلپر asp-if موجود در زیرساخت و متد IsNew متناظر با مدل جاری، عنوان نمایشی مودال را مشخص کرده‌ایم. 
 ‌


<form asp-action="@(Model.IsNew() ? "Create" : "Edit")" asp-controller="Blogs" asp-modal-form="BlogForm">
    <div>
        <input type="hidden" name="save-continue" value="true"/>
        <input asp-for="RowVersion" type="hidden"/>
        <input asp-for="Id" type="hidden"/>
        <div>
            <div>
                <label asp-for="Title"></label>
                <input asp-for="Title" autocomplete="off"/>
                <span asp-validation-for="Title"></span>
            </div>
        </div>
        <div>
            <div>
                <label asp-for="Url"></label>
                <input asp-for="Url" type="url"/>
                <span asp-validation-for="Url"></span>
            </div>
        </div>
    </div>

...

</form>
با استفاده از متد IsNew مدل جاری، اکشن متد متناظر با درخواست POST مشخص شده و سپس از طریق تگ‌هلپر asp-modal-form، فرآیند افزودن ویژگی‌های data-ajax به فرم را خودکار کرده‌ایم. سپس با استفاده از یک Hidden Input با نام save-continue و مقدار true به فرم، عملیات ذخیره کردن بدون بسته شدن فرم را شبیه سازی کرده‌ایم؛ این Input می‌تواند به صورت شرطی به فرم اضافه شود (دکمه‌های ذخیره/ذخیره و بستن و ...) که در سمت سرور با توجه به مقدار ارسالی تصمیم گرفته می‌شود که دوباره فرم با اطلاعات جدید (در اینجا Id و RowVersion) به کلاینت ارسال شود یا خیر.

<div>
    <a asp-modal-delete-link asp-model-id="@Model.Id" asp-modal-toggle="false"
       asp-controller="Blogs" asp-action="Delete" asp-if="!Model.IsNew()" asp-permission="@PermissionNames.Blogs_Delete"
       title="Delete Blog">
        <i></i>
    </a>
    <a title="Refresh Blog" asp-if="!Model.IsNew()" asp-modal-link asp-modal-toggle="false"
       asp-controller="Blogs" asp-action="Edit" asp-route-id="@Model.Id">
        <i></i>
    </a>
    <a title="New Blog" asp-modal-link asp-modal-toggle="false"
       asp-controller="Blogs" asp-action="Create">
        <i></i>
    </a>
    <button type="button" data-dismiss="modal">
        <i></i>&nbsp; Cancel
    </button>
    <button type="submit">
        <i></i>&nbsp; Save Changes
    </button>
</div>
بخش فوتر این مودال در برگیرنده دکمه‌های متناظر با اکشن‌های قابل انجام بر روی موجودیت جاری می‌باشد.
در این فرم ابتدا 3 دکمه میانبر برای عملیات حذف، بازنشانی و جدید درنظر گرفته شده‌اند؛ برای حذف که از طریق تگ‌هلپر asp-modal-delete-link کار افزودن خودکار ویژگی‌های data-ajax انجام شده و در نهایت با توجه به اکشن‌متد و کنترلر مشخص شده، یک مودال، برای گرفتن تأیید از کاربر باز می‌شود و در زمان پذیرش درخواست، حذف مدل جاری به سرور ارسال خواهد شد. با استفاده از تگ‌هلپر asp-permission نیز دسترسی مورد نیاز برای مشاهده دکمه مورد نظر را مشخص کرده‌ایم. دکمه‌های بازنشانی و جدید، در اینجا از طریق تگ‌هلپر asp-modal-link تبدیل به یک لینک Ajaxای شده است که کار بارگذاری یک پارشال‌ویو را انجام می‌دهند؛ همچنین با استفاده از ویژگی asp-modal-toggle متناظر با تگ‌هلپر مذکور با مقدار false، مشخص کرده‎ایم که صرفا محتوای مودال جاری جایگزین شود و نیاز به گشوده شدن دوباره آن نیست.


خروجی نهایی در حالت ویرایش به شکل زیر می‌باشد:

‌‌
مثال دوم: پیاده‌سازی RolesController و طراحی فرم ثبت و ویرایش
‌‌نکته قابل توجه در طراحی فرم‌های ثبت و ویرایش موجودیت‌های پیچیده‌تر مربوط است به ارسال اطلاعات اضافه‌تر از هر آنچه در مدل وجود دارد به کلاینت و همچنین دریافت اطلاعات در ساختار تودرتو یا به اصطلاح یک گراف از اشیاء. به عنوان مثال برای ثبت یک گروه کاربری نیاز است لیست دسترسی‌ها را نیز در فرم نمایش دهیم تا توسط کاربر انتخاب شود؛ در این حالت نیاز است یک مدل متناظر و متناسب را به شکل زیر طراحی کرد:
public class RoleModalViewModel : RoleModel
{
    public IReadOnlyList<LookupItem> PermissionList { get; set; }
}
در اینجا با ارث‌بری از RoleModel متناظر با موجودیت گروه کاربری، لیست دسترسی‌ها نیز در طریق خصوصیت PermissionList قابل استفاده می‌باشد. حال برای مقداردهی خصوصیت اضافه شده و به طور کلی برای ارسال اطلاعات اضافه به کلاینت در زمان بارگذاری فرم، نیاز است متد RenderView را به شکل زیر بازنویسی کنید:
protected override IActionResult RenderView(RoleModel role)
{
    var model = _mapper.Map<RoleModalViewModel>(role);
    model.PermissionList = ReadPermissionList();

    return PartialView(ViewName, model);
}
به عنوان مثال در اینجا از AutoMapper برای وهله سازی از RoleModalViewModel و نگاشت خصوصیات RoleModel، استفاده شده است. به این ترتیب خروجی نهایی در حالت ویرایش به شکل زیر می‌باشد:



برای مدیریت سناریوهای Master-Detail به مانند قسمت مدیریت دسترسی‌ها در تب Permissions فرم بالا، امکاناتی در زیرساخت تعبیه شده است ولی پیاده‌سازی آن را به عنوان یک تمرین و با توجه به سری مطالب «Editing Variable Length Reorderable Collections in ASP.NET MVC» به شما واگذار می‌کنم.


نکته تکمیلی: برای ارسال اطلاعات اضافی به ویو Index متناظر با یک موجودیت می‌توانید متد RenderIndex را به شکل زیر بازنویسی کنید:

protected override IActionResult RenderIndex(IPagedQueryResult<RoleReadModel> model)
{
    var indexModel = new RoleIndexViewModel
    {
        Items = model.Items,
        TotalCount = model.TotalCount,
        Permissions = ReadPermissionList()
    };

    return Request.IsAjaxRequest()
        ? (IActionResult) PartialView(indexModel)
        : View(indexModel);
}

مدل RoleIndexViewModel استفاده شده در تکه کد بالا نیز به شکل زیر خواهد بود:

public class RoleIndexViewModel : PagedQueryResult<RoleReadModel>
{
    public IReadOnlyList<LookupItem> Permissions { get; set; }
}


فرآیند بارگذاری یک پارشال‌ویو در مودال

به عنوان مثال برای استفاده از مودال‌های بوت استرپ، ایده کار به این شکل است که یک مودال را به شکل زیر در فایل Layout قرار دهید:

<div class="modal fade" @*tabindex="-1"*@ id="main-modal" data-keyboard="true" data-backdrop="static" role="dialog" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered" role="document">
        <div class="modal-content">
            <div class="modal-body">
                Loading...
            </div>
        </div>
    </div>
</div>

سپس در زمان کلیک بروی یک دکمه Ajaxای، ابتدا main-modal را نمایش داده و بعد از دریافت پارشال‌ویو از سرور، آن را با محتوای modal-content جایگزین می‌کنیم. به همین دلیل Tag Halperهای مطرح شده در مطلب جاری، callbackهای failure/complete/success متناظر با unobtrusive-ajax را نیز مقداردهی می‌کنند. برای این منظور نیاز است تا متدهای جاوااسکریپتی زیر نیز در سطح شیء window تعریف شده باشند:‌‌

    /*----------------------------------  asp-modal-link ---------------------------*/
    window.handleModalLinkLoaded = function (data, status, xhr) {
        prepareForm('#main-modal.modal form');
    };
    window.handleModalLinkFailed = function (xhr, status, error) {
        //....
    };
    /*----------------------------------  asp-modal-form ---------------------------*/
    window.handleModalFormBegin = function (xhr) {
        $('#main-modal a').addClass('disabled');
        $('#main-modal button').attr('disabled', 'disabled');
    };
    window.handleModalFormComplete = function (xhr, status) {
        $('#main-modal a').removeClass('disabled');
        $('#main-modal button').removeAttr('disabled');
    };
    window.handleModalFormSucceeded = function (data, status, xhr) {
        if (xhr.getResponseHeader('Content-Type') === 'text/html; charset=utf-8') {
            prepareForm('#main-modal.modal form');
        } else {
            hideMainModal();
        }
    };
    window.handleModalFormFailed = function (xhr, status, error, formId) {
        if (xhr.status === 400) {
            handleBadRequest(xhr, formId);
        }
    };


برای بررسی بیشتر، پیشنهاد می‌کنم پروژه DNTFrameworkCore.TestWebApp موجود در مخزن این زیرساخت را بازبینی کنید. 
مطالب
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
نظرات مطالب
استفاده از pjax بجای ajax در ASP.NET MVC
با سلام
از کمک شما ممنون
بالاخره خطا رو پیدا کردم
The following sections have been defined but have not been rendered for the layout page "~/Views/Shared/_PjaxLayout.cshtml": "Scripts".
ولی دلیلش چی می‌تونه باشه مگه فقط نمیاد قسمت مثلا main در کد زیر را جایگذاری کنه؟
<div id="main">
            @RenderBody()
        </div>
//********** @Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@RenderSection("Scripts", required: false)
و برای فراخوانی لینک‌های pjax نوشته شده:
<script type="text/javascript">
        $(function () {
            $(document).pjax('a[withpjax]', '#main', { timeout: 5000 });

و لینک هم به اینصورت:
@Html.ActionLink("ارتباط با ما","Contact", "Home"
                                      , null,new { withpjax="with-pjax" })

اشتراک‌ها
بررسی تغییرات ASP.NET Core در NET 8 Preview 6.

Here’s a summary of what’s new in this preview release:

  • Improved startup debugging experience
  • Blazor
    • Form model binding & validation with server-side rendering
    • Enhanced page navigation & form handling
    • Preserve existing DOM elements with streaming rendering
    • Specify component render mode at the call site
    • Interactive rendering with Blazor WebAssembly
    • Sections improvements
    • Cascade query string values to Blazor components
    • Blazor Web App template option for enabling server interactivity
    • Blazor template consolidation
  • Metrics
    • Testing metrics in ASP.NET Core apps
    • New, improved, and renamed counters
  • API authoring
    • Complex form binding support in minimal APIs
  • Servers & middleware
    • HTTP.sys kernel response buffering
    • Redis-based output-cache
     
بررسی تغییرات ASP.NET Core در NET 8 Preview 6.
مطالب
بررسی تغییرات Blazor 8x - قسمت دهم - مدیریت حالت کاربران در روش‌های مختلف رندر
رفتار Blazorهای پیش از دات‌نت 8 در مورد مدیریت حالت

پیش از دات نت 8، دو حالت عمده برای توسعه‌ی برنامه‌های Blazor وجود داشت: Blazor Server و Blazor WASM. در هر دو حالت، طول عمر سیستم تزریق وابستگی‌های ایجاد و مدیریت شده‌ی توسط Blazor، معادل طول عمر برنامه‌است.

در برنامه‌های Blazor Server، طول عمر سیستم تزریق وابستگی‌ها، توسط ASP.NET Core قرار گرفته‌ی بر روی سرور مدیریت شده و نمونه‌های ایجاد شده‌ی سرویس‌های توسط آن، به ازای هر کاربر متفاوت است. بنابراین اگر طول عمر سرویسی در اینجا به صورت Scoped تعریف شود، این سرویس فقط یکبار در طول عمر برنامه، به ازای یک کاربر جاری برنامه، تولید و نمونه سازی می‌شود. در این مدل برنامه‌ها، سرویس‌هایی با طول عمر Singleton، بین تمام کاربران به اشتراک گذاشته می‌شوند. به همین جهت است که در این نوع برنامه‌ها، مدیریت سرویس Context مخصوص EF-Core‌ نکات خاصی را به همراه دارد. چون اگر بر اساس سیستم پیش‌فرض تزریق وابستگی‌ها و طول عمر Scoped این سرویس عمل شود، یک Context فقط یکبار به‌ازای یک کاربر، یکبار نمونه سازی شده و تا پایان طول عمر برنامه، بدون تغییر زنده نگه داشته می‌شود؛ در حالیکه عموم توسعه دهندگان EF-Core تصور می‌کنند سرویس‌های Scoped، پس از پایان یک درخواست، پایان یافته و Dispose می‌شوند، اما در اینجا پایان درخواستی نداریم. یک اتصال دائم SignalR را داریم و تا زمانیکه برقرار است، یعنی برنامه زنده‌است. بنابراین در برنامه‌های Blazor Server، سرویس‌های Scoped، به ازای هر کاربر، همانند Singleton رفتار می‌کنند (در سراسر برنامه به ازای یک کاربر در دسترس هستند) و سرویس‌هایی از اساس Singleton، بین تمام کاربران به اشتراک گذاشته می‌شوند.

در برنامه‌های Blazor WASM، طول عمر سیستم تزریق وابستگی‌ها، توسط برنامه‌ی وب‌اسمبلی در حال اجرای بر روی مرورگر مدیریت می‌شود. یعنی مختص به یک کاربر بوده و طول عمر آن وابسته‌است به طول عمر برگه‌ی جاری مرورگر. بنابراین دراینجا بین سرویس‌های Scoped و Singleton، تفاوتی وجود ندارد و همانند هم رفتار می‌کنند (هر دو مختص به یک کاربر و وابسته به طول عمر برگه‌ی جاری هستند).

در هیچکدام از این حالت‌ها، امکان دسترسی به HttpContext وجود ندارد (نه داخل اتصال دائم SignalR برنامه‌های Blazor Server و نه داخل برنامه‌ی وب‌اسمبلی در حال اجرای در مرورگر). اطلاعات بیشتر
بنابراین در این برنامه‌ها برای نگهداری اطلاعات کاربر لاگین شده‌ی به سیستم و یا سایر اطلاعات سراسری برنامه، عموما از سرویس‌هایی با طول عمر Scoped استفاده می‌شود که در تمام قسمت‌های برنامه به ازای هر کاربر، قابل دسترسی هستند.

 
رفتار Blazor 8x در مورد مدیریت حالت

هرچند دات نت 8 به همراه حالت‌های رندر جدیدی است، اما هنوز هم می‌توان برنامه‌هایی کاملا توسعه یافته بر اساس مدل‌های قبلی Blazor Server و یا Blazor WASM را همانند دات‌نت‌های پیش از 8 داشت. بنابراین اگر تصمیم گرفتید که بجای استفاده از جزیره‌های تعاملی، کل برنامه را به صورت سراسری تعاملی کنید، همان نکات قبلی، در اینجا هم صادق هستند و از لحاظ مدیریت حالت، تفاوتی نمی‌کنند.

اما ... اگر تصمیم گرفتید که از حالت‌های رندر جدید استفاده کنید، مدیریت حالت آن متفاوت است؛ برای مثال دیگر با یک سیستم مدیریت تزریق وابستگی‌ها که طول عمر آن با طول عمر برنامه‌ی Blazor یکی است، مواجه نیستیم و حالت‌های زیر برای آن‌ها متصور است:

حالت رندر: صفحات رندر شده‌ی در سمت سرور یا Server-rendered pages
مفهوم: یک صفحه‌ی Blazor که در سمت سرور رندر شده و HTML نهایی آن به سمت مرورگر کاربر ارسال می‌شود. در این حالت هیچ اتصال SignalR و یا برنامه‌ی وب‌اسمبلی اجرا نخواهد شد.
عواقب: طول عمر سرویس‌های Scoped، به‌محض پایان رندر صفحه در سمت سرور، پایان خواهند یافت.
بنابراین در این حالت طول عمر یک سرویس Scoped، بسیار کوتاه است (در حد ابتدا و انتهای رندر صفحه). همچنین چون برنامه در سمت سرور اجرا می‌شود، دسترسی کامل و بدون مشکلی را به HttpContext دارد.
صفحات SSR، بدون حالت (stateless) هستند؛ به این معنا که حالت کاربر در بین هدایت به صفحات مختلف برنامه ذخیره نمی‌شود. به آن‌ها می‌توان از این لحاظ به‌مانند برنامه‌های MVC/Razor pages نگاه کرد. در این حالت اگر می‌خواهید حالت کاربران را ذخیره کنید، استفاده از کوکی‌ها و یا سشن‌ها، راه‌حل متداول اینکار هستند.

حالت رندر: صفحات استریمی (Streamed pages)
مفهوم: یک صفحه‌ی Blazor که در سمت سرور رندر شده و قطعات آماده شده‌ی HTML آن به صورت استریمی از داده‌ها، به سمت مرورگر کاربر ارسال می‌شوند. در این حالت هیچ اتصال SignalR و یا برنامه‌ی وب‌اسمبلی اجرا نخواهد شد.
عواقب: طول عمر سرویس‌های Scoped، به‌محض پایان رندر صفحه در سمت سرور، پایان خواهند یافت.
بنابراین در این حالت طول عمر یک سرویس Scoped، بسیار کوتاه است (در حد ابتدا و انتهای رندر صفحه). همچنین چون برنامه در سمت سرور اجرا می‌شود، دسترسی کامل و بدون مشکلی را به HttpContext دارد.

حالت رندر: Blazor server page
مفهوم: یک صفحه‌ی Blazor Server که یک اتصال دائم SignalR را با سرور دارد.
عواقب: طول عمر سرویس‌های Scoped، معادل طول عمر اتصال SignalR است و با قطع این اتصال، پایان خواهند یافت. این نوع برنامه‌ها اصطلاحا stateful هستند و از لحاظ دسترسی به حالت کاربر، تجربه‌ی کاربری همانند یک برنامه‌ی دسکتاپ را ارائه می‌دهند.
در این نوع برنامه‌ها و درون اتصال SignalR، دسترسی به HttpContext وجود ندارد.

حالت رندر: Blazor wasm page
مفهوم: صفحه‌ای که به کمک فناوری وب‌اسمبلی، درون مرورگر کاربر اجرا می‌شود.
عواقب: طول عمر سرویس‌های Scoped، معادل طول عمر برگه و صفحه‌ی جاری است و با بسته شدن آن، پایان می‌پذیرد. این نوع برنامه‌ها نیز اصطلاحا stateful هستند و از لحاظ دسترسی به حالت کاربر، تجربه‌ی کاربری همانند یک برنامه‌ی دسکتاپ را ارائه می‌دهند (البته فقط درون مروگر کاربر).
در این نوع برنامه‌ها، دسترسی به HttpContext وجود ندارد.

حالت رندر: جزیره‌ی تعاملی Blazor Server و یا Blazor server island
مفهوم: یک کامپوننت Blazor Server که درون یک صفحه‌ی دیگر (که عموما از نوع SSR است) قرار گرفته و یک اتصال SignalR را با سرور برقرار می‌کند.
عواقب: طول عمر سرویس‌های Scoped، معادل طول عمر اتصال SignalR است و با قطع این اتصال، پایان خواهند یافت؛ برای مثال کاربر به صفحه‌ای دیگر در این برنامه مراجعه کند. بنابراین این نوع کامپوننت‌ها هم تا زمانیکه کاربر در صفحه‌ی جاری قرار دارد، stateful هستند.
در این نوع برنامه‌ها و درون اتصال SignalR، دسترسی به HttpContext وجود ندارد.

حالت رندر: جزیره‌ی تعاملی Blazor WASM و یا Blazor wasm island
مفهوم: یک کامپوننت Blazor WASM که درون یک صفحه‌ی دیگر (که عموما از نوع SSR است) توسط فناوری وب‌اسمبلی، درون مرورگر کاربر اجرا می‌شود.
عواقب: طول عمر سرویس‌های Scoped، معادل مدت زمان فعال بودن صفحه‌ی جاری است. به محض اینکه کاربر به صفحه‌ای دیگر مراجعه و این کامپوننت دیگر فعال نباشد، طول عمر آن خاتمه خواهد یافت. بنابراین این نوع کامپوننت‌ها هم تا زمانیکه کاربر در صفحه‌ی جاری قرار دارد، stateful هستند (البته این حالت درون مرورگر کاربر مدیریت می‌شود و نه در سمت سرور).
در این نوع برنامه‌ها، دسترسی به HttpContext وجود ندارد.


نتیجه‌گیری

همانطور که مشاهده می‌کنید، در صفحات SSR، دسترسی کاملی به HttpContext سمت سرور وجود دارد (که البته کوتاه مدت بوده و با پایان رندر صفحه، خاتمه خواهد یافت؛ حالتی مانند صفحات MVC و Razor pages)، اما در جزایر تعاملی واقع در آن‌ها، خیر.
مساله‌ی مهم در اینجا، مدیریت اختلاط حالت صفحات SSR و جزایر تعاملی واقع در آن‌ها است. مایکروسافت جهت پیاده سازی اعتبارسنجی و احراز هویت کاربران در Blazor 8x و برای انتقال حالت به این جزایر، از دو روش Root-level cascading values و سرویس PersistentComponentState استفاده کرده‌است که آن‌ها را در دو قسمت بعدی، با توضیحات بیشتری بررسی می‌کنیم.
مطالب
React 16x - قسمت 28 - احراز هویت و اعتبارسنجی کاربران - بخش 3 - فراخوانی منابع محافظت شده و مخفی کردن عناصر صفحه
ارسال خودکار هدرهای ویژه‌ی Authorization، به سمت سرور

در برنامه‌ی backend این سری (که از انتهای مطلب قابل دریافت است)، به Controllers\MoviesController.cs مراجعه کرده و متدهای Get/Delete/Create آن‌را با فیلتر [Authorize] مزین می‌کنیم تا دسترسی به آن‌ها، تنها به کاربران لاگین شده‌ی در سیستم، محدود شود. در این حالت اگر به برنامه‌ی React مراجعه کرده و برای مثال سعی در ویرایش رکوردی کنیم، اتفاقی رخ نخواهد داد:


علت را نیز در برگه‌ی network کنسول توسعه دهندگان مرورگر، می‌توان مشاهده کرد. این درخواست از سمت سرور با Status Code: 401، برگشت خورده‌است. برای رفع این مشکل باید JSON web token ای را که در حین لاگین، از سمت سرور دریافت کرده بودیم، به همراه درخواست خود، مجددا به سمت سرور ارسال کنیم. این ارسال نیز باید به صورت یک هدر مخصوص با کلید Authorization و مقدار "Bearer jwt" باشد.
به همین جهت ابتدا به src\services\authService.js مراجعه کرده و متدی را برای بازگشت JWT ذخیره شده‌ی در local storage به آن اضافه می‌کنیم:
export function getLocalJwt(){
  return localStorage.getItem(tokenKey);
}
سپس به src\services\httpService.js مراجعه کرده و از آن استفاده می‌کنیم:
import * as auth from "./authService";

axios.defaults.headers.common["Authorization"] = "Bearer " + auth.getLocalJwt();
کار این یک سطر که در ابتدای ماژول httpService قرار می‌گیرد، تنظیم هدرهای پیش‌فرض تمام انواع درخواست‌های ارسالی توسط Axios است. البته می‌توان از حالت‌های اختصاصی‌تری نیز مانند فقط post، بجای common استفاده کرد. برای نمونه در تنظیم فوق، تمام درخواست‌های HTTP Get/Post/Delete/Put ارسالی توسط Axios، دارای هدر Authorization که مقدار آن به ثابتی شروع شده‌ی با Bearer و سپس مقدار JWT دریافتی از سرور تنظیم می‌شود، خواهند بود.

مشکل! اگر برنامه را در این حالت اجرا کنید، یک چنین خطایی را مشاهده خواهید کرد:
Uncaught ReferenceError: Cannot access 'tokenKey' before initialization
علت اینجا است که سرویس httpService، دارای ارجاعی به سرویس authService شده‌است و برعکس (در httpService، یک import از authService را داریم و در authService، یک import از httpService را)! یعنی یک وابستگی حلقوی و دو طرفه رخ‌داده‌است.
برای رفع این خطا باید ابتدا مشخص کنیم که کدامیک از این ماژول‌ها، اصلی است و کدامیک باید وابسته‌ی به دیگری باشد. در این حالت httpService، ماژول اصلی است و بدون آن و با نبود امکان اتصال به backend، دیگر authService قابل استفاده نخواهد بود.
به همین جهت به httpService مراجعه کرده و import مربوط به authService را از آن حذف می‌کنیم. سپس در همینجا متدی را برای تنظیم هدر Authorizationاضافه کرده و آن‌را به لیست default exports این ماژول نیز اضافه می‌کنیم:
function setJwt(jwt) {
  axios.defaults.headers.common["Authorization"] = "Bearer " + jwt;
}

//...

export default {
  // ...
  setJwt
};
در آخر، در authService که ارجاعی را به httpService دارد، فراخوانی متد setJwt را در ابتدای ماژول، انجام خواهیم داد:
http.setJwt(getLocalJwt());
به این ترتیب، وابستگی حلقوی بین این دو ماژول برطرف می‌شود و اکنون این authService است که به httpService وابسته‌است و نه برعکس.

تا اینجا اگر تغییرات را ذخیره کرده و سعی در ویرایش یکی از رکوردهای فیلم‌های نمایش داده شده کنیم، این‌کار با موفقیت انجام می‌شود؛ چون اینبار درخواست ارسالی، دارای هدر ویژه‌ی authorization است:



روش بررسی انقضای توکن‌ها در سمت کلاینت

اگر JWT قدیمی و منقضی شده‌ی از روز گذشته را آزمایش کنید، باز هم از سمت سرور، Status Code: 401 دریافت خواهد شد. اما اینبار در لاگ‌های برنامه‌ی سمت سرور، OnChallenge error مشخص است. در این حالت باید یکبار logout کرد تا JWT قدیمی حذف شود. سپس نیاز به لاگین مجدد است تا یک JWT جدید دریافت گردد. می‌توان اینکار را پیش از ارسال اطلاعات به سمت سرور، در سمت کلاینت نیز بررسی کرد:
function checkExpirationDate(user) {
  if (!user || !user.exp) {
    throw new Error("This access token doesn't have an expiration date!");
  }

  user.expirationDateUtc = new Date(0); // The 0 sets the date to the epoch
  user.expirationDateUtc.setUTCSeconds(user.exp);

  const isAccessTokenTokenExpired =
    user.expirationDateUtc.valueOf() < new Date().valueOf();
  if (isAccessTokenTokenExpired) {
    throw new Error("This access token is expired!");
  }
}
در اینجا user، همان شیء حاصل از const user = jwtDecode(jwt) است که در قسمت قبل به آن پرداختیم. سپس خاصیت exp آن با زمان جاری مقایسه شده و در صورت وجود مشکلی، استثنایی را صادر می‌کند. می‌توان این متد را پس از فراخوانی jwtDecode، قرار داد.


محدود کردن حذف رکوردهای فیلم‌ها به نقش Admin در Backend

تا اینجا تمام کاربران وارد شده‌ی به سیستم، می‌توانند علاوه بر ویرایش فیلم‌ها، آن‌ها را نیز حذف کنند. به همین جهت می‌خواهیم دسترسی حذف را از کاربرانی که ادمین نیستند، بگیریم. برای این منظور، در سمت سرور کافی است در کنترلر MoviesController، ویژگی [Authorize(Policy = CustomRoles.Admin)] را به اکشن متد Delete، اضافه کنیم. به این ترتیب اگر کاربری در سیستم ادمین نبود و درخواست حذف رکوردی را صادر کرد، خطای 403 را از سمت سرور دریافت می‌کند:


در برنامه‌ی مثال backend این سری، در فایل Services\UsersDataSource.cs، یک کاربر ادمین پیش‌فرض ثبت شده‌است. مابقی کاربرانی که به صورت معمولی در سایت ثبت نام می‌کنند، ادمین نیستند.
در این حالت اگر کاربری ادمین بود، چون در توکن او که در فایل Services\TokenFactoryService.cs صادر می‌شود، یک User Claim ویژه‌ی از نوع Role و با مقدار Admin وجود دارد:
if (user.IsAdmin)
{
   claims.Add(new Claim(ClaimTypes.Role, CustomRoles.Admin, ClaimValueTypes.String, _configuration.Value.Issuer));
}
این مقدار، در payload توکن نهایی او نیز ظاهر خواهد شد:
{
  // ...
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
  // ...
}
بنابراین هربار که برنامه‌ی React ما، هدر Bearer jwt را به سمت سرور ارسال می‌کند، فیلتر Authorize محدود شده‌ی به نقش ادمین، این نقش را در صورت وجود در توکن او، پردازش کرده و دسترسی‌های لازم را به صورت خودکار صادر می‌کند و همانطور که پیشتر نیز عنوان شد، اگر کاربری این نقش را به صورت دستی به توکن ارسالی به سمت سرور اضافه کند، به دلیل دسترسی نداشتن به کلیدهای خصوصی تولید مجدد امضای دیجیتال توکن، درخواست او در سمت سرور تعیین اعتبار نشده و برگشت خواهد خورد.


نکته 1: اگر در اینجا چندین بار یک User Claim را با مقادیر متفاوتی، به لیست claims اضافه کنیم، مقادیر آن در خروجی نهایی، به شکل یک آرایه ظاهر می‌گردند.

نکته 2: پیاده سازی سمت سرور backend این سری، یک باگ امنیتی مهم را دارد! در حین ثبت نام، کاربران می‌توانند مقدار خاصیت isAdmin شیء User را:
    public class User : BaseModel
    {
        [Required, MinLength(2), MaxLength(50)]
        public string Name { set; get; }

        [Required, MinLength(5), MaxLength(255)]
        public string Email { set; get; }

        [Required, MinLength(5), MaxLength(1024)]
        public string Password { set; get; }

        public bool IsAdmin { set; get; }
    }
خودشان دستی تنظیم کرده و ارسال کنند تا به صورت ادمین ثبت شوند! به این مشکل مهم، اصطلاحا mass assignment گفته می‌شود.
راه حل اصولی مقابله‌ی با آن، داشتن یک DTO و یا ViewModel خاص قسمت ثبت نام و جدا کردن مدل متناظر با موجودیت User، از شیءای است که اطلاعات نهایی را از کاربر، دریافت می‌کند. شیءای که اطلاعات را از کاربر دریافت می‌کند، نباید دارای خاصیت isAdmin قابل تنظیم در حین ثبت نام معمولی کاربران سایت باشد. یک روش دیگر حل این مشکل، استفاده از ویژگی Bind و ذکر صریح نام خواصی است که قرار است bind شوند و نه هیچ خاصیت دیگری از شیء User:
[HttpPost]
public ActionResult<User> Create(
            [FromBody]
            [Bind(nameof(Models.User.Name), nameof(Models.User.Email), nameof(Models.User.Password))]
            User data)
        {
و یا حتی می‌توان ویژگی [BindNever] را بر روی خاصیت IsAdmin، در مدل User قرار داد.

نکته 3: اگر می‌خواهید در برنامه‌ی React، با مواجه شدن با خطای 403 از سمت سرور، کاربر را به یک صفحه‌ی عمومی «دسترسی ندارید» هدایت کنید، می‌توانید از interceptor سراسری که در قسمت 24 تعریف کردیم، استفاده کنید. در اینجا status code = 403 را جهت history.push به یک آدرس access-denied سفارشی و جدید، پردازش کنید.


نمایش یا مخفی کردن المان‌ها بر اساس سطوح دسترسی کاربر وارد شده‌ی به سیستم

می‌خواهیم در صفحه‌ی نمایش لیست فیلم‌ها، دکمه‌ی new movie را که بالای صفحه قرار دارد، به کاربرانی که لاگین نکرده‌اند، نمایش ندهیم. همچنین نمی‌خواهیم اینگونه کاربران، بتوانند فیلمی را ویرایش و یا حذف کنند؛ یعنی لینک به صفحه‌ی جزئیات ویرایشی فیلم‌ها و ستونی که دکمه‌ها‌ی حذف هر ردیف را نمایش می‌دهد، به کاربران وارد نشده‌ی به سیستم نمایش داده نشوند.

در قسمت قبل، در فایل app.js، شیء currentUser را به state اضافه کردیم و با استفاده از ارسال آن به کامپوننت NavBar:
<NavBar user={this.state.currentUser} />
 نام کاربر وارد شده‌ی به سیستم را نمایش دادیم. با استفاده از همین روش می‌توان شیء currentUser را به کامپوننت Movies ارسال کرد و سپس بر اساس محتوای آن، قسمت‌های مختلف صفحه را مخفی کرد و یا نمایش داد. البته در اینجا (در فایل app.js) خود کامپوننت Movies درج نشده‌است؛ بلکه مسیریابی آن‌را تعریف کرده‌ایم که با روش ارسال پارامتر به یک مسیریابی، در قسمت 15، قابل تغییر و پیاده سازی است:
<Route
   path="/movies"
   render={props => <Movies {...props} user={this.state.currentUser} />}
/>
در اینجا برای ارسال props به یک کامپوننت، نیاز است از ویژگی render استفاده شود. سپس پارامتر arrow function را به همان props تنظیم می‌کنیم. همچنین با استفاده از spread operator، این props را در المان JSX تعریف شده، گسترده و تزریق می‌کنیم؛ تا از سایر خواص پیشینی که تزریق شده بودند مانند history، location و match، محروم نشویم و آن‌ها را از دست ندهیم. در نهایت المان کامپوننت مدنظر را همانند روش متداولی که برای تعریف تمام کامپوننت‌های React و تنظیم ویژگی‌های آن‌ها استفاده می‌شود، بازگشت می‌دهیم.

پس از این تغییر به فایل src\components\movies.jsx مراجعه کرده و شیء user را در متد رندر، دریافت می‌کنیم:
class Movies extends Component {
  // ...

  render() {
    const { user } = this.props;
    // ...
اکنون که در کامپوننت Movies به این شیء user دسترسی پیدا کردیم، توسط آن می‌توان قسمت‌های مختلف صفحه را مخفی کرد و یا نمایش داد:
{user && (
  <Link
    to="/movies/new"
    className="btn btn-primary"
    style={{ marginBottom: 20 }}
  >
    New Movie
  </Link>
)}
با این تغییر، اگر شیء user مقدار دهی شده باشد، عبارت پس از &&، در صفحه درج خواهد شد و برعکس:


در این تصویر همانطور که مشخص است، کاربر هنوز به سیستم وارد نشده‌است؛ بنابراین به علت null بودن شیء user، دکمه‌ی New Movie را مشاهده نمی‌کند.


روش دریافت نقش‌های کاربر وارد شده‌ی به سیستم در سمت کلاینت

همانطور که پیشتر در مطلب جاری عنوان شد، نقش‌های دریافتی از سرور، یک چنین شکلی را در jwtDecode نهایی (یا user در اینجا) دارند:
{
  // ...
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
  // ...
}
که البته اگر چندین Role تعریف شده باشند، مقادیر آن‌ها در خروجی نهایی، به شکل یک آرایه ظاهر می‌گردد. بنابراین برای بررسی آن‌ها می‌توان نوشت:
function addRoles(user) {
  const roles =
    user["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"];
  if (roles) {
    if (Array.isArray(roles)) {
      user.roles = roles.map(role => role.toLowerCase());
    } else {
      user.roles = [roles.toLowerCase()];
    }
  }
}
کار این متد، دریافت نقش و یا نقش‌های ممکن از jwtDecode، و بازگشت آن‌ها (افزودن آن‌ها به صورت یک خاصیت جدید، به نام roles، به شیء user دریافتی) به صورت یک آرایه‌ی با عناصری LowerCase است. سپس اگر نیاز به بررسی نقش‌، یا نقش‌های کاربری خاص بود، می‌توان از یکی از متدهای زیر استفاده کرد:
export function isAuthUserInRoles(user, requiredRoles) {
  if (!user || !user.roles) {
    return false;
  }

  if (user.roles.indexOf(adminRoleName.toLowerCase()) >= 0) {
    return true; // The `Admin` role has full access to every pages.
  }

  return requiredRoles.some(requiredRole => {
    if (user.roles) {
      return user.roles.indexOf(requiredRole.toLowerCase()) >= 0;
    } else {
      return false;
    }
  });
}

export function isAuthUserInRole(user, requiredRole) {
  return isAuthUserInRoles(user, [requiredRole]);
}
متد isAuthUserInRoles، آرایه‌ای از نقش‌ها را دریافت می‌کند و سپس بررسی می‌کند که آیا کاربر انتخابی، دارای این نقش‌ها هست یا خیر و متد isAuthUserInRole، تنها یک نقش را بررسی می‌کند.
در این کدها، adminRoleName به صورت زیر تامین شده‌است:
import { adminRoleName, apiUrl } from "../config.json";
یعنی محتویات فایل config.json تعریف شده را به صورت زیر با افزودن نام نقش ادمین، تغییر داده‌ایم:
{
  "apiUrl": "https://localhost:5001/api",
  "adminRoleName": "Admin"
}


عدم نمایش ستون Delete ردیف‌های لیست فیلم‌ها، به کاربرانی که Admin نیستند

اکنون که امکان بررسی نقش‌های کاربر لاگین شده‌ی به سیستم را داریم، می‌خواهیم ستون Delete ردیف‌های لیست فیلم‌ها را فقط به کاربری که دارای نقش Admin است، نمایش دهیم. برای اینکار نیاز به دریافت شیء user، در src\components\moviesTable.jsx وجود دارد. یک روش دریافت کاربر جاری وارد شده‌ی به سیستم، همانی است که تا به اینجا بررسی کردیم: شیء currentUser را به صورت props، از بالاترین کامپوننت، به پایین‌تر کامپوننت موجود در component tree ارسال می‌کنیم. روش دیگر اینکار، دریافت مستقیم کاربر جاری از خود src\services\authService.js است و ... اینکار ساده‌تر است! به علاوه اینکه همیشه بررسی تاریخ انقضای توکن را نیز به صورت خودکار انجام می‌دهد و در صورت انقضای توکن، کاربر را در قسمت catch متد getCurrentUser، از سیستم خارج خواهد کرد.
بنابراین در src\components\moviesTable.jsx، ابتدا authService را import می‌کنیم:
import * as auth from "../services/authService";
در ادامه ابتدا تعریف ستون حذف را از آرایه‌ی columns خارج کرده و تبدیل به یک خاصیت می‌کنیم. یعنی در ابتدای کار، چنین ستونی تعریف نشده‌است:
class MoviesTable extends Component {
  columns = [ ... ];
  // ...

  deleteColumn = {
    key: "delete",
    content: movie => (
      <button
        onClick={() => this.props.onDelete(movie)}
        className="btn btn-danger btn-sm"
      >
        Delete
      </button>
    )
  };
در آخر در متد سازنده‌ی این کامپوننت، کاربر جاری را از authService دریافت کرده و اگر این کاربر دارای نقش Admin بود، ستون deleteColumn را به لیست ستون‌های موجود، اضافه می‌کنیم تا نمایش داده شود:
  constructor() {
    super();
    const user = auth.getCurrentUser();
    if (user && auth.isAuthUserInRole(user, "Admin")) {
      this.columns.push(this.deleteColumn);
    }
  }
اکنون برای آزمایش برنامه، یکبار از آن خارج شوید؛ دیگر نباید ستون Delete نمایش داده شود. همچنین یکبار هم تحت عنوان یک کاربر معمولی در سایت ثبت نام کنید. این کاربر نیز چنین ستونی را مشاهده نمی‌کند.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-28-backend.zip و sample-28-frontend.zip