۱۳۹۰/۱۰/۰۷

MVVM و رويدادگرداني - قسمت دوم


قسمت اول اين بحث و همچنين پيشنياز آن‌را در اينجا و اينجا مي‌توانيد مطالعه نمائيد.
همه‌ي اين‌ها بسيار هم نيكو! اما ... آيا واقعا بايد به ازاي هر روال رويدادگرداني يك Attached property نوشت تا بتوان از آن در الگوي MVVM استفاده كرد؟ براي يكي دو مورد شايد اهميتي نداشته باشد؛ اما كم كم با بزرگتر شدن برنامه نوشتن اين Attached properties تبديل به يك كار طاقت فرسا مي‌شود و اشخاص را از الگوي MVVM فراري خواهد داد.
براي حل اين مساله، تيم Expression Blend راه حلي را ارائه داده‌اند به نام Interaction.Triggers كه در ادامه به توضيح آن پرداخته خواهد شد.
ابتدا نياز خواهيد داشت تا SDK‌ مرتبط با Expression Blend را دريافت كنيد: (^)
سپس با فايل System.Windows.Interactivity.dll موجود در آن كار خواهيم داشت.

يك مثال عملي:
فرض كنيد مي‌خواهيم رويداد Loaded يك View را در ViewModel دريافت كنيم. زمان وهله سازي يك ViewModel با زمان وهله سازي View يكي است، اما بسته به تعداد عناصر رابط كاربري قرار گرفته در View ، زمان بارگذاري نهايي آن ممكن است متفاوت باشد به همين جهت رويداد Loaded براي آن درنظر گرفته شده است. خوب، ما الان در ViewModel نياز داريم بدانيم كه چه زماني كار بارگذاري يك View به پايان رسيده.
يك راه حل آن‌را در قسمت قبل مشاهده كرديد؛ بايد براي اين كار يك Attached property جديد نوشت چون نمي‌توان Command ايي را به رويداد Loaded انتساب داد يا Bind كرد. اما به كمك امكانات تعريف شده در System.Windows.Interactivity.dll به سادگي مي‌توان اين رويداد را به يك Command استاندارد ترجمه كرد:

<Window x:Class="WpfEventTriggerSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
        xmlns:vm="clr-namespace:WpfEventTriggerSample.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <vm:MainWindowViewModel x:Key="vmMainWindowViewModel" />
    </Window.Resources>
    <Grid DataContext="{Binding Source={StaticResource vmMainWindowViewModel}}">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="Loaded">
                <i:InvokeCommandAction Command="{Binding DoLoadCommand}" 
                                       CommandParameter="I am loaded!" />
            </i:EventTrigger>
        </i:Interaction.Triggers>

        <TextBlock Text="Testing InvokeCommandAction..." 
                   Margin="5" VerticalAlignment="Top" />
    </Grid>
</Window>

ابتدا ارجاعي به اسمبلي System.Windows.Interactivity.dll بايد به پروژه اضافه شود. سپس فضاي نام xmlns:i بايد به فايل XAML جاري مطابق كدهاي فوق اضافه گردد. در نهايت به كمك Interaction.Triggers آن، ابتدا نام رويداد مورد نظر را مشخص مي‌كنيم (EventName) و سپس به كمك InvokeCommandAction، اين رويداد به يك Command استاندارد ترجمه مي‌شود.
ViewModel اين View هم مي‌تواند به شكل زير باشد كه با كلاس DelegateCommand آن در پيشنيازهاي بحث جاري آشنا شده‌ايد.

using WpfEventTriggerSample.Helper;

namespace WpfEventTriggerSample.ViewModels
{
    public class MainWindowViewModel
    {
        public DelegateCommand<string> DoLoadCommand { set; get; }
        public MainWindowViewModel()
        {
            DoLoadCommand = new DelegateCommand<string>(doLoadCommand, canDoLoadCommand);
        }

        private void doLoadCommand(string param)
        {
            //do something
        }

        private bool canDoLoadCommand(string param)
        {
            return true;
        }
    }
}

به اين ترتيب حجم قابل ملاحظه‌اي از كد نويسي Attached properties مورد نياز، به ساده‌ترين شكل ممكن، كاهش خواهد يافت.
بديهي است اين Interaction.Triggers را جهت تمام عناصر UI ايي كه حداقل يك رويداد منتسب تعريف شده داشته باشند، مي‌توان بكار گرفت؛ مثلا تبديل رويداد Click يك دكمه به يك Command استاندارد:

<Button>
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <i:InvokeCommandAction Command="{Binding DoClick}" 
                                       CommandParameter="I am loaded!" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
</Button>