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

مطالب
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 و امکان استفاده از یک وهله از 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» پیشتر در این سایت دیده‌اید.

مطالب
راهبری در 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 فراخوانی خواهد گردید.
کدهای این مثال را از اینجا می‌توانید دریافت کنید.

مطالب دوره‌ها
استفاده از AOP Interceptors برای حذف کدهای تکراری INotifyPropertyChanged در WPF
هرکسی که با WPF کار کرده باشد با دردی به نام اینترفیس INotifyPropertyChanged و پیاده سازی‌های تکراری مرتبط با آن آشنا است:
public class MyClass : INotifyPropertyChanged
{
    private string _myValue;
    public event PropertyChangedEventHandler PropertyChanged;
    public string MyValue
    {
        get
        {
            return _myValue;
        }
        set
        {
            _myValue = value;
            RaisePropertyChanged("MyValue");
        }
    }
    protected void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}
چندین راه‌حل هم برای ساده سازی و یا بهبود آن وجود دارد از Strongly typed کردن آن تا روش‌های اخیر دات نت 4 و نیم در مورد استفاده از ویژگی‌های متدهای فراخوان. اما ... با استفاده از AOP Interceptors می‌توان در وهله سازی‌ها و فراخوانی‌ها دخالت کرد و کدهای مورد نظر را در مکان‌های مناسبی تزریق نمود. بنابراین در مطلب جاری قصد داریم ارائه متفاوتی را از پیاده سازی خودکار INotifyPropertyChanged ارائه دهیم. به عبارتی چقدر خوب می‌شد فقط می‌نوشتیم :
public class MyDreamClass : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public string MyValue { get; set; }
}
و ... همه چیز مثل سابق کار می‌کرد. برای رسیدن به این هدف، باید فراخوانی‌های set خواص را تحت نظر قرار داد (یا همان Interception در اینجا). ابتدا باید اجازه دهیم تا set صورت گیرد، پس از آن کدهای معروف RaisePropertyChanged را به صورت خودکار فراخوانی کنیم.


پیشنیازها

ابتدا یک برنامه جدید WPF را آغاز کنید. تنظیمات آن‌را از حالت Client profile به Full تغییر دهید.
سپس همانند قسمت قبل، ارجاعات لازم را به StructureMap و Castle.Core نیز اضافه نمائید:
 PM> Install-Package structuremap
PM> Install-Package Castle.Core


ساختار برنامه

برنامه ما از یک اینترفیس و کلاس سرویس تشکیل شده است:
namespace AOP01.Services
{
    public interface ITestService
    {
        int GetCount();
    }
}

namespace AOP01.Services
{
    public class TestService: ITestService
    {     
        public int GetCount()
        {
            return 10; //این فقط یک مثال است برای بررسی تزریق وابستگی‌ها
        }
    }
}
همچنین دارای یک ViewModel به شکل زیر می‌باشد:
using AOP01.Services;
using AOP01.Core;

namespace AOP01.ViewModels
{
    public class TestViewModel  : BaseViewModel
    {
        private readonly ITestService _testService;
        //تزریق وابستگی‌ها در سازنده کلاس
        public TestViewModel(ITestService testService)
        {
            _testService = testService;
        }

        // Note: it's a virtual property.
        public virtual string Text { get; set; }
    }
}
سه نکته در این ViewModel حائز اهمیت هستند:
الف) استفاده از کلاس پایه BaseViewModel برای کاهش کدهای تکراری مرتبط با INotifyPropertyChanged که به صورت زیر تعریف شده است:
using System.ComponentModel;

namespace AOP01.Core
{
    public abstract class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public void RaisePropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;

            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
ب) کلاس سرویس، در حالت تزریق وابستگی‌ها در سازنده کلاس در اینجا مورد استفاده قرار گرفته است. وهله سازی خودکار آن توسط کلاس‌های پروکسی و DI صورت خواهند گرفت.
ج) خاصیتی که در اینجا تعریف شده از نوع virtual است؛ بدون پیاده سازی مفصل قسمت set آن و فراخوانی مستقیم RaisePropertyChanged کلاس پایه به صورت متداول. علت virtual تعریف کردن آن به امکان دخل و تصرف در نواحی get و set این خاصیت توسط Interceptor ایی که در ادامه تعریف خواهیم کرد بر می‌گردد.


پیاده سازی NotifyPropertyInterceptor

using System;
using Castle.DynamicProxy;

namespace AOP01.Core
{
    public class NotifyPropertyInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            // متد ست، ابتدا فراخوانی می‌شود و سپس کار اطلاع رسانی را انجام خواهیم داد
            invocation.Proceed();

            if (invocation.Method.Name.StartsWith("set_"))
            {
                var propertyName = invocation.Method.Name.Substring(4);
                raisePropertyChangedEvent(invocation, propertyName, invocation.TargetType);
            }
        }

        void raisePropertyChangedEvent(IInvocation invocation, string propertyName, Type type)
        {
            var methodInfo = type.GetMethod("RaisePropertyChanged");
            if (methodInfo == null)
            {
                if (type.BaseType != null)
                    raisePropertyChangedEvent(invocation, propertyName, type.BaseType);
            }
            else
            {
                methodInfo.Invoke(invocation.InvocationTarget, new object[] { propertyName });
            }
        }
    }
}
با اینترفیس IInterceptor در قسمت قبل آشنا شدیم.
در اینجا ابتدا اجازه خواهیم داد تا کار set به صورت معمول انجام شود. دو حالت get و set ممکن است رخ دهند. بنابراین در ادامه بررسی خواهیم کرد که اگر حالت set بود، آنگاه متد RaisePropertyChanged کلاس پایه BaseViewModel را یافته و به صورت پویا با propertyName صحیحی فراخوانی می‌کنیم.
به این ترتیب دیگر نیازی نخواهد بود تا به ازای تمام خواص مورد نیاز، کار فراخوانی دستی RaisePropertyChanged صورت گیرد.


اتصال Interceptor به سیستم

خوب! تا اینجای کار صرفا تعاریف اولیه تدارک دیده شده‌اند. در ادامه نیاز است تا DI و DynamicProxy را از وجود آن‌ها مطلع کنیم.
برای این منظور فایل App.xaml.cs را گشوده و در نقطه آغاز برنامه تنظیمات ذیل را اعمال نمائید:
using System.Linq;
using System.Windows;
using AOP01.Core;
using AOP01.Services;
using Castle.DynamicProxy;
using StructureMap;

namespace AOP01
{
    public partial class App
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            ObjectFactory.Initialize(x =>
            {
                x.For<ITestService>().Use<TestService>();

                var dynamicProxy = new ProxyGenerator();
                x.For<BaseViewModel>().EnrichAllWith(vm =>
                {
                    var constructorArgs = vm.GetType()
                            .GetConstructors()
                            .FirstOrDefault()
                            .GetParameters()
                            .Select(p => ObjectFactory.GetInstance(p.ParameterType))
                            .ToArray();

                    return dynamicProxy.CreateClassProxy(
                                classToProxy: vm.GetType(),
                                constructorArguments: constructorArgs,
                                interceptors: new[] { new NotifyPropertyInterceptor() });
                });
            });
        }
    }
}
مطابق این تنظیمات، هرجایی که نیاز به نوعی از ITestService بود، از کلاس TestService استفاده خواهد شد.
همچنین در ادامه به DI مورد استفاده اعلام می‌کنیم که ViewModelهای ما دارای کلاس پایه BaseViewModel هستند. بنابراین هر زمانی که این نوع موارد وهله سازی شدند، آن‌ها را یافته و با پروکسی حاوی NotifyPropertyInterceptor مزین کن.
مثالی که در اینجا انتخاب شده، تقریبا مشکل‌ترین حالت ممکن است؛ چون به همراه تزریق خودکار وابستگی‌ها در سازنده کلاس ViewModel نیز می‌باشد. اگر ViewModelهای شما سازنده‌ای به این شکل ندارند، قسمت تشکیل constructorArgs را حذف کنید.


استفاده از ViewModel مزین شده با پروکسی در یک View

<Window x:Class="AOP01.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>
        <TextBox Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
    </Grid>
</Window>
اگر فرض کنیم که پنجره اصلی برنامه مصرف کننده ViewModel فوق است، در code behind آن خواهیم داشت:
using AOP01.ViewModels;
using StructureMap;

namespace AOP01
{
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();

            //علاوه بر تشکیل پروکسی
            //کار وهله سازی و تزریق وابستگی‌ها در سازنده را هم به صورت خودکار انجام می‌دهد
            var vm = ObjectFactory.GetInstance<TestViewModel>(); 
            this.DataContext = vm;
        }
    }
}
به این ترتیب یک ViewModel محصور شده توسط DynamicProxy مزین با NotifyPropertyInterceptor به DataContext  ارسال می‌گردد.

اکنون اگر برنامه را اجرا کنیم، مشاهده خواهیم کرد که با وارد کردن مقداری در TextBox برنامه، NotifyPropertyInterceptor مورد استفاده قرار می‌گیرد:



دریافت مثال کامل این قسمت
AOP01.zip
مطالب
آشنایی با WPF قسمت پنجم : DataContext بخش اول
یکی از مهمترین قسمت‌های برنامه، کار با داده‌های بانک اطلاعاتی (یا در کل منابع اطلاعاتی) است. اینکه چگونه با آن‌ها ارتباط برقرار کنیم و آن‌ها را در یک قالب کاربر پسند به کاربران برنامه نشان دهیم. افزودن شیء DataContext و مفاهیمی چون DataBinding باعث ارتباط سریع‌تر و راحت‌تری با منبع داده‌ها شده است. همچنین این قابلیت وجود دارد که هر گونه به روز آوری در اطلاعات دریافت شده، شما را با خبر سازد تا بتوانید طبق آن چه که می‌خواهید اطلاعات نمایشی را به روز کنید. در این مقاله به نحوه‌ی ارتباط بین منبع داده با DataContext و سپس کنترل‌هایی را چون Grid و ListBox و ... در رابطه با این منابع داده بررسی می‌کنیم.

در مورد بررسی ارتباط با داده‌ها در WPF باید سه مورد را بشناسیم:

  • DataContext: این شیء اتصالش را به منبع داده‌ها برقرار کرده و هر موقع داده‌ای را نیاز داریم، از طریق این شیء تامین می‌شود.
  • DataBinding: یک واسطه بین DataContext و هر آن چیزی است که قرار است از داده‌ها تغذیه کند. در تعریفی رسمی‌تر می‌گوییم: روشی ساده و قدرتمند بوده و واسطی است بین مدل تجاری و رابط کاربری. هر زمانی که داده‌ای تغییر کند، ما را آگاه می‌سازد که می‌تواند یک ارتباط یک طرفه یا دو طرفه باشد.
  • DataTemplate: نحوه‌ی فرمت بندی و نمایش داده‌ها را تعیین می‌کند.
ابتدا کار را با یک مثال ساده آغاز می‌کنیم. قصد داریم فرمی را که در قسمت قبلی ساختیم، با استفاده از یک منبع داده پر کنیم:
ابتدا قبل از هر چیزی کلاس فرم قبلی را پیاده سازی می‌کنیم. در این پیاده سازی از یک enum برای انتخاب زمینه‌های کاری هم کمک گرفته ایم و هچنین با یک متد ایستا، منبع داده‌ی تک رکوردی را جهت تست برنامه آماده کرده‌ایم:
   public enum FieldOfWork
    {
        Actor=0,
        Director=1,
        Producer=2
    }
    public class Person
    {
        public string Name { get; set; }

        public bool Gender { get; set; }

        public string ImageName { get; set; }

        public string Country { get; set; }

        public DateTime Date { get; set; }

        public IList<FieldOfWork> FieldOfWork { get; set; }
        public static Person GetPerson()
        {
            return new Person()
            {
                Name = "Leo",
                Gender = true,
                ImageName ="man.jpg",
                Country = "Italy",
                Date = DateTime.Now
            };
        }
    }

حالا لازم است که این منبع داده را در اختیار DataContext بگذاریم. وارد بخش کد نویسی شده و در سازنده‌ی پنجره کد زیر را می‌نویسیم:
 DataContext = Person.GetPerson();
public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = Person.GetPerson();
        }  
    }
با این کار، ارتباط شما با منبع داده آغاز می‌شود و طبق درخواست‌هایی که از DataBinding به آن می‌رسد، اطلاعات را تحویل DataBinding می‌دهد. برای نمایش داده‌ها در کنترل‌ها و استفاده از DataBinding، به سراغ خصوصیات وابسته می‌رویم. در حال حاضر فعلا برنامه را با دو کنترل عکس و نام که رشته‌ای هستند آغاز می‌کنیم؛ چون بقیه‌ی کنترل‌ها کمی متفاوت هستند.
همانطور که می‌دانید متن کنترل TextBox توسط خصوصیت Text پر می‌شود و برای همین در این خصوصیت می‌نویسیم:
Text="{Binding Name}"
علامت {} را باز کرده و در ابتدا نام Binding را می‌آوریم. سپس بعد از یک فاصله، نام پراپرتی کلاسی را که حاوی اطلاعات مدنظر است، می‌نویسیم و بدین صورت اتصال برقرار می‌شود. برای کنترل عکس هم وضعیت به همین صورت است:
Source="{Binding ImageName}"
حال برنامه را اجرا کرده و دو کنترل textbox و Image را بررسی می‌کنیم:


کلمه‌ی Leo داخل کادر متنی قرار گرفته و عکس اینبار به صورت ایستا خوانده نشده، بلکه نام عکس از طریق یک منبع داده برای آن فراهم شده است.

اطلاع از به روزرسانی در منبع داده‌ها:
حال این نکته پیش می‌آید که اگر همین اطلاعات دریافت شده در مدل منبع داده تغییر کند، چگونه می‌توانیم از این موضوع مطلع شده و همین اطلاعات به روز شده را که نمایش داده‌ایم، تغییر دهیم. بنابراین جهت اطلاع از این مورد، کد را به شکل زیر تغییر می‌دهیم.

کار را از یک کلاس آغاز می‌کنیم. از اینترفیس INotifyPropertyChanged ارث بری کرده و در آن یک رویداد و یک متد را تعریف می‌کنیم و کمی در هم در تعریف Property‌ها دست می‌بریم. فعلا اینکار را فقط برای پراپرتی Name انجام می‌دهیم:
 private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged("Name");
            }
        }
        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(string property)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(property));
            }
        }
در کد بالا یک رویداد از نوع PropertyChangedEventHandler تعریف می‌کنیم که وظیفه‌ی به روزآوری را به عهده دارد؛ ولی صدا زدن این رویداد بر عهده‌ی ماست و خود به خود صدا زده نمی‌شود. پس نیاز است متدی را فراهم کرده تا بدانیم که چه خصوصیتی تغییر یافته‌است و از آن طریق رویداد را فراخوانی کنیم و به رویداد بگوییم که کدام پراپرتی تغییر کرده است. این متد را OnpropertyChanged می‌نامیم که آرگومان ورودی آن نام خصوصیتی است که تغییر یافته است و پس از ارزیابی از صحت آن، رویداد را Invoke می‌کنیم.
در بخش Setter آن خصوصیت هم باید این متد را صدا زده و نام خصوصیت را به آن پاس بدهیم تا موقعی که مدل تغییر پیدا کرد، بگوید که خصوصیت Name بوده است که تغییر کرده است.
برای اینکه بدانیم کد واقعا کار می‌کند و تستی بر آن زده باشیم، فعلا دکمه‌ی Save را به Change تغییر می‌دهیم و کد داخل پنجره را بدین صورت تغییر می‌دهیم:
  public partial class MainWindow : Window
    {
        private Person person;
        public MainWindow()
        {
            InitializeComponent();
            person = Person.GetPerson();
            DataContext = person;

        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            person.Name = "Leonardo Decaperio";
        }  
    }

متغیر کلاسی را از حالت محلی Local به عمومی Global تغییر دادم که از طریق دکمه‌ی منبع داده در دسترس باشد. حال در رویداد دکمه نام بازیگر را تغییر می‌دهم. برنامه را اجرا کنید و بر روی دکمه کلیک کنید. باید بعد از یک لحظه‌ی کوتاه، نام بازیگر از Leo به Leonardo Decaperio تغییر کند.
این کد واقعا کدی مفید جهت به روزرسانی است ولی مشکلی دارد که نام پراپرتی باید به صورت String به آن پاس شود که در یک برنامه بزرگ این مورد یک مشکل خواهد شد و اگر نام خصوصیت تغییر کند باید نام داخل آن هم تغییر کند؛ پس کد را به شکل دیگری بازنویسی می‌کنیم:
 private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged();
            }
        } 

  private void OnPropertyChanged([CallerMemberName] string property="")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(property));
            }
        }

در متد OnPropertyChanged در کنار پارامتر اول، ویژگی attribute به نام CallerMemberName را که در فضای نام system.runtime.compilerservice قرار دارد استفاده می‌کنیم (دات نت 4.5). این ویژگی، نام پراپرتی یا متدی که متد OnpropertyChnaged را صدا زده است، به دست می‌آورد. پارامتر اول را هم اختیاری می‌کنیم که سیستم بر ورود پارامتر اجباری نداشته باشد و نهایتا در هر پراپرتی تنها لازم است همانند بالا، خط زیر ذکر شود:
OnPropertyChanged();
اگر الان یک تست از آن بگیرید، می‌بینید که بدون مشکل کار می‌کند. حالا همین متد را در setter تمام پراپرتی‌هایی که دوست دارید از تغییر آن‌ها آگاه شوید قرار دهید.
کد این قسمت
در قسمت‌های آینده به بررسی تبدیل مقادیر و framework element و کنترل‌ها می‌پردازیم.
مطالب
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 و فراخوانی متدهای اشیاء 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 پنجره را فراخوانی می‌کند.

مطالب
کاربردهای Static reflection - قسمت اول

در مورد static reflection مقدمه‌ای پیشتر در این سایت قابل مطالعه است (^) و پیشنیاز بحث جاری است. در ادامه قصد داریم یک سری از کاربردهای متداول آن‌را که این روزها در گوشه و کنار وب یافت می‌شود، به زبان ساده بررسی کنیم.

بهبود کدهای موجود

از static reflection در دو حالت کلی می‌توان استفاده کرد. یا قرار است کتابخانه‌ای را از صفر طراحی کنیم یا اینکه خیر؛ کتابخانه‌ای موجود است و می‌خواهیم کیفیت آن‌را بهبود ببخشیم. هدف اصلی هم «حذف رشته‌ها» و «استفاده از کد بجای رشته‌ها» است.
برای مثال قطعه کد زیر یک مثال متداول مرتبط با WPF و یا Silverlight است. در آن با پیاده سازی اینترفیس INotifyPropertyChanged و استفاده از متد raisePropertyChanged ، به رابط کاربری برنامه اعلام خواهیم کرد که لطفا خودت را بر اساس اطلاعات جدید تنظیم شده در قسمت set خاصیت Name ، به روز کن:
using System.ComponentModel;


namespace StaticReflection
{
public class User : 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));
}
}
}

تعاریف قسمت PropertyChangedEventArgs این پیاده سازی، خارج از کنترل ما است و در دات نت فریم ورک تعریف شده است. حتما هم نیاز به رشته دارد؛ آن هم نام خاصیتی که تغییر کرده است. چقدر خوب می‌شد اگر می‌توانستیم این رشته را حذف کنیم تا کامپایلر بتواند صحت بکارگیری اطلاعات وارد شده را دقیقا پیش از اجرای برنامه بررسی کند. الان فقط در زمان اجرا است که متوجه خواهیم شد، مثلا آیا به روز رسانی مورد نظر صورت گرفته‌است یا خیر؛ اگر نه، یعنی احتمالا یک اشتباه تایپی جایی وجود دارد.
برای بهبود این کد همانطور که در قسمت قبل نیز گفته شد، از ترکیب کلاس‌های Expression و Func استفاده خواهیم کرد. در اینجا Func قرار نیست چیزی را اجرا کند، بلکه از آن به عنوان قطعه‌ کدی که اطلاعاتش قرار است استخراج شود (Lambdas as Data) استفاده می‌شود. این استخراج اطلاعات هم توسط کلاس Expression انجام می‌شود. بنابراین قسمت اول بهبود کد به صورت زیر شروع می‌شود:
void raisePropertyChanged(Expression<Func<object>> expression)


الان اگر متد raisePropertyChanged بکارگرفته شده در خاصیت Name را بخواهیم اصلاح کنیم، حداقل با دو واقعه‌ی مطلوب زیر مواجه خواهیم شد:
Intellisense به صورت خودکار کار می‌کند:


حتی بدوی‌ترین ابزارهای Refactoring موجود (منظور همان ابزار توکار VS.NET است!) هم امکان Refactoring را در اینجا فراهم خواهند ساخت:



در پایان کد تکمیل شده فوق به شرح زیر خواهد بود که در آن از کلاس Expression جهت استخراج Member.Name استفاده شده است:
using System;

using System.ComponentModel;
using System.Linq.Expressions;

namespace StaticReflection
{
public class User : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value) return;
_name = value;
raisePropertyChanged(() => Name);
}
}

public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(Expression<Func<object>> expression)
{
var memberExpression = expression.Body as MemberExpression;
if (memberExpression == null)
throw new InvalidOperationException("Not a member access.");

var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(memberExpression.Member.Name));
}
}
}

در اینجا باز هم نهایتا به همان PropertyChangedEventArgs استاندارد و موجود، برمی‌گردیم؛ اما آرگومان رشته‌ای آن‌را به کمک ترکیب کلاس‌های Expression و Func تامین خواهیم کرد.