在上一篇博客中,筆者分享了一些從頁面整體的角度對頁面與ViewModel的思考。在本文中筆者希望從相對細節的角度分享一些對頁面與ViewModel的思考。 比如,當我們在更新View Model中的綁定數據時,應該怎樣更新呢?簡單的自然可以用新的數據實例直接替代舊的,但是這樣容易造成UI界面閃爍。尤 ...
在上一篇博客中,筆者分享了一些從頁面整體的角度對頁面與ViewModel的思考。在本文中筆者希望從相對細節的角度分享一些對頁面與ViewModel的思考。
比如,當我們在更新View Model中的綁定數據時,應該怎樣更新呢?簡單的自然可以用新的數據實例直接替代舊的,但是這樣容易造成UI界面閃爍。尤其是綁定數據是一個列表的情況下,如果整個列表被替換,可以非常明顯的看到列表"一閃"。這樣的用戶體驗無疑是不理想的。那麼我們在更新View Model中綁定的數據實例時,可以採用差異更新的方法。以一個數據列表為例,在更新時對比新舊列表,先遍歷新表,對每一個元素查看在舊表中有無對應元素。如果沒有,說明是新增的數據,可以將該新表中的元素同時加入到一個臨時表和舊表中,如果舊表有排序則還需要註意插入的位置。如果有,說明是舊元素更新,則用新元素的值更新舊元素後,將舊元素加入到臨時表中。然後遍歷舊表,對舊表中每一個元素在臨時表中查看有無對應元素。如果有,則不用做任何處理。如果沒有,則說明該元素已經被刪除,應該在舊表中將這個元素移除。這樣對UI界面的更新看起來會比較平滑。
這裡寫一下筆者在旺信UWP中所寫的差異化更新演算法,權當拋磚引玉。
var bList = new List<bool>();//輔助列表 for (int j = 0; j < MainList.Count; j++)//輔助列表初始與舊表同長 { bList.Add(false); } for (int i = 0; i < groups.Count; i++)//遍歷新表 { bool inserted = false; bool contains = false; for (int j = 0; j < MainList.Count; j++)//新表中的元素與舊表對比 { if (groups[i].key != MainList[j].key)//如果不是同一元素 { if ((groups[i].key == "群主")//嘗試插入 || ( MainList[j].key != "群主" && MainList[j].key != "管理員" && (groups[i].key == "管理員" || groups[i].key.CompareTo(MainList[j].key) < 0) ) ) { MainList.Insert(j, groups[i]); bList.Insert(j, true); inserted = true; Debug.WriteLine("inserted:" + j + "," + groups[i].key); break; } } else//如果是同一元素,用新表元素內容更新舊表 { contains = true; MainList[j].update(groups[i]); bList[j] = true; break; } } if ((!contains) && (!inserted))//不包括在舊表內,也沒有插入,則追加在舊表尾部 { MainList.Add(groups[i]); bList.Add(true); } } for (int i = bList.Count; i > 0; i--)//對比輔助列表,移除舊表中不應再存在的元素 { if (!bList[i-1]) { try { MainList.RemoveAt(i - 1); } catch (Exception) { Debug.WriteLine("RemoveAt error:" + i); } } }
在這段代碼中,用新的數據groups更新舊的數據MainList。
再比如,在我們的頁面上,我們一般都會放置一個表示正在載入數據的控制項。這個載入中控制項的狀態一般也是綁定一個後臺數據的。對於一般的頁面,我們可以採取在載入數據前後設置該綁定值的方法來修改頁面所顯示的載入狀態。而對於UWP旺信這種依賴網路,一個頁面可能同時調用多個網路介面更新數據的情況,就不是非常合適了。比如a,b兩個介面同時請求數據,將載入狀態置為載入中。如果a介面先返回,則會將載入狀態置為完成。而實際上b介面仍然在請求數據,正確的載入狀態應該還是載入中,直到b介面也返回。為此筆者想到了可以增加一個初始值為0的計數變數,當有載入請求時就自增1,當請求非同步結束或回調返回時就自減1,綁定的載入狀態的get方法根據當計數是否為0返回是否在載入狀態。這樣一來就可以使多個載入請求都能正確的改變載入狀態。
在旺信UWP中,筆者就為ViewModel添加了這樣的變數:
public bool isLoading { get { return loadingCount > 0; } } private int _loadingCount = 0; public int loadingCount { get { return _loadingCount; } set { _loadingCount = (value < 0 ? 0 : value); RaisePropertyChanged("isLoading"); } }
在xaml頁面上則將ProgressRing控制項的IsActive屬性綁定到isLoading變數上:
<ProgressRing Grid.Row="2" Grid.RowSpan="2" IsActive="{x:Bind thisData.isLoading,Mode=OneWay}" Width="60" Height="60" Foreground="{ThemeResource WXThemeColorBrush}"></ProgressRing>
在調用非同步方法前後,並不直接設置isLoading變數,而是採取上面提到的,調用前loadingCount變數自增1,完成後loadingCount變數自減1的方法來影響ProgressRing控制項所顯示的載入狀態IsActive。
另外,在使用x:Bind方法時,筆者發現如果把綁定image圖片控制項的source綁定到一個string,當綁定的string值為空時會在log中出現exception。即使在string值為空時把image控制項隱藏也仍然會出現。然而旺信中數據的屬性值基本都是從伺服器傳輸到客戶端的,有時確實會有一些圖片的url為空。這樣一來最好給圖片屬性值都給一個預設值。那麼預設值該如何確定呢?如果是普通的占點陣圖片,那麼可能在不該出現圖片的地方顯示。經過實踐,筆者選擇了在應用中加入一個長度為0的圖片,把該圖片的uri作為圖片屬性的預設值。當然這個方法只是消除了log中的exception,具體是否提升了應用的效率,還有待驗證。
以上就是筆者對對頁面與ViewModel的細節的思考,希望能對小伙伴們開發UWP應用有所幫助。當然也歡迎大家拍磚,提出更多更好的經驗,讓我們共同進步。