我被迫在WPF应用程序中使用View First MVVM,我很难看到它如何才能优雅地工作。
问题的根源在于嵌套的UserControls
。在MVVM体系结构中,每个UserControl
需要将其视图模型分配给其DataContext
,这样可以使绑定表达式保持简单,而且这也是WPF实例化通过{{{{1}生成的任何视图的方式。 1}}。
但是如果子DataTemplate
具有父项需要绑定到其自己的viewmodel的依赖项属性,那么子UserControl
将其UserControl
设置为其自己的viewmodel的事实意味着父XAML文件中的“隐式路径”绑定将解析为子视图模型而不是父视图。
要解决此问题,应用程序中每个DataContext
的每个父项都需要默认使用显式命名绑定(这是详细的,丑陋的和错误的),或者它必须知道是否具体control将UserControl
设置为自己的viewmodel,并使用适当的绑定语法(同样是错误的,并且是对基本封装的重大违反)。
经过几天的研究,我没有遇到过这个问题的单一解决方案。与我遇到的解决方案最接近的是将DataContext
viewmodel设置为UserControl's
(最顶层的UserControl
或其他)的内部元素,这仍然会让您遇到问题尝试将Grid
本身的属性绑定到自己的viewmodel! (UserControl
绑定在这种情况下不起作用,因为绑定将在指定元素之前声明,并且viewmodel已分配给其ElementName
)。
我怀疑没有其他人遇到这个问题的原因是他们要么使用viewmodel第一个没有这个问题的MVVM,要么他们正在使用视图第一个MVVM和一个依赖注入实现来解决这个问题
有人为此准备了一个干净的解决方案吗?
更新:
请求的示例代码。
DataContext
<!-- 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); }
}
}
}
这导致:
System.Windows.Data错误:40:BindingExpression路径错误: 在'object'''UserControl6Vm'上找不到'MainWindowVmString1'属性 (的HashCode = 44204140)”。 BindingExpression:路径= MainWindowVmString1; DataItem ='UserControl6Vm'(HashCode = 44204140);目标元素是 'UserControl6'(Name ='_ this'); target属性是'Text'(类型 '字符串')
因为namespace UiInteraction
{
// UserControl's viewmodel.
class UserControl6Vm
{
public string UserControlVmString1
{
get { return "UserControl6Vm.String1"; }
}
}
}
声明MainWindow.xaml
正试图解决<local:UserControl6 Text="{Binding MainWindowVmString1}"/>
上的MainWindowVmString1
。
在UserControl6Vm
评论UserControl6.xaml
和第一个DataContext
的声明时,代码将有效,但TextBlock
需要UserControl
。在DataContext
中使用MainWIndow1
而不是隐含路径绑定也可以,但是为了使用ElementName
绑定语法,您必须知道ElementName
分配它的viewmodel到它的UserControl
(封装失败)或者只是采用在任何地方使用DataContext
绑定 的策略。这两者都没什么吸引力。
答案 0 :(得分:0)
立即解决方案是使用RelativeSource
并将其设置为查找父DataContext
的{{1}}:
UserControl
您还可以将子视图模型视为父视图模型的属性,并从父视图模型传播它。这样,父视图模型就会知道子视图,因此它可以更新它们的属性。子视图模型也可能具有<UserControl>
<UserControl.DataContext>
<local:ParentViewModel />
</UserControl.DataContext>
<Grid>
<local:ChildControl MyProperty="{Binding DataContext.PropertyInParentDataContext, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"/>
</Grid>
</UserControl>
属性,该属性保存对父项的引用,父项在创建时由父项注入,可以直接访问父项。
"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>
可供子DataContext
使用。我没有完全实现它,但它将包含一个附加属性来请求该功能(类似于UserControl
),其中"HasAccessToParentDT"
事件将连接Load和DependencyPropertyChanged
事件在Unload
中,访问ChildUserControl
属性(如果加载了控件,则可用)并将其Parent
绑定到第二个附加属性DataContext
,然后可以将其用于XAML。
"ParentDataContext"
答案 1 :(得分:0)
最明显的解决方案是使用RelativeSource。绑定本身看起来不是很漂亮,但实际上很常见。我不会避免它 - 这正是它在那里的情况。
您可以使用的另一种方法是对父视图模型的引用,如果它具有逻辑性。就像我有一个FlightPlan视图,它显示了一个导航点列表及其图形&#34; map&#34;并排。点列表是一个单独的视图,具有单独的视图模型:
for n in x:
if x[n] == 'Red':
p = 0
if x[n] == 'Green':
p = 1
y.append(p);
然后视图可以像这样绑定到这个属性:
public class PlanPointsPartViewModel : BindableBase
{
//[...]
private FlightPlanViewModel _parentFlightPlan;
public FlightPlanViewModel ParentFlightPlan
{
get { return _parentFlightPlan; }
set
{
SetProperty(ref _parentFlightPlan, value);
OnPropertyChanged(() => ParentFlightPlan);
}
}
//[...]
}
然而,编写像这样的视图模型通常是值得怀疑的。
答案 2 :(得分:0)
在第二级UserControl的ViewModel上具有ParentDataContextProperty怎么样。然后在具有相同名称的该用户控件上创建依赖项属性,然后将其值设置为xaml.cs文件中VM的属性。然后,Parentcontrol可以将其DataContext绑定到子控件的依赖项属性,以向子VM提供对其(父)数据上下文的访问权限。子控件可以通过其自己的ParentDataContextProperty VM属性绑定到父级的数据上下文。 (应该命名为PContext或简称)。
您可以创建一个派生自UserControl的基类,该基类具有DependencyProperty设置,因此您无需为每个新控件编写它。