来自俄罗斯的爱 - 从模型程序集检索 ViewModel 对象






4.98/5 (26投票s)
回顾“来自俄罗斯的爱”技术,简化了从其他库创建ViewModel对象的过程,同时不损害MVVM架构
引言
本文探讨了一种将数据模型对象轻松转换为ViewModel对象,并将其安全地添加到可观察集合中,而不会在应用程序中引入尴尬的程序集依赖关系的方法。该技术利用通用委托和SynchronizationContext
来保持程序集之间的松散耦合。这种数据类型转换方法适用于WPF和Model-View-ViewModel (MVVM)模式之外的领域,但本文重点介绍了它在基于MVVM构建的WPF应用程序中的使用。
我将这种技术昵称为“来自俄罗斯的爱”,因为它允许您方便地从应用程序中一个遥远的“异域”位置接收UI友好的对象。
背景
本文假设读者熟悉基于Model-View-ViewModel设计模式构建WPF应用程序。本文顶部的演示应用程序可供下载,它使用了我CodePlex上的MVVM Foundation库中的ObservableObject
和RelayCommand
类。MVVM Foundation DLL已包含在演示项目中。
问题所在
在构建遵循MVVM设计模式的应用程序时,让Model和ViewModel类型存在于不同的程序集中通常很有用且有必要。检索数据对象(即Model对象)的类通常存在于一个不了解或无法访问应用程序ViewModel类型的程序集中。ViewModel类对Model类有深入的了解,并且通常会向检索和实例化Model数据的类发出请求。因此,ViewModel类所在的程序集引用了Model类所在的程序集,反之则不然。
这种配置是MVVM设计模式的自然和预期的副作用,其中Model类代表正在开发的系统的“纯”领域,而ViewModel是用户界面(UI)和数据模型之间的适配器。这种设置的一个结果是,ViewModel类通常承担创建包含一个或多个Model对象的ViewModel对象的任务。执行此任务的逻辑很容易导致ViewModel类中的代码膨胀,并可能赋予它们过多的“代码引力”——这意味着随着时间的推移,越来越多的代码最终会被添加到ViewModel类中。
一个更优的解决方案是将Model对象转换为ViewModel对象的任务分散到相关各方,以便各方都包含负责与其最适合的工作的可重用代码。当然,“最适合”这个术语表明这个决定有待解释。在本文的其余部分,我将探讨一种满足我对如何最好地解决这个问题看法的设计。如果本文中提出的选择不能满足您的应用程序需求,您可以自由放弃我的方法,采用最适合您特定需求的方法。
在继续之前,让我们首先更详细地回顾一下问题。以下是解决方案必须满足的要求列表
- ViewModel应该能够请求一组Model对象,提供一些将Model对象转换为ViewModel对象的逻辑,并且永远不必进行任何后续处理。
- 创建的ViewModel对象应该添加到
ObservableCollection<T>
中,其中T
是正在创建的ViewModel的类型。 - 可观察集合必须在UI线程上填充,根据WPF绑定系统施加的约束。
- 一旦所有数据对象都已检索并转换为ViewModel对象并添加到可观察集合中,发出数据检索请求的ViewModel对象应该收到通知。这使其有机会更新UI以指示数据加载过程已完成。
- 包含Model类型并将其转换为ViewModel对象的程序集不能引用包含ViewModel类型的程序集。
问题示例
假设模型程序集包含一个Person
类
/// <summary>
/// Model class that represents a person.
/// </summary>
public class Person
{
public Person(string firstName, string lastName)
{
this.FirstName = firstName;
this.LastName = lastName;
}
public string FirstName { get; private set; }
public string LastName { get; private set; }
}
现在想象一下,UI需要在一个列表中显示Person
对象,其中每个按钮代表一个人,单击按钮会使应用程序“选择”该人(在此上下文中选择的含义与讨论无关)。
UI中的每个按钮在被单击时都会执行其Command
属性引用的命令。这意味着每个Person
数据对象需要以某种方式与命令对象关联,以便当按钮被单击时,其命令知道选择了哪个人。为了满足此应用程序的要求,让我们创建一个CommandViewModel
类
public class CommandViewModel
{
public CommandViewModel(ICommand command, string displayName, object commandParameter)
{
this.Command = command;
this.DisplayName = displayName;
this.CommandParameter = commandParameter;
}
public ICommand Command { get; private set; }
public object CommandParameter { get; private set; }
public string DisplayName { get; private set; }
}
所有CommandViewModel
都由一个CommunityViewModel
对象(一个代表人群社区的类)存储在其MemberCommands
集合中。
/// <summary>
/// Returns a read-only collection of command objects, each of which represents a
/// member of the community. When a command executes, it 'selects' the associated member.
/// </summary>
public ReadOnlyObservableCollection<CommandViewModel> MemberCommands{ get; private set; }
现在让我们看看UI如何显示这些CommandViewModel
对象的集合
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.DataContext>
<CollectionViewSource Source="{Binding Path=MemberCommands}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="DisplayName" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</ItemsControl.DataContext>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button
Command="{Binding Command}"
CommandParameter="{Binding CommandParameter}"
Content="{Binding DisplayName}"
Margin="4"
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
此时我们了解了数据模型(Person
类)以及视图和ViewModel(一个ItemsControl
,它显示CommunityViewModel
的MemberCommands
属性中的每个CommandViewModel
)。现在我们需要确定应用程序的这两个部分是如何连接在一起的。CommunityViewModel
类需要以某种方式检索Person
对象,并使用表示每个Person
的CommandViewModel
填充其MemberCommands
集合。
解决方案
模型程序集中检索Person
数据的方法可以传递一个接受Person
参数并返回CommandViewModel
对象的委托。然而,由于模型程序集不能引用ViewModel程序集,有人可能会认为这会导致编译器错误,因为模型程序集不能引用ViewModel类型。这个问题可以通过使用通用委托来解决。由于委托的返回类型可以作为通用类型参数保持未指定,因此即使模型程序集用于创建它不了解的ViewModel类型,它也能成功编译。
这是来自模型程序集的PersonDataSource
,用于执行此任务
public class PersonDataSource
{
public void RetrieveAndTransformAsync<TOutput>(
ObservableCollection<TOutput> output,
Func<Person, TOutput> transform,
Action onCompleted)
{
if (output == null)
throw new ArgumentNullException("output");
if (transform == null)
throw new ArgumentNullException("transform");
// One possible improvement would be to create an overload of this method that
// has a SynchronizationContext parameter, so it can be invoked on any thread.
var syncContext = SynchronizationContext.Current;
if (syncContext == null)
throw new InvalidOperationException("...");
ThreadPool.QueueUserWorkItem(delegate
{
try
{
// Fetch the data objects.
var payload = RetrievePeople();
// Transform each data object and add it to the output collection,
// on the UI thread.
if (payload != null)
syncContext.Post(
arg => CreateOutput(payload, output, transform, onCompleted), null);
}
catch
{
// Implement error processing here...
}
});
}
static void CreateOutput<TPayload, TOutput>(
IEnumerable<TPayload> payload,
ObservableCollection<TOutput> output,
Func<TPayload, TOutput> transform,
Action onCompleted)
{
foreach (TPayload dataItem in payload)
{
TOutput outputItem = transform(dataItem);
output.Add(outputItem);
}
if (onCompleted != null)
onCompleted();
}
static Person[] RetrievePeople()
{
// Simulate network latency for the demo app...
Thread.Sleep(2500);
// In a real app this would call an external data service.
// The data access call would not have to be async, because
// this method is executed on a worker thread.
return new Person[]
{
new Person("Franklin", "Argominion"),
new Person("Douglas", "Mintagissimo"),
new Person("Mertha", "Laarensorp"),
new Person("Zenith", "Binklefoot"),
new Person("Tommy", "Frankenfield"),
};
}
}
PersonDataSource
类提供了数据检索和转换的支持。将每个Person
转换为ViewModel对象的逻辑通过transform
委托传递给它,该委托是RetrieveAndTransformAsync
方法的参数。请注意,它使用当前的SynchronizationContext
调度回UI线程,而不是使用WPF的Dispatcher
,以防此代码最终必须用于支持其他UI平台。我故意省略了任何错误处理逻辑,因为这种代码在不同的应用程序中可能差异很大。
现在是时候看看主EXE程序集中的CommunityViewModel
如何使用PersonDataSource
类了。
public CommunityViewModel()
{
_memberCommandsInternal = new ObservableCollection<CommandViewModel>();
this.MemberCommands =
new ReadOnlyObservableCollection<CommandViewModel>(_memberCommandsInternal);
// Create the command that is used by each CommandViewModel in MemberCommands.
_selectMemberCommand = new RelayCommand<Person>(this.SelectMember);
// Create the data source and begin a call to fetch and transform Person objects.
var dataSource = new PersonDataSource();
dataSource.RetrieveAndTransformAsync(
_memberCommandsInternal, // output
this.CreateCommandForPerson, // transform
() => this.IsLoadingData = false // onCompleted
);
this.IsLoadingData = true;
}
void SelectMember(Person person)
{
// In a real app this method would do something with the selected Person.
string msg = String.Format("You selected '{0}'.", FormatName(person));
MessageBox.Show(msg, "Congratulations!");
}
CommandViewModel CreateCommandForPerson(Person person)
{
return new CommandViewModel(_selectMemberCommand, FormatName(person), person);
}
static string FormatName(Person person)
{
return String.Format("{0}, {1}", person.LastName, person.FirstName);
}
请注意,CommunityViewModel
仅包含将Person
封装在CommandViewModel
中的必要逻辑,如其CreateCommandForPerson
方法所示。它不需要处理数据访问调用的完成、检查错误、将执行调度回UI线程以及遍历Person
对象列表。所有这些重复的后处理代码已方便地整合到PersonDataSource
中。我发现这种关注点分离在ViewModel类“做”得太多与Model类“知道”得太多之间取得了适当的平衡。
修订历史
- 2009年9月7日 – 文章发表