۱۳۹۰/۱۰/۱۰

MVVM و فراخواني متدهاي اشياء View از طريق ViewModel


ما در ViewModel دسترسي مستقيمي به هيچ يك از اشياء موجود در View نداريم (و درستش هم همين است). الان فرض كنيد كه مي‌خواهيم از طريق ViewModel يك View را ببنديم؛ مثلا متد Close آن پنجره را فراخواني كنيم. به عبارتي در حالت كلي مي‌خواهيم يكي از متدهاي تعريف شده يكي از عناصر بصري موجود در View را از طريق ViewModel فراخواني نمائيم.
براي حل اين مساله از فايل‌هاي همان SDK‌ مرتبط با Expression blend استفاده خواهيم كرد.

ابتدا ارجاعاتي را به اسمبلي‌هاي System.Windows.Interactivity.dll و Microsoft.Expression.Interactions.dll اضافه مي‌كنيم.
سپس دو فضاي نام مرتبط هم بايد اضافه شوند:

  xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"  
  xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"

يك مثال عملي:
قصد داريم از طريق ViewModel ، پنجره‌اي را ببنديم. كدهاي XAML اين مثال را در ادامه مشاهده خواهيد كرد:

<Window x:Class="WpfCallMethodActionSample.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:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:vm="clr-namespace:WpfCallMethodActionSample.ViewModels" 
        Name="ThisWindow"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <vm:MainWindowViewModel x:Key="vmMainWindowViewModel" />
    </Window.Resources>
    <Grid DataContext="{Binding Source={StaticResource vmMainWindowViewModel}}">

        <Button Content="Save &amp; Close" VerticalAlignment="Top" Margin="5">            
            <i:Interaction.Triggers>
                <!--فراخواني متدي در ويوو مدل-->
                <i:EventTrigger EventName="Click">
                    <ei:CallMethodAction 
                            TargetObject="{Binding}"
                            MethodName="SaveButtonClicked" />
                </i:EventTrigger>

                <!--فراخواني متدي در شيء جاري از طريق ويوو مدل-->
                <i:EventTrigger SourceObject="{Binding}"  EventName="CloseMainWindow">
                    <ei:CallMethodAction
                            TargetObject="{Binding ElementName=ThisWindow}"
                            MethodName="Close"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>            
        </Button>
    </Grid>
</Window>

همچنين ViewModel تعريف شده نيز همين چند سطر زير است:

using System;

namespace WpfCallMethodActionSample.ViewModels
{
    public class MainWindowViewModel
    {
        public void SaveButtonClicked()
        {
            close();
        }

        public event EventHandler CloseMainWindow;
        private void close()
        {
            if (CloseMainWindow != null) CloseMainWindow(this, EventArgs.Empty);
        }
    }
}

توضيحات:
اگر به ViewModel دقت كنيد خبري از DelegateCommand در آن نيست. بله، به كمك تركيبي از EventTrigger و CallMethodAction مي‌توان جايگزيني را جهت DelegateCommand معرفي شده در قسمت‌هاي قبل اين سري مباحث MVVM ارائه داد.
EventTrigger در اينجا به اين معنا است كه اگر EventName ذكر شده رخ داد، آنگاه اين اعمال را انجام بده. مثلا در اينجا CallMethodAction را فراخواني كن.
CallMethodAction در اسمبلي Microsoft.Expression.Interactions.dll تعريف شده است و تنها متدي از نوع void و بدون پارامتر را مي‌تواند به صورت خودكار فراخواني كند (محدوديت مهم آن است).
اينكه اين متد كجا قرار دارد، توسط TargetObject آن مشخص مي‌شود. اگر TargetObject را مساوي Binding قرار داديم، يعني به دنبال متدي كه در DataContext گريد وجود دارد بگرد. به عبارتي به صورت خودكار به SaveButtonClicked تعريف شده در ViewModel ما متصل خواهد شد و آن‌را فراخواني مي‌كند.

تا اينجا رخداد Click دكمه تعريف شده را به متد SaveButtonClicked موجود در ViewModel سيم كشي كرديم.

در مرحله بعد مي‌خواهيم از طريق ViewModel ، متدي را در View فراخواني كنيم. نكته آن هم پيشتر ذكر شد؛ TargetObject صحيحي را بايد انتخاب كرد. در اينجا براي پنجره جاري نام ThisWindow تعريف شده است و از طريق تعريف:

TargetObject="{Binding ElementName=ThisWindow}"

به CallMethodAction خواهيم گفت كه قرار است متد Close را در شيء ThisWindow فراخواني كني.
همچنين نحوه تعريف EventTrigger ما هم در اينجا برعكس شده است:

<i:EventTrigger SourceObject="{Binding}"  EventName="CloseMainWindow">

قبلا به دنبال مثلا رخداد Click يك دكمه بوديم، اكنون با توجه به SourceObject تعريف شده، در ViewModel به دنبال اين رخداد كه براي نمونه در اينجا CloseMainWindow نام گرفته خواهيم گشت.

بنابراين View اينبار به رخداد CloseMainWindow تعريف شده در ViewModel سيم كشي خواهد شد. اكنون اگر اين رخداد در ViewModel فراخواني شود، CallMethodAction متناظر فعال شده و متد Close پنجره را فراخواني مي‌كند.

۱۳۹۰/۱۰/۰۷

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>


۱۳۹۰/۱۰/۰۴

MVVM و الگوي ViewModel Locator


اگر ViewModel را همان فايل code behind عاري از ارجاعاتي به اشياء بصري بدانيم، يك تفاوت مهم را علاوه بر مورد ذكر شده نسبت به Code behind متداول خواهد داشت: وهله سازي آن بايد دستي انجام شود و خودكار نيست.
اگر به ابتداي كلاس‌هاي code behind‌ دقت كنيد هميشه واژه‌ي partial قابل رويت است، به اين معنا كه اين كلاس در حقيقت جزئي از همان كلاس متناظر با XAML ايي است كه مشاهده مي‌كنيد؛ يا به عبارتي با آن يكي است. فقط جهت زيبايي يا مديريت بهتر، در دو كلاس قرار گرفته‌اند اما واژه كليدي partial اين‌ها را نهايتا به صورت يكسان و يكپارچه‌اي به كامپايلر معرفي خواهد كرد. بنابراين وهله سازي code behind هم خودكار خواهد بود و به محض نمايش رابط كاربري،‌ فايل code behind آن هم وهله سازي مي‌شود؛ چون اساسا و در پشت صحنه، از ديدگاه كامپايلر تفاوتي بين اين دو وجود ندارد.

اكنون سؤال اينجا است كه آيا مي‌توان با ViewModel ها هم همين وهله سازي خودكار را به محض نمايش يك View متناظر، پياده سازي كرد؟
البته صحيح آن اين است كه عنوان شود ViewModel متناظر با يك View و نه برعكس. چون روابط در الگوي MVVM از View به ViewModel به Model است و نه حالت عكس؛ مدل نمي‌داند كه ViewModel ايي وجود دارد. ViewModel هم از وجود View ها در برنامه بي‌خبر است و اين «بي‌خبري‌ها» اساس الگوهايي مانند MVC ، MVVM ، MVP‌ و غيره هستند. به همين جهت شاعر در وصف ViewModel فرموده‌اند كه:

اي در درون برنامه‌ام و View از تو بي خبر_________وز تو برنامه‌ام پر است و برنامه از تو بي خبر :)

پاسخ:
بله. براي اين منظور الگوي ديگري به نام ViewModel Locator طراحي شده است؛ روش‌هاي زيادي براي پياده سازي اين الگو وجود دارند كه ساده‌ترين آن‌ها مورد زير است:
فرض كنيد ViewModel ساده زير را قصد داريم به كمك الگوي ViewModel Locator به View ايي تزريق كنيم:

namespace WpfViewModelLocator.ViewModels
{
    public class MainWindowViewModel
    {
        public string SomeText { set; get; }
        public MainWindowViewModel()
        {
            SomeText = "Data ...";
        }
    }
}

براي اين منظور ابتدا كلاس ViewModelLocatorBase زير را تدارك خواهيم ديد:

using WpfViewModelLocator.ViewModels;

namespace WpfViewModelLocator.ViewModelLocator
{
    public class ViewModelLocatorBase
    {
        public MainWindowViewModel MainWindowVm
        {
            get { return new MainWindowViewModel(); }
        }
    }
}

در اينجا يك وهله از كلاس MainWindowViewModel توسط خاصيتي به نام MainWindowVm در دسترس قرار خواهد گرفت. براي اينكه بتوان اين كلاس را در تمام Viewهاي برنامه قابل دسترسي كنيم، آن‌را در App.Xaml تعريف خواهيم كرد:

<Application x:Class="WpfViewModelLocator.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vml="clr-namespace:WpfViewModelLocator.ViewModelLocator"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <vml:ViewModelLocatorBase x:Key="ViewModelLocatorBase" />
    </Application.Resources>
</Application>

اكنون فقط كافي است در View خود DataContext را به نحو زير مقدار دهي كنيم تا در زمان اجرا به صورت خودكار بتوان به خاصيت MainWindowVm ياد شده دسترسي يافت:

<Window x:Class="WpfViewModelLocator.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"        
        Title="MainWindow" Height="350" Width="525">
    <Grid DataContext="{Binding Path=MainWindowVm, Source={StaticResource ViewModelLocatorBase}}">
        <TextBlock Text="{Binding SomeText}" VerticalAlignment="Top" Margin="5" />
    </Grid>
</Window>

در مورد ViewModel ها و Viewهاي ديگر هم به همين ترتيب خواهد بود. يك وهله از آن‌ها به كلاس ViewModelLocatorBase اضافه مي‌شود. سپس Binding Path مرتبط به DataContext به نام خاصيتي كه در كلاس ViewModelLocatorBase مشخص خواهيم كرد، Bind خواهد شد.

روش دوم:
اگر در اينجا بخواهيم Path را حذف كنيم و فقط دسترسي عمومي به ViewModelLocatorBase را ذكر كنيم، بايد يك Converter نوشت (چون به اين ترتيب مي‌توان به اطلاعات Binding در متد Convert دسترسي يافت). سپس يك قرار داد را هم تعريف خواهيم كرد؛ به اين صورت كه ما در Converter به نام View دسترسي پيدا مي‌كنيم (از طريق ريفلكشن). سپس نام viewModel ايي را كه بايد به دنبال آن گشت مثلا ViewName به علاوه كلمه ViewModel در نظر خواهيم گرفت. در حقيقت يك نوع Convection over configuration است:

using System;
using System.Globalization;
using System.Linq;
using System.Windows.Data;

namespace WpfViewModelLocator.ViewModelLocator
{
    public class ViewModelLocatorBaseConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            //مقدار در اينجا همان مشخصات ويوو است
            if (value == null) return null;
            string viewTypeName = value.GetType().Name;

            //قرار داد ما است
            //ViewModel Name = ViewName + "ViewModel"
            string viewModelName = string.Concat(viewTypeName, "ViewModel");

            //يافتن اسمبلي كه حاوي ويوو مدل ما است
            var asms = AppDomain.CurrentDomain.GetAssemblies();
            var viewModelAsmName = "WpfViewModelLocator"; //نام پروژه مرتبط
            var viewModelAsm = asms.Where(x => x.FullName.Contains(viewModelAsmName)).First();

            //يافتن اين كلاس ويوو مدل مرتبط
            var viewModelType = viewModelAsm.GetTypes().Where(x => x.FullName.Contains(viewModelName)).FirstOrDefault();
            if (viewModelType == null)
                throw new InvalidOperationException(string.Format("Could not find view model '{0}'", viewModelName));

            //وهله سازي خودكار آن
            return Activator.CreateInstance(viewModelType);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

كار اين تبديلگر بسيار ساده و واضح است. Value‌ دريافتي، وهله‌اي از view است. پس به اين ترتيب مي‌توان نام آن‌را يافت. سپس قرارداد ويژه خودمان را اعمال مي‌كنيم به اين ترتيب كه ViewModel Name = ViewName + "ViewModel" و سپس به دنبال اسمبلي كه حاوي اين نام است خواهيم گشت. آن‌را يافته، كلاس مرتبط را در آن پيدا مي‌كنيم و در آخر، به صورت خودكار آن‌را وهله سازي خواهيم كرد.
اينبار تعريف عمومي اين Conveter در فايل App.Xaml به صورت زير خواهد بود:

<Application x:Class="WpfViewModelLocator.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vml="clr-namespace:WpfViewModelLocator.ViewModelLocator"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <vml:ViewModelLocatorBaseConverter x:Key="ViewModelLocatorBaseConverter" />
    </Application.Resources>
</Application>

و استفاده‌ي آن در تمام View هاي برنامه به شكل زير مي‌باشد (بدون نياز به ذكر هيچ نام خاصي و بدون نياز به كلاس ViewModelLocatorBase ياد شده در ابتداي مطلب):

<Window x:Class="WpfViewModelLocator.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"                
        DataContext="{Binding RelativeSource={RelativeSource Self}, 
                              Converter={StaticResource ViewModelLocatorBaseConverter}}"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TextBlock Text="{Binding SomeText}" VerticalAlignment="Top" Margin="5" />
    </Grid>
</Window>


۱۳۹۰/۰۹/۳۰

خلاصه اشتراک‌های روز چهار شنبه 30 آذر 1390

MVVM و امكان استفاده از يك وهله از ViewModel جهت چند View مرتبط


عموما هنگام طراحي يك View، خيلي زود به حجم انبوهي از كدهاي XAML خواهيم رسيد. در ادامه بررسي خواهيم كرد كه چطور مي‌توان يك View را به چندين View خرد كرد، بدون اينكه نيازي باشد تا از چندين ViewModel (يا همان code behind عاري از ارجاعات بصري سابق قرار گرفته در يك پروژه جداي ديگر) استفاده شود و تمام اين View هاي خرد شده هم تنها از يك وهله از ViewModel ايي خاص استفاده كنند و با اطلاعاتي يكپارچه سروكار داشته باشند؛ يا در عمل يكپارچه كار كنند.
اين مشكل از جايي شروع مي‌شود كه مثلا خرد كردن يك user control به چند يوزر كنترل، يعني كار كردن با چند وهله از اشيايي متفاوت. هر چند نهايتا تمام اين‌ها قرار است در يك صفحه در كنار هم قرار گيرند اما در عمل از هم كاملا مجزا هستند و اگر به ازاي هر كدام يكبار ViewModel را وهله سازي كنيم، به مشكل برخواهيم خورد؛ چون هر وهله نسبت به وهله‌اي ديگر ايزوله است. اگر در يكي Name مثلا Test بود در ديگري ممكن است مقدار پيش فرض نال را داشته باشد؛ چون با چند وهله از يك كلاس، در يك فرم نهايي سروكار خواهيم داشت.

ابتدا Model و ViewModel ساده زير را در نظر بگيريد:
using System.ComponentModel;

namespace SplittingViewsInMvvm.Models
{
    public class GuiModel : INotifyPropertyChanged
    {
        string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                raisePropertyChanged("Name");
            }
        }

        string _lastName;
        public string LastName
        {
            get { return _lastName; }
            set
            {
                _lastName = value;
                raisePropertyChanged("LastName");
            }
        }

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

using SplittingViewsInMvvm.Models;

namespace SplittingViewsInMvvm.ViewModels
{
    public class MainViewModel
    {
        public GuiModel GuiModelData { set; get; }

        public MainViewModel()
        {
            GuiModelData = new GuiModel();
            GuiModelData.Name = "Name";
            GuiModelData.LastName = "LastName";
        }
    }
}

سپس View زير هم از اين اطلاعات استفاده خواهد كرد:

<UserControl x:Class="SplittingViewsInMvvm.Views.Main"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             xmlns:VM="clr-namespace:SplittingViewsInMvvm.ViewModels"
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>
        <VM:MainViewModel x:Key="vmMainViewModel" />
    </UserControl.Resources>
    <StackPanel DataContext="{Binding Source={StaticResource vmMainViewModel}}">
        <GroupBox Margin="2" Header="Group 1">
            <TextBlock Text="{Binding GuiModelData.Name}" />
        </GroupBox>
        <GroupBox Margin="2" Header="Group 2">
            <TextBlock Text="{Binding GuiModelData.LastName}" />
        </GroupBox>
    </StackPanel>
</UserControl>

اكنون فرض كنيد كه مي‌خواهيم Group 1 و Group 2 را جهت مديريت ساده‌تر View اصلي در دو user control مجزا قرار دهيم؛ مثلا:

<UserControl x:Class="SplittingViewsInMvvm.Views.Group1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <GroupBox Margin="2" Header="Group 1">
            <TextBlock Text="{Binding GuiModelData.Name}" />
        </GroupBox>
    </Grid>
</UserControl>
و
<UserControl x:Class="SplittingViewsInMvvm.Views.Group2"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <GroupBox Margin="2" Header="Group 2">
            <TextBlock Text="{Binding GuiModelData.LastName}" />
        </GroupBox>
    </Grid>
</UserControl>

اكنون اگر اين دو را مجددا در همان View اصلي ساده شده قبلي قرار دهيم (بدون اينكه در هر user control به صورت جداگانه data context را تنظيم كنيم):
<UserControl x:Class="SplittingViewsInMvvm.Views.Main"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             xmlns:V="clr-namespace:SplittingViewsInMvvm.Views"
             xmlns:VM="clr-namespace:SplittingViewsInMvvm.ViewModels"
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>
        <VM:MainViewModel x:Key="vmMainViewModel" />
    </UserControl.Resources>
    <StackPanel DataContext="{Binding Source={StaticResource vmMainViewModel}}">
        <V:Group1 />
        <V:Group2 />
    </StackPanel>
</UserControl>

باز هم .... برنامه همانند سابق كار خواهد كرد و ViewModel وهله سازي شده در user control فوق به صورت يكساني در اختيار هر دو View اضافه شده قرار مي‌گيرد و نهايتا يك View يكپارچه را در زمان اجرا مي‌توان مورد استفاده قرار داد. علت هم بر مي‌گردد به مقدار دهي خودكار DataContext هر View اضافه شده به بالاترين DataContext موجود در Visual tree كه ذكر آن الزامي نيست:

<UserControl x:Class="SplittingViewsInMvvm.Views.Main"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             xmlns:V="clr-namespace:SplittingViewsInMvvm.Views"
             xmlns:VM="clr-namespace:SplittingViewsInMvvm.ViewModels"
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>
        <VM:MainViewModel x:Key="vmMainViewModel" />
    </UserControl.Resources>
    <StackPanel DataContext="{Binding Source={StaticResource vmMainViewModel}}">
        <V:Group1 DataContext="{Binding}" />
        <V:Group2 DataContext="{Binding}"/>
    </StackPanel>
</UserControl>


بنابراين به صورت خلاصه زمانيكه از MVVM استفاده ‌مي‌كنيد لازم نيست كار خاصي را جهت خرد كردن يك View به چند Sub View انجام دهيد! فقط اين‌ها را در چند User control جدا كنيد و بعد مجددا به كمك فضاي نامي كه تعريف خواهد (مثلا V در اينجا) در همان View اصلي تعريف كنيد. بدون هيچ تغيير خاصي باز هم برنامه همانند سابق كار خواهد كرد.


۱۳۹۰/۰۹/۲۶

خلاصه اشتراک‌های روز شنبه 26 آذر 1390

استفاده از MVVM زمانيكه امكان Binding وجود ندارد


ساده‌ترين تعريف MVVM، نهايت استفاده از امكانات Binding موجود در WPF و Silverlight است. اما خوب، هميشه همه چيز بر وفق مراد نيست. مثلا كنترل WebBrowser را در WPF در نظر بگيريد. فرض كنيد كه مي‌خواهيم خاصيت Source آن‌را در ViewModel مقدار دهي كنيم تا صفحه‌اي را نمايش دهد. بلافاصله با خطاي زير متوقف خواهيم شد:

A 'Binding' cannot be set on the 'Source' property of type 'WebBrowser'.
A 'Binding' can only be set on a DependencyProperty of a DependencyObject.

بله؛ اين خاصيت از نوع DependencyProperty نيست و نمي‌توان چيزي را به آن Bind كرد. بنابراين اين نكته مهم را توسعه دهنده‌هاي كنترل‌هاي WPF و Silverlight هميشه بايد بخاطر داشته باشند كه اگر قرار است كنترل‌هاي شما MVVM friendly باشند بايد كمي بيشتر زحمت كشيده و بجاي تعريف خواص ساده دات نتي، خواص مورد نظر را از نوع DependencyProperty تعريف كنيد.
الان كه تعريف نشده چه بايد كرد؟
پاسخ متداول آن اين است: مهم نيست؛ خودمان مي‌توانيم اين‌كار را انجام دهيم! يك Attached property يا به عبارتي يك Behavior را تعريف و سپس به كمك آن عمليات Binding را ميسر خواهيم ساخت. براي مثال:
در اين Attached property قصد داريم يك خاصيت جديد به نام BindableSource را جهت كنترل WebBrowser تعريف كنيم:

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

namespace WebBrowserSample.Behaviors
{
    public static class WebBrowserBehaviors
    {
        public static readonly DependencyProperty BindableSourceProperty =
            DependencyProperty.RegisterAttached("BindableSource",
                            typeof(object),
                            typeof(WebBrowserBehaviors),
                            new UIPropertyMetadata(null, BindableSourcePropertyChanged));

        public static object GetBindableSource(DependencyObject obj)
        {
            return (string)obj.GetValue(BindableSourceProperty);
        }

        public static void SetBindableSource(DependencyObject obj, object value)
        {
            obj.SetValue(BindableSourceProperty, value);
        }

        public static void BindableSourcePropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            WebBrowser browser = o as WebBrowser;
            if (browser == null) return;

            Uri uri = null;

            if (e.NewValue is string)
            {
                var uriString = e.NewValue as string;
                uri = string.IsNullOrWhiteSpace(uriString) ? null : new Uri(uriString);
            }
            else if (e.NewValue is Uri)
            {
                uri = e.NewValue as Uri;
            }

            if (uri != null) browser.Source = uri;
        }
    }
}


يك مثال ساده از استفاده‌ي آن هم به صورت زير مي‌تواند باشد:
ابتدا ViewModel مرتبط با فرم برنامه را تهيه خواهيم كرد. اينجا چون يك خاصيت را قرار است Bind كنيم، همينجا داخل ViewModel آن‌را تعريف كرده‌ايم. اگر تعداد آن‌ها بيشتر بود بهتر است به يك كلاس مجزا مثلا GuiModel منتقل شوند.

using System;
using System.ComponentModel;

namespace WebBrowserSample.ViewModels
{
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        Uri _sourceUri;
        public Uri SourceUri
        {
            get { return _sourceUri; }
            set
            {
                _sourceUri = value;
                raisePropertyChanged("SourceUri");
            }
        }

        public MainWindowViewModel()
        {
            SourceUri = new Uri(@"C:\path\arrow.png");
        }

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

در ادامه بجاي استفاده از خاصيت Source كه قابليت Binding ندارد، از Behavior سفارشي تعريف شده استفاده خواهيم كرد. ابتدا بايد فضاي نام آن تعريف شود، سپس BindableSource مرتبط آن در دسترس خواهد بود:

<Window x:Class="WebBrowserSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:VM="clr-namespace:WebBrowserSample.ViewModels"
        xmlns:B="clr-namespace:WebBrowserSample.Behaviors"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <VM:MainWindowViewModel x:Key="vmMainWindowViewModel" />
    </Window.Resources>
    <Grid DataContext="{Binding Source={StaticResource vmMainWindowViewModel}}">
        <WebBrowser B:WebBrowserBehaviors.BindableSource="{Binding SourceUri}" />
    </Grid>
</Window>



نمونه مشابه اين مورد را در مثال «استفاده از كنترل‌هاي Active-X در WPF» پيشتر در اين سايت ديده‌ايد.

۱۳۹۰/۰۹/۲۳

خلاصه اشتراک‌های روز چهار شنبه 23 آذر 1390

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 ارسال خواهد شد.

۱۳۹۰/۰۹/۲۱

خلاصه اشتراک‌های روز دو شنبه 21 آذر 1390

مروري سريع بر اصول مقدماتي 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 قرار مي‌گيرند. مابقي عمليات تفاوتي نمي‌كند و يكسان است.

۱۳۹۰/۰۹/۱۹

خلاصه اشتراک‌های روز شنبه 19 آذر 1390


تكميل كلاس 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 ، فراخواني و تعيين اعتبار مجدد مي‌شود.


۱۳۹۰/۰۹/۱۸

۱۳۹۰/۰۹/۱۷

خلاصه اشتراک‌های روز پنج شنبه 17 آذر 1390