۱۳۹۰/۰۹/۱۹

تكميل كلاس DelegateCommand


مدت‌ها از كلاس DelegateCommand معرفي شده در اين آدرس استفاده مي‌كردم. اين كلاس يك مشكل جزئي دارد و آن هم عدم بررسي مجدد قسمت canExecute به صورت خودكار هست.

خلاصه‌اي براي كساني كه بار اول هست با اين مباحث برخورد مي‌كنند؛ يا MVVM به زبان بسيار ساده:

در برنامه نويسي متداول سيستم مايكروسافتي، در هر سيستمي كه ايجاد كرده و در هر فناوري كه ارائه داده از زمان VB6 تا امروز، شما روي يك دكمه مثلا دوبار كليك مي‌كنيد و در فايل اصطلاحا code behind اين فرم و در روال رخدادگردان آن شروع به كد نويسي خواهيد كرد. اين مورد تقريبا در همه جا صادق است؛ از WinForms تا WPF تا Silverlight تا حتي ASP.NET Webforms . به عمد هم اين طراحي صورت گرفته تا برنامه نويس‌ها در اين محيط‌ها زياد احساس غريبي نكنند. اما اين روش يك مشكل مهم دارد و آن هم «توهم» جداسازي رابط كاربر از كدهاي برنامه است. به ظاهر يك فايل فرم وجود دارد و يك فايل جداي code behind ؛ اما در عمل هر دوي اين‌ها يك partial class يا به عبارتي «يك كلاس» بيشتر نيستند. «فكر مي‌كنيم» كه از هم جدا شدند اما واقعا يكي هستند. شما در code behind صفحه به صورت مستقيم با عناصر رابط كاربري سروكار داريد و كدهاي شما به اين عناصر گره خورده‌اند.
شايد بپرسيد كه چه اهميتي دارد؟
مشكل اول: امكان نوشتن آزمون‌ها واحد براي اين متدها وجود ندارد يا بسيار سخت است. اين متدها فقط با وجود فرم و رابط كاربري متناظر با آن‌ها هست كه معنا پيدا مي‌كنند و تك تك عناصر آن‌ها وهله سازي مي‌شوند.
مشكل دوم: كد نوشته فقط براي همين فرم جاري آن قابل استفاده است؛ چون به صورت صريح به عناصر موجود در فرم اشاره مي‌كند. نمي‌تونيد اين فايل code behind رو برداريد بدون هيچ تغييري براي فرم ديگري استفاده كنيد.
مشكل سوم: نمي‌تونيد طراحي فرم رو بديد به يك نفر، كد نويسي اون رو به شخصي ديگر. چون ايندو لازم و ملزوم يكديگرند.

اين سيستم كد نويسي دهه 90 است.
چند سالي است كه طراحان سعي كرده‌اند اين سيستم رو دور بزنند و روش‌هايي رو ارائه بدن كه در آن‌ها فرم‌هاي برنامه و فايل‌هاي پياده سازي كننده‌ي منطق آن هيچگونه ارتباط مستقيمي باهم نداشته باشند؛ به هم گره نخورده باشند؛ ارجاعي به هيچيك از عناصر بصري فرم را در خود نداشته باشند. به همين دليل ASP.NET MVC به وجود آمده و در همان سال‌ها مثلا MVVM .

سؤال:
الان كه رابط كاربري از فايل پياده سازي كننده منطق آن جدا شده و ديگر Code behind هم نيست (همان partial class هاي متداول)، اين فايل‌ها چطور متوجه مي‌شوند كه مثلا روي يك فرم، شيءايي قرار گرفته؟ از كجا متوجه خواهند شد كه روي دكمه‌اي كليك شده؟ اين‌ها كه ارجاعي از فرم را در درون خود ندارند.
در الگوي MVVM اين سيم كشي توسط امكانات قوي Binding موجود در WPF ميسر مي‌شود. در ASP.NET MVC چيزي شبيه به آن به نام Model binder و همان مكانيزم‌هاي استاندارد HTTP اين كار رو مي‌كنه. در MVVM شما بجاي code behind خواهيد داشت ViewModel (اسم جديد آن). در ASP.NET MVC اين اسم شده Controller. بنابراين اگر اين اسامي رو شنيديد زياد تعجب نكنيد. اين‌ها همان Code behind قديمي هستند اما ... بدون داشتن ارجاعي از رابط كاربري در خود كه ... اطلاعات موجود در فرم به نحوي به آن‌ها Bind و ارسال مي‌شوند.
اين سيم كشي‌ها هم نامرئي هستند. يعني فايل ViewModel يا فايل Controller نمي‌دونند كه دقيقا از چه كنترلي در چه فرمي اين اطلاعات دريافت شده.
اين ايده هم جديد نيست. شايد بد نباشه به دوران طلايي Win32 برگرديم. همان توابع معروف PostMessage و SendMessage را به خاطر داريد؟ شما در يك ترد مي‌تونيد با مثلا PostMessage شيءايي رو به يك فرم كه در حال گوش فرا دادن به تغييرات است ارسال كنيد (اين سيم كشي هم نامرئي است). بنابراين پياده سازي اين الگوها حتي در Win32 و كليه فريم ورك‌هاي ساخته شده بر پايه آن‌ها مانند VCL ، VB6 ، WinForms و غيره ... «از روز اول» وجود داشته و مي‌تونستند بعد از 10 سال نيان بگن كه اون روش‌هاي RAD ايي رو كه ما پيشنهاد داديم، مي‌شد خيلي بهتر از همان ابتدا، طور ديگري پياده سازي بشه.

ادامه بحث!
اين سيم كشي يا اصطلاحا Binding ، در مورد رخدادها هم در WPF وجود داره و اينبار به نام Commands معرفي شده‌است. به اين معنا كه بجاي اينكه بنويسيد:
<Button  Click="btnClick_Event">Last</Button>

بنويسيد:
<Button Command="{Binding GoLast}">Last</Button>

حالا بايد مكانيزمي وجود داشته باشه تا اين پيغام رو به ViewModel برنامه برساند. اينكار با پياده سازي اينترفيس ICommand قابل انجام است كه معرفي يك كلاس عمومي از پياده سازي آن‌را در ابتداي بحث مشاهده نموديد.
در يك DelegateCommand،‌ توسط متد منتسب به executeAction، مشخص خواهيم كرد كه اگر اين سيم كشي برقرار شد (كه ما دقيقا نمي‌دانيم و نمي‌خواهيم كه بدانيم از كجا و كدام فرم دقيقا)، لطفا اين اعمال را انجام بده و توسط متد منتسب به canExecute به سيستم Binding خواهيم گفت كه آيا مجاز هستي اين اعمال را انجام دهي يا خير. اگر اين متد false برگرداند، مثلا دكمه ياد شده به صورت خودكار غيرفعال مي‌شود.
اما مشكل كلاس DelegateCommand ذكر شده هم دقيقا همينجا است. اين دكمه تا ابد غيرفعال خواهد ماند. در WPF كلاسي وجود دارد به نام CommandManager كه حاوي متدي استاتيكي است به نام InvalidateRequerySuggested. اگر اين متد به صورت دستي فراخواني شود، يكبار ديگر كليه متدهاي منتسب به تمام canExecute هاي تعريف شده، به صورت خودكار اجرا مي‌شوند و اينجا است كه مي‌توان دكمه‌اي را كه بايد مجددا بر اساس شرايط جاري تغيير وضعيت پيدا كند، فعال كرد. بنابراين فراخواني متد InvalidateRequerySuggested يك راه حل كلي رفع نقيصه‌ي ذكر شده است.
راه حل دومي هم براي حل اين مشكل وجود دارد. مي‌توان از رخدادگردان CommandManager.RequerySuggested استفاده كرد. روال منتسب به اين رخدادگردان هر زماني كه احساس كند تغييري در UI رخ داده، فراخواني مي‌شود. بنابراين پياده سازي بهبود يافته كلاس DelegateCommand به صورت زير خواهد بود:

using System;
using System.Windows.Input;

namespace MvvmHelpers
{
    // Ref.
    // - http://johnpapa.net/silverlight/5-simple-steps-to-commanding-in-silverlight/
    // - http://joshsmithonwpf.wordpress.com/2008/06/17/allowing-commandmanager-to-query-your-icommand-objects/
    public class DelegateCommand<T> : ICommand
    {
        readonly Func<T, bool> _canExecute;
        bool _canExecuteCache;
        readonly Action<T> _executeAction;

        public DelegateCommand(Action<T> executeAction, Func<T, bool> canExecute = null)
        {
            if (executeAction == null)
                throw new ArgumentNullException("executeAction");

            _executeAction = executeAction;
            _canExecute = canExecute;
        }

        public event EventHandler CanExecuteChanged
        {
            add { if (_canExecute != null) CommandManager.RequerySuggested += value; }
            remove { if (_canExecute != null) CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute == null ? true : _canExecute((T)parameter);
        }

        public void Execute(object parameter)
        {
            _executeAction((T)parameter);
        }
    }
}

استفاده از آن هم در ViewModel ساده است. يكبار خاصيتي به اين نام تعريف مي‌شود. سپس در سازنده كلاس مقدار دهي شده و متدهاي متناظر آن تعريف خواهند شد:

public DelegateCommand<string> GoLast { set; get; }

//in ctor
GoLast = new DelegateCommand<string>(goLast, canGoLast);

private bool canGoLast(string data)
{
    //ex.
    return ListViewGuiData.CurrentPage != ListViewGuiData.TotalPage - 1;
}

private void goLast(string data)
{
  //do something
}

مزيت كلاس DelegateCommand جديد هم اين است كه مثلا متد canGoLast فوق، به صورت خودكار با به روز رساني UI ، فراخواني و تعيين اعتبار مجدد مي‌شود.