使用 MVVM 模式的 UI 设计

Posted

技术标签:

【中文标题】使用 MVVM 模式的 UI 设计【英文标题】:UI design using MVVM pattern 【发布时间】:2011-07-05 05:55:24 【问题描述】:

我正在尝试选择以 MVVM 方式实现此 UI 的最佳方式。我是 WPF 新手(比如 2 个月),但我有丰富的 WinForms 经验。

这里的ListBox 就像一个TabControl(因此它将视图切换到右侧),并且基本上包含表中显示的项目的类型。所有 UI 都是动态的(ListBox 项、TabItems 和 Columns 在运行时确定)。该应用程序的目标是 WPF 和 Silverlight。

ViewModel 需要的类:

public abstract class ViewModel : INotifyPropertyChanged 
public abstract class ContainerViewModel : ViewModel

    public IList<ViewModel> Workspaces get;set;
    public ViewModel ActiveWorkspace get;set;

public class ListViewModel<TItem> where TItem : class

    public IList<TItem> ItemList  get; set; 
    public TItem ActiveItem  get; set; 
    public IList<TItem> SelectedItems  get; set; 

public class TableViewModel<TItem> : ListViewModel<TItem> where TItem : class

    public Ilist<ColumnDescription> ColumnList  get; set; 

现在的问题是如何将其连接到 View。

我可以在这里看到两种基本方法:

使用 XAML:由于 XAML 中缺乏泛型支持,我将失去强类型。 没有 XAML:我可以重复使用相同的 ListView&lt;T&gt; : UserControl.

接下来,如何连接数据,我在这里看到了 3 种方法(这里有没有 XAML 都无所谓)。由于没有简单的 DataBinding 到 DataGrid 的列或 TabControl 的 TabItems 我看到的方法是:

将 DataBinding 与 IValueConverter 一起使用:我认为这不适用于 WPF|Silverlight 开箱即用的控件,因为我需要的某些属性是只读的或无法以双工方式绑定。 (对此我不确定,但我觉得它行不通)。

通过在视图中订阅 INotifyPropertyChanged 来使用手动逻辑:ViewModel.PropertyChanged+= ....ViewModel.ColumnList.CollectionChanged+= ....

使用支持此绑定的自定义控件:自己编写代码或查找支持此绑定的 3d 方控件(我不喜欢此选项,我的 WPF 技能太低,无法自己编写代码,我怀疑我自己会找到免费控件)


更新:28.02.2011 事情越来越糟,我决定用 TreeView 代替 ListBox,简直是一场噩梦。正如您可能猜到的那样,TreeView.SelectedItems 是一个只读属性,因此没有数据绑定。嗯,好吧,让我们用旧方法来订阅事件以将视图与视图模型同步。此时突然发现 DisplayMemberPath 对 TreeView 没有任何作用(嗯,好吧,让我们使用旧方式 ToString())。然后在 View 的方法中,我尝试将 ViewModel.SelectedItem 与 TreeView 同步:

private void UpdateTreeViewSelectedItem()

    //uiCategorySelector.SelectedItem = ReadOnly....

    //((TreeViewItem) uiCategorySelector.Items[uiCategorySelector.Items.IndexOf(Model.ActiveCategory)]).IsSelected = true;
    // Will not work Items's are not TreeViewItem but Category object......

    //((TreeViewItem) uiCategorySelector.ItemContainerGenerator.ContainerFromItem(Model.ActiveCategory)).IsSelected = true;
    //Doesn't work too.... NULL // Changind DataContext=Model and Model = new MainViewModel line order doesn't matter.
    //Allright.. figure this out later...

而且我能想到的方法都没有奏效......

这里是我演示使用 MVVM 控制库地狱的示例项目的链接:http://cid-b73623db14413608.office.live.com/self.aspx/.Public/MVVMDemo.zip

【问题讨论】:

我在 Silverlight 论坛上的类似主题:forums.silverlight.net/forums/64.aspx 【参考方案1】:

我写了一篇文章和一个带有可用源代码的示例应用程序,在那里我讨论并展示了我在这里提到的问题以及如何解决这些问题。

http://alexburtsev.wordpress.com/2011/03/05/mvvm-pattern-in-silverlight-and-wpf/

【讨论】:

【参考方案2】:

Maciek 和 Robert 已经为您提供了一些关于如何实现此功能的想法。

对于绑定 DataGrid 的列的细节,我强烈建议 Meleak's answer 解决该问题。

与此类似,您可以使用 attached properties(或 Behaviors)并在 MVVM 中仍然保持干净的 ViewModel。

我知道 WPF 的学习曲线非常陡峭,而您已经在苦苦挣扎。我也知道以下建议无济于事,甚至使曲线更陡峭。但是你的场景很复杂,我建议使用PRISM。

【讨论】:

感谢您的回复,我认为附加属性会完成这项工作,我并不是要维护干净的 View 或 ViewModel,我只是想完成这项工作。我不确定 PRISM(从未见过,但我知道它是 MVVM 框架),因为问题在于 WPF 的控件(如 DataGrid)设计不良,解决方案是使用\编写另一个表控件,或在一个中使用事件表单或其他(如附加属性) @Alex 不得不依赖行为形式的事件并不是一件坏事,因为行为是某种密封形式的代码,它本身很容易testable。当然,如果您只关心完成工作(您或其他任何人都不必维护该代码),您可以省去这个,因为它确实涉及一些开销。 +1 推荐使用 PRISM 来管理总体布局——“区域”管理让生活更轻松。我们同样需要动态绑定列,所以我们创建了一个自定义控件。这是相当多的工作,但如果它结束,您将了解更多关于 WPF 的信息 :)。如果时间紧迫,您可以打破 MVVM 模式以在代码中添加列,然后再找到更简洁的解决方案。【参考方案3】:

Maciek 的答案实际上比它需要的还要复杂。您根本不需要模板选择器。创建异构选项卡控件:

    为您希望显示为选项卡项的每种视图类型创建一个视图模型类。确保每个类都实现了一个 Text 属性,该属性包含您希望在其项目的选项卡中显示的文本。

    为每个视图模型类创建一个DataTemplate,将DataType设置为类的类型,并将模板放入资源字典中。

    使用视图模型的实例填充集合。

    创建一个TabControl 并将其ItemsSource 绑定到此集合,并添加一个ItemTemplate 以显示每个项目的Text 属性。

这是一个不使用视图模型的示例(也没有实现 Text 属性,因为我绑定到的对象是简单的 CLR 类型),但显示了模板选择在此上下文中的工作方式:

<Page
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:sys="clr-namespace:System;assembly=mscorlib"
  xmlns:coll="clr-namespace:System.Collections;assembly=mscorlib">
  <DockPanel>  
   <DockPanel.Resources>
        <coll:ArrayList x:Key="Data">
          <sys:String>This is a string.</sys:String>
          <sys:Int32>12345</sys:Int32>
          <sys:Decimal>23456.78</sys:Decimal>
        </coll:ArrayList>
        <DataTemplate DataType="x:Type sys:String">
          <TextBlock Text="Binding"/>
        </DataTemplate>
        <DataTemplate DataType="x:Type sys:Int32">
          <StackPanel Orientation="Horizontal">
           <TextBlock>This is an Int32:</TextBlock>
           <TextBlock Text="Binding"/>
          </StackPanel>
        </DataTemplate>
        <DataTemplate DataType="x:Type sys:Decimal">
          <StackPanel Orientation="Horizontal">
           <TextBlock>This is a Decimal: </TextBlock>
           <TextBlock Text="Binding"/>
          </StackPanel>
        </DataTemplate>
   </DockPanel.Resources>
    <TabControl ItemsSource="StaticResource Data">  
      <TabControl.ItemTemplate>
       <DataTemplate>
        <TextBlock Text="Binding"/>
       </DataTemplate>
      </TabControl.ItemTemplate>
    </TabControl>
  </DockPanel>
</Page>

当然,在真正的 MVVM 应用程序中,DataTemplates 会使用 UserControls 将每种类型映射到其视图:

<DataTemplate DataType="x:Type my:ViewModel">
   <my:View DataContext="Binding"/>
</DataTemplate>

【讨论】:

感谢您对TabControl的回复,但我更感兴趣的是:绑定DataGrid的列、排序列、DataGrid的选定行。 这行不通的是 Silverlight(我同时针对 WPF 和 Silverlight),Maciek 是对的,您需要使用 IValueConverter 或从 TabItem 派生。 啊,我没有看到 Silverlight 的必要性。这确实改变了一些事情。更好的模板选择确实会提高 SL。【参考方案4】:

为了将您的 ViewModel 连接到您的 View,您需要分配 View 的 DataContext。这通常在视图的构造函数中完成。

public View()

    DataContext = new ViewModel();

如果您想在设计时查看视图模型的效果,您需要在 XAML 中声明它,在视图的资源中,为其分配一个键,然后通过 StaticResource 设置目标的 DataContext。

<UserControl
xmlns:vm="clr-namespace:MyViewModels
>
    <UserControl.Resources>
         <vm:MyViewModel x:Key="MyVM"/>
    </UserControl.Resources>
    <MyControl DataContext=StaticResource MyVM/>
</UserControl>

(以上是为了演示设计时技巧的工作原理)

由于您正在处理包含容器(例如 TabControl)的场景,我建议您考虑以下事项:

将您的子 ViewModel 保存在 ObservableCollection 类型的属性中 将 TabControls ItemsSource 绑定到该属性 创建一个派生自 TabItem 的新视图 使用模板选择器根据视图模型的类型自动选择视图的类型。 将 IDisposable 添加到您的子 ViewModel 并创建关闭视图的功能。

希望对您有所帮助,如果您还有其他问题,请告诉我。

【讨论】:

是的,它确实有点帮助,我还没有听说过模板选择器。虽然我对 DataContext 并不陌生 :-) Alex,考虑下面的例子,你有一个视图 MyView,它有一个 ItemsControl 类型的控件(显示项目列表)。在 XAML 中,您编写: 。如果您的 MyItemsProperty 已正确初始化并包含项目,这可能不会产生所需的效果事件。这是因为 MyItemsControl 不知道它的 DataContext。在这种情况下,在 MyView 的构造函数中,您需要编写 MyItemsControl.DataContext = this。现在 MyItemsControl 知道在哪里查看(续) 如果您在同一场景下的同一视图中有多个控件,则需要重复 n 次。 MVVM 的目的是通过 ONE SWIFT STROKE (DataContext = new ViewModel()) 消除这种情况。视图中的所有控件都以 ViewModel 作为其 DataContext。 DataGrid 有点棘手,我会在 ChildView 中处理它并将其数据绑定到 ChildViewModel - 很大程度上取决于您使用的是什么 DataGrid,它是来自 WPF Toolkit 还是一个不同的,这个控件会变得非常讨厌。 如何绑定 DataGrid 的 Column 的排序列(我的 UI 逻辑中需要它)。我正在使用 Net 4.0 附带的 DataGrid。我知道如何绑定可以通过 ItemsSource 轻松绑定的东西,但是 ReadOnly 属性、不是 Dependency 属性的属性,或者像 DataGrid 的当前排序列这样根本没有属性的东西,让我卡住了。

以上是关于使用 MVVM 模式的 UI 设计的主要内容,如果未能解决你的问题,请参考以下文章

WPF使用MVVM设计模式 问题

iOS开发之MVVM设计模式

mvvm设计模式下怎么更新数据库

JS源码分析│简易mvvm库的设计实现

iOS架构篇-4 架构模式MVVM

Angular 设计模式:MVC、MVVM 还是 MV*?