使用MVVM模式进行UI设计

发布于 2024-10-19 23:38:08 字数 2789 浏览 0 评论 0原文

我正在尝试选择以 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; :UserControl。

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

  • 将 DataBinding 与 IValueConverter 一起使用:我认为这不适用于 WPF|Silverlight 开箱即用的控件,因为我需要的某些属性是只读的或无法以双工方式绑定。 (我不确定这一点,但我觉得它不会起作用)。
  • 通过订阅视图中的 INotifyPropertyChanged 使用手动逻辑:ViewModel.PropertyChanged+= ....ViewModel.ColumnList.CollectionChanged+= ....

  • 使用支持此绑定的自定义控件:自己编写代码或查找支持此绑定的 3d 方控件(我不喜欢此选项,我的 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

I'm trying to choose the best way to implement this UI in MVVM manner. I'm new to WPF (like 2 month's) but I have huge WinForms experience.
enter image description here

The ListBox here act's like a TabControl (so it switches the view to the right), and contains basically the Type of item's displayed in tables. All UI is dynamic (ListBox items, TabItems and Columns are determined during run-time). The application is targeting WPF and Silverlight.

Classes we need for 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; }
}

Now the question is how to wire this to View.

There are 2 base approaches I can see here:

  • With XAML: due to lack of Generics support in XAML, I will lose strong typing.
  • Without XAML: I can reuse same ListView<T> : UserControl.

Next, how to wire data, I see 3 methods here (with XAML or without doesn't matter here). As there is no simple DataBinding to DataGrid's Columns or TabControl's TabItems the methods I see, are:

  • Use DataBinding with IValueConverter: I think this will not work with WPF|Silverlight out of the box control's, as some properties I need are read-only or unbindable in duplex way. (I'm not sure about this, but I feel like it will not work).
  • Use manual logic by subscribing to INotifyPropertyChanged in View: ViewModel.PropertyChanged+= ....ViewModel.ColumnList.CollectionChanged+= ....

  • Use custom controll's that support this binding: Code by myself or find 3d party controls that support this binding's (I don't like this option, my WPF skill is too low to code this myself, and I doubt I will find free controls)


Update: 28.02.2011
Things get worser and worser, I decided to use TreeView instead of ListBox, and it was a nightmare. As you probably guess TreeView.SelectedItems is a readonly property so no data binding for it. Ummm all right, let's do it the old way and subscribe to event's to sync view with viewmodel. At this point a suddenly discovered that DisplayMemberPath does nothing for TreeView (ummmm all right let's make it old way ToString()). Then in View's method I try to sync ViewModel.SelectedItem with TreeView's:

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...
}

And none of methods I was able to think of worked....

And here is the link to my sample project demonstrating Control Library Hell with MVVM: http://cid-b73623db14413608.office.live.com/self.aspx/.Public/MVVMDemo.zip

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

篱下浅笙歌 2024-10-26 23:38:08

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

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

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

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

  4. 创建一个 TabControl 并将其 ItemsSource 绑定到此集合,然后添加一个显示 TextItemTemplate每个项目的属性。

这是一个不使用视图模型的示例(并且也没有实现 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 会使用 UserControl 将每种类型映射到其视图:

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

Maciek's answer is actually even more complicated than it needs to be. You don't need template selectors at all. To create a heterogeneous tab control:

  1. Create a view model class for each type of view that you want to appear as tab items. Make sure each class implements a Text property that contains the text that you want to appear in the tab for its item.

  2. Create a DataTemplate for each view model class, with DataType set to the class's type, and put the template in the resource dictionary.

  3. Populate a collection with instances of your view models.

  4. Create a TabControl and bind its ItemsSource to this collection, and add an ItemTemplate that displays the Text property for each item.

Here's an example that doesn't use view models (and that doesn't implement a Text property either, because the objects I'm binding to are simple CLR types), but shows how template selection works in this context:

<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>

Of course in a real MVVM application those DataTemplates would use UserControls to map each type to its view:

<DataTemplate DataType="{x:Type my:ViewModel}">
   <my:View DataContext="{Binding}"/>
</DataTemplate>
路弥 2024-10-26 23:38:08

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

对于绑定 DataGrid 列的细节,我强烈建议 Meleak 对这个问题的回答

与此类似,您可以使用附加属性(或行为),并且仍然在 MVVM 中保持干净的 ViewModel。

我知道 WPF 的学习曲线相当陡峭,而且您已经陷入困境。我也知道以下建议无济于事,甚至使曲线变得更陡。但您的场景足够复杂,我建议使用 PRISM

Maciek and Robert already gave you some ideas on how to implement this.

For the specifics of binding the columns of the DataGrid I strongly recommend Meleak's answer to that question.

Similar to that you can use attached properties (or Behaviors) and still maintain a clean ViewModel in MVVM.

I know the learning curve for WPF is quite steep and you're struggling already. I also know that the following suggestion doesn't help that and even makes that curve steeper. But your scenario is complex enough that I'd recommend to use PRISM.

爱人如己 2024-10-26 23:38:08

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

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

I wrote an article and a sample application with source code available, where I discuss and show the problems I have mentioned here and how to solve them.

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

赏烟花じ飞满天 2024-10-26 23:38:08

为了将 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 并创建关闭视图的功能。

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

In order to connect your ViewModel to your View you need to assign the View's DataContext. This is normally done in the View's Constructor.

public View()
{
    DataContext = new ViewModel();
}

If you'd like to see your view model's effect at design time, you need to declare it in XAML, in the View's resources, assign a key to it, and then set the target's DataContext via a StaticResource.

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

(The above is to demonstrate the design-time trick works)

Since you're dealing with a scenario that includes a container such as the TabControl I'd advocate considering the following things :

  • Hold your child ViewModels in a Property of type ObservableCollection
  • Bind the TabControls ItemsSource to that property
  • Create a new View that derives from TabItem
  • Use a template selector to automatically pick the type of the view based on the type of the view model.
  • Add IDisposable to yoour child ViewModels and create functionality to close the views.

Hope that helps a bit, if you have further questions let me know.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文