程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 利用 Windows 8 功能和 MVVM

利用 Windows 8 功能和 MVVM

編輯:關於.NET

Windows 8 引入了許多新功能,開發人員可利用這些功能創建引人注目的應用程 序和形式豐富的 UX。遺憾的是,這些功能並非總是易於進行單元測試。共享和輔助磁貼等功 能可提高應用程序的互動性和趣味,但也會變得不太易於測試。

在本文中,我將介紹 讓應用程序可使用共享、設置、輔助磁貼、應用程序設置和應用程序存儲等功能的多種不同 方式。通過使用模型-視圖-視圖模型 (MVVM) 模式、依賴注入和某些抽象,我將向您演示如 何利用這些功能,同時將表示層保持易於進行單元測試。

關於示例應用程序

為了說明將在本文中談論的概念,我已使用 MVVM 編寫了一個示例 Windows 應用商 店應用程序,用戶使用它可通過其喜愛的博客的 RSS 源查看博客文章。該應用程序說明了如 何:

通過“共享”超級按鈕與其他應用程序共享有關某篇博客文章的信息

用“設置”超級按鈕更改用戶要閱讀的博客

用輔助磁貼將喜愛的博客文章固定到“開始”屏幕供以後閱讀

保存喜愛的博客以供在所有具有漫游設置的設備上查看

除了該示例應用程序,我還使用了將在本文中談論的特定 Windows 8 功能,並將其抽象 化為一個名為 Charmed 的開源庫。Charmed 可用作幫助程序庫或僅用作參考。Charmed 的目 標是成為一個適用於 Windows 8 和 Windows Phone 8 的跨平台 MVVM 支持庫。我將在以後 的文章中詳細談論該庫的 Windows Phone 8 一面。可在 bit.ly/17AzFxW 了解 Charmed 庫的進展。

我 對於本文和示例代碼的目標是演示我使用 Windows 8 提供的某些新功能開發采用 MVVM 模式 的可測試應用程序的方法。

MVVM 概述

在深入探討代碼和特定 Windows 8 功能之前,我將簡要介紹一下 MVVM。MVVM 是近年來在基於 XAML 的技術方面廣受青睐的一 種設計模式,這些技術包括 Windows Presentation Foundation (WPF)、Silverlight、 Windows Phone 7、Windows Phone 8 和 Windows 8(Windows Runtime,簡稱 WinRT)。 MVVM 將應用程序的體系結構劃分為三個邏輯層: 模型、視圖模型和視圖,如圖 1 所示。

圖 1:模型-視圖- 視圖模型的三個邏輯層

模型層涉及應用程序的業務邏輯,即業務對象、數據驗證、數 據訪問等。實際上,模型層通常分為更多層,甚至可能分為多個層級。如圖 1 所示,模型層 是應用程序在邏輯意義上的底部,或稱基礎。

視圖模型層容納應用程序的表示邏輯, 其中包括要顯示的數據、幫助啟用 UI 元素或使其可見的屬性以及將同時與模型層和視圖層 進行交互的方法。基本上,視圖模型層是對於 UI 當前狀態的一種與視圖無關的表示形式。 我說“與視圖無關”是因為它僅僅為要與之交互的視圖提供數據和方法,而不指示該視圖將 如何表示數據,也不允許用戶與這些方法進行交互。如圖 1 所示,視圖模型層在邏輯上位於 模型層與視圖層之間,並可與後兩者交互。視圖模型層包含以前將位於視圖層的隱藏代碼中 的代碼。

視圖層包含應用程序的實際表示形式。對於基於 XAML 的應用程序,如 Windows Runtime 應用程序,視圖層主要(如果不是全部)由 XAML 構成。視圖層利用強大 的 XAML 數據綁定引擎綁定到視圖模型上的屬性,同時將某種外觀應用於在其他情況下沒有 可視化表示形式的數據。如圖 1 所示,視圖層是應用程序在邏輯意義上的頂部。視圖層直接 與視圖模型層交互,但對模型層一無所知。

MVVM 模式的主要用途是將應用程序的表 示形式與其功能相分離。這樣做使應用程序對於單元測試更加有益,因為功能現在位於普通 舊 CLR 對象 (POCO) 中,而非自行決定生命周期的視圖中。

合約

Windows 8 引入了合約的概念,即兩個或更多應用程序對於用戶系統達成的協議。這些合約使所有應 用程序保持一致,並使開發人員可從任何支持功能的應用程序中利用這些功能。應用程序可 在 Package.appxmanifest 文件中聲明其支持的合約,如圖 2 所示。

圖 2: Package.appxmanifest 文件中的合約

雖然支持合約並非必需,但一般來說這樣做是 個好主意。尤其有三個合約應被應用程序支持:“共享”、“設置”和“搜索”,因為始終 可通過超級按鈕菜單使用這三項,如圖 3 所示。

圖 3:超級按鈕菜 單

我將重點介紹兩種合約類型: “共享”和“設置”。

共享

通過 “共享”合約,應用程序可與用戶系統中的其他應用程序共享特定於上下文的數據。“共享 ”合約有兩個方面: 源和目標。源是進行共享的應用程序。它以所需的任何格式提供一些要 共享的數據。目標是接收共享數據的應用程序。由於用戶始終可通過超級按鈕菜單使用“共 享”超級按鈕,因此我希望示例應用程序至少是一個共享源。並非每個應用程序都需要成為 共享目標,因為並非每個應用程序都需要接受來自其他源的輸入。但是,很有可能任何給定 應用程序將至少有一件事值得與其他應用程序共享。因此,大部分應用程序很可能將發現成 為共享源很有用。

當用戶按“共享”超級按鈕時,一個名為共享代理的對象即開始此 過程:取得某個應用程序共享的數據,然後將這些數據發送到用戶指定的共享目標。有一個 名為 DataTransferManager 的對象,我可使用它在該過程中共享數據。 DataTransferManager 有一個名為 DataRequested 的事件,當用戶按“共享”超級按鈕時引 發該事件。以下代碼演示如何引用 DataTransferManager 和訂閱 DataRequested 事件:

          public void Initialize()
{
  this.DataTransferManager = DataTransferManager.GetForCurrentView();
  this.DataTransferManager.DataRequested += 
    this.DataTransferManager_DataRequested;
}
private void DataTransferManager_DataRequested(
  DataTransferManager sender, DataRequestedEventArgs args)
{
  // Do stuff ...
          }

調用 DataTransferManager.GetForCurrentView 將返回對當前視圖的 活動 DataTransferManager 的引用。雖然可將這段代碼放入視圖模型,但它將產生 DataTransferManager 的強依賴項,一個無法在單元測試中模擬的密封類。由於我確實希望 盡可能可測試我的應用程序,因此這不是理想情況。一個更好的解決方案是將 DataTransferManager 交互抽象化為一個幫助程序類,並為該幫助程序類定義一個要實現的 接口。

將此交互抽象化之前,我必須決定哪些部分真正重要。在與 DataTransferManager 的交互中,有三個部分引起我的關注:

激活我的視圖時訂閱 DataRequested 事件。

停用我的視圖時取消訂閱 DataRequested 事件。

可向 DataPackage 添加共享數據。

考慮到這三點,我的接口具體形式為:

          public interface IShareManager
{
  void Initialize();
  void Cleanup();
  Action<DataPackage> OnShareRequested { get; set; }
}

Initialize 應引用 DataTransferManager 並訂閱 DataRequested 事件。 Cleanup 應取消訂閱 DataRequested 事件。可在 OnShareRequested 中定義在引發 DataRequested 事件後調用什麼方法。現在我可以實現 IShareManager,如圖 4 所示。

圖 4:實現 IShareManager

          public sealed class ShareManager : IShareManager
{
  private DataTransferManager DataTransferManager { get; set; }
  public void Initialize()
  {
    this.DataTransferManager = DataTransferManager.GetForCurrentView();
    this.DataTransferManager.DataRequested +=
      this.DataTransferManager_DataRequested;
  }
  public void Cleanup()
  {
    this.DataTransferManager.DataRequested -=
      this.DataTransferManager_DataRequested;
  }
  private void DataTransferManager_DataRequested(
    DataTransferManager sender, DataRequestedEventArgs args)
  {
    if (this.OnShareRequested != null)
    {
      this.OnShareRequested(args.Request.Data);
    }
  }
  public Action<DataPackage> OnShareRequested { get; set; }
}

當引發 DataRequested 事件時,所得的事件參數包含 DataPackage。需要在該 DataPackage 中放置實際的共享數據,而這正是 OnShareRequested 的 Action 采用 DataPackage 作為參數的原因。通過定義 IShareManager 接口並由 ShareManager 實現它, 現已准備好在視圖模型中加入共享,同時不會無法進行我以之為目標的單元測試。

使用特選的控制反轉 (IoC) 容器向視圖模型注入 IShareManager 實例後,即可將該模型 投入使用,如圖 5 所示。

圖 5:接通 IShareManager

          public FeedItemViewModel(IShareManager shareManager)
{
  this.shareManager = shareManager;
}
public override void LoadState(
  FeedItem navigationParameter, Dictionary<string, 
  object> pageState)
{
  this.shareManager.Initialize();
  this.shareManager.OnShareRequested = ShareRequested;
}
public override void SaveState(Dictionary<string, 
  object> pageState)
{
  this.shareManager.Cleanup();
}

在激活頁面和視圖模型時調用 LoadState,在停用頁面和視圖模型時調用 SaveState。既然 ShareManager 已設置妥當並准備好處理共享,那麼我需要實現將在用戶發 起共享時調用的 ShareRequested 方法。我要共享有關某篇特定博客文章 (FeedItem) 的一 些信息,如圖 6 所示。

圖 6:填充 ShareRequested 上的 DataPackage

          private void ShareRequested(DataPackage dataPackage)
{
  // Set as many data types as possible.
          dataPackage.Properties.Title = this.FeedItem.Title;
  // Add a Uri.
          dataPackage.SetUri(this.FeedItem.Link);
  // Add a text-only version.
          var text = string.Format(
    "Check this out!
          {0} ({1})", 
    this.FeedItem.Title, this.FeedItem.Link);
  dataPackage.SetText(text);
  // Add an HTML version.
          var htmlBuilder = new StringBuilder();
  htmlBuilder.AppendFormat("<p>Check this out!</p>", 
    this.FeedItem.Author);
  htmlBuilder.AppendFormat(
    "<p><a href='{0}'>{1}</a></p>", 
    this.FeedItem.Link, this.FeedItem.Title);
  var html = HtmlFormatHelper.CreateHtmlFormat(htmlBuilder.ToString());
  dataPackage.SetHtmlFormat(html);
}

我決定共享多種不同的數據類型。一般來說這是個好主意,因為無法控制用戶在 其系統中擁有什麼應用程序或這些應用程序支持什麼數據類型。請記住,共享本質上是一種 即發即棄的方案,這一點很重要。您不知道用戶將決定與什麼應用程序進行共享以及該應用 程序將對共享數據做什麼。為了與盡可能最廣泛的受眾進行共享,我提供一個標題、一個 URI、一個僅文本版本和一個 HTML 版本。

設置

通過“設置”合約,用戶可更改應用程序中特定於上下文的設置。這些設置可影響整個應 用程序,也可僅影響與當前上下文相關的特定項。Windows 8 的用戶將習慣於使用“設置” 超級按鈕對應用程序作出更改,而我希望示例應用程序支持該超級按鈕,因為用戶始終可通 過超級按鈕菜單使用它。實際上,如果應用程序通過 Package.appxmanifest 文件聲明 Internet 功能,則它必須通過在“設置”菜單中的某處提供基於 Web 的隱私策略的鏈接, 實現“設置”合約。由於使用 Visual Studio 2012 模板的應用程序在產生後即自動聲明 Internet 功能,因此不應忽視這一點。

當用戶按“設置”超級按鈕時,操作系統開始動態生成將顯示的菜單。菜單和關聯的浮出 控件由操作系統控制。我無法控制菜單和浮出控件的外觀,但我可向菜單添加選項。一個名 為 SettingsPane 的對象將在用戶選擇“設置”超級按鈕時通過 CommandsRequested 事件通 知我。引用 SettingsPane 和訂閱 CommandsRequested 事件頗為簡單:

          public void Initialize()
{
  this.SettingsPane = SettingsPane.GetForCurrentView();
  this.SettingsPane.CommandsRequested += 
    SettingsPane_CommandsRequested;
}
private void SettingsPane_CommandsRequested(
  SettingsPane sender, 
  SettingsPaneCommandsRequestedEventArgs args)
{
  // Do stuff ...
          }

麻煩的是這又會產生一個硬依賴項。這次,依賴項是 SettingsPane, 它又是一個無法模擬的類。由於我希望能夠對使用 SettingsPane 的視圖模型進行單元測試 ,因此我需要將對它的引用抽象化,如同我對於對 DataTransferManager 的引用所做的一樣 。結果證明,我與 SettingsPane 的交互與我與 DataTransferManager 的交互非常類似:

訂閱當前視圖的 CommandsRequested 事件。

取消訂閱當前視圖的 CommandsRequested 事件。

在引發該事件時添加我自己的 SettingsCommand 對象。

因此,我需要抽象化的接口與 IShare­Manager 接口非常類似:

          public interface ISettingsManager
{
  void Initialize();
  void Cleanup();
  Action<IList<SettingsCommand>> OnSettingsRequested { get; set; }
}

Initialize 應引用 SettingsPane 並訂閱 CommandsRequested 事件。Cleanup 應取消訂閱 CommandsRequested 事件。可在 OnSettingsRequested 中定義在引發 CommandsRequested 事件後調用什麼方法。現在我可以實現 ISettings­Manager,如 圖 7 所示。

圖 7:實現 ISettingsManager

          public sealed class SettingsManager : ISettingsManager
{
  private SettingsPane SettingsPane { get; set; }
  public void Initialize()
  {
    this.SettingsPane = SettingsPane.GetForCurrentView();
    this.SettingsPane.CommandsRequested += 
      SettingsPane_CommandsRequested;
  }
  public void Cleanup()
  {
    this.SettingsPane.CommandsRequested -= 
      SettingsPane_CommandsRequested;
  }
  private void SettingsPane_CommandsRequested(
    SettingsPane sender, SettingsPaneCommandsRequestedEventArgs args)
  {
    if (this.OnSettingsRequested != null)
    {
      this.OnSettingsRequested(args.Request.ApplicationCommands);
    }
  }
  public Action<IList<SettingsCommand>> OnSettingsRequested { get; 

set; }
}

當引發 CommandsRequested 事件時,事件參數最終允許我訪問表示“設置”菜單 選項的 SettingsCommand 對象的列表。若要添加我自己的“設置”菜單選項,我只需要向該 列表添加一個 SettingsCommand 實例。SettingsCommand 對象要求的不多,僅僅是唯一標識 符、標簽文本和要在用戶選擇選項時執行的代碼。

我使用 IoC 容器向視圖模型注入一個 ISettingsManager 實例,然後設置它以進行初始 化和清理,如圖 8 所示。

圖 8:接通 ISettingsManager

          public ShellViewModel(ISettingsManager settingsManager)
{
  this.settingsManager = settingsManager;
}
public void Initialize()
{
  this.settingsManager.Initialize();
  this.settingsManager.OnSettingsRequested = 
    OnSettingsRequested;
}
public void Cleanup()
{
  this.settingsManager.Cleanup();
}

我將使用“設置”允許用戶更改其可用示例應用程序查看哪些 RSS 源。此時我希 望用戶可從應用程序中的任意位置進行更改,因此我已加入了 ShellViewModel,它在應用程 序啟動時即實例化。如果我希望僅從其他某個視圖中更改 RSS 源,則我要在關聯的視圖模型 中加入設置代碼。

Windows 運行時中缺少用於為設置創建浮出控件和維護它的內置功能。為了獲得應在所有 應用程序間保持一致的功能,需要進行更多本不應進行的手動編碼。幸運的是,不僅是我有 這種感覺。Tim Heuer 是 Microsoft XAML 團隊中的一名計劃經理,它創造了一個傑出的框 架,名為 Callisto,可幫助解決這一難點。可在 GitHub (bit.ly/Kijr1S) 和 NuGet (bit.ly/112ehch) 上獲得 Callisto。我在示例應用程 序中使用了它,建議您仔細研究一下它。

由於我在視圖模型中完全接通了 SettingsManager,因此我只需提供要在請求設置時執行 的代碼,如圖 9 所示。

圖 9:用 Callisto 在 SettingsRequested 時顯示 SettingsView

          private void OnSettingsRequested(IList<SettingsCommand> 

commands)
{
  SettingsCommand settingsCommand =
    new SettingsCommand("FeedsSetting", "Feeds", (x) =>
  {
    SettingsFlyout settings = new Callisto.Controls.SettingsFlyout();
    settings.FlyoutWidth =
      Callisto.Controls.SettingsFlyout.SettingsFlyoutWidth.Wide;
    settings.HeaderText = "Feeds";
    var view = new SettingsView();
    settings.Content = view;
    settings.HorizontalContentAlignment = 
      HorizontalAlignment.Stretch;
    settings.VerticalContentAlignment = 
      VerticalAlignment.Stretch;
    settings.IsOpen = true;
  });
  commands.Add(settingsCommand);
}

我新建一個 SettingsCommand,向其提供 ID“FeedsSetting”和標簽文本 “Feeds”。我用於回調的 lambda(在用戶選擇“Feeds”菜單項時調用)利用了 Callisto 的 SettingsFlyout 控件。SettingsFlyout 控件處理在何處放置浮出控件、決定其寬度以及 何時打開和關閉它等重要工作。我只需告訴它我需要寬版還是窄版,向其提供一些標題文本 和內容,然後將 IsOpen 設置為 true 即可打開它。我還建議將 HorizontalContentAlignment 和 VerticalContent­Alignment 設置為 Stretch。否則 ,您的內容將不符合 SettingsFlyout 的大小。

消息總線

在處理“設置”合約時,一個要點是對設置的任何更改都應立即應用於應用程序並在應用 程序中反映出來。可使用多種方法將用戶進行的設置更改廣播出去。我更願意使用的方法是 消息總線(也稱為事件聚合器)。消息總線是整個應用程序范圍內的一種消息發布系統。 Windows 運行時中並未內置消息總線的概念,這意味著我不得不創建一個消息總線或使用其 他框架中的消息總線。我已加入了一個消息總線實現,而我已在許多項目中將其與 Charmed 框架配合使用。可在 bit.ly/12EBHrb 上找到源代碼。還有許多其他好的實 現。Caliburn.Micro 具有 EventAggregator,而 MVVM Light 具有 Messenger。所有實現通 常都遵循同一模式,並提供訂閱、取消訂閱和發布消息的方式。

通過在設置方案中使用 Charmed 消息總線,我將 MainViewModel(顯示源的那個模型) 配置為訂閱 FeedsChangedMessage:

        this.messageBus.Subscribe<FeedsChangedMessage>((message) =>
{
  LoadFeedData();
});

將 MainViewModel 設置為偵聽對源的更改後,我將 SettingsViewModel 配置 為在用戶添加或刪除 RSS 源時發布 FeedsChanged­Message:

this.messageBus.Publish<FeedsChangedMessage>(new FeedsChangedMessage

());

只要涉及消息總線,應用程序的每個部分就要使用同一消息總線實例,這一點很重要。 因此,我確保將我的 IoC 容器配置為向每個請求僅提供單一實例以解析 IMessageBus。

現在,示例應用程序經過設置,使用戶可對通過“設置”超級按鈕顯示的 RSS 源作 出更改並更新主視圖以反映這些更改。
漫游設置

Windows 8 引入的另一個好東 西是漫游設置的概念。 通過漫游設置,應用程序開發人員可在用戶的所有設備中轉移少量數 據。 這些數據必須小於 100KB,並且應僅限於在所有設備上創造持久、自定義的 UX 所需的 那些信息。 在示例應用程序的情況下,我希望能夠在所有此類設備上保持用戶要閱讀的 RSS 源。

我先前談論過的“設置”合約通常與漫游設置並用。 只有在具有漫游設置的設 備上保持我允許用戶使用“設置”合約做出的自定義保留才有意義。

訪問漫游設置就 像我到現在為止談到的其他問題一樣,比較簡單。 通過 ApplicationData 類可同時訪問 LocalSettings 和 RoamingSettings。 向 RoamingSettings 加入信息只需提供密鑰和對象 :

             ApplicationData.Current.RoamingSettings.Values[key] = value;
           

雖然 ApplicationData 易於使用,但另有一 個密封類在單元測試中無法模擬。 因此,為了盡可能可測試我的視圖模型,我需要將與 ApplicationData 的交互抽象化。 在定義將漫游設置功能抽象化出的接口之前,我需要決定 要對它做些什麼:

   查看是否存在密鑰。
   添加或 更新設置。
   刪除設置。
   獲取設置。

現在 我萬事俱備,可創建一個名為 ISettings 的接口:

          public interface ISettings
{
  void AddOrUpdate(string key, object value);
  bool TryGetValue<T>(string key, out T value);
  bool Remove(string key);
  bool ContainsKey(string key);
}

定義該接口後,需要實現它,如圖 10 所示。

圖 10:實現 ISettings

          public sealed class Settings : ISettings
{
  public void AddOrUpdate(string key, object value)
  {
    ApplicationData.Current.RoamingSettings.Values[key] = value;
  }
  public bool TryGetValue<T>(string key, out T value)
  {
    var result = false;
    if (ApplicationData.Current.RoamingSettings.Values.ContainsKey(key))
    {
      value = (T)ApplicationData.Current.RoamingSettings.Values[key];
      result = true;
    }
    else
    {
      value = default(T);
    }
    return result;
  }
  public bool Remove(string key)
  {
    return ApplicationData.Current.RoamingSettings.Values.Remove(key);
  }
  public bool ContainsKey(string key)
  {
    return ApplicationData.Current.RoamingSettings.Values.ContainsKey(key);
  }
}

TryGetValue 將首先檢查是否存在給定的密鑰,如果存在,則向 out 參數賦值。如果未 找到該密鑰,它並不引發異常,而是返回一個布爾值,指示是否找到了該密鑰。其余方法不 言自明。

現在,可讓 IoC 容器解析 ISettings,然後將其提供給 SettingsViewModel。這樣做後 ,視圖模型將使用這些設置加載用戶的源以進行編輯,如圖 11 所示。

圖 11:加載並保存用戶的源

          public SettingsViewModel(
  ISettings settings,
  IMessageBus messageBus)
{
  this.settings = settings;
  this.messageBus = messageBus;
  this.Feeds = new ObservableCollection<string>();
  string[] feedData;
  if (this.settings.TryGetValue<string[]>(Constants.FeedsKey, out 

feedData))
  {
    foreach (var feed in feedData)
    {
      this.Feeds.Add(feed);
    }
  }
}
public void AddFeed()
{
  this.Feeds.Add(this.NewFeed);
  this.NewFeed = string.Empty;
  SaveFeeds();
}
public void RemoveFeed(string feed)
{
  this.Feeds.Remove(feed);
  SaveFeeds();
}
private void SaveFeeds()
{
  this.settings.AddOrUpdate(Constants.FeedsKey, this.Feeds.ToArray());
  this.messageBus.Publish<FeedsChangedMessage>(new FeedsChangedMessage());
}

關於圖 11 中的代碼要注意的一點是:實際保存到設置中的數據是一 個字符串數組。由於漫游設置限制為最大 100KB,因此需要使內容保持簡潔並堅持使用基元 類型。

輔助磁貼

開發出吸引用戶參與的應用程序說得上是一個難題。但在用戶安裝您的應用程序之後,怎 樣讓他們不斷地再次使用?可幫助應對這種難題的一種方法是輔助磁貼。通過輔助磁貼,可 深入鏈接到應用程序中,從而使用戶可跳過應用程序的其余部分,直達他們最關心的部分。 輔助磁貼固定在用戶的主屏幕,上面顯示您選擇的圖標。點擊輔助磁貼後,它就會啟動您的 應用程序,帶有告知該應用程序去何處和加載什麼的參數。向用戶提供輔助磁貼功能是讓其 可自定義其體驗的好方法,這樣使他們想再次使用。

輔助磁貼比我在本文中介紹的其他主題復雜,因為有許多東西必須先實現,然後使用輔助 磁貼的完整體驗才能正常發揮作用。

固定輔助磁貼涉及將 SecondaryTile 類實例化。SecondaryTile 采用多個參數幫助它決 定磁貼的外觀,包括顯示名稱、要用於磁貼的徽標圖像文件的 URI 以及在按該磁貼時將向應 用程序提供的字符串參數。將 SecondaryTile 實例化後,我必須調用一個方法,該方法最後 將顯示一個小型的彈出窗口,其中請求用戶允許固定磁貼,如圖 12 所示 。

圖 12 :SecondaryTile 請求允許將磁貼固定到“開始”屏幕

用戶按“固定到‘開始’屏幕”後,即完成前一半工作。後一半是使用在按磁貼時它提供 的參數配置應用程序,使其真正支持深入鏈接。在我詳細介紹後一半之前,我要談論一下我 將怎樣以可測試的方式實現前一半。

由於 SecondaryTile 使用直接與操作系統交互的方法(接下來由操作系統顯示 UI 組件 ),因此無法在不影響可測試性的前提下直接從視圖模型中使用它。因此,我將抽象化出另 一個接口,我將其稱為 ISecondaryPinner(通過它,我應可固定和取消固定磁貼以及檢查磁 貼是否已固定):

          public interface ISecondaryPinner
{
  Task<bool> Pin(FrameworkElement anchorElement,
    Placement requestPlacement, TileInfo tileInfo);
  Task<bool> Unpin(FrameworkElement anchorElement,
    Placement requestPlacement, string tileId);
  bool IsPinned(string tileId);
}

注意,Pin 和 Unpin 都返回 Task<bool>。這是因為 SecondaryTile 使用異步任 務提示用戶固定或取消固定磁貼。這還意味著可等待 ISecondaryPinner 的 Pin 和 Unpin 方法。

另請注意,Pin 和 Unpin 均采用 FrameworkElement 和 Placement 枚舉值作為參數。原 因是 SecondaryTile 需要矩形和 Placement 指示它將固定請求彈出窗口放在何處。我打算 讓我的 SecondaryPinner 實現根據傳入的 FrameworkElement 計算該矩形。

最後,我創建一個幫助器類 TileInfo 以傳遞由 SecondaryTile 使用的必要和可選參數 ,如圖 13 所示。

圖 13:TileInfo 幫助器類

          public sealed class TileInfo
{
  public TileInfo(
    string tileId,
    string shortName,
    string displayName,
    TileOptions tileOptions,
    Uri logoUri,
    string arguments = null)
  {
    this.TileId = tileId;
    this.ShortName = shortName;
    this.DisplayName = displayName;
    this.Arguments = arguments;
    this.TileOptions = tileOptions;
    this.LogoUri = logoUri;
    this.Arguments = arguments;
  }
  public TileInfo(
    string tileId,
    string shortName,
    string displayName,
    TileOptions tileOptions,
    Uri logoUri,
    Uri wideLogoUri,
    string arguments = null)
  {
    this.TileId = tileId;
    this.ShortName = shortName;
    this.DisplayName = displayName;
    this.Arguments = arguments;
    this.TileOptions = tileOptions;
    this.LogoUri = logoUri;
    this.WideLogoUri = wideLogoUri;
    this.Arguments = arguments;
  }
  public string TileId { get; set; }
  public string ShortName { get; set; }
  public string DisplayName { get; set; }
  public string Arguments { get; set; }
  public TileOptions TileOptions { get; set; }
  public Uri LogoUri { get; set; }
  public Uri WideLogoUri { get; set; }
}

根據數據的不同,TileInfo 可使用兩個構造函數。現在,我實現 ISecondaryPinner,如 圖 14 所示。

圖 14 實現 ISecondaryPinner

          public sealed class SecondaryPinner : ISecondaryPinner
{
  public async Task<bool> Pin(
    FrameworkElement anchorElement,
    Placement requestPlacement,
    TileInfo tileInfo)
  {
    if (anchorElement == null)
    {
      throw new ArgumentNullException("anchorElement");
    }
    if (tileInfo == null)
    {
      throw new ArgumentNullException("tileInfo");
    }
    var isPinned = false;
    if (!SecondaryTile.Exists(tileInfo.TileId))
    {
      var secondaryTile = new SecondaryTile(
        tileInfo.TileId,
        tileInfo.ShortName,
        tileInfo.DisplayName,
        tileInfo.Arguments,
        tileInfo.TileOptions,
        tileInfo.LogoUri);
      if (tileInfo.WideLogoUri != null)
      {
        secondaryTile.WideLogo = tileInfo.WideLogoUri;
      }
      isPinned = await secondaryTile.RequestCreateForSelectionAsync(
        GetElementRect(anchorElement), requestPlacement);
    }
    return isPinned;
  }
  public async Task<bool> Unpin(
    FrameworkElement anchorElement,
    Placement requestPlacement,
    string tileId)
  {
    var wasUnpinned = false;
    if (SecondaryTile.Exists(tileId))
    {
      var secondaryTile = new SecondaryTile(tileId);
      wasUnpinned = await secondaryTile.RequestDeleteForSelectionAsync(
        GetElementRect(anchorElement), requestPlacement);
    }
    return wasUnpinned;
  }
  public bool IsPinned(string tileId)
  {
    return SecondaryTile.Exists(tileId);
  }
  private static Rect GetElementRect(FrameworkElement element)
  {
    GeneralTransform buttonTransform =
      element.TransformToVisual(null);
    Point point = buttonTransform.TransformPoint(new Point());
    return new Rect(point, new Size(
      element.ActualWidth, element.ActualHeight));
  }
}

查看本欄目

Pin 將首先確保尚未存在所請求的磁貼,然後它將提示用戶固定該磁貼。Unpin 將首先確 保已存在所請求的磁貼,然後它將提示用戶取消固定該磁貼。兩者都將返回一個布爾值,指 示固定或取消固定是否成功。

現在,可將一個 ISecondaryPinner 實例注入視圖模型並將其投入使用,如圖 15 所示。

圖 15:用 ISecondaryPinner 進行固定和解除固定

          public FeedItemViewModel(
  IShareManager shareManager,
  ISecondaryPinner secondaryPinner)
{
  this.shareManager = shareManager;
  this.secondaryPinner = secondaryPinner;
}
public async Task Pin(FrameworkElement anchorElement)
{
  var tileInfo = new TileInfo(
    FormatSecondaryTileId(),
    this.FeedItem.Title,
    this.FeedItem.Title,
    TileOptions.ShowNameOnLogo | TileOptions.ShowNameOnWideLogo,
    new Uri("ms-appx:///Assets/Logo.png"),
    new Uri("ms-appx:///Assets/WideLogo.png"),
    this.FeedItem.Id.ToString());
    this.IsFeedItemPinned = await this.secondaryPinner.Pin(
    anchorElement,
    Windows.UI.Popups.Placement.Above,
    tileInfo);
}
public async Task Unpin(FrameworkElement anchorElement)
{
  this.IsFeedItemPinned = !await this.secondaryPinner.Unpin(
    anchorElement,
    Windows.UI.Popups.Placement.Above,
    this.FormatSecondaryTileId());
}

在 Pin 中,我創建一個 TileInfo 幫助器實例,並向其提供一個格式獨一無二的 ID、源 標題、徽標和寬徽標的 URI 以及作為啟動參數的源 ID。Pin 將所單擊的按鍵作為決定固定 請求彈出窗口位置的定位元素。我使用 SecondaryPinner.Pin 方法的結果判斷源項是否已固 定。

在 Unpin 中,我給出格式獨一無二的磁貼 ID,並使用結果的顛倒形式判斷源項是否仍固 定。又一次,將所單擊的按鍵作為取消固定請求彈出窗口的定位元素傳遞給 Unpin。

將此安排妥當並使用它將一篇博客文章 (FeedItem) 固定到“開始”屏幕之後,點擊新創 建的磁貼即可啟動應用程序。但是,它啟動應用程序的方式將與以前相同,即進入主頁,顯 示所有博客文章。我想讓它進入我所固定的特定博客文章。而這正是後一半功能發揮作用的 地方。

後一半功能通過所啟動的應用程序進入 app.xaml.cs,如圖 16 所示 。

圖 16:啟動應用程序

          protected override async void OnLaunched(LaunchActivatedEventArgs 

args)
{
  Frame rootFrame = Window.Current.Content as Frame;
  if (rootFrame.Content == null)
  {
    Ioc.Container.Resolve<INavigator>().
          NavigateToViewModel<MainViewModel>();
  }
  if (!string.IsNullOrWhiteSpace(args.Arguments))
  {
    var storage = Ioc.Container.Resolve<IStorage>();
    List<FeedItem> pinnedFeedItems =
      await storage.LoadAsync<List<FeedItem>>

(Constants.PinnedFeedItemsKey);
    if (pinnedFeedItems != null)
    {
      int id;
      if (int.TryParse(args.Arguments, out id))
      {
        var pinnedFeedItem = pinnedFeedItems.FirstOrDefault(fi => fi.Id == 

id);
        if (pinnedFeedItem != null)
        {
          Ioc.Container.Resolve<INavigator>().
          NavigateToViewModel<FeedItemViewModel>(
            pinnedFeedItem);
        }
      }
    }
  }
  Window.Current.Activate();
}

我向重寫的 OnLaunched 方法的結尾添加了一些代碼以檢查是否已在啟動過程中傳入了參 數。如果已傳入參數,則我將這些參數分析為要用作源 ID 的 int。我從保存的源中獲得具 有該 ID 的源,然後將其傳遞給要顯示的 FeedItemViewModel。要注意的一點是,我確保該 應用程序已顯示主頁,如果尚未顯示主頁,則我先導航到那裡。這樣,用戶可按後退按鈕並 進入主頁,無論他是否已在運行應用程序都是如此。

總結

在本文中,我談論了我的一種方法,該方法使用 MVVM 模式實現可測試的 Windows 應用 商店應用程序,同時仍利用 Windows 8 提供的一些絕妙新功能。具體而言,我談到將共享、 設置、漫游設置和輔助磁貼抽象化為實現可模擬接口的幫助器類。通過此方法,我可以盡可 能多地對視圖模型功能進行單元測試。

既然已將這些視圖模型設置得更加可進行測試,那麼在以後的文章中,我將深入介紹有關 可怎樣真正編寫對這些視圖模型的單元測試。我還將探討可怎樣應用同樣這些方法以使視圖 模型可跨平台用於 Windows Phone 8,同時仍可測試這些模型。

稍作規劃,即可創建具有創新 UX 的優秀應用程序,其中利用 Windows 8 的重要新功能 ,同時並不影響最佳實踐或單元測試。

下載代碼示例

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved