Silverlight 模式

Silverlight 2 应用程序中的 Model-View-ViewModel

Shawn Wildermuth

代码下载可从 MSDN 代码库
浏览代码联机

本文讨论:

  • Silverlight 2 开发
  • 模型-视图-ViewModel 模式
  • 视图和查看模型
  • concessions 向 Silverlight 2
本文涉及以下技术:
Silverlight 2,Visual Studio

内容

此问题
Layering 在 Silverlight 2 中的应用程序
MVVM: A Walk-Through
创建模型
视图和查看模型
concessions 向 Silverlight 2
我们

现在,在发布 Silverlight 2 在它的上面构建的应用程序数是增长,和与提供一些 growing pains。支持的 Silverlight 2 模板的基本结构表示您正在使用的任何数据的用户界面 (UI) 之间的紧密集成。用于学习该技术此紧密集成时就测试、 重构,和维护的障碍。我将介绍如何使用的应用程序设计的成熟的模式将在用户界面分开数据。

此问题

该问题的核心是为您的应用程序层混合在一起的结果的紧耦合。一层时 intimate 了解如何其他层执行其作业,然后应用程序紧密耦合。需要简单数据输入应用程序,允许您查询特定城市中的销售的家庭。紧密结合的应用程序中您可以定义查询以在您的用户界面中的按钮处理程序中执行搜索。架构更改或搜索的语义更改,数据层和用户接口层必须进行更新。

这提供代码质量和复杂性中的一个问题。每次将数据层更改,您必须同步和测试以确保所做的更改重要更改应用程序。所有为一起紧密绑定时, 应用程序的一部分中的任何移动导致 rippling 更改在代码的其余部分。在创建内容 (如电影播放机或菜单 Widget Silverlight2,简单,紧密离合器应用程序的组件不太可能有问题。但是,一个项目的大小增加会越来越感到在获得认可。

此问题的其他部分是单元测试。应用程序紧密耦合时, 您可以只执行功能 (或用户界面) 的应用程序测试。再次,这不是一个小的项目的问题,但随着项目增加的大小和复杂性,能够单独测试应用程序层将成为非常重要。请记住该单元测试不几乎确保单位您在系统中使用它,但有关确保继续在系统中工作时适用。有单元测试系统的部分添加保证随着系统的更改的问题是 revealed 先前在此过程中而不是更高版本 (如会发生与功能测试)。回归测试 (例如在每次生成系统上运行单元测试),然后将成为重要确保添加到系统的进行细微改动不打算导致级联的错误。

通过定义不同的层创建应用程序可能会出现一些开发人员的 over-Engineering 的情况。事实是,是否或不用记住的图层生成,您正在从事一个 n 物理) 层的平台和应用程序将具有层。但没有正式计划将得到了两种紧密耦合的系统,这些问题以前详细或应用程序的将是维护麻烦的型代码的完整。

它易于假定生成单独的图层的应用程序或层需要大量的基础结构以使其正常,但实际上,简单分离层是简单实现。(可以使用控件的技术的反转设计应用程序的更复杂的分层但与本文中讨论的解决另一个不同的问题)。

Layering 在 Silverlight 2 中的应用程序

Silverlight 2 不需要创造新以帮助您决定如何层应用程序。有一些已知的模式,可用于您的设计的。

用户接收大量有关的信息权限现在的模式是模型-视图-控制器 (MVC) 模式。该 MVC 模式中,模型都将数据视图处于用户界面,该控制器是编程接口,在视图、 模型和用户输入之间。这种模式但是,不起作用 (如 Windows Presentation Foundation (WPF) 或 Silverlight 的声明性的用户界面中因为这些技术所使用的 XAML 可以定义输入和视图之间的接口的一些 (因为在 XAML 中,可以声明数据绑定、 触发器和状态)。

模型-视图-演示者 (MVP) 是将应用程序的另一个常见模式。在 MVP 方式演示者负责设置和管理视图状态。像 MVC,MVP 不很适合在 Silverlight 2 模型因为 XAML 可能包含声明性数据绑定、 触发器,和状态管理。因此,,其中会我们保留?

幸运的是 Silverlight 2,WPF 社区已 rallied 后面称为模型-视图-ViewModel (MVVM) 模式。此模式是一个适配 MVC 和 MVP 模式的视图模型提供了数据模型和视图的行为,但允许视图以声明方式绑定到该视图模型。视图成为多种 XAML 和 C# (如 Silverlight 2 控制)、 模型表示可供应用程序,数据并查看模型准备模型中才绑定到该视图。

模型很尤其重要,因为它封装在访问数据,访问是通过一组 Web 服务、 的 ADO.NET 数据服务或数据检索的某些其他形式。该模型分开视图模型,以便在从实际数据的隔离,可以测试视图的数据 (视图模型)。图 1 显示 MVVM 图案的示例。

fig01.gif

图 1 模型视图 ViewModel 模式

MVVM: A Walk-Through

若要帮助您了解如何实现 MVVM 模式一点,让我们遍历一个示例。本示例不一定表示如何实际代码将使用。它只被旨在说明模式。

本示例由一个 Visual Studio 解决方案中五个单独的项目组成。(尽管不需要作为一个单独的项目中创建每个图层,它是通常一个不错的主意)。 通过将它们放在客户端和服务器的文件夹中,示例进一步分隔项目。服务器文件夹在两个项目: 一个 ASP.NET Web (MVVMExample) 承载应用程序将我们的 Silverlight 项目和服务和包含数据模型的.NET 项目。

客户端文件夹在三个项目: 一个 Silverlight 项目 (MVVM.client) 为我们的应用程序 Silverlight 客户端库 (MVVM.client.data) 的主 UI 包含该模型视图模型以及服务引用和 Silverlight 项目 (MVVM.client.tests) 包含单元测试。您可以看到在 图 2 中的这些项目的细分结构。

fig02.gif

图 2 项目布局

例如,我 ASP.NET、 实体框架和一个 ADO.NET 数据服务服务器上使用。实质上是,我具有一个简单的数据模型我通过基于 REST 的服务公开的服务器上。请 Silverlight 2 中, 使用 ADO.NET 数据服务,参阅我 2008 年 9 文章"数据服务: 创建使用 Silverlight 2 的数据中心 Web 应用程序"有关这些详细信息的详细说明。

创建模型

若要在我们的 Silverlight 应用程序中的分层我们首先需要定义 MVVM.client.data 项目中的应用程序数据模型。定义模型的一部分确定您要使用的应用程序内的实体的类型。实体的类型取决于应用程序如何将与服务器数据进行交互。是例如如果要使用 Web 服务,您的实体都可能会由创建服务引用到您的 Web 服务生成的数据合同类。或者,您可以使用简单的 XML 元素,如果要在您的数据访问检索原始 XML。此处,我使用一个 ADO.NET 数据服务因此创建时将对数据服务的实体的一组服务引用创建。

在此示例中,服务引用创建我们关心的三类: 游戏,供应商和 GameEntities (在上下文对象来访问数据服务)。游戏和供应商类是在实际的实体,我们用来与视图进行交互,GameEntities 类用于内部访问数据服务检索数据。

我们可以创建模型之前,但是,我们需要创建该模型和视图模型之间的通信界面。此接口通常包括任何方法、 属性,和访问数据所需的事件。此组功能由接口表示以允许它被替换其他实现根据需要 (测试,例如)。在此处,显示在此示例在模型界面称为 IGameCatalog。

public interface IGameCatalog
{
  void GetGames();
  void GetGamesByGenre(string genre);
  void SaveChanges();

  event EventHandler<GameLoadingEventArgs> GameLoadingComplete;
  event EventHandler<GameCatalogErrorEventArgs> GameLoadingError;
  event EventHandler GameSavingComplete;
  event EventHandler<GameCatalogErrorEventArgs> GameSavingError;
}

IGameCatalog 接口包含检索和保存数据的方法。 但是,不操作将返回实际的数据。 相反,它们具有相应的成功与失败的事件。 此行为使解决 Silverlight 2 要求的异步网络活动的异步执行。 虽然通常推荐在 WPF 中的异步设计,此特定的设计和 2 中工作 Silverlight 由于 Silverlight 2 需要 asynchronicity。

若要以便我们的接口的结果以调用方的通知将以发送请求的结果的事件中实现一个 GameLoadingEventArgs 类用于。 此类公开我们实体类型 (游戏) 包含结果的枚举列表的实体请求,呼叫者可以看到在下面的代码。

public class GameLoadingEventArgs : EventArgs
{
  public IEnumerable<Game> Results { get; private set; }

  public GameLoadingEventArgs(IEnumerable<Game> results)
  {
    Results = results;
  }
}

现在,我们接口我们定义我们可以创建模型类 (GameCatalog) 实现 IGameCatalog 接口的。 GameCatalog 类只是包装 ADO.NET 数据服务,以便当对数据的请求进入 (GetGames 或 GetGamesByGenre) 时, 执行该请求并引发数据 (或错误,) 包含一个发生时的事件。 此代码旨在简化对数据的访问不 imparting 任何特定知识向调用方。 类包括一个重载构造函数指定该的服务的 URI,但是,始终不需要,并可以在而实现作为配置元素。 图 3 显示了 GameCatalog 类的代码。

图 3 GameCatalog 类

public class GameCatalog : IGameCatalog
{
  Uri theServiceRoot;
  GamesEntities theEntities;
  const int MAX_RESULTS = 50;

  public GameCatalog() : this(new Uri("/Games.svc", UriKind.Relative))
  {
  }

  public GameCatalog(Uri serviceRoot)
  {
    theServiceRoot = serviceRoot;
  }

  public event EventHandler<GameLoadingEventArgs> GameLoadingComplete;
  public event EventHandler<GameCatalogErrorEventArgs> GameLoadingError;
  public event EventHandler GameSavingComplete;
  public event EventHandler<GameCatalogErrorEventArgs> GameSavingError;

  public void GetGames()
  {
    // Get all the games ordered by release date
    var qry = (from g in Entities.Games
               orderby g.ReleaseDate descending
               select g).Take(MAX_RESULTS) as DataServiceQuery<Game>;

    ExecuteGameQuery(qry);
  }

  public void GetGamesByGenre(string genre)
  {
    // Get all the games ordered by release date
    var qry = (from g in Entities.Games
               where g.Genre.ToLower() == genre.ToLower()
               orderby g.ReleaseDate
               select g).Take(MAX_RESULTS) as DataServiceQuery<Game>;

    ExecuteGameQuery(qry);
  }

  public void SaveChanges()
  {
    // Save Not Yet Implemented
    throw new NotImplementedException();
  }

  // Call the query asynchronously and add the results to the collection
  void ExecuteGameQuery(DataServiceQuery<Game> qry)
  {
    // Execute the query
    qry.BeginExecute(new AsyncCallback(a =>
    {
      try
      {
        IEnumerable<Game> results = qry.EndExecute(a);

        if (GameLoadingComplete != null)
        {
          GameLoadingComplete(this, new GameLoadingEventArgs(results));
        }
      }
      catch (Exception ex)
      {
        if (GameLoadingError != null)
        {
          GameLoadingError(this, new GameCatalogErrorEventArgs(ex));
        }
      }

    }), null);
  }

  GamesEntities Entities
  {
    get
    {
      if (theEntities == null)
      {
        theEntities = new GamesEntities(theServiceRoot);
      }
      return theEntities;
    }
  }
}

请注意则 ExecuteGameQuery 为方法这将 ADO.NET 数据服务查询,并执行它。 此方法将异步执行结果,并返回到调用方的结果。

请注意该模型执行查询,但只会触发事件完成时。 可以查看此并想知道该模型不确保事件封送 Silverlight 2 中用户界面线程在调用。 原因是 Silverlight (如其其他用户界面 brethren,如 Windows 窗体和 WPF) 可以只更新从主或 UI 线程在用户界面。 但如果我们在此代码的封送,会将我们模型用户界面是完全计数器我们规定的目的 (将该问题)。 如果您担任的数据需要在 UI 线程上返回,您将此类绑定到用户的接口调用,但这是 antithetical 您应用程序中使用单独的层的原因。

视图和查看模型

可能看起来以创建视图模型来公开数据直接到我们视图类的明显。 使用此方法问题是在查看模型应只公开直接需要在视图的数据,; 因此,您需要了解该视图所需要的内容。 在很多的情况下您将正在创建视图模型和并行,视图当视图具有新的要求时重构该视图模型。 尽管视图模型公开数据视图,视图正在还与实体类交互 (间接因为模型的实体被传递到视图查看模型)。

在此示例,我们有用于浏览 XBox 360 游戏的数据,如 图 4 所示的简单设计。 这种设计意味着我们需要游戏的实体的列表按流派 (通过下拉列表选择) 筛选我们模型中。 为满足此要求,则需要提供以下视图模型:

  • 数据绑定游戏列表的当前所选的流派。
  • 一种方法将所选的流派的请求。
  • 通知用户界面 (因为我们的数据请求将是异步) 已更新的游戏列表的事件。

fig04.gif

图 4 示例用户界面

一旦我们查看模型都支持此组的要求,它可将绑定向 XAML 直接,GameView.XAML (位于 MVVM.client 项目) 中所示。 此绑定是由创建视图的资源中的视图模型的一个新实例,然后绑主容器 (在这种情况下网格) 定到视图模型实现的。 这意味着在整个的 XAML 文件中将数据绑定基于视图模型直接。 图 5 显示了 GameView.XAML 代码。

图 5 GameView.XAML

// GameView.XAML
<UserControl x:Class="MVVM.Client.Views.GameView"
             xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:data="clr-namespace:MVVM.Client.Data;assembly=MVVM.Client.Data">

  <UserControl.Resources>
    <data:GamesViewModel x:Key="TheViewModel" />
  </UserControl.Resources>

  <Grid x:Name="LayoutRoot"
        DataContext="{Binding Path=Games, Source={StaticResource TheViewModel}}">
    ...
  </Grid>
</UserControl>

我们查看模型需要满足这些要求使用 IGameCatalog 接口。 一般情况下,其用于具有默认构造函数,查看模型的创建默认模型,以便绑定到 XAML 很容易,但是还应包括的模型提供以允许情况下 (例如测试构造函数的重载。 示例查看模型 (GameViewModel) 类似于 图 6

图 6 GameViewModel 类

public class GamesViewModel
{
  IGameCatalog theCatalog;
  ObservableCollection<Game> theGames = new ObservableCollection<Game>();

  public event EventHandler LoadComplete;
  public event EventHandler ErrorLoading;

 public GamesViewModel() : 
    this(new GameCatalog())
  {
  }

  public GamesViewModel(IGameCatalog catalog)
  {
    theCatalog = catalog;
    theCatalog.GameLoadingComplete += 
      new EventHandler<GameLoadingEventArgs>(games_GameLoadingComplete);
    theCatalog.GameLoadingError += 
      new EventHandler<GameCatalogErrorEventArgs>(games_GameLoadingError);
  }

  void games_GameLoadingError(object sender, GameCatalogErrorEventArgs e)
  {
    // Fire Event on UI Thread
    Application.Current.RootVisual.Dispatcher.BeginInvoke(() =>
      {
        if (ErrorLoading != null) ErrorLoading(this, null);
      });
  }

  void games_GameLoadingComplete(object sender, GameLoadingEventArgs e)
  {
    // Fire Event on UI Thread
    Application.Current.RootVisual.Dispatcher.BeginInvoke(() =>
      {
        // Clear the list
        theGames.Clear();

        // Add the new games
        foreach (Game g in e.Results) theGames.Add(g);

        if (LoadComplete != null) LoadComplete(this, null);
      });
  }

  public void LoadGames()
  {
    theCatalog.GetGames();
  }

  public void LoadGamesByGenre(string genre)
  {
    theCatalog.GetGamesByGenre(genre);
  }

  public ObservableCollection<Game> Games
  {
    get
    {
      return theGames;
    }
  }
}

查看模型中的特定感兴趣的处理程序个是 GameLoadingComplete (以及 GameLoadingError)。 这些处理程序从模型中接收事件,,然后会触发事件,该视图。 什么是有趣此处是该模型将视图模型结果,列表但而不是直接向基础的视图,请传递结果,在查看模型将结果存储自己可绑定列表 (ObservableCollection <game>) 中。

出现此问题的原因绑视图模型所定直接到视图使结果将显示在视图,通过数据绑定。 因为视图模型具有的用户界面 (因为它的目的是为满足 UI 请求),它可以然后确保它所触发的事件发生在 UI 线程上 (通过 Dispatcher.BeginInvoke,虽然可以使用其他方法调用在 UI 线程上,如果您愿意)。

concessions 向 Silverlight 2

整个巨大成功许多 WPF 项目使用 MVVM 模式。 在 Silverlight 2 中使用该问题是以使此模式简单无缝,Silverlight 2 真正需要支持的命令和触发器。 如果的案例,我们可能有与应用程序的用户交互时直接调用视图模型的方法的 XAML。

Silverlight 2 中此行为需要一些更多的工作,但幸运的是它涉及编写只有少的代码。 是例如当用户选择使用下拉列表的其他流派时, 我们想已为我们执行 GameViewModel.GetGameByGenre 方法的命令。 因为需要在基础结构不可用,我们只是必须使用代码来执行同样的操作。 当组合框 (genreComboBox) 选定内容更改,从视图游戏手动模型在代码而不是一个命令中的示例负载。 此处所需的是请求加载数据会发生,因为我们绑定到的游戏列表,基础的视图模型将更改我们绑定到集合和更新的数据只是自动显示。 您可以看到在 图 7 中的此代码。

图 7 更新用户界面中的数据

void genreComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
  string item = genreComboBox.SelectedItem as string;
  if (item != null)
  {
    LoadGames(item);
  }
}

void LoadGames(string genre)
{
  loadingBar.Visibility = Visibility.Visible;
  if (genre == "(All)")
  {
    viewModel.LoadGames();
  }
  else
  {
    viewModel.LoadGamesByGenre(genre);
  }

}

有多个位置的缺乏元素绑定和命令将强制 Silverlight 2 开发人员处理代码中的此问题。 因为代码是视图的一部分,这不会中断应用程序的分层,它不只是为您将看到在 WPF 中的所有 XAML 示例一样简单。

我们

Silverlight 2 不需要创建单一应用程序。 layering Silverlight 2 应用程序非常简单使用我们令人高兴的是从我们的 WPF brethren 借用的模型-视图-ViewModel 模式。 此外,使用该分层方法允许您松散结合应用程序中的职责,以便它们更易于维护、 将扩展测试,和部署。

我想感谢 Laurent Bugnion (作者 Silverlight 2 Unleashed) 以及其他 WPF Disciples 邮寄列表的这篇文章的帮助。 在 Laurent 博客 blog.galasoft.ch.

Shawn Wildermuth 是 Microsoft MVP (C#) 和 Wildermuth Consulting Services 的创始人。 他是几个书籍和许多文章的作者时。 此外,Shawn 当前运行周围国家讲授 Silverlight 2,Silverlight 教程。 他可以联系在 Shawn@wildermuthconsulting.com.