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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (26投票s)

2009年9月7日

CPOL

6分钟阅读

viewsIcon

119483

downloadIcon

385

回顾“来自俄罗斯的爱”技术,简化了从其他库创建ViewModel对象的过程,同时不损害MVVM架构

引言

本文探讨了一种将数据模型对象轻松转换为ViewModel对象,并将其安全地添加到可观察集合中,而不会在应用程序中引入尴尬的程序集依赖关系的方法。该技术利用通用委托和SynchronizationContext来保持程序集之间的松散耦合。这种数据类型转换方法适用于WPF和Model-View-ViewModel (MVVM)模式之外的领域,但本文重点介绍了它在基于MVVM构建的WPF应用程序中的使用。

我将这种技术昵称为“来自俄罗斯的爱”,因为它允许您方便地从应用程序中一个遥远的“异域”位置接收UI友好的对象。

背景

本文假设读者熟悉基于Model-View-ViewModel设计模式构建WPF应用程序。本文顶部的演示应用程序可供下载,它使用了我CodePlex上的MVVM Foundation库中的ObservableObjectRelayCommand类。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对象,其中每个按钮代表一个人,单击按钮会使应用程序“选择”该人(在此上下文中选择的含义与讨论无关)。

MainWindow.png

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,它显示CommunityViewModelMemberCommands属性中的每个CommandViewModel)。现在我们需要确定应用程序的这两个部分是如何连接在一起的。CommunityViewModel类需要以某种方式检索Person对象,并使用表示每个PersonCommandViewModel填充其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日 – 文章发表
© . All rights reserved.