View-First-MVVM中的UserControls和viewmodels

问题描述 投票:7回答:3

我被迫在WPF应用程序中使用View First MVVM,我很难看到如何使它优雅地工作。

问题的根源在于嵌套的UserControls。在MVVM架构中,每个UserControl都需要将其视图模型分配给它的DataContext,这使得绑定表达式保持简单,而且这也是WPF实例化通过DataTemplate生成的任何视图的方式。

但是如果一个子UserControl具有父类需要绑定到其自己的viewmodel的依赖属性,那么子UserControl将其DataContext设置为其自己的viewmodel这一事实意味着父XAML文件中的“隐式路径”绑定将解析为child的viewmodel而不是parent的。

要解决这个问题,应用程序中每个UserControl的每个父项都需要默认使用显式命名绑定(这是详细的,丑陋的和错误的),或者必须知道特定控件是否将其DataContext设置为是否拥有自己的viewmodel并使用适当的绑定语法(同样是errorprone,并且是对基本封装的重大违反)。

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

我怀疑没有其他人遇到这个问题的原因是他们要么使用viewmodel第一个没有这个问题的MVVM,要么他们正在使用视图第一个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'属性。 BindingExpression:路径= MainWindowVmString1; DataItem ='UserControl6Vm'(HashCode = 44204140); target元素是'UserControl6'(Name ='_ this'); target属性是'Text'(类型'String')

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

UserControl6.xaml评论DataContext和第一个TextBlock的声明代码将起作用,但UserControl需要一个DataContext。在MainWIndow1中使用ElementName而不是implict路径绑定也会起作用,但是为了使用ElementName绑定语法,你要么必须知道UserControl将其viewmodel分配给它的DataContext(封装失败)或者只是采用使用策略ElementName到处绑定。这两者都没有吸引力。

wpf xaml mvvm binding
3个回答
0
投票

一个直接的解决方案是使用RelativeSource并将其设置为寻找父母DataContextUserControl

<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事件你将连接Unload的Load和ChildUserControl事件,访问Parent属性(如果控制被加载)并将其DataContext绑定到第二个附加属性"ParentDataContext",然后可以在xaml中使用。

        <local:ChildControl BindingHelper.AccessParentDataContext="True"
            MyProperty="{Binding BindingHelper.ParentDataContext.TargetProperty}"   />

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>

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


0
投票

如何在二级UserControl的ViewModel上拥有ParentDataContextProperty。然后在该usercontrol上创建一个具有相同名称的dependencyproperty,并将其设置为xaml.cs文件中VM的属性。然后,Parentcontrol可以将其DataContext绑定到子控件dependencyproperty,以便为子VM提供对其(父)datacontext的访问。 childcontrol可以通过其自己的ParentDataContextProperty VM属性绑定到父级的datacontext。 (应该简单地命名为PContext或简称)。

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

© www.soinside.com 2019 - 2024. All rights reserved.