wpf View-First-MVVM 中的用户控件和视图模型
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/14442418/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
UserControls and viewmodels in View-First-MVVM
提问by Neutrino
I'm forced to use View First MVVM in a WPF application and I'm struggling to see how it can be made to work elegantly.
我被迫在 WPF 应用程序中使用 View First MVVM,我正在努力了解如何使其优雅地工作。
The root of the problem lies with nested UserControls. In an MVVM architecture each UserControlneeds to have its view model assigned to its DataContext, this keeps the binding expressions simple, and what's more this is also the way WPF will instantiate any view generated via a DataTemplate.
问题的根源在于嵌套的UserControls. 在 MVVM 架构中,每个架构都UserControl需要将其视图模型分配给它DataContext,这使绑定表达式保持简单,而且这也是 WPF 将实例化通过DataTemplate.
But if a child UserControlhas dependency properties which the parent needs to bind to its own viewmodel then the fact that the child UserControlhas its DataContextset to its own viewmodel means that an 'implicit path' binding in the parent XAML file will resolve to the child's viewmodel instead of the parent's.
但是,如果子项UserControl具有父项需要绑定到其自己的视图模型的依赖项属性,那么子项UserControl将其DataContext设置为自己的视图模型这一事实意味着父 XAML 文件中的“隐式路径”绑定将解析为子项的视图模型父母的。
To work around this every parent of every UserControlin the application will either need to use explicit named bindings for everything by default (which is verbose, ugly and errorprone), or it will have to know whether a specific control has its DataContextset to its own viewmodel or not and use the appropriate binding syntax, (which is equally errorprone, and a major violation of basic encapsulation).
要解决这个UserControl问题,应用程序中的每个父项要么需要默认为所有内容使用显式命名绑定(这是冗长、丑陋且容易出错的),要么必须知道特定控件是否已将其DataContext设置为自己的视图模型或者不使用适当的绑定语法,(这同样容易出错,并且严重违反了基本封装)。
After days of research I haven't come across a single half decent solution to this issue. The closest thing to a solution I've come across is setting the UserControl'sviewmodel to an inner element of the UserControl(the topmost Gridor whatever), which still leaves you facing a problem trying to bind properties of the UserControlitself to its own viewmodel! (ElementNamebinding won't work in this case because the binding would be declared before the named element with the viewmodel assigned to its DataContext).
经过几天的研究,我还没有找到解决这个问题的半个像样的解决方案。我遇到的最接近解决方案的方法是将UserControl's视图模型设置为UserControl(最顶层Grid或其他)的内部元素,这仍然让您面临尝试将UserControl自身的属性绑定到其自己的视图模型的问题!(ElementName在这种情况下绑定将不起作用,因为绑定将在具有分配给其的视图模型的命名元素之前声明DataContext)。
I suspect that the reason not many other people run into this it that they are either using viewmodel first MVVM which doesn't have this issue, or they are using view first MVVM in conjunction with a dependency injection implementation that amelliorates this issue.
我怀疑没有多少其他人遇到这个问题的原因是他们要么使用没有这个问题的 viewmodel first MVVM,要么他们使用 view first MVVM 结合依赖注入实现来改善这个问题。
Does anyone have a clean solution for this please?
有没有人对此有一个干净的解决方案?
UPDATE:
更新:
Sample code as requested.
根据要求提供示例代码。
<!-- MainWindow.xaml -->
<Window x:Class="UiInteraction.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:UiInteraction"
Title="MainWindow" Height="350" Width="525"
x:Name="_this">
<Window.DataContext>
<local:MainWindowVm/>
</Window.DataContext>
<StackPanel>
<local:UserControl6 Text="{Binding MainWindowVmString1}"/>
</StackPanel>
</Window>
namespace UiInteraction
{
// MainWindow viewmodel.
class MainWindowVm
{
public string MainWindowVmString1
{
get { return "MainWindowVm.String1"; }
}
}
}
<!-- UserControl6.xaml -->
<UserControl x:Class="UiInteraction.UserControl6" x:Name="_this"
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:local="clr-namespace:UiInteraction">
<UserControl.DataContext>
<local:UserControl6Vm/>
</UserControl.DataContext>
<StackPanel>
<!-- Is bound to this UserControl's own viewmodel. -->
<TextBlock Text="{Binding UserControlVmString1}"/>
<!-- Has its value set by the UserControl's parent via dependency property. -->
<TextBlock Text="{Binding Text, ElementName=_this}"/>
</StackPanel>
</UserControl>
namespace UiInteraction
{
using System.Windows;
using System.Windows.Controls;
// UserControl code behind declares DependencyProperty for parent to bind to.
public partial class UserControl6 : UserControl
{
public UserControl6()
{
InitializeComponent();
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
"Text", typeof(string), typeof(UserControl6));
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
}
}
namespace UiInteraction
{
// UserControl's viewmodel.
class UserControl6Vm
{
public string UserControlVmString1
{
get { return "UserControl6Vm.String1"; }
}
}
}
This results in:
这导致:
System.Windows.Data Error: 40 : BindingExpression path error: 'MainWindowVmString1' property not found on 'object' ''UserControl6Vm' (HashCode=44204140)'. BindingExpression:Path=MainWindowVmString1; DataItem='UserControl6Vm' (HashCode=44204140); target element is 'UserControl6' (Name='_this'); target property is 'Text' (type 'String')
System.Windows.Data 错误:40:BindingExpression 路径错误:在“对象”“UserControl6Vm”(HashCode=44204140)上找不到“MainWindowVmString1”属性。BindingExpression:Path=MainWindowVmString1; DataItem='UserControl6Vm' (HashCode=44204140); 目标元素是 'UserControl6' (Name='_this'); 目标属性是“文本”(类型“字符串”)
because in MainWindow.xamlthe declaration <local:UserControl6 Text="{Binding MainWindowVmString1}"/>is attempting to resolve MainWindowVmString1on UserControl6Vm.
因为在MainWindow.xaml申报<local:UserControl6 Text="{Binding MainWindowVmString1}"/>正试图解决MainWindowVmString1的UserControl6Vm。
In UserControl6.xamlcommenting out the declaration of the DataContextand the first TextBlockthe code will work, but the UserControlneeds a DataContext. In MainWIndow1using an ElementNameinstead of an implict path binding will also work, but in order to use the ElementNamebinding syntax you would either have to know that the UserControlassigns its viewmodel to its DataContext(encapsulation failure) or altenatively adopt a policy of using ElementNamebindings everywhere. Neither of which is appealing.
在UserControl6.xaml注释掉DataContext和第一个的声明时,TextBlock代码将起作用,但UserControl需要一个DataContext. 在MainWIndow1使用ElementName,而不是隐式的路径结合也将工作,但为了使用ElementName绑定语法,你要么必须知道UserControl它的视图模型其受让人DataContext(封装衰竭)或altenatively采取利用的政策ElementName绑定随处可见。两者都没有吸引力。
回答by Arthur Nunes
An immediate solution is to use a RelativeSourceand set it to look for the DataContextof a parent UserControl:
一个直接的解决方案是使用 aRelativeSource并将其设置为查找DataContext父级的UserControl:
<UserControl>
<UserControl.DataContext>
<local:ParentViewModel />
</UserControl.DataContext>
<Grid>
<local:ChildControl MyProperty="{Binding DataContext.PropertyInParentDataContext, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"/>
</Grid>
</UserControl>
You could also treat the child viewmodels as properties of the parent viewmodel, and propagate it from the parent. That way, the parent viewmodel is aware of the children so it can update their properties. The child viewmodels also may have a "Parent"property which holds a reference to the parent, injected by the parent itelf upon their creation, which may grant direct access to the parent.
您还可以将子视图模型视为父视图模型的属性,并从父视图传播它。这样,父视图模型就知道子视图模型,因此它可以更新它们的属性。子视图模型也可能有一个"Parent"属性,该属性保存对父级的引用,由父级在创建时注入,这可以授予对父级的直接访问权限。
public class ParentViewModel : INotifyPropertyChanged
{
#region INotifyPropertyChanged values
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
private ChildViewModel childViewModel;
public ChildViewModel ChildViewModel
{
get { return this.childViewModel; }
set
{
if (this.childViewModel != value)
{
this.childViewModel = value;
this.OnPropertyChanged("ChildViewModel");
}
}
}
}
<UserControl>
<UserControl.DataContext>
<local:ParentViewModel />
</UserControl.DataContext>
<Grid>
<local:ChildControl DataContext="{Binding ChildViewModel}"
MyProperty1="{Binding PropertyInTheChildControlledByParent}"
MyProperty2="{Binding Parent.PropertyWithDirectAccess}"/>
</Grid>
</UserControl>
EDITAnother approach and more complex would be making the parent's DataContextavailable to the child UserControlusing an attached property. I have not fully implemented it, but it would consist in an attached property to request the feature (something like "HasAccessToParentDT"), in which DependencyPropertyChangedevent you would hook up the Load and Unloadevents of the ChildUserControl, access the Parentproperty (available if the control is loaded) and bind its DataContextto a second attached property, "ParentDataContext", which could then be used in xaml.
编辑另一种更复杂的方法是使用附加属性使父级对子级DataContext可用UserControl。我还没有完全实现,但它会在附加属性来请求该功能由(像"HasAccessToParentDT"),在这种DependencyPropertyChanged情况下,你会挂钩负载和Unload事件ChildUserControl,访问Parent(如果控制装载可用的)财产将其绑定DataContext到第二个附加属性 ,"ParentDataContext"然后可以在 xaml 中使用该属性。
<local:ChildControl BindingHelper.AccessParentDataContext="True"
MyProperty="{Binding BindingHelper.ParentDataContext.TargetProperty}" />
回答by eMko
The most obvious solution really is using the RelativeSource. The binding itself doesn't look very pretty, but it's actually very common to see. I wouldn't avoid it - this is exactly the scenario why it's there.
最明显的解决方案确实是使用RelativeSource。绑定本身看起来不是很漂亮,但实际上很常见。我不会避免它 - 这正是它存在的原因。
Another approach which you can use is a reference to a parent viewmodel, if it's logical to have it. Like I have a FlightPlan view, which shows a list of navigation point and its graphical "map" side by side. The list of points is a separate view with separate viewmodel:
您可以使用的另一种方法是对父视图模型的引用,如果它是合乎逻辑的。就像我有一个 FlightPlan 视图,它并排显示导航点列表及其图形“地图”。点列表是具有单独视图模型的单独视图:
public class PlanPointsPartViewModel : BindableBase
{
//[...]
private FlightPlanViewModel _parentFlightPlan;
public FlightPlanViewModel ParentFlightPlan
{
get { return _parentFlightPlan; }
set
{
SetProperty(ref _parentFlightPlan, value);
OnPropertyChanged(() => ParentFlightPlan);
}
}
//[...]
}
Then the view can bind to this property like this:
然后视图可以像这样绑定到这个属性:
<ListView ItemsSource="{Binding Path=ParentFlightPlan.Waypoints}"
AllowDrop="True"
DragEnter="ListViewDragEnter"
Drop="ListViewDrop"
>
[...]
</ListView>
However composing viewmodels like this is often quite questionable.
然而,像这样组合视图模型通常是有问题的。
回答by Johan
What about having a ParentDataContextProperty on the second level UserControl's ViewModel. Then create a dependencyproperty on that usercontrol with the same name and let it set the value to the VM's property in the xaml.cs file. Then the Parentcontrol can bind its DataContext to the child controls dependencyproperty to provide the child VM with access to its (the parent) datacontext. The childcontrol can bind to the parent's datacontext through its own ParentDataContextProperty VM property. (should probably be named just PContext or something short).
在二级 UserControl 的 ViewModel 上拥有 ParentDataContextProperty 怎么样。然后在该用户控件上创建一个具有相同名称的依赖项属性,并让它在 xaml.cs 文件中将值设置为 VM 的属性。然后,Parentcontrol 可以将其 DataContext 绑定到子控件的依赖项属性,以向子 VM 提供对其(父)数据上下文的访问权限。子控件可以通过其自己的 ParentDataContextProperty VM 属性绑定到父级的数据上下文。(可能应该仅命名为 PContext 或一些简短的名称)。
You could create a base class deriving from UserControl that has this DependencyProperty setup so you dont need to write it for every new control.
您可以创建一个派生自 UserControl 的基类,该基类具有此 DependencyProperty 设置,因此您无需为每个新控件编写它。

