۱۳۹۰/۰۹/۲۳

MVVM و رويدادگرداني


در دو قسمت قبل به اينجا رسيديم كه بجاي شروع به كدنويسي مستقيم در code behind يك View (يك پنجره، يك user control ...)، كلاس مجزاي ديگري را به نام ViewModel به برنامه اضافه خواهيم كرد و اين كلاس از وجود هيچ فرمي در برنامه مطلع نيست.
بنابراين جهت انتقال رخدادها به ViewModel، بجاي روش متداول تعريف روال‌هاي رخدادگردان در Code behind:
<Button  Click="btnClick_Event">Last</Button>

آن‌ها را با Commands به ViewModel ارسال خواهيم كرد:
<Button Command="{Binding GoLast}">Last</Button>  


همچنين بجاي اينكه مستقيما بخواهيم از طريق نام يك شيء به مثلا خاصيت متني آن دسترسي پيدا كنيم:
<TextBox Name="txtName" />  

از طريق Binding، اطلاعات مثلا متني آن‌را به ViewModel منتقل خواهيم كرد:
<TextBox Text="{Binding Name}" />  


و همينجا است كه 99 درصد آموزش‌هاي MVVM موجود در وب به پايان مي‌رسند؛ البته پس از مشاهده 10 تا 20 ويديو و خواندن بيشتر از 30 تا مقاله! و اينجا است كه خواهيد گفت: فقط همين؟! با اين‌ها ميشه يك برنامه رو مديريت كرد؟!
البته همين‌ها براي مديريت قسمت عمده‌اي از اكثر برنامه‌ها كفايت مي‌كنند؛ اما خيلي از ريزه‌ كاري‌ها وجود دارند كه به اين سادگي‌ها قابل حل نيستند و در طي چند مقاله به آن‌ها خواهيم پرداخت.

سؤال: در همين مثال فوق، اگر متن ورودي در TextBox تغيير كرد، چگونه مي‌توان بلافاصله از تغييرات آن در ViewModel مطلع شد؟ قديم‌ترها مي‌شد نوشت:
<TextBox TextChanged="TextBox_TextChanged" />


اما الان كه قرار نيست در code behind كد بنويسيم (تا حد امكان البته)، بايد چكار كرد؟
پاسخ: امكان Binding به TextChanged وجود ندارد، پس آن‌را فراموش مي‌كنيم. اما همان Binding معمولي را به اين صورت هم مي‌شود نوشت (همان مثال قسمت قبل):
<TextBox Text="{Binding 
                            MainPageModelData.Name, 
                            Mode=TwoWay, 
                            UpdateSourceTrigger=PropertyChanged}" />


و نكته مهم آن UpdateSourceTrigger است. اگر روي حالت پيش فرض باشد، ViewModel پس از تغيير focus از اين TextBox به كنترلي ديگر، از تغييرات آگاه خواهد شد. اگر آن‌را صريحا ذكر كرده و مساوي PropertyChanged قرار دهيم (اين مورد در سيلورلايت 5 جديد است؛ هر چند از روز نخست WPF وجود داشته است)، با هر تغييري در محتواي TextBox، خاصيت MainPageModelData.Name به روز رساني خواهد شد.
اگر هم بخواهيم اين تغييرات آني‌را در ViewModel تحت نظر قرار دهيم، مي‌توان نوشت:

using System.ComponentModel;

namespace SL5Tests
{
    public class MainPageViewModel
    {
        public MainPageModel MainPageModelData { set; get; }
        public MainPageViewModel()
        {
            MainPageModelData = new MainPageModel();
            MainPageModelData.Name = "Test1";
            MainPageModelData.PropertyChanged += MainPageModelDataPropertyChanged;
        }

        void MainPageModelDataPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            switch (e.PropertyName)
            {
                case "Name":
                    //do something
                    break;
            }
        }
    }
}

تعريف MainPageModel را در قسمت قبل مشاهده كرده‌ايد و اين كلاس اينترفيس INotifyPropertyChanged را پياده سازي مي‌كند. بنابراين مي‌توان از رويدادگردان PropertyChanged آن در ViewModel هم استفاده كرد.
به اين ترتيب همان كار رويدادگردان TextChanged را اينطرف هم مي‌توان شبيه سازي كرد و تفاوتي نمي‌كند. البته با اين تفاوت كه در ViewModel فقط به اطلاعات به روز موجود در MainPageModelData.Name دسترسي داريم، اما نمي‌دانيم و نمي‌خواهيم هم بدانيم كه منبع آن دقيقا كدام شيء رابط كاربري برنامه است.

سؤال: ما قبلا مثلا مي‌توانستيم بررسي كنيم كه اگر كاربر حين تايپ در يك TextBox بر روي دكمه‌ي Enter كليك كرد، آن‌گاه براي نمونه، جستجويي بر اساس اطلاعات وارد شده صورت گيرد. الان اين فشرده شدن دكمه‌ي Enter را چگونه دريافت و چگونه به ViewModel ارسال كنيم؟
اين مورد كمي پيشرفته‌تر از حالت‌هاي قبلي است. براي حل اين مساله ابتدا بايد UpdateSourceTrigger ياد شده را مساوي Explicit قرار داد. يعني اينبار مي‌خواهيم نحوه ي به روز رساني خاصيت MainPageModelData.Name را از طريق Binding خودمان مديريت كنيم. اين مديريت كردن هم با استفاده از امكاناتي به نام Attached properties قابل انجام است كه به آن‌ها Behaviors هم مي‌گويند. مثلا:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace SL5Tests
{
    public static class InputBindingsManager
    {
        public static readonly DependencyProperty UpdatePropertySourceWhenEnterPressedProperty
                    = DependencyProperty.RegisterAttached(
                                "UpdatePropertySourceWhenEnterPressed",
                                typeof(bool),
                                typeof(InputBindingsManager),
                                new PropertyMetadata(false, OnUpdatePropertySourceWhenEnterPressedPropertyChanged));

        static InputBindingsManager()
        { }

        public static void SetUpdatePropertySourceWhenEnterPressed(DependencyObject dp, bool value)
        {
            dp.SetValue(UpdatePropertySourceWhenEnterPressedProperty, value);
        }

        public static bool GetUpdatePropertySourceWhenEnterPressed(DependencyObject dp)
        {
            return (bool)dp.GetValue(UpdatePropertySourceWhenEnterPressedProperty);
        }

        private static void OnUpdatePropertySourceWhenEnterPressedPropertyChanged(DependencyObject dp, 
    DependencyPropertyChangedEventArgs e)
        {
            var txt = dp as TextBox;
            if (txt == null)
                return;

            if ((bool)e.NewValue)
            {
                txt.KeyDown += HandlePreviewKeyDown;
            }
            else
            {
                txt.KeyDown -= HandlePreviewKeyDown;
            }
        }

        static void HandlePreviewKeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key != Key.Enter) return;

            var txt = sender as TextBox;
            if (txt == null)
                return;

            var binding = txt.GetBindingExpression(TextBox.TextProperty);
            if (binding == null) return;
            binding.UpdateSource();
        }
    }
}

تعريف Attached properties يك قالب استاندارد دارد كه آن را در كد فوق ملاحظه مي‌كنيد. يك تعريف به صورت static و سپس تعريف متدهاي Get و Set آن. با تغيير مقدار آن كه اينجا از نوع bool تعريف شده، متد OnUpdatePropertySourceWhenEnterPressedPropertyChanged به صورت خودكار فراخواني مي‌شود. اينجا است كه ما از طريق آرگومان dp به textBox جاري دسترسي كاملي پيدا مي‌كنيم. مثلا در اينجا بررسي شده كه آيا كليد فشرده شده enter است يا خير. اگر بله، يك سري فرامين را انجام بده. به عبارتي ما توانستيم، قطعه كدي را به درون شيءايي موجود تزريق كنيم. Txt تعريف شده در اينجا، واقعا همان كنترل TextBox ايي است كه به آن متصل شده‌ايم.

و براي استفاده از آن خواهيم داشت:

<UserControl x:Class="SL5Tests.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:VM="clr-namespace:SL5Tests"
    mc:Ignorable="d" Language="fa"    
    d:DesignHeight="300" d:DesignWidth="400">
    <UserControl.Resources>
        <VM:MainPageViewModel x:Name="vmMainPageViewModel" />
    </UserControl.Resources>
    <Grid DataContext="{Binding Source={StaticResource vmMainPageViewModel}}"
          x:Name="LayoutRoot" 
          Background="White">
        <TextBox Text="{Binding 
                            MainPageModelData.Name, 
                            Mode=TwoWay, 
                            UpdateSourceTrigger=Explicit}"
                 VerticalAlignment="Top"
                 VM:InputBindingsManager.UpdatePropertySourceWhenEnterPressed="True"  />
    </Grid>
</UserControl>

همانطور كه مشاهده مي‌كنيد، UpdateSourceTrigger به Explicit تنظيم شده و سپس InputBindingsManager.UpdatePropertySourceWhenEnterPressed به اين كنترل متصل گرديده است. يعني تنها زمانيكه در متد HandlePreviewKeyDown ذكر شده، متد UpdateSource فراخواني گردد، خاصيت MainPageModelData.Name به روز رساني خواهد شد (كنترل آن‌را خودمان در دست گرفته‌ايم نه حالت‌هاي از پيش تعريف شده).

اين روش، روش متداولي است براي تبديل اكثر حالاتي كه Binding و Commanding متداول در مورد آن‌ها وجود ندارد. مثلا نياز است focus را به آخرين سطر يك ListView از داخل ViewModel انتقال داد. در حالت متداول چنين امري ميسر نيست، اما با تعريف يك Attached properties مي‌توان به امكانات شيء ListView مورد نظر دسترسي يافت (به آن متصل شد، يا نوعي تزريق)، آخرين عنصر آن‌را يافته و سپس focus را به آن منتقل كرد يا به هر انديسي مشخص كه بعدا در ViewModel به اين Behavior از طريق Binding ارسال خواهد شد.