۱۳۹۰/۰۹/۲۶

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