使用MVVM和依赖属性的WPF用户控件困境

15

这是我想要完成的:

  • 我正在编写一个UserControl,希望其可以被其他开发人员使用。
  • 我希望最终用户可以使用依赖属性来使用我的控件。

<lib:ControlView ControlsText={Binding Path=UsersOwnViewModelText} />
我正在使用MVVM模式。 我使用将我的ViewModel绑定到它们的View。
<DataTemplate DataType="{x:Type local:ControlViewModel}">  
    <local:ControlView />  
</DataTemplate>

那么我有两个问题:

  1. 如果一个UserControl在XAML中被使用,我是否正确地认为,在控件的Loaded事件触发时,UserControl必须将ViewModel设置为它的DataContext,而不能使用<DataTemplate>方法?

  2. 我如何允许用户绑定到我的控件的依赖属性,同时仍然与我的ViewModel进行数据绑定?


在我准备这个问题的时候,我看到了它。谢谢你的提问 :) - Davut Gürbüz
4个回答

26

你应该将这两种用例分开:

  1. 将被其他开发人员使用的(用户)控件。
  2. 将被你的应用程序使用的用户控件。

重要的是,后者依赖于前者,而不是反过来。

用例1将使用依赖属性、模板绑定,以及制作常规 WPF 控件所需的所有内容:

MyControl.cs:

public class MyControl : Control
{
    // dependency properties and other logic
}

通用.xaml:

<ControlTemplate Type="local:MyControl">
    <!-- define the default look in here, using template bindings to bind to your d-props -->
</ControlTemplate>

那么你接下来需要定义第二个用例:

MyViewModel.cs:

public class MyViewModel : ViewModel
{
    // properties and business logic
}

MyView.xaml:

<UserControl ...>
    <local:MyControl SomeProperty="{Binding SomePropertyOnViewModel}" .../>
</UserControl>

通过干净的分离获得最佳双赢。其他开发人员只依赖于该控件,该控件可以(而且可能应该)与您的视图模型和视图完全不同的程序集中。


谢谢回复。我正在创建一个复合控件,类似级联组合框。以我的需求来看,这种方式创建控件似乎有点过于繁重。不过还是感谢您的回复,因为它仍然是有用的。 - Jon Mitchell
非常好的设计模式,谢谢。这样做没问题,但是如果您的组件没有经过设计的ViewModel,那么您需要进行管理。例如,在您的ViewModel中有CollectionViewSource,它们过滤或排序项目。如果没有ViewModel内部变量的移动,其他依赖属性将失去意义。因此,这个问题的答案取决于您的意图。 - Davut Gürbüz

10

首先,我认为如果您正在开发一个将被其他人使用的UserControl,则MVVM不是一个好选择。您真正应该开发的是一个无样式控件。Jeremiah Morrill在他的博客文章中谈到了这个主题。

话虽如此,如果您有一个默认的公共构造函数,可以使用XAML设置数据上下文。

在ControlView.xaml中放置:

<UserControl.DataContext>
    <local:ControlViewModel />
</UserControl.DataContext>

看起来MVVM并不是创建用户控件的理想选择。所以,克服了这个心理障碍,我能够解决我的问题。我需要将UserControl中子控件的DataContext设置为UserControl本身,这意味着使用控件可以为我的UserControl提供DataContext,因此可以绑定到我的DependencyProperties。(我认为是对的...)无论如何,它有效,我很高兴! - Jon Mitchell
3
在XAML中将UserControl的ViewModel分配给UserControl的DataContext将破坏父级尝试使用UserControl的DependencyProperties的绑定。具体来说,在OP中的此绑定<lib:ControlView ControlsText={Binding Path=UsersOwnViewModelText} />将导致绑定引用UserControl的ViewModel而不是父级的ViewModel。唯一的解决方法是父级需“某种方式”知道这一点并改用ElementName绑定。 - Neutrino
我认为MVVM模式适用于用户控件,如果它需要从数据库中获取项目列表并且必须在用户控件中的控件中反映出来。但是,如果您想直接向控件的使用者公开属性,则应在代码后台公开属性,可能会将某些子控件属性重新路由到与控件相关的数据类型中。 - steviesama
3
如前所述,无论如何在“UserControl”中设置“UserControl.DataContext”,都会覆盖从控件最终使用的位置继承的上下文,这会导致绑定问题。如果需要ViewModel,则我通常将DC设置在最外层容器(通常是“Grid”)中的控件内部。这样可以继承控件本身的“DataContext”(用户尝试绑定其DP时不会出现意外情况),并允许在控件内部使用ViewModel进行正常操作。 - Bob Sammers

2

基本上,与其将UserControl的数据上下文绑定到userControlViewModel,最好将其绑定到用户控件的第一个子元素。这样,在控件中进行的所有引用都将绑定到userControlViewModel,但可以从您想使用UserControl的数据上下文设置中设置依赖属性。

这是我正在处理的一个项目中的内容:

<UserControl x:Class="Six_Barca_Main_Interface.MyUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:Six_Barca_Main_Interface"
             xmlns:System="clr-namespace:System;assembly=mscorlib" 
             mc:Ignorable="d" 
             d:DesignHeight="900" d:DesignWidth="900">

    <DockPanel  x:Name="rootDock" >
        <TextBlock>{Binding SomethingInMyUserControlViewModel}</TabControl>
    </DockPanel>
</UserControl>

然后在代码后台:

public partial class MyUserControl : UserControl
{
    UserControlViewModel _vm;

    public MyUserControl()
    {
        InitializeComponent();

        //internal viewModel set to the first child of MyUserControl
         rootDock.DataContext = new UserControlViewModel();

        _vm = (UserControlViewModel)rootDock.DataContext;


        //sets control to be able to use the viewmodel elements

     }

     #region Dependency properties 
     public string textSetFromApplication
     {
         get{return (string)GetValue(textSetFromApplicationProperty);}
         set{SetValue(textSetFromApplicationProperty, value);}
     }

     public static readonly DependencyProperty textSetFromApplicationProperty = DependencyProperty.Register("textSetFromApplication", typeof(string), typeof(MyUserControl), new PropertyMetadata(null, OnDependencyPropertyChanged));

     private static void  OnDependencyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
     {
        ((MyUserControl)d)._vm.SomethingInMyUserControlViewModel = 
             e.NewValue as string;
     }
#endregion

然后当您在主视图上使用此控件时,您可以使用所需传递给MyUserControl的值来设置依赖属性。


1
一个 UserControl 是 "MVVM" 中的 "View" 的一部分,就像 TextBoxListView 控件是 View 的一部分。
无论您决定使用 MVVM 来开发您的 UserControl,还是用 QBASIC 编写它(不建议),只要消费者可以通过绑定到 UserControl 上公开的 DependencyProperty 来完成所有需要的操作,就不会破坏 MVVM 模式。即您的 UserControl 应该公开其所依赖的属性(因此得名)。一旦您掌握了这个概念,DependencyProperty 就会突然变得非常有意义,并且您会想要它们的有用的 on changed 事件处理程序和在构造函数中指定的默认值。
如果您的 UserControl 在不同的程序集中或者不在,我看不出这有什么区别。
我认为许多人会建议您使用MVVM模式来构建UserControl,因为MVVM模式可以带来很多好处,比如帮助其他开发人员查看您的代码。然而,有些事情是不可能的,或者通过XAML进行更加困难、复杂和低效的操作,我指的不是常规的添加用户表单,而是例如处理成千上万个可视化元素布局的UserControl。此外,由于您正在处理View,因此您不希望将UserControl的ViewModel与应用程序混合在一起!基本上,我想说的是,在View中不使用MVVM完全符合MVVM的要求!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接