۱۳۹۰/۱۰/۰۴

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>