نیاز به یک Dispatcher برای تعامل با بیش از یک مخزن حالت
در اینجا برای نمونه دو مخزن حالت تعریف شدهاند؛ اما روش تعامل با این مخازن حالت، دیگر مانند قبل نیست. برای نمونه در اثر تعامل یک کاربر با View ای خاص، رخدادی صادر شده و اینبار مدیریت این رخداد توسط یک Action (که عموما یک پیام رشتهای است)، به Dispatcher مرکزی ارسال میشود (و نه مستقیما به مخزن حالت خاصی). اکنون این Dispatcher، اکشن رسیده را به مخازن کد مشترک به آن ارسال میکند تا عمل متناسب با آن اکشن درخواستی را انجام دهند. مابقی آن همانند قبل است که پس از تغییر حالت در هر کدام از مخازن حالت، کار به روز رسانی UI، در کامپوننتهای مشترک صورت خواهد گرفت. بدیهی است در اینجا مخازن حالت، مجاز به صرفنظر کردن از یک اکشن خاص هستند و الزامی به پیاده سازی آن ندارند. هدف اصلی این است که اگر اکشنی قرار بود در تمام مخازن حالت پیاده سازی شود و حالتهای آنها را تغییر دهد، روشی را برای مدیریت آن داشته باشیم.
بنابراین اگر به این الگوی جدید دقت کنید، چیزی نیست بجز یک الگوی Observer دو سطحی:
الف) Dispatcher ای (Subject) که مشترکهایی را مانند مخازن حالت دارد (Observers).
ب) مخازن حالتی (Subjects) که مشترکهایی را مانند کامپوننتها دارند (Observers).
اگر پیشتر با React کار کرده باشید، این الگو را تحت عناوینی مانند Flux و یا Redux میشناسید و در اینجا میخواهیم پیاده سازی #C آنرا بررسی کنیم:
در الگوی Flux، در اثر تعامل یک کاربر با کامپوننتی، اکشنی به سمت یک Dispatcher ارسال میشود. سپس Dispatcher این اکشن را به مخزن حالتی جهت مدیریت آن ارسال میکند که در نهایت سبب تغییر حالت آن شده و به روز رسانی UI را در پی خواهد داشت.
پیاده سازی یک Dispatcher برای تعامل با بیش از یک مخزن حالت
پیش از هر کاری نیاز است قالب اکشنهای ارسالی را که قرار است توسط مخازن حالت مورد پردازش قرار گیرند، مشخص کنیم:
namespace BlazorStateManagement.Stores { public interface IAction { public string Name { get; } } }
namespace BlazorStateManagement.Stores.CounterStore { public class IncrementAction : IAction { public const string Increment = nameof(Increment); public string Name { get; } = Increment; } public class DecrementAction : IAction { public const string Decrement = nameof(Decrement); public string Name { get; } = Decrement; } }
پس از تعریف ساختار یک اکشن، اکنون نوبت به پیاده سازی راه حلی برای ارسال آن به تمام مخازن حالت برنامه است:
using System; namespace BlazorStateManagement.Stores { public interface IActionDispatcher { void Dispatch(IAction action); void Subscribe(Action<IAction> actionHandler); void Unsubscribe(Action<IAction> actionHandler); } public class ActionDispatcher : IActionDispatcher { private Action<IAction> _actionHandlers; public void Subscribe(Action<IAction> actionHandler) => _actionHandlers += actionHandler; public void Unsubscribe(Action<IAction> actionHandler) => _actionHandlers -= actionHandler; public void Dispatch(IAction action) => _actionHandlers?.Invoke(action); } }
این سرویس را نیز با طول عمر Scoped به سیستم تزریق وابستگیهای برنامه معرفی میکنیم که سبب میشود تا پایان عمر برنامه (بسته شدن مرورگر یا ریفرش کامل صفحهی جاری)، در حافظه باقی مانده و وهله سازی مجدد نشود. به همین جهت تزریق آن در مخازن حالت مختلف برنامه، دقیقا حالت یک Dispatcher اشتراکی را پیدا خواهد کرد.
namespace BlazorStateManagement.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddScoped<IActionDispatcher, ActionDispatcher>(); // ... } } }
استفاده از IActionDispatcher در مخازن حالت برنامه
در ادامه میخواهیم مخازن حالت برنامه را تحت کنترل سرویس IActionDispatcher قرار دهیم تا کاربر بتواند اکشنی را به Dispatcher ارسال کند و سپس Dispatcher این درخواست را به تمام مخازن حالت موجود، جهت بروز واکنشی (در صورت نیاز)، اطلاعات رسانی نماید.
برای این منظور سرویس ICounterStore قسمت قبل ، به صورت زیر تغییر میکند که اینترفیس IDisposable را پیاده سازی کرده و همچنین دیگر به همراه متدهای عمومی افزایش و یا کاهش مقدار نیست:
using System; namespace BlazorStateManagement.Stores.CounterStore { public interface ICounterStore : IDisposable { CounterState State { get; } 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 bool _isDisposed; private Action _listeners; private readonly IActionDispatcher _actionDispatcher; public CounterStore(IActionDispatcher actionDispatcher) { _actionDispatcher = actionDispatcher ?? throw new ArgumentNullException(nameof(actionDispatcher)); _actionDispatcher.Subscribe(HandleActions); } private void HandleActions(IAction action) { switch (action) { case IncrementAction: IncrementCount(); break; case DecrementAction: DecrementCount(); break; } } public CounterState State => _state; private void IncrementCount() { _state.Count++; BroadcastStateChange(); } private void DecrementCount() { _state.Count--; BroadcastStateChange(); } public void AddStateChangeListener(Action listener) => _listeners += listener; public void RemoveStateChangeListener(Action listener) => _listeners -= listener; public void BroadcastStateChange() => _listeners.Invoke(); public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_isDisposed) { try { if (disposing) { _actionDispatcher.Unsubscribe(HandleActions); } } finally { _isDisposed = true; } } } } }
- با توجه به اینکه CounterStore یک سرویس ثبت شدهی در سیستم است، میتواند از مزیت تزریق سایر سرویسها در سازندهی خودش بهرهمند شود؛ مانند تزریق سرویس جدید IActionDispatcher.
- پس از تزریق سرویس جدید IActionDispatcher، متدهای Subscribe آنرا در سازندهی کلاس و Unsubscribe آنرا در حین Dispose سرویس، فراخوانی میکنیم. البته فراخوانی و یا پیاده سازی Unsubscribe و Dispose در اینجا غیرضروری است؛ چون طول عمر این کلاس با طول عمر برنامه یکی است.
- بر اساس این الگوی جدید، هر اکشنی که به سمت Dispatcher مرکزی ارسال میشود، در نهایت به متد HandleActions یکی از مخازن حالت تعریف شده، خواهد رسید:
private void HandleActions(IAction action) { switch (action) { case IncrementAction: IncrementCount(); break; case DecrementAction: DecrementCount(); break; } }
@inject ICounterStore CounterStore @code { private void IncrementCount() { CounterStore.IncrementCount(); }
- ابتدا در انتهای فایل Client\_Imports.razor، فضای نام سرویس جدید IActionDispatcher را اضافه میکنیم:
@using BlazorStateManagement.Stores
// ... @inject IActionDispatcher ActionDispatcher @code { private void IncrementCount() { ActionDispatcher.Dispatch(new IncrementAction()); }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorStateManagement-Part-2.zip