C/S分布式開發相比BS開發要考慮更多問題,難度也相對要高。
本文以最基本的Client端請求展示數據為例來討論一下C/S分布式開發中的用戶體驗!
在本文中你將看到關於C/S分布式設計中可能需要考慮的問題,MVVM模式的應用,Frame控件在WPF導航中無法適用需求的問題、Prism Region的應用等
首先,我們看下QQ當中對於用戶資料查詢時的UI設計:
1、加載過程
在這個過程當中,我們的數據是通過服務從服務端請求而來,請求過程存在一定的耗時操作,所以我們通常都會采用異步請求方式,確保UI正常!
2、請求失敗的情況
對於這種遠程服務請求來說,必然會遇到網絡異常或請求失敗的情況,或許很多人認為這種幾率幾乎不存在,或這認為您的應用是內聯網應用不需要考慮;
我個人認為作為一個好的設計來說,要盡可能的考慮到所有情況,即便是理論上有可能不存在的情況。
好,上面2則演示我們看到了最簡單的對於C/S方式客戶端遠程請求的一個處理示例;
接下來,我們再考慮一下分布式存在的情況,
如上圖展示,我們注意當前窗口中有一個“更新”按鈕, 在QQ客戶端這個例子當中,我們對於資料查詢這個頁面來說不會是持久化展示的一個頁面,
所以假設該頁面中的數據在后台 或者 被其他客戶端修改后 , 在當前客戶端中並沒有自動刷新,QQ的效果是您需要查看的時候手動去點擊“更新”
而我們常見的BS系統與C/S系統很大的一個區別在於大多數BS系統的頁面跳轉頻率較高,當每次頁面跳轉產生后會自動請求數據,所以不需要特別考慮數據一致性問題!(特殊應用系統除外)
如果我們當前的應用是一個監測系統或實時信息顯示系統,那么必然要求我們的頁面處於持久展示狀態,在這樣的情況下頁面就需要能夠及時刷新數據;
在BS當中我們通常通過ajax來異步不斷的刷新頁面中需要及時展示的數據;那么C/S系統當中如何做呢? 也像ajax一樣不斷的去異步請求嗎?
我個人認為死循環的異步請求並不合適,在移動應用中流量就是一個需要考慮的問題,還有如果數據沒有變動,這樣的異步請求是否耗費太多的資源!
好,我們就來實際考慮一下如何進行這樣一個設計;
首先以一個稍微復雜Client端數據請求與查詢為例來實現類似QQ的良好體驗:
在這個描述圖中,我們的導航區域 與 主內容區域的數據都是從服務端請求來的,類似QQ資料查看窗口(區別在於QQ的導航區域是固定的)
按照前面QQ所展示的,[導航區域]、[主內容區域] 都有可能請求失敗,那就要求如果請求失敗就要展示“錯誤頁面”,且“錯誤頁面”可以進行<刷新> 重新加載數據;
在上圖的描述中我們的[主內容區域]是通過[導航區域]的“導航”后產生的結果,所以它的數據加載應該是由 [導航區域]的 SelectedChanged事件觸發;
這樣,我們就需要考慮這個“資料查看”窗口應該是分2部分區域,每部分區域展示屬於它自己區域的頁面,我們來看下在WPF中如何設計這個窗口;
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="150" MaxWidth="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Frame NavigationUIVisibility="Hidden" Source="/Views/NavigatePage.xaml" Grid.Column="0"/>
<Frame NavigationUIVisibility="Hidden" Source="/Views/MyPage.xaml" x:Name="myContent" Grid.Column="1"/>
</Grid>
通過放置兩個Frame控件來承載不同的2個區域的內容頁面;
繼續,我們考慮下通過選中導航項如何通知[主內容區域] 進行內容加載;既然是導航,那必然涉及到導航參數,這時候我們要考慮如果在2個頁面進行參數傳遞;
我們可以通過選擇導航項來控制[主內容區域]的Frame進行Navigate,同時傳遞參數;
在這里我先通過MVVM模式中由ViewModel通訊來演示如何實現這一步;
class NavigatePageViewModel:ViewModelBase<NavigatePageViewModel>
{
public NavigatePageViewModel()
{
m_NavigateCommand = new DelegateCommand<string>(this.NavigateCallback);
}
ICommand m_NavigateCommand = null;
public ICommand NavigateCommand
{
get { return m_NavigateCommand; }
}
void NavigateCallback(string args)
{
this.SendMessage("Navigate", new NotificationEventArgs(args));
}
}
我們通過導航項綁定Command,再通過Command發送消息的方式來通知內容頁面接收參數並加載數據 (注意:這里所說的內容頁面 並非內容展示區域,通過MessageBus進行的消息通訊是VM之間的,並不能跨VM與View)
接着我們看下內容展示頁面接收到導航參數之后做怎樣的操作,
1、注冊接收消息(MVVM模式中的應用)
public MyPageViewModel()
{
this.RegisterToReceiveMessages("Navigate",
new EventHandler<NotificationEventArgs>((sender, e) =>
{
m_ReceiveMessage = e.Message;
this.AsyncLoad();
}));
}
我們來分析下段流程; 導航--->發送消息到內容展示頁的VM中--->保存導航參數--->加載數據,有什么問題嗎?
假設我們[主內容區域]當前展示為“錯誤頁面”,此時我們的主內容頁面即便接收到消息並且成功加載了數據,我們的View還是沒有正確展示出來,(這里主要的問題是我們使用MVVM模式將邏輯與UI徹底隔離,僅進行數據驅動的結果)
既然我們發現這里的問題,那我們就需要修改下流程: ---->保存導航參數--->讓主內容區域顯示內容頁面--->加載數據 ,這個時候就需要ViewModel通知View了,我們可以使用事件進行通知,
例如:
EventHandler handler = this.Loaded;
if (handler != null)
{
handler(this, new EventArgs());
}
View頁面中接收該事件並執行【讓主內容區域展示內容頁面】這一邏輯;
private void MyPageViewModel_Loaded(object sender, EventArgs e)
{
var service = NavigationService.GetNavigationService(this);
if (service != null)
{
service.Navigate(this);
}
}
如果我們使用了Frame作為主內容區域,到這里你就會發現無法獲取NavigationService對象,因為當前Frame中存放的是“錯誤頁面”
當然還有另外一個非常重要的問題:Frame在進行導航的時候 每次都會創建新的頁面(類似與瀏覽器每次打開一個連接就會重新請求頁面),這時候你就發現你緩存在ViewModel中的數據是沒用的,這里你就無法解決上面提到的問題 “設我們[主內容區域]當前展示為“錯誤頁面””
到這里我們就先暫停一下,先不來考慮UI上的問題,我們來討論一下分布式下如何較好的處理客戶端請求數據問題;
在這個圖中,我描述了一個簡單的發布-訂閱 模式,也就是要求我們的客戶端需要訂閱服務端的一個服務,當服務端自己判斷到需要推送數據給客戶端的時候它主動進行推送一個消息,這個時候客戶端會收到這個消息,然后客戶端主動請求服務端刷新數據。(請注意:這里服務端不是主動推送數據給客戶端,而是告訴客戶端一個消息讓它知道自己該刷新數據了)
在《WCF服務編程》一書中,作者已經附帶了一個發布-訂閱框架,我們可以直接拿來進行使用,經過測試基本能夠滿足這里我們所說的需求。
這里還有一點需要說明,客戶端收到訂閱的推送消息后 是主動請求刷新 還是被動請求刷新的問題, 在文章一開始我們看到的QQ的資料查看窗口中,它並沒有訂閱任何東西,僅僅是最簡單的提供一個刷新按鈕,
我們需要根據具體的應用場景來設計自己的需求,在QQ的例子中這應該是最好的方式,而在一個實時信息展示版中最好的應該是實時自動刷新,如果在一個即不需要實時刷新又需要持久展示的頁面來說怎么辦呢?
我認為最好的方式就是,服務端推送消息,客戶端收到訂閱的消息,同時在UI上提示有新的數據,讓用戶自己選擇手動刷新(因為某些場景下我們不能替用戶做主自動去刷新他正在瀏覽的頁面),不知道您同意我的想法嗎?
---------------------------------------------------------------------------------------------------------------------------------
好了,關於分布式如何更好的請求與展示數據這里就不多說了,這方面還是比較復雜的,目前我也希望能夠學習到更多經驗;
我們現在繼續來解決前面UI展示的問題,Frame在這里已經不能滿足我們的需求了,我們考慮用Prism的Region來操作;
如果你對Prism了解的話,應該知道prism框架是與IOC緊密相連的,涉及到IOC與DI 就會設計到生命周期問題,包括Region區域中的內容頁面的生命周期問題;
在Prism框架的應用中,Region區域的內容都是通過 ServiceLocator服務定位器來查找並添加進去的,所以就要求我們的View都配置為 依賴輸出項,以MEF為例就是需要View頁面標注[Export]
對主窗口的改造很簡單,無非是把Frame進行一下替換:
<DockPanel>
<ContentControl MinWidth="100" MaxWidth="200"
DockPanel.Dock="Left"
prism:RegionManager.RegionName="NavigationRegion" />
<ContentControl prism:RegionManager.RegionName="ContentRegion"/>
</DockPanel>
其他的部分也基本與之前提到的相似,比如:
//接收導航參數
this.RegisterToReceiveMessages("Navigate",
new EventHandler<NotificationEventArgs>((sender, e) =>
{
//緩存參數
m_ReceiveArgs = e.Message;
//通知View已接收到導航參數
EventHandler handler = this.Requested;
if (handler != null)
{
handler(this, new EventArgs());
}
}));
這樣的話,就需要在 內容頁面 的View中接收對應的事件做不同的操作,類似如下:
/// <summary>
/// 由ViewModel通知自身已接收導航參數
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ContentPageViewModel_Requested(object sender, EventArgs e)
{
//首先確保內容區域為當前View
m_RegionManager.RequestNavigate("ContentRegion",
"ContentView");
}
/// <summary>
/// 由ViewModel通知異步數據加載失敗
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ContentPageViewModel_Errored(object sender, EventArgs e)
{
StringBuilder bulider = new StringBuilder("ErrorView");
var query = new UriQuery();
query.Add("ErrorMessage", "模擬加載失敗");
bulider.Append(query);
//使內容區域跳轉至ErrorPage
m_RegionManager.RequestNavigate("ContentRegion",
bulider.ToString());
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
ContentViewModel viewModel = this.DataContext as ContentViewModel;
if (viewModel != null)
{
//通知ViewModel異步加載數據
viewModel.AsyncLoad();
}
}
相對來說,如果加載失敗后跳轉到了錯誤頁面,那我們只需要讓Region 進行返回導航就可以了,那通過Region怎么去導航和管理導航歷史?有興趣的可以去參看Prism框架應用!
實際的使用類似如下:
IRegionNavigationJournal m_Journal = null;
public void OnNavigatedTo(NavigationContext navigationContext)
{
//根據參數顯示錯誤信息
m_ErrorMessage = navigationContext.Parameters["ErrorMessage"];
this.NotifyPropertyChanged(m => m.ErrorMessage);
//保存Journal對象
m_Journal = navigationContext.NavigationService.Journal;
}
private void BackCallback()
{
//注意此處:由於目前是模擬操作,ContentViewModel中已經緩存了 導航參數,
//如果緩存的導航參數是Faild,那么這里使用Goback()方法無疑會使主內容區域循環展示為ErrorView
//所以此處暫時使用SendMessage給ContentViewModel的方式來模擬使 “重新刷新”生效
//this.SendMessage("Navigate", new NotificationEventArgs("Success"));
if (m_Journal != null)
{
m_Journal.GoBack();
}
}
以上這部分基本都是關於Prism框架的簡單的應用, 需要注意的就是在這里我們的頁面與之前我們使用Frame導航有所區別,這里的頁面生命周期都是由IOC去控制的,如果沒有特別指明,一般都是單例的;
OK,我們還需要考慮一個問題,前面提到了View頁面的生命周期問題,如果[導航區域]和[內容區域]的展示都出現錯誤,那都需要展示錯誤頁面, 我們的錯誤頁面通過導航歷史去進行返回的時候怎么辦呢?
其實這就是為什么我要提到IOC管理生命周期的原因,我們只需要讓ErrorView 也就是錯誤頁面的生命周期模式為 “NonShared” 就可以了,
例如:
[Export]
[PartCreationPolicy(CreationPolicy.NonShared)]
public partial class ErrorView : UserControl
{
public ErrorView()
{
InitializeComponent();
}
}
有了以上的所有步驟,我們就可以模擬出類似QQ的資料查看窗口的一個用戶體驗效果了(對Loading\Loaded事件沒有添加,有需要完全了解的可以看我的上篇文章中程序示例 點此傳送)!!!
(文中所使用的UI技術以及各類框架都只是我們的一種工具,QQ並沒有用WPF也沒有Prism框架的Region,這里我只是演示一種形式,希望大家能理解我的意思)
最后,結合我們討論的分布式情況下的客戶端數據請求方式,加上這種分區域的利用頁面來展示數據的方式,我想大家一定能夠設計出比較不錯的用戶體驗;
這里將窗口區域的演示項目上傳,希望能幫到有需要的朋友進行學習, 同時非常歡迎與大家討論C/S模式的分布式開發,很希望能向大家請教經驗!
示例項目中需要的第三方框架都是nuget來的,為了保證附件大小沒有打包組建,需要組件的自己nuget就可以;



