首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

善用 Windows 8 特色與 MVVM:可測試的共享、設定、輔助磚、漫遊設定實作

     編輯:關於.NET

前言:MVVM 與 Windows 8 合約

Windows 8 引入共享、設定、搜尋等「合約」,透過超級按鈕隨時可用。直接在 ViewModel 呼叫 DataTransferManager、SettingsPane 等密封類別會造成難以單元測試的硬性相依。本文示範以介面抽象、依賴注入(IoC)封裝這些系統服務,保持 ViewModel 可測試。

示範應用:RSS 部落格閱讀器

功能:共享文章、設定切換 RSS 來源、輔助磚釘選文章、漫遊設定同步訂閱清單。

開源輔助庫 Charmed(GitHub)封裝此類抽象,支援 Win8/WinPhone 8 跨平台 MVVM。

共享合約抽象

關鍵三點:啟動訂閱 DataRequested、停用取消訂閱、提供 DataPackage 內容。

public interface IShareManager {
    void Initialize();
    void Cleanup();
    Action<DataPackage> OnShareRequested { get; set; }
}
public sealed class ShareManager : IShareManager {
    private DataTransferManager dtm;
    public void Initialize() {
        dtm = DataTransferManager.GetForCurrentView();
        dtm.DataRequested += Dtm_DataRequested;
    }
    public void Cleanup() => dtm.DataRequested -= Dtm_DataRequested;
    private void Dtm_DataRequested(DataTransferManager s, DataRequestedEventArgs e) {
        OnShareRequested?.Invoke(e.Request.Data);
    }
    public Action<DataPackage> OnShareRequested { get; set; }
}

ViewModel 注入 IShareManager,於 LoadState/SaveState 初始化/清理:

public FeedItemViewModel(IShareManager shareManager) { this.shareManager = shareManager; }
public override void LoadState(...) { shareManager.Initialize(); shareManager.OnShareRequested = ShareRequested; }
public override void SaveState(...) { shareManager.Cleanup(); }
private void ShareRequested(DataPackage dp) {
    dp.Properties.Title = FeedItem.Title;
    dp.SetUri(FeedItem.Link);
    dp.SetText($"Check this out! {FeedItem.Title} ({FeedItem.Link})");
    var html = $"

Check this out!

{FeedItem.Title}

"; dp.SetHtmlFormat(HtmlFormatHelper.CreateHtmlFormat(html)); }

設定合約抽象

同樣模式:訂閱 CommandsRequested、加入自訂 SettingsCommand。

public interface ISettingsManager {
    void Initialize(); void Cleanup();
    Action<IList<SettingsCommand>> OnSettingsRequested { get; set; }
}
public sealed class SettingsManager : ISettingsManager {
    private SettingsPane pane;
    public void Initialize() {
        pane = SettingsPane.GetForCurrentView();
        pane.CommandsRequested += Pane_CommandsRequested;
    }
    public void Cleanup() => pane.CommandsRequested -= Pane_CommandsRequested;
    private void Pane_CommandsRequested(SettingsPane s, SettingsPaneCommandsRequestedEventArgs e) {
        OnSettingsRequested?.Invoke(e.Request.ApplicationCommands);
    }
    public Action<IList<SettingsCommand>> OnSettingsRequested { get; set; }
}

ShellViewModel 注入並開啟 Callisto SettingsFlyout:

private void OnSettingsRequested(IList<SettingsCommand> cmds) {
    var cmd = new SettingsCommand("FeedsSetting", "Feeds", _ => {
        var flyout = new Callisto.Controls.SettingsFlyout {
            FlyoutWidth = SettingsFlyout.SettingsFlyoutWidth.Wide,
            HeaderText = "Feeds", Content = new SettingsView(),
            HorizontalContentAlignment = HorizontalAlignment.Stretch,
            VerticalContentAlignment = VerticalAlignment.Stretch, IsOpen = true
        };
    });
    cmds.Add(cmd);
}

設定變更透過訊息總線(IMessageBus)廣播 FeedsChangedMessage,MainViewModel 訂閱並重新載入資料。

漫遊設定抽象

ApplicationData.Current.RoamingSettings(<100KB)同步裝置間設定。封裝介面:

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);
}
public sealed class Settings : ISettings {
    private readonly ApplicationDataContainer roaming = ApplicationData.Current.RoamingSettings;
    public void AddOrUpdate(string key, object value) => roaming.Values[key] = value;
    public bool TryGetValue<T>(string key, out T value) {
        if (roaming.Values.ContainsKey(key)) { value = (T)roaming.Values[key]; return true; }
        value = default(T); return false;
    }
    public bool Remove(string key) => roaming.Values.Remove(key);
    public bool ContainsKey(string key) => roaming.Values.ContainsKey(key);
}

SettingsViewModel 注入 ISettings 載入/儲存 RSS 陣列(字串陣列以節省空間),儲存後發布 FeedsChangedMessage。

輔助磚(Secondary Tile)抽象

固定/取消固定需非同步彈窗確認,封裝 ISecondaryPinner:

public interface ISecondaryPinner {
    Task<bool> Pin(FrameworkElement anchor, Placement placement, TileInfo info);
    Task<bool> Unpin(FrameworkElement anchor, Placement placement, string tileId);
    bool IsPinned(string tileId);
}
public sealed class TileInfo {
    public string TileId, ShortName, DisplayName, Arguments;
    public TileOptions TileOptions; public Uri LogoUri, WideLogoUri;
    // 兩個建構子...
}
public sealed class SecondaryPinner : ISecondaryPinner {
    public async Task<bool> Pin(FrameworkElement anchor, Placement placement, TileInfo info) {
        if (SecondaryTile.Exists(info.TileId)) return false;
        var tile = new SecondaryTile(info.TileId, info.ShortName, info.DisplayName,
            info.Arguments, info.TileOptions, info.LogoUri) { WideLogo = info.WideLogoUri };
        return await tile.RequestCreateForSelectionAsync(GetElementRect(anchor), placement);
    }
    public async Task<bool> Unpin(FrameworkElement anchor, Placement placement, string tileId) {
        if (!SecondaryTile.Exists(tileId)) return false;
        var tile = new SecondaryTile(tileId);
        return await tile.RequestDeleteForSelectionAsync(GetElementRect(anchor), placement);
    }
    public bool IsPinned(string tileId) => SecondaryTile.Exists(tileId);
    private static Rect GetElementRect(FrameworkElement e) {
        var t = e.TransformToVisual(null); var p = t.TransformPoint(new Point());
        return new Rect(p, new Size(e.ActualWidth, e.ActualHeight));
    }
}

FeedItemViewModel 注入 ISecondaryPinner,按鈕點擊呼叫 Pin/Unpin,以文章 ID 為參數。

深度連結啟動處理

App.xaml.cs 覆寫 OnLaunched,解析參數導航至對應 FeedItemViewModel:

protected override async void OnLaunched(LaunchActivatedEventArgs args) {
    var 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>();
        var items = await storage.LoadAsync<List<FeedItem>>(Constants.PinnedFeedItemsKey);
        if (items != null && int.TryParse(args.Arguments, out int id)) {
            var item = items.FirstOrDefault(f => f.Id == id);
            if (item != null) Ioc.Container.Resolve<INavigator>().NavigateToViewModel<FeedItemViewModel>(item);
        }
    }
    Window.Current.Activate();
}

總結

以介面抽象系統服務(共享、設定、漫遊、輔助磚),搭配 IoC 注入 ViewModel,兼顧 Windows 8 豐富 UX 與單元測試性。後續將探討 ViewModel 單元測試撰寫與跨平台移植至 Windows Phone 8。

程式碼下載:Charmed 庫 / 示範專案原始碼

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved