在UWP淘寶與旺信中,筆者主要負責頁面與控制項的製作,這些工作看似簡單,但要想做的全面細緻仍然需要深入的思考。本文想分享一些在UWP旺信的製作過程中,筆者在UI頁面與控制項製作上體會到的一些心得。可能筆者的有些方法並不見得高明,或者仍需要時間的檢驗,所以也歡迎大家拍磚,共同進步。 UWP旺信是一個非常依 ...
在UWP淘寶與旺信中,筆者主要負責頁面與控制項的製作,這些工作看似簡單,但要想做的全面細緻仍然需要深入的思考。本文想分享一些在UWP旺信的製作過程中,筆者在UI頁面與控制項製作上體會到的一些心得。可能筆者的有些方法並不見得高明,或者仍需要時間的檢驗,所以也歡迎大家拍磚,共同進步。
UWP旺信是一個非常依賴網路的應用,在應用頁面中的很多數據都需要訪問網路才能取到最新的結果,這樣一來網路狀況就會影響到用戶體驗。為了把網路對用戶體驗的影響降低,在UWP旺信中採用了比較通用的做法:數據緩存。在進入頁面以後先把緩存中的數據呈現給用戶,然後在後臺進行聯網拉取最新的數據並更新頁面顯示。以UWP旺信中的群信息頁面為例:在Loaded方法中,頁面會先從緩存中獲得群的數據並更新頁面顯示。接著頁面繼續調用網路介面,並通過介面返回數據對頁面UI進行更新。
看起來是非常簡單的過程,但是其中存在幾個需要註意的問題:
首先,我們在UI頁面上顯示內容時,一般會採取綁定到後臺數據的方法。而UI頁面實際上還有很多狀態,如各個元素的顯示隱藏,Progress控制項的激活與停止等等。這些狀態往往也需要綁定到後臺數據。如果我們把這些內容和狀態的數據都放在頁面的code Behind中,則會大大增加code Behind的複雜度,因此我們可以將這些內容和狀態數據集中放在一個View Model類中,讓UI頁面的元素來綁定。
其次,一般數據綁定的方法有Binding 或 x:Bind。使用Binding方法會比較簡單,只要在code Behind中設置頁面的DataContext為View Model就可以綁定了。而由於微軟官方已經明確了x:Bind方法在運行效率上是優於Binding方法的,那麼我們應當優先使用x:Bind方法。但是x:Bind方法有個缺點就是它綁定的屬性是頁面或控制項自身code Behind的屬性,而不能靈活的選擇不同類型的DataContext來進行綁定。因此我們可以將View Model作為Code behind的一個成員變數,這樣一來也能實現頁面對View Model中數據的綁定。
第三,在UWP應用中,當導航到一個頁面時可以用OnNavigatedTo方法的參數來傳遞數據到該頁面。但當從一個頁面goback到之前的頁面時,卻沒有方法來返回一個數據到之前的頁面。對於這種情況有很多解決辦法:可以使用全局變數,可以讓前一個頁面的緩存模式設為enabled或required併在導航到下一個頁面時傳遞引用型變數參數等等。筆者在旺信中則嘗試了把ViewModel做成單例,讓相關頁面使用同一個ViewModel的方法。例如旺信中群成員,群成員管理,添加群成員,群設置管理等頁面是一系列相關的頁面,需要統一從群成員頁面進入,在應用中不存在同類型頁面有多個實例同時存在的情況,並且有很多數據是共用的。於是筆者為它們創建了一個共同的ViewModel,在這些頁面之間導航時,都使用一個ViewModel實例。這樣就解決了數據回傳的問題。並且用戶操作後,各個頁面都能同步更新。
第四,在更新View Model中與頁面UI綁定的數據時,如果是從頁面的code Behind中採用await的非同步方法來更新的話,會非常順利。UWP旺信獲取群信息的介面是通過回調來返回數據的,當回調方法試圖更新View Model中綁定到UI界面的數據時,會觸發異常,提示"The application called an interface that was marshalled for a different thread."。這是由於回調方法一般來說並不是由UI線程發起的,而頁面綁定的數據只能通過UI線程來修改。如果一定要通過其他線程來修改,需要使用頁面的Dispatcher的RunAsync方法來進行。因此在View Model中我們需要增加一個CoreDispatcher成員變數,在頁面初始化View Model時,將頁面的Dispatcher賦值給該變數。當回調方法更新View Model時,如果CoreDispatcher成員變數不為空,就調用CoreDispatcher來更新綁定數據。
根據這些註意點,以旺信的群成員頁面為例:
在xaml頁面中群成員列表數據源綁定了View Model中的mainList列表變數:
<Page.Resources> <CollectionViewSource x:Name="ContactsCVS" Source="{x:Bind thisData.MainList,Mode=OneWay}" IsSourceGrouped="True" /> </Page.Resources>
而在code Behind中,聲明瞭thisData變數作為ViewModel:
public sealed partial class TribeCardMorePage : BasePage { //… private TribeCardMoreVM thisData; //… }
並且在頁面初始化時把ViewModel類型的單例賦值給它,並將頁面的Dispatcher傳遞給ViewModel:
public TribeCardMorePage() { this.InitializeComponent(); thisData = TribeCardMoreVM.Instance; thisData.dispatcher = Dispatcher; }
在頁面的OnNavigatedTo方法中設置ViewModel的一些參數,並嘗試從緩存中取出緩存的數據:
public override async void OnNavigatedTo(NavigationEventArgs args) { base.OnNavigatedTo(args); if (args != null && args.Parameter != null && args.Parameter is Tribe) { thisData.para = args.Parameter as Tribe; } thisData.LoadDataFromCache(); //… }
在頁面的Loaded事件中再通過網路更新數據:
protected async override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); await thisData.LoadData(); }
而ViewModel則是一個繼承了INotifyPropertyChanged介面的數據類型,這樣ViewModel中數據的變化才能通知到頁面,數據綁定才有效。一般我們會寫一個類來繼承INotifyPropertyChanged介面,而ViewModel則繼承這個類就可以了。
public class ObservableObject : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void RaisePropertyChanged([CallerMemberName]string propertyName = null) { var handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } }
如上所述,ViewModel需要繼承ObservableObject類,要有供UI綁定的數據,要有更新數據用的Dispatcher,數據取回之後要用Dispatcher來更新:
public class TribeCardMoreVM : ObservableObject { //… private ContactMgr _cmgr = new ContactMgr(); private volatile static TribeCardMoreVM _instance = null; public static TribeCardMoreVM Instance //ViewModel的單例 { get { if (_instance == null) { _instance = new TribeCardMoreVM(); } return _instance; } } public CoreDispatcher dispatcher { get; set; }//頁面的Dispatcher private Tribe _para; public Tribe para //獲取數據的參數 { get { return _para; } set { _para = value; RaisePropertyChanged(); } } private ObservableCollection<TribeMemberUIGroup> _MainList = new ObservableCollection<TribeMemberUIGroup>(); public ObservableCollection<TribeMemberUIGroup> MainList//頁面綁定的數據 { get { return _MainList; } set { _MainList = value; RaisePropertyChanged(); } } public async Task LoadData() { //… _cmgr.OnOnlineContactComplete += (ex, tx) => { dispatcher?.RunAsync(CoreDispatcherPriority.Normal, () => //在回調方法中用dispatcher來更新頁面UI { updateTribeMemberUIGroup(); }); }; await _cmgr.OnEventOnlineContactList(); //從網路獲取數據 //… } //… }
以上這些就是筆者所體會到的在類似於UWP旺信這種依賴於網路的應用的頁面的實現中需要註意的一些地方。
在下一篇博客中,筆者將進一步分享對於頁面細節實現的體會。歡迎大家關註,拍磚,共同進步。