«حالت» یا state، شیءای است، حاوی اطلاعاتی که برنامه با آن سر و کار دارد. بنابراین مدیریت حالت، روشی است برای ردیابی و مدیریت دادههای مورد استفادهی در برنامه و تقریبا تمام برنامهها، به نحوی به آن نیاز دارند. هر کامپوننت در Blazor، دارای state خاص خودش است و این state از سایر کامپوننتها کاملا مستقل و ایزولهاست. این مورد با بزرگتر شدن برنامه و برقراری ارتباط بین کامپوننتها، مشکل ایجاد میکند. برای مثال اگر قرار است در منوی بالای سایت، تعداد محصولات موجود در سبد خرید یک شخص را نمایش دهیم، این تعداد، حاصل تعامل او با چندین کامپوننت مجزا خواهد بود که اینها الزاما در یک سلسه مراتب هم قرار نمیگیرند و به سادگی نمیتوان اطلاعات را به صورت آبشاری در بین آنها به اشتراک گذاشت. به همین جهت نیاز به روشی برای مدیریت حالت و به اشتراک گذاری آن در بین کامپوننتهای مختلف برنامه وجود دارد و خوشبختانه چون Blazor به همراه یک سیستم تزریق وابستگیهای توکار است، پیاده سازی یک چنین مدیریت کنندهای، سادهاست.
استفاده از الگوی Observer جهت مدیریت حالت برنامههای Blazor
زمانیکه همانند تصویر فوق با یک کامپوننت کار میکنیم، کاربر همواره کارش از تعامل با یک View آغاز میشود. این تعامل سبب صدور رخدادهایی میشود که این رخدادها، حالت و state کامپوننت را تغییر میدهند. تغییر حالت کامپوننت نیز بلافاصله سبب بهروز رسانی View میشود. در این مثال، حالت کامپوننت، داخل همان کامپوننت نگهداری میشود؛ مانند فیلدهایی که در قسمت code@ یک کامپوننت Blazor تعریف میکنیم و محدود به همان کامپوننت هستند.
با بزرگتر شدن برنامه، زمانی خواهد رسید که نیاز است حالت یک کامپوننت را با کامپوننتهای دیگر به اشتراک گذاشت. در این حالت باید این state را از داخل کامپوننت مدنظر استخراج کرد و در جائی دیگر قرار داد که عموما به آن state store گفته میشود:
در تصویر فوق، در بالای آن یک state store را داریم که محل نگهداری و ذخیره سازی حالت اشتراکی بین کامپوننتها است. سپس برای نمونه دو کامپوننت دیگر را داریم که رابطهی بین آنها، همان رابطهی مثلثی است که در تصویر اول این مطلب مشاهده کردیم. برای مثال در اثر تعامل کاربری با View کامپوننت 1، رخدادی صادر خواهد شد. مدیریت این رخداد، سبب تغییر state خواهد شد، اما اینبار این state دیگر داخل کامپوننت 1 قرار ندارد؛ بلکه داخل state store است و این store پس از آگاه شدن از تغییر وضعیت خود، دو کامپوننتی را که از آن تغدیه میکنند، جهت به روز رسانی Viewهایشان، مطلع میکند. همین چرخه در مورد کامپوننت 2 نیز برقرار است. اگر تعاملی با آن صورت گیرد، در نهایت اثر آن به هر دو کامپوننت متصل به state store اشتراکی، اطلاع رسانی میشود تا Viewهای هر دوی آنها به روز رسانی شوند. الگویی را که در اینجا مشاهده میکنید، در اصل یک الگوی Observer است:
در الگوی مشاهدهگر، یک Subject را داریم که تعداد زیادی Observer، مشترک آن هستند. در این مثال ما، Subject، همان State Store است و Observerها دقیقا همان کامپوننتهای مشترک به آن. Observerها به تغییرات Subject گوش فرا داده و بلافاصله بر اساس آن واکنش مناسبی را نشان میدهند.
پیاده سازی الگوی Observer جهت مدیریت حالت برنامههای Blazor
زمانیکه یک برنامهی متداول Blazor را توسط قالب پیشفرض آن ایجاد میکنیم، به همراه یک کامپوننت Counter است:
@page "/counter" <h1>Counter</h1> <p>Current count: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } }
بنابراین در قدم اول نیاز به یک State Store اشتراکی را داریم که بتوانیم توسط آن، مقدار جاری currentCount را ذخیره کرده و سپس تغییرات آنرا جهت به روز رسانی دو View (در کامپوننتهای Counter و NavMenu)، به مشترکین آن اطلاع رسانی کنیم. به همین جهت ابتدا پوشهی جدید Stores را در ریشهی پروژهی Blazor ایجاد میکنیم. نام این پوشه، از این جهت یک اسم جمع است که یک برنامه بنابر نیاز خودش میتواند چندین State Store را داشته باشد. سپس داخل این پوشه، پوشهی دیگری را به نام CounterStore، ایجاد میکنیم.
در اینجا در ابتدا شیء حالت مدنظر را ایجاد میکنیم که برای نمونه بر اساس نیاز برنامه و این مثال، از مقدار نهایی کلیک بر روی دکمهی شمارشگر تشکیل میشود:
namespace BlazorStateManagement.Stores.CounterStore { public class CounterState { public int Count { get; set; } } }
using System; namespace BlazorStateManagement.Stores.CounterStore { public interface ICounterStore { void DecrementCount(); void IncrementCount(); CounterState GetState(); void AddStateChangeListener(Action listener); void BroadcastStateChange(); void RemoveStateChangeListener(Action listener); } }
using System; namespace BlazorStateManagement.Stores.CounterStore { public class CounterStore : ICounterStore { private readonly CounterState _state = new(); private Action _listeners; public CounterState GetState() { return _state; } public void IncrementCount() { _state.Count++; BroadcastStateChange(); } public void DecrementCount() { _state.Count--; BroadcastStateChange(); } public void AddStateChangeListener(Action listener) { _listeners += listener; } public void RemoveStateChangeListener(Action listener) { _listeners -= listener; } public void BroadcastStateChange() { _listeners.Invoke(); } } }
- مخزن حالت پیاده سازی شدهی بر اساس الگوی مشاهدهگر، نیاز دارد تا بتواند لیست مشاهدهگرها را ثبت کند. به همین جهت به همراه متدهای AddStateChangeListener جهت ثبت یک مشاهدهگر جدید و RemoveStateChangeListener، جهت حذف مشاهدهگری از لیست موجود است.
- همچنین الگوی مشاهدهگر باید بتواند تغییرات صورت گرفتهی در حالتی را که نگهداری میکند (CounterState در اینجا)، به مشترکین خود اطلاع رسانی کند. اینکار را توسط متد BroadcastStateChange انجام میدهد. هر زمانیکه این متد فراخوانی شود، Actionهایی که به صورت پارامتر به متد AddStateChangeListener ارسال شدهاند، به صورت خودکار اجرا خواهند شد. این کار سبب میشود تا بتوان منطق خاصی را مانند به روز رسانی UI، در سمت کامپوننتهای مشترک به این مخزن، پیاده سازی کرد.
- در اینجا همچنین متدهایی برای افزایش و کاهش مقدار Count را نیز به همراه اطلاع رسانی به مشترکین، مشاهده میکنید.
پس از این تعریف نیاز است سرویس Store ایجاد شده را به برنامه معرفی کرد:
namespace BlazorStateManagement.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); //... builder.Services.AddScoped<ICounterStore, CounterStore>(); //... } } }
تغییر کامپوننتهای برنامه برای استفاده از سرویس ICounterStore
پس از معرفی سرویس ICounterStore به سیستم تزریق وابستگیهای برنامه، جهت سهولت استفادهی از آن، در ابتدا فضای نام آنرا به فایل سراسری Client\_Imports.razor اضافه میکنیم:
@using BlazorStateManagement.Stores.CounterStore
@page "/counter" @implements IDisposable @inject ICounterStore CounterStore <h1>Counter</h1> <p>Current count: @CounterStore.GetState().Count</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { protected override void OnInitialized() { base.OnInitialized(); CounterStore.AddStateChangeListener(UpdateView); } private void IncrementCount() { CounterStore.IncrementCount(); } private void UpdateView() { StateHasChanged(); } public void Dispose() { CounterStore.RemoveStateChangeListener(UpdateView); } }
- در اینجا در ابتدا سرویس ICounterStore، به کامپوننت تزریق شدهاست.
- سپس در متد رویدادگران آغازین OnInitialized، با استفاده از متد AddStateChangeListener، مشترک سرویس مخزن حالت شمارشگر شدهایم.
- همواره جهت پاکسازی کد و عدم اشتراک بیش از اندازهی به یک مخزن حالت، نیاز است در پایان کار یک کامپوننت، با پیاده سازی implements IDisposable@، کار حذف اشتراک را انجام دهیم. در غیراینصورت هربار که کامپوننت بارگذاری میشود، یک اشتراک جدید از این کامپوننت، به مخزن حالتی که طول عمر Singleton دارد، اضافه خواهد شد که نشانی از نشتی حافظهاست.
- دو قسمت دیگر را هم تغییر دادهایم. اینبار با استفاده از متد ()GetState، این Count اشتراکی را نمایش میدهیم و همچنین عمل به روز رسانی State را هم توسط متد IncrementCount انجام دادهایم.
در ادامه کامپوننت Client\Shared\NavMenu.razor را نیز جهت نمایش مقدار جاری Count، به صورت زیر به روز رسانی میکنیم:
@inject ICounterStore CounterStore <li class="nav-item px-3"> <NavLink class="nav-link" href="counter"> <span class="oi oi-plus" aria-hidden="true"></span> Counter: @CounterStore.GetState().Count </NavLink> </li> @code { protected override void OnInitialized() { base.OnInitialized(); CounterStore.AddStateChangeListener(() => StateHasChanged()); } // ... }
- در اینجا نیز در ابتدا سرویس ICounterStore، به کامپوننت تزریق شدهاست.
- سپس در متد رویدادگران آغازین OnInitialized، با استفاده از متد AddStateChangeListener، مشترک سرویس مخزن حالت شمارشگر شدهایم و هربار که متد BroadcastStateChange ای توسط یکی از کامپوننتهای متصل به مخزن حالت فراخوانی میشود (برای مثال در انتهای متد IncrementCount خود سرویس)، سبب اجرای Action آن که در اینجا StateHasChanged است، خواهد شد. فراخوانی StateHasChanged، کار اطلاع رسانی به UI، جهت رندر مجدد را انجام میدهد. به این ترتیب مقدار جدید Count توسط CounterStore.GetState().Count@ در منو نیز ظاهر خواهد شد:
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorStateManagement.zip
ثبت پیاده سازیهای متعدد یک اینترفیس در سیستم تزریق وابستگیهای NET Core.
داشتن چندین پیاده سازی از یک اینترفیس، شاید یکی از اهداف اصلی وجود آنها باشد. برای نمونه قرار داد ارسال پیامی را در برنامه، مانند IMessageService به صورت زیر طراحی و سپس بر اساس آن، دو پیاده سازی ارسال پیامها را از طریق ایمیل و SMS، اضافه میکنیم:
namespace CoreIocServices { public interface IMessageService { void Send(string message); } public class EmailService : IMessageService { public void Send(string message) { // ... } } public class SmsService : IMessageService { public void Send(string message) { //todo: ... } } }
namespace CoreIocSample02 { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddTransient<IMessageService, EmailService>(); services.AddTransient<IMessageService, SmsService>(); }
using CoreIocServices; using Microsoft.AspNetCore.Mvc; namespace CoreIocSample02.Controllers { public class MessagesController : Controller { private readonly IMessageService _emailService; private readonly IMessageService _smsService; public MessagesController(IMessageService emailService, IMessageService smsService) { _emailService = emailService; _smsService = smsService; } } }
همانطور که مشاهده میکنید، هر دو پارامتر، با وهلهای از آخرین سرویس معرفی شدهی از نوع IMessageService مقدار دهی شدهاند؛ یعنی SmsService در اینجا. در ادامه روشهای مختلفی را برای رفع این مشکل بررسی میکنیم.
روش اول: سیستم تزریق وابستگیهای NET Core. از سازندههای IEnumerable پشتیبانی میکند
برای مدیریت دریافت سرویسهایی که از یک اینترفیس مشتق شدهاند، خود NET Core. بدون نیاز به تنظیمات اضافهتری، روش تزریق IEnumerableها را در سازندههای کلاسها پشتیبانی میکند:
using System.Collections.Generic; using CoreIocServices; using Microsoft.AspNetCore.Mvc; namespace CoreIocSample02.Controllers { public class MessagesController : Controller { private readonly IEnumerable<IMessageService> _messageServices; public MessagesController(IEnumerable<IMessageService> messageServices) { _messageServices = messageServices; }
به این ترتیب لیست وهلههای تمام سرویسهایی از نوع IMessageService در اختیار ما قرار گرفتهاست و برای اهدافی مانند پیاده سازی الگوهایی در ردهی chain of responsibility و یا الگوی استراتژی، مفید است. در این حالت اگر نیاز به سرویس از نوع خاصی وجود داشت، میتوان از روش زیر استفاده کرد:
var emailService = _messageServices.OfType<EmailService>().First();
var messageServices = serviceProvider.GetServices<IMessageService>();
روش دوم: دریافت شرطی وهلههای مورد نیاز با «دخالت در مراحل وهله سازی اشیاء توسط IoC Container»
روش «دخالت در مراحل وهله سازی اشیاء توسط IoC Container» را در قسمت قبل بررسی کردیم. یکی دیگر از مثالهایی را که در این مورد میتوان ارائه داد، شرطی کردن بازگشت وهلهها است که برای سناریوی مطلب جاری بسیار مفید است:
الف) برای شرطی کردن بازگشت وهلهها، بهتر است این شرط را بجای رشتهها و یا اعدادی خاص، بر اساس یک enum مشخص انجام دهیم:
namespace CoreIocServices { public enum MessageServiceType { EmailService, SmsService }
ب) سپس هر دو سرویس مشتق شدهی از یک اینترفیس را به صورت معمولی ثبت میکنیم:
namespace CoreIocSample02 { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddTransient<EmailService>(); services.AddTransient<SmsService>();
ج) اکنون میتوان مرحلهی شرطی کردن بازگشت این وهلهها را به صورت زیر، با دخالت در مراحل وهله سازی اشیاء، پیاده سازی کرد:
namespace CoreIocSample02 { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddTransient<EmailService>(); services.AddTransient<SmsService>(); services.AddTransient<Func<MessageServiceType, IMessageService>>(serviceProvider => key => { switch (key) { case MessageServiceType.EmailService: return serviceProvider.GetRequiredService<EmailService>(); case MessageServiceType.SmsService: return serviceProvider.GetRequiredService<SmsService>(); default: throw new NotImplementedException($"Service of type {key} is not implemented."); } });
د) مرحلهی آخر کار، روش استفادهی از این امضایء ویژهی Lazy load شدهاست:
namespace CoreIocSample02.Controllers { public class MessagesController : Controller { private readonly Func<MessageServiceType, IMessageService> _messageServiceResolver; public MessagesController(Func<MessageServiceType, IMessageService> messageServiceResolver) { _messageServiceResolver = messageServiceResolver; }
public IActionResult Index() { var emailService = _messageServiceResolver(MessageServiceType.EmailService); //use emailService ... return View(); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: CoreDependencyInjectionSamples-07.zip
افزودن strong typing به کامپوننت نمایش لیست محصولات
یکی از مزایای کار با TypeScript امکان انتساب نوعهای مشخص یا سفارشی، به متغیرها و اشیاء تعریف شدهاست. برای مثال تاکنون هر خاصیت تعریف شدهای دارای نوع است. اما هنوز نوعی را برای آرایهی محصولات تعریف نکردهایم و نوع آن، نوع پیش فرض any است که برخلاف رویهی متداول کار با TypeScript است.
برای تعریف نوعهای سفارشی میتوان از اینترفیسهای TypeScript استفاده کرد. یک اینترفیس، قراردادی است که نحوهی تعریف تعدادی خاصیت و متد به هم مرتبط را مشخص میکند. سپس کلاسهای مختلف میتوانند با پیاده سازی این اینترفیس، قرارداد تعریف شدهی در آن را عملی کنند. همچنین از اینترفیسها میتوان به عنوان یک data type جدید نیز استفاده کرد. البته ES 5 و ES 6 از اینترفیسها پشتیبانی نمیکنند و تعریف آنها در TypeScript صرفا جهت کمک به کامپایلر، برای یافتن خطاها، پیش از اجرای برنامه است و به کدهای جاوا اسکریپتی معادلی ترجمه نمیشوند.
در ادامه برای تکمیل مثال این سری، فایل جدید App\products\product.ts را به پروژه اضافه کنید؛ با این محتوا:
export interface IProduct { productId: number; productName: string; productCode: string; releaseDate: string; price: number; description: string; starRating: number; imageUrl: string; }
همچنین از آنجائیکه این اینترفیس را در یک فایل ts مجزا قرار دادهایم، برای اینکه بتوان از آن در سایر قسمتهای برنامه استفاده کرد، نیاز است در ابتدای آن، واژهی کلیدی export را نیز ذکر کرد.
پس از تعریف این اینترفیس، برای استفاده از آن به عنوان یک data type جدید، ابتدا ماژول آن import خواهد شد و سپس از نام آن به عنوان نوع دادهی جدیدی، استفاده میشود. برای این منظور فایل product-list.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
import { Component } from 'angular2/core'; import { IProduct } from './product'; @Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html' }) export class ProductListComponent { // as before ... products: IProduct[] = [ // as before ... ]; // as before ... }
مزیت اینکار این است که برای مثال در اینجا اگر در لیست اعضای آرایهی products، نام خاصیتی اشتباه تایپ شده باشد یا حتی بجای عدد، از رشته استفاده شده باشد، بلافاصله در ادیتور مورد استفاده، خطای مرتبط گوشزد شده و همچنین این فایل دیگر کامپایل نخواهد شد. به علاوه اینبار برای تعریف خواص اعضای آرایهی products، ادیتور مورد استفاده، intellisense را نیز دراختیار ما قرار میدهد و کاملا مشخص است که چه اعضایی مدنظر هستند و نوع آنها چیست.
مدیریت cssهای هر کامپوننت به صورت مجزا
هنگام ساخت یک قالب یا template، در بسیاری از اوقات نیاز است css مرتبط با آن نیز، منحصر به همان قالب بوده و نشتی نداشته باشد. برای مثال زمانیکه یک کامپوننت را درون کامپوننتی دیگر قرار میدهیم، باید css آن نیز در دسترس قرار بگیرد و css فعلی کامپوننت دربرگیرنده را بازنویسی نکند. روشهای مختلفی برای مدیریت این مساله وجود دارند:
الف) تعریف شیوه نامهها به صورت inline داخل خود قالبها. این حالت، مشکلات نگهداری و استفادهی مجدد را دارد.
ب) تعریف شیوه نامهها در یک فایل خارجی css و سپس لینک کردن آن به صفحهای اصلی یا index.html
در این حالت به ازای هر فایل، یکبار باید این تعریف در صفحهای اصلی سایت صورت گیرد. همچنین این فایلها میتوانند مقادیر یکدیگر را بازنویسی کرده و بر روی هم تاثیر بگذارند.
ج) تعریف شیوه نامهها به همراه تعریف کامپوننت. این روشی است که توسط AngularJS توصیه شدهاست و نگهداری و مقیاس پذیری آن سادهتر است.
تزئین کنندهی Component به همراه دو خاصیت دیگر به نامهای styles و stylesUrl نیز میباشد.
در حالت استفاده از خاصیت styles، شیوهنامهی متناظر با کامپوننت، در همانجا به صورت inline تعریف میشود:
@Component({ //... styles: ['thead {color: blue;}'] })
روش بهتر، استفاده از خاصیت styleUrls است که در آن میتوان مسیر یک یا چند فایل css را مشخص کرد:
@Component({ //... styleUrls: ['app/products/product-list.component.css'] })
برای آزمایش آن فایل جدید product-list.component.css را به پوشهی products مثال این سری اضافه کنید؛ با این محتوا:
thead { color: #337AB7; }
@Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html', styleUrls: ['app/products/product-list.component.css'] }) export class ProductListComponent { //...
یک نکته
شیوه نامهای که به این صورت توسط AngularJS 2.0 اضافه میشود، با سایر شیوه نامههای موجود تداخل نخواهد کرد. علت آنرا در تصویر ذیل که با استفاده از developer tools مرورگرها قابل بررسی است، میتوان مشاهده کرد:
در اینجا AngularJS 2.0، با ایجاد ویژگیهای سفارشی خودکار (attributes) میدان دید css را کنترل میکند. به این ترتیب شیوه نامهی کامپوننت یک، که درون کامپوننت دو قرار گرفتهاست، نشتی نداشته و بر روی سایر قسمتهای صفحه تاثیری نخواهد گذاشت؛ برخلاف شیوه نامههایی که به صورت متداولی به صفحهی اصلی سایت لینک شدهاند.
بررسی چرخهی حیات کامپوننتها
هر کامپوننت دارای چرخهی حیاتی است که توسط AngularJS 2.0 مدیریت میشود و شامل مراحلی مانند ایجاد، رندر، ایجاد و رندر فرزندان آن، پردازش تغییرات آن و در نهایت تخریب آن کامپوننت میشود. برای اینکه بتوان با برنامه نویسی به این مراحل چرخهی حیات یک کامپوننت دسترسی یافت، تعدادی life cycle hook طراحی شدهاند. سه مورد از مهمترین life cycle hooks شامل موارد ذیل هستند:
الف) OnInit: از این hook برای انجام کارهای آغازین یک کامپوننت مانند دریافت اطلاعات از سرور، استفاده میشود.
ب) OnChanges: از آن جهت انجام اعمالی پس از تغییرات input properties استفاده میشود.
خواص ورودی و همچنین کار با سرور را در قسمتهای بعدی بررسی خواهیم کرد.
ج) OnDestroy: از آن جهت پاکسازی منابع اختصاص داده شده استفاده میشود.
برای استفادهی از این hookها، نیاز است اینترفیس آنها را پیاده سازی کنیم. از آنجائیکه AngularJS 2.0 نیز با TypeScript نوشته شدهاست، به همراه تعدادی اینترفیس از پیش تعریف شده میباشد. برای مثال به ازای هر life cycle hook، یک اینترفیس تعریف شده در آن وجود دارد. برای نمونه اینترفیس hook ایی به نام OnInit، دقیقا همان OnInit، نام دارد (و با I شروع نشدهاست):
export class ProductListComponent implements OnInit {
import { Component, OnInit } from 'angular2/core';
ngOnInit(): void { console.log('In OnInit'); }
به عنوان تمرین، فایل product-list.component.ts را گشوده و سه مرحلهی implements سپس import و در آخر تعریف متد ngOnInit فوق را به آن اضافه کنید.
در ادامه برنامه را اجرا کرده و به کنسول developer tools مرورگر خود جهت مشاهدهی console.log فوق مراجعه کنید:
ساخت یک Pipe سفارشی جهت فعال سازی textbox فیلتر کردن محصولات
همانطور که در قسمت قبل نیز عنوان شد، کار pipes، تغییر اطلاعات حاصل از data binding، پیش از نمایش آنها در رابط کاربری است و AngularJS 2.0 به همراه تعدادی pipe توکار است؛ مانند currency، percent و غیره. در ادامه قصد داریم یک pipe سفارشی را ایجاد کنیم تا بر روی حلقهی ngFor* نمایش لیست محصولات تاثیرگذار شود و همچنین ورودی خود را از مقدار وارد شدهی توسط کاربر دریافت کند.
برای این منظور، یک فایل جدید را به نام product-filter.pipe.ts به پوشهی products اضافه کنید. سپس کدهای آنرا به نحو ذیل تغییر دهید:
import { PipeTransform, Pipe } from 'angular2/core'; import { IProduct } from './product'; @Pipe({ name: 'productFilter' }) export class ProductFilterPipe implements PipeTransform { transform(value: IProduct[], args: string[]): IProduct[] { let filter: string = args[0] ? args[0].toLocaleLowerCase() : null; return filter ? value.filter((product: IProduct) => product.productName.toLocaleLowerCase().indexOf(filter) != -1) : value; } }
برای مثال در اینجا میخواهیم شرایط فیلتر محصولات وارد شدهی توسط کاربر را دریافت کنیم.
خروجی این متد نیز از نوع آرایهای از IProduct تعریف شدهاست؛ از این جهت که نتیجه نهایی فیلتر اطلاعات نیز آرایهای از همین نوع است. کار این pipe پیاده سازی متد contains به صورت غیرحساس به کوچکی و بزرگی حروف است.
سپس بلافاصله بالای نام این کلاس، از یک decorator جدید به نام Pipe استفاده شدهاست تا به AngularJS 2.0 اعلام شود، این کلاس، صرفا یک کلاس معمولی نیست و یک Pipe است.
در ابتدای فایل هم importهای لازم جهت تعریف اینترفیسهای مورد استفادهی در این ماژول، ذکر شدهاند.
اگر دقت کنید، الگوی ایجاد یک pipe جدید، بسیار شبیه است به الگوی ایجاد یک کامپوننت و از این لحاظ سعی شدهاست طراحی یک دستی در سراسر این فریم ورک بکار گرفته شود.
پس از تعریف این pipe سفارشی، برای استفادهی از آن در یک template، به فایل product-list.component.html مراجعه کرده و سپس ngFor* آنرا به نحو ذیل تغییر میدهیم:
<tr *ngFor='#product of products | productFilter:listFilter'>
اگر از قسمت قبل به خاطر داشته باشید، این خاصیت را توسط two-way binding به روز میکنیم (اطلاعات وارد شدهی در textbox، بلافاصله به این خاصیت منعکس میشوند و برعکس):
<input type='text' [(ngModel)]='listFilter' />
import { Component, OnInit } from 'angular2/core'; import { IProduct } from './product'; import { ProductFilterPipe } from './product-filter.pipe'; @Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html', styleUrls: ['app/products/product-list.component.css'], pipes: [ProductFilterPipe] }) export class ProductListComponent implements OnInit { //...
اکنون اگر برنامه را اجرا کنید، خروجی ذیل را مشاهده خواهید کرد:
در اینجا چون مقدار فیلتر وارد شدهی پیش فرض، cart است، فقط ردیف Garden Cart نمایش داده شدهاست. اگر این مقدار را خالی کنیم، تمام ردیفها نمایش داده میشوند و اگر برای مثال ham را جستجو کنیم، فقط ردیف Hammer نمایش داده میشود.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part5.zip
خلاصهی بحث
- اینترفیسها یکی از روشهای بهبود strong typing برنامههای AngularJS 2.0 هستند.
- جهت مدیریت بهتر شیوهنامههای هر کامپوننت بهتر است از روش styleUrls استفاده شود تا از نشتیهای تعاریف شیوهنامهها جلوگیری گردد.
- از life cycle hooks برای مدیریت رخدادهای مرتبط با طول عمر یک کامپوننت استفاده میشود؛ برای مثال دریافت اطلاعات از سرور و یا پاکسازی منابع مصرفی.
- تعریف یک pipe سفارشی با پیاده سازی اینترفیس PipeTransform انجام میشود. سپس نام این Pipe، به قالب مدنظر اضافه شده و در ادامه نیاز است کامپوننت استفاده کنندهی از این قالب را نیز از وجود این Pipe مطلع کرد.
var user = _documentSession .Include<User>(x => x.Apps[59].AddressId) .Load("Users/131-A"); var address = _documentSession.Load<Address>(user.Apps[59].AddressId)
var user = _documentSession .Include<User>(x => x.Apps.Values.Select(app => app.AddressId)) .Load("Users/131-A"); var addresses = List<Address>(); foreach(app in user.Apps) { addresses.Add(_documentSession.Load<Address>(app.AddressId)); //queryسمت کلاینت انجام اجرا میشود }
- حتما باید Id سند(ها) را داشته باشیم.
- کل سند را بازیابی میکند.
List<User> users = await _documentSession .Query<Users>() .Where(u => u.PhoneNumber.StartsWith("915")) .ToListAsync();
var users = await _documentSession.Query<AppUser>() .Where(u => u.Id.Equals("915")) .Select(u => new { u.Apps[appCode].FirstName, u.Apps [appCode].LastName, }) .ToListAsync();
from Users as user where startsWith(user.PhoneNumber, "915") select { FirstName : user.Apps ["59"].FirstName, LastName : user.Apps ["59"].LastName }
from u in _documentSession.Query<User>() where u.PhoneNumber.StartsWith("915") let app = u.Apps["59"] select new { app.FirstName, app.LastName, };
declare function output(u) { var app = u.Apps["59"]; return { FirstName : app.FirstName, LastName : app.LastName}; } from Users as user where startsWith(user.PhoneNumber, "915") select output(user)
app.FirstName, app.LastName, *key = u.ActiveInApps.Select(a => a.Key)
query = query.Search(u => u.key, "59");
public class User_MyIndex : AbstractIndexCreationTask<User> { Map = users => from u in users from app in u.Apps select new { Id = u.Id, PhoneNumber = u.PhoneNumber, UserName = app.Value.UserName, FirstName = app.Value.FirstName, LastName = app.Value.LastName, IsActive = app.Value.IsActive, key = app.Key }; }
new User_MyIndex().Execute(store);
IndexCreation.CreateIndexes(typeof(User_MyIndex).Assembly, store);
from u in _documentSession.Query<User, User_MyIndex>() ...
select new { ... key = aia.Key, Address = LoadDocument<Address>(aia.Value.AddressId), // City = LoadDocument<Address>(aia.Value.AddressId).City, };
Message = app.Messages.Select(m => LoadDocument<Message>(m).Content)
var users = _documentSession.Advanced.AsyncDocumentQuery<User, User_MyIndex>() .WhereStartsWith(nameof(AppUser.PhoneNumber), "915") .WhereEquals("key", appCode, exact: true) .SelectFields<AppUserModel>(new[] { $"Apps[{appCode}].FirstName", $"Apps[{appCode}].LastName" }) .ToListAsync();
public class Post { public int Id { get; set; } public string Content { get; set; } public string Title { get; set; } public List<string> Tags { get; set; } public string WriterName { get; set; } public string WriterId { get; set; } }
public class Post_ByContent : AbstractIndexCreationTask<Post> { public Post_ByContent() { Map = posts=> from post in posts select new { post.Content }; Analyzers.Add(p => p.Content, "StandardAnalyzer"); } }
List<Post> posts = _documentSession .Query<Post, Post_ByContent>() .MoreLikeThis(builder => builder .UsingDocument(p => p.Id == "posts/59-A") .WithOptions(new MoreLikeThisOptions { Fields = new[] { nameof(Post.Content) }, StopWordsDocumentId = "appConfig/StopWords" })) .ToList();
از ویندوز ویستا به بعد، ویندوز به صورت توکار دارای یک موتور تشخیص صدا شده است که در این مسیر قابل مشاهده میباشد:
این سرویس از طریق اسمبلی استاندارد System.Speech در دات نت فریم ورک قابل استفاده است که اکنون با برنامهی Subtitle tools یکپارچه شده است.
یکی از خصوصیات مفید این موتور تشخیص صدا، امکان دریافت فایلهای صوتی نیز میباشد. فایل صوتی دریافتی باید مطابق یکی از فرمتهای پشتیبانی شده توسط آن، تهیه شود؛ که این مورد را ذیل قسمت Supported audio formats شکل فوق میتوانید مشاهده کنید.
برای نمونه توسط برنامه AoA Audio Extractor Basic، میتوان این تبدیلات را انجام داد و یکی از تنظیمات قابل قبول توسط موتور Speech Recognition ویندوز 7 را در تصویر ذیل میتوانید مشاهده کنید: (و در غیراینصورت هیچ خروجی را نخواهید گرفت؛ خیلی مهم!)
پس از انتخاب و گشودن فایل صوتی در برنامه Subtitle tools (کلیک بر روی دکمه Open WAV در اینجا) و سپس کلیک بر روی دکمهی Recognize یا Start ، کار موتور Speech Recognition ویندوز شروع شده و برنامه هم در اینجا از فرصت استفاده کرده و دریافتی نهایی را تبدیل به رکوردهای فایل زیرنویس میکند که نمونهای از آنرا در شکل فوق میتوانید ملاحظه کنید.
نکاتی در مورد استفاده بهینه از موتور تشخیص صدای ویندوز:
الف) برای آزمایش برنامه، یک فایل voice را از اینجا دریافت کنید. این فایل voice از همان سری مترو PluralSight تهیه شده است.
ابتدا موتور تشخیص صدای انتخابی را بر روی حالت US قرار داده و تست کنید. در ادامه یکبار هم برروی حالت UK قرار دهید و کار تشخیص صدا را آغاز نمائید.
نتایج کاملا متفاوت خواهند بود و با توجه به لهجه انگلیسی گوینده، تشخیصهای حالت UK، به واقعیت نزدیکتر هستند. این مورد را در گزینهی Average confidence هم میتوانید مشاهده نمائید. مثلا در اینجا موتور تشخیص صدا در کل به 60 درصد خروجی تولیدیاش اطمینان دارد و مابقی ... آنچنان اعتباری ندارند.
مثلا متن صحیح سطر چهارم در تصویر فوق باید «when they are not in the foreground» باشد!
ب) تنظیمات Timeouts
اگر به فایل voice فوق دقت کنید، گوینده یک نفس از ابتدا تا انتها صحبت میکند. اینجا است که به کمک مقادیر Silence timeout ، میتوان تعداد رکوردها را بر اساس فواصل تنفس کوتاهتری، بیشتر کرد. مثلا با اعداد پیش فرض سیستم، با فایل صوتی فوق به 5 خروجی خواهید رسید؛ اما با توجه به تنظیماتی که در تصویر مشاهده میکنید، به 8 خروجی متعادلتر میرسیم.
مزایا:
- کار زمانبندی زیر نویس خودکار میشود.
- تا حدود 60 درصد، خروجی متنی مطمئنی را میتوان شاهد بود.
در مورد ویندوز XP :
ویندوز XP به صورت پیش فرض دارای موتور Speech Recognition نیست. دو راه برای نصب آن در این سیستم وجود دارد:
الف) استفاده از بسته نرم افزاری آفیس XP
به کنترل پنل مراجعه کرده، گزینهی Add/remove programs را انتخاب نمائید. در اینجا Microsoft Office XP را انتخاب و بر روی دکمه Change کلیک کنید. نیاز است تا یکی از ویژگیهای نصب نشده آنرا نصب کنیم. به همین جهت در صفحه ظاهر شده، Add or Remove Features را انتخاب و در ادامه در قسمت Features to install ، گزینهی Office Shared Features را انتخاب کنید. ذیل مدخل Alternative User Input، امکان انتخاب و نصب Speech مهیا است.
ب) استفاده از Microsoft Speech SDK Setup 5.1
بر روی ویندوز 7، نگارش 8 این برنامه نصب است؛ اما برای ویندوز XP تا نگارش 5.1 بیشتر ارائه نشده است. فایلهای آنرا از اینجا میتوانید دریافت کنید. نصب آن هم در اینجا توضیح داده شده.
من در کل ویندوز XP را برای اینکار توصیه نمیکنم چون هم موتور تشخیص صدای آن قدیمی است و هم حالت Asynchronous آن درست کار نمیکند. برای مثال این یک خروجی تهیه شده از همان فایل voice فوق، توسط موتور تشخیص صدای مخصوص ویندوز XP است که بیشباهت به طنز نیست!
سناریویی را در نظر بگیرید که یک برنامه وب نوشته شده، قرار است به چندین مستاجر (مشتری یا tenant) خدماتی را ارائه کند. در این حالت اطلاعات هر مشتری به صورت کاملا جدا شده از دیگر مشتریان در سیستم قرار دارد و فقط به همان قسمتها دسترسی دارد.
مثلا یک برنامه مدیریت رستوران را در نظر بگیرید که برای هر مشتری، در دامین
مخصوص به خود قرار دارد و همه آنها به یک سیستم متمرکز متصل شده و اطلاعات
خود را از آنجا دریافت میکنند.
در معماری Multi-Tenancy، چندین کاربر میتوانند از یک نمونه (Single
Instance) از اپلیکیشن نرمافزاری استفاده کنند. یعنی این نمونه روی سرور
اجرا میشود و به چندین کاربر سرویس میدهد. هر کاربر را یک Tenant
مینامیم. میتوان به Tenantها امکان تغییر و شخصیسازی بخشی از اپلیکیشن
را داد؛ مثلا امکان تغییر رنگ رابط کاربری و یا قوانین کسبوکار، اما آنها نمیتوانند
کدهای اپلیکیشن را شخصیسازی کنند.
خوشبختانه اوضاع با وجود OWIN بهتر شده و ما در این مطلب قصد استفاده از یک تولکیت را به نام SaasKit، برای پیاده سازی این معماری در ASP.NET Core داریم. هدف از این toolkit، سادهتر کردن هر چه بیشتر ساخت برنامههای SaaS (Software as a Service) هست. با استفاده از OWIN ما قادریم که بدون در نظر گرفتن فریم ورک مورد استفاده، رفتار مورد نظر خودمان را مستقیما در یک چرخه درخواست HTTP پیاده سازی کنیم و البته به لطف طراحی خاص ASP.NET Core 1.0 و استفاده از میان افزارهایی مشابه OWIN در برنامه، کار ما با SaasKit باز هم راحتتر خواهد بود.
شروع کار
یک پروژه ASP.NET Core جدید را ایجاد کنید و سپس ارجاعی را به فضای نام SaasKit.Multitenancy (موجود در Nuget) بدهید.PM> Install-Package SaasKit.Multitenancy
شناسایی مستاجر (tenant)
اولین جنبه در معماری multi-tenant، شناسایی مستاجر بر اساس اطلاعات درخواست جاری میباشد که میتواند از hostname ، کاربر جاری یا یک HTTP header باشد.ابتدا به تعریف کلاس مستاجر میپردازیم:
public class AppTenant { public string Name { get; set; } public string[] Hostnames { get; set; } }
public class AppTenantResolver : ITenantResolver<AppTenant> { IEnumerable<AppTenant> tenants = new List<AppTenant>(new[] { new AppTenant { Name = "Tenant 1", Hostnames = new[] { "localhost:6000", "localhost:6001" } }, new AppTenant { Name = "Tenant 2", Hostnames = new[] { "localhost:6002" } } }); public async Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context) { TenantContext<AppTenant> tenantContext = null; var tenant = tenants.FirstOrDefault(t => t.Hostnames.Any(h => h.Equals(context.Request.Host.Value.ToLower()))); if (tenant != null) { tenantContext = new TenantContext<AppTenant>(tenant); } return tenantContext; } }
سیم کشی کردن
بعد از پیاده سازی این اینترفیس نوبت به سیم کشیهای SaasKit میرسد. من در اینجا سعی کردم که مثل الگوی برنامههای ASP.NET Core عمل کنم. ابتدا نیاز داریم که وابستگیهای SaasKit را ثبت کنیم. فایل startups.cs را باز کنید و کدهای زیر را در متد ConfigureServices اضافه نمایید:public void ConfigureServices(IServiceCollection services) { services.AddMultitenancy<AppTenant, AppTenantResolver>(); }
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { // after .UseStaticFiles() app.UseMultitenancy<AppTenant>(); // before .UseMvc() }
دریافت مستاجر جاری
حالا هر جا که نیاز به وهلهای از شیء مستاجر جاری داشتید، میتوانید به روش زیر عمل کنید:public class HomeController : Controller { private AppTenant tenant; public HomeController(AppTenant tenant) { this.tenant = tenant; } }
@inject AppTenant Tenant;
<a asp-controller="Home" asp-action="Index">@Tenant.Name</a>
اجرای نمونه مثال
فایل project.json را باز کنید و مقدار web را به شکل زیر مقدار دهی کنید: (در اینجا برای سایت خود 3 آدرس را نگاشت کردیم)"commands": { "web": "Microsoft.AspNet.Server.Kestrel --server.urls=http://localhost:6000;http://localhost:6001;http://localhost:6002", },
dotnet run
و اگر آدرس http://localhost:6002 را وارد کنیم، مستاجر 2 را مشاهده میکنیم:
قابل پیکربندی کردن مستاجر ها
از آنجائیکه نوشتن مشخصات مستاجرها در کد زیاد جالب نیست، برای همین
تصمیم داریم که این مشخصات را با استفاده از قابلیتهای ASP.NET Core از
فایل appsettings.json دریافت کنیم. تنظیمات مستاجرها را مطابق اطلاعات زیر به این فایل اضافه کنید:
"Multitenancy": { "Tenants": [ { "Name": "Tenant 1", "Hostnames": [ "localhost:6000", "localhost:6001" ] }, { "Name": "Tenant 2", "Hostnames": [ "localhost:6002" ] } ] }
public class MultitenancyOptions { public Collection<AppTenant> Tenants { get; set; } }
services.Configure<MultitenancyOptions>(Configuration.GetSection("Multitenancy"));
public class AppTenantResolver : ITenantResolver<AppTenant> { private readonly IEnumerable<AppTenant> tenants; public AppTenantResolver(IOptions<MultitenancyOptions> options) { this.tenants = options.Value.Tenants; } public async Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context) { TenantContext<AppTenant> tenantContext = null; var tenant = tenants.FirstOrDefault(t => t.Hostnames.Any(h => h.Equals(context.Request.Host.Value.ToLower()))); if (tenant != null) { tenantContext = new TenantContext<AppTenant>(tenant); } return Task.FromResult(tenantContext); } }
در آخر
اولین قدم در پیاده سازی یک معماری multi-tenant، تصمیم گیری درباره این
موضوع است که شما چطور مستاجر خود را شناسایی کنید. به محض این شناسایی شما میتوانید عملیاتهای بعدی خود را مثل تفکیک بخشی از برنامه، فیلتر کردن دادهای، نمایش یک view خاص برای هر مستاجر و یا بازنویسی قسمتهای مختلف
برنامه بر اساس هر مستاجر، انجام دهید.
_ سورس مثال بالا در گیت هاب قابل دریافت میباشد.
_ منبع: اینجا
ASP.NET MVC #23
1- تنظیم .* در iis5 با خطای wrong extension format مواجه میشود آیا راهی برای اصلاح آن وجود دارد.
2- اگر سیستم مسریابی را پسونددار کنیم چگونه به روش مناسبی میتوانیم همه جای پروژه را کنترل کنیم که مسیریابی دچار مشکل نشود ازجمله در area
3- چگونه بفهمیم که iis یکپارچه است یا کلاسیک
4 - آیا iis7 مد یکپارچه آن در ویندوز سرور 2003 و 2008 قابل نصب است
5-آیا برای ویندوز 8 تنظیم خاصی نیاز دارد . من یک مثال ساده را اجرا کردم و برنامه را بر روی iis قرار دادم با خطای 403 forbidden مربوط به صفحه آغازین مواجه شدم
6- طبق روش گفته شده در آدرس زیر نمیتوان یک صفحه آغازین دستی درست کرد و در iis تنظیم کرد مثلا deafualt.aspx و در لود آن مستقیما ادامه کار را به داخل مسیریابی mvc هدایت کرد
http://weblog.west-wind.com/posts/2013/Aug/15/IIS-Default-Documents-vs-ASPNET-MVC-Routes
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> ... <aspNetCore processPath="bin\IISSupport\VSIISExeLauncher.exe" arguments="-argFile IISExeLauncherArgs.txt" forwardWindowsAuthToken="false" startupTimeLimit="3600" requestTimeout="23:00:00" stdoutLogEnabled="false" /> ... </system.webServer> </configuration>
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> ... <aspNetCore processPath=".\ApplicationName.exe" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" /> ... </system.webServer> </configuration>