拆分 ViewModel






4.17/5 (3投票s)
使用 Silvelight - RIA Services 实现 MVVM 设计模式。
引言
本文提供了一些关于 ViewModel 在 WPF/Silvelight 应用中的用法的见解。它还推导了一个实现 MVVM 的实用设计模式,该模式尤其适用于 Silverlight 和其他 RIA 应用程序。
必备组件
本文假设您对 Silverlight、MVVM 设计模式和 WCF RIA Services 有一定的了解。如果不是这样,请尝试在此处说明:维基百科上的 SilverLight,微软赞助的开发人员资源网站,MVVM 设计模式的解释和实现,请参阅微软文章,WCF RIA Services 资源。还建议您观看此视频,其中 Tim Huer 演示了 Silverlight 与 RIA Services 的使用。
MVVM 示例
假设我们有一个简单的模型,包含两个表:一个任务表,另一个子任务表。两个表都有一个记录 UID 字段(可能是一个 int
或 Guid
),子任务表有一个指向父任务的字段。现在,我们假设我们使用了我们最喜欢的 ORM 来创建相应的数据模型。这是使用 Entity Framework 为此类模型创建的图
暂时,让我们忽略整个 Silverlight 服务器-客户端的麻烦,而是考虑一个普通的 WPF ViewModel 类,我们将生成它来向我们的视图公开我们的任务。在此示例中,我们继承了一个 ViewModel 基类,该基类提供了一些框架服务(可能是 INotifiyPropertyChanged
接口的实现),将我们的模型作为构造函数参数,并将其暴露给直接绑定
public class TaskViewModel : ViewModelBase
{
Task _task;
public TaskViewModel(Task task)
{
_task = task;
}
public Task Task
{
get
{
return _task;
}
}
}
到目前为止,一切顺利。现在,假设我们的 UI 需要显示一个任务列表,其外观如下
没问题。任务列表视图可能有一个自己的 ViewModel,它将公开一个 IEnumerable<TaskViewModel>
。数据将使用对象模型检索,任务类将被包装在 TaskViewModel
类中并用于绑定,也许使用单例模式,如下所示
List<TaskViewModel> _taskViewModels;
public IEnumerable<TaskViewModel> TaskViewModels
{
get
{
if (_taskViewModels == null)
{
_taskViewModels = tasksContext.Tasks
.Select(task=> new TaskViewModel( task ) }).ToList();
}
return _taskViewModels;
}
}
DataGrid
或视图中的任何控件都将绑定到 IEnumerable
,一切都会顺利。
然而,此时,我们可能会被叫到老板的办公室,老板会“重新设计” UI,以便在显示的数据中包含每个任务的子任务数量
好的。没问题,我们可以像这样向 ViewModel 添加一个 SubtasksCount
属性
public class TaskViewModel : ViewModelBase
{
...
public int SubasksCount
{
get
{
return _task.Subtasks.Count();
}
}
}
重点来了:这一切都会奏效,而且对于具有本地数据访问的 WPF 应用程序来说,可能会相当好用。但是,这非常低效,因为绑定机制会调用此属性并为每个任务运行一个查询来查找其子任务的数量。更糟糕的是,在没有任何缓存机制的情况下,UI 可能会多次运行此每个任务的查询,因为 WPF 数据绑定是一种不时会过来看看有什么新东西的“女士”。当然,当每次查询都必须经历与服务器来回通信的过程时,情况会更糟。
请注意,我在这里假设子任务不会与任务一起加载;否则,我将使用 _task.Subtasks.Count
属性,并且不需要进一步查询。我的假设是,您不想加载所有子任务只是为了显示它们的数量。在某些情况下,您仍然会获取子任务,因此没有问题,但很容易看出,在某些场景下,有些事情最好留给 SQL Server 处理。
怎么办?
回到 SQL 思维。我们希望一次查询数据库,并检索任务数据和子任务的数量。例如(原谅我拙劣的 SQL)
SELECT Tasks .*,
(SELECT COUNT(1)
FROM Subtasks
WHERE (TaskID = Tasks.ID)) AS SubtasksCount
FROM Tasks
这的 LINQ 等效查询是带有投影的查询,如下所示
var tasks = tasksContext.Tasks
.Select(
task => new {
Task = task,
SubasksCount = task.Subtasks.Count() });
好多了。现在我们可以提供此任务集合作为绑定的源。但是我们的 ViewModel 有很多事情要做,所以我们不能仅仅使用 LINQ 为我们生成的匿名类型投影。我们必须使用我们的视图模型类并在查询中对其进行初始化。我们将用一个常规属性替换 Subtask
属性,该属性将为我们保存该值
public class TaskViewModel : ViewModelBase
{
public TaskViewModel()
{
}
public Task Task
{
get;
set;
}
public int SubasksCount
{
get;
set;
}
}
并以视图模型包装的数据
var tasks = tasksContext.Tasks.Select
(task => new TaskViewModel
{ Task = task,
SubasksCount = task.Subtasks.Count() });
请注意,新版本的 ViewModel 不再是不可变的,这并非最佳选择;然而,Entity Framework 只投影到带有无参构造函数的新对象,所以……
简而言之
每次查询服务器一条记录的数据,而这些数据本应与对象一起检索,这是低效的。这些数据可能是与子记录相关的数据,如本例所示,也可能来自父记录。可以获取其他实体,但在大多数现实场景中,当对象复杂且数量庞大时,这项工作应该留给查询(除非你确实需要以后使用这些对象)。
回到 Silverlight RIA Services
以上所有内容都适用于具有额外通信开销(时间和成本)的服务器-客户端模型。这就是为什么在 Silverlight 中构建 MVVM 应用程序时,应采用相同的方法。但是,在这里我们必须处理系统的架构。本文的其余部分将处理使用 Silverlight 和 RIA Services 实现此设计模式(如果您是 WPF 人员,可以停止阅读并跳转到结尾,在评论中抨击这个奇怪的想法)。
拆分 ViewModel
因此,我们希望通过 LINQ 查询数据模型来填充我们的 ViewModel 属性。但是,嘿,我们从 RIA 服务获取的数据是预先打包好的,形式是我们的模型(您可能已经注意到 DomainContext
公开的 EntitySet
集合有一个 Select()
扩展方法;不幸的是,这只能投影实体类型)。
看起来这正朝着将 ViewModel 定义在服务器上的奇怪方向发展,因为我们肯定会在客户端拥有它,而我们实际上正在分割 ViewModel,因此称为“Split ViewModel”。工作流程大致如下
- 创建数据模型(ORM)
- 创建 ViewModel 的服务器端
- 通过 RIA 服务公开 ViewModel
- 在客户端使用部分代理类作为 ViewModel 的基础,您将绑定 UI 到该 ViewModel
当然,您也可以公开 Model 本身(如果合适的话),适用于某些情况。
为了实现此设计模式,您可能需要了解一些内容,所以让我们继续以 Tasks 示例完成它,并以“Split ViewModel”风格完成
在服务器端创建 ViewModel
首先,服务器上创建的 TaskViewModel
应使用 DataContext
和 DataMember
属性进行准备,以便进行传输。此外,为了让 RIA Services 正确理解 Task
属性,我们必须用 System.ComponentModel.DataAnnotations
命名空间中的 AssociationAttribute
和 System.ServiceModel.DomainServices.Server
命名空间中的 IncludeAttribute
来装饰它。最后但同样重要的是,我们必须有一个用 Key
属性装饰的键属性。我使用了任务的 ID
[DataContract]
public class TaskViewModel : ViewModelBase
{
public TaskViewModel()
{
}
[DataMember]
[Association("TaskViewModel", "ID", "ID")]
[Include]
public Task Task
{
get;
set;
}
[DataMember]
public int SubasksCount
{
get;
set;
}
[DataMember]
[Key]
public Guid ID
{
get
{
return Task.ID;
}
set
{
}
}
}
通过 RIA 服务公开 ViewModel
要公开 ViewModel,域服务必须公开一个返回 IQueryable<TaskViewModel>
的方法。在此方法中,我们查询数据库并创建 ViewModel,包括子任务的数量
public IQueryable<TaskViewModel> GetTaskViewModels()
{
return this.ObjectContext.Tasks.Select
(task => new TaskViewModel()
{ Task = task,
SubasksCount = task.Subtasks.Count()
});
}
由于子任务计数是只读的,服务器 CUD 方法(创建、更新、删除)只需要对 ViewModel 中返回的任务对象执行操作
public void InsertTaskViewModel(TaskViewModel taskViewModel)
{
InsertTask(taskViewModel.Task);
}
public void UpdateTaskViewModel(TaskViewModel currentTaskViewModel)
{
UpdateTask(currentTaskViewModel.Task);
}
public void DeleteTaskViewModel(TaskViewModel taskViewModel)
{
DeleteTask(taskViewModel.Task);
}
此方法的另一种选择是从客户端调用 Task CUD 方法,而不公开 TaskViewModel 的 CUD 方法。这可能不太可取,因为您不得不在客户端混合一些关注点,并且对服务器端的控制较少。此外,如果您倾向于使用 DomainDataSource
对象,这将使其更难使用。
在客户端使用部分代理类作为 ViewModel 的基础,您将绑定 UI 到该 ViewModel
如果我们现在构建项目,我们会在服务器端获得代理。这意味着,我们可以用它做任何我们能用模型对象做的事情,包括,例如,将其从数据源窗格拖到页面上
从数据源窗格拖放视图可能不会产生一个非常可维护的应用程序,但为了演示目的,我们就这样做。工具将生成一个网格,其中每个暴露的属性(不包括任务)都有一个列。如果我们现在想添加一列显示任务名称,我们将需要绑定到其路径
<sdk:DataGrid AutoGenerateColumns="False"
ItemsSource="{Binding ElementName=taskViewModelDomainDataSource, Path=Data}"
Name="taskViewModelDataGrid" RowDetailsVisibilityMode="VisibleWhenSelected">
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn x:Name="iDColumn"
Binding="{Binding Path=ID, Mode=OneWay}" Header="ID"
IsReadOnly="True" Width="SizeToHeader" />
<sdk:DataGridTextColumn x:Name="taskNameColumn"
Binding="{Binding Path=Task.Text}" Header="Task" Width="SizeToHeader" />
<sdk:DataGridTextColumn x:Name="subasksCountColumn"
Binding="{Binding Path=SubasksCount}"
Header="Subasks Count" Width="SizeToHeader" />
</sdk:DataGrid.Columns>
</sdk:DataGrid>
当然,可以扩展客户端部分 ViewModel 代理以包含命令和其他有用的逻辑。
遗留问题
- 正如您可能已经注意到的,所涉及的技术让我以不恰当的方式公开了所有数据作为可设置的属性。这将使客户端设计人员负责设计视图以禁用不当访问。
- 使用客户端代理作为 ViewModel 意味着您无法派生自己的 ViewModel 基类。这是一个严重的问题,因为大多数 MVVM 框架的 ViewModel 基类都会完成一些工作。
最后说明
一方面,我觉得这一切听起来有点离谱,但另一方面,它似乎非常适合。ViewModel 是一个显示层元素,因此它应该是客户端元素,但似乎不将其部分移动到服务器,您就无法获得所需的东西。我很想听听您的想法——请投票并评论。