‏نمایش پست‌ها با برچسب MVVM. نمایش همه پست‌ها
‏نمایش پست‌ها با برچسب MVVM. نمایش همه پست‌ها

۱۳۹۰/۱۰/۱۲

MVVM و نمايش ديالوگ‌ها


بسياري از برنامه‌هاي دسكتاپ نياز به نمايش پنجره‌هاي ديالوگ استاندارد ويندوز مانند OpenFileDialog و SaveFileDialog را دارند و سؤال اينجا است كه چگونه اينگونه موارد را بايد از طريق پياده سازي صحيح الگوي MVVM مديريت كرد؛ از آنجائيكه خيلي راحت در فايل ViewModel مي‌توان نوشت new OpenFileDialog و الي آخر. اين مورد هم يكي از دلايل اصلي استفاده از الگوي MVVM را زير سؤال مي‌برد : اين ViewModel ديگر قابل تست نخواهد بود. هميشه شرايط آزمون‌هاي واحد را به اين صورت در نظر بگيريد:
سروري وجود دارد در جايي كه به آن دسترسي نداريم. روي اين سرور با اتوماسيوني كه راه انداخته‌ايم، آخر هر روز آزمون‌هاي واحد موجود به صورت خودكار انجام شده و يك گزارش تهيه مي‌شود (مثلا يك نوع continuous integration سرور). بنابراين كسي دسترسي به سرور نخواهد داشت تا اين OpenFileDialog ظاهر شده را مديريت كرده، فايلي را انتخاب و به برنامه آزمون واحد معرفي كند. به صورت خلاصه ظاهر شدن هر نوع ديالوگي حين انجام آزمون‌هاي واحد «مسخره» است!
يكي از روش‌هاي حل اين نوع مسايل، استفاده از dependency injection يا تزريق وابستگي‌ها است و در ادامه خواهيم ديد كه چگونه WPF‌ بدون نياز به هيچ نوع فريم ورك تزريق وابستگي خارجي، از اين مفهوم پشتيباني مي‌كند.

مروري مقدماتي بر تزريق وابستگي‌ها
امكان نوشتن آزمون واحد براي new OpenFileDialog وجود ندارد؟ اشكالي نداره، يك Interface بر اساس نياز نهايي برنامه درست كنيد (نياز نهايي برنامه از اين ماجرا فقط يك رشته LoadPath است و بس) سپس در ViewModel با اين اينترفيس كار كنيد؛ چون به اين ترتيب امكان «تقليد» آن فراهم مي‌شود.

يك مثال عملي:
ViewModel نياز دارد تا مسير فايلي را از كاربر بپرسد. اين مساله را با كمك dependency injection در ادامه حل خواهيم كرد.
ابتدا سورس كامل اين مثال:

ViewModel برنامه (تعريف شده در پوشه ViewModels برنامه):

namespace WpfFileDialogMvvm.ViewModels
{
    public interface IFilePathContract
    {
        string GetFilePath();       
    }

    public class MainWindowViewModel
    {
        IFilePathContract _filePathContract;
        public MainWindowViewModel(IFilePathContract filePathContract)
        {
            _filePathContract = filePathContract;
        }

        //...

        private void load()
        {
            string loadFilePath = _filePathContract.GetFilePath();
            if (!string.IsNullOrWhiteSpace(loadFilePath))
            {
                // Do something
            }
        }
    }
}

دو نمونه از پياده سازي اينترفيس IFilePathContract تعريف شده (در پوشه Dialogs برنامه):

using Microsoft.Win32;
using WpfFileDialogMvvm.ViewModels;

namespace WpfFileDialogMvvm.Dialogs
{
    public class OpenFileDialogProvider : IFilePathContract
    {
        public string GetFilePath()
        {
            var ofd = new OpenFileDialog
            {
                Filter = "XML files (*.xml)|*.xml"
            };
            string filePath = null;
            bool? dialogResult = ofd.ShowDialog();
            if (dialogResult.HasValue && dialogResult.Value)
            {
                filePath = ofd.FileName;
            }
            return filePath;
        }
    }

    public class FakeOpenFileDialogProvider : IFilePathContract
    {
        public string GetFilePath()
        {
            return @"c:\path\data.xml";
        }
    }
}

و View برنامه:

<Window x:Class="WpfFileDialogMvvm.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:WpfFileDialogMvvm.ViewModels"
        xmlns:dialogs="clr-namespace:WpfFileDialogMvvm.Dialogs"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>        
        <ObjectDataProvider x:Key="mainWindowViewModel" 
                            ObjectType="{x:Type vm:MainWindowViewModel}">
            <ObjectDataProvider.ConstructorParameters>
                <dialogs:OpenFileDialogProvider/>
            </ObjectDataProvider.ConstructorParameters>
        </ObjectDataProvider>
    </Window.Resources>
    <Grid DataContext="{Binding Source={StaticResource mainWindowViewModel}}">
        
    </Grid>
</Window>

توضيحات:
ما در ViewModel نياز داريم تا مسير نهايي فايل را دريافت كنيم و اين عمليات نياز به فراخواني متد ShowDialog ايي را دارد كه امكان نوشتن آزمون واحد خودكار را از ViewModel ما سلب خواهد كرد. بنابراين بر اساس نياز برنامه يك اينترفيس عمومي به نام IFilePathContract را طراحي مي‌كنيم. در حالت كلي كلاسي كه اين اينترفيس را پياده سازي مي‌كند، قرار است مسيري را برگرداند. اما به كمك استفاده از اينترفيس، به صورت ضمني اعلام مي‌كنيم كه «براي ما مهم نيست كه چگونه». مي‌خواهد OpenFileDialogProvider ذكر شده باشد، يا نمونه تقليدي مانند FakeOpenFileDialogProvider. از نمونه واقعي OpenFileDialogProvider در برنامه اصلي استفاده خواهيم كرد، از نمونه تقليدي FakeOpenFileDialogProvider در آزمون واحد و نكته مهم هم اينجا است كه ViewModel ما چون بر اساس اينترفيس IFilePathContract پياده سازي شده، با هر دو DialogProvider ياد شده مي‌تواند كار كند.
مرحله آخر نوبت به وهله سازي نمونه واقعي، در View برنامه است. يا مي‌توان در Code behind مرتبط با View نوشت:

namespace WpfFileDialogMvvm
{
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new MainWindowViewModel(new OpenFileDialogProvider());
        }
    }
}

و يا از روش ObjectDataProvider توكار WPF هم مي‌شود استفاده كرد؛ كه مثال آن‌را در كدهاي XAML مرتبط با View ذكر شده مي‌توانيد مشاهده كنيد. ابتدا دو فضاي نام vm و dialog تعريف شده (با توجه به اينكه مثلا در اين مثال، دو پوشه ViewModels و Dialogs وجود دارند). سپس كار تزريق وابستگي‌ها به سازنده كلاس MainWindowViewModel،‌ از طريق ObjectDataProvider.ConstructorParameters انجام مي‌شود:

<ObjectDataProvider x:Key="mainWindowViewModel" 
                            ObjectType="{x:Type vm:MainWindowViewModel}">
            <ObjectDataProvider.ConstructorParameters>
                <dialogs:OpenFileDialogProvider/>
            </ObjectDataProvider.ConstructorParameters>
</ObjectDataProvider>

۱۳۹۰/۱۰/۱۰

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>


۱۳۹۰/۰۹/۳۰

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 اصلي تعريف كنيد. بدون هيچ تغيير خاصي باز هم برنامه همانند سابق كار خواهد كرد.


۱۳۹۰/۰۹/۲۶

استفاده از 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» پيشتر در اين سايت ديده‌ايد.

۱۳۹۰/۰۹/۲۳

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

۱۳۹۰/۰۹/۲۱

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

۱۳۹۰/۰۹/۱۹

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


۱۳۹۰/۰۹/۰۸

استفاده از كنترل‌هاي Active-X در WPF


گاهي از اوقات شايد نياز شود تا از يك كنترل Active-X در WPF استفاده شود؛ مثلا هيچ نمايش دهنده‌ي PDF ايي را در ويندوز نمي‌توان يافت كه امكانات و كيفيت آن در حد Acrobat reader و Active-X آن باشد. يك روش استفاده از آن‌را به كمك كنترل WebBrowser در WPF پيشتر در اين سايت مطالعه كرده‌ايد. روش معرفي شده براي WinForm هم در WPF قابل استفاده است كه در ادامه شرح آ‌ن‌ خواهد آمد.

الف) بجاي اضافه كردن يك User control مخصوص WPF يك user control از نوع WinForms را به يك پروژه WPF اضافه كنيد.


سپس مراحل مشابهي را مانند حالت WinForms، بايد طي كرد:
ب) در VS.NET‌ از طريق منوي Tools گزينه‌ي Choose toolbox items ، برگه‌ي Com components را انتخاب كنيد.
ج) سپس گزينه‌ي Adobe PDF reader را انتخاب نمائيد و بر روي دكمه‌ي OK‌ كليك كنيد.


د) اكنون اين كنترل جديد را بر روي فرم user control قسمت الف برنامه قرار دهيد. به صورت خودكار COMReference هاي متناظر هم به پروژه اضافه مي‌شوند.
پس از اينكه كنترل بر روي فرم قرار گرفت بهتر است به خواص آن مراجعه كرده و خاصيت Dock آن‌را با Fill مقدار دهي كرد تا كنترل به صورت خودكار در هر اندازه‌اي كل ناحيه‌ي متناظر را پوشش دهد.


كد‌هاي مرتبط با نمايش فايل PDF اين كنترل هم به شرح زير است:

using System.Windows.Forms;

namespace WpfPdfViewer.Controls
{
    public partial class AcroReader : UserControl
    {
        public AcroReader(string fileName)
        {
            InitializeComponent();
            ShowPdf(fileName);
        }

        public void ShowPdf(string fileName)
        {
            if (string.IsNullOrWhiteSpace(fileName)) return;
            axAcroPDF1.LoadFile(fileName);
            axAcroPDF1.setShowToolbar(true);
            axAcroPDF1.Show();
        }
    }
}


خوب، ما تا اينجا يك كنترل Active-X را از طريق يك User controls مخصوص WinForms به پروژه‌ي WPF جاري اضافه كرده‌ايم. براي اينكه بتوانيم اين كنترل را درون مثلا يك User control از جنس WPF و XAML نمايش دهيم بايد از كنترل WindowsFormsHost استفاده كرد. براي اين منظور نياز است تا ارجاعي را به اسمبلي WindowsFormsIntegration اضافه كنيم. پس از آن كنترل ياد شده قابل استفاده خواهد بود.


براي نمونه كدهاي XAML پنجره اصلي برنامه مي‌تواند به صورت زير باشد:

<Window x:Class="WpfPdfViewer.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>
        <WindowsFormsHost x:Name="WindowsFormsHost1" />
    </Grid>
</Window>

سپس جهت استفاده از كنترل WindowsFormsHost خواهيم داشت:

using WpfPdfViewer.Controls;

namespace WpfPdfViewer
{
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();
            WindowsFormsHost1.Child = new AcroReader(@"PageSummary.pdf");
        }
    }
}

فقط كافي است شيء Child اين كنترل را با وهله‌اي از يوزركنترل AcroReader اضافه شده به برنامه مقدار دهي كنيم.

سؤال: اين روش زياد MVVM friendly نيست. به عبارتي Child را نمي‌توان از طريق Binding مقدار دهي كرد. آيا راهي براي آن وجود دارد؟
پاسخ: بله. روش متداول براي حل اين نوع مشكلات، نوشتن يك DependencyObject و Attached property مناسب مي‌باشد كه به آن‌ها Behaviors هم مي‌گويند. براي مثال يك نمونه از اين پياده سازي را در ذيل مشاهده مي‌كنيد:

using System;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Forms.Integration;

namespace WpfPdfViewer.Behaviors
{
    public class WindowsFormsHostBehavior : DependencyObject
    {
        public static readonly DependencyProperty BindableChildProperty =
                                    DependencyProperty.RegisterAttached("BindableChild",
                                    typeof(Control),
                                    typeof(WindowsFormsHostBehavior),
                                    new UIPropertyMetadata(null, BindableChildPropertyChanged));

        public static Control GetBindableChild(DependencyObject obj)
        {
            return (Control)obj.GetValue(BindableChildProperty);
        }

        public static void SetBindableChild(DependencyObject obj, Control value)
        {
            obj.SetValue(BindableChildProperty, value);
        }

        public static void BindableChildPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var windowsFormsHost = o as WindowsFormsHost;
            if (windowsFormsHost == null)
                throw new InvalidOperationException("This behavior can only be attached to a WindowsFormsHost.");

            var control = (Control)e.NewValue;
            windowsFormsHost.Child = control;
        }
    }
}

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

<WindowsFormsHost 
       Behaviors:WindowsFormsHostBehavior.BindableChild="{Binding ...}" />

و در ViewModel برنامه هم مانند مثال فوق، فقط كافي است يك وهله از new AcroReader به اين خاصيت قابل انقياد از نوع Control، انتساب داده شود.
يا حتي مي‌توان بجاي نوشتن يك BindableChild، براي مثال مسير فايل pdf را به DependencyObject تعريف شده ارسال كرد و سپس در همانجا اين وهله سازي و انتسابات صورت گيرد (بجاي ViewModel برنامه كه اينبار فقط مسير را تنظيم مي‌كند).


۱۳۸۹/۰۸/۰۱

معرفي يك ابزار گزارشگيري رايگان مخصوص WPF


تا صحبت از گزارشگيري به ميان بيايد احتمالا معرفي ابزارهاي تجاري مانند Reporting services ، كريستال ريپورت، stimulsoft.com ، fast-report.com و امثال آن درصدر ليست توصيه كنندگان و مشاوران قرار خواهند داشت. اما خوب براي ايجاد يك گزارشگيري ساده حتما نيازي نيست تا به اين نوع ابزارهاي تجاري مراجعه كرد. ابزار رايگان و سورس باز جالبي هم در اين باره جهت پروژه‌هاي WPF در دسترس است:



در ادامه در طي يك مثال قصد داريم از اين كتابخانه استفاده كنيم:

1) تنظيم وابستگي‌ها
پس از دريافت كتابخانه فوق، ارجاعات زير بايد به پروژه شما اضافه شوند:
CodeReason.Reports.dll (از پروژه فوق) و ReachFramework.dll (جزو اسمبلي‌هاي استاندارد دات نت است)

2) تهيه منبع داده‌ گزارش
كتابخانه‌ي فوق به صورت پيش فرض با DataTable‌ كار مي‌كند. بنابراين كوئري‌هاي شما يا بايد خروجي DataTable داشته باشد يا بايد از يك سري extension methods براي تبديل IEnumerable به DataTable استفاده كرد (در پروژه پيوست شده در پايان مطلب، اين موارد موجود است).
براي مثال فرض كنيد مي‌خواهيم ركوردهايي را از نوع كلاس Product زير در گزارش نمايش دهيم:

namespace WpfRptTests.Model
{
public class Product
{
public string Name { set; get; }
public int Price { set; get; }
}
}
3) تعريف گزارش
الف) اضافه كردن فايل تشكيل دهنده ساختار و ظاهر گزارش
گزارش‌‌هاي اين كتابخانه مبتني است بر اشياء FlowDocument استاندارد WPF . بنابراين از منوي پروژه گزينه‌ي Add new item در قسمت WPF آن يك FlowDocument جديد را به پروژه اضافه كنيد ( بايد دقت داشت كه Build action اين فايل بايد به Content تنظيم گردد). ساختار ابتدايي اين FlowDocument به صورت زير خواهد بود كه به آن FlowDirection و FontFamily مناسب جهت گزارشات فارسي اضافه شده است. همچنين فضاي نام مربوط به كتابخانه‌ي گزارشگيري CodeReason.Reports نيز بايد اضافه گردد.
<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
FlowDirection="RightToLeft" FontFamily="Tahoma"
xmlns:xrd="clr-namespace:CodeReason.Reports.Document;assembly=CodeReason.Reports"
PageHeight="29.7cm" PageWidth="21cm" ColumnWidth="21cm">

</FlowDocument>

مواردي كه در ادامه ذكر خواهند شد محتواي اين گزارش را تشكيل مي‌دهند:
ب) مشخص سازي خواص گزارش

<xrd:ReportProperties>
<xrd:ReportProperties.ReportName>SimpleReport</xrd:ReportProperties.ReportName>
<xrd:ReportProperties.ReportTitle>گزارش از محصولات</xrd:ReportProperties.ReportTitle>
</xrd:ReportProperties>
در اينجا ReportName و ReportTitle بايد مقدار دهي شوند (دو dependency property كه در كتابخانه‌ي CodeReason.Reports تعريف شده‌اند)

ج) مشخص سازي Page Header و Page Footer
اگر مي‌خواهيد عباراتي در بالا و پايين تمام صفحات گزارش تكرار شوند مي‌توان از SectionReportHeader و SectionReportFooter اين كتابخانه به صورت زير استفاده كرد:
    <xrd:SectionReportHeader PageHeaderHeight="2" Padding="10,10,10,0" FontSize="12">
<Table CellSpacing="0">
<Table.Columns>
<TableColumn Width="*" />
<TableColumn Width="*" />
</Table.Columns>
<TableRowGroup>
<TableRow>
<TableCell>
<Paragraph>
<xrd:InlineContextValue PropertyName="ReportTitle" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Right">
<xrd:InlineDocumentValue PropertyName="PrintDate" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
</xrd:SectionReportHeader>

<xrd:SectionReportFooter PageFooterHeight="2" Padding="10,0,10,10" FontSize="12">
<Table CellSpacing="0">
<Table.Columns>
<TableColumn Width="*" />
<TableColumn Width="*" />
</Table.Columns>
<TableRowGroup>
<TableRow>
<TableCell>
<Paragraph>
نام كاربر:
<xrd:InlineDocumentValue PropertyName="RptBy" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Right">
صفحه
<xrd:InlineContextValue PropertyName="PageNumber" FontWeight="Bold" /> از
<xrd:InlineContextValue PropertyName="PageCount" FontWeight="Bold" />
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
</xrd:SectionReportFooter>

دو نكته در اينجا حائز اهميت هستند: xrd:InlineDocumentValue و xrd:InlineContextValue
InlineDocumentValue را مي‌توان در كد‌هاي برنامه به صورت سفارشي اضافه كرد. بنابراين هر جايي كه نياز بود مقدار ثابتي از طريق كد نويسي به گزارش تزريق و اضافه شود مي‌توان از InlineDocumentValue استفاده كرد. براي مثال در كدهاي ViewModel برنامه كه در ادامه ذكر خواهد شد دو مقدار PrintDate و RptBy به صورت زير تعريف و مقدار دهي شده‌اند:
data.ReportDocumentValues.Add("PrintDate", DateTime.Now);
data.ReportDocumentValues.Add("RptBy", "وحيد");
براي مشاهده مقادير مجاز مربوط به InlineContextValue به فايل ReportContextValueType.cs سورس كتابخانه مراجعه كنيد كه شامل PageNumber, PageCount, ReportName, ReportTitle است و توسط CodeReason.Reports به صورت پويا تنظيم خواهد شد.

د) مشخص سازي ساختار توليدي گزارش

<Section Padding="80,10,40,10" FontSize="12">
<Paragraph FontSize="24" TextAlignment="Center" FontWeight="Bold">
<xrd:InlineContextValue PropertyName="ReportTitle" />
</Paragraph>
<Paragraph TextAlignment="Center">
گزارش از ليست محصولات در تاريخ:
<xrd:InlineDocumentValue PropertyName="PrintDate" Format="dd.MM.yyyy HH:mm:ss" />
توسط:
<xrd:InlineDocumentValue PropertyName="RptBy" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
<xrd:SectionDataGroup DataGroupName="ItemList">
<Table CellSpacing="0" BorderBrush="Black" BorderThickness="0.02cm">
<Table.Resources>
<!-- Style for header/footer rows. -->
<Style x:Key="headerFooterRowStyle" TargetType="{x:Type TableRowGroup}">
<Setter Property="FontWeight" Value="DemiBold"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="Background" Value="LightGray"/>
</Style>

<!-- Style for data rows. -->
<Style x:Key="dataRowStyle" TargetType="{x:Type TableRowGroup}">
<Setter Property="FontSize" Value="12"/>
</Style>

<!-- Style for data cells. -->
<Style TargetType="{x:Type TableCell}">
<Setter Property="Padding" Value="0.1cm"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="0.01cm"/>
</Style>
</Table.Resources>

<Table.Columns>
<TableColumn Width="0.8*" />
<TableColumn Width="0.2*" />
</Table.Columns>
<TableRowGroup Style="{StaticResource headerFooterRowStyle}">
<TableRow>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>نام محصول</Bold>
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>قيمت</Bold>
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>

<TableRowGroup Style="{StaticResource dataRowStyle}">
<xrd:TableRowForDataTable TableName="Product">
<TableCell>
<Paragraph>
<xrd:InlineTableCellValue PropertyName="Name" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<xrd:InlineTableCellValue PropertyName="Price" AggregateGroup="Group1" />
</Paragraph>
</TableCell>
</xrd:TableRowForDataTable>
</TableRowGroup>

<TableRowGroup Style="{StaticResource headerFooterRowStyle}">
<TableRow>
<TableCell>
<Paragraph TextAlignment="Right">
<Bold>جمع كل</Bold>
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Sum"
EmptyValue="0"
FontWeight="Bold" />
</Bold>
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>

</Table>

<Paragraph TextAlignment="Center" Margin="5">
در اين گزارش
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Count"
EmptyValue="هيچ"
FontWeight="Bold" /> محصول با جمع كل قيمت
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Sum"
EmptyValue="0"
FontWeight="Bold" /> وجود دارند.
</Paragraph>
</xrd:SectionDataGroup>
</Section>
براي اينكه بتوان اين قسمت‌ها را بهتر توضيح داد، نياز است تا تصاوير مربوط به خروجي اين گزارش نيز ارائه شوند:




در ابتدا توسط دو پاراگراف، عنوان گزارش و يك سطر زير آن نمايش داده شده‌اند. بديهي است هر نوع شيء و فرمت مجاز در FlowDocument را مي‌توان در اين قسمت نيز قرار داد.
سپس يك SectionDataGroup جهت نمايش ليست آيتم‌ها اضافه شده و داخل آن يك جدول كه بيانگر ساختار جدول نمايش ركوردهاي گزارش مي‌باشد، ايجاد گرديده است.
سه TableRowGroup در اين جدول تعريف شده‌اند.
TableRowGroup هاي اولي و آخري دو سطر اول و آخر جدول گزارش را مشخص مي‌كنند (سطر عناوين ستون‌ها در ابتدا و سطر جمع كل در پايان گزارش)
از TableRowGroup مياني براي نمايش ركوردهاي مرتبط با نام جدول مورد گزارشگيري استفاده شده است. توسط TableRowForDataTable آن نام اين جدول بايد مشخص شود كه در اينجا همان نام كلاس مدل برنامه است. به كمك InlineTableCellValue، خاصيت‌هايي از اين كلاس را كه نياز است در گزارش حضور داشته باشند، ذكر خواهيم كرد. نكته‌ي مهم آن AggregateGroup ذكر شده است. توسط آن مي‌توان اعمال جمع، محاسبه تعداد، حداقل و حداكثر و امثال آن‌را كه در فايل InlineAggregateValue.cs سورس كتابخانه ذكر شده‌اند، به فيلدهاي مورد نظر اعمال كرد. براي مثال مي‌خواهيم جمع كل قيمت را در پايان گزارش نمايش دهيم به همين جهت نياز بود تا يك AggregateGroup را براي اين منظور تعريف كنيم.
از اين AggregateGroup در سومين TableRowGroup تعريف شده به كمك xrd:InlineAggregateValue جهت نمايش جمع نهايي استفاده شده است.
همچنين اگر نياز بود در پايان گزارش اطلاعات بيشتري نيز نمايش داده شود به سادگي مي‌توان با تعريف يك پاراگراف جديد، اطلاعات مورد نظر را نمايش داد.

4) نمايش گزارش تهيه شده
نمايش اين گزارش بسيار ساده است. View برنامه به صورت زير خواهد بود:
<Window x:Class="WpfRptTests.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:CodeReason.Reports.Controls;assembly=CodeReason.Reports"
xmlns:vm="clr-namespace:WpfRptTests.ViewModel"
Title="MainWindow" WindowState="Maximized" Height="350" Width="525">
<Window.Resources>
<vm:ProductViewModel x:Key="vmProductViewModel" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource vmProductViewModel}}">
<c:BusyDecorator IsBusyIndicatorHidden="{Binding RptGuiModel.IsBusyIndicatorHidden}">
<DocumentViewer Document="{Binding RptGuiModel.Document}" />
</c:BusyDecorator>
</Grid>
</Window>

تعريف ابتدايي RptGuiModel به صورت زير است (جهت مشخص سازي مقادير IsBusyIndicatorHidden و Document در حين بايندينگ اطلاعات):

using System.ComponentModel;
using System.Windows.Documents;

namespace WpfRptTests.Model
{
public class RptGuiModel
{
public IDocumentPaginatorSource Document { get; set; }
public bool IsBusyIndicatorHidden { get; set; }
}
}
و اين View اطلاعات خود را از ViewModel زير دريافت خواهد نمود:
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using CodeReason.Reports;
using WpfRptTests.Helper;
using WpfRptTests.Model;

namespace WpfRptTests.ViewModel
{
public class ProductViewModel
{
#region Constructors (1)

public ProductViewModel()
{
RptGuiModel = new RptGuiModel();
if (Stat.IsInDesignMode) return;
//انجام عمليات نمايش گزارش در يك ترد ديگر جهت قفل نشدن ترد اصلي برنامه
showReportAsync();
}

#endregion Constructors

#region Properties (1)

public RptGuiModel RptGuiModel { set; get; }

#endregion Properties

#region Methods (3)

// Private Methods (3)

private static List<Product> getProducts()
{
var products = new List<Product>();
for (var i = 0; i < 100; i++)
products.Add(new Product { Name = string.Format("Product{0}", i), Price = i });

return products;
}

private void showReport()
{
try
{
//Show BusyIndicator
RptGuiModel.IsBusyIndicatorHidden = false;

var reportDocument =
new ReportDocument
{
XamlData = File.ReadAllText(@"Report\SimpleReport.xaml"),
XamlImagePath = Path.Combine(Environment.CurrentDirectory, @"Report\")
};

var data = new ReportData();

// تعريف متغيرهاي دلخواه و مقدار دهي آن‌ها
data.ReportDocumentValues.Add("PrintDate", DateTime.Now);
data.ReportDocumentValues.Add("RptBy", "وحيد");

// استفاده از يك سري اطلاعات آزمايشي به عنوان منبع داده
data.DataTables.Add(getProducts().ToDataTable());

var xps = reportDocument.CreateXpsDocument(data);
//انقياد آن به صورت غير همزمان در ترد اصلي برنامه
DispatcherHelper.DispatchAction(
() => RptGuiModel.Document = xps.GetFixedDocumentSequence()
);
}
catch (Exception ex)
{
//وجود اين مورد ضروري است زيرا بروز استثناء در يك ترد به معناي خاتمه آني برنامه است
//todo: log errors
}
finally
{
//Hide BusyIndicator
RptGuiModel.IsBusyIndicatorHidden = true;
}
}

private void showReportAsync()
{
var thread = new Thread(showReport);
thread.SetApartmentState(ApartmentState.STA); //for DocumentViewer
thread.Start();
}

#endregion Methods
}
}

توضيحات:
براي اينكه حين نمايش گزارش، ترد اصلي برنامه قفل نشود، از ترد استفاده شد و استفاده ترد به همراه DocumentViewer كمي نكته دار است:
- ترد تعريف شده بايد از نوع STA باشد كه در متد showReportAsync مشخص شده است.
- حين باينديگ Document توليد شده توسط كتابخانه‌ي گزارشگيري به خاصيت Document كنترل، حتما بايد كل عمليات در ترد اصلي برنامه صورت گيرد كه سورس كلاس DispatcherHelper را در فايل پيوست خواهيد يافت.

كل عمليات اين ViewModel در متد showReport رخ مي‌دهد، ابتدا فايل گزارش بارگذاري مي‌شود، سپس متغيرهاي سفارشي مورد نظر تعريف و مقدار دهي خواهند شد. در ادامه يك سري داده آزمايشي توليد و به DataTables گزارش ساز اضافه مي‌شوند. در پايان XPS Document متناظر آن توليد شده و به كنترل نمايشي برنامه بايند خواهد شد.

دريافت سورس اين مثال

۱۳۸۹/۰۵/۰۸

راهبري در Silverlight به كمك الگوي MVVM


مقدمات راهبري (Navigation) در سيلورلايت را در اينجا مي‌توانيد مطالعه نمائيد : +
مطلبي را كه در فصل فوق نخواهيد يافت در مورد نحوه‌ي بكارگيري الگوي MVVM جهت پياده سازي Navigation در يك برنامه‌ي سيلورلايت است؛ علت آن هم به اين بر مي‌گردد كه اين فصل پيش از مباحث Binding مطرح شد.

صورت مساله:
يكي از اصول MVVM اين است كه در ViewModel‌ نبايد ارجاعي از View وجود داشته باشد (ViewModel بايد در بي‌خبري كامل از وجود اشياء UI و ارجاع مستقيم به آن‌ها طراحي شود)، اما براي پياده سازي مباحث Navigation نياز است به نحوي به شيء Frame قرار داده شده در صفحه‌ي اصلي يا قالب اصلي برنامه دسترسي يافت تا بتوان درخواست رهنمون شدن به صفحات مختلف را صادر كرد. اكنون چكار بايد كرد؟

راه حل:
يكي از راه حل‌هاي جالبي كه براي اين منظور وجود دارد استفاده از امكانات كلاس Messenger مجموعه‌ي MVVM Light toolkit است. از طريق ViewModel برنامه، آدرس صفحه‌ي مورد نظر را به صورت يك پيغام به View مورد نظر ارسال مي‌كنيم و سپس View برنامه كه به اين پيغام‌ها گوش فرا مي‌دهد، پس از دريافت آدرس مورد نظر، نسبت به فراخواني تابع Navigate شيء Frame رابط كاربري برنامه اقدام خواهد كرد. به اين صورت ViewModel برنامه به View خود جهت اعمال راهبري برنامه، گره نخواهد خورد.

روش پياده سازي:
ابتدا ساختار پروژه را در نظر بگيريد (اين شكل دگرگون شده‌ي Solution explorer مرتبط است با productivity tools نصب شده):



در پوشه‌ي Views ، دو صفحه اضافه شده‌اند كه توسط user control ايي به نام menu ليست شده و راهبري خواهند شد. مونتاژ نهايي هم در MainPage.xaml صورت مي‌گيرد.
كدهاي XAML‌ مرتبط با منوي ساده برنامه به شرح زير هستند (Menu.xaml) :

<UserControl x:Class="MvvmLight6.Views.Menu"
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:MvvmLight6.ViewModels" mc:Ignorable="d"
FlowDirection="RightToLeft" d:DesignHeight="300" d:DesignWidth="400">
<UserControl.Resources>
<vm:MenuViewModel x:Key="vmMenuViewModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource vmMenuViewModel}}">
<HyperlinkButton Content="صفحه يك" Margin="5"
Command="{Binding DoNavigate}"
CommandParameter="/Views/Page1.xaml"
/>
<HyperlinkButton Content="صفحه دو" Margin="5"
Command="{Binding DoNavigate}"
CommandParameter="/Views/Page2.xaml"
/>
</StackPanel>
</UserControl>

كدهاي ViewModel مرتبط با اين View كه كار Command گرداني را انجام خواهد داد به شرح زير است:
using GalaSoft.MvvmLight.Command;
using GalaSoft.MvvmLight.Messaging;

namespace MvvmLight6.ViewModels
{
public class MenuViewModel
{
public RelayCommand<string> DoNavigate { set; get; }

public MenuViewModel()
{
DoNavigate = new RelayCommand<string>(doNavigate);
}

private static void doNavigate(string url)
{
Messenger.Default.Send(url, "MyNavigationService");
}
}
}

تمام آيتم‌هاي منوي فوق يك روال را صدا خواهند زد : DoNavigate . تنها تفاوت آن‌ها در CommandParameter ارسالي به RelayCommand ما است كه حاوي آدرس قرارگيري فايل‌هاي صفحات تعريف شده است. اين آدرس‌ها با كمك امكانات كلاس Messenger مجموعه‌ي MVVM light toolkit به View اصلي برنامه ارسال مي‌گردند.
كدهاي XAML مرتبط با MainPage.xaml به شرح زير هستند:

<UserControl x:Class="MvvmLight6.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:sdk="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
xmlns:usr="clr-namespace:MvvmLight6.Views"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="268" />
</Grid.ColumnDefinitions>
<usr:Menu Grid.Column="1" />
<sdk:Frame Margin="5"
Name="frame1"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Grid.Column="0" />
</Grid>
</UserControl>

و كار دريافت پيغام‌ها (يا همان آدرس صفحات جهت انجام راهبري) و عكس العمل نشان دادن به آن‌ها توسط كدهاي ذيل صورت خواهد گرفت:
using System;
using GalaSoft.MvvmLight.Messaging;

namespace MvvmLight6
{
public partial class MainPage
{
public MainPage()
{
registerMessenger();
InitializeComponent();
}

private void registerMessenger()
{
Messenger.Default.Register<string>(this, "MyNavigationService", doNavigate);
}

private void doNavigate(string uri)
{
frame1.Navigate(new Uri(uri, UriKind.Relative));
}
}
}

ابتدا يك Messenger در اينجا رجيستر مي‌شود و سپس به ازاي هر بار دريافت پيغامي با token مساوي MyNavigationService ، متد doNavigate فراخواني خواهد گرديد.
كدهاي اين مثال را از اينجا مي‌توانيد دريافت كنيد.

۱۳۸۹/۰۲/۰۴

آشنايي با الگوي M-V-VM‌ - قسمت پنجم


در اين قسمت قصد داريم از امكانات جديد اعتبار سنجي تعريف شده در فضاي نام استاندارد System.ComponentModel.DataAnnotations استفاده نمائيم. از سيلورلايت سه به بعد امكان استفاده از اين فضاي نام به سادگي در برنامه‌هاي سيلورلايت ميسر است (همچنين در برنامه‌هاي ASP.Net MVC)؛ اما براي كار با آن در WPF نياز به تعدادي متد كمكي مي‌باشد...

فهرست مطالب:
فصل 5- تعيين اعتبار ورودي كاربر و الگوي MVVM
  • مقدمه
  • معرفي برنامه فصل
  • مدل برنامه‌ي فصل
  • ViewModel برنامه فصل
  • View برنامه فصل


دريافت قسمت پنجم
دريافت مثال قسمت پنجم


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

1. Model-View-ViewModel (MVVM) Explained
2. Model View ViewModel
3. DataModel-View-ViewModel pattern
4. 5 Minute Overview of MVVM in Silverlight
5. A Field Guide to WPF Presentation Patterns
6. An attempt at simple MVVM with WPF
7. WPF: If Heineken did MVVM Frameworks Part 1 of n
8. Modal dialogs with MVVM and Silverlight 4
9. How do I do… With the Model-View-ViewModel pattern
10. Intro to WPF MVVM
11. Introduction to MVVM pattern in WPF
12. Learning WPF M-V-VM
13. Binding Combo Boxes in WPF with MVVM
14. Model-View-ViewModel Pattern
15. Unit Testable WCF Web Services in MVVM and Silverlight 4
16. MVVM Part 1: Overview
17. Which came first, the View or the Model?
18. Stackoverflow's questions tagged with MVVM
19. WPF: MVVM (Model View View-Model) Simplified
20. WPF and MVVM tutorial 01, Introduction
21. WPF patterns : MVC, MVP or MVVM or…?
22. Silverlight, MVVM and Validation Part III
23. DotNetKicks.com - Stories recently tagged with 'MVVM'
24. DotNetShoutout - Stories tagged with MVVM
25. MVVM Light Toolkit
26. MVVM screen casts
27. What’s new in MVVM Light V3
28. Using RelayCommands in Silverlight 3 and WPF
29. WPF Apps With The Model-View-ViewModel Design Pattern
30. WPF MVVM and Showing Dialogs