مطالب
آشنایی با Promises در جاوا اسکریپت
در حین انجام اعمال غیرهمزمان جاوا اسکریپتی مانند فراخوانی‌های jQuery AJAX، برای مدیریت دریافت نتایج، عموما از یک سری callback استفاده می‌شود. برای مثال:
 $.get('http://site-url', function(data) {
//این تابع پس از پایان کار عملیات ای‌جکسی در آینده فراخوانی خواهد شد
});
تا اینجا مشکلی به نظر نمی‌رسد. اما مورد ذیل چطور؟
$.get('http://site-url/0', function(data0) {
    // callback #1
    $.get('http://site-url/1', function(data1) {
        // callback #2
        $.post('http://site-url/2', function(data2) {
            // callback #3
        });
    });
});
در اینجا نیاز است پس از پایان کار عملیات Ajax ایی اول، عملیات دوم و پس از آن عملیات سومی انجام شود. همانطور که مشاهده می‌کنید، این نوع کدها به سرعت از کنترل خارج می‌شوند؛ خوانایی پایینی داشته و مدیریت استثناءهای رخ داده در آن‌ها نیز در این بین مشکل است. از این جهت که خطاهای هر کدام به سطحی بالاتر منتقل نمی‌شود و باید همانجا محلی و داخل هر callback مدیریت گردد.
روش‌های زیادی برای حل این مساله ارائه شده‌است و در حال حاضر کار کردن با promiseها متداول‌ترین روش حل مدیریت فراخوانی کدهای همزمان جاوا اسکریپتی است. برای نمونه اگر از AngularJS استفاده کنید، سرویس‌های آن برای دریافت اطلاعات از سرور، از یک چنین مفهومی استفاده می‌کنند.


Promise در جاوا اسکریپت چیست؟

شیء Promise، نمایانگر قراردادی است که در آینده می‌تواند مورد قبول واقع شود، یا رد گردد. بررسی این قرارداد، تنها یکبار می‌تواند رخ دهد (پذیرش یا رد آن). هنگامیکه این بررسی صورت گرفت (رد یا پذیرش آن و نه هردو)، یک callback برای اطلاع رسانی فراخوانی می‌گردد. سپس این callback می‌تواند یک Promise دیگر را سبب شود. به این ترتیب می‌توان Promiseها را زنجیر وار به یکدیگر متصل کرد. برای نمونه jQuery به صورت توکار از promises پشتیبانی می‌کند:
// returns a promise
$.get('http://site-url/0')    
.then(function(data) {
    // callback 1
    // returns a promise
    return $.get('http://site-url/1');   
})
.then(function(data) {
    // callback 2
    // returns a promise
    return $.post('http://site-url/2');
})
.then(function(data) {
    // callback 3
});
متد get در jQuery یک شیء promise را بازگشت می‌دهد. در ادامه می‌توان این نتیجه را توسط متد then، زنجیروار ادامه داد. متدی که به عنوان پارامتر به then ارسال می‌شود، یک callback بوده و پس از پایان کار promise قبلی رخ می‌دهد. آرگومانی که به این callback ارسال می‌شود، نتیجه‌ی promise قبلی است. در حین اعمال jQuery Ajax، این callback تنها زمانی فراخونی می‌شود که عملیات قبلی موفقیت آمیز بوده باشد و data ارائه شده، اطلاعاتی است که توسط response دریافتی از سرور، دریافت گردیده‌است.
در این حالت، هر callback حداقل سه کار را می‌تواند انجام دهد:
الف) یک promise دیگر را بازگشت دهد. نمونه آن‌را با return $.get در کدهای فوق ملاحظه می‌کنید.
ب) خاتمه عادی. همینجا کار promise با مقدار بازگشت داده شده، پایان می‌یابد.
ج) صدور یک استثناء. سبب برگشت خوردن و عدم پذیرش promise می‌شود.


استفاده از Promises در سایر کتابخانه‌ها

jQuery پیاده سازی توکاری از promises دارد؛ اما سایر کتابخانه‌ها، مانند AngularJS ایی که مثال زده شده چطور عمل می‌کنند؟
استانداردی به نام  +Promises/A جهت یک دست سازی پیاده سازی‌های promise در جاوا اسکریپت پیشنهاد شده‌است. jQuery نیمی از آن‌را پیاده سازی کرده‌است؛ اما کتابخانه‌ی دیگری به نام Q Library، پیاده سازی نسبتا مفصل‌تری را از این استاندارد ارائه می‌دهد. فریم ورک AngularJS نیز در پشت صحنه از همین کتابخانه برای پیاده سازی promises استفاده می‌کند.


آشنایی با کتابخانه Q

استفاده مقدماتی از Q همانند مثالی است که از jQuery ملاحظه کردید.
 Q.fcall(callback1)
.then(callback2);
اشیاء promise بازگشت داده شده توسط jQuery نیز توسط کتابخانه Q مورد پذیرش واقع می‌شوند:
Q.fcall(function() {
    return $.get('http://my-url');
})
.then(callback3);
علاوه بر این‌ها مفهومی به نام deferred objects نیز در کتابخانه‌ی Q پیاده سازی شده‌است:
function waitForClick() {
    var deferred = Q.defer();

$('#okButton').click(function() {
        deferred.resolve();
    });

$('#cancelButton').click(function() {
        deferred.reject();
    });

return deferred.promise;
}

Q.fcall(waitForClick)
.then(function() {
    //  ok button was clicked
}, function() {
    //  cancel button was clicked
});
توسط deferred objects می‌توان بررسی یک promise را به تاخیر انداخت. در مثال فوق، اولین callback فراخوانی شده به نام waitForClick، از اشیاء به تاخیر افتاده استفاده می‌کند. ابتدا توسط فراخوانی متد Q.defer، یک deferred object ایجاد می‌شود. در این بین اگر کاربر بر روی دکمه‌ی OK کلیک کرد، با فراخوانی deferred.resolve، این promise مورد پذیرش واقع خواهد شد و یا اگر کاربر بر روی دکمه‌ی cancel کلیک کند، با فراخوانی متد deferred.reject، این promise رد می‌گردد. نهایتا شیء promise توسط deferred.promise بازگشت داده خواهد شد.
در ادامه کار، اینبار متد then، دو callback را قبول می‌کند. Callback اول پس از پذیرش قرار داد و Callback دوم پس از رد قرار داد، فراخوانی خواهد گردید.
در رنجیره تعریف شده، اگر معادلی برای reject درنظر گرفته نشده باشد، مانند مثال ذیل:
 Q.fcall(myFunction1)
.then(success1)
.then(success2, failure1);
Q به دنبال نزدیک‌ترین متد callback گزارش خطای کار خواهد گشت. در این حالت متد failure1 در صورت شکست اولین promise فراخوانی خواهد شد.
همچنین اگر نتیجه‌ی success1 با شکست مواجه شود نیز failure1 فراخوانی می‌گردد. اما باید درنظر داشت که شکست success2، توسط failure1 مدیریت نمی‌شود.


Promises در AngularJS

در AngularJS امکانات کتابخانه Q توسط پارامتری به نام q$ در اختیار سرویس‌های برنامه قرار می‌گیرد (تزریق می‌شود):
var app = angular.module("myApp", []);
app.factory('dataSvc', function($http, $q){
  var basePath="api/books";
  getAllBooks = function(){
   var deferred = $q.defer();
 $http.get(basePath).success(function(data){
    deferred.resolve(data);
   }).error(function(err){
    deferred.reject("service failed!");
   });
   return deferred.promise;
  };
 
  return{
   getAllBooks:getAllBooks
  };
});
 
app.controller('HomeController', function($scope, $window, dataSvc){
 function initialize(){
   dataSvc.getAllBooks().then(function(data){
    $scope.books = data;
   }, function(msg){
    $window.alert(msg);
   });
 }
 
 initialize();
});
در اینجا اگر دقت کنید، مباحث و عملکرد آن دقیقا مانند قبل است. ابتدا یک deferred object با فراخوانی متد q.defer ایجاد شده است. سپس با استفاده از امکانات توکار http آن (بجای استفاده از jQuery Ajax)، کار فراخوانی یک restful service صورت گرفته است (مثلا فراخوانی یک ASP.NET Web API). در صورت موفقیت کار، متد deferred.resolve و در صورت عدم موفقیت، متد deferred.reject فراخوانی شده‌است. نهایتا این سرویس، یک deferred.promise را بازگشت می‌دهد.
اکنون در کنترلری که قرار است از این سرویس استفاده کند، متد then کتابخانه Q را ملاحظه می‌کنید که دو Callback متناظر resolve و reject مدیریت promise بازگشت داده شده را به همراه دارد. اگر عملیات Ajaxایی موفقیت آمیز باشد، شیء books را مقدار دهی می‌کند و اگر خیر، پیامی را به کاربر نمایش خواهد داد.


پشتیبانی مرورگرهای جدید از استاندارد Promise

در حال حاضر کروم 32 و نگارش‌های شبانه فایرفاکس، Promise را که جزئی از استاندارد JavaScript شده‌است، به صورت توکار و بدون نیاز به کتابخانه‌های جانبی، پشتیبانی می‌کنند.
if (window.Promise) { // Check if the browser supports Promises
 var promise = new Promise(function(resolve, reject) {
  //asynchronous code goes here
 });
}
در اینجا با فراخوانی window.Promise مشخص می‌شود که آیا مرورگر جاری از Promises پشتیبانی می‌کند یا خیر. سپس یک شیء promise ایجاد شده و این شیء توسط پارامترهای resolve و reject که هر دو تابع می‌باشند، کار مدیریت کدهای غیرهمزمان را انجام می‌دهد:
if (window.Promise) {
 console.log('Promise found');
 
 var promise = new Promise(function(resolve, reject) {
      // async
  if (result) {
   resolve(data);
  } else {
   reject('error');
  }  
 });
 
 promise.then(function(data) {
  console.log('Promise fulfilled.');
 }, function(error) {
  console.log('Promise rejected.');
 });
} else {
 console.log('Promise not available');
}
در مثال فوق ابتدا یک شیء Promise ایجاد شده است. این شیء استاندارد بوده و با کروم 32 قابل آزمایش است. سپس در callback ابتدایی آن می‌توان یک عملیات AJAX ایی را انجام داد. اگر نتیجه‌ی آن موفقیت آمیز بود، تنها کافی است پارامتر اول این callback را فراخوانی کنیم و اگر خیر، پارامتر دوم آن‌را. برای استفاده از این شیء Promise ایجاد شده، می‌توان از متد then استفاده کرد. این متد نیز در اینجا دو callback پذیرش و رد promise را می‌تواند دریافت کند. برای زنجیر کردن آن کافی است متد then، یک Promise دیگر را بازگشت دهد و از نتیجه‌ی آن در then بعدی استفاده گردد.
نظرات مطالب
آشنایی با Refactoring - قسمت 7
یک نکته‌ی تکمیلی
اگر در حین refactoring و استخراج متدها، به متدی با تعداد پارامترهای بالا رسیدید و این متد هم نیازی نبود به کلاس دیگری منتقل شود، می‌توان برای کاهش تعداد پارامترهای آن از قابلیت «C# 7 - Local Functions» هم استفاده کرد. یک متد محلی به پارامترهای تعریف شده‌ی در متد دربرگیرنده‌ی آن دسترسی دارد.
مطالب
Blazor 5x - قسمت 23 - احراز هویت و اعتبارسنجی کاربران Blazor Server - بخش 3 - کار با نقش‌های کاربران
در قسمت قبل، روش یکپارچه سازی context مربوط به ASP.NET Core Identity را با یک برنامه‌ی Blazor Server، بررسی کردیم. در این قسمت می‌خواهیم محدود کردن دسترسی‌ها را بر اساس نقش‌های کاربران و همچنین کدنویسی مستقیم، بررسی کنیم.


کار با Authentication State از طریق کدنویسی

فرض کنید در کامپوننت HotelRoomUpsert.razor نمی‌خواهیم دسترسی‌ها را به کمک اعمال ویژگی attribute [Authorize]@ محدود کنیم؛ می‌خواهیم اینکار را از طریق کدنویسی مستقیم انجام دهیم:
// ...

@*@attribute [Authorize]*@


@code
{
    [CascadingParameter] public Task<AuthenticationState> AuthenticationState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        var authenticationState = await AuthenticationState;
        if (!authenticationState.User.Identity.IsAuthenticated)
        {
            var uri = new Uri(NavigationManager.Uri);
            NavigationManager.NavigateTo($"/identity/account/login?returnUrl={uri.LocalPath}");
        }
        // ...
- در اینجا در ابتدا اعمال ویژگی Authorize را کامنت کردیم.
- سپس یک پارامتر ویژه را از نوع CascadingParameter، به نام AuthenticationState تعریف کردیم. این خاصیت از طریق کامپوننت CascadingAuthenticationState که در قسمت قبل به فایل BlazorServer.App\App.razor اضافه کردیم، تامین می‌شود.
- در آخر در روال رویدادگردان OnInitializedAsync، بر اساس آن می‌توان به اطلاعات User جاری وارد شده‌ی به سیستم دسترسی یافت و برای مثال اگر اعتبارسنجی نشده بود، با استفاده از NavigationManager، او را به صفحه‌ی لاگین هدایت می‌کنیم.
- در اینجا روش ارسال آدرس صفحه‌ی فعلی را نیز مشاهده می‌کنید. این امر سبب می‌شود تا پس از لاگین، کاربر مجددا به همین صفحه هدایت شود.

authenticationState، امکانات بیشتری را نیز در اختیار ما قرار می‌دهد؛ برای مثال با استفاده از متد ()authenticationState.User.IsInRole آن می‌توان دسترسی به قسمتی را بر اساس نقش‌های خاصی محدود کرد.


ثبت کاربر ادمین Identity

در ادامه می‌خواهیم دسترسی به کامپوننت‌های مختلف را بر اساس نقش‌ها، محدود کنیم. به همین جهت نیاز است تعدادی نقش و یک کاربر ادمین را به بانک اطلاعاتی برنامه اضافه کنیم. برای اینکار به پروژه‌ی BlazorServer.Common مراجعه کرده و تعدادی نقش ثابت را تعریف می‌کنیم:
namespace BlazorServer.Common
{
    public static class ConstantRoles
    {
        public const string Admin = nameof(Admin);
        public const string Customer = nameof(Customer);
        public const string Employee = nameof(Employee);
    }
}
علت قرار دادن این کلاس در پروژه‌ی Common، نیاز به دسترسی به آن در پروژه‌ی اصلی Blazor Server و همچنین در پروژه‌ی سرویس‌های برنامه‌است. فضای نام این کلاس را نیز در فایل imports.razor_ قرار می‌دهیم.

سپس در فایل BlazorServer.App\appsettings.json، مشخصات ابتدایی کاربر ادمین را ثبت می‌کنیم:
{
  "AdminUserSeed": {
    "UserName": "vahid@dntips.ir",
    "Password": "123@456#Pass",
    "Email": "vahid@dntips.ir"
  }
}
جهت دریافت strongly typed این تنظیمات در برنامه، کلاس معادل AdminUserSeed را به پروژه‌ی Models اضافه می‌کنیم:
namespace BlazorServer.Models
{
    public class AdminUserSeed
    {
        public string UserName { get; set; }
        public string Password { get; set; }
        public string Email { get; set; }
    }
}
که به صورت زیر در فایل BlazorServer.App\Startup.cs به سیستم تزریق وابستگی‌های برنامه معرفی می‌شود:
namespace BlazorServer.App
{
    public class Startup
    {

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOptions<AdminUserSeed>().Bind(Configuration.GetSection("AdminUserSeed"));
            // ...
اکنون می‌توان سرویس افزودن نقش‌ها و کاربر ادمین را در پروژه‌ی BlazorServer.Services تکمیل کرد:
using System;
using System.Linq;
using System.Threading.Tasks;
using BlazorServer.Common;
using BlazorServer.DataAccess;
using BlazorServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace BlazorServer.Services
{
    public class IdentityDbInitializer : IIdentityDbInitializer
    {
        private readonly ApplicationDbContext _dbContext;
        private readonly UserManager<IdentityUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;
        private readonly IOptions<AdminUserSeed> _adminUserSeedOptions;

        public IdentityDbInitializer(
            ApplicationDbContext dbContext,
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager,
            IOptions<AdminUserSeed> adminUserSeedOptions)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _roleManager = roleManager ?? throw new ArgumentNullException(nameof(roleManager));
            _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
            _adminUserSeedOptions = adminUserSeedOptions ?? throw new ArgumentNullException(nameof(adminUserSeedOptions));
        }

        public async Task SeedDatabaseWithAdminUserAsync()
        {
            if (_dbContext.Roles.Any(role => role.Name == ConstantRoles.Admin))
            {
                return;
            }

            await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Admin));
            await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Customer));
            await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Employee));

            await _userManager.CreateAsync(
                new IdentityUser
                {
                    UserName = _adminUserSeedOptions.Value.UserName,
                    Email = _adminUserSeedOptions.Value.Email,
                    EmailConfirmed = true
                },
                _adminUserSeedOptions.Value.Password);

            var user = await _dbContext.Users.FirstAsync(u => u.Email == _adminUserSeedOptions.Value.Email);
            await _userManager.AddToRoleAsync(user, ConstantRoles.Admin);
        }
    }
}
این سرویس، با استفاده از دو سرویس توکار UserManager و RoleManager کتابخانه‌ی Identity، ابتدا سه نقش ادمین، مشتری و کارمند را ثبت می‌کند. سپس بر اساس اطلاعات AdminUserSeed تعریف شده، کاربر ادمین را ثبت می‌کند. البته این کاربر در این مرحله، یک کاربر معمولی بیشتر نیست. در مرحله‌ی بعد است که با انتساب نقش ادمین به او، می‌توان کاربر او را بر اساس این نقش ویژه، شناسایی کرد. کلاس‌های IdentityRole و IdentityUser، کلاس‌های پایه‌ی نقش‌ها و کاربران کتابخانه‌ی Identity هستند.

پس از تعریف این سرویس، نیاز است آن‌را به سیستم تزریق وابستگی‌های برنامه اضافه کرد:
namespace BlazorServer.App
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IIdentityDbInitializer, IdentityDbInitializer>();
            // ...
مرحله‌ی آخر، اعمال و اجرای سرویس IIdentityDbInitializer، در زمان آغاز برنامه‌است و محل توصیه شده‌ی آن، در متد Main برنامه‌ی اصلی، پیش از اجرای برنامه‌است. به همین جهت، نیاز است BlazorServer.DataAccess\Utils\MigrationHelpers.cs را به صورت زیر ایجاد کرد:
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Polly;

namespace BlazorServer.DataAccess.Utils
{
    public static class MigrationHelpers
    {
        public static void MigrateDbContext<TContext>(
                this IServiceProvider serviceProvider,
                Action<IServiceProvider> postMigrationAction
                ) where TContext : DbContext
        {
            using var scope = serviceProvider.CreateScope();
            var scopedServiceProvider = scope.ServiceProvider;
            var logger = scopedServiceProvider.GetRequiredService<ILogger<TContext>>();
            using var context = scopedServiceProvider.GetService<TContext>();

            logger.LogInformation($"Migrating the DB associated with the context {typeof(TContext).Name}");

            var retry = Policy.Handle<Exception>().WaitAndRetry(new[]
                {
                    TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(15)
                });

            retry.Execute(() =>
                {
                    context.Database.Migrate();
                    postMigrationAction(scopedServiceProvider);
                });

            logger.LogInformation($"Migrated the DB associated with the context {typeof(TContext).Name}");
        }
    }
}
در مورد این متد و استفاده از Polly جهت تکرار عملیات شکست خورده پیشتر در مطلب «اضافه کردن سعی مجدد به اجرای عملیات Migration در EF Core» بحث شده‌است.
کار متد الحاقی فوق، دریافت یک IServiceProvider است که به سرویس‌های اصلی برنامه اشاره می‌کند. سپس بر اساس آن، یک Scoped ServiceProvider را ایجاد می‌کند تا درون آن بتوان با Context برنامه در طی مدت کوتاهی کار کرد و در پایان آن، سرویس‌های ایجاد شده را Dispose کرد.
در این متد ابتدا Database.Migrate فراخوانی می‌شود تا اگر مرحله‌ای از Migrations برنامه هنوز به بانک اطلاعاتی اعمال نشده، کار اجرا و اعمال آن انجام شود. سپس یک متد سفارشی را از فراخوان دریافت کرده و اجرا می‌کند. برای مثال توسط آن می‌توان IIdentityDbInitializer در فایل BlazorServer.App\Program.cs به صوت زیر فراخوانی کرد:
public static void Main(string[] args)
{
    var host = CreateHostBuilder(args).Build();
    host.Services.MigrateDbContext<ApplicationDbContext>(
     scopedServiceProvider =>
            scopedServiceProvider.GetRequiredService<IIdentityDbInitializer>()
                                 .SeedDatabaseWithAdminUserAsync()
                                 .GetAwaiter()
                                 .GetResult()
    );
    host.Run();
}
تا اینجا اگر برنامه را اجرا کنیم، سه نقش پیش‌فرض، به بانک اطلاعاتی برنامه اضافه شده‌اند:


و همچنین کاربر پیش‌فرض سیستم را نیز می‌توان مشاهده کرد:


که نقش ادمین و کاربر پیش‌فرض، به این صورت به هم مرتبط شده‌اند (یک رابطه‌ی many-to-many برقرار است):



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

پس از ایجاد کاربر ادمین و تعریف نقش‌های پیش‌فرض، اکنون محدود کردن دسترسی به کامپوننت‌های برنامه بر اساس نقش‌ها، ساده‌است. برای این منظور فقط کافی است لیست نقش‌های مدنظر را که می‌توانند توسط کاما از هم جدا شوند، به ویژگی Authorize کامپوننت‌ها معرفی کرد:
@attribute [Authorize(Roles = ConstantRoles.Admin)]
و یا از طریق کدنویسی به صورت زیر نیز قابل اعمال است:
    protected override async Task OnInitializedAsync()
    {
        var authenticationState = await AuthenticationState;
        if (!authenticationState.User.Identity.IsAuthenticated ||
            !authenticationState.User.IsInRole(ConstantRoles.Admin))
        {
            var uri = new Uri(NavigationManager.Uri);
            NavigationManager.NavigateTo($"/identity/account/login?returnUrl={uri.LocalPath}");
        }


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-23.zip
نظرات مطالب
Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت
تکمیل JavaScript Isolation در Blazor 6x

همانطور که کمی بالاتر نیز عنوان شد، CSS Isolation جزئی از تازه‌های Blazor 5x بود؛ اکنون مشابه این قابلیت جهت فایل‌های js. به Blazor 6x هم اضافه شده‌است و روش کار با آن نیز همانند CSS Isolation است (البته این قابلیت از نگارش 5 هم وجود داشت؛ اما اینبار دیگر نیازی به تعریف فایل ماژول آن در wwwroot نیست). یعنی اگر کامپوننت ما در مسیر Pages/Panel.razor قرار داشته باشد، می‌توان برای این تک فایل، فایل js. متناظری را به نام Pages/Panel.razor.js تعریف کرد؛ با الگوی Component>.razor.js>. سپس اگر محتوی این فایل js. به صورت زیر باشد:
export function error(){
    alert('oops, an error');
}
روش فراخوانی آن در همان کامپوننت به صورت زیر خواهد بود (یعنی در اصل دیگر مهم نیست که این فایل js. کجا قرار می‌گیرد، هنگام publish به خروجی کپی خواهد شد):
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./Panel.razor.js");
await module.InvokeVoidAsync("error");
مزیت اینکار، نزدیک نگه داشتن static assets یک کامپوننت، در کنار آن است و به این ترتیب قابل درک‌تر کردن برنامه:
Pages/Panel.razor
Pages/Panel.razor.js
Pages/Panel.razor.css
به علاوه JS Isolation دو مزیت دیگر را هم به همراه دارد:
- دیگر نیازی به تعریف توابع و متدهای جاوا اسکریپتی در global namespace نیست (این متدها و اشیاء، به شیء سراسری window اضافه نمی‌شوند). Isolation در اینجا در اصل به معنای امکان استفاده‌ی از JavaScript modules است.
- استفاده کنندگان از پروژه‌های کتابخانه‌ای دیگر نیازی به الحاق دستی این فایل‌ها ندارند. منظور از الحاق دستی یا ذکر src تگ script اضافه شده به index.html، در مطلب تولید کتابخانه‌های Razor توضیح داده شده‌است و از الگوی content/{PACKAGE ID}/{SCRIPT PATH AND FILENAME (.js)}_/. جهت مشخص سازی نام نهایی فایل js. به همراه کتابخانه، پیروی می‌کند. البته اگر نیاز است این نوع فایل‌ها در کتابخانه‌ها مستقیما استفاده شوند، باید مسیر فوق در کدها حتما ذکر شود:
_module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/RazorClassLibrary/componentName.razor.js");

یک نکته: روش صحیح کار با ماژول‌ها همانطور که در نکات فوق نیز بررسی شد، به صورت زیر است و باید در OnAfterRenderAsync شروع شده و سپس در آخر کار Dispose شوند:
export function showPrompt(message) {
  return prompt(message, 'Type anything here');
}

@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference? module;
    private string? result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string?> Prompt(string message) =>
        module is not null ? 
            await module.InvokeAsync<string>("showPrompt", message) : null;

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}
مطالب دوره‌ها
تزریق وابستگی‌ها
خوب! بالاخره بعد از دو قسمت قبل، به بحث اصلی تزریق وابستگی‌ها رسیدیم. بحثی که بسیاری از برنامه نویس‌های تصور می‌کنند همان IoC است که پیشتر در مورد آن بحث شد.

تزریق وابستگی‌ها یا DI چیست؟

تزریق وابستگی‌ها یکی از انواع IoC بوده که در آن ایجاد و انقیاد (binding) یک وابستگی، در خارج از کلاسی که به آن نیاز دارد صورت می‌گیرد. روش‌های متفاوتی برای ارائه این وابستگی وهله سازی شده در خارج از کلاس به آن وجود دارد که در ادامه مورد بررسی قرار خواهند گرفت.
یک مثال: بسته غذایی را که با خود به سر کار می‌برید درنظر بگیرید. تمام اجزای مورد نیاز تشکیل دهنده یک بسته غذا عموما داخل آن قرار گرفته و حمل می‌شوند. حالت عکس آن زمانی است که در محل کار به شما غذا می‌دهند. این دقیقا همان حالتی است که تزریق وابستگی‌ها کار می‌کند؛ یک سری سرویس‌های خارجی، نیازهای کلاس جاری را برآورده می‌سازند.


در تصویر فوق یک طراحی مبتنی بر معکوس سازی کنترل‌ها را مشاهده می‌کنید. وابستگی‌های یک کلاس توسط اینترفیسی مشخص شده و سپس کلاسی وجود دارد که این وابستگی را پیاده سازی کرده است.
همچنین در این تصویر یک خط منقطع از کلاس MyClass به کلاس Dependency رسم شده است. این خط، مربوط به حالتی است که خود کلاس، مستقیما کار وهله سازی وابستگی مورد نیاز خود را انجام دهد؛ هر چند اینترفیسی نیز در این بین تعریف شده باشد. بنابراین اگر در بین کدهای این کلاس، چنین کدی مشاهده شد:
 IDependency dependency= new Dependency();
تعریف dependency از نوع IDependency به معنای معکوس سازی کنترل نبوده و عملا همان معماری سابق و متداول بکارگرفته شده است. برای بهبود این وضعیت، از تزریق وابستگی‌ها کمک خواهیم گرفت:


در اینجا یک کلاس دیگر به نام Injector اضافه شده است که قابلیت تزریق وابستگی‌ها را به کلاس نیازمند آن به روش‌های مختلفی دارا است. کار کلاس Injector، وهله سازی MyClass و همچنین وابستگی‌های آن می‌باشد؛ سپس وابستگی را دراختیار MyClass قرار می‌دهد.


تزریق وابستگی‌ها در سازنده کلاس

یکی از انواع روش‌های تزریق وابستگی‌ها، Constructor Injection و یا تزریق وابستگی‌ها در سازنده کلاس می‌باشد که متداول‌ترین نوع آن‌ها نیز به‌شمار می‌رود. در این حالت، وابستگی پس از وهله سازی، از طریق پارامترهای سازنده یک کلاس، در اختیار آن قرار می‌گیرند.
یک مثال:
public class Shopper
{
   private readonly ICreditCard _creditCard;
   public Shopper(ICreditCard creditCard)
   {
      _creditCard = creditCard;
   }
}
در اینجا شما یک کلاس خریدار را مشاهده می‌کنید که وابستگی مورد نیاز خود را به شکل یک اینترفیس از طریق سازنده کلاس دریافت می‌کند.
 ICreditCard creditCard = new MasterCard();
Shopper shopper = new Shopper(creditCard);
برای نمونه، وهله‌ای از مستر کارتی که  ICreditCard را پیاده سازی کرده باشد، می‌توان به سازنده این کلاس ارسال کرد. کار وهله سازی وابستگی و واژه کلیدی new به خارج از کلاس استفاده کننده از وابستگی منتقل شده است. بنابراین همانطور که ملاحظه می‌کنید، این مفاهیم آنچنان پیچیده نبوده و به همین سادگی قابل تعریف و اعمال هستند.


تزریق در خواص عمومی کلاس

Setter Injection و یا تزریق در خواص عمومی یک کلاس، یکی دیگر از روش‌های تزریق وابستگی‌ها است (setter در اینجا منظور همان get و set یک خاصیت می‌باشد).
در حالت تزریق وابستگی‌ها در سازنده کلاس، امکان وهله سازی آن کلاس بدون ارسال وابستگی‌ها به سازنده آن ممکن نیست؛ اما در اینجا خیر و امکان وهله سازی و استفاده از یک کلاس، پیش از اعمال وابستگی‌ها نیز وجود دارد. بنابراین امکان تغییر و تعویض وابستگی‌ها پس از وهله سازی کلاس نیز میسر است.
public class Shopper
{
   public ICreditCard CreditCard { get; set; }
}

ICreditCard creditCard = new MasterCard();
Shopper shopper = new Shopper();
shopper.CreditCard = creditCard;
نمونه‌ای از این روش را در مثال فوق مشاهده می‌کنید. در این کلاس، وابستگی مورد نیاز از طریق یک خاصیت عمومی دریافت شده است.


تزریق اینترفیس‌ها

تزریق اینترفیس‌ها نیز یکی دیگر از روش‌های تزریق وابستگی‌ها است؛ اما آنچنان استفاده گسترده‌ای برخلاف دو روش قبل نیافته است.
در این روش نه از سازنده کلاس استفاده می‌شود و نه از یک خاصیت عمومی. ابتدا یک اینترفیس که بیانگر ساختار کلاسی که قرار است تزریق شود ایجاد می‌گردد. سپس این اینترفیس را در کلاس وابستگی مورد نظر پیاده سازی خواهیم کرد. در این اینترفیس نیاز است متد خاصی تعریف شود تا کار تزریق وابستگی را انجام دهد.
یک مثال:
public interface IDependOnCreditCard
{
   void Inject(ICreditCard creditCard);
}

public class Shopper : IDependOnCreditCard
{
  private ICreditCard creditCard;
  public void Inject(ICreditCard creditCard)
  {
     this.creditCard = creditCard;
  }
}  

ICreditCard creditCard = new MasterCard();
Shopper shopper = new Shopper();
((IDependOnCreditCard)shopper).Inject(creditCard);
در اینجا یک اینترفیس به نام IDependOnCreditCard  تعریف گردیده و متد خاصی را به نام Inject تدارک دیده است که نوعی از کردیت کارد را دریافت می‌کند.
در ادامه کلاس خریدار، اینترفیس IDependOnCreditCard را پیاده سازی کرده است. به این ترتیب کلاس Injector تنها کافی است بداند تا این متد خاص را باید جهت تنظیم و تزریق وابستگی‌ها فراخوانی نماید. این روش نسبتا شبیه به روش Setter injection است.
این روش بیشتر می‌تواند جهت فریم ورک‌هایی که قابلیت یافتن کلیه کلاس‌های مشتق شده از IDependOnCreditCard  را داشته و سپس می‌دانند که باید متد Inject آن‌ها را فراخوانی کنند، مناسب است.


نکاتی که باید حین کار با تزریق وابستگی‌ها درنظر داشت

الف) حین استفاده از تزریق وابستگی‌ها، وهله سازی به تاخیر افتاده وابستگی‌ها میسر نیست. برای مثال زمانیکه یک وابستگی قرار است در سازنده کلاسی تزریق شود، وهله سازی آن باید پیش از نیاز واقعی به آن صورت گیرد. البته امکان استفاده از اشیاء Lazy دات نت 4 برای مدیریت این مساله وجود دارد اما در حالت کلی، دیگر همانند قبل و روش‌های متداول، وهله سازی تنها زمانیکه نیاز به آن وابستگی خاص باشد، میسر نیست. به همین جهت باید به تعداد وابستگی‌هایی که قرار است در یک کلاس استفاده شوند نیز جهت کاهش مصرف حافظه دقت داشت.
ب) یکی از مزایای دیگر تزریق وابستگی‌ها، ساده‌تر شدن نوشتن آزمون‌های واحد است. زیرا تهیه Mocks در این حالت کار با اینترفیس‌ها بسیار ساده‌تر است. اما باید دقت داشت، کلاسی که در سازنده آن حداقل 10 اینترفیس را به عنوان وابستگی دریافت می‌کند، احتمالا دچار مشکلاتی در طراحی و همچنین مصرف حافظه می‌باشد.
 
مطالب
ایجاد Drop Down List های آبشاری توسط Kendo UI
پیشتر مطلبی را در مورد ایجاد Drop Down List‌های به هم پیوسته توسط jQuery Ajax در این سایت مطالعه کرده بودید. شبیه به همان مطلب را اینبار قصد داریم توسط Kendo UI پیاده سازی کنیم.


مدل‌های برنامه

در اینجا قصد داریم لیست گروه‌ها را به همراه محصولات مرتبط با آن‌ها، توسط دو drop down list نمایش دهیم:
public class Category
{
    public int CategoryId { set; get; }
    public string CategoryName { set; get; }
 
    [JsonIgnore]
    public IList<Product> Products { set; get; }
}


public class Product
{
    public int ProductId { set; get; }
    public string ProductName { set; get; }
}
از ویژگی JsonIgnore جهت عدم درج لیست محصولات، در خروجی JSON نهایی تولیدی گروه‌ها، استفاده شده‌است.


منبع داده JSON سمت سرور

پس از مشخص شدن مدل‌های برنامه، اکنون توسط دو اکشن متد، لیست گروه‌ها و همچنین لیست محصولات یک گروه خاص را با فرمت JSON بازگشت می‌دهیم:
using System.Linq;
using System.Text;
using System.Web.Mvc;
using KendoUI12.Models;
using Newtonsoft.Json;
 
namespace KendoUI12.Controllers
{
 
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View(); // shows the page.
        }
 
        [HttpGet]
        public ActionResult GetCategories()
        {
            return new ContentResult
            {
                Content = JsonConvert.SerializeObject(CategoriesDataSource.Items),
                ContentType = "application/json",
                ContentEncoding = Encoding.UTF8
            };
        }
 
        [HttpGet]
        public ActionResult GetProducts(int categoryId)
        {
 
            var products = CategoriesDataSource.Items
                            .Where(category => category.CategoryId == categoryId)
                            .SelectMany(category => category.Products)
                            .ToList();
 
            return new ContentResult
            {
                Content = JsonConvert.SerializeObject(products),
                ContentType = "application/json",
                ContentEncoding = Encoding.UTF8
            };
        }
    }
}
بار اولی که صفحه بارگذاری می‌شود، توسط یک درخواست Ajax ایی، لیست گروه‌ها دریافت خواهد شد. سپس با انتخاب یک گروه، اکشن متد GetProducts جهت بازگرداندن لیست محصولات آن گروه، فراخوانی می‌گردد.
در اینجا به عمد از JsonConvert.SerializeObject استفاده شده‌است تا ویژگی JsonIgnore کلاس گروه‌ها، توسط کتابخانه‌ی JSON.NET مورد استفاده قرار گیرد (ASP.NET MVC برخلاف ASP.NET Web API به صورت پیش فرض از JSON.NET استفاده نمی‌کند).


کدهای سمت کاربر برنامه

کدهای جاوا اسکریپتی Kendo UI را جهت تعریف دو drop down list به هم مرتبط و آبشاری، در ادامه ملاحظه می‌کنید:
<!--نحوه‌ی راست به چپ سازی -->
<div class="k-rtl k-header demo-section">
    <label for="categories">گروه‌ها: </label><input id="categories" style="width: 270px" />
    <label for="products">محصولات: </label><input id="products" disabled="disabled" style="width: 270px" />
</div>
 
@section JavaScript
{
    <script type="text/javascript">
        $(function () {
            $("#categories").kendoDropDownList({
                optionLabel: "انتخاب گروه...",
                dataTextField: "CategoryName",
                dataValueField: "CategoryId",
                dataSource: {
                    transport: {
                        read: {
                            url: "@Url.Action("GetCategories", "Home")",
                            dataType: "json",
                            contentType: 'application/json; charset=utf-8',
                            type: 'GET'
                        }
                    }
                }
            });
 
            $("#products").kendoDropDownList({
                autoBind: false, // won’t try and read from the DataSource when it first loads
                cascadeFrom: "categories", // the id of the DropDown you want to cascade from
                optionLabel: "انتخاب محصول...",
                dataTextField: "ProductName",
                dataValueField: "ProductId",
                dataSource: {
                    // When the serverFiltering is disabled, then the combobox will not make any additional requests to the server.
                    serverFiltering: true, // the DataSource will send filter values to the server
                    transport: {
                        read: {
                            url: "@Url.Action("GetProducts", "Home")",
                            dataType: "json",
                            contentType: 'application/json; charset=utf-8',
                            type: 'GET',
                            data: function () {
                                return { categoryId: $("#categories").val() };
                            }
                        }
                    }
                }
            });
        });
    </script>
 
    <style scoped>
        .demo-section {
            width: 100%;
            height: 100px;
        }
    </style>
}
دراپ داون اول، به صورت متداولی تعریف شده‌است. ابتدا فیلدهای Text و Value هر ردیف آن مشخص و سپس منبع داده آن به اکشن متد GetCategories تنظیم گردیده‌است. به این ترتیب با اولین بار مشاهده‌ی صفحه، این دراپ داون پر خواهد شد.
سپس دراپ دوم که وابسته‌است به دراپ داون اول، با این نکات طراحی شده‌است:
الف) خاصیت autoBind آن به false تنظیم شده‌است. به این ترتیب این دراپ داون در اولین بار نمایش صفحه، به سرور جهت دریافت اطلاعات مراجعه نخواهد کرد.
ب) خاصیت cascadeFrom آن به id دراپ داون اول تنظیم شده‌است.
ج) در منبع داده‌ی آن دو تغییر مهم وجود دارند:
- خاصیت serverFiltering به true تنظیم شده‌است. این مورد سبب خواهد شد تا آیتم گروه انتخاب شده، به سرور ارسال شود.
- خاصیت data نیز تنظیم شده‌است. این مورد پارامتر categoryId اکشن متد GetProducts را تامین می‌کند و مقدار آن از مقدار انتخاب شده‌ی دراپ داون اول دریافت می‌گردد.

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


سپس با انتخاب یک گروه، لیست محصولات مرتبط با آن در دراپ داون دوم ظاهر می‌گردند:



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
مطالب
اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity
پروژه‌ی ASP.NET Identity که نسل جدید سیستم Authentication و Authorization مخصوص ASP.NET است، دارای دو سری مثال رسمی است:
الف) مثال‌های کدپلکس
 ب) مثال نیوگت

در ادامه قصد داریم مثال نیوگت آن‌را که مثال کاملی است از نحوه‌ی استفاده از ASP.NET Identity در ASP.NET MVC، جهت اعمال الگوی واحد کار و تزریق وابستگی‌ها، بازنویسی کنیم.


پیشنیازها
- برای درک مطلب جاری نیاز است ابتدا دور‌ه‌ی مرتبطی را در سایت مطالعه کنید و همچنین با نحوه‌ی پیاده سازی الگوی واحد کار در EF Code First آشنا باشید.
- به علاوه فرض بر این است که یک پروژه‌ی خالی ASP.NET MVC 5 را نیز آغاز کرده‌اید و توسط کنسول پاور شل نیوگت، فایل‌های مثال Microsoft.AspNet.Identity.Samples را به آن افزوده‌اید:
 PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre


ساختار پروژه‌ی تکمیلی

همانند مطلب پیاده سازی الگوی واحد کار در EF Code First، این پروژه‌ی جدید را با چهار اسمبلی class library دیگر به نام‌های
AspNetIdentityDependencyInjectionSample.DataLayer
AspNetIdentityDependencyInjectionSample.DomainClasses
AspNetIdentityDependencyInjectionSample.IocConfig
AspNetIdentityDependencyInjectionSample.ServiceLayer
تکمیل می‌کنیم.


ساختار پروژه‌ی AspNetIdentityDependencyInjectionSample.DomainClasses

مثال Microsoft.AspNet.Identity.Samples بر مبنای primary key از نوع string است. برای نمونه کلاس کاربران آن‌را به نام ApplicationUser در فایل Models\IdentityModels.cs می‌توانید مشاهده کنید. در مطلب جاری، این نوع پیش فرض، به نوع متداول int تغییر خواهد یافت. به همین جهت نیاز است کلاس‌های ذیل را به پروژه‌ی DomainClasses اضافه کرد:
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNet.Identity.EntityFramework;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class ApplicationUser : IdentityUser<int, CustomUserLogin, CustomUserRole, CustomUserClaim>
  {
   // سایر خواص اضافی در اینجا
 
   [ForeignKey("AddressId")]
   public virtual Address Address { get; set; }
   public int? AddressId { get; set; }
  }
}

using System.Collections.Generic;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class Address
  {
   public int Id { get; set; }
   public string City { get; set; }
   public string State { get; set; }
 
   public virtual ICollection<ApplicationUser> ApplicationUsers { set; get; }
  }
}

using Microsoft.AspNet.Identity.EntityFramework;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class CustomRole : IdentityRole<int, CustomUserRole>
  {
   public CustomRole() { }
   public CustomRole(string name) { Name = name; }
 
 
  }
}

using Microsoft.AspNet.Identity.EntityFramework;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class CustomUserClaim : IdentityUserClaim<int>
  {
 
  }
}

using Microsoft.AspNet.Identity.EntityFramework;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class CustomUserLogin : IdentityUserLogin<int>
  {
 
  }
}

using Microsoft.AspNet.Identity.EntityFramework;
 
namespace AspNetIdentityDependencyInjectionSample.DomainClasses
{
  public class CustomUserRole : IdentityUserRole<int>
  {
 
  }
}
در اینجا نحوه‌ی تغییر primary key از نوع string را به نوع int، مشاهده می‌کنید. این تغییر نیاز به اعمال به کلاس‌های کاربران و همچنین نقش‌های آن‌ها نیز دارد. به همین جهت صرفا تغییر کلاس ابتدایی ApplicationUser کافی نیست و باید کلاس‌های فوق را نیز اضافه کرد و تغییر داد.
بدیهی است در اینجا کلاس پایه کاربران را می‌توان سفارشی سازی کرد و خواص دیگری را نیز به آن افزود. برای مثال در اینجا یک کلاس جدید آدرس تعریف شده‌است که ارجاعی از آن در کلاس کاربران نیز قابل مشاهده است.
سایر کلاس‌های مدل‌های اصلی برنامه که جداول بانک اطلاعاتی را تشکیل خواهند داد نیز در آینده به همین اسمبلی DomainClasses اضافه می‌شوند.


ساختار پروژه‌ی AspNetIdentityDependencyInjectionSample.DataLayer جهت اعمال الگوی واحد کار

اگر به همان فایل Models\IdentityModels.cs ابتدایی پروژه که اکنون کلاس ApplicationUser آن‌را به پروژه‌ی DomainClasses منتقل کرده‌ایم، مجددا مراجعه کنید، کلاس DbContext مخصوص ASP.NET Identity نیز در آن تعریف شده‌است:
 public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
این کلاس را به پروژه‌ی DataLayer منتقل می‌کنیم و از آن به عنوان DbContext اصلی برنامه استفاده خواهیم کرد. بنابراین دیگر نیازی نیست چندین DbContext در برنامه داشته باشیم. IdentityDbContext، در اصل از DbContext مشتق شده‌است.
اینترفیس IUnitOfWork برنامه، در پروژه‌ی DataLayer چنین شکلی را دارد که نمونه‌ای از آن‌را در مطلب آشنایی با نحوه‌ی پیاده سازی الگوی واحد کار در EF Code First، پیشتر ملاحظه کرده‌اید.
using System.Collections.Generic;
using System.Data.Entity;
 
namespace AspNetIdentityDependencyInjectionSample.DataLayer.Context
{
  public interface IUnitOfWork
  {
   IDbSet<TEntity> Set<TEntity>() where TEntity : class;
   int SaveAllChanges();
   void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class;
   IList<T> GetRows<T>(string sql, params object[] parameters) where T : class;
   IEnumerable<TEntity> AddThisRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class;
   void ForceDatabaseInitialize();
  }
}
اکنون کلاس ApplicationDbContext منتقل شده به DataLayer یک چنین امضایی را خواهد یافت:
public class ApplicationDbContext :
  IdentityDbContext<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>,
  IUnitOfWork
{
  public DbSet<Category> Categories { set; get; }
  public DbSet<Product> Products { set; get; }
  public DbSet<Address> Addresses { set; get; }
تعریف آن باید جهت اعمال کلاس‌های سفارشی سازی شده‌ی کاربران و نقش‌های آن‌ها برای استفاده از primary key از نوع int به شکل فوق، تغییر یابد. همچنین در انتهای آن مانند قبل، IUnitOfWork نیز ذکر شده‌است. پیاده سازی کامل این کلاس را از پروژه‌ی پیوست انتهای بحث می‌توانید دریافت کنید.
کار کردن با این کلاس، هیچ تفاوتی با DbContext‌های متداول EF Code First ندارد و تمام اصول آن‌ها یکی است.

در ادامه اگر به فایل App_Start\IdentityConfig.cs مراجعه کنید، کلاس ذیل در آن قابل مشاهده‌است:
 public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext>
نیازی به این کلاس به این شکل نیست. آن‌را حذف کنید و در پروژه‌ی DataLayer، کلاس جدید ذیل را اضافه نمائید:
using System.Data.Entity.Migrations;
 
namespace AspNetIdentityDependencyInjectionSample.DataLayer.Context
{
  public class Configuration : DbMigrationsConfiguration<ApplicationDbContext>
  {
   public Configuration()
   {
    AutomaticMigrationsEnabled = true;
    AutomaticMigrationDataLossAllowed = true;
   }
  }
}
در این مثال، بحث migrations به حالت خودکار تنظیم شده‌است و تمام تغییرات در پروژه‌ی DomainClasses را به صورت خودکار به بانک اطلاعاتی اعمال می‌کند. تا همینجا کار تنظیم DataLayer به پایان می‌رسد.


ساختار پروژ‌ه‌ی AspNetIdentityDependencyInjectionSample.ServiceLayer

در ادامه مابقی کلاس‌‌های موجود در فایل App_Start\IdentityConfig.cs را به لایه سرویس برنامه منتقل خواهیم کرد. همچنین برای آن‌ها یک سری اینترفیس جدید نیز تعریف می‌کنیم، تا تزریق وابستگی‌ها به نحو صحیحی صورت گیرد. اگر به فایل‌های کنترلر این مثال پیش فرض مراجعه کنید (پیش از تغییرات بحث جاری)، هرچند به نظر در کنترلرها، کلاس‌های موجود در فایل App_Start\IdentityConfig.cs تزریق شده‌اند، اما به دلیل عدم استفاده از اینترفیس‌ها، وابستگی کاملی بین جزئیات پیاده سازی این کلاس‌ها و نمونه‌های تزریق شده به کنترلرها وجود دارد و عملا معکوس سازی واقعی وابستگی‌ها رخ نداده‌است. بنابراین نیاز است این مسایل را اصلاح کنیم.

الف) انتقال کلاس ApplicationUserManager به لایه سرویس برنامه
کلاس ApplicationUserManager فایل App_Start\IdentityConfig.c را به لایه سرویس منتقل می‌کنیم:
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNetIdentityDependencyInjectionSample.DomainClasses;
using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.DataProtection;
 
namespace AspNetIdentityDependencyInjectionSample.ServiceLayer
{
  public class ApplicationUserManager
   : UserManager<ApplicationUser, int>, IApplicationUserManager
  {
   private readonly IDataProtectionProvider _dataProtectionProvider;
   private readonly IIdentityMessageService _emailService;
   private readonly IApplicationRoleManager _roleManager;
   private readonly IIdentityMessageService _smsService;
   private readonly IUserStore<ApplicationUser, int> _store;
 
   public ApplicationUserManager(IUserStore<ApplicationUser, int> store,
    IApplicationRoleManager roleManager,
    IDataProtectionProvider dataProtectionProvider,
    IIdentityMessageService smsService,
    IIdentityMessageService emailService)
    : base(store)
   {
    _store = store;
    _roleManager = roleManager;
    _dataProtectionProvider = dataProtectionProvider;
    _smsService = smsService;
    _emailService = emailService;
 
    createApplicationUserManager();
   }
 
 
   public void SeedDatabase()
   {
   }
 
   private void createApplicationUserManager()
   {
    // Configure validation logic for usernames
    this.UserValidator = new UserValidator<ApplicationUser, int>(this)
    {
      AllowOnlyAlphanumericUserNames = false,
      RequireUniqueEmail = true
    };
 
    // Configure validation logic for passwords
    this.PasswordValidator = new PasswordValidator
    {
      RequiredLength = 6,
      RequireNonLetterOrDigit = true,
      RequireDigit = true,
      RequireLowercase = true,
      RequireUppercase = true,
    };
 
    // Configure user lockout defaults
    this.UserLockoutEnabledByDefault = true;
    this.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
    this.MaxFailedAccessAttemptsBeforeLockout = 5;
 
    // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
    // You can write your own provider and plug in here.
    this.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser, int>
    {
      MessageFormat = "Your security code is: {0}"
    });
    this.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser, int>
    {
      Subject = "SecurityCode",
      BodyFormat = "Your security code is {0}"
    });
    this.EmailService = _emailService;
    this.SmsService = _smsService;
 
    if (_dataProtectionProvider != null)
    {
      var dataProtector = _dataProtectionProvider.Create("ASP.NET Identity");
      this.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser, int>(dataProtector);
    }
   } 
  }
}
تغییراتی که در اینجا اعمال شده‌اند، به شرح زیر می‌باشند:
- متد استاتیک Create این کلاس حذف و تعاریف آن به سازنده‌ی کلاس منتقل شده‌اند. به این ترتیب با هربار وهله سازی این کلاس توسط IoC Container به صورت خودکار این تنظیمات نیز به کلاس پایه UserManager اعمال می‌شوند.
- اگر به کلاس پایه UserManager دقت کنید، به آرگومان‌های جنریک آن یک int هم اضافه شده‌است. این مورد جهت استفاده از primary key از نوع int ضروری است.
- در کلاس پایه UserManager تعدادی متد وجود دارند. تعاریف آن‌ها را به اینترفیس IApplicationUserManager منتقل خواهیم کرد. نیازی هم به پیاده سازی این متدها در کلاس جدید ApplicationUserManager نیست؛ زیرا کلاس پایه UserManager پیشتر آن‌ها را پیاده سازی کرده‌است. به این ترتیب می‌توان به یک تزریق وابستگی واقعی و بدون وابستگی به پیاده سازی خاص UserManager رسید. کنترلری که با IApplicationUserManager بجای ApplicationUserManager کار می‌کند، قابلیت تعویض پیاده سازی آن‌را جهت آزمون‌های واحد خواهد یافت.
- در کلاس اصلی ApplicationDbInitializer پیش فرض این مثال، متد Seed هم قابل مشاهده‌است. این متد را از کلاس جدید Configuration اضافه شده به DataLayer حذف کرده‌ایم. از این جهت که در آن از متدهای کلاس ApplicationUserManager مستقیما استفاده شده‌است. متد Seed اکنون به کلاس جدید اضافه شده به لایه سرویس منتقل شده و در آغاز برنامه فراخوانی خواهد شد. DataLayer نباید وابستگی به لایه سرویس داشته باشد. لایه سرویس است که از امکانات DataLayer استفاده می‌کند.
- اگر به سازنده‌ی کلاس جدید ApplicationUserManager دقت کنید، چند اینترفیس دیگر نیز به آن تزریق شده‌اند. اینترفیس IApplicationRoleManager را ادامه تعریف خواهیم کرد. سایر اینترفیس‌های تزریق شده مانند IUserStore، IDataProtectionProvider و IIdentityMessageService جزو تعاریف اصلی ASP.NET Identity بوده و نیازی به تعریف مجدد آن‌ها نیست. فقط کلاس‌های EmailService و SmsService فایل App_Start\IdentityConfig.c را نیز به لایه سرویس منتقل کرده‌ایم. این کلاس‌ها بر اساس تنظیمات IoC Container مورد استفاده، در اینجا به صورت خودکار ترزیق خواهند شد. حالت پیش فرض آن، وهله سازی مستقیم است که مطابق کدهای فوق به حالت تزریق وابستگی‌ها بهبود یافته‌است.


ب) انتقال کلاس ApplicationSignInManager به لایه سرویس برنامه
کلاس ApplicationSignInManager فایل App_Start\IdentityConfig.c را نیز به لایه سرویس منتقل می‌کنیم.
using AspNetIdentityDependencyInjectionSample.DomainClasses;
using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
 
namespace AspNetIdentityDependencyInjectionSample.ServiceLayer
{
  public class ApplicationSignInManager :
   SignInManager<ApplicationUser, int>, IApplicationSignInManager
  {
   private readonly ApplicationUserManager _userManager;
   private readonly IAuthenticationManager _authenticationManager;
 
   public ApplicationSignInManager(ApplicationUserManager userManager,
              IAuthenticationManager authenticationManager) :
    base(userManager, authenticationManager)
   {
    _userManager = userManager;
    _authenticationManager = authenticationManager;
   }
  }
}
در اینجا نیز اینترفیس جدید IApplicationSignInManager را برای مخفی سازی پیاده سازی کلاس پایه توکار SignInManager، اضافه کرده‌ایم. این اینترفیس دقیقا حاوی تعاریف متدهای کلاس پایه SignInManager است و نیازی به پیاده سازی مجدد در کلاس ApplicationSignInManager نخواهد داشت.


ج) انتقال کلاس ApplicationRoleManager به لایه سرویس برنامه
کلاس ApplicationRoleManager فایل App_Start\IdentityConfig.c را نیز به لایه سرویس منتقل خواهیم کرد:
using AspNetIdentityDependencyInjectionSample.DomainClasses;
using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts;
using Microsoft.AspNet.Identity;
 
namespace AspNetIdentityDependencyInjectionSample.ServiceLayer
{
  public class ApplicationRoleManager : RoleManager<CustomRole, int>, IApplicationRoleManager
  {
   private readonly IRoleStore<CustomRole, int> _roleStore;
   public ApplicationRoleManager(IRoleStore<CustomRole, int> roleStore)
    : base(roleStore)
   {
    _roleStore = roleStore;
   }
 
 
   public CustomRole FindRoleByName(string roleName)
   {
    return this.FindByName(roleName); // RoleManagerExtensions
   }
 
   public IdentityResult CreateRole(CustomRole role)
   {
    return this.Create(role); // RoleManagerExtensions
   }
  }
}
روش کار نیز در اینجا همانند دو کلاس قبل است. اینترفیس جدید IApplicationRoleManager را که حاوی تعاریف متدهای کلاس پایه توکار RoleManager است، به لایه سرویس اضافه می‌کنیم. کنترلرهای برنامه با این اینترفیس بجای استفاده مستقیم از کلاس ApplicationRoleManager کار خواهند کرد.

تا اینجا کار تنظیمات لایه سرویس برنامه به پایان می‌رسد.


ساختار پروژه‌ی AspNetIdentityDependencyInjectionSample.IocConfig 

پروژه‌ی IocConfig جایی است که تنظیمات StructureMap را به آن منتقل کرده‌ایم:
using System;
using System.Data.Entity;
using System.Threading;
using System.Web;
using AspNetIdentityDependencyInjectionSample.DataLayer.Context;
using AspNetIdentityDependencyInjectionSample.DomainClasses;
using AspNetIdentityDependencyInjectionSample.ServiceLayer;
using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Owin.Security;
using StructureMap;
using StructureMap.Web;
 
namespace AspNetIdentityDependencyInjectionSample.IocConfig
{
  public static class SmObjectFactory
  {
   private static readonly Lazy<Container> _containerBuilder =
    new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);
 
   public static IContainer Container
   {
    get { return _containerBuilder.Value; }
   }
 
   private static Container defaultContainer()
   {
    return new Container(ioc =>
    {
      ioc.For<IUnitOfWork>()
        .HybridHttpOrThreadLocalScoped()
        .Use<ApplicationDbContext>();
 
      ioc.For<ApplicationDbContext>().HybridHttpOrThreadLocalScoped().Use<ApplicationDbContext>();
      ioc.For<DbContext>().HybridHttpOrThreadLocalScoped().Use<ApplicationDbContext>();
 
      ioc.For<IUserStore<ApplicationUser, int>>()
       .HybridHttpOrThreadLocalScoped()
       .Use<UserStore<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>>();
 
      ioc.For<IRoleStore<CustomRole, int>>()
       .HybridHttpOrThreadLocalScoped()
       .Use<RoleStore<CustomRole, int, CustomUserRole>>();
 
      ioc.For<IAuthenticationManager>()
        .Use(() => HttpContext.Current.GetOwinContext().Authentication);
 
      ioc.For<IApplicationSignInManager>()
        .HybridHttpOrThreadLocalScoped()
        .Use<ApplicationSignInManager>();
 
      ioc.For<IApplicationUserManager>()
        .HybridHttpOrThreadLocalScoped()
        .Use<ApplicationUserManager>();
 
      ioc.For<IApplicationRoleManager>()
        .HybridHttpOrThreadLocalScoped()
        .Use<ApplicationRoleManager>();
 
      ioc.For<IIdentityMessageService>().Use<SmsService>();
      ioc.For<IIdentityMessageService>().Use<EmailService>();
      ioc.For<ICustomRoleStore>()
        .HybridHttpOrThreadLocalScoped()
        .Use<CustomRoleStore>();
 
      ioc.For<ICustomUserStore>()
        .HybridHttpOrThreadLocalScoped()
        .Use<CustomUserStore>();
 
      //config.For<IDataProtectionProvider>().Use(()=> app.GetDataProtectionProvider()); // In Startup class
 
      ioc.For<ICategoryService>().Use<EfCategoryService>();
      ioc.For<IProductService>().Use<EfProductService>();
    });
   }
  }
}
در اینجا نحوه‌ی اتصال اینترفیس‌های برنامه را به کلاس‌ها و یا نمونه‌هایی که آن‌ها را می‌توانند پیاده سازی کنند، مشاهده می‌کنید. برای مثال IUnitOfWork به ApplicationDbContext مرتبط شده‌است و یا دوبار تعاریف متناظر با DbContext را مشاهده می‌کنید. از این تعاریف به صورت توکار توسط ASP.NET Identity زمانیکه قرار است UserStore و RoleStore را وهله سازی کند، استفاده می‌شوند و ذکر آن‌ها الزامی است.
در تعاریف فوق یک مورد را به فایل Startup.cs موکول کرده‌ایم. برای مشخص سازی نمونه‌ی پیاده سازی کننده‌ی IDataProtectionProvider نیاز است به IAppBuilder کلاس Startup برنامه دسترسی داشت. این کلاس آغازین Owin اکنون به نحو ذیل بازنویسی شده‌است و در آن، تنظیمات IDataProtectionProvider را به همراه وهله سازی CreatePerOwinContext مشاهده می‌کنید:
using System;
using AspNetIdentityDependencyInjectionSample.IocConfig;
using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts;
using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.DataProtection;
using Owin;
using StructureMap.Web;
 
namespace AspNetIdentityDependencyInjectionSample
{
  public class Startup
  {
   public void Configuration(IAppBuilder app)
   {
    configureAuth(app);
   }
 
   private static void configureAuth(IAppBuilder app)
   {
    SmObjectFactory.Container.Configure(config =>
    {
      config.For<IDataProtectionProvider>()
        .HybridHttpOrThreadLocalScoped()
        .Use(()=> app.GetDataProtectionProvider());
    });
    SmObjectFactory.Container.GetInstance<IApplicationUserManager>().SeedDatabase();
 
    // Configure the db context, user manager and role manager to use a single instance per request
    app.CreatePerOwinContext(() => SmObjectFactory.Container.GetInstance<IApplicationUserManager>());
 
    // Enable the application to use a cookie to store information for the signed in user
    // and to use a cookie to temporarily store information about a user logging in with a third party login provider
    // Configure the sign in cookie
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
      AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
      LoginPath = new PathString("/Account/Login"),
      Provider = new CookieAuthenticationProvider
      {
       // Enables the application to validate the security stamp when the user logs in.
       // This is a security feature which is used when you change a password or add an external login to your account.
       OnValidateIdentity = SmObjectFactory.Container.GetInstance<IApplicationUserManager>().OnValidateIdentity()
      }
    });
    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
 
    // Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
    app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
 
    // Enables the application to remember the second login verification factor such as phone or email.
    // Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
    // This is similar to the RememberMe option when you log in.
    app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie); 
   }
 
  }
}
این تعاریف از فایل پیش فرض Startup.Auth.cs پوشه‌ی App_Start دریافت و جهت کار با IoC Container برنامه، بازنویسی شده‌اند.


تنظیمات برنامه‌ی اصلی ASP.NET MVC، جهت اعمال تزریق وابستگی‌ها

الف) ابتدا نیاز است فایل Global.asax.cs را به نحو ذیل بازنویسی کنیم:
using System;
using System.Data.Entity;
using System.Web;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using AspNetIdentityDependencyInjectionSample.DataLayer.Context;
using AspNetIdentityDependencyInjectionSample.IocConfig;
using StructureMap.Web.Pipeline;
 
namespace AspNetIdentityDependencyInjectionSample
{
  public class MvcApplication : HttpApplication
  {
   protected void Application_Start()
   {
    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
 
 
    setDbInitializer();
    //Set current Controller factory as StructureMapControllerFactory
    ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory());
   }
 
   protected void Application_EndRequest(object sender, EventArgs e)
   {
    HttpContextLifecycle.DisposeAndClearAll();
   }
 
   public class StructureMapControllerFactory : DefaultControllerFactory
   {
    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
      if (controllerType == null)
       throw new InvalidOperationException(string.Format("Page not found: {0}", requestContext.HttpContext.Request.RawUrl));
      return SmObjectFactory.Container.GetInstance(controllerType) as Controller;
    }
   }
 
   private static void setDbInitializer()
   {
    Database.SetInitializer(new MigrateDatabaseToLatestVersion<ApplicationDbContext, Configuration>());
    SmObjectFactory.Container.GetInstance<IUnitOfWork>().ForceDatabaseInitialize();
   }
  }
}
در اینجا در متد setDbInitializer، نحوه‌ی استفاده و تعریف فایل Configuration لایه Data را ملاحظه می‌کنید؛ به همراه متد آغاز بانک اطلاعاتی و اعمال تغییرات لازم به آن در ابتدای کار برنامه. همچنین ControllerFactory برنامه نیز به StructureMapControllerFactory تنظیم شده‌است تا کار تزریق وابستگی‌ها به کنترلرهای برنامه به صورت خودکار میسر شود. در پایان کار هر درخواست نیز منابع Disposable رها می‌شوند.

ب) به پوشه‌ی Models برنامه مراجعه کنید. در اینجا در هر کلاسی که Id از نوع string وجود داشت، باید تبدیل به نوع int شوند. چون primary key برنامه را به نوع int تغییر داده‌ایم. برای مثال کلاس‌های EditUserViewModel و RoleViewModel باید تغییر کنند.

ج) اصلاح کنترلرهای برنامه جهت اعمال تزریق وابستگی‌ها

اکنون اصلاح کنترلرها جهت اعمال تزریق وابستگی‌ها ساده‌است. در ادامه نحوه‌ی تغییر امضای سازنده‌های این کنترلرها را جهت استفاده از اینترفیس‌های جدید مشاهده می‌کنید:
  [Authorize]
public class AccountController : Controller
{
  private readonly IAuthenticationManager _authenticationManager;
  private readonly IApplicationSignInManager _signInManager;
  private readonly IApplicationUserManager _userManager;
  public AccountController(IApplicationUserManager userManager,
          IApplicationSignInManager signInManager,
          IAuthenticationManager authenticationManager)
  {
   _userManager = userManager;
   _signInManager = signInManager;
   _authenticationManager = authenticationManager;
  }

  [Authorize]
public class ManageController : Controller
{
  // Used for XSRF protection when adding external logins
  private const string XsrfKey = "XsrfId";
 
  private readonly IAuthenticationManager _authenticationManager;
  private readonly IApplicationUserManager _userManager;
  public ManageController(IApplicationUserManager userManager, IAuthenticationManager authenticationManager)
  {
   _userManager = userManager;
   _authenticationManager = authenticationManager;
  }

  [Authorize(Roles = "Admin")]
public class RolesAdminController : Controller
{
  private readonly IApplicationRoleManager _roleManager;
  private readonly IApplicationUserManager _userManager;
  public RolesAdminController(IApplicationUserManager userManager,
           IApplicationRoleManager roleManager)
  {
   _userManager = userManager;
   _roleManager = roleManager;
  }


  [Authorize(Roles = "Admin")]
public class UsersAdminController : Controller
{
  private readonly IApplicationRoleManager _roleManager;
  private readonly IApplicationUserManager _userManager;
  public UsersAdminController(IApplicationUserManager userManager,
           IApplicationRoleManager roleManager)
  {
   _userManager = userManager;
   _roleManager = roleManager;
  }
پس از این تغییرات، فقط کافی است بجای خواص برای مثال RoleManager سابق از فیلدهای تزریق شده در کلاس، مثلا roleManager_ جدید استفاده کرد. امضای متدهای یکی است و تنها به یک search و replace نیاز دارد.
البته تعدادی اکشن متد نیز در اینجا وجود دارند که از string id استفاده می‌کنند. این‌ها را باید به int? Id تغییر داد تا با نوع primary key جدید مورد استفاده تطابق پیدا کنند.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
AspNetIdentityDependencyInjectionSample


معادل این پروژه جهت ASP.NET Core Identity : «سفارشی سازی ASP.NET Core Identity - قسمت اول - موجودیت‌های پایه و DbContext برنامه »
مطالب
ویژگی های کمتر استفاده شده در NET. - بخش سوم

__arglist __reftype __makeref __refvalue کلمات کلیدی

در حالیکه، ویرایشگر Visual Studio این کلمات را به صورت رنگی و جزء کلمات کلیدی نمایش می‌دهد، ولی به دلیل عدم وجود مستندات برای این کلمات کلیدی، برای استفاده از آنها باید مراقب باشید؛ چرا که ممکن است به اندازه کافی تست نشده باشند. 
شما می‌توانید با استفاده از کلمه کلیدی makeref__ یک TypeReference را از یک متغیر، ایجاد کنید. با استفاده از کلمه کلیدی reftype__ می‌توانید نوع اصلی از متغیری را که TypeReference را از آن ایجاد کرده اید، استخراج کنید. در انتها می‌توانید با استفاده از کلمه کلیدی refvalue__ مقدار متغیر را از TypeReference ایجاد شده، بدست آورد. با استفاده از کلمه کلیدی arglist__ همانند کلمه کلیدی params می‌توانید به لیستی از پارامترهای یک تابع دسترسی داشته باشید.
var i = 28;
TypedReference tr = __makeref( i );
Type t = __reftype( tr );
Console.WriteLine( t );
int rv = __refvalue( tr, int );
Console.WriteLine( rv );
ArglistTest.DisplayNumbers( __arglist( 1, 2, 3, 5, 6 ) );
و برای استفاده از arglist__ کلاس ArglistTest را پیاده سازی میکنیم.
public static class ArglistTest
{
    public static void DisplayNumbers( __arglist )
    {
        var ai = new ArgIterator( __arglist );
        while ( ai.GetRemainingCount() > 0 )
        {
            var tr = ai.GetNextArg();
            Console.WriteLine( TypedReference.ToObject( tr ) );
        }
    }
}
شی ArgIterator لیست آرگومان‌ها را از اولین آرگومان اختیاری، شروع به شمارش می‌کند. این سازنده برای استفاده در زبان C++/C ایجاد شده است.

Environment.NewLine

رشته خط جدید (↵  Enter) تعریف شده در محیط در حال استفاده را می‌توان با استفاده از این دستور بدست آورد.
Console.WriteLine( "NewLine: {0}first line{0}second line{0}third line", Environment.NewLine );
این رشته شامل "r\n\" برای پلتفرم‌های غیر یونیکس و رشته "n\" برای پلتفرم‌های یونیکس است.

ExceptionDispatchInfo

ExceptionDispatchInfo بیان کننده یک استثناء در یک نقطه خاص از کد، که وضعیت آن قبلا کپچر شده‌است، می‌باشد. شما می‌توانید با استفاده از متد ExceptionDispatchInfo.Throw  (در فضای نام System.Runtime.ExceptionServices) یک استثناء را (با حفظ Stack Trace اصلی) ایجاد کنید.
ExceptionDispatchInfo possibleException = null;
try
{
    int.Parse( "a" );
}
catch ( FormatException ex )
{
    possibleException = ExceptionDispatchInfo.Capture( ex );
}
possibleException?.Throw();


Debug.Assert & Debug.WriteIf & Debug.Indent 

Debug.Assert  – بررسی صحت شرط تعیین شده و در صورت false بودن شرط، نمایش پیام نوشته شده به همراه call stack مربوطه می‌شود.
Debug.Assert(1 == 0, "عدد 1 برابر با 0 نیست");
و خروجی آن در تصویر زیر قابل مشاهده است:

Debug.WriteIf  – در صورت صحت شرط تعیین شده، پیام مشخص شده‌ای را در پنجره output نشان می‌دهد.
Debug.WriteIf( 1 == 1, "display message in output window :D" );

Debug.Indent/Debug.Unindent - برای افزایش/کاهش یک واحد تورفتگی در خروجی نمایش داده شده در پنجره Output، استفاده می‌شود.
Debug.WriteLine("تست تورفتگی");
Debug.Indent();
Debug.WriteLine("یک واحد افزایش داده شد");
Debug.Unindent();
Debug.WriteLine("یک واحد کاهش داده شد");
Debug.WriteLine("پایان تست");

مطالب
پیاده سازی SoftDelete در EF Core
در مورد حذف منطقی در EF 6x، پیشتر مطالبی را در این سایت مطالعه کرده‌اید:
- «پیاده سازی حذف منطقی در Entity framework» حذف منطقی، یکی از الگوهای بسیار پرکاربرد در برنامه‌های تجاری است. توسط آن بجای حذف فیزیکی اطلاعات، آن‌ها را تنها به عنوان رکوردی حذف شده، «علامتگذاری» می‌کنیم. مزایای آن نیز به شرح زیر هستند:
- داشتن سابقه‌ی حذف اطلاعات
- جلوگیری از cascade delete
- امکان بازیابی رکوردها و امکان ایجاد قسمتی به نام recycle bin در برنامه (شبیه به recycle bin در ویندوز که امکان بازیابی موارد حذف شده را می‌دهد)
- امکان داشتن رکوردهایی که در یک برنامه (به ظاهر) حذف شده‌اند، اما هنوز در برنامه‌ی دیگری در حال استفاده هستند.
- بالابردن میزان امنیت برنامه. فرض کنید سایت شما هک شده و شخصی، دسترسی به پنل مدیریتی و سطوح دسترسی مدیریتی برنامه را پیدا کرده‌است. در این حالت حذف تمام رکوردهای سایت توسط او، تنها به معنای تغییر یک بیت، از یک به صفر است و بازگرداندن این درجه از خسارت، تنها با روشن کردن این بیت، برطرف می‌شود.

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


نیاز به تعریف دو خاصیت جدید در هر جدول

هر جدولی که قرار است soft delete به آن اعمال شود، باید دارای دو فیلد جدید bool IsDeleted و DateTime? DeletedAt باشد. می‌توان این خواص را به هر موجودیتی به صورت دستی اضافه کرد و یا می‌توان ابتدا یک کلاس پایه‌ی abstract را برای آن ایجاد کرد:
using System;

namespace EFCoreSoftDelete.Entities
{
    public abstract class BaseEntity
    {
        public int Id { get; set; }


        public bool IsDeleted { set; get; }
        public DateTime? DeletedAt { set; get; }
    }
}
و سپس موجودیت‌هایی را که قرار است از soft delete پشتیبانی کنند، توسط آن علامتگذاری کرد؛ مانند موجودیت Blog:
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCoreSoftDelete.Entities
{
    public class Blog : BaseEntity
    {
        public string Name { set; get; }

        public virtual ICollection<Post> Posts { set; get; }
    }

    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            builder.Property(blog => blog.Name).HasMaxLength(450).IsRequired();
            builder.HasIndex(blog => blog.Name).IsUnique();

            builder.HasData(new Blog { Id = 1, Name = "Blog 1" });
            builder.HasData(new Blog { Id = 2, Name = "Blog 2" });
            builder.HasData(new Blog { Id = 3, Name = "Blog 3" });
        }
    }
}
که هر بلاگ از تعدادی مطلب تشکیل شده‌است:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCoreSoftDelete.Entities
{
    public class Post : BaseEntity
    {
        public string Title { set; get; }

        public Blog Blog { set; get; }
        public int BlogId { set; get; }
    }

    public class PostConfiguration : IEntityTypeConfiguration<Post>
    {
        public void Configure(EntityTypeBuilder<Post> builder)
        {
            builder.Property(post => post.Title).HasMaxLength(450);
            builder.HasOne(post => post.Blog).WithMany(blog => blog.Posts).HasForeignKey(post => post.BlogId);

            builder.HasData(new Post { Id = 1, BlogId = 1, Title = "Post 1" });
            builder.HasData(new Post { Id = 2, BlogId = 1, Title = "Post 2" });
            builder.HasData(new Post { Id = 3, BlogId = 1, Title = "Post 3" });
            builder.HasData(new Post { Id = 4, BlogId = 1, Title = "Post 4" });
            builder.HasData(new Post { Id = 5, BlogId = 2, Title = "Post 5" });
        }
    }
}
مزیت علامتگذاری این کلاس‌ها، امکان کوئری گرفتن از آن‌ها نیز می‌باشد که در ادامه از آن استفاده خواهیم کرد.


حذف خودکار رکوردهایی که Soft Delete شده‌اند، از نتیجه‌ی کوئری‌ها و گزارشات

تا اینجا فقط دو خاصیت ساده را به کلاس‌های مدنظر خود اضافه کرده‌ایم. پس از آن یا می‌توان در هر جائی برای مثال شرط context.Blogs.Where(blog => !blog.IsDeleted) را به صورت دستی اعمال کرد و در گزارشات، رکوردهای حذف منطقی شده را نمایش نداد و یا از زمان ارائه‌ی EF Core 2x می‌توان برای آن‌ها Query Filter تعریف کرد. برای مثال می‌توان به تنظیمات موجودیت Blog و یا Post مراجعه نمود و با استفاده از متد HasQueryFilter، همان شرط blog => !blog.IsDeleted را به صورت سراسری به تمام کوئری‌های مرتبط با این موجودیت‌ها اعمال کرد:
    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            // ...
            builder.HasQueryFilter(blog => !blog.IsDeleted);
        }
    }
از این پس ذکر context.Blogs دقیقا معنای context.Blogs.Where(blog => !blog.IsDeleted) را می‌دهد و دیگر نیازی به ذکر صریح شرط متناظر با soft delete نیست.
در این حالت کوئری‌های نهایی به صورت خودکار دارای شرط زیر خواهند شد:
SELECT [b].[Id], [b].[DeletedAt], [b].[IsDeleted], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[IsDeleted] <> CAST(1 AS bit)


اعمال خودکار QueryFilter مخصوص Soft Delete به تمام موجودیت‌ها

همانطور که عنوان شد، مزیت علامتگذاری موجودیت‌ها با کلاس پایه‌ی BaseEntity، امکان کوئری گرفتن از آن‌ها است:
namespace EFCoreSoftDelete.DataLayer
{
    public static class GlobalFiltersManager
    {
        public static void ApplySoftDeleteQueryFilters(this ModelBuilder modelBuilder)
        {
            foreach (var entityType in modelBuilder.Model
                                                    .GetEntityTypes()
                                                    .Where(eType => typeof(BaseEntity).IsAssignableFrom(eType.ClrType)))
            {
                entityType.addSoftDeleteQueryFilter();
            }
        }

        private static void addSoftDeleteQueryFilter(this IMutableEntityType entityData)
        {
            var methodToCall = typeof(GlobalFiltersManager)
                                .GetMethod(nameof(getSoftDeleteFilter), BindingFlags.NonPublic | BindingFlags.Static)
                                .MakeGenericMethod(entityData.ClrType);
            var filter = methodToCall.Invoke(null, new object[] { });
            entityData.SetQueryFilter((LambdaExpression)filter);
        }

        private static LambdaExpression getSoftDeleteFilter<TEntity>() where TEntity : BaseEntity
        {
            return (Expression<Func<TEntity, bool>>)(entity => !entity.IsDeleted);
        }
    }
}
در اینجا در ابتدا تمام موجودیت‌هایی که از BaseEntity ارث بری کرده‌اند، یافت می‌شوند. سپس بر روی آن‌ها قرار است متد SetQueryFilter فراخوانی شود. این متد بر اساس تعاریف EF Core، یک LambdaExpression کلی را قبول می‌کند که نمونه‌ی آن در متد getSoftDeleteFilter تعریف شده و سپس توسط متد addSoftDeleteQueryFilter به صورت پویا به modelBuilder اعمال می‌شود.

محل اعمال آن نیز در انتهای متد OnModelCreating است تا به صورت خودکار به تمام موجودیت‌های موجود اعمال شود:
namespace EFCoreSoftDelete.DataLayer
{
    public class ApplicationDbContext : DbContext
    {

        //...


        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.ApplyConfigurationsFromAssembly(typeof(BaseEntity).Assembly);
            modelBuilder.ApplySoftDeleteQueryFilters();
        }


مشکل! هنوز هم حذف فیزیکی رخ می‌دهد!

تنظیمات فوق، تنها بر روی کوئری‌های نوشته شده تاثیر دارند؛ اما هیچگونه تاثیری را بر روی متد Remove و سپس SaveChanges نداشته و در این حالت، هنوز هم حذف واقعی و فیزیکی رخ می‌دهد.
 برای رفع این مشکل باید به EF Core گفت، هر چند دستور حذف صادر شده، اما آن‌را تبدیل به دستور Update کن؛ یعنی فیلد IsDelete را به 1 و فیلد DeletedAt را با زمان جاری مقدار دهی کن:
namespace EFCoreSoftDelete.DataLayer
{
    public static class AuditableEntitiesManager
    {
        public static void SetAuditableEntityOnBeforeSaveChanges(this ApplicationDbContext context)
        {
            var now = DateTime.UtcNow;

            foreach (var entry in context.ChangeTracker.Entries<BaseEntity>())
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                        //TODO: ...
                        break;
                    case EntityState.Modified:
                        //TODO: ...
                        break;
                    case EntityState.Deleted:
                        entry.State = EntityState.Unchanged; //NOTE: For soft-deletes to work with the original `Remove` method.

                        entry.Entity.IsDeleted = true;
                        entry.Entity.DeletedAt = now;
                        break;
                }
            }
        }
    }
}
در اینجا با استفاده از سیستم tracking، رکوردهای حذف شده‌ی با وضعیت EntityState.Deleted، به وضعیت EntityState.Unchanged تغییر پیدا می‌کنند، تا دیگر حذف نشوند. اما در ادامه چون دو خاصیت IsDeleted و DeletedAt این موجودیت، ویرایش می‌شوند، وضعیت جدید Modified خواهد بود که به کوئری‌های Update تفسیر می‌شوند. به این ترتیب می‌توان همانند قبل یک رکورد را حذف کرد:
var post1 = context.Posts.Find(1);
if (post1 != null)
{
   context.Remove(post1);

   context.SaveChanges();
}
اما دستوری که توسط EF Core صادر می‌شود، یک Update است:
Executing DbCommand [Parameters=[@p2='1', @p0='2020-09-17T05:11:32' (Nullable = true), @p1='True'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Posts] SET [DeletedAt] = @p0, [IsDeleted] = @p1
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;

محل اعمال متد SetAuditableEntityOnBeforeSaveChanges فوق، پیش از فراخوانی SaveChanges و به صورت زیر است:
namespace EFCoreSoftDelete.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        // ...

        public override int SaveChanges(bool acceptAllChangesOnSuccess)
        {
            ChangeTracker.DetectChanges();

            beforeSaveTriggers();

            ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again.
            var result = base.SaveChanges(acceptAllChangesOnSuccess);

            ChangeTracker.AutoDetectChangesEnabled = true;
            return result;
        }

        // ...

        private void beforeSaveTriggers()
        {
            setAuditProperties();
        }

        private void setAuditProperties()
        {
            this.SetAuditableEntityOnBeforeSaveChanges();
        }
    }
}


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

حالت پیش‌فرض حذف رکوردها در EFCore به cascade delete تنظیم شده‌است. یعنی اگر blog با id=1 حذف شود، نه فقط این blog، بلکه تمام مطالب وابسته‌ی به آن نیز حذف خواهند شد. اما در اینجا اگر این بلاگ را حذف کنیم:
 ar blog1 = context.Blogs.FirstOrDefault(blog => blog.Id == 1);
if (blog1 != null)
{
   context.Remove(blog1);

   context.SaveChanges();
}
تنها تک رکورد متناظر با آن حذف منطقی شده و مطالب متناظر با آن خیر. برای رفع این مشکل باید به صورت زیر عمل کرد:
var blog1AndItsRelatedPosts = context.Blogs
    .Include(blog => blog.Posts)
    .FirstOrDefault(blog => blog.Id == 1);
if (blog1AndItsRelatedPosts != null)
{
    context.Remove(blog1AndItsRelatedPosts);

    context.SaveChanges();
}
ابتدا باید رکوردهای وابسته را توسط یک Include به حافظه وارد کرد و سپس دستور Delete را بر روی کل آن صادر نمود که یک چنین خروجی را تولید می‌کند:
SELECT [t].[Id], [t].[DeletedAt], [t].[IsDeleted], [t].[Name], [t0].[Id], [t0].[BlogId], [t0].[DeletedAt], [t0].[IsDeleted], [t0].[Title]
FROM (
SELECT TOP(1) [b].[Id], [b].[DeletedAt], [b].[IsDeleted], [b].[Name]
FROM [Blogs] AS [b]
WHERE ([b].[IsDeleted] <> CAST(1 AS bit)) AND ([b].[Id] = 1)
) AS [t]
LEFT JOIN (
SELECT [p].[Id], [p].[BlogId], [p].[DeletedAt], [p].[IsDeleted], [p].[Title]
FROM [Posts] AS [p]
WHERE [p].[IsDeleted] <> CAST(1 AS bit)
) AS [t0] ON [t].[Id] = [t0].[BlogId]
ORDER BY [t].[Id], [t0].[Id]

Executing DbCommand [Parameters=[@p2='1', @p0='2020-09-17T05:25:00' (Nullable = true), @p1='True',
 @p5='2', @p3='2020-09-17T05:25:00' (Nullable = true), @p4='True', @p8='3',
@p6='2020-09-17T05:25:00' (Nullable = true), @p7='True',
 @p11='4', @p9='2020-09-17T05:25:00' (Nullable = true), @p10='True'], CommandType='Text', CommandTimeout='30']

SET NOCOUNT ON;
UPDATE [Blogs] SET [DeletedAt] = @p0, [IsDeleted] = @p1
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p3, [IsDeleted] = @p4
WHERE [Id] = @p5;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p6, [IsDeleted] = @p7
WHERE [Id] = @p8;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p9, [IsDeleted] = @p10
WHERE [Id] = @p11;
SELECT @@ROWCOUNT;
ابتدا اولین بلاگ را حذف منطقی کرده؛ سپس تمام مطالب متناظر با آن‌را که پیشتر حذف منطقی نشده‌اند، یکی یکی به صورت حذف شده، علامتگذاری می‌کند. به این ترتیب cascade delete منطقی نیز در اینجا میسر می‌شود.


یک نکته: مشکل حذف منطقی و رکوردهای منحصربفرد

فرض کنید در جدولی، فیلد نام کاربری را به عنوان یک فیلد منحصربفرد تعریف کرده‌اید و اکنون رکوردی در این بین، حذف منطقی شده‌است. مشکلی که در آینده بروز خواهد کرد، عدم امکان ثبت رکورد جدیدی با همان نام کاربری است که حذف منطقی شده‌است؛ چون یک unique index بر روی آن وجود دارد. در این حالت اگر از SQL Server استفاده می‌کنید، از قابلیتی به نام filtered indexes پشتیبانی می‌کند که در آن امکان تعریف یک شرط و predicate، در حین تعریف ایندکس‌ها وجود دارد. در این حالت می‌توان رکوردهای حذف منطقی شده را به ایندکس وارد نکرد.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EFCoreSoftDelete.zip
مطالب
مسیریابی در Angular - قسمت هفتم - بهبودهای بصری
در این قسمت ویژگی‌های بصری را مانند مشخص سازی مسیر انتخاب شده، در منوی سایت و همچنین نمایش «لطفا منتظر بمانید» را در حین نمایش قسمت‌هایی که با تاخیر از سرور دریافت می‌شوند، پیاده سازی خواهیم کرد.


تزئین مسیر انتخاب شده در منوی سایت

برای بهبود ظاهر برنامه نیاز است منوی سایت را به نحوی تغییر دهیم که مشخص کند، اکنون کاربر کدام گزینه را انتخاب کرده‌است. این مورد شامل سلسه مراتب مسیریابی‌ها نیز می‌شود؛ برای مثال فعالسازی حالت انتخاب شده‌ی منوی سایت، به همراه برگه‌ی انتخاب شده در یکی از Child Routes.
برای پیاده سازی این قابلیت، دایرکتیو ویژه‌ای به نام routerLinkActive تدارک دیده شده‌است. این دایرکتیو را می‌توان به یک anchor tag و یا المان والد آن انتساب داد. مقدار آن‌را نیز می‌توان به یکی از کلاس‌های CSS برنامه مانند کلاس active تعریف شده‌ی در بوت استرپ تنظیم کرد. هر زمانیکه این مسیریابی فعال شود، مسیریاب به صورت خودکار این کلاس را با درج آن، به المان مرتبط اضافه می‌کند و برعکس.
برای نمونه فایل src\app\product\product-edit\product-edit.component.html را گشوده و سپس تغییرات ذیل را اعمال کنید:
<div class="wizard">
            <a [routerLink]="['info']" routerLinkActive="active">
                Basic Information
            </a>
            <a [routerLink]="['tags']" routerLinkActive="active">
                Search Tags
            </a>
</div>
در اینجا دایرکتیو‌های routerLinkActive، به هر کدام از لینک‌های تعریف شده اضافه گردیده‌اند. مقدار active در اینجا، به کلاس active بوت استرپ اشاره می‌کند. یا حتی می‌توان تعدادی کلاس جدا شده‌ی با کاما را نیز در اینجا ذکر کرد.

یک نکته: از آنجائیکه در اینجا مقدار active یک string است و نه یک خاصیت یا عبارت متغیر، به همین جهت نیازی نیست تا این دایرکتیو را به صورت [routerLinkActive] تعریف کنیم.


همانطور که مشاهده می‌کنید، همین دو تنظیم ساده سبب مشخص شدن برگه‌ی انتخابی شده‌اند.

منوی بالای سایت نیز چنین تنظیماتی را نیاز دارد. برای این منظور به فایل src\app\app.component.html که دربرگیرنده‌ی منوی سایت است مراجعه کرده و تغییرات ذیل را اعمال می‌کنیم:
    <ul class="nav navbar-nav">
      <li routerLinkActive="active">
        <a [routerLink]="['/home']">Home</a>
      </li>
      <li routerLinkActive="active">
        <a [routerLink]="['/products']">Product List</a>
      </li>
      <li routerLinkActive="active">
        <a [routerLink]="['/products', 0, 'edit']">Add Product</a>
       </li>      
    </ul>
اینبار routerLinkActive به المان‌های li اعمال شده‌است؛ چون این المان‌های لیست، شیوه نامه‌ی المان‌های anchor را بازنویسی می‌کنند و اگر routerLinkActive را به لینک‌ها اعمال می‌کردیم، تغییری مشاهده نمی‌شد.


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

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


اینبار هر دو گزینه با هم انتخاب شده‌اند. علت اینجا است که این دو مسیر دارای root URL segment یکسانی هستند؛ یا همان products/ در اینجا. به همین جهت routerLinkActive هر دو را به عنوان فعال انتخاب کرده‌است. برای مدیریت میدان دید آن می‌توان از دایرکتیو دیگری به نام routerLinkActiveOptions استفاده کرد:
      <li routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">
        <a [routerLink]="['/products']">Product List</a>
      </li>
routerLinkActiveOptions را تنها به ریشه‌ی مسیر products اعمال کرده‌ایم؛ چون این مسیر است که می‌تواند با تمام مسیرهای مشتق شده‌ی از آن نیز تطابق داشته باشد. تنظیم exact: true آن سبب خواهد شد تا تطابق با مسیرهای مشتق شده‌ی از آن ندید گرفته شوند.


اکنون کاربران بهتر می‌توانند درک کنند در کجای برنامه قرار دارند.


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

در ادامه می‌خواهیم اگر برگه‌ای دارای مشکلات اعتبارسنجی بود، آیکن خطایی را در کنار برچسب آن برگه نمایش دهیم. به این ترتیب مدیریت چندین برگه برای کاربران ساده‌تر خواهد شد و به سادگی می‌توانند برگه‌های مشکل دار را پیدا کنند.
در انتهای مطلب «مسیریابی در Angular - قسمت پنجم - تعریف Child Routes» متد isValid را تعریف کردیم. این متد مسیر یک tab را دریافت کرده و اگر اعتبارسنجی آن مشکلی نداشت، مقدار true را بر می‌گرداند. از این متد جهت نمایش آیکن خطای اعتبارسنجی برگه‌ها استفاده خواهیم کرد.
        <div class="wizard">
            <a [routerLink]="['info']" routerLinkActive="active">
                Basic Information
                <span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('info')}"></span>
            </a>
            <a [routerLink]="['tags']" routerLinkActive="active">
                Search Tags
                <span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('tags')}"></span>
            </a>
        </div>
در اینجا دو span را تعریف کرده‌ایم که با کمک دایرکتیو ngClass سبب نمایش آیکن exclamation-sign در صورت وجود یک خطای اعتبارسنجی می‌شوند. به عبارتی اگر برگه‌ای معتبر نباشد، سبب درج کلاس آن در span جاری می‌شود:



رخ‌دادهای مسیریابی

هر زمانیکه کاربری مسیرهای مختلف برنامه را پیمایش می‌کند، مسیریاب تعدادی رخ‌داد را نیز تولید خواهد کرد. از این رخ‌دادها جهت تحت نظر قرار دادن، عیب‌یابی و یا اجرای منطقی می‌توان استفاده کرد. این رخ‌دادها شامل موارد ذیل هستند:
- NavigationStart، با آغاز پیمایش یک مسیر رخ می‌دهد.
- RoutesRecognized، با تشخیص و تطابق یک مسیر، با یکی از المان‌های تعریف شده‌ی در تنظیمات مسیریابی رخ می‌دهد.
- NavigationEnd، با پایان پیمایش یک مسیر رخ می‌دهد.
- NavigationCancel، در صورت لغو پیمایش یک مسیریابی توسط محافظ‌های مسیرها و یا هدایت به یک جهت دیگر رخ می‌دهد.
- NavigationError، با شکست پیمایش یک مسیر رخ می‌دهد.

این رخ‌دادها با فعالسازی تنظیم enableTracing تنظیمات مسیریابی به true فعال می‌شوند. برای این منظور فایل src\app\app-routing.module.ts را گشوده و به نحو ذیل تغییر دهید:
@NgModule({
  imports: [RouterModule.forRoot(routes/*, { useHash: true }*/, { enableTracing: true })],
پس از این تغییر، اگر به developer tools مرورگر دقت کنید، یک چنین خروجی را می‌توان مشاهده کرد:


در اینجا ترتیب اجرای رخ‌دادهای متفاوت پیمایش مسیر نمایش لیست محصولات را مشاهده می‌کنید.
- Router به هر مسیر، یک id خود افزایش یابنده را به صورت خودکار نسبت می‌دهد. برای نمونه، این مسیر خاص، id:2 را یافته‌است. از این id می‌توان برای دسترسی به مجموعه‌ای از رخ‌دادها استفاده کرد.
- در این خروجی، url همان آدرس اصلی مسیر است و urlAfterRedirects به معنای مسیری است که پس از تنظیم redirect در تنظیمات مسیریابی (در صورت وجود) حاصل شده‌است.
- یکی از روش‌هایی که برای دیباگ مسیریابی‌ها می‌توان استفاده کرد، همین فعالسازی enableTracing است.


کار با رخ‌دادهای مسیریابی با کدنویسی

به رخ‌دادهایی که در کنسول developer tools مرورگر مشاهده کردید، با کدنویسی نیز می‌توان دسترسی یافت. برای مثال می‌توان یک تصویر چرخنده یا لطفا منتظر بمانید را در آغاز پیمایش یک مسیریابی نمایش داد و سپس در پایان پیمایش این مسیریابی، آن‌را مخفی کرد. این events نیز از نوع Observable بوده و برای کار با آن‌ها باید مشترکشان شد:
this.router.events.subscribe((routerEvent: Event) => {
    if (routerEvent instanceof NavigationStart) {
      //...
    }
});
شیء router به همراه خاصیت events است که با گوش فرادادن به رخ‌دادهای صادر شده‌ی توسط آن می‌توان دریافت چه نوع رخ‌دادی هم اکنون صادر شده‌است.

در مثال جاری این سری، در «مسیریابی در Angular - قسمت چهارم - پیش واکشی اطلاعات»، سبب شدیم تا کل اطلاعات مورد نیاز یک مسیر، پیش از نمایش آن از سرور دریافت شوند تا به این صورت ابتدا یک قاب خالی نمایش داده نشده و پس از مدتی تکمیل شود. هرچند تجربه‌ی کاربری این روش بهتر از روش قبلی است، اما هنوز هم کاربر تاخیری را در ابتدا حس خواهد کرد (به اندازه‌ی زمان delay تنظیم شده)، بدون اینکه راهنمایی به او ارائه شود. در این حالت بهتر است در ابتدای کار، یک تصویر چرخنده نمایش داده شود تا کاربر متوجه شود، نیاز است اندکی منتظر بماند.
در اینجا می‌خواهیم این تصویر چرخنده برای تمام مسیرهای برنامه فعال شود. به همین جهت گوش فرادادن به رخ‌دادها را در نقطه‌ی آغازین برنامه و یا همان src\app\app.component.ts انجام می‌دهیم:
import { Router, Event, NavigationStart, NavigationEnd, NavigationError, NavigationCancel } from '@angular/router';

import { AuthService } from './user/auth.service';
import { Component } from '@angular/core';


@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  pageTitle: string = 'Routing Lab';

  loading: boolean = true;

  constructor(private authService: AuthService,
    private router: Router) {
    router.events.subscribe((routerEvent: Event) => {
      this.checkRouterEvent(routerEvent);
    });
  }

  checkRouterEvent(routerEvent: Event): void {
    if (routerEvent instanceof NavigationStart) {
      this.loading = true;
    }

    if (routerEvent instanceof NavigationEnd ||
      routerEvent instanceof NavigationCancel ||
      routerEvent instanceof NavigationError) {
      this.loading = false;
    }
  }

  logOut(): void {
    this.authService.logout();
    this.router.navigateByUrl('/welcome');
  }
}
کدهای کامل AppComponent را جهت گوش فرادادن به رخ‌دادهای شروع و یا خاتمه/لغو/شکست پیمایش یک مسیریابی، در اینجا مشاهده می‌کنید.
- ابتدا وابستگی‌های لازم آن import شده‌اند.
- سپس می‌خواهیم خاصیت عمومی loading را در شروع به پیمایش یک مسیر، به true تنظیم کنیم و اگر این پیمایش به هر نحوی خاتمه یافت، آن‌را false خواهیم کرد.

اکنون برای استفاده‌ی از این خاصیت عمومی و نمایش تصویر چرخنده، نیاز است قالب src\app\app.component.html را ویرایش کنیم:
<span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="loading"></span>
با افزودن span فوق به ابتدای فایل app.component.html به تغییرات خاصیت loading واکنش نشان خواهیم داد. کلاس‌های CSS ایی را که در اینجا اضافه شده‌اند، به فایل src\styles.css اضافه می‌کنیم:
/* Spinner */
.spinner {
  font-size:300%;
  position:absolute;
  top: 50%;
  left: 50%;
  z-index:10
}

.glyphicon-spin {
    -webkit-animation: spin 1000ms infinite linear;
    animation: spin 1000ms infinite linear;
}
@-webkit-keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    100% {
        -webkit-transform: rotate(359deg);
        transform: rotate(359deg);
    }
}
@keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    100% {
        -webkit-transform: rotate(359deg);
        transform: rotate(359deg);
    }
}


اکنون مسیرهایی که دارای route resolver هستند (مانند نمایش جزئیات/ویرایش یک محصول)، به همراه یک spinner نمایش داده خواهند شد و سایر مسیرها ابتدا نمایش داده خواهند شد و سپس اطلاعات آن‌ها از سرور دریافت می‌شود (مانند مسیر نمایش لیست محصولات که دارای route resolver نیست).
البته می‌توان این true/false کردن loading را به ابتدا و انتهای کار یک Observable، مانند حالت نمایش لیست محصولات نیز منتقل کرد. اما در این حالت باید span مرتبط را نیز به قالب همان کامپوننت انتقال داد و دیگر سراسری نخواهد بود.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-06.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.