کتابخانه Angular Material 6x
دریافت کدهای کامل این پروژه
کدهای کامل پروژهای که نیازمندیهای فوق را پیاده سازی میکند، در اینجا میتوانید مشاهده و دریافت کنید. در این مطلب از قرار دادن مستقیم این کدها صرفنظر شده و سعی خواهد شد بجای آن، نقشهی ذهنی درک کدهای آن توضیح داده شود.
پیشنیازها
در پروژهی فوق برای شروع به کار، از اطلاعات مطرح شدهی در سلسله مطالب زیر استفاده شدهاست:
- «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity»
- «مدیریت مرکزی شماره نگارشهای بستههای NuGet در پروژههای NET Core.»
- «کاهش تعداد بار تعریف usingها در C# 10.0 و NET 6.0.»
- «روش یافتن لیست تمام کنترلرها و اکشن متدهای یک برنامهی ASP.NET Core»
نیاز به علامتگذاری صفحات امن شدهی سمت کلاینت، جهت نمایش خودکار آنها
صفحات امن سازی شدهی سمت کلاینت، با ویژگی Authorize مشخص میشوند. بنابراین قید آن الزامی است، تا صرفا جهت کاربران اعتبارسنجی شده، قابل دسترسی شوند. در اینجا میتوان یک نمونهی سفارشی سازی شدهی ویژگی Authorize را به نام ProtectedPageAttribute نیز مورد استفاده قرار داد. این ویژگی از AuthorizeAttribute ارثبری کرده و دقیقا مانند آن عمل میکند؛ اما این اضافات را نیز به همراه دارد:
- به همراه یک Policy از پیش تعیین شده به نام CustomPolicies.DynamicClientPermission است تا توسط قسمتهای بررسی سطوح دسترسی پویا و همچنین منوساز برنامه، یافت شده و مورد استفاده قرار گیرد.
- به همراه خواص اضافهتری مانند GroupName و Title نیز هست. GroupName نام سرتیتر منوی dropdown نمایش داده شدهی در منوی اصلی برنامهاست و Title همان عنوان صفحه که در این منو نمایش داده میشود. اگر صفحهی محافظت شدهای به همراه GroupName نباشد، یعنی باید به صورت یک آیتم اصلی نمایش داده شود. همچنین در اینجا یک سری Order هم درنظر گرفته شدهاند تا بتوان ترتیب نمایش صفحات را نیز به دلخواه تغییر داد.
نمونهای از استفادهی از ویژگی فوق را در مسیر src\Client\Pages\Feature1 میتوانید مشاهده کنید که خلاصهی آن به صورت زیر است:
@attribute [ProtectedPage(GroupName = "Feature 1", Title = "Page 1", GlyphIcon = "bi bi-dot", GroupOrder = 1, ItemOrder = 1)]
ویژگی ProtectedPage را معادل یک ویژگی Authorize سفارشی، به همراه چند خاصیت بیشتر، جهت منوساز پویای برنامه درنظر بگیرید.
نیاز به لیست کردن صفحات علامتگذاری شدهی با ویژگی ProtectedPage
پس از اینکه صفحات مختلف برنامه را توسط ویژگی ProtectedPage علامتگذاری کردیم، اکنون نوبت به لیست کردن پویای آنها است. اینکار توسط سرویس ProtectedPagesProvider صورت میگیرد. این سرویس با استفاده از Reflection، ابتدا تمام IComponentها یا همان کامپوننتهای تعریف شدهی در برنامه را از اسمبلی جاری استخراج میکند. بنابراین اگر نیاز دارید که این جستجو در چندین اسمبلی صورت گیرد، فقط کافی است ابتدای این کدها را تغییر دهید. پس از یافت شدن IComponent ها، فقط آنهایی که دارای RouteAttribute هستند، پردازش میشوند؛ یعنی کامپوننتهایی که به همراه مسیریابی هستند. پس از آن بررسی میشود که آیا این کامپوننت دارای ProtectedPageAttribute هست یا خیر؟ اگر بله، این کامپوننت در لیست نهایی درج خواهد شد.
نیاز به یک منوساز پویا جهت نمایش خودکار صفحات امن سازی شدهی با ویژگی ProtectedPage
اکنون که لیست صفحات امن سازی شدهی توسط ویژگی ProtectedPage را در اختیار داریم، میتوانیم آنها را توسط کامپوننت سفارشی NavBarDynamicMenus به صورت خودکار نمایش دهیم. این کامپوننت لیست صفحات را توسط کامپوننت NavBarDropdownMenu نمایش میدهد.
تهیهی جداول و سرویسهای ثبت دسترسیهای پویای سمت کلاینت
جداول و فیلدهای مورد استفادهی در این پروژه را در تصویر فوق ملاحظه میکنید که در پوشهی src\Server\Entities نیز قابل دسترسی هستند. در این برنامه نیاز به ذخیره سازی اطلاعات نقشهای کاربران مانند نقش Admin، ذخیره سازی سطوح دسترسی پویای سمت کلاینت و همچنین سمت سرور است. بنابراین بجای اینکه به ازای هر کدام، یک جدول جداگانه را تعریف کنیم، میتوان از همان طراحی ASP.NET Core Identity مایکروسافت با استفاده از جدول UserClaimها ایده گرفت. یعنی هر کدام از این موارد، یک Claim خواهند شد:
در اینجا نقشها با Claim استانداردی به نام http://schemas.microsoft.com/ws/2008/06/identity/claims/role که توسط خود مایکروسافت نامگذاری شده و سیستمهای اعتبارسنجی آن بر همین اساس کار میکنند، قابل مشاهدهاست. همچنین دو Claim سفارشی دیگر ::DynamicClientPermission:: برای ذخیره سازی اطلاعات صفحات محافظت شدهی سمت کلاینت و ::DynamicServerPermission:: جهت ذخیره سازی اطلاعات اکشن متدهای محافظت شدهی سمت سرور نیز تعریف شدهاند. رابطهای این اطلاعات با جدول کاربران، many-to-many است.
به این ترتیب است که مشخص میشود کدام کاربر، به چه claimهایی دسترسی دارد.
برای کار با این جداول، سه سرویس UsersService، UserClaimsService و UserTokensService پیش بینی شدهاند. UserTokens اطلاعات توکنهای صادر شدهی توسط برنامه را ذخیره میکند و توسط آن میتوان logout سمت سرور را پیاده سازی کرد؛ از این جهت که JWTها متکی به خود هستند و تا زمانیکه منقضی نشوند، در سمت سرور پردازش خواهند شد، نیاز است بتوان به نحوی اگر کاربری غیرفعال شد، از آن ثانیه به بعد، توکنهای او در سمت سرور پردازش نشوند که به این نکات در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» پیشتر پرداخته شدهاست.
اطلاعات این سرویسها توسط اکشن متدهای UsersAccountManagerController، در اختیار برنامهی کلاینت قرار میگیرند.
نیاز به قسمت مدیریتی ثبت دسترسیهای پویای سمت کلاینت و سرور
قبل از اینکه بتوان قسمتهای مختلف کامپوننت NavBarDynamicMenus را توضیح داد، نیاز است ابتدا یک قسمت مدیریتی را جهت استفادهی از لیست ProtectedPageها نیز تهیه کرد:
در این برنامه، کامپوننت src\Client\Pages\Identity\UsersManager.razor کار لیست کردن کاربران، که اطلاعات آنرا از کنترلر UsersAccountManagerController دریافت میکند، انجام میدهد. در مقابل نام هر کاربر، دو دکمهی ثبت اطلاعات پویای دسترسیهای سمت کلاینت و سمت سرور وجود دارد. سمت کلاینت آن توسط کامپوننت UserClientSidePermissions.razor مدیریت میشود و سمت سرور آن توسط UserServerSidePermissions.razor.
کامپوننت UserClientSidePermissions.razor، همان لیست صفحات محافظت شدهی توسط ویژگی ProtectedPage را به صورت گروه بندی شده و به همراه یک سری chekmark، ارائه میدهد. اگر در اینجا صفحهای انتخاب شد، اطلاعات آن به سمت سرور ارسال میشود تا توسط Claim ای به نام ::DynamicClientPermission:: به کاربر انتخابی انتساب داده شود.
شبیه به همین عملکرد در مورد دسترسی سمت سرور نیز برقرار است. UserServerSidePermissions.razor، لیست اکشن متدهای محافظت شده را از کنترلر DynamicPermissionsManagerController دریافت کرده و نمایش میدهد. این اطلاعات توسط سرویس ApiActionsDiscoveryService جمع آوری میشود. همچنین این اکشن متدهای ویژه نیز باید با ویژگی Authorize(Policy = CustomPolicies.DynamicServerPermission) مزین شده باشند که نمونه مثال آنها را در مسیر src\Server\Controllers\Tests میتوانید مشاهده کنید. اگر در سمت کلاینت و قسمت مدیریتی آن، اکشن متدی جهت کاربر خاصی انتخاب شد، اطلاعات آن ذیل Claimای به نام ::DynamicServerPermission:: به کاربر انتخابی انتساب داده میشود.
بازگشت اطلاعات پویای دسترسیهای سمت کلاینت از API
تا اینجا کامپوننتهای امن سازی شدهی سمت کلاینت و اکشن متدهای امن سازی شدهی سمت سرور را توسط صفحات مدیریتی برنامه، به کاربران مدنظر خود انتساب دادیم و توسط سرویسهای سمت سرور، اطلاعات آنها را در بانک اطلاعاتی ذخیره کردیم. اکنون نوبت به استفادهی از claims تعریف شده و مرتبط با هر کاربر است. پس از یک لاگین موفقیت آمیز توسط UsersAccountManagerController، سه توکن به سمت کاربر ارسال میشوند:
- توکن دسترسی: اطلاعات اعتبارسنجی کاربر به همراه نام و نقشهای او در این توکن وجود دارند.
- توکن به روز رسانی: هدف از آن، دریافت یک توکن دسترسی جدید، بدون نیاز به لاگین مجدد است. به این ترتیب کاربر مدام نیاز به لاگین مجدد نخواهد داشت و تا زمانیکه refresh token منقضی نشدهاست، برنامه میتواند از آن جهت دریافت یک access token جدید استفاده کند.
- توکن سطوح دسترسی پویای سمت کلاینت: در اینجا لیست ::DynamicClientPermission::ها به صورت یک توکن مجزا به سمت کاربر ارسال میشود. این اطلاعات به توکن دسترسی اضافه نشدهاند تا بیجهت حجم آن اضافه نشود؛ از این جهت که نیازی نیست تا به ازای هر درخواست HTTP به سمت سرور، این لیست حجیم claims پویای سمت کلاینت نیز به سمت سرور ارسال شود. چون سمت سرور از claims دیگری به نام ::DynamicServerPermission:: استفاده میکند.
اگر دقت کنید، هم refresh-token و هم DynamicPermissions هر دو به صورت JWT ارسال شدهاند. میشد هر دو را به صورت plain و ساده نیز ارسال کرد. اما مزیت refresh token ارسال شدهی به صورت JWT، انجام اعتبارسنجی خودکار سمت سرور اطلاعات آن است که دستکاری سمت کلاینت آنرا مشکل میکند.
این سه توکن توسط سرویس BearerTokensStore، در برنامهی سمت کلاینت ذخیره و بازیابی میشوند. توکن دسترسی یا همان access token، توسط ClientHttpInterceptorService به صورت خودکار به تمام درخواستهای ارسالی توسط برنامه الصاق خواهد شد.
مدیریت خودکار اجرای Refresh Token در برنامههای Blazor WASM
دریافت refresh token از سمت سرور تنها قسمتی از مدیریت دریافت مجدد یک access token معتبر است. قسمت مهم آن شامل دو مرحلهی زیر است:
الف) اگر خطاهای سمت سرور 401 و یا 403 رخ دادند، ممکن است نیاز به refresh token باشد؛ چون احتمالا یا کاربر جاری به این منبع دسترسی ندارد و یا access token دریافتی که طول عمر آن کمتر از refresh token است، منقضی شده و دیگر قابل استفاده نیست.
ب) پیش از منقضی شدن access token، بهتر است با استفاده از refresh token، یک access token جدید را دریافت کرد تا حالت الف رخ ندهد.
- برای مدیریت حالت الف، یک Policy ویژهی Polly طراحی شدهاست که آنرا در کلاس ClientRefreshTokenRetryPolicy مشاهده میکنید. در این Policy ویژه، هرگاه خطاهای 401 و یا 403 رخ دهند، با استفاده از سرویس جدید IClientRefreshTokenService، کار به روز رسانی توکن انجام خواهد شد. این Policy در کلاس program برنامه ثبت شدهاست. مزیت کار با Policyهای Polly، عدم نیاز به try/catch نوشتنهای تکراری، در هر جائیکه از سرویسهای HttpClient استفاده میشود، میباشد.
- برای مدیریت حالت ب، حتما نیاز به یک تایمر سمت کلاینت است که چند ثانیه پیش از منقضی شدن access token دریافتی پس از لاگین، کار دریافت access token جدیدی را به کمک refresh token موجود، انجام دهد. پیاده سازی این تایمر را در کلاس ClientRefreshTokenTimer مشاهده میکنید که محل فراخوانی و راه اندازی آن یا پس از لاگین موفق در سمت کلاینت و یا با ریفرش صفحه (فشرده شدن دکمهی F5) و در کلاس آغازین ClientAuthenticationStateProvider میباشد.
نیاز به پیاده سازی Security Trimming سمت کلاینت
از داخل DynamicPermissions دریافتی پس از لاگین، لیست claimهای دسترسی پویای سمت کلاینت کاربر لاگین شده استخراج میشود. بنابراین مرحلهی بعد، استخراج، پردازش و اعمال این سطوح دسترسی پویای دریافت شدهی از سرور است.
سرویس BearerTokensStore، کار ذخیره سازی توکنهای دریافتی پس از لاگین را انجام میدهد و سپس با استفاده از سرویس DynamicClientPermissionsProvider، توکن سوم دریافت شده که مرتبط با لیست claims دسترسی کاربر جاری است را پردازش کرده و تبدیل به یک لیست قابل استفاده میکنیم تا توسط آن بتوان زمانیکه قرار است آیتمهای منوها را به صورت پویا نمایش داد، مشخص کنیم که کاربر، به کدامیک دسترسی دارد و به کدامیک خیر. عدم نمایش قسمتی از صفحه که کاربر به آن دسترسی ندارد را security trimming گویند. برای نمونه کامپوننت ویژهی SecurityTrim.razor، با استفاده از نقشها و claims یک کاربر، میتواند تعیین کند که آیا قسمت محصور شدهی صفحه توسط آن قابل نمایش به کاربر است یا خیر. این کامپوننت از متدهای کمکی AuthenticationStateExtensions که کار با user claims دریافتی از طریق JWTها را ساده میکنند، استفاده میکند. یک نمونه از کاربرد کامپوننت SecurityTrim را در فایل src\Client\Shared\MainLayout.razor میتوانید مشاهده کنید که توسط آن لینک Users Manager، فقط به کاربران دارای نقش Admin نمایش داده میشود.
نحوهی مدیریت security trimming منوی پویای برنامه، اندکی متفاوت است. DynamicClientPermissionsProvider لیست claims متعلق به کاربر را بازگشت میدهد. این لیست پس از لاگین موفقیت آمیز دریافت شدهاست. سپس لیست کلی صفحاتی را که در ابتدای برنامه استخراج کردیم، در طی حلقهای از سرویس ClientSecurityTrimmingService عبور میدهیم. یعنی مسیر صفحه و همچنین دسترسیهای پویای کاربر، مشخص هستند. در این بین هر مسیری که در لیست claims پویای کاربر نبود، در لیست آیتمهای منوی پویای برنامه، نمایش داده نمیشود.
نیاز به قطع دسترسی به مسیرهایی در سمت کلاینت که کاربر به صورت پویا به آنها دسترسی ندارد
با استفاده از ClientSecurityTrimmingService، در حلقهای که آیتمهای منوی سایت را نمایش میدهد، موارد غیرمرتبط با کاربر جاری را حذف کردیم و نمایش ندادیم. اما این حذف، به این معنا نیست که اگر این آدرسها را به صورت مستقیم در مرورگر وارد کند، به آنها دسترسی نخواهد داشت. برای رفع این مشکل، نیاز به پیاده سازی یک سیاست دسترسی پویای سمت کلاینت است. روش ثبت این سیاست را در کلاس DynamicClientPermissionsPolicyExtensions مشاهده میکنید. کلید آن همان CustomPolicies.DynamicClientPermission که در حین تعریف ProtectedPageAttribute به عنوان مقدار Policy پیشفرض مقدار دهی شد. یعنی هرگاه ویژگی ProtectedPage به صفحهای اعمال شد، از این سیاست دسترسی استفاده میکند که پردازشگر آن DynamicClientPermissionsAuthorizationHandler است. این هندلر نیز از ClientSecurityTrimmingService استفاده میکند. در هندلر context.User جاری مشخص است. این کاربر را به متد تعیین دسترسی مسیر جاری به سرویس ClientSecurityTrimming ارسال میکنیم تا مشخص شود که آیا به مسیر درخواستی دسترسی دارد یا خیر؟
نیاز به قطع دسترسی به منابعی در سمت سرور که کاربر به صورت پویا به آنها دسترسی ندارد
شبیه به ClientSecurityTrimmingService سمت کلاینت را در سمت سرور نیز داریم؛ به نام ServerSecurityTrimmingService که کار آن، پردازش claimهایی از نوع ::DynamicServerPermission:: است که در صفحهی مدیریتی مرتبطی در سمت کلاینت، به هر کاربر قابل انتساب است. هندلر سیاست دسترسی پویایی که از آن استفاده میکند نیز DynamicServerPermissionsAuthorizationHandler میباشد. این سیاست دسترسی پویا با کلید CustomPolicies.DynamicServerPermission در کلاس ConfigureServicesExtensions تعریف شدهاست. به همین جهت هر اکشن متدی که Policy آن با این کلید مقدار دهی شده باشد، از هندلر پویای فوق جهت تعیین دسترسی پویا عبور خواهد کرد. منطق پیاده سازی شدهی در اینجا، بسیار شبیه به مطلب «سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاستهای دسترسی پویا» است؛ اما بدون استفادهی از ASP.NET Core Identity.
روش اجرای برنامه
چون این برنامه از نوع Blazor WASM هاست شدهاست، نیاز است تا برنامهی Server آنرا در ابتدا اجرا کنید. با اجرای آن، بانک اطلاعاتی SQLite برنامه به صورت خودکار توسط EF-Core ساخته شده و مقدار دهی اولیه میشود. لیست کاربران پیشفرض آنرا در اینجا میتوانید مشاهده کنید. ابتدا با کاربر ادمین وارد شده و سطوح دسترسی سایر کاربران را تغییر دهید. سپس بجای آنها وارد سیستم شده و تغییرات منوها و سطوح دسترسی پویا را بررسی کنید.
مدلهای سمت سرور برنامه
در این مطلب قصد داریم لیست گروهها را به همراه محصولات مرتبط با آنها، توسط دو 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; } }
منبع داده JSON سمت سرور
پس از مشخص شدن مدلهای برنامه، اکنون توسط دو اکشن متد، لیست گروهها و همچنین لیست محصولات یک گروه خاص را با فرمت JSON بازگشت میدهیم:
namespace AngularTemplateDrivenFormsLab.Controllers { [Route("api/[controller]")] public class ProductController : Controller { [HttpGet("[action]")] public async Task<IActionResult> GetCategories() { await Task.Delay(500); return Json(CategoriesDataSource.Items); } [HttpGet("[action]/{categoryId:int}")] public async Task<IActionResult> GetProducts(int categoryId) { await Task.Delay(500); var products = CategoriesDataSource.Items .Where(category => category.CategoryId == categoryId) .SelectMany(category => category.Products) .ToList(); return Json(products); } } }
- در اینجا از یک Delay نیز استفاده شدهاست تا بتوان آیکنهای چرخندهی Loading سمت کاربر را در حین کار با عملیاتی زمانبر، بهتر مشاهده کرد.
کدهای سمت کاربر برنامه
کدهای سمت کاربر این مثال در ادامهی همان مطلب «فرمهای مبتنی بر قالبها در Angular - قسمت پنجم - ارسال اطلاعات به سرور» هستند که بر روی آن این دستورات فراخوانی شدهاست:
>ng g m Product -m app.module --routing
>ng g c product/product-group
>ng g cl product/product >ng g cl product/Category >ng g cl product/product-group-form
export class ProductGroupForm { constructor( public categoryId?: number, public productId?: number ) { } } export class Product { constructor( public productId: number, public productName: string ) { } } export class Category { constructor( public categoryId: number, public categoryName: string ) { } }
سپس سرویسی را جهت دریافت اطلاعات دراپ داونها از سرور تهیه کردهایم:
>ng g s product/product-items -m product.module
import { Injectable } from "@angular/core"; import { Http, Response, Headers, RequestOptions } from "@angular/http"; import { Observable } from "rxjs/Observable"; import "rxjs/add/operator/do"; import "rxjs/add/operator/catch"; import "rxjs/add/observable/throw"; import "rxjs/add/operator/map"; import "rxjs/add/observable/of"; import { Category } from "./category"; import { Product } from "./product"; @Injectable() export class ProductItemsService { private baseUrl = "api/product"; constructor(private http: Http) { } private handleError(error: Response): Observable<any> { console.error("observable error: ", error); return Observable.throw(error.statusText); } getCategories(): Observable<Category[]> { return this.http .get(`${this.baseUrl}/GetCategories`) .map(response => response.json() || {}) .catch(this.handleError); } getProducts(categoryId: number): Observable<Product[]> { return this.http .get(`${this.baseUrl}/GetProducts/${categoryId}`) .map(response => response.json() || {}) .catch(this.handleError); } }
پس از این مقدمات اکنون میتوان کدهای ProductGroupComponent را تکمیل کرد.
ابتدا در متد ngOnInit آن کار دریافت لیست آغازین گروههای محصولات را انجام میدهیم:
export class ProductGroupComponent implements OnInit { categories: Category[] = []; model = new ProductGroupForm(); constructor(private productItemsService: ProductItemsService) { } ngOnInit() { this.productItemsService.getCategories().subscribe( data => { this.categories = data; }, err => console.log("get error: ", err) ); }
اکنون چون این خاصیت در دسترس است، میتوان به قالب این کامپوننت مراجعه کرده و قسمت ابتدایی فرم را تکمیل کرد:
<div class="container"> <h3>Cascading Drop-down Lists</h3> <form #form="ngForm" (submit)="submitForm(form)" novalidate> <div class="form-group"> <label class="control-label">Category</label> <span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="categories.length == 0"></span> <select class="form-control" name="categoryCtrl" #categoryCtrl (change)="fetchProducts(categoryCtrl.value)" [(ngModel)]="model.categoryId"> <option value="undefined">Select a Category...</option> <option *ngFor="let category of categories" value="{{category.categoryId}}"> {{ category.categoryName }} </option> </select> </div>
- سپس ngModel به خاصیت categoryId وهلهای از کلاس ProductGroupForm که مدل معادل فرم است، متصل شدهاست.
- همچنین با اتصال به رخداد change، مقدار Id عضو انتخابی به متد fetchProducts ارسال میشود. دسترسی به این Id از طریق یک template reference variable به نام categoryCtrl# انجام شدهاست.
- در آخر، ngFor تعریف شده به ازای هر عضو آرایهی categories، یکبار تگ option را تکرار میکند و در هربار تکرار، مقدار ویژگی value را به categoryId تنظیم میکند و برچسب نمایشی آنرا از categoryName دریافت خواهد کرد.
بنابراین مرحلهی بعدی تکمیل این drop down آبشاری، واکنش نشان دادن به رخداد change و تکمیل متد fetchProducts است:
products: Product[] = []; isLoadingProducts = false; fetchProducts(categoryId?: number) { console.log(categoryId); this.products = []; if (categoryId === undefined || categoryId.toString() === "undefined") { return; } this.isLoadingProducts = true; this.productItemsService.getProducts(categoryId).subscribe( data => { this.products = data; this.isLoadingProducts = false; }, err => { console.log("get error: ", err); this.isLoadingProducts = false; } ); }
- سپس بررسی میکنیم که آیا categoryId دریافتی undefined است یا خیر؟ این مساله دو علت دارد:
الف) اولین عضو drop down انتخاب محصولات را با مقدار undefined مشخص کردهایم:
<option value="undefined">Select a Category...</option>
public categoryId?: number
model = new ProductGroupForm();
- پس از آن همانند قسمت قبل، این categoryId را به سرور ارسال کرده و سپس اطلاعات متناظری را دریافت و به خاصیت عمومی products نسبت دادهایم. همچنین از یک خاصیت عمومی دیگر به نام isLoadingProducts نیز استفاده شدهاست تا مشخص شود چه زمانی کار دریافت اطلاعات از سرور خاتمه پیدا میکند. از آن برای نمایش یک آیکن چرخندهی دیگر استفاده میکنیم:
<div class="form-group"> <label class="control-label">Product</label> <span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="isLoadingProducts"></span> <select class="form-control" name="productCtrl" [(ngModel)]="model.productId"> <option value="undefined">Select a Product...</option> <option *ngFor="let product of products" value="{{product.productId}}"> {{ product.productName }} </option> </select> </div>
/* Spinner */ .spinner { font-size:15px; 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); } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-06.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
>npm install >ng build --watch
>dotnet restore >dotnet watch run
سازماندهی برنامههای Angular توسط ماژولها
ویژگی جدیدی به Angular 6 به نام tree-shakable providers اضافه شدهاست که نمونهای از کاربرد آن به صورت پیشفرض در حین «ایجاد پروژهی «کتابخانه» توسط Angular CLI 6.0» ارائه میشود. به این معنا که با استفادهی از آن دیگر نیازی نیست تا سرویسهای سراسری برنامه را در قسمت providers مربوط به CoreModule ثبت کرد. همینقدر که یک سرویس سراسری را به صورت ذیل تعریف کنید:
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class MyCoreService { }
در این حالت تزریق وابستگیهایی که در مطلب «تزریق وابستگیها فراتر از کلاسها در برنامههای Angular» نیز بحث شدند، پشتیبانی میشود:
@Injectable({ providedIn: 'root', useClass: LazyFlightCancellingService, deps: [NgModuleFactoryLoader, Injector] }) export class FlightCancellingService { }
به عنوان مثال کدهای مخزن کد «اعتبارسنجی کاربران در برنامههای Angular» جهت استفادهی از این قابلیت به روز شد.
اس کیوال سرور
توسعه وب
دات نت فریم ورک
شیرپوینت
متفرقه
محیطهای مجتمع توسعه
مسایل انسانی، اجتماعی و مدیریتی برنامه نویسی
ویندوز
حالت forms authentication در ASP.Net ، امکان تعریف کوکیهایی ماندگار را نیز جهت ورود خودکار کاربران در دفعات بعدی بازدید آنها فراهم میکند. اما زمان منقضی شدن این کوکیهای ماندگار در ASP.Net 1.1 و ASP.Net 2.0 به بعد کاملا با هم متفاوت بوده و اگر برنامه نویس از این تغییر حاصل شده مطلع نباشد ممکن است بارها و بارها برنامه را آزمایش کند اما نتیجهای نگیرد.
مدت زمان منقضی شدن کوکیهای ماندگار forms authentication در ASP.Net 1.1 به صورت زیر است (بود):
زمان منقضی شدن کوکی سشن = زمان جاری + زمان منقضی شدن سشن کاربر
زمان منقضی شدن کوکی ماندگار (persistent) = زمان جاری + 50 سال
این عدد 50 سال بسیار غیرمنطقی بوده و در ASP.Net 2.0 به بعد به صورت زیر اصلاح شده است:
زمان منقضی شدن کوکی سشن = زمان جاری + زمان منقضی شدن سشن کاربر
زمان منقضی شدن کوکی ماندگار (persistent) = زمان جاری + زمان منقضی شدن سشن کاربر (پیش فرض آن 30 دقیقه است)
و جهت بیشتر کردن طول عمر این ماندگاری باید زمان مورد نظر را در وب کانفیگ به صورت زیر اعمال کرد:
<authentication mode="Forms">
<forms
…
name="MyCookieName"
slidingExpiration="true"
timeout="43200"
/>
</authentication>
همچنین نکتهی مهم دیگری را که باید رعایت کرد، name ایی است که در این فایل config عنوان میکنید (در قسمت تنظیمات form authentication). اگر بر روی یک وب سرور، چندین برنامه وب ASP.Net را در حال اجرا دارید، باید برای هر کدام از اینها نامی جداگانه و منحصربفرد انتخاب کنید، در غیراینصورت تداخل رخ داده و باز هم گزینه مرا به خاطر بسپار شما کار نخواهد کرد.
کار slidingExpiration که در اینجا تنظیم شده است نیز به صورت زیر میباشد:
اگر لاگین موفقیت آمیزی ساعت 5 عصر صورت گیرد و timeout شما به عدد 10 تنظیم شده باشد، این لاگین به صورت خودکار در 5:10 منقضی خواهد شد. اما اگر در این حین در ساعت 5:05 ، کاربر یکی از صفحات سایت شما را مرور کند، زمان منقضی شدن کوکی ذکر شده به 5:15 تنظیم خواهد شد. (مفهوم تنظیم slidingExpiration)
لازم به ذکر است که اگر کاربر پیش از نصف زمان منقضی شدن کوکی (مثلا در 5:04)، یکی از صفحات را مرور کند، تغییری در این زمان نهایی منقضی شدن رخ نخواهد داد.
روش تعیین نوع useState Hook
برای این منظور در ابتدا فایل جدید src\components\Input.tsx را ایجاد کرده و به صورت زیر تکمیل میکنیم:
import React, { useState } from "react"; export const Input = () => { const [name, setName] = useState(""); return <input value={name} onChange={(e) => setName(e.target.value)} />; };
پس از این تعاریف ... برنامه بدون مشکل کار میکند و کامپایل میشود. اکنون سؤال اینجا است که آیا تایپاسکریپت در ایجا اصلا کاری را هم انجام میدهد؟ برای درک این موضوع، سطر useState را به صورت زیر تغییر میدهیم:
const [name, setName] = useState(0);
عنوان میکند که مقدار رشتهای e.target.value، به مقدار عددی name قابل انتساب نیست. به عبارتی TypeScript از قابلیت Type Inference خود در اینجا استفاده میکند. درست است که به ظاهر نوعی را برای useState و خروجی منتسب به آن تعیین نکردهایم، اما بر اساس نوع مقدار پیشفرض آن، نوع name و setName به صورت خودکار مشخص میشوند و نیازی به ذکر صریح این نوع، نیست. برای مثال در حالت اول چون مقدار پیشفرض useState را یک رشتهی خالی معرفی کرده بودیم، نوع name نیز string درنظر گرفته شده بود. در حالت دوم بر اساس مقدار پیشفرض عددی useState، اینبار نوع name نیز یک number خواهد بود و دیگر نمیتوان یک مقدار رشتهای را مانند e.target.value به آن انتساب داد. مزیت کار کردن با TypeScript در اینجا، مشاهدهی آنی خطای یک چنین استفادهها و انتسابهای نادرستی است.
مفهوم Type Inference را در تصاویر زیر بهتر میتوان مشاهده کرد. اشارهگر ماوس را به تعریف useState نزدیک کنید. در توضیحاتی که ظاهر میشود، بر اساس نوع مقدار پیشفرض آن، نوع آرگومان جنریک متد useState نیز به صورت خودکار تغییر میکند:
و نکتهی مهم اینجا است که نیازی به ذکر صریح این نوع جنریک، مانند مثال زیر نیست:
const [name, setName] = useState<string>("");
const [name, setName] = useState("");
سؤال: اگر مقدار اولیهی useState را null تعیین کردیم و یا اصلا تعریف نکردیم و undefined بود، چطور؟
در یک چنین حالتی که نوع دقیق state، از طریق مقدار اولیهی آن قابل استنتاج نیست، نیاز هست همانند تصاویر فوق، تعریف جنریک useState را به نحو صریحی ذکر کرده و آنرا با union types تکمیل کنیم:
const [name, setName] = useState<string | null>(null);
روش تعیین نوع useRef Hook
در ادامه میخواهیم نحوهی تعیین نوع DOM Elements را در React بررسی کنیم. با استفاده از useRef میتوان به ارجاعی از یک DOM Element دسترسی یافت.
import React, { useRef, useState } from "react"; export const Input = () => { const [name, setName] = useState(""); const inputRef = useRef(null); if (inputRef && inputRef.current) { console.log("ref", inputRef.current.value); } return ( <input ref={inputRef} value={name} onChange={(e) => setName(e.target.value)} /> ); };
عنوان میکند نوعی که inputRef.current دارد، نال است و نال به همراه خاصیت value نیست. برای اینکه نوع inputRef را بهتر بتوانیم بررسی کنیم، دقیقا بر روی آن کلیک راست کرده و گزینهی Go to Type Definition را انتخاب کنید. بلافاصله به تعریف زیر هدایت خواهیم شد:
interface MutableRefObject<T> { current: T; }
برای رفع این مشکل فقط کافی است نوع المان مدنظر را صریحا به عنوان آرگومان جنریک useRef معرفی کنیم:
const inputRef = useRef<HTMLInputElement>(null);
فعال بودن Strict Null Checking در پروژههای TypeScript ای React
نکات مطلب «نوعهای نال نپذیر در TypeScript» به صورت پیشفرض در پروژههای تایپ اسکریپتی React هم فعال هستند؛ چون پرچم strict واقع در فایل tsconfig.json پروژه، به صورت پیشفرض به true تنظیم شدهاست و این پرچم، Strict Null Checking را نیز شامل میشود. برای آزمایش فعال بودن آن، کدهای فوق را به صورت زیر تغییر دهید تا صرفا if آن حذف شود:
//if (inputRef && inputRef.current) { console.log("ref", inputRef.current.value); //}
که برای رفع آن، همانند قبل باید ذکر if بررسی نال بودن inputRef و خاصیت current آنرا اضافه کرد؛ تا دیگر در زمان اجرای برنامه، با شانس نال بودن یکی از ایندو مواجه نشویم و به کیفیت بالاتری در برنامهی خود برسیم.
روش بررسی if (inputRef && inputRef.current) معادل سادهتری را نیز در TypeScript 3.7 به بعد دارد که به Optional Chaining معروف است؛ به صورت زیر:
console.log("ref", inputRef?.current?.value);
8 ویژگی جذاب Angular
I've been doing some work the last couple of weeks with Angular2. I really like it. Not just because it uses typescript, but also because it feels really natural and straightforward while working with it. No more string based dependency injection, or strange digest cycle stuff, it just seems to work. This last week I've migrated our beta-13 Angular app to the latest rc-1, and used that to keep track of the fun and easy stuff Angular 2 provides. Note though, that the application we're developing is really that complex, so I can only assume we'll run into more complex Angular2 features in the near future. For now, though, let me share some general tips and tricks we've encountered thus far (in no particular order). Oh, all examples are in typescript, since after using that, I really don't want to go back to plain old javascript (POJS?).
try { // some code to check ... } catch (InvalidOperationException ex) { // do your handling for invalid operation ... } catch (IOException ex) { // do your handling for IO error ... }
catch (SomeException ex) when (someConditionIsMet) { // Your handler logic }
علاوه بر این در اینجا میتوان چندین بدنهی catch مجزا را به ازای یک نوع استثنای مشخص به همراه whenهای متفاوتی نیز تعریف کرد و از این لحاظ محدودیتی وجود ندارد. فقط در این حالت باید به تقدم و تاخرها دقت داشت. برای نمونه در مثال ذیل، ترکیب چندین شرط متفاوت را بر اساس یک نوع مشخص استثناء، مشاهده میکنید. در اینجا اگر برای نمونه شرط ذکر شدهی در قسمت when مربوط به catch اولی صادق باشد، همینجا کار خاتمه مییابد و سایر catchها بررسی نمیشوند:
catch (SomeDependencyException ex) when (condition1 && condition2) { } catch (SomeDependencyException ex) when (condition1) { } catch (SomeDependencyException ex) { }
catch (SomeDependencyException ex) { } catch (SomeDependencyException ex) when (condition1 && condition2) { } catch (SomeDependencyException ex) when (condition1) { }
لاگ کردن استثناءها در C# 6 بدون مدیریت آنها
به مثال ذیل دقت کنید:
try { DoSomethingThatMightFail(s); } catch (Exception ex) when (Log(ex, "An error occurred")) { // this catch block will never be reached } ... static bool Log(Exception ex, string message, params object[] args) { Debug.Print(message, args); return false; }
تفاوت C# 6 - Exception Filtering با if/else نوشتن در بدنهی catch چیست؟
تا اینجا به این نتیجه رسیدیم که کدهای if/else دار داخل بدنهی catch کدهای قدیمی را مانند کد ذیل:
try { var request = WebRequest.Create("http://www.google.coom/"); var response = request.GetResponse(); } catch (WebException we) { if (we.Status == WebExceptionStatus.NameResolutionFailure) { //handle DNS error return; } if (we.Status == WebExceptionStatus.ConnectFailure) { //handle connection error return; } throw; }
try { var request = WebRequest.Create("http://www.google.coom/"); var response = request.GetResponse(); } catch (WebException we) when (we.Status == WebExceptionStatus.NameResolutionFailure) { //Handle NameResolutionFailure Separately } catch (WebException we) when (we.Status == WebExceptionStatus.ConnectFailure) { //Handle ConnectFailure Separately }
به این معنا که exception filters سبب Stack unwinding نمیشوند. با هربار ورود به بدنهی catch، اصطلاحا عملیات Stack unwinding صورت میگیرد. یعنی اطلاعات stack مربوط به متدهای پیش از فراخوانی متدی که سبب بروز استثناء شدهاست، از بین میروند. به این ترتیب تشخیص مقادیر متغیرهایی که سبب بروز این استثناء شدهاند نیز میسر نخواهد بود و دیگر نمیتوان با قطعیت عنوان کرد که چه مقادیری و چه اطلاعاتی سبب بروز این مشکل شدهاند. اما در حالت exception filters در قسمت when آن هنوز وارد بدنهی catch نشدهایم. در اینجا دسترسی کاملی به اطلاعات stack جاری و مقادیر متغیرهای محلی که سبب بروز این استثناء شدهاند وجود دارد.
تفاوت stack با stack trace چیست؟ stack قطعهای از حافظهاست که اطلاعاتی در مورد نحوهی فراخوانی متدها، آدرس بازگشتی آنها، آرگومان و همچنین متغیرهای محلی آنها را دارا است. اما stack trace تنها یک رشتهاست و بیانگر نام متدهایی است که هم اکنون بر روی stack قرار دارند. احتمالا پیشتر خوانده بودید که فراخوانی throw داخل بدنهی catch سبب حفظ stack trace میشود و اگر throw ex صورت گیرد، این اطلاعات از دست میروند و بازنویسی میشوند. اما در C# 6 امکان حفظ کل اطلاعات stack به همراه exception filtering میسر شدهاست.