本文章是由機器翻譯。

Silverlight 的模式

模型-檢視-ViewModel 在 Silverlight 2 應用程式

Shawn Wildermuth

可從 MSDN 程式庫 的程式碼下載
瀏覽線上的程式碼

本文將告訴您:

  • Silverlight 2 開發
  • 模型-檢視-ViewModel 模式
  • 檢視及檢視模型
  • concessions Silverlight 2
本文將使用下列技術:
Silverlight 2,Visual Studio

內容

這個問題
在 Silverlight 2 Layering 的應用程式
A Walk-Through MVVM:
建立模型
檢視及檢視模型
concessions Silverlight 2
我們嗎

現在,Silverlight 2 發行的之上它建置應用程式數目就會是成長,以及與來自某些 growing pains。Silverlight 2 範本所支援的基本結構,表示在使用者介面 (UI) 」 與 「 您正在使用的任何資料之間的緊密整合。這種緊密整合有用學習技術時它會變成瓶頸,以測試、 重整,及維護。我將示範如何使用應用程式設計的成熟的模式,UI 分開資料。

這個問題

問題的核心會是緊密是因為您的應用程式的各層混合在一起的連結。當一層時 intimate 瞭解如何另一層執行其工作,然後您的應用程式緊密結合。需要簡單的資料項目應用程式,可讓您的家用特定城市的查詢。在緊密結合的應用程式,您無法定義查詢,以在按鈕處理常式,在您的使用者介面中執行搜尋。在結構描述變更或搜尋的語意的變更,兩者的資料層和使用者介面層必須加以更新。

這可以提供問題的程式碼品質和複雜性。每次資料層變更時,您必須同步處理,並測試應用程式,確定變更不會中斷變更。當緊密地一起繫結所有時,任何動作,在應用程式的一部分會造成 rippling 的變更,在其餘程式碼。當您建立如電影的播放程式或功能表 Widget 簡單 Silverlight2 中, 時,緊密連接應用程式的元件不太可能會有問題。專案的大小增加,但是,您會覺得在痛苦更多。

問題的其他部分則是單元測試。當應用程式緊密結合時,您可以只執行功能 (或使用者介面) 應用程式的測試。再次,這不是一個小型的專案的問題,但是會隨著專案會在大小和複雜度增加,能夠個別測試應用程式層變得非常重要。請記住,單元測試不只要在於確定單位運作當您在系統中使用它,但有關確定它繼續系統中的工作時。有系統的部分的單元測試會加入當系統變更,問題是 revealed 稍早在處理序中而非更新 (如會發生與功能性測試) 的保證。迴歸測試 (例如,在每個組建上的系統上執行單元測試) 然後會成為重要,以確保會加入至系統的小型變更不打算造成階層式的錯誤。

over-Engineering 的情況下,為某些開發人員可能會建立應用程式藉由定義不同的層級。事實上是是否或不使用層記住建置,您正在使用 N 層 (N-Tier) 的平台,您的應用程式將會有圖層。但沒有正式的計劃會最後使用非常緊密結合的系統 (和問題的詳細說明之前),或應用程式的完整的 spaghetti 程式碼將會在維護麻煩。

容易假設建置不同層級的應用程式或層需要大量的基礎結構,讓它正常,但是事實上,簡單層級區隔直接實作。(您可以設計應用程式的更複雜的分層,使用控制項的技巧的反向,但的位址不同的問題,不是本文所述)。

在 Silverlight 2 Layering 的應用程式

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 模式中檢視模型會提供一個資料模型和行為,以檢視,但是允許檢視,以宣告方式繫結至檢視模型的配接。檢視 (如 Silverlight 2 控制項) 會變成 XAML 和 C# 的混合模型表示讓的應用程式使用資料,檢視模型準備模型,以繫結至檢視。

此模型很特別重要,因為它包裝在存取資料,存取是透過一組 Web 服務、 的 ADO.NET 資料] 服務或其他形式的資料擷取。模型分開檢視模型,讓檢視的資料 (檢視模型) 可以測試的實際資料隔離。[圖 1 ] 顯示 MVVM 模式的範例。

fig01.gif

[圖 1 模型-檢視-ViewModel 模式

A Walk-Through MVVM:

若要協助您瞭解如何實作 MVVM 模式的資訊,讓我們逐步範例。這個範例不一定代表如何實際的程式碼會使用。它是只是為了說明模式。

這個範例是由一個單一的 Visual Studio 方案中的五個不同的專案所組成。(雖然您不需要以不同專案中建立每個圖層,不過它是通常最好)。 範例進一步分隔藉由在用戶端和伺服器的資料夾放置它們的專案。在 [伺服器] 資料夾中,是兩個專案: 一個 ASP.NET Web 應用程式 (MVVMExample),會將裝載我們的 Silverlight 專案 」 和 「 服務和 「.NET Library 專案包含之資料的模型。

在用戶端的資料夾中,是三個專案: 一個 Silverlight 專案 (MVVM.client) 我們的應用程式 Silverlight 用戶端程式庫 (MVVM.client.data) 的主要 UI 的模型和檢視模型以及服務的參考,包含和 Silverlight 專案 (MVVM.client.tests) 包含單元測試。您可以看到 [圖 2 ] 中,這些專案的分析]。

fig02.gif

[圖 2] 專案的版面配置

這個範例中,我用於 ASP.NET]、 [Entity Framework 和 [ADO.NET 資料服務伺服器。基本上,我有我透過 REST-基礎服務所公開的伺服器的簡單資料模型。請在 Silverlight 2,使用 ADO.NET 資料服務,參閱我的 9 月 2008 文件 」資料服務: 使用 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 的非同步設計,這個特定的設計會適用在 Silverlight 2 因為 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 (像是其其他使用者介面弟兄,例如 Windows Form 和 WPF) 可以只更新主要或 UI 執行緒的使用者介面。 但是,如果我們做在這段程式碼的封送處理,它會結合我們的模型,使用者介面,我們所述的用途 (區隔,考量) 是完全計數器。 如果您假設資料都需要在 UI 執行緒上傳回,您繫結至這個類別使用者的介面呼叫但 antithetical 您為什麼在應用程式中使用不同的層級。

檢視及檢視模型

似乎明顯地建立檢視模型,以公開資料直接到我們的檢視類別。 這種方法的問題,是檢視模型應該只公開所直接檢視所需資料,; 因此,您需要瞭解檢視所需要的項目]。 在許多情況下,您會建立檢視模型和平行,檢視檢視具有新的需求時,重整檢視模型。 雖然檢視模型會公開檢視資料,檢視也與互動實體) 類別 (間接因為模型的實體 (Entity) 被傳遞至檢視檢視模型所)。

在這個範例中,我們會有簡單的設計,用於瀏覽 Xbox 360 遊戲的資料,如 [圖 4 ] 所示。 這種設計,表示我們需要的遊戲的實體清單,從我們的模型篩選類型 (透過下拉式清單選取)。 若要滿足這個需求,此範例會需要檢視模型公開 (Expose) 下列:

  • 目前所選取的內容類型的資料繫結遊戲清單。
  • 提出要求的內容類型選取方法。
  • 事件通知 UI (因為我們的資料要求都將會非同步的),已更新的遊戲清單。

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 更新資料,在 UI 中

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 開發人員處理程式碼中的,這個問題。 因為程式碼將是您,檢視的一部分,這不會中斷應用程式的分層,它就無法為所有 XAML 範例,您會看到在 WPF 中,簡單。

我們嗎

Silverlight 2 不需要您建置整合型應用程式。 直接使用模型-檢視-ViewModel 模式,我們願意借用我們的 WPF 弟兄 layering Silverlight 2 應用程式。 此外,使用此分層方法,可讓您彈性 BackgroundWorker 責任,在您的應用程式中,以便更容易維護,擴充,測試,和部署。

我想要感謝 Laurent Bugnion (作者的 Silverlight 2 Unleashed) 以及其他 WPF Disciples 郵寄清單,協助完成本文上。 在 Laurent 的部落格 blog.galasoft.ch.

Shawn Wildermuth 是 Microsoft MVP (C# 中) Wildermuth Consulting Services) 的創始者。 他是數個書籍的許多文件作者。 此外,Shawn 目前執行,Silverlight 教學課程,教導 Silverlight 2 周圍國家 (地區)。 他可以在連絡 Shawn@wildermuthconsulting.com.