۱۳۹۰/۰۹/۳۰

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