使用 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<T> : 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 应用程序中,DataTemplate
s 会使用 UserControl
s 将每种类型映射到其视图:
<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 中,您编写:以上是关于使用 MVVM 模式的 UI 设计的主要内容,如果未能解决你的问题,请参考以下文章