8

我被迫在 WPF 应用程序中使用 View First MVVM,我正在努力了解如何让它优雅地工作。

问题的根源在于嵌套UserControls。在 MVVM 架构中,每个都UserControl需要将其视图模型分配给它DataContextDataTemplate.

但是,如果子项UserControl具有父项需要绑定到其自己的视图模型的依赖属性,那么子项UserControl将其DataContext设置为自己的视图模型这一事实意味着父 XAML 文件中的“隐式路径”绑定将改为解析为子项的视图模型父母的。

为了解决这个问题,应用程序中的每个父级都UserControl需要默认使用显式命名绑定(这很冗长、丑陋且容易出错),或者它必须知道特定控件是否已将其DataContext设置为自己的视图模型或不使用并使用适当的绑定语法(这同样容易出错,并且严重违反了基本封装)。

经过几天的研究,我还没有找到一个半体面的解决方案来解决这个问题。我遇到的最接近解决方案的是将视图模型设置为(最上面的或其他)UserControl's的内部元素,这仍然会让您在尝试将自身的属性绑定到自己的视图模型时遇到问题!(在这种情况下绑定将不起作用,因为绑定将在命名元素之前声明,并将视图模型分配给它)。UserControlGridUserControlElementNameDataContext

我怀疑没有多少其他人遇到此问题的原因是他们要么使用没有此问题的 viewmodel first MVVM,要么将 view first MVVM 与改善此问题的依赖注入实现结合使用。

请问有人有干净的解决方案吗?

更新:

根据要求提供示例代码。

<!-- 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"; }
        }
    }
}

这导致:

System.Windows.Data 错误:40:BindingExpression 路径错误:在“对象”“UserControl6Vm”(HashCode=44204140)上找不到“MainWindowVmString1”属性。绑定表达式:路径=MainWindowVmString1;DataItem='UserControl6Vm' (HashCode=44204140); 目标元素是'UserControl6'(名称='_this');目标属性是“文本”(类型“字符串”)

因为在MainWindow.xaml声明中<local:UserControl6 Text="{Binding MainWindowVmString1}"/>试图解决MainWindowVmString1.UserControl6Vm

UserControl6.xaml注释掉 theDataContext和 first的声明时TextBlock,代码将起作用,但UserControl需要DataContext. MainWIndow1使用 an而ElementName不是隐式路径绑定也可以,但是为了使用ElementName绑定语法,您要么必须知道UserControl将其视图模型分配给它DataContext(封装失败),要么采用在任何地方ElementName使用绑定的策略。两者都没有吸引力。

4

3 回答 3

2

一个直接的解决方案是使用 aRelativeSource并将其设置为查找DataContextparent 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"),在这种DependencyPropertyChanged情况下,您将连接 Load 和Unload的事件ChildUserControl,访问该Parent属性(如果控件已加载,则可用)和将其绑定DataContext到第二个附加属性 ,"ParentDataContext"然后可以在 xaml 中使用。

        <local:ChildControl BindingHelper.AccessParentDataContext="True"
            MyProperty="{Binding BindingHelper.ParentDataContext.TargetProperty}"   />
于 2013-01-21T17:01:19.753 回答
0

在二级用户控件的 ViewModel 上有一个 ParentDataContextProperty 怎么样。然后在该用户控件上创建一个具有相同名称的依赖属性,并让它在 xaml.cs 文件中将值设置为 VM 的属性。然后,父控件可以将其 DataContext 绑定到子控件的依赖属性,以向子 VM 提供对其(父)数据上下文的访问权限。子控件可以通过其自己的 ParentDataContextProperty VM 属性绑定到父级的数据上下文。(可能应该只命名为 PContext 或简称)。

您可以创建一个派生自具有此 DependencyProperty 设置的 UserControl 的基类,因此您不需要为每个新控件编写它。

于 2019-04-27T19:33:50.747 回答
0

最明显的解决方案确实是使用RelativeSource。绑定本身看起来不是很漂亮,但实际上很常见。我不会避免它——这正是它存在的原因。

您可以使用的另一种方法是对父视图模型的引用,如果它是合乎逻辑的。就像我有一个 FlightPlan 视图,它并排显示导航点列表及其图形“地图”。点列表是具有单独视图模型的单独视图:

public class PlanPointsPartViewModel : BindableBase
{

    //[...]

    private FlightPlanViewModel _parentFlightPlan;
    public FlightPlanViewModel ParentFlightPlan
    {
        get { return _parentFlightPlan; }
        set
        {
            SetProperty(ref _parentFlightPlan, value);
            OnPropertyChanged(() => ParentFlightPlan);
        }
    } 

    //[...]

}

然后视图可以像这样绑定到这个属性:

<ListView ItemsSource="{Binding Path=ParentFlightPlan.Waypoints}"
          AllowDrop="True"
          DragEnter="ListViewDragEnter"
          Drop="ListViewDrop"
          >
    [...]
</ListView>

然而,像这样组合视图模型通常是非常值得怀疑的。

于 2015-11-01T15:18:41.740 回答