۱۳۹۰/۰۹/۲۱

مروري سريع بر اصول مقدماتي MVVM


در قسمت قبل، فلسفه وجودي MVVM و MVC و امثال آن‌را به بياني ساده مطالعه كرديد. همچنين به اينجا رسيديم كه بجاي نوشتن روال رخدادگردان، از Commands استفاده كنيد.
در اين قسمت «تفكر MVVM ايي» بررسي خواهد شد! بنابراين سطح اين قسمت را هم مقدماتي درنظر بگيريد.

در سيستم متداول مايكروسافتي ما هميشه يك فرم داريم به همراه يك سري كنترل. براي استفاده از اين‌ها هم در فايل code behind فرم مرتبط، امكان دسترسي به اين كنترل‌ها وجود دارد. مثلا textBox1.Text يعني ارجاعي مستقيم به شيء textBox1 و سپس دسترسي به خاصيت متني آن.
«تفكر MVVM ايي» مي‌گه كه: خير! اينكار رو نكنيد؛ ارجاع مستقيم به يك كنترل روش كار من نيست! فرم رو طراحي كنيد؛ براي هيچكدام از كنترل‌ها هم نامي را مشخص نكنيد (برخلاف رويه متداول). يك فايل درست كنيد به نام Model ، داخل آن معادل textBox1.Text را كه مي‌خواهيد استفاده كنيد، پيش بيني و تعريف كنيد؛ مثلا Public string Name . همين!
ما نمي‌خواهيم بدانيم كه اصلا textBox1 وجود خارجي دارد يا نه. ما فقط با خاصيت متني آن كه در ادامه نحوه‌ي سيم كشي آن‌را هم بررسي خواهيم كرد، كار داريم.
بنابراين بجاي اينكه بنويسيد:

<TextBox Name="txtName" />

كه ممكن است بعدا وسوسه شويد تا از txtName.Text آن استفاده كنيد، بنويسيد:

<TextBox Text="{Binding Name}" />

اين مهم‌ترين قسمت «تفكر MVVM ايي» به زبان ساده است. يعني قرار است تا حد ممكن از Binding استفاده كنيم. مثلا در قسمت قبل هم ديديد كه بجاي نوشتن روال رخدادگردان، فرمان مرتبط با آن‌را به جاي ديگري Bind كرديم.

بنابراين تا اينجا Model ما به اين شكل خواهد بود:

using System.ComponentModel;

namespace SL5Tests
{
    public class MainPageModel : INotifyPropertyChanged
    {
        string _name;
        public string Name 
        {
            get { return _name; }
            set
            {
                if (_name == value) return;
                _name = value;
                raisePropertyChanged("Name");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        void raisePropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler == null) return;
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}


سؤال مهم:
تا اينجا يك فايل Model داريم كه خاصيت Name در آن تعريف شده؛ يك فرم (View) هم داريم كه فقط در آن نوشته شده Binding Name. الان اين‌ها چطور به هم متصل خواهند شد؟
پاسخ: اينجا است كه كلاس ديگري به نام ViewModel (همان فايل Code behind قديمي است با اين تفاوت كه به هيچ فرم خاصي گره نخورده است و اصلا نمي‌داند كه در برنامه فرمي وجود دارد يا نه)، كار خودش را شروع خواهد كرد:

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

ما در اين كلاس يك وهله از MainPageModel را ايجاد خواهيم كرد. اگر فرمي (كه ما دقيقا نمي‌دانيم كدام فرم) در برنامه نياز به يك ViewModel بر اساس مدل ياد شده داشت، مي‌تواند آن‌را مورد استفاده قرار دهد. مقدار دهي آن در ViewModel موجب مقدار دهي خاصيت Text در فرم مرتبط خواهد شد و برعكس (البته به شرطي كه مدل ما INotifyPropertyChanged را پياده سازي كرده باشد و در فرم برنامه Binding Mode دو طرفه تعريف شود).

در قسمت بعد هم كار اتصال نهايي صورت مي‌گيرد:
ابتدا xmlns:VM تعريف مي‌شود تا بتوان به ViewModelها در طرف XAML دسترسي پيدا كرد. سپس در قسمت مثلا UserControl.Resources، اين ViewModel را تعريف كرده و به عنوان DataContext بالاترين شيء فرم مقدار دهي خواهيم كرد:

<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=PropertyChanged}" />
    </Grid>
</UserControl>

اكنون اگر يك breakpoint روي اين سطر Binding قرار دهيم و برنامه را اجرا كنيم، جزئيات اين سيم كشي را در عمل بهتر مي‌توان مشاهده كرد:


البته اين قابليت قرار دادن breakpoint روي Bindingهاي تعريف شده در View فعلا به سيلورلايت 5 اضافه شده و هنوز در WPF موجود نيست.

حداقل مزيتي را كه اينجا مي‌توان مشاهده كرد اين است كه فايل MainPageViewModel چون نمي‌داند كه قرار است در كدام View وهله سازي شود، به سادگي در Viewهاي ديگر نيز قابل استفاده خواهد بود يا تغيير و تعويض كلي View آن كار ساده‌اي است.
Commanding قسمت قبل را هم اينجا مي‌شود اضافه كرد. تعاريف DelegateCommandهاي مورد نياز در ViewModel قرار مي‌گيرند. مابقي عمليات تفاوتي نمي‌كند و يكسان است.