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






4.99/5 (44投票s)
WPF、Silverlight 和 Windows Phone 应用程序演示层上的设计模式。
引言
近年来,各种不同的模式,如 MVVM/MVC 和 Command,已成功应用于用户界面导向的应用程序开发中。本文描述了专门的设计模式,它们可用于基于 XAML 的应用程序的用户界面开发。
通过一个示例库,将使用几个日常应用示例来演示这些设计模式,并说明如何将它们编码为可重用组件。
除了增加重用性之外,这些设计模式还有助于降低系统的复杂性。它们通过促进参与设计人员的共同理解来支持未来的开发设计过程。
在设计模式的实现过程中,必须考虑通信、性能、资源管理以及 WPF/Silverlight/Windows Phone 7 复合开发等方面的各种问题。将涉及以下领域:
- 动作和同步/异步命令
- 绑定到 UI 元素的对象容器的性能
将 ObservableCollection 性能提高多达 1000% - 同步/异步数据加载
- 按需加载数据
使用延迟加载扩展 TreeView - 用于导航和编辑的容器视图
- 控制集合中的选择
跟踪 ListBox、ComboBox 和 DataGrid 选择 - 项目模型 – 视图模型抽象
- 不同类型对象的层次管理
- 用于编辑元素的编辑器
使用 CRUD 操作增强 TreeView 和 DataGrid
如前所述,本文中介绍的库应被视为在演示层上实现给定设计模式的一种可能变体。为了在此广泛的主题上保持对重要方面的关注,没有使用额外的工具和组件,并且只使用了标准的 .NET 框架项目。尽管该库可以按原样使用,但仍建议根据目标领域进行调整。
设计模式可以单独查看,并可用于现有架构。
动作和命令
操作
一个 Action 描述了一个简单且自包含的操作,并将其实现开放。它充当 Action Provider 和 Action Consumer 之间的契约。
一个 Action 的主要特征是它专注于实际情况或操作。几个动作可以通过主题组合成一个 Action Group,尽管它们通常彼此之间没有其他关系。目标是减少依赖关系以降低整体复杂性。Actions 和 Action Groups 可以看作是构建块。它们的组合将由 Action Consumer 决定,例如视图模型。
以下示例演示了 Action 模式的用法:
由于 Action 是一个中立的定义,因此可以开发标准化的组件,然后将其用于不同的应用程序。在接下来的过程中,我们将使用一个包含以下 Action Groups 和 Actions 的库:
动作组 | 操作 |
Data |
|
项目 |
|
媒体 |
|
导航 |
|
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 ,命令为同步和异步执行环境提供了通用基础。通过注册 Started 和 Ended 事件,外部观察者可以跟踪执行状态。 |
状态 | 命令的可用性通过 IsAvailable 发出信号。此处实现的变体确保命令在执行期间不可用。方法 Refresh 可用于显式刷新可用性状态。 |
状态变化 | 为了传达值的变化,支持 .NET 接口 INoftifyPropertyChanged 。 |
资源管理 | 为了确保正确释放任何持有的资源,实现了 .NET 接口 IDisposable 。 |
为了简化多个命令的处理(执行控制、资源管理等),它们可以注册在 ICommandCollection
中。
动作命令
Action Command 设计模式结合了 Action 和 Command 模式。通常,一个 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>
- 排序:
ISortable
、IComparisonSortable<TItem>
和IComparerSortable<TItem>
- 消息传递:
INotifyCollectionChanged
和INotifyPropertyChanged
- 数据更新:
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 元素(ListBox
、DataGrid
、TreeView
等)旨在处理元素列表。属性 ItemsSource
将列表绑定到 UI 元素。如果在运行时列表的内容发生变化,这将反映在相应的 UI 元素中。此机制基于接口 INotifyCollectionChanged
。列表(在本例中为 ObservableCollection
)实现了 INotifyCollectionChanged
,并且 UI 元素检查数据源是否实现了此接口。如果是这种情况,列表的通知将被接收并显示。事件序列如下所示:
为了减少容器和 UI 元素之间的通信量,Item Collection 使用更新机制扩展了标准列表,该机制由操作 Updateable
描述。这将导致以下事件序列:
通过方法 BeginUpdate
和 EndUpdate
(向 VisualBasic 致敬),发送到 UI 元素的通知将手动协调。在这两个时间点之间,可以在容器上执行任意数量的任意操作(Add
、Replace
、Sort
、Clear
...),而 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
的使用,应遵循以下建议:
- 对于加载、合并等批量操作最有效。
- 在对单个列表元素进行
Add
、Insert
或Remove
操作时没有意义。 - 调用必须始终保持平衡,这在异常处理中应予以考虑。
测量结果表明,根据使用场景,使用 Item Collection 可以极大地提高性能。但是,无法给出关于性能提升的通用声明,因为这取决于各种因素:
- 更新序列期间操作的数量和顺序
- 绑定方法:
DependecyProperty
或INotifiyPropertyChanged
- UI 元素渲染绑定值的方式
- WPF 和 Silverlight 的 UI 元素实现不同
- Silverlight 运行时:在浏览器中或浏览器外
为了获得关于可实现的性能提升的可靠声明,需要在目标使用场景中进行单独的测试运行。
以下结果汇编显示了绑定到 ListBox
的 Item 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% |
WPF ListBox
的测量结果显示,特别是在使用 ICollectionView
和大量项目时,性能可以显著提高。Silverlight ListBox
也显示出显著改进,而在 Windows Phone 7 上,优化几乎难以察觉。
下表显示了 DataGrid Binding 示例的结果,该示例测量了绑定到 DataGrid
的 Item 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 通过以下功能支持此类情况:
与 .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.Synchronous
:CurrentItem
的调整同步发生ViewSyncMode.Asynchronous
:CurrentItem
的调整异步发生
当视图的 Items
和 CurrentItem
绑定到同一个 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 模式的用法。
视图模型
以下章节介绍了几种表示视图模型的模式。
项目模型
Item Model 设计模式用于描述为在 UI 中表示而设计的对象。UI 元素和 Item Model 之间的通信通过动作(参见命令章节)和属性绑定发生。绑定用于在 UI 中显示属性值并更改它们。值的绑定通过 DependencyProperty
DP 或借助接口 INotifyPropertyChanged
INPC 进行。
哪种变体更有意义取决于具体情况。一个通用建议是,DP 主要用于 UI 元素,而 INPC 用于以数据为中心的对象。由于 Item Model 是一个应该涵盖这两个类别的构造,因此并非总是能够做出明确的选择。以下概述提供了一些帮助,用于决定哪种方法是合适的,并列出了几个方面及其各自的优缺点:
方面 | 声明 | DP | INPC |
继承 | DP 要求从基类 DependencyObject 继承,这并非总是可取或可实现的。DependencyObject 的 WPF 变体密封了方法 Equals 和 GetHashCode ,这可能导致派生中的不良限制。 |
- | + |
只读值 | DP 的值始终可以通过绑定修改。但是,有些值应该只由项本身更改,例如加载状态。在这种情况下,应该使用 INPC。 | - | + |
多线程 | 由于基类 DependencyObject ,DP 应该只在单个线程中使用。 |
- | + |
序列化 | .NET 序列化无法对 DP 基类 DependencyObject 进行序列化。这可以通过 XamlReader 和 XamlWriter 工具规避。 |
- | + |
触发器 | 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.ItemModel
和 Notifiable.ItemModel
。
以下派生演示了 Item Model、Action 和 Command 设计模式的组合。
// ------------------------------------------------------------------------
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 Model 是 Data Model 的专门化,增加了支持加载操作的功能。
// ------------------------------------------------------------------------
public interface IDataItemModel : IItemModel, ILoadable
{
// ----------------------------------------------------------------------
event EventHandler Loading;
// ----------------------------------------------------------------------
event EventHandler Loaded;
} // interface IDataItemModel
Data Item Model 支持其数据的同步和异步加载。与 Item Model 对应,提供了两种实现变体 Dependency.DataItemModel
和 Notifiable.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 应用程序的对象在分层显示中:
下图显示了一个简单订单管理的可能分层结构:
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.HierarchicalItemModel
和 Notifiable.HierarchicalItemModel
。
分层数据项模型
分层数据项模型结合了数据项模型和分层项模型的模式,并允许管理其元素可以动态加载的分层结构。
// ------------------------------------------------------------------------
public interface IHierarchicalDataItemModel<TItem> : IDataItemModel, IHierarchicalItemModel<TItem>
where TItem : IDataItemModel, IHierarchicalDataItemModel<TItem>
{
} // interface IHierarchicalDataItemModel
Hierarchical Data Item Model 的用法将在后面的示例 Assembly Browser 和 Order Browser 中详细描述。
HierarchicalDataItemModel
的实现演示了如何开发一个用于通用用途的类。根据其声明,元素必须支持 ILoadable
接口(通过 IDataModel
)。为了不失去 Item Collection 提供的更新机制的优势,该类在加载时检查元素是否支持 IUpdateable
。在这种情况下,加载过程还支持 BeginUpdate
和 EndUpdate
。
视图编辑器
项目编辑器
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 Editor 是 Item 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
使用方法 BuildContent
,Item 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
如何在现有控件 MediaPlayer
和 Button
之间搭建桥梁,以控制与 Action 和 Command 的交互。
MediaPlayerController
控制一个 MediaPlayer
,并且没有自己的视觉表示。属性 ElementName
将 MediaPlayerController
与 MediaPlayer
连接起来。
<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 Binding 和 GridView Binding 演示了列表与相应 UI 元素的绑定。它们的主要用途是测量加载性能(参见 Item Collection 一章)。可以调整以下运行时方面:
- 数据源的类型:
IItemCollection
或ICollectionView
(仅适用于 WPFListBox
) - 绑定到 Item Model 的类型:
DependencyProperty
或INotifyPropertyChanged
- 项目数量
- 更新模式 – 支持或不支持
IUpdateable
示例 GridView Binding 仅适用于 WPF 和 Silverlight。
选择
示例 Selection 使用 ListBox
和 DataGrid
控件演示了如何使用 Item Collection Selection 模式处理多重选择。
控件与 ItemCollectionSelection
的连接通过辅助元素 SelectorSelection
(ListBox
,ComboBox
)以及 DataGridSelection
(DataGrid
)进行。
<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 ItemCreate
、ItemEdit
和 ItemDelete
之外,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
及其各种派生类作为层次结构的基础。
类 HierarchicalDataItemModel
用作 ReflectionModel
的基类,使用 ItemCollection<ReflectionModel>
的列表声明,这导致 IUpdateable
的优化可用于所有列表的加载。
程序集数据显示在 TreeView
中。TreeView
控件不支持其数据的按需加载(延迟加载),因此通过派生类 ModeTreeView
使其能够实现此功能。基于动作 ILoadable
,类 ModeTreeView
和 ModelTreeViewItem
提供相应的支持。所需数据将在 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 示例用于演示如何使用分层结构来管理以下对象:
- 公司管理
- 公司客户管理
- 公司订单管理
- 显示订单项
为了表示视图模型,使用了基类为 OrderModelBase
的 Hierarchical Data Item Model 模式。
公司的表示在 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
并在 BeginUpdate
和 EndUpdate
中执行方法 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 ItemCreate
、ItemEdit
和 ItemDelete
。
对于元素类型,相应的 Item Presenters 在 ItemPresenterProvider
中注册。此顺序演示了如何为元素使用多个 View Item Providers。通过 ItemChanged
,ItemEditorProvider
的活动元素与 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
- 首次公开发布