65.9K
CodeProject 正在变化。 阅读更多。
Home

基于 XAML 的应用程序的演示模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (44投票s)

2012年5月14日

CPOL

23分钟阅读

viewsIcon

100795

downloadIcon

1582

WPF、Silverlight 和 Windows Phone 应用程序演示层上的设计模式。

XAML Patterns

引言

近年来,各种不同的模式,如 MVVM/MVC 和 Command,已成功应用于用户界面导向的应用程序开发中。本文描述了专门的设计模式,它们可用于基于 XAML 的应用程序的用户界面开发。

通过一个示例库,将使用几个日常应用示例来演示这些设计模式,并说明如何将它们编码为可重用组件。

除了增加重用性之外,这些设计模式还有助于降低系统的复杂性。它们通过促进参与设计人员的共同理解来支持未来的开发设计过程。

在设计模式的实现过程中,必须考虑通信、性能、资源管理以及 WPF/Silverlight/Windows Phone 7 复合开发等方面的各种问题。将涉及以下领域:

  • 动作和同步/异步命令
  • 绑定到 UI 元素的对象容器的性能
    将 ObservableCollection 性能提高多达 1000%
  • 同步/异步数据加载 
  • 按需加载数据
    使用延迟加载扩展 TreeView
  • 用于导航和编辑的容器视图 
  • 控制集合中的选择
    跟踪 ListBox、ComboBox 和 DataGrid 选择
  • 项目模型 – 视图模型抽象
  • 不同类型对象的层次管理 
  • 用于编辑元素的编辑器
    使用 CRUD 操作增强 TreeView 和 DataGrid 

如前所述,本文中介绍的库应被视为在演示层上实现给定设计模式的一种可能变体。为了在此广泛的主题上保持对重要方面的关注,没有使用额外的工具和组件,并且只使用了标准的 .NET 框架项目。尽管该库可以按原样使用,但仍建议根据目标领域进行调整。

设计模式可以单独查看,并可用于现有架构。

动作和命令

操作

一个 Action 描述了一个简单且自包含的操作,并将其实现开放。它充当 Action ProviderAction Consumer 之间的契约。

Action

一个 Action 的主要特征是它专注于实际情况或操作。几个动作可以通过主题组合成一个 Action Group,尽管它们通常彼此之间没有其他关系。目标是减少依赖关系以降低整体复杂性。ActionsAction Groups 可以看作是构建块。它们的组合将由 Action Consumer 决定,例如视图模型。

以下示例演示了 Action 模式的用法:

Action Examples

由于 Action 是一个中立的定义,因此可以开发标准化的组件,然后将其用于不同的应用程序。在接下来的过程中,我们将使用一个包含以下 Action GroupsActions 的库:

动作组 操作
Data
  • 可清除
  • 可加载
  • 可刷新
  • 可重置
  • 可选择
  • 可排序
  • 比较器可排序
  • 比较可排序
  • 可更新
项目
  • 项目创建
  • 项目刷新
  • 项目编辑
  • 项目删除
媒体
  • 播放
  • Pause
  • 恢复
  • 停止
导航
  • 可展开
  • 可折叠

Action 的实现以简单接口的形式完成

// ------------------------------------------------------------------------
public interface IExpandable
{

  // ----------------------------------------------------------------------
  bool IsExpandable { get; }

  // ----------------------------------------------------------------------
  void Expand();

} // interface IExpandable

一个 TreeView 将作为 Action Provider 的示例

// ------------------------------------------------------------------------
public class MyTreeView : TreeView, IExpandable
{

  // ----------------------------------------------------------------------
  bool IExpandable.IsExpandable
  {
    get
    {
      MyTreeViewItem selectedItem = SelectedTreeViewItem;
      return selectedItem != null && !selectedItem.IsExpanded &&
        selectedItem.Items != null && selectedItem.Items.Count > 0;
    }
  } // IExpandable.IsExpandable

  // ----------------------------------------------------------------------
  void IExpandable.Expand()
  {
    MyTreeViewItem selectedItem = SelectedTreeViewItem;
    if ( selectedItem != null )
    {
      selectedItem.IsExpanded = true;
    }
  } // IExpandable.Expand

} // class MyTreeView

Action Consumers 的实现通过命令完成,这将在下一章中描述。

命令

GoF 引入的 Command 设计模式通过接口 ICommand 存在于 .NET 框架中。一个命令启动一系列步骤,这些步骤通常由用户通过某个用户界面控件调用。命令可以由不同的元素提供:

  • 类实用程序/命令容器,例如 System.Windows.Input.ApplicationCommands
  • 视图模型
  • UI 控件

Command 的触发通常是像按钮这样的东西,它通过绑定到属性 Command 连接到命令。通常,命令的可用性通过 ICommand.CanExecute(启用)在 UI 元素中可视化。命令的调用通过调用方法 ICommand.Execute 发生。

以下派生通过以下示例方面扩展了 ICommand

// ------------------------------------------------------------------------
public interface ICommand : System.Windows.Input.ICommand, INotifyPropertyChanged, IDisposable
{

  // ----------------------------------------------------------------------
  event EventHandler Started;

  // ----------------------------------------------------------------------
  event EventHandler Finished;

  // ----------------------------------------------------------------------
  string Name { get; }

  // ----------------------------------------------------------------------
  bool IsExecuting { get; }

  // ----------------------------------------------------------------------
  void Refresh( object parameter );

} // interface ICommand
标识 命令有一个名称。
异步执行 根据技术和使用场景,命令将执行异步操作。通过扩展 Execute 和执行状态 IsExecuting,命令为同步和异步执行环境提供了通用基础。
通过注册 StartedEnded 事件,外部观察者可以跟踪执行状态。
状态 命令的可用性通过 IsAvailable 发出信号。此处实现的变体确保命令在执行期间不可用。方法 Refresh 可用于显式刷新可用性状态。
状态变化 为了传达值的变化,支持 .NET 接口 INoftifyPropertyChanged
资源管理 为了确保正确释放任何持有的资源,实现了 .NET 接口 IDisposable

为了简化多个命令的处理(执行控制、资源管理等),它们可以注册在 ICommandCollection 中。

动作命令

Action Command 设计模式结合了 ActionCommand 模式。通常,一个 Action Command 只应涵盖一个 Action。这有助于减少相互关联和复杂性。这种限制也导致更好的可重用性,并且是具有预定义命令的库的基础,例如 .NET 框架的 ApplicationCommands

以下示例演示了 Action Commands 的实现:

// ------------------------------------------------------------------------
public class ExpandCommand : Command
{

  // ----------------------------------------------------------------------
  public ExpandCommand( IExpandable expandable )
  {
    if ( expandable == null )
    {
      throw new ArgumentNullException( "expandable" );
    }
    this.expandable = expandable;
  } // ExpandCommand

  // ----------------------------------------------------------------------
  public IExpandable Expandable
  {
    get { return expandable; }
  } // Expandable

  // ----------------------------------------------------------------------
  protected override bool IsAvailable( object parameter )
  {
    return Expandable.IsExpandable;
  } // IsAvailable

  // ----------------------------------------------------------------------
  protected override bool DoExecute( object parameter )
  {
    Expandable.Expand();
    return true; // finished
  } // DoExecute

  // ----------------------------------------------------------------------
  // members
  private readonly IExpandable expandable;

} // class ExpandCommand

文章 WPF 命令模式应用 描述了 Command 设计模式的更多使用场景。

集合

项目集合

一个 Item Collection 是一个专门用于带有绑定的 UI 元素的容器。Item Collection 的声明涵盖了几个方面:

  • 列表功能:ICollection<TItem>
  • 排序:ISortableIComparisonSortable<TItem>IComparerSortable<TItem>
  • 消息传递:INotifyCollectionChangedINotifyPropertyChanged
  • 数据更新:IUpdateable
  • 资源管理:IDisposable

以下接口 IItemCollection 描述了一个 Item Collection

// ------------------------------------------------------------------------
public interface IItemCollection<TItem> : ICollection<TItem>,
  ISortable, IComparisonSortable<TItem>, IComparerSortable<TItem>,
  INotifyCollectionChanged, INotifyPropertyChanged, IUpdateable, IDisposable
{

  // ----------------------------------------------------------------------
  void AddAll( IEnumerable<TItem> items );

  // ----------------------------------------------------------------------
  void Replace( TItem oldItem, TItem newItem );

} // interface IItemCollection
排序 IItemCollection 提供了通过不同方式对其元素进行排序的功能。这种排序是永久性的,不应与 .NET ICollectionView 的排序混淆,后者用于排序显示。
资源管理 实现 .NET 接口 IDisposable 有助于释放任何持有的资源。在列表处置期间,所有支持 IDisposable 契约的元素都将被处置。
添加所有 添加可枚举源的所有元素。
替换 用另一个元素替换一个元素。

除了各种辅助方法外,使用 Item Collection 的主要好处在于它支持 Updateable 操作,这有助于在批量操作时大大提高性能。

许多 UI 元素(ListBoxDataGridTreeView 等)旨在处理元素列表。属性 ItemsSource 将列表绑定到 UI 元素。如果在运行时列表的内容发生变化,这将反映在相应的 UI 元素中。此机制基于接口 INotifyCollectionChanged。列表(在本例中为 ObservableCollection)实现了 INotifyCollectionChanged,并且 UI 元素检查数据源是否实现了此接口。如果是这种情况,列表的通知将被接收并显示。事件序列如下所示:

Observable Collection

为了减少容器和 UI 元素之间的通信量,Item Collection 使用更新机制扩展了标准列表,该机制由操作 Updateable 描述。这将导致以下事件序列:

Item Collection

通过方法 BeginUpdateEndUpdate(向 VisualBasic 致敬),发送到 UI 元素的通知将手动协调。在这两个时间点之间,可以在容器上执行任意数量的任意操作(AddReplaceSortClear ...),而 UI 元素不会收到它们的通知。用户界面的最终更新发生在更新序列的末尾,这导致在布局和渲染方面进行优化。

// ----------------------------------------------------------------------
public void LoadCustomers()
{
  Customers.BeginUpdate();
  Customers.Clear();
  Customers.Add( new CustomerModel( "Joe", "Smith" ) );
  Customers.Add( new CustomerModel( "Mary", "Jones" ) );
  Customers.Add( new CustomerModel( "Lisa", "Black" ) );
  Customers.Add( new CustomerModel( "Peter", "Brown" ) );
  Customers.EndUpdate();
} // LoadCustomers

关于 BeginUpdate/EndUpdate 的使用,应遵循以下建议:

  • 对于加载、合并等批量操作最有效。
  • 在对单个列表元素进行 AddInsertRemove 操作时没有意义。
  • 调用必须始终保持平衡,这在异常处理中应予以考虑。

测量结果表明,根据使用场景,使用 Item Collection 可以极大地提高性能。但是,无法给出关于性能提升的通用声明,因为这取决于各种因素:

  • 更新序列期间操作的数量和顺序
  • 绑定方法:DependecyPropertyINotifiyPropertyChanged
  • UI 元素渲染绑定值的方式
  • WPF 和 Silverlight 的 UI 元素实现不同
  • Silverlight 运行时:在浏览器中或浏览器外

为了获得关于可实现的性能提升的可靠声明,需要在目标使用场景中进行单独的测试运行。

以下结果汇编显示了绑定到 ListBoxItem Collection 的一些测量结果,其中每个列表元素有 2 个绑定。示例 ListBox Binding 的测量结果显示了绑定到 DependencyProperty DP 以及使用 INotifyPropertyChanged INPC 接口的绑定的加载时间的改进。

加载项目
Count
DP
[毫秒]
DP 更新
[毫秒]
DP 更新
vs
DP
INPC
[毫秒]
INPC 更新
[毫秒]
INPC 更新
vs
INPC
INPC
vs
DP
INPC 更新
vs
DP 更新
WPF
ItemCollection
1'000'000 16'966 8'370 203% 10'522 3'538 297% 161% 237%
WPF
CollectionView
100'000 17'395 2'180 798% 19'687 1'853 1063% 88% 118%
WPF
CollectionView
10'000 750 217 346% 1'066 182 584% 70% 119%
Silverlight 1'000'000 33'316 24'035 139% 13'962 8'523 164% 239% 282%
Silverlight
OOB
1'000'000 29'599 21'820 136% 12'928 7'342 176% 229% 297%
Windows
Phone 7
100'000 11'827 10'937 108% 6'419 5'382 119% 184% 203%
测试计算机:ThinkPad T410s, i5M520, 4 GB RAM, 120 GB SSD HD, Win7-64, Windows-Index 4.4

WPF ListBox 的测量结果显示,特别是在使用 ICollectionView 和大量项目时,性能可以显著提高。Silverlight ListBox 也显示出显著改进,而在 Windows Phone 7 上,优化几乎难以察觉。

下表显示了 DataGrid Binding 示例的结果,该示例测量了绑定到 DataGridItem Collection 的加载时间的改进。DataGrid 中的每个元素有 5 个绑定。

加载项目
Count
DP
[毫秒]
DP 更新
[毫秒]
DP 更新
vs
DP
INPC
[毫秒]
INPC 更新
[毫秒]
INPC 更新
vs
INPC
INPC
vs
DP
INPC 更新
vs
DP 更新
WPF 1'000'000 17'277 8'478 204% 11'099 3'523 315% 156% 241%
Silverlight 1'000 307 166 184% 254 156 163% 121% 107%
Silverlight 10'000 2'148 338 635% 1'940 177 1098% 111% 192%
Silverlight 100'000 202'375 1'851 10935% 185'641 484 38382% 109% 383%
Silverlight
OOB
1'000 213 125 171% 192 109 176% 111% 114%
Silverlight
OOB
10'000 1'690 260 650% 1'534 140 1093% 110% 185%
Silverlight
OOB
100'000 198'116 1'695 11688% 183'494 452 40566% 108% 375%

WPF DataGrid 显示出与 ListBox 类似的改进,与项目数量无关。然而,对于 Silverlight DataGrid,优化与项目数量相关。列表中的项目越多,性能提升越大。

在实际情况中,许多其他因素对整体加载时间有相当大的影响,例如通信、服务、数据读取等。然而,UI 的准备工作占据了整体处理时间的很大一部分,因此使用此优化将导致整体性能显著提高。

项目集合视图

许多应用程序提供选择/聚焦列表元素的功能,然后该元素将用于进一步的操作(编辑、删除、复制等)。Item Collection View 通过以下功能支持此类情况:

Item Collection View

与 .NET ICollectionView 不同,后者设计在 IEnumerable 列表之上,用于导航、过滤和分组,Item Collection View 专注于绑定到 UI 的列表,这些列表支持 INotifyCollectionChanged 接口。以下实现仅关注导航,不支持过滤和分组:

// ------------------------------------------------------------------------
public interface IItemCollectionView<out TItemCollection, TItem> :
  IEnumerable<TItem>, INotifyCollectionChanged, INotifyPropertyChanged, IDisposable
    where TItemCollection : class, INotifyCollectionChanged, IList<TItem>
{

  // ----------------------------------------------------------------------
  event CurrentChangingEventHandler CurrentChanging;

  // ----------------------------------------------------------------------
  event EventHandler CurrentItemChanged;

  // ----------------------------------------------------------------------
  TItemCollection Items { get; }

  // ----------------------------------------------------------------------
  ViewSyncMode SyncMode { get;}

  // ----------------------------------------------------------------------
  TItem CurrentItem { get; set; }

  // ----------------------------------------------------------------------
  int CurrentPosition { get; }

  // ----------------------------------------------------------------------
  bool SyncCurrentToNewItem { get; set; }

  // ----------------------------------------------------------------------
  void EnsureCurrent();

  // ----------------------------------------------------------------------
  bool MoveCurrentToFirst();

  // ----------------------------------------------------------------------
  bool MoveCurrentToLast();

  // ----------------------------------------------------------------------
  bool MoveCurrentToNext();

  // ----------------------------------------------------------------------
  bool MoveCurrentToPosition( int position );

  // ----------------------------------------------------------------------
  bool MoveCurrentToPrevious();

} // interface IItemCollectionView

属性 CurrentItem 代表指针,可以手动或通过绑定更改。以下示例将 DataGrid 的选择与 CurrentItem 同步:

<DataGrid
  ItemsSource="{Binding View.Items}"
  SelectedItem="{Binding View.CurrentItem, Mode=TwoWay}">
</DataGrid>

指针的位置可以通过各种 MoveCurrentToXxx 操作进行控制。

Item Collection 发生变化时,可以使用 EnsureCurrent 同步指针。开关 SyncCurrentToNewItem(默认值=true)确定在插入新元素时是否应调整 CurrentItem 指针。此外,视图保证指针在修改(例如删除当前项)时保持其位置。这可以防止活动元素随意跳动。

在打开 CurrentItem 的自动同步时,可以使用属性 SyncMode 控制同步模式:

  • ViewSyncMode.SynchronousCurrentItem 的调整同步发生
  • ViewSyncMode.AsynchronousCurrentItem 的调整异步发生

当视图的 ItemsCurrentItem 绑定到同一个 UI 元素时,异步调整是必要的。在这种情况下,异步调整可确保在列表发生更改时,UI 元素不会重置 CurrentItem 的值(双向绑定)。

项目集合选择

有些应用程序提供了选择多个元素并将其用于进一步操作的可能性。模式 Item Collection Selection 基于动作 Selectable,提供了对元素选择的控制。

// ------------------------------------------------------------------------
public interface ISelectable
{

  // ----------------------------------------------------------------------
  event SelectionChangedEventHandler SelectionChanged;

  // ----------------------------------------------------------------------
  IEnumerable SelectedItems { get; }

  // ----------------------------------------------------------------------
  void UpdateSelection( IEnumerable removedItems, IEnumerable addedItems );

} // interface ISelectable

它的声明在接口 IItemCollectionSelection 中进行。

// ------------------------------------------------------------------------
public interface IItemCollectionSelection<out TItemCollection, TItem> : IEnumerable<TItem>,
  INotifyCollectionChanged, INotifyPropertyChanged, ISelectable
    where TItemCollection : class, INotifyCollectionChanged, ICollection<TItem>, new()
{

  // ----------------------------------------------------------------------
  new TItemCollection SelectedItems { get; }

} // interface IItemCollectionSelection

示例 Selection 演示了 Item Collection Selection 模式的用法。

视图模型

以下章节介绍了几种表示视图模型的模式。

View Models

项目模型

Item Model 设计模式用于描述为在 UI 中表示而设计的对象。UI 元素和 Item Model 之间的通信通过动作(参见命令章节)和属性绑定发生。绑定用于在 UI 中显示属性值并更改它们。值的绑定通过 DependencyProperty DP 或借助接口 INotifyPropertyChanged INPC 进行。

哪种变体更有意义取决于具体情况。一个通用建议是,DP 主要用于 UI 元素,而 INPC 用于以数据为中心的对象。由于 Item Model 是一个应该涵盖这两个类别的构造,因此并非总是能够做出明确的选择。以下概述提供了一些帮助,用于决定哪种方法是合适的,并列出了几个方面及其各自的优缺点:

方面 声明 DP INPC
继承 DP 要求从基类 DependencyObject 继承,这并非总是可取或可实现的。DependencyObject 的 WPF 变体密封了方法 EqualsGetHashCode,这可能导致派生中的不良限制。 - +
只读值 DP 的值始终可以通过绑定修改。但是,有些值应该只由项本身更改,例如加载状态。在这种情况下,应该使用 INPC。 - +
多线程 由于基类 DependencyObject,DP 应该只在单个线程中使用。 - +
序列化 .NET 序列化无法对 DP 基类 DependencyObject 进行序列化。这可以通过 XamlReaderXamlWriter 工具规避。 - +
触发器 INPC 在控制执行时机以及组合多个值的更改方面提供了更大的灵活性。 - +
测试 对于测试,DependencyObject 在线程方面的限制变得相关,这会阻止自动化测试。 - +
性能 尽管 Microsoft 建议使用 DP 来提高性能(MSDN),但本文的测量结果表明 INPC 通常会带来更好的行为(参见 Item Collection 一章)。 ? ?
可读性/
简单
这是一个品味问题。初学者通常觉得 DP 模式更容易理解。 ? ?
资源 DP 支持从资源赋值。 + -
样式/
模板化
DP 属性可以通过样式(使用设置器)更改。 + -
动画 只有 DP 可以动画。 + -
元数据
重写
DP 可以通过派生更改,例如用于分配不同的样式。 + -

来源:DP vs INPC, MSDN 上的 WPF-DP, MSDN 上的 Silverlight-DP

为了能够以标准化方式处理 Item Model,必须隐藏 DP 和 INPC 之间的差异,并将定义打包到中立结构中。接口是实现此目的的标准选择,并为 Item Model 的抽象定义提供了基础。

// ------------------------------------------------------------------------
public interface IItemModel : INotifyPropertyChanged, IDisposable
{

  // ----------------------------------------------------------------------
  TDisposable RegisterDisposable<TDisposable>( TDisposable item )
    where TDisposable : IDisposable;

  // ----------------------------------------------------------------------
  void UnregisterDisposable<TDisposable>( TDisposable disposable, bool dispose = true )
    where TDisposable : IDisposable;

} // interface IItemModel

该定义基于接口 INotifyPropertyChanged,因为它可以在两种模型变体(DP 和 INPC)中使用。为了支持资源的正确释放,支持 IDisposable 接口。借助实用方法 RegisterDisposable,可以注册在 Item Model 处置时也应释放的资源。

为了支持这两种实现变体,提供了基类 Dependency.ItemModelNotifiable.ItemModel

以下派生演示了 Item ModelActionCommand 设计模式的组合。

// ------------------------------------------------------------------------
public class MyModel : ItemModel, IRefreshable
{

  // ----------------------------------------------------------------------
  public MyModel()
  {
    refreshCommand = RegisterDisposable( new RefreshCommand( this ) );
  } // MyModel

  // ----------------------------------------------------------------------
  public ICommand RefreshCommand
  {
    get { return refreshCommand; }
  } // RefreshCommand
  
  // ----------------------------------------------------------------------
  void IRefreshable.Refresh()
  {
    // refresh data
  } // IRefreshable.Refresh

  // ----------------------------------------------------------------------
  // members
  private readonly RefreshCommand refreshCommand;

} // class MyModel

数据项模型

Data Item ModelData Model 的专门化,增加了支持加载操作的功能。

// ------------------------------------------------------------------------
public interface IDataItemModel : IItemModel, ILoadable
{

  // ----------------------------------------------------------------------
  event EventHandler Loading;

  // ----------------------------------------------------------------------
  event EventHandler Loaded;

} // interface IDataItemModel

Data Item Model 支持其数据的同步和异步加载。与 Item Model 对应,提供了两种实现变体 Dependency.DataItemModelNotifiable.DataItemModel

以下示例演示了数据的同步加载:

// ------------------------------------------------------------------------
public class MyModel : DataItemModel
{

  // ----------------------------------------------------------------------
  protected override bool DoLoad()
  {
    // data load
    LoadDataSync();
    return true; // true = load finished -> synchronous
  } // DoLoad

} // class MyModel

以下示例演示了数据的异步加载:

// ------------------------------------------------------------------------
public class MyDataModel : DataItemModel
{

  // ----------------------------------------------------------------------
  protected override bool DoLoad()
  {
    // invoke data load
    LoadDataAsync( new Action( DataLoaded ) );
    return false; // false = continue loading -> asynchronous
  } // DoLoad

  // ----------------------------------------------------------------------
  private void DataLoaded()
  {
    // apply new data
    LoadFinished(); // indicate end of load
  } // DataLoaded

} // class MyDataModel

分层项目模型

对于更复杂的数据结构的表示,通常使用分层模型。许多应用程序使用层次结构来表示功能区域或文档。

表示分层结构的困难在于,需要在一个抽象结构中协调具有不同表示和运行时行为的元素。以下方面对于层次结构中的任何一组元素都是有效的:

  • 该元素具有可编辑的属性
  • 该元素可以删除
  • 该元素可以有子元素
  • 子元素的数量是静态的,因此它是一个纯结构元素
  • 子元素必须从某个数据源获取
  • 子元素应仅按需(重新)加载
  • 可以添加子元素

这些(相对抽象的)考虑的最佳说明可以通过一些示例来展示。第一个示例显示了 .NET 应用程序的对象在分层显示中:

Reflection Model

下图显示了一个简单订单管理的可能分层结构:

Order Model

Hierarchical Item Model 定义了一个模式,通过该模式可以以分层方式管理 UI 模型。此模型中的所有元素都必须派生自相同的 Item Model。不支持与其他元素的混合。Hierarchical Item Model 由接口 IHierarchicalItemModel 描述,如下所示:

// ------------------------------------------------------------------------
public interface IHierarchicalItemModel<out TItemCollection, out TItem> : IItemModel
  where TItemCollection : class, ICollection<TItem>, new()
  where TItem : IItemModel, IHierarchicalItemModel<TItemCollection,TItem>
{

  // ----------------------------------------------------------------------
  bool HasChildren { get; }

  // ----------------------------------------------------------------------
  TItemCollection Children { get; }

  // ----------------------------------------------------------------------
  TItem Parent { get; }

  // ----------------------------------------------------------------------
  bool IsRoot { get; }

} // interface IHierarchicalItemModel

该定义控制父元素和子元素之间的关系。层次结构中的根元素没有父元素。

Item Model 一样,这里也存在两种实现变体:Dependency.HierarchicalItemModelNotifiable.HierarchicalItemModel

分层数据项模型

分层数据项模型结合了数据项模型分层项模型的模式,并允许管理其元素可以动态加载的分层结构。

// ------------------------------------------------------------------------
public interface IHierarchicalDataItemModel<TItem> : IDataItemModel, IHierarchicalItemModel<TItem>
  where TItem : IDataItemModel, IHierarchicalDataItemModel<TItem>
{
} // interface IHierarchicalDataItemModel

Hierarchical Data Item Model 的用法将在后面的示例 Assembly BrowserOrder Browser 中详细描述。

HierarchicalDataItemModel 的实现演示了如何开发一个用于通用用途的类。根据其声明,元素必须支持 ILoadable 接口(通过 IDataModel)。为了不失去 Item Collection 提供的更新机制的优势,该类在加载时检查元素是否支持 IUpdateable。在这种情况下,加载过程还支持 BeginUpdateEndUpdate

视图编辑器

项目编辑器

Item Editor 设计模式表示元素的编辑上下文并提供相应的操作。Item Editor 作为选定元素和编辑命令之间的连接协调器。以下实现侧重于 LOB 对象的操作,这相当于数据库术语中的标准 CRUD 操作。

// ------------------------------------------------------------------------
public interface IItemEditor : IItemCreate, IItemEdit, IItemDelete, IItemRefresh,
  INotifyPropertyChanged, IDisposable
{

  // ----------------------------------------------------------------------
  event EventHandler ItemChanged;

  // ----------------------------------------------------------------------
  object Item { get; set; }

  // ----------------------------------------------------------------------
  ItemCreateCommand ItemCreateCommand { get; }

  // ----------------------------------------------------------------------
  ItemEditCommand ItemEditCommand { get; }

  // ----------------------------------------------------------------------
  ItemDeleteCommand ItemDeleteCommand { get; }

  // ----------------------------------------------------------------------
  ItemRefreshCommand ItemRefreshCommand { get; }

} // interface IItemEditor

关于此设计的一点说明:
此实现变体不限制其在项目方面的使用,并使用 object 类型。在封闭的使用场景中,Item Editor 可以专门针对特定类型:IItemEditor<TItem> where TItem : IMyItemType

Item Editor 提供编辑命令,这些命令将绑定到视图中的相应控件。

<Button
  Content="New"
  Command="{Binding Editor.ItemCreateCommand}" />
<Button
  Content="Edit"
  Command="{Binding Editor.ItemEditCommand}" />
<Button
  Content="Delete"
  Command="{Binding Editor.ItemDeleteCommand}" />
<Button
  Content="Refresh"
  Command="{Binding Editor.ItemRefreshCommand}" />

哪些条件必须满足哪些相应的操作,这在 ItemEditor 类的派生中确定。

// ------------------------------------------------------------------------
public class MyItemEditor : ItemEditor
{

  // ----------------------------------------------------------------------
  protected override void UpdateCommands()
  {
    CanCreate =
      Item is CompanyCollectionModel ||
      Item is CustomerCollectionModel ||
      Item is OrderCollectionModel;
    CanEdit =
      Item is CompanyModel ||
      Item is CustomerModel ||
      Item is OrderModel;
    CanDelete = CanEdit;
    CanRefresh = Item is IRefreshable;
  } // UpdateCommands

  // ----------------------------------------------------------------------
  public override void Create( Action onFinished )
  {
    // create operations
    …
    base.Create( onFinished );
  } // Create

  // ----------------------------------------------------------------------
  public override void Edit( Action onFinished )
  {
    // edit operations
    …
    base.Edit( onFinished );
  } // Edit

  // ----------------------------------------------------------------------
  public override void Delete( Action onFinished )
  {
    // delete operations
    …
    base.Delete( onFinished );
  } // Delete

} // class MyItemEditor

项目集合编辑器

Item Collection EditorItem Editor 的一个特化,涵盖了 Item Collection 中元素的编辑。集成的 Item Collection View 允许导航相关的 Item Collection,从而允许针对当前位置进行特定操作。

// ------------------------------------------------------------------------
public interface IItemCollectionEditor<TItemCollection, TItem> : IItemEditor
  where TItemCollection : class, INotifyCollectionChanged, IList<TItem>
{

  // ----------------------------------------------------------------------
  TItemCollection Items { get; set; }

  // ----------------------------------------------------------------------
  IItemCollectionView<TItemCollection, TItem> View { get; }

  // ----------------------------------------------------------------------
  TItem CurrentItem { get; set; }

  // ----------------------------------------------------------------------
  int CurrentPosition { get; }

  // ----------------------------------------------------------------------
  void EnsureItem();

} // interface IItemCollectionEditor

示例 Customer Admin 演示了 Item Collection Editor 的用法。

项目编辑器提供者

Item Editor Provider 是一个 Item Editor,它为当前元素动态提供一个匹配的编辑器。基于 .NET 对象类型 Type,可以为每种类型注册一个编辑器。这种基于元素类型的编辑器最适用于分层结构,其中元素类型通过用户交互动态变化,并且元素类型可以在层次结构中的多个位置出现。接口 IItemEditorProvider 代表一个 Item Editor Provider,如下所示:

// ------------------------------------------------------------------------
public interface IItemEditorProvider : IItemEditor
{

  // ----------------------------------------------------------------------
  void RegisterEditor( Type itemType, IItemEditor editor );

  // ----------------------------------------------------------------------
  void UnregisterEditor( Type itemType );

} // interface IItemEditorProvider

要使用 ItemEditorProvider,需要为每种类型注册一个编辑器。

ItemEditorProvider editor = new ItemEditorProvider();
editor.RegisterEditor( typeof( MyModel ), new MyModelEditor() );

MyItemModelEditor itemEditor = new MyItemModelEditor();
editor.RegisterEditor( typeof( MyCollectionModel ), itemEditor );
editor.RegisterEditor( typeof( MyItemModel ), itemEditor );

示例 Order Browser 演示了 Item Editor Provider 的用法。

视图演示者

项目演示者

一个 Item Presenter 描述了负责显示元素的某个对象。在 UI 中,Item Presenter 确保选择一个元素将显示其属性的相应 UI 元素,例如。它的表示在接口 IItemPresenter 中涵盖:

// ------------------------------------------------------------------------
public interface IItemPresenter
{

  // ----------------------------------------------------------------------
  object BuildContent( object item );

} // interface IItemPresenter

使用方法 BuildContentItem Presenter 为元素创建 UI 内容。

视图项演示者

专门的 View Item Presenter 用于确定一个 Item Presenter,它为元素提供一个视图,或者更准确地说是一个 FrameworkElement

// ------------------------------------------------------------------------
public interface IViewItemPresenter<TView> : IItemPresenter
  where TView : FrameworkElement, new()
{
} // interface IViewItemPresenter

项目演示者提供者

Item Presenter Provider 根据类型协调元素的可用 Item Presenters

// ------------------------------------------------------------------------
public interface IItemPresenterProvider : INotifyPropertyChanged, IDisposable
{

  // ----------------------------------------------------------------------
  object Item { get; set; }

  // ----------------------------------------------------------------------
  IEnumerable ItemContent { get; }

  // ----------------------------------------------------------------------
  void RegisterPresenter( Type itemType, IItemPresenter presenter );

  // ----------------------------------------------------------------------
  void UnregisterPresenter( Type itemType, IItemPresenter presenter );

} // interface IItemPresenterProvider

可以为每种元素类型注册一个 IItemPresenter。每次 Item 更改时,可用 Item Presenters 的内容将在属性 ItemContent 中提供。通过属性 DefaultPresenter,可以选择一个 Item Presenter 用于未知类型。

示例 Order Browser 演示了 Item Presenter Provider 的用法。

示例

媒体播放器

通过一个媒体播放器演示了控件 MediaPlayerController 如何在现有控件 MediaPlayerButton 之间搭建桥梁,以控制与 ActionCommand 的交互。

MediaPlayerController 控制一个 MediaPlayer,并且没有自己的视觉表示。属性 ElementNameMediaPlayerControllerMediaPlayer 连接起来。

<MediaElement
  x:Name="MediaElement"
  Grid.Row="0"
  LoadedBehavior="Manual"
  Source="{Binding Source}" />

<XamlControls:MediaPlayerController
  x:Name="MediaPlayerController"
  MediaElement="{Binding ElementName=MediaElement}" />

MediaPlayer 的控制通过按钮进行,这些按钮绑定到 MediaPlayerControllers 的命令。

<Button
  Content="Play"
  ToolTipService.ToolTip="Play Movie"
  Visibility="{Binding CanPlay, ElementName=MediaPlayerController,
    Converter={StaticResource FlagToVisibleConverter}}"
  Command="{Binding PlayCommand, ElementName=MediaPlayerController}" />

媒体播放器 Play/Pause/Resume/Stop 的各种命令保存在 CommandCollection 中,并将在 MediaPlayerController 处置时释放。

此外,MediaPlayerController 演示了如何协调 WPF 和 Silverlight/WP7 的 MediaPlayer 控件版本之间的差异。

集合绑定

示例 ListBox BindingGridView Binding 演示了列表与相应 UI 元素的绑定。它们的主要用途是测量加载性能(参见 Item Collection 一章)。可以调整以下运行时方面:

  • 数据源的类型:IItemCollectionICollectionView(仅适用于 WPF ListBox
  • 绑定到 Item Model 的类型:DependencyPropertyINotifyPropertyChanged
  • 项目数量
  • 更新模式 – 支持或不支持 IUpdateable

示例 GridView Binding 仅适用于 WPF 和 Silverlight。

选择

示例 Selection 使用 ListBoxDataGrid 控件演示了如何使用 Item Collection Selection 模式处理多重选择。

控件与 ItemCollectionSelection 的连接通过辅助元素 SelectorSelectionListBoxComboBox)以及 DataGridSelectionDataGrid)进行。

<ListBox
  SelectionMode="Extended"
  ItemsSource="{Binding Customers}"
  Controls:SelectorSelection.Selection="{Binding CustomerSelection}" />

<DataGrid
  SelectionMode="Extended"
  ItemsSource="{Binding Customers}"
  Controls:DataGridSelection.Selection="{Binding CustomerSelection}" />

客户管理

示例 Customer Admin 演示了 Item Collection Editor 模式如何应用于 DataGrid。它提供以下功能:

  • 注册新客户
  • 确定新客户的插入位置
  • 插入新客户时的选择行为
  • 修改客户
  • 删除客户(带删除确认)
  • 支持键盘输入(Insert、F2 和 Delete)
  • 支持鼠标双击

为了正确支持所需功能,编写了 DataGrid 的派生类 ModelDataGrid。除了 Commands ItemCreateItemEditItemDelete 之外,ModelDataGrid 还提供了键盘输入和鼠标点击处理(工具 MouseClickManager)所需的支持。

客户的编辑功能在 CustomerEditor 类中实现。CustomerAdminModel 结合了客户信息和编辑器,并充当视图的视图模型。

// ------------------------------------------------------------------------
public class CustomerAdminModel : ItemModel
{

  // ----------------------------------------------------------------------
  public CustomerAdminModel( ViewSyncMode syncMode )
  {
    RegisterDisposable( customers );
    editor = new CustomerAdminEditor( customers, syncMode );
    Load();
  } // CustomerAdminModel

  // ----------------------------------------------------------------------
  public ItemCollection<CustomerModel> Customers
  {
    get { return customers; }
  } // Customers

  // ----------------------------------------------------------------------
  public IItemEditor Editor
  {
    get { return editor; }
  } // Editor

  // ----------------------------------------------------------------------
  private void Load()
  {
  } // Load

  // ----------------------------------------------------------------------
  // members
  private readonly ItemCollection<CustomerModel> customers =
    new ItemCollection<CustomerModel>();
  private readonly CustomerAdminEditor editor;

} // class CustomerAdminModel

在将 CustomerAdminModel 作为 DataContext 分配给视图后,视图可以按以下方式使用这些元素:

<Button
  Content="New"
  Command="{Binding Editor.ItemCreateCommand}" />

<XamlControls:ModelDataGrid
  ...
  IsSynchronizedWithCurrentItem="True"
  ItemsSource="{Binding Customers}"
  SelectedItem="{Binding Editor.Item, Mode=TwoWay}"
  ItemCreateCommand="{Binding Editor.ItemCreateCommand}" />
  ...
</XamlControls:ModelDataGrid>

程序集浏览器

Assembly Browser 以层次结构列出应用程序的程序集,并为此使用了 Hierarchical Data Item Model。基类 ReflectionModel 及其各种派生类作为层次结构的基础。

Assembly Browser

HierarchicalDataItemModel 用作 ReflectionModel 的基类,使用 ItemCollection<ReflectionModel> 的列表声明,这导致 IUpdateable 的优化可用于所有列表的加载。

程序集数据显示在 TreeView 中。TreeView 控件不支持其数据的按需加载(延迟加载),因此通过派生类 ModeTreeView 使其能够实现此功能。基于动作 ILoadable,类 ModeTreeViewModelTreeViewItem 提供相应的支持。所需数据将在 TreeView 元素的构造期间加载(PrepareContainerForItemOverride)。有关此方法的更多信息可以在 Silverlight TreeView 高级场景 中找到。

此外,控件 ModeTreeView 提供了展开和折叠分支的命令,这些命令可以绑定到视图中任何支持 ICommand 的控件。

<Button
  Content="Expand"
  Command="{Binding ExpandCommand, ElementName=AssembliesTree}" />
<Button
  Content="Collapse"
  Command="{Binding CollapseCommand, ElementName=AssembliesTree}" />

<XamlControls:ModelTreeView
  x:Name="AssembliesTree"
  ItemsSource="{Binding Assemblies.Children}" />

订单浏览器

Order Browser 示例用于演示如何使用分层结构来管理以下对象:

  • 公司管理
  • 公司客户管理
  • 公司订单管理
  • 显示订单项

为了表示视图模型,使用了基类为 OrderModelBaseHierarchical Data Item Model 模式。

Order Browser

公司的表示在 CompanyModel 类中实现,该类除了公司数据之外,还充当客户和订单的纯结构元素。

// ----------------------------------------------------------------------
protected override bool DoLoad()
{
  Children.Add( new CustomerCollectionModel( this ) );
  Children.Add( new OrderCollectionModel( this ) );
  return true;
} // DoLoad

CustomerModel 类表示的客户将由容器类 CustomerCollectionModel 从数据源按需加载。

// ----------------------------------------------------------------------
protected override bool DoLoad()
{
  ILIst<ICustomer> customers = LoadCustomers(); // load from your data source
  foreach ( ICustomer customer in customers )
  {
    Children.Add( new CustomerModel( this, customer.FirstName, customer.LastName, customer.Address ) );
  }
  return true;
} // DoLoad

基类 HierarchicalDataItemModel 考虑 IUpdateable 并在 BeginUpdateEndUpdate 中执行方法 DoLoad

元素的编辑由视图的视图模型 OrderBrowserModel 中的 Item Editor Provider 定义。

// ------------------------------------------------------------------------
public class OrderBrowserModel : OrderModelBase
{

  // ----------------------------------------------------------------------
  public OrderBrowserModel()
  {
    // companies
    companies = new CompanyCollectionModel( this );
    Children.Add( companies );

    // editor
    RegisterDisposable( editor );
    editor.ItemChanged += ItemChanged;
    editor.RegisterEditor( typeof( CompanyCollectionModel ), new CompanyCollectionEditor() );
    editor.RegisterEditor( typeof( CompanyModel ), new CompanyEditor() );

    CustomerEditor customerEditor = new CustomerEditor();
    editor.RegisterEditor( typeof( CustomerCollectionModel ), customerEditor );
    editor.RegisterEditor( typeof( CustomerModel ), customerEditor );

    OrderEditor orderEditor = new OrderEditor();
    editor.RegisterEditor( typeof( OrderCollectionModel ), orderEditor );
    editor.RegisterEditor( typeof( OrderModel ), orderEditor );

    // presenter
    RegisterDisposable( presenter );
    presenter.RegisterPresenter( typeof( CompanyModel ), new ViewItemPresenter<CompanyInfoView>() );
    presenter.RegisterPresenter( typeof( CustomerModel ), new ViewItemPresenter<CustomerInfoView>() );
    presenter.RegisterPresenter( typeof( OrderModel ), new ViewItemPresenter<OrderInfoView>() );
    presenter.RegisterPresenter( typeof( OrderModel ), new ViewItemPresenter<OrderItemsInfoView>() );
  } // OrderBrowserModel

  // ----------------------------------------------------------------------
  public override string Name
  {
    get { return "Companies"; }
  } // Name

  // ----------------------------------------------------------------------
  public CompanyCollectionModel Companies
  {
    get { return companies; }
  } // Customers

  // ----------------------------------------------------------------------
  public IItemEditor Editor
  {
    get { return editor; }
  } // Editor

  // ----------------------------------------------------------------------
  public IItemPresenterProvider Presenter
  {
    get { return presenter; }
  } // Presenter

  // ----------------------------------------------------------------------
  private void ItemChanged( object sender, EventArgs e )
  {
    presenter.Item = editor.Item;
  } // ItemChanged

  // ----------------------------------------------------------------------
  // members
  private readonly CompanyCollectionModel companies;
  private readonly ItemEditorProvider editor = new ItemEditorProvider();
  private readonly IItemPresenterProvider presenter = new ItemPresenterProvider();

} // class OrderBrowserModel

ItemEditorProvider 中注册的编辑器可以用于单一类型或组合用于多种类型。示例中由 CustomerEditor 演示了这一点,它既用于 CustomerCollectionModel 类型,也用于 CustomerModel 类型。

为了确保编辑器的功能可以与 TreeView 结合使用,派生类 ModelTreeView(另请参阅示例 Assembly Browser)允许绑定到 Commands ItemCreateItemEditItemDelete

对于元素类型,相应的 Item PresentersItemPresenterProvider 中注册。此顺序演示了如何为元素使用多个 View Item Providers。通过 ItemChangedItemEditorProvider 的活动元素与 ItemPresenterProvider 同步。

视图的使用如下:

<Button
  Content="New"
  Command="{Binding Editor.ItemCreateCommand}" />
<Button
  Content="Edit"
  Command="{Binding Editor.ItemEditCommand}" />
<Button
  Content="Delete"
  Command="{Binding Editor.ItemDeleteCommand}" />
<Button
  Content="Refresh"
  Command="{Binding Editor.ItemRefreshCommand}" />

<XamlControls:ModelTreeView
  …
  ItemsSource="{Binding Children}"
  ItemTemplate="{StaticResource NameTemplate}"
  SelectedItem="{Binding Editor.Item, Mode=TwoWay}"
  ItemCreateCommand="{Binding Editor.ItemCreateCommand}"
  ItemEditCommand="{Binding Editor.ItemEditCommand}"
  ItemDeleteCommand="{Binding Editor.ItemDeleteCommand}"/>

<ItemsControl
  ItemsSource="{Binding Presenter.ItemContent}"/>

工具和环境

转换器

为了控制控件之间的依赖关系,使用了以下基本转换器:

ObjectToVisibleConverter 如果源未定义,则将目标元素的可见性更改为 Visibility.Visible
ObjectToCollapsedConverter 如果源未定义,则将目标元素的可见性更改为 Visibility.Collapsed
FlagToVisibleConverter 如果源是布尔值 true,则将目标元素的可见性更改为 Visibility.Visible
FlagToCollapsedConverter 如果源是布尔值 true,则将目标元素的可见性更改为 Visibility.Collapsed
UriToImageConverter Uri 转换为 BitmapImage

复合库开发

文章 .NET 时间段库 深入描述了复合库的开发。

在 Silverlight 和 Windows Phone 7 的复合开发中,Windows Phone 模拟器的调试器不考虑临时构建文件的设置。这可以通过以下后期构建事件来纠正:

xcopy "$(ProjectDir)obj\WindowsPhone.$(ConfigurationName)\$(ConfigurationName)\XapCacheFile.xml" "$(ProjectDir)obj\$(ConfigurationName)\XapCacheFile.xml" /Y
  • 2012年5月24日 - v1.1.0.0
    • 新实用程序 PropertyTool
  • 2012年5月14日 - v1.0.0.0
    • 首次公开发布
基于 XAML 应用程序的演示模式 - CodeProject - 代码之家
© . All rights reserved.