WPF - 用户控件是否应该拥有自己的ViewModel?

Awk*_*der 27 wpf model-view-controller viewmodel

我有一个由几个用户控件组成的窗口,并且想知道每个用户控件是否有自己的ViewModel,或者整个窗口是否只有一个ViewModel?

小智 40

绝对,积极

没有

你的用户控件应该具有专为他们设计的ViewModels.事实上,这是一种代码味道.它不会立即破坏您的应用程序,但在您使用它时会让您感到痛苦.

UserControl只是使用合成创建Control的简单方法.UserControls仍然是控件,因此应该只关注UI的问题.

为UserControl创建ViewModel时,您要么在其中放置业务逻辑或UI逻辑.使用ViewModels包含UI逻辑是不正确的,因此如果这是您的目标,请丢弃您的VM并将代码放在该控件的代码隐藏中.如果您将业务逻辑放在UserControl中,很可能您正在使用它来隔离应用程序的各个部分,而不是简化控件创建.控件应该简单,并且具有设计它们的单一目的.

为UserControl创建ViewModel时,还可以通过DataContext中断自然的数据流.这是您将经历最痛苦的地方.为了演示,请考虑这个简单的例子.

我们有一个包含People的ViewModel,每个都是Person类型的一个实例.

public class ViewModel
{
    public IEnumerable<Person> People { get; private set; }
    public ViewModel()
    {
        People = PeopleService.StaticDependenciesSuckToo.GetPeople();
    }
}

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

在我们的窗口中显示人员列表是微不足道的.

<Window x:Class="YoureDoingItWrong.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:YoureDoingItWrong"
        Title="Derp">
    <Window.DataContext>
        <l:ViewModel />
    </Window.DataContext>
    <Window.Resources>
        <DataTemplate DataType="{x:Type l:Person}">
            <l:PersonView />
        </DataTemplate>
    </Window.Resources>
    <ListView ItemsSource="{Binding People}" />
</Window>
Run Code Online (Sandbox Code Playgroud)

该列表自动为Person选择正确的项目模板,并使用PersonView向用户显示该人的信息.

什么是PersonView?它是一个UserControl,旨在显示此人的信息.它是一个人的显示控件,类似于TextBlock是文本的显示控件.它旨在绑定一个人,因此工作顺利.请注意上面的窗口中ListView如何将每个Person实例传输到PersonView,在PersonView中它成为此视觉子树的DataContext.

<UserControl x:Class="YoureDoingItWrong.PersonView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <StackPanel>
        <Label>Name</Label>
        <TextBlock Text="{Binding Name}" />
        <Label>Age</Label>
        <TextBlock Text="{Binding Age}" />
    </StackPanel>
</UserControl>
Run Code Online (Sandbox Code Playgroud)

为了使其顺利运行,UserControl的ViewModel 必须是它所针对的Type的实例.当你通过做愚蠢的事情来打破这个

public PersonView()
{
    InitializeComponent();
    this.DataContext = this; // omfg
}
Run Code Online (Sandbox Code Playgroud)

要么

public PersonView()
{
    InitializeComponent();
    this.DataContext = new PersonViewViewModel();
}
Run Code Online (Sandbox Code Playgroud)

你已经破坏了模型的简洁性.通常在这些情况下,您最终会得到令人憎恶的变通方法,其中最常见的是为DataContext 实际应用的内容创建伪DataContext属性.现在你不能将一个绑定到另一个,所以你最终会遇到糟糕的黑客

public partial class PersonView : UserControl
{        
    public PersonView()
    {
        InitializeComponent();
        var vm = PersonViewViewModel();
        // JUST KILL ME NOW, GET IT OVER WITH 
        vm.PropertyChanged = (o, e) =>
        {
            if(e.Name == "Age" && MyRealDataContext != null)
                MyRealDataContext.Age = vm.PersonAge;
        };
        this.DataContext = vm; 
    }
    public static readonly DependencyProperty MyRealDataContextProperty =
        DependencyProperty.Register(
            "MyRealDataContext",
            typeof(Person),
            typeof(PersonView),
            new UIPropertyMetadata());
    public Person MyRealDataContext
    {
        get { return (Person)GetValue(MyRealDataContextProperty); }
        set { SetValue(MyRealDataContextProperty, value); }
    }
}
Run Code Online (Sandbox Code Playgroud)

您应该将UserControl视为一个更复杂的控件.TextBox是否有自己的ViewModel?不可以.将VM的属性绑定到控件的Text属性,控件在其UI中显示文本.

MVVM不代表"无代码隐藏".将用户控件的UI逻辑放在代码隐藏中.如果它太复杂以至于您需要在用户控件中使用业务逻辑,那么这表明它太过笼统.简化!

想想MVVM中的UserControls - 对于每个模型,您都有一个UserControl,它旨在将该模型中的数据呈现给用户.您可以在任何想要向用户显示该模型的地方使用它.它需要一个按钮吗?在UserControl上公开ICommand属性,并让您的业务逻辑绑定到它.您的业​​务逻辑是否需要知道内部发生的事情?添加路由事件.

通常情况下,在WPF中,如果你发现自己问为什么做某事很痛,那是因为你不应该这样做.

  • 这个答案对我来说很难理解.首先你写_"UserControls不应该有专为他们设计的ViewModels"_.然后你写_"UserControl的ViewModel必须是它为_"设计的Type的实例.首先,如何排除在窗口中使用多个视图模型,例如绑定到不同的`UserControl(作为OP的_actual_问题)或甚至其他类型的控件?其次,因为必须知道它所绑定的对象的`UserControl`,如何避免"正好设计"? (5认同)
  • 不确定测试UI逻辑应该被认为是一个疯狂的角落案例.Afterall,不是MVVM的原则之一,从视图中删除UI逻辑并将其放入ViewModel,以便您可以测试它 - *"将UI逻辑移动到可以独立于UI技术实例化的单独类使单元测试更容易"*([MSDN](https://msdn.microsoft.com/en-us/library/hh848246.aspx)).您的回答指出"使用ViewModels包含UI逻辑不正确" - 这不是MSDN鼓励我们做的事情.我认为测试你的虚拟机远非有用. (5认同)
  • @Will,我同意你的观点,因为我经历过你所说的痛苦.但我仍然不知道这是否是正确的答案 - 你说:"如果你在UserControl中放置业务逻辑,很可能你正在使用它来隔离应用程序的各个部分,而不是简化控件的创建." 您对此案例的解决方案有何建议?例如,想象一套应用程序,它们共享一个共同的控件和一些用于该控件的通用业务逻辑.消费应用程序是否真的必须将这两个东西连接在一起? (4认同)
  • 不确定我同意.如果您的用户控件只显示数据,那么在后面的代码中完成所有操作.但是,如果您有一些更复杂的UI逻辑,控件可以影响复合控件中的其他控件,为什么不将该逻辑放在可以轻松进行单元测试的POCO(ViewModel)中.是的,您的View与ViewModel相关联,但ViewModel是专门为该View设计的 - 它们作为一对存在.设置这样并不困难,您可以访问依赖项属性和查看模型属性. (3认同)
  • “通常,在 WPF 中,如果您发现自己问为什么做某事会让人痛苦,那是因为您不应该这样做。” 很好的提醒!也感谢您的深入解释!帮助了很多 (3认同)
  • @peterduniho 1)我有一个MainWindowViewModel,它在Persons属性中包含Person模型。我的PersonUserControl应该设计为在Person模型中显示信息。它不应该具有PersonUserControlViewModel,后者会以某种方式获取一个Person实例,并将其融合到与PersonUserControl中的元素绑定在一起的PUCVM的属性中。看看第二条路径如何成为PITA?设计UserControl时应假定其DataContext是Person。 (2认同)
  • 100%同意MVVM!=没有任何代码(我从来没有说过这样做吗???)。对于仅显示数据的简单控件,实现ViewModel几乎不会带来任何好处。但是,恕我直言,如果您将更复杂的控件组合在一起,那么您可能应该对其进行测试,并且需要使用ViewModel来完成。每个都有自己的特点,但是我要说的是,我们发现将VM与某些复合控件一起使用非常有益。这仅取决于您的用例以及您所追求的测试范围。 (2认同)

Ken*_*art 15

这不是一个是或否的问题.这取决于是否有额外的视图模型为您提供更好的可维护性或可测试性.添加视图模型是没有意义的,如果它没有获得任何东西.您需要根据特定用例来衡量开销是否值得.

  • 你的答案与@Will的答案形成鲜明对比.他似乎更有意义:用户控制仍然只是控制.我很好奇你自从09年写这个答案以来你是否改变了自己的观点. (3认同)

Pet*_*iho 11

[应该] 每个用户控件都有自己的 ViewModel 还是整个窗口应该只有一个 ViewModel?

不幸的是,这个问题的最高投票答案具有误导性,并且基于我在其他问题中交换的评论,为尝试学习 WPF 的人提供了糟糕的指导。那个回答回复:

你的用户控件应该具有专为他们设计的ViewModels。

问题是,这不是被问到的问题

我同意一般观点,即当您编写一个 时UserControl,控件的公共 API 不应该还涉及创建专门用于该控件的视图模型类型。客户端代码必须能够使用它想要的任何视图模型。

但是,这并不排除“每个用户控件 [可能] 拥有自己的 ViewMomdel”的想法。我能想到的至少有两个明显的场景,其中的答案是“是的,每个用户控件都有一个视图模型”:

  1. 用户控件是项目展示器(例如ItemsControl)中数据模板的一部分。在这种情况下,视图模型将对应于每个单独的数据元素,并且视图模型对象和呈现该视图模型对象的用户控件之间将存在一对一的对应关系。

    在这种情况下,视图模型对象不是“专门为他们设计的”(因此与有问题的答案没有矛盾),但肯定是每个用户控件都有自己的视图模型(对实际问题做出回答)是的,每个用户控件都可能有自己的视图模型”)。

  2. 用户控件的实现受益于甚至需要专门为用户控件设计的视图模型数据结构。这个视图模型数据结构不会暴露给客户端代码;这是一个实现细节,因此将使用用户控件从客户端代码中隐藏。但是,这肯定仍然是“专为”该用户控件设计的视图模型数据结构。

    这种情形显然不是在所有的,这直接违背了声称有问题“你的用户控件应该具有专为他们设计的ViewModels。”

现在,我不相信该答案的作者的意图是排除这两种情况中的任何一种。但问题是,试图学习 WPF 的人可能没有足够的上下文来识别差异,因此可能会根据这个强调、高度赞成和误导性的答案错误地概括用户控件和视图模型。

我希望通过提出这个替代观点作为澄清点,并以不那么狭隘的方式回答原始问题,那些在更多地了解 WPF 的同时发现这个问题的人将拥有更好的背景、更好的想法和何时可以实现特定于用户控件的视图模型以及何时不应该实现。

  • @ÉtienneLaneville:_“如何将其设置为用户控件的 DataContext?”_ - 你不知道。您不应修改顶级“UserControl.DataContext”属性,因为这是客户端代码可以看到并且可能想要修改的属性。相反,您可能希望设置“UserControl”内部某些元素的“DataContext”属性。“UserControl”必须有一个顶级元素作为其内容(例如“StackPanel”、“Grid”等),并且这将是设置“DataContext”的良好元素。或者,其中的一个或多个子元素。 (2认同)

Mar*_*ann 10

我会说每个用户控件都应该有自己的ViewModel,因为这将允许您将来在新的星座中重用ViewModel/UserControl对.

据我所知,您的窗口是用户控件的复合,因此您始终可以创建一个ViewModel,它为每个用户控件组成所有单独的ViewModel.这将为您提供两全其美的体验.

  • 威尔似乎非常坚定,但我在马克方面犯了错误.对我来说,没有理由为什么控件不能有自己的视图模型:你只需要意识到它们作为耦合对存在.我们将视图绑定到视图模型,并务实地说视图与其视图模型之间存在一对一的关系(视图模型和模型一对多).对于只显示数据的简单视图,请在后面的代码中执行.如果涉及逻辑,为什么不在视图模型中进行,这样您就可以进行所有重要的单元测试. (5认同)
  • 您的回答与@Will 的回答形成鲜明对比。他的似乎更有意义:用户控件仍然只是本质上的控件。我很好奇自 09 年您写下这个答案以来,您是否改变了看法。 (2认同)