使用MVVM模式进行UI设计

5

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

这里的ListBox像TabControl一样运作(因此它将视图切换到右侧),并包含基本上在表格中显示的项目类型。所有UI都是动态的(ListBox项、TabItems和列在运行时确定)。该应用程序针对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还是不使用XAML都没有关系)。由于没有简单的DataBinding到DataGrid的列或TabControl的选项卡,我看到的方法有:

  • 使用IValueConverter进行DataBinding:我认为这在WPF | Silverlight的开箱即用控件中不起作用,因为我需要的一些属性是只读或双向不可绑定的。(我不确定,但我觉得它不会起作用)。
  • 通过订阅View中的INotifyPropertyChanged来使用手动逻辑: ViewModel.PropertyChanged+= ....ViewModel.ColumnList.CollectionChanged+= ....

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


更新:2011年2月28日 事情变得越来越糟糕,我决定使用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论坛上的类似话题:http://forums.silverlight.net/forums/64.aspx - Alex Burtsev
4个回答

1

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

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

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

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

  4. 创建一个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 DataType="{x:Type my:ViewModel}">
   <my:View DataContext="{Binding}"/>
</DataTemplate>

谢谢您关于TabControl的回复,但我更感兴趣的是:绑定DataGrid的列,排序列,DataGrid的选定行。 - Alex Burtsev
这在Silverlight中不起作用(我同时针对WPF和Silverlight),Maciek是正确的,你需要使用IValueConverter或从TabItem派生。 - Alex Burtsev
啊,我没看到需要Silverlight。这确实改变了事情。更好的模板选择会真正改善SL。 - Robert Rossney

1

1

Maciek和Robert已经给出了一些实现这个的想法。

关于绑定DataGrid列的具体内容,我强烈推荐参考Meleak's answer

类似于此,您可以使用附加属性(或行为),并仍然保持MVVM中的清晰ViewModel。

我知道WPF的学习曲线非常陡峭,而且您已经在努力。我也知道以下建议对此没有帮助,甚至使曲线更陡峭。但是,您的情况足够复杂,我建议使用PRISM


谢谢您的回复,我认为附加属性可以完成这项工作,我并不试图维护清洁的视图或ViewModel,我只是想完成任务。我不确定PRISM(从未见过,但我知道它是MVVM框架)是否适用,因为问题在于WPF控件(如DataGrid)设计不良,解决方案是使用\编写另一个表格控件,或者使用某种形式的事件(如附加属性)。 - Alex Burtsev
@Alex 依赖于以行为形式的事件并不是一件坏事,因为行为本身就是一种封闭的代码形式,很容易进行测试。当然,如果你只关心完成工作(而且你和其他人都不需要维护这段代码),你可以省去这个步骤,因为它确实会增加一些额外的开销。 - Markus Hütter
推荐使用PRISM来管理整体布局,'regions'管理可让生活更轻松。我们有同样的需要动态绑定列,所以我们创建了自定义控件。这是相当大量的工作,但最终你会对WPF更加了解。如果时间紧迫,可以打破MVVM模式通过代码添加列,然后找到更清晰的解决方案。 - David Masters

0
为了将您的ViewModel连接到View,您需要分配View的DataContext。这通常在View的构造函数中完成。
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类型的属性中
  • 将TabControl的ItemsSource绑定到该属性
  • 创建一个从TabItem派生的新视图
  • 使用模板选择器根据视图模型的类型自动选择视图类型。
  • 向您的子ViewModel添加IDisposable并创建关闭视图的功能。

希望这能有所帮助,如果您有进一步的问题,请告诉我。


是的,它确实有点帮助,我还没有听说过模板选择器。虽然我不是那么新,但也知道DataContext :-) - Alex Burtsev
Alex,考虑以下示例,您有一个名为MyView的视图,其中包含一个ItemsControl类型的控件(显示项目列表)。在XAML中,您编写:<ItemsControl x:Name =“MyItemsControl” ItemsSource = {Binding Path = MyItems Mode = TwoWay,UpdateSourceTrigger = PropertyChanged}/>。即使您的MyItemsProperty已正确初始化并包含项目,这也可能没有预期的效果。这是因为MyItemsControl不知道它的DataContext。在这种情况下,在MyView的构造函数中,您需要编写MyItemsControl.DataContext = this。现在,MyItemsControl知道要查找的位置(续)。 - Maciek
如果在同一场景下,您在同一视图中有多个控件,则需要重复n次。 MVVM的目的是通过一次快速操作(DataContext = new ViewModel())消除这种情况。 视图中的所有控件都将ViewModel作为其DataContext。 - Maciek
DataGrid有点棘手,我会在ChildView中处理它,并将其数据绑定到ChildViewModel - 很大程度上取决于您使用的DataGrid是哪个,是来自WPF Toolkit还是其他不同的控件,这个控件可能会变得非常棘手。 - Maciek
绑定DataGrid的列,排序列怎么办(这是我在UI逻辑中需要的)。我正在使用随Net 4.0一起提供的DataGrid。我知道如何绑定那些通过ItemsSource容易绑定的东西,但只读属性、不是依赖属性的属性或者像当前DataGrid的已排序列一样根本没有属性的东西让我卡住了。 - Alex Burtsev

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