نظرات مطالب
بررسی تفصیلی رابطه Many-to-Many در EF Code first
پشتیبانی نمی‌شود. این جدول واسط در رابطه many-to-many به صورت داخلی توسط EF مدیریت می‌شود و از دسترس برنامه نویس خارج است.
البته یک راه حل برای آن در اینجا مطرح شده. رابطه دیگر many-to-many نیست. دو رابطه one-to-many تشکیل شده به جدول واسط.
مطالب
چگونگی استفاده از افزونه 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);
                
            });
   
در نهایت تمامی نکات گفته شده را به صورت یک نمونه کد آماده کردم:
   

مطالب
Blazor 5x - قسمت 33 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 3- بهبود تجربه‌ی کاربری عدم دسترسی‌ها
در قسمت قبل، دسترسی به قسمت‌هایی از برنامه‌ی کلاینت را توسط ویژگی Authorize و همچنین نقش‌های مشخصی، محدود کردیم. در این مطلب می‌خواهیم اگر کاربری هنوز وارد سیستم نشده‌است و قصد مشاهده‌ی صفحات محافظت شده را دارد، به صورت خودکار به صفحه‌ی لاگین هدایت شود و یا اگر کاربری که وارد سیستم شده‌است اما نقش مناسبی را جهت دسترسی به یک صفحه ندارد، بجای هدایت به صفحه‌ی لاگین، پیام مناسبی را دریافت کند.


هدایت سراسری و خودکار کاربران اعتبارسنجی نشده به صفحه‌ی لاگین

در برنامه‌ی این سری، اگر کاربری که به سیستم وارد نشده‌است، بر روی دکمه‌ی Book یک اتاق کلیک کند، فقط پیام «Not Authorized» را مشاهده خواهد کرد که تجربه‌ی کاربری مطلوبی به‌شمار نمی‌رود. بهتر است در یک چنین حالتی، کاربر را به صورت خودکار به صفحه‌ی لاگین هدایت کرد و پس از لاگین موفق، مجددا او را به همین آدرس درخواستی پیش از نمایش صفحه‌ی لاگین، هدایت کرد. برای مدیریت این مساله کامپوننت جدید RedirectToLogin را طراحی می‌کنیم که جایگزین پیام «Not Authorized» در کامپوننت ریشه‌ای BlazorWasm.Client\App.razor خواهد شد. بنابراین ابتدا فایل جدید BlazorWasm.Client\Pages\Authentication\RedirectToLogin.razor را ایجاد می‌کنیم. چون این کامپوننت بدون مسیریابی خواهد بود و قرار است مستقیما داخل کامپوننت دیگری درج شود، نیاز است فضای نام آن‌را نیز به فایل BlazorWasm.Client\_Imports.razor اضافه کرد:
@using BlazorWasm.Client.Pages.Authentication
پس از آن، محتوای این کامپوننت را به صورت زیر تکمیل می‌کنیم:
@using System.Security.Claims

@inject NavigationManager NavigationManager

if(AuthState is not null)
{
    <div class="alert alert-danger">
        <p>You [@AuthState.User.Identity.Name] do not have access to the requested page</p>
        <div>
            Your roles:
            <ul>
            @foreach (var claim in AuthState.User.Claims.Where(c => c.Type == ClaimTypes.Role))
            {
                <li>@claim.Value</li>
            }
            </ul>
        </div>
    </div>
}

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

    AuthenticationState AuthState;

    protected override async Task OnInitializedAsync()
    {
        AuthState = await AuthenticationState;
        if (!IsAuthenticated(AuthState))
        {
            var returnUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
            if (string.IsNullOrEmpty(returnUrl))
            {
                NavigationManager.NavigateTo("login");
            }
            else
            {
                NavigationManager.NavigateTo($"login?returnUrl={Uri.EscapeDataString(returnUrl)}");
            }
        }
    }

    private bool IsAuthenticated(AuthenticationState authState) =>
            authState?.User?.Identity is not null && authState.User.Identity.IsAuthenticated;
}
توضیحات:
در اینجا روش کار کردن با AuthenticationState را از طریق کدنویسی ملاحظه می‌کنید. در زمان بارگذاری اولیه‌ی این کامپوننت، بررسی می‌شود که آیا کاربر جاری، به سیستم وارد شده‌است یا خیر؟ اگر خیر، او را به سمت صفحه‌ی لاگین هدایت می‌کنیم. اما اگر کاربر پیشتر به سیستم وارد شده باشد، متن شما دسترسی ندارید، به همراه لیست نقش‌های او در صفحه ظاهر می‌شوند که برای دیباگ برنامه مفید است و دیگر به سمت صفحه‌ی لاگین هدایت نمی‌شود.

در ادامه برای استفاده از این کامپوننت، به کامپوننت ریشه‌ای BlazorWasm.Client\App.razor مراجعه کرده و قسمت NotAuthorized آن‌را به صورت زیر، با معرفی کامپوننت RedirectToLogin، جایگزین می‌کنیم:

<NotAuthorized>
    <RedirectToLogin></RedirectToLogin>
</NotAuthorized>
چون این کامپوننت اکنون در بالاترین سطح سلسله مراتب کامپوننت‌های تعریف شده قرار دارد، به صورت سراسری به تمام صفحات و کامپوننت‌های برنامه اعمال می‌شود.


چگونه دسترسی نقش ثابت Admin را به تمام صفحات محافظت شده برقرار کنیم؟

اگر خاطرتان باشد در زمان ثبت کاربر ادمین Identity، تنها نقشی را که برای او ثبت کردیم، Admin بود که در تصویر فوق هم مشخص است؛ اما ویژگی Authorize استفاده شده جهت محافظت از کامپوننت (attribute [Authorize(Roles = ConstantRoles.Customer)]@)، تنها نیاز به نقش Customer را دارد. به همین جهت است که کاربر وارد شده‌ی به سیستم، هرچند از دیدگاه ما ادمین است، اما به این صفحه دسترسی ندارد. بنابراین اکنون این سؤال مطرح است که چگونه می‌توان به صورت خودکار دسترسی نقش Admin را به تمام صفحات محافظت شده‌ی با نقش‌های مختلف، برقرار کرد؟
برای رفع این مشکل همانطور که پیشتر نیز ذکر شد، نیاز است تمام نقش‌های مدنظر را با یک کاما از هم جدا کرد و به خاصیت Roles ویژگی Authorize انتساب داد؛ و یا می‌توان این عملیات را به صورت زیر نیز خلاصه کرد:
using System;
using BlazorServer.Common;
using Microsoft.AspNetCore.Authorization;

namespace BlazorWasm.Client.Utils
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
    public class RolesAttribute : AuthorizeAttribute
    {
        public RolesAttribute(params string[] roles)
        {
            Roles = $"{ConstantRoles.Admin},{string.Join(",", roles)}";
        }
    }
}
در این حالت، AuthorizeAttribute سفارشی تهیه شده، همواره به همراه نقش ثابت ConstantRoles.Admin هم هست و همچنین دیگر نیازی نیست کار جمع زدن قسمت‌های مختلف را با کاما انجام داد؛ چون string.Join نوشته شده همین‌کار را انجام می‌دهد.
پس از این تعریف می‌توان در کامپوننت‌ها، ویژگی Authorize نقش دار را با ویژگی جدید Roles، جایگزین کرد که همواره دسترسی کاربر Admin را نیز برقرار می‌کند:
@attribute [Roles(ConstantRoles.Customer, ConstantRoles.Employee)]


مدیریت سراسری خطاهای حاصل از درخواست‌های HttpClient

تا اینجا نتایج حاصل از شکست اعتبارسنجی سمت کلاینت را به صورت سراسری مدیریت کردیم. اما برنامه‌های سمت کلاینت، به کمک HttpClient خود نیز می‌توانند درخواست‌هایی را به سمت سرور ارسال کرده و در پاسخ، برای مثال not authorized و یا forbidden را دریافت کنند و یا حتی internal server error ای را در صورت بروز استثنایی در سمت سرور.
فرض کنید Web API Endpoint جدید زیر را تعریف کرده‌ایم که نقش ادیتور را می‌پذیرد. این نقش، جزو نقش‌های تعریف شده‌ی در برنامه و سیستم Identity ما نیست. بنابراین هر درخواستی که به سمت آن ارسال شود، برگشت خواهد خورد و پردازش نمی‌شود:
namespace BlazorWasm.WebApi.Controllers
{
    [Route("api/[controller]")]
    [Authorize(Roles = "Editor")]
    public class MyProtectedEditorsApiController : Controller
    {
        [HttpGet]
        public IActionResult Get()
        {
            return Ok(new ProtectedEditorsApiDTO
            {
                Id = 1,
                Title = "Hello from My Protected Editors Controller!",
                Username = this.User.Identity.Name
            });
        }
    }
}
برای مدیریت سراسری یک چنین خطای سمت سروری در یک برنامه‌ی Blazor WASM می‌توان یک Http Interceptor نوشت:
namespace BlazorWasm.Client.Services
{
    public class ClientHttpInterceptorService : DelegatingHandler
    {
        private readonly NavigationManager _navigationManager;
        private readonly ILocalStorageService _localStorage;
        private readonly IJSRuntime _jsRuntime;

        public ClientHttpInterceptorService(
                NavigationManager navigationManager,
                ILocalStorageService localStorage,
                IJSRuntime JsRuntime)
        {
            _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
            _localStorage = localStorage ?? throw new ArgumentNullException(nameof(localStorage));
            _jsRuntime = JsRuntime ?? throw new ArgumentNullException(nameof(JsRuntime));
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            // How to add a JWT to all of the requests
            var token = await _localStorage.GetItemAsync<string>(ConstantKeys.LocalToken);
            if (token is not null)
            {
                request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
            }

            var response = await base.SendAsync(request, cancellationToken);

            if (!response.IsSuccessStatusCode)
            {
                await _jsRuntime.ToastrError($"Failed to call `{request.RequestUri}`. StatusCode: {response.StatusCode}.");

                switch (response.StatusCode)
                {
                    case HttpStatusCode.NotFound:
                        _navigationManager.NavigateTo("/404");
                        break;
                    case HttpStatusCode.Forbidden: // 403
                    case HttpStatusCode.Unauthorized: // 401
                        _navigationManager.NavigateTo("/unauthorized");
                        break;
                    default:
                        _navigationManager.NavigateTo("/500");
                        break;
                }
            }

            return response;
        }
    }
}
توضیحات:
با ارث‌بری از کلاس پایه‌ی DelegatingHandler می‌توان متد SendAsync تمام درخواست‌های ارسالی توسط برنامه را بازنویسی کرد و تحت نظر قرار داد. برای مثال در اینجا، پیش از فراخوانی await base.SendAsync کلاس پایه (یا همان درخواست اصلی که در قسمتی از برنامه صادر شده‌است)، یک توکن را به هدرهای درخواست، اضافه کرده‌ایم و یا پس از این فراخوانی (که معادل فراخوانی اصل کد در حال اجرای برنامه است)، با بررسی StatusCode بازگشتی از سمت سرور، کاربر را به یکی از صفحات یافت نشد، خطایی رخ داده‌است و یا دسترسی ندارید، هدایت کرده‌ایم. برای نمونه کامپوننت Unauthorized.razor را با محتوای زیر تعریف کرده‌ایم:
@page "/unauthorized"

<div class="alert alert-danger mt-3">
    <p>You don't have access to the requested resource.</p>
</div>
که سبب می‌شود زمانیکه StatusCode مساوی 401 و یا 403 را از سمت سرور دریافت کردیم، خطای فوق را به صورت خودکار به کاربر نمایش دهیم.

پس از تدارک این Interceptor سراسری، نوبت به معرفی آن به برنامه‌است که ... در ابتدا نیاز به نصب بسته‌ی نیوگت زیر را دارد:
dotnet add package Microsoft.Extensions.Http
این بسته‌ی نیوگت، امکان دسترسی به متدهای الحاقی AddHttpClient و سپس AddHttpMessageHandler را میسر می‌کند که توسط متد AddHttpMessageHandler است که می‌توان Interceptor سراسری را به سیستم معرفی کرد. بنابراین تعاریف قبلی و پیش‌فرض HttpClient را حذف کرده و با AddHttpClient جایگزین می‌کنیم:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            //...

            // builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
            /*builder.Services.AddScoped(sp => new HttpClient
            {
                BaseAddress = new Uri(builder.Configuration.GetValue<string>("BaseAPIUrl"))
            });*/

            // dotnet add package Microsoft.Extensions.Http
            builder.Services.AddHttpClient(
                    name: "ServerAPI",
                    configureClient: client =>
                    {
                        client.BaseAddress = new Uri(builder.Configuration.GetValue<string>("BaseAPIUrl"));
                        client.DefaultRequestHeaders.Add("User-Agent", "BlazorWasm.Client 1.0");
                    }
                )
                .AddHttpMessageHandler<ClientHttpInterceptorService>();
            builder.Services.AddScoped<ClientHttpInterceptorService>();
            builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("ServerAPI"));

            //...
        }
    }
}
پس از این تنظیمات، در هر قسمتی از برنامه که با HttpClient تزریق شده کار می‌شود، تفاوتی نمی‌کند که چه نوع درخواستی به سمت سرور ارسال می‌شود، هر نوع درخواستی که باشد، تحت نظر قرار گرفته شده و بر اساس پاسخ دریافتی از سمت سرور، واکنش نشان داده خواهد شد. به این ترتیب دیگر نیازی نیست تا switch (response.StatusCode) را که در Interceptor تکمیل کردیم، در تمام قسمت‌های برنامه که با HttpClient کار می‌کنند، تکرار کرد. همچنین مدیریت سراسری افزودن JWT به تمام درخواست‌ها نیز به صورت خودکار انجام می‌شود.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-33.zip
اشتراک‌ها
Bootstrap 4.4.0 منتشر شد
  • New responsive containers! Over a year in the making, fluid up to a particular breakpoint, available for all responsive tiers.
  • New responsive .row-cols classes for quickly specifying the number of columns across breakpoints. This one is huge for those of you who have asked for responsive card decks. 
Bootstrap 4.4.0 منتشر شد