使用ViewModel模式来简化WPF的TreeView(用正确的方法使用TreeView)(转)

3 篇文章 0 订阅

英文原文地址:Simplifying the WPF TreeView by Using the ViewModel Pattern

作者:Josh Smith

文中代码的下载地址:http://www.codeproject.com/KB/WPF/TreeViewWithViewModel/TreeViewWithViewModelDemo.zip

好像需要登录才能下载,我放一个上来吧http://files.cnblogs.com/RMay/TreeViewWithViewModelDemo.zip

译者按:WPF中对TreeView的操作同WinForm中有很大的不同。这篇文章讲述了如何用ViewModel模式来简化WPF的TreeView,个人感觉非常有价值,尤其是在WPF中思维模式跟以往有很大不同的情况下。希望能对大家有所帮助并且触类旁通。第一次做翻译,有错误的地方敬请谅解。

介绍

 这篇文章探讨了如何通过使用ViewModel模式来更容易的使用WPF中的TreeView控件。在此过程中,我们会看到为何人们经常在使用WPF的TreeView时遇到困难,什么是ViewModel,以及两个实例程序,这两个程序展现了如何结合TreeView和ViewModel。其中一个实例展示了如何创建一个具有搜索功能的TreeView,另一个则说明了如何实现延迟加载(lazy-loading)。

TreeView的背景

 WPF中的TreeView控件背负了一个名不副实的坏名声。很多人尝试着使用它,却发现非常难用。其实问题在于人们经常试着按照Windows Forms的TreeView控件的使用方式来使用它。为了发挥(原文是leverage,意为杠杆作用,不好翻译,呵呵)WPF TreeView的丰富特性,你不能再使用跟Windows Forms一样的编程技术了。这也是WPF要求你为了更好恰当地使用它(指WPF)而转换思维方式的另一个例子。毕竟我们已经走到这一步了。(原文是We aren't in Kansas anymore, Toto。来自电影《绿野仙踪》:Toto,I've got a feeling we're not in Kansas anymore)     



 在Windows Forms中,使用TreeView非常容易,因为它非常简单。这种简单建立在Windows Forms的TreeView完全不灵活的事实上,比如不提供UI的虚拟化(UI virtualization),不提供外观的个性化,同时由于它不支持数据绑定,你必须将数据存到它的节点中。WinForm的TreeView根本不够好(原文是The WinForms TreeView is "good enough for government work","good enough for government work"是说不够好)。     



 对比之下,WPF的TreeView非常的灵活,天生支持UI虚拟化(比如,TreeViewItems是按需创建created on-demand),完全允许个性化外观,同时完全支持数据绑定。这些优秀的特性是需要代价的。他们让WPF的TreeView比WinForm的TreeView更加复杂。一旦你学会了如何正确地使用WPF的TreeView,这些复杂性将不在话下,同时发挥WPF TreeView的全部能力将变得非常容易。



 如果你对如何个性化WPF的TreeView感兴趣,可以查看这篇文章和这篇文章。

ViewModel的背景

 早在2005年,John Gossman写了一篇关于Model-View-ViewModel模式的博文,这种模式被他所在的微软的项目组用来创建Expression Blend(即'Sparkle')。它跟Martin Fowler的Presentation Model非常相似,唯一不同的是,它填平了presentation model和使用了WPF的丰富的数据绑定的view之间的沟壑。在Dan Crevier发表了神作DataModel-View-ViewModel series博文系列之后,(D)MVVM模式开始变得流行起来。





 (Data)Model-View-ViewModel模式跟经典的Model-View-Presenter模式很相似,除了你需要一个为View量身定制的model,这个model就是ViewModel。ViewModel包含所有由UI特定的接口和属性,它们是轻松构建UI的必要元素。View绑定到ViewModel,然后执行一些命令在向它请求一个动作。而反过来,ViewModel跟Model通讯,告诉它更新来响应UI。



 这使得为应用构建UI非常的容易。往一个应用程序上贴一个界面越容易,外观设计师就越容易使用Blend来创建一个漂亮的界面。同时,当UI和功能越来越松耦合的时候,功能的可测试性就越来越强。为什么不想要一个漂亮的界面同时又有一套干净有效的单元测试呢?

究竟是什么让TreeView这么难用?

 如果你的使用方法正确的话,其实TreeView是很好用的。正确的使用方法,反过来说,是根本不要直接的使用它!一般地,你需要直接地对一个TreeView设置属性,不时地调用方法。这是无法逃避的,同时这么做也没什么错。不过,如果你发现你正深陷于对控件的调用(原文是the guts of the control,不好翻译),那也许你并没有采取最佳的方式。如果你的TreeView是数据绑定的,然后你发现你正试图通过程序来折腾这些项,那么你就是没有使用正确的方法。如果你发现监听ItemContainerGenerator的StatusChanged事件来访问TreeViewItem的子节点,那你简直就是脱轨了!相信我,根本没必要这么丑陋和困难。有更好的方法!

按照WinForm TreeView的方式来使用WPF TreeView的根本问题在于,正如我前面提到的,它们是非常不同的控件。WPF的TreeView允许你通过数据绑定来生成它的Items。这意味着它会为你创建TreeViewItems。由于TreeViewItems是被控件创建的,而不是你,因此它不能保证当你需要时,某个数据项对应的TreeViewItem还存在。你必须问问TreeView的ItemContainerGenerator是否已经为你创建了TreeViewItem。如果没有,你必须监听它的StatusChanged事件,当它创建了自己的子元素之后通知你。

 有趣的不止这些!如果你想获得树中一个非常复杂,非常深的TreeViewItem,你必须问问他的父TreeViewItem,而不是TreeView控件,是否它的ItemContainerGenerator已经创建了该项。但是,你要怎样才能拿到它的父节点的引用当这个父节点还没有被创建呢?当父节点的父节点还没有被创建的时候又是怎样的呢?子子孙孙,无穷溃也。这是相当痛苦的。



 正如你所见的,WPF的TreeView是一个复杂的野兽。如果你试着用错误的方式来使用它,将不是那么容易的。幸运的是,如果你用正确的方式使用它,那就是小事一桩。那么,让我们来看看怎么样通过正确的方式来使用它……

ViewModel是解救的办法

 WPF是伟大的,因为它基本上要求你分离应用程序的数据和UI。前面击节中列出的问题都是应为违背了这个原则并且把UI当作数据存储的地方。 一旦你不再把TreeView当成一个存储数据的地方,而是看做一个展现数据的地方,那么一切都将水到渠成。这就是ViewModel这个想法的由来。



 比起写代码去折腾TreeView里面的项,更好的方法是写一个被TreeView绑定的ViewModel,然后写代码来操作你的ViewModel。这不仅仅能让你无视TreeView的复杂性,还能让你写出能够很容易进行单元测试的代码。要为那些紧密依赖于TreeView运行时行为的类写有意义的单元测试代码几乎是不可能的,但是要为那些对这些无关行为一无所知的类写单元测试代码却很容易。



 现在,我们来看看如何实现这些概念。

示例解决方案

 这篇文章附带了两个示例程序,能从页面顶部下载。该Solution包含了两个工程。BusinessLib类库工程包含了简单的描述地域的类,这些类被当作纯粹的数据传输对象。同时它还包含了一个Database类,这个类初始化数据,并且返回这些数据传输对象(译者:说白了就是模拟的数据源)。另外的一个工程,TreeViewWithViewModelDemo,包含一些示例程序。这些程序使用BusinessLib程序集中给出的数据对象,并且在放到TreeView中展示之前,把它们包装到一个ViewModel中去。



 下面是这个Solution的一个工程树截图:

Demo1 - 带文本搜索的Family Tree

 第一个示例程序中我们构建了一个展示Family Tree的TreeView。它在界面的底部提供了搜索的功能。截图如下:







 当用户输入一些关键字,敲回车,或者是点击了“Find”按钮之后,第一个匹配的项目将会被展示出来。继续搜索将在所有匹配项之间循环。所有的这些逻辑都在ViewModel中。在深入ViewModel的工作方式之前,我们先看看相关代码。下面是TextSearchDemoControl的后台代码。

public partial class TextSearchDemoControl : UserControl
{
readonly FamilyTreeViewModel _familyTree;

public TextSearchDemoControl()
{
    InitializeComponent();

    // Get raw family tree data from a database.
    Person rootPerson = Database.GetFamilyTree();

    // Create UI-friendly wrappers around the 
    // raw data objects (i.e. the view-model).
    _familyTree = new FamilyTreeViewModel(rootPerson);

    // Let the UI bind to the view-model.
    base.DataContext = _familyTree;
}

void searchTextBox_KeyDown(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Enter)
        _familyTree.SearchCommand.Execute(null);
}

}

 构造函数展示了我们如何把原始数据转换到ViewModel中,然后把它设为UserControl的DataContext。在BusinessLib程序集中定义的Person类,非常简单:

///
/// A simple data transfer object (DTO) that contains raw data about a person.
///
public class Person
{
readonly List _children = new List();
public IList Children
{
get { return _children; }
}

public string Name { get; set; }

}

PersonViewModel

 由于Person类是应用的数据访问层返回的东西,它绝对不适合直接被UI使用。每个Person对象最终被包装到一个PersonViewModel类的实例中,这样就让它拥有了额外的能力,比如被展开和选中。由此看出,FamilyTreeViewModel类,完成了把Person对象包装到PersonViewModel对象中的过程,下面是它的构造函数:

public FamilyTreeViewModel(Person rootPerson)
{
_rootPerson = new PersonViewModel(rootPerson);

_firstGeneration = new ReadOnlyCollection<PersonViewModel>(
    new PersonViewModel[] 
    { 
        _rootPerson 
    });

_searchCommand = new SearchFamilyTreeCommand(this);

}

 私有的PersonViewModel的构造函数通过递归的方式遍历Family Tree,把每一个Person包装到PersonViewModel中。下面是代码:

public PersonViewModel(Person person)
: this(person, null)
{
}

private PersonViewModel(Person person, PersonViewModel parent)
{
_person = person;
_parent = parent;

_children = new ReadOnlyCollection<PersonViewModel>(
        (from child in _person.Children
         select new PersonViewModel(child, this))
         .ToList<PersonViewModel>());

}

 PersonViewModel有两种成员:一种关联到展现,另外一种关联到Person的状态。展现相关的属性将会被TreeViewItem所绑定,而状态相关的属性绑定到TreeViewItem的内容。其中一个展现相关的属性IsSelected,如下:

///
/// Gets/sets whether the TreeViewItem
/// associated with this object is selected.
///
public bool IsSelected
{
get { return _isSelected; }
set
{
if (value != _isSelected)
{
_isSelected = value;
this.OnPropertyChanged(“IsSelected”);
}
}
}

 这个属性跟一个"person"没有任何关系,而是一个简单的用于同步View和ViewModel的状态标识。注意属性的setter调用了OnPropertyChanged方法,该方法会激发PropertyChanged事件。该事件是INotifyPropertyChanged接口的唯一成员。INotifyPropertyChanged是一个UI相关的接口,这就是为什么PersonViewModel类实现该接口,而Person类不实现。



 一个展现相关属性的更有趣的例子是PersonViewModel的IsExpaned属性。这个属性解决了怎么保证在必要的时候,跟某个数据对象对应的TreeViewItem的展开问题。记住,如果直接用TreeView来编程解决这些问题,可是痛苦不堪的。

Code

 正如我前面所提到的,PersonViewModel同时还有跟Person状态相关的属性,比如:

Code

界面部分

 把一个TreeView绑定到PersonViewModel的代码是非常简洁的。注意TreeViewItem和PersonViewModel之间的联系依靠的是控件的ItemContainerStyle:


### 回答1: MVVM(Model-View-ViewModel)是一种设计模式,以一种分离职责并且高可测试性的方式构建应用程序的UI层。在WPF应用程序中,MVVM模式是非常流行的,它将UI控件的状态和行为从UI层分离出来,使得代码更容易测试和维护。 在使用MVVM模式实现WPF TreeView中节点的添加、重命名、删除、上/下移动时,我们需要定义以下几个重要的类和接口: 1. Model:表示我们的业务逻辑,比如树形结构中节点的保存和查询,在这个例子中,我们可以定义一个Node类来表示树形结构中的一个节点。 2. View:表示WPF应用程序中的UI层。在本例中,我们可以在MainWindow.xaml中创建一个TreeView控件用于显示树形结构中的节点。 3. ViewModel:是连接Model和View的纽带,使用它来协调模型和视图之间的交互。在这个例子中,我们需要定义一个NodeViewModel类来表示一个节点在UI中的状态和行为,比如属性IsExpanded用于表示该节点是否展开,方法AddNode用于添加子节点等。 通过ViewModel类,我们可以实现对TreeView中节点的添加、重命名、删除、上/下移动等功能。具体实现可以通过ICommand接口和Command类来完成,它们可以实现对操作的数据绑定以及实现对控件的事件处理。 总之,使用MVVM模式可以让我们更好地管理WPF应用程序中的UI层,提高代码的可测试性和可维护性,它是一种非常强大和实用的设计模式。 ### 回答2: MVVM模式是一种用于构建用户界面的软件架构模式,在WPF中得到了广泛应用。在MVVM模式中,界面(View)、业务逻辑(ViewModel)和数据模型(Model)是分开的,提供了更好的可测试性和更好的代码复用性。 在WPF TreeView控件中,我们可以通过MVVM模式实现对节点的添加、重命名、删除以及上(下)移动操作。首先,我们需要定义TreeViewItemViewModel类作为TreeView节点的ViewModel,该类包含节点的名称、子节点集合以及节点的相关操作方法。 在实现TreeView节点的添加操作时,我们需要向TreeViewItemViewModel中的子节点集合中添加一个新的节点,并且将其同步更新到TreeView中。在重命名节点操作中,根据节点名称的改变,我们需要同步更新对应节点在TreeView中的名称。在删除节点操作中,我们需要移除当前节点以及其所有的子节点,并同步更新TreeView。在上(下)移动节点操作中,我们需要判断当前节点的位置以及上(下)一个兄弟节点的位置,并将其在TreeView中进行移动。 总之,通过使用MVVM模式实现WPF TreeView中节点的添加、重命名、删除和上(下)移动操作,可以提高程序的可维护性、可扩展性以及代码复用性,使得程序的开发效率和代码质量得到了提高。 ### 回答3: MVVM是一种常用的软件架构模式,用于分离用户界面的开发和业务逻辑的开发,使程序更加可维护和可测试。在WPF应用程序中,MVVM模式也非常常用。在MVVM模式中,View(视图)与ViewModel(视图模型)通过数据绑定来进行通信。ViewModel处理业务逻辑,View负责UI的呈现和交互。 本程序在MVVM模式下实现了对WPF TreeView中节点的添加、重命名、删除、上(下)移动功能。具体实现如下: 1. 添加节点:在ViewModel中维护一个ObservableCollection,用于存储TreeView的节点。View中有一个Button,绑定Command到ViewModel中的AddCommand。当用户点击Button时,会触发AddCommand的Execute方法,创建一个新的TreeViewItem,并加入到ObservableCollection中。通过数据绑定,TreeView实时地显示所有节点。 2. 重命名节点:在View中,为TreeViewItem绑定了一个ContextMenu,包括了重命名和删除两个选项。当用户右键点击选中的TreeViewItem,弹出ContextMenu。当用户选择“重命名”选项时,会触发重命名命令的Execute方法,弹出一个窗口供用户输入新的节点名称。当用户确认新名称时,ViewModel中的节点名称更新,TreeView实时刷新节点名称。 3. 删除节点:删除节点也是在ContextMenu中实现。当用户选择“删除”选项时,会触发删除命令的Execute方法,从ViewModel中的ObservableCollection中移除选中的节点。TreeView实时刷新节点显示。 4. 移动节点:在View中,为TreeViewItem绑定了两个Button,分别代表上移和下移。当用户点击上移按钮时,会触发上移命令的Execute方法,将当前选中节点与上一个节点交换位置。当用户点击下移按钮时,会触发下移命令的Execute方法,将当前选中节点与下一个节点交换位置。 综上所述,该程序通过使用MVVM模式,实现了对WPF TreeView中节点的添加、重命名、删除、上(下)移操作,提高了程序的可维护性和可测试性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值