PDA

View Full Version : سوال: UserControlهای تو در تو در MVVM



رافعی مهدی
دوشنبه 12 فروردین 1392, 18:21 عصر
با سلام

دو تا UserControl دارم که یکی از اونها داخل دیگری استفاده میشه. (ChildUserControl و ParentUserControl) پراپرتیهای کنترل Child از نظر استفاده به دو دسته تقسیم میشوند. پراپرتی هایی که مستقیماً نیاز هست مقدار دهی بشوند و پراپرتیهایی که کنترل Parent اونها رو مدیریت (مقداردهی) میکنه. این کار رو قصد دارم با استفاده از الگوی MVVM انجام بدهم، اما ایده درستی برای انجام این کار ندارم و نمیدونم باید از چه روشی استفاده کنم؟

________________________________________

برای واضحتر شدن سوال، مثال بسیار ساده ولی کاملی رو مطرح میکنم. در اینجا برای سادگی، کنترل Child فقط دو تا پراپرتی داره. پراپرتی IsSelected رو باید ParentUserControl مدیریت کنه و پراپرتی PathData رو مستقیماً باید مقداردهی کرد. همچنین، ParentUserControl فقط یک پراپرتی براش درنظر گرفته شده تا صورت این سوال رو پیچیده نکنه.

کنترل فرزند (داخلی):

<UserControl x:Class="NestedUserControls_MVVM.View.ChildUserControl"
xmlns:v="clr-namespace:NestedUserControls_MVVM.View"
...>

<UserControl.Resources>
<Style TargetType="{x:Type v:ChildUserControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type v:ChildUserControl}">
<Border x:Name="brdRoot" Background="#01FFFFFF" Margin="5,3" Height="16" Width="16">
<Path x:Name="pathShape" Stretch="Uniform" Stroke="Gray" Fill="Red" Data="{Binding PathData}"/>
</Border>

<ControlTemplate.Triggers>
<Trigger SourceName="brdRoot" Property="IsMouseOver" Value="True">
<Setter TargetName="pathShape" Property="Stroke" Value="Black"/>
</Trigger>

<DataTrigger Binding="{Binding IsSelected}" Value="False">
<Setter TargetName="pathShape" Property="Fill" Value="Transparent"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="pathShape" Property="Fill" Value="Gray"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
</UserControl>

مدل نمایش این کنترل:

public class ChildUserControlViewModel : DependencyObject
{
#region ________________________________________ IsSelected

public bool IsSelected
{
get { return (bool)GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}

public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register("IsSelected",
typeof(bool),
typeof(ChildUserControlViewModel),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.None));

#endregion


#region ________________________________________ PathData

public Geometry PathData
{
get { return (Geometry)GetValue(PathDataProperty); }
set { SetValue(PathDataProperty, value); }
}

public static readonly DependencyProperty PathDataProperty =
DependencyProperty.Register("PathData",
typeof(Geometry),
typeof(ChildUserControlViewModel),
new FrameworkPropertyMetadata(Geometry.Empty, FrameworkPropertyMetadataOptions.None));

#endregion
}

کنترل والد (خارجی-دربرگیرنده):

<UserControl x:Class="NestedUserControls_MVVM.View.ParentUserControl"
xmlns:v="clr-namespace:NestedUserControls_MVVM.View"
xmlns:vm="clr-namespace:NestedUserControls_MVVM.ViewModel"
...>

<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>

<v:ChildUserControl Grid.Column="0">
<v:ChildUserControl.DataContext>
<!-- Setting explicit value to VM-DPs is OK. -->
<vm:ChildUserControlViewModel
IsSelected="True"
PathData="M9.227273,0.5 L11.5,0.5 11.5,11.5 9.227273,11.5 z M4.8636366,0.5 L7.1363636,0.5 7.1363636,11.5 4.8636366,11.5 z M0.5,0.5 L2.7727273,0.5 2.7727273,11.5 0.5,11.5 z"/>
</v:ChildUserControl.DataContext>
</v:ChildUserControl>
<v:ChildUserControl Grid.Column="1">
<v:ChildUserControl.DataContext>
<!-- Binding any VM-DP causes error. -->
<vm:ChildUserControlViewModel
IsSelected="{Binding ArePathsSelected}"
PathData="M0.5,9.227273 L11.5,9.227273 11.5,11.5 0.5,11.5 z M0.5,4.8636367 L11.5,4.8636367 11.5,7.1363637 0.5,7.1363637 z M0.5,0.5 L11.5,0.5 11.5,2.7727275 0.5,2.7727275 z"/>
</v:ChildUserControl.DataContext>
</v:ChildUserControl>
</Grid>
</UserControl>

مدل نمایش این کنترل:

public class ParentUserControlViewModel : DependencyObject
{
#region ________________________________________ ArePathsSelected

public bool ArePathsSelected
{
get { return (bool)GetValue(ArePathsSelectedProperty); }
set { SetValue(ArePathsSelectedProperty, value); }
}

public static readonly DependencyProperty ArePathsSelectedProperty =
DependencyProperty.Register("ArePathsSelected",
typeof(bool),
typeof(ParentUserControlViewModel),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.None));

#endregion
}

نحوه استفاده از این کنترلها (مثلاً در یک Window)

<Grid>
<StackPanel VerticalAlignment="Center">
<v:ParentUserControl HorizontalAlignment="Center" VerticalAlignment="Center">
<v:ParentUserControl.DataContext>
<vm:ParentUserControlViewModel ArePathsSelected="True"/>
</v:ParentUserControl.DataContext>
</v:ParentUserControl>

<v:ParentUserControl HorizontalAlignment="Center" VerticalAlignment="Center">
<v:ParentUserControl.DataContext>
<vm:ParentUserControlViewModel ArePathsSelected="False"/>
</v:ParentUserControl.DataContext>
</v:ParentUserControl>
</StackPanel>
</Grid>

چیزی که الان داریم:
102176

چیزی که انتظار میره داشته باشیم:
102175

همونطور که به صورت کامنت هم مشخص کردم، اضافه کردن عبارت

IsSelected="{Binding ArePathsSelected}"

در کنترل والد (XAML)، باعث ایجاد خطا میشه که در پنجره Output این پیام رو میده:

System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=ArePathsSelected; DataItem=null; target element is 'ChildUserControlViewModel' (HashCode=13040701); target property is 'IsSelected' (type 'Boolean')

این پست برای سریع تر به پاسخ رسیدن به صورت موازی در اینجا (http://stackoverflow.com/questions/15741515/nested-usercontrols-in-mvvm-with-different-dps-usage) هم در جریان هست.

رافعی مهدی
سه شنبه 13 فروردین 1392, 22:38 عصر
دوستانی که بیشتر با MVVM آشنایی دارند، حتماً میدونند که این کار با استفاده از فریم ورکهای الگوی MVVM مثل Catel قابل انجام هست. ولی چیزی که از ابتدا مد نظرم بود، این بود که این کار رو مستقیماً بدون استفاده از یک فریم ورک دیگر انجام بدهم.

بنابراین برای پیدا کردن پاسخ، فروم ها و مقالات و کتابهای بیشتری رو مطالعه کردم تا اینکه در اینجا (http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/5b835e0a-457b-4bb3-8c18-4d8dfdcf8fd3) به یه جمله طلایی از Josh Smith که لابد همه دوستان ایشون رو میشناسند، رسیدم.

Code-behind files are a perfectly legitimate place to put certain kinds of code. There is a common misconception about MVVM requiring empty code-behind files.


در اون تاپیک آقای Smith کتاب Advanced MVVM خودشون رو هم معرفی کردند. بنابراین شروع کردم به خوندن این کتاب کوتاه و به جمله طلایی دیگه ای رسیدم:

...When using ViewModels, your views can and, in many cases, should still have certain kinds of code in their code-behind files...

بنابراین در MVVM، مهم نیست که Code-Behind یک View خالی باشه، (چیزی که بیشتر افراد رو به اشتباه میکشونه!) مهم اینه که اگر کدی در این قسمت وجود داره، فقط و فقط مربوط به View باشه. اگر data مربوط به View از ViewModel گرفته میشه به این دلیل هست که ViewModel کانال ارتباطی بین View و Model هست. بنابراین به کارگیری صحیح MVVM، دست و پای برنامه نویس رو نه تنها نمیبنده، بلکه باعث نظم پیدا کردن کد، بر اساس مفهومی هست که کد باهاش درگیره (نمایش، مدل نمایش یا مدل داده).

خب با این توضیحات، منطقی هست که برای یک UserControl پراپرتیهایی در نظر بگیریم در حالی که ViewModel هم داره! (چطور؟) به عقیده من باید پیش از هر چیز، نحوه استفاده از پراپرتی برای یک UserControl مشخص بشه:


پراپرتی که قرار هست مستقیماً در والد UserControl (خواه یک Window باشه، خواه یک UserControl دیگه) مقداردهی بشه (مقدار ثابتی داره) تنها مرتبط با View هست. (چون قرار نیست از محل دیگری مقداردهی بشه پس به Model و به طور کلی Data Access Layer ارتباطی نداره، در نتیجه دلیلی نداره که در ViewModel قرار بگیره.)
پراپرتی که قرار هست از هر منبع دیگه ای مقداردهی بشه، باید به ViewModel داده بشه.


بر این اساس، پراپرتی PathData که نیاز به مقدار ثابتی داره از ViewModel خارج شده و در Code-Behind قرار میگیره.

کنترل Child:

<UserControl x:Class="NestedUserControls_MVVM.View.ChildUserControl"
xmlns:v="clr-namespace:NestedUserControls_MVVM.View"
...>

<UserControl.Resources>
<Style TargetType="{x:Type v:ChildUserControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type v:ChildUserControl}">
<Border x:Name="brdRoot" Background="#01FFFFFF" Margin="5,3" Height="16" Width="16">
<Path x:Name="pathShape" Stretch="Uniform" Stroke="Gray" Fill="Red" Data="{Binding PathData}" DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type v:ChildUserControl}}}"/>
</Border>

<ControlTemplate.Triggers>
<Trigger SourceName="brdRoot" Property="IsMouseOver" Value="True">
<Setter TargetName="pathShape" Property="Stroke" Value="Black"/>
</Trigger>

<DataTrigger Binding="{Binding IsSelected}" Value="False">
<Setter TargetName="pathShape" Property="Fill" Value="Transparent"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="pathShape" Property="Fill" Value="Gray"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
</UserControl>


Code-Behind:

public partial class ChildUserControl : UserControl
{
#region ________________________________________ PathData

public Geometry PathData
{
get { return (Geometry)GetValue(PathDataProperty); }
set { SetValue(PathDataProperty, value); }
}

public static readonly DependencyProperty PathDataProperty =
DependencyProperty.Register("PathData",
typeof(Geometry),
typeof(ChildUserControl),
new FrameworkPropertyMetadata(Geometry.Empty, FrameworkPropertyMetadataOptions.None));

#endregion


public ChildUserControl()
{
InitializeComponent();
}


ViewModel:

public class ChildUserControlViewModel : DependencyObject
{
#region ________________________________________ IsSelected

public bool IsSelected
{
get { return (bool)GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}

public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register("IsSelected",
typeof(bool),
typeof(ChildUserControlViewModel),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.None));

#endregion
}


در کنترل Parent، برای Child دو حالت ممکن تست شده، اول اینکه به یک DP از ViewModel به صورت مستقیم مقدار دهی شود (صرفاً جهت تست انجام شده، هیچ کاربردی در حال حاضر از انجام این کار مورد نظرم نبوده.) و دوم اینکه DataContext کنترل Child به صورت صریح set نشود تا IsSelected در DataContext کنترل parent جستجو شود. به همین دلیل قاعده ای برای نام گذاری DPهای وابسته به Parent در نظر گرفتم، اینکه باید در هر دو کلاس فرزند و والد همنام باشند. (ArePathsSelected به IsSelected تغییر نام داده شده.)

کنترل Parent:

<UserControl x:Class="NestedUserControls_MVVM.View.ParentUserControl"
xmlns:v="clr-namespace:NestedUserControls_MVVM.View"
xmlns:vm="clr-namespace:NestedUserControls_MVVM.ViewModel"
...>

<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>

<v:ChildUserControl Grid.Column="0" PathData="M9.227273,0.5 L11.5,0.5 11.5,11.5 9.227273,11.5 z M4.8636366,0.5 L7.1363636,0.5 7.1363636,11.5 4.8636366,11.5 z M0.5,0.5 L2.7727273,0.5 2.7727273,11.5 0.5,11.5 z">
<v:ChildUserControl.DataContext>
<vm:ChildUserControlViewModel IsSelected="True"/>
</v:ChildUserControl.DataContext>
</v:ChildUserControl>
<v:ChildUserControl Grid.Column="1" PathData="M0.5,9.227273 L11.5,9.227273 11.5,11.5 0.5,11.5 z M0.5,4.8636367 L11.5,4.8636367 11.5,7.1363637 0.5,7.1363637 z M0.5,0.5 L11.5,0.5 11.5,2.7727275 0.5,2.7727275 z"/>
</Grid>
</UserControl>


ViewModel:

public class ParentUserControlViewModel : DependencyObject
{
#region ________________________________________ IsSelected

public bool IsSelected
{
get { return (bool)GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}

public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register("IsSelected",
typeof(bool),
typeof(ParentUserControlViewModel),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.None));

#endregion
}


برای استفاده از کنترل parent داریم:

<Grid>
<StackPanel VerticalAlignment="Center">
<v:ParentUserControl HorizontalAlignment="Center" VerticalAlignment="Center">
<v:ParentUserControl.DataContext>
<vm:ParentUserControlViewModel IsSelected="True"/>
</v:ParentUserControl.DataContext>
</v:ParentUserControl>

<v:ParentUserControl HorizontalAlignment="Center" VerticalAlignment="Center">
<v:ParentUserControl.DataContext>
<vm:ParentUserControlViewModel IsSelected="False"/>
</v:ParentUserControl.DataContext>
</v:ParentUserControl>
</StackPanel>
</Grid>


که خروجی این برنامه دقیقاً همان تصویر مورد انتظار که در پست قبل نمایش داده شده، میباشد.

رافعی مهدی
چهارشنبه 14 فروردین 1392, 07:58 صبح
وقتی بیشتر این راه حل رو بررسی کردم، متوجه شدم که یه قسمت از برنامه به کلی از مدار خارج شده! (در این روش از ViewModel کنترل Child دیگه هیچ استفاده ای نمیشه) طبق توضیحات پست قبل، پراپرتیهای باقی مونده در ViewModel از DataContext کنترل والد مقداردهی میشوند و به این ترتیب دیگه نیازی به تعریف ViewModel برای کنترلهای داخلی نیست. (موضوعی که قبلاً هم در دیاگرامهای مختلف اشاراتی بهش رو دیده بودم، ولی... :اشتباه:)

http://www.codeproject.com/KB/WPF/CatelPart3/image006.png

به هر حال امیدوارم این تحلیل برای سایر دوستان مفید بوده باشه.

رافعی مهدی
پنج شنبه 15 فروردین 1392, 14:05 عصر
برای اطمینان از این تحلیل، همین سوال رو در msdn هم مطرح کردم و به جواب مشابهی رسیدم. تاپیک مورد نظر رو میتونید در اینجا (http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/b752245a-4a69-4f42-9914-725d8168d392) ببینید.