問題
在使用MVVM模式(使用了MvvmLight框架)開發Windows Phone應用的時候,遇到如下問題:
- ViewModel中如何實現導航?
- ViewModel中如何處理OnNavigatedTo事件?
- ViewModel中如何處理所有界面導航事件(OnNavigatedTo、OnNavigatingFrom及OnNavigatedFrom)?
這里將第2和第3個問題分開,是因為如果只需處理OnNavigatedTo事件,有一個不得不提十分巧妙的解決辦法,后文具體展開。
ViewModel中實現導航
在ViewModel中實現導航有多種辦法,google一下就能搜到很多種做法,最簡單的莫過於定義一個Navigator類,在類中獲取PhoneApplicationFrame進行導航:
public class Navigator { public Navigator() { _frame = ((App)App.Current).RootFrame; } public void Navigate(string uriString) { _frame.Navigate(new Uri(uriString, UriKind.Relative)); } public void GoBack() { if (_frame.CanGoBack) { _frame.GoBack(); } } private PhoneApplicationFrame _frame; }
然后在App.cs中定義一個靜態Navigator類型的屬性即可實現全局的調用。
public partial class App : Application { private static Navigator _navigator = null; public static Navigator Navigator { get { return _navigator ?? (_navigator = new Navigator()); } } ... }
如果你也一樣使用了MvvmLight框架,可以參考Laurent Bugnion (GalaSoft) 的這篇博文《Navigation in a #WP7 application with MVVM Light》。
想要實現基本的導航功能相對來說比較簡單,核心就是使用PhoneApplicationFrame。
ViewModel中處理OnNavigatedTo事件
關於如何在ViewModel中實現導航以及處理OnNavigatedTo事件,目前個人覺得實現的最為巧妙的就是 Agile.Zhou(kklldog) 在博文《豆瓣電台WP7客戶端 MVVM重構記錄之使用MVVM Light的Message實現導航》中介紹的使用一個NavgationController對象來統一實現導航的方法。這里簡單描述一下原理及用法。
原文用一句話介紹了思路,這里引用一下:
當一個VM需要導航的時候,Send一個Message把導航的URL傳遞出去,這個消息被一個NavgationController截獲,執行導航操作,導航完成之后NavgationController會Send一個Message,通知導航到的View對應的ViewModel執行Navigated方法。
具體步驟是:
- 定義NavigationHelper類,實現真正的導航功能以及導航消息的發送。
NavigationHelper類的GetPhoneFrameRoot方法其實是實現了PhoneApplicationFrame的單例模式,通過注冊PhoneApplicationFrame的Navigated事件,達到了截獲OnNavigatedTo事件的目的,同時在事件方法中發送導航完成的消息。private static void GetPhoneFrameRoot() { ...... _root = Application.Current.RootVisual as PhoneApplicationFrame; ...... _root.Navigated += new System.Windows.Navigation.NavigatedEventHandler(RootNavigated); } private static void RootNavigated(object sender, System.Windows.Navigation.NavigationEventArgs e) { string token = e.Uri.OriginalString; if (token.Contains("?")) { int index = e.Uri.OriginalString.IndexOf('?'); token = token.Substring(0, index); } Messenger.Default.Send(e.Uri,token);// 發送導航完成的消息 }
NavigationMsgSend方法實現發送導航的消息/// <summary> /// 發送導航Msg /// </summary> /// <param name="pageUri"></param> public static void NavigationMsgSend(Uri pageUri) { Messenger.Default.Send(pageUri, MsgToken.Navigation); } /// <summary> /// 發送導航Msg /// </summary> /// <param name="pageUrl"></param> public static void NavigationMsgSend(string pageUrl) { Messenger.Default.Send(CreateUri(pageUrl), MsgToken.Navigation); }
NavigatedMsgReg方法實現接收導航完成的消息/// <summary> /// 注冊導航完成MSG /// </summary> public static void NavigatedMsgReg(object recipient) { INavigation navigation = recipient as INavigation; if (navigation!=null) { Messenger.Default.Register<Uri>(recipient, navigation.GetViewUrl(), navigation.Navigated);// 調用實現INavigation接口的ViewModel的Navigated方法 } }
- 定義INavigation接口,來約束ViewModel實現Navigated行為。
public interface INavigation { /// <summary> /// 獲取對應的View的Url /// </summary> /// <returns></returns> string GetViewUrl(); /// <summary> /// 導航完成后發生 /// </summary> /// <param name="uri"></param> void Navigated(Uri uri); }
- 定義NavigationController,接收導航消息,實現真正的導航動作。
public class NavigationController { public NavigationController() { Messenger.Default.Register<Uri>(this, MsgToken.Navigation, Navigation); } private void Navigation(Uri uri) { NavigationHelper.NavigationTo(uri); } }
具體用法詳見源碼,下載地址:http://dbfm7.codeplex.com/
我在使用這種做法的時候,發現每次在ViewModel的構造函數中都要通過調用NavigationHelper的NavigatedMsgReg方法來注冊接收導航完成的消息,這實在是繁瑣,既然每次都要注冊,能不能實現“自動”注冊呢?
我們都知道MVVM模式同MVC相同,提倡“約定大於配置”的思想,即把所有界面放在Views文件夾,頁面的路徑通常就是/Views/xxxx.xaml,那么就可以通過獲取頁面的強類型來得到頁面的名稱,從而到頁面的路徑。
具體做法是,修改INavigation接口為NavigationViewModelBase基類,該基類直接繼承MvvmLight的ViewModelBase
public abstract class NavigationViewModelBase : ViewModelBase { /// <summary> /// 注冊導航 /// 約定頁面路徑為/Views/... /// </summary> /// <param name="page">頁面類型</param> protected NavigationViewModelBase(Type page) { var urlToken = string.Format("/Views/{0}.xaml", page.ToString().Split('.').LastOrDefault()); Messenger.Default.Register<Uri>(this, urlToken, Navigationed); } /// <summary> /// 導航完成回調函數 /// </summary> /// <param name="url"></param> protected abstract void Navigationed(Uri url); }
ViewModel則這樣寫:
public class MainViewModel : NavigationViewModelBase { public MainViewModel() : base(typeof(MainPage)) { ... } ... protected override void Navigationed(Uri url) { ... } }
ViewModel中處理所有界面導航事件
在Windows Phone開發中,除非項目實在非常簡單,我們基本無法回避要使用頁面的OnNavigatedTo、OnNavigatingFrom、OnNavigatedFrom事件,來處理一下進入或離開頁面的邏輯,那么如何在ViewModel中直接使用這些事件呢?
我在stackoverflow見到通過在頁面的相關事件中發送消息到ViewModel的方式來實現在ViewModel處理這些事件(Handling the OnNavigatedFrom / OnNavigatedTo events in the ViewModel),這種做法雖然簡單直接,但在每個頁面的Code-behind里都這么發消息總感覺十分的蹩腳。
那么使用一個繼承自PhoneApplicationPage的頁面基類來統一做這些事情是個不錯的選擇。順着這個思路,一番google之后,在github的源碼庫里看到名為JoshClose已經這么做了,其實現簡單明了,我根據需要修改如下:
定義頁面基類PhoneApplicationPageBase
PhoneApplicationPageBase繼承自PhoneApplicationPage,作為所有頁面的基類,通過DataContext來調用自定義的NavigationViewModelBase基類中已定義的相關方法,實現ViewModel與頁面的解耦。
public class PhoneApplicationPageBase : PhoneApplicationPage { protected PhoneApplicationPageBase() { Loaded += PageBaseLoaded; } private void PageBaseLoaded(object sender, RoutedEventArgs e) { var viewModel = DataContext as NavigationViewModelBase; if (viewModel != null) { viewModel.NavigationService = NavigationService; } } protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e) { base.OnNavigatedTo(e); var viewModel = DataContext as NavigationViewModelBase; if (viewModel != null) { viewModel.NavigationContext = NavigationContext; viewModel.OnNavigatedTo(e); } } protected override void OnNavigatingFrom(System.Windows.Navigation.NavigatingCancelEventArgs e) { base.OnNavigatingFrom(e); var viewModel = DataContext as NavigationViewModelBase; if (viewModel != null) { viewModel.NavigationContext = NavigationContext; viewModel.OnNavigatingFrom(e); } } protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e) { base.OnNavigatedFrom(e); var viewModel = DataContext as NavigationViewModelBase; if (viewModel != null) { viewModel.NavigationContext = NavigationContext; viewModel.OnNavigatedFrom(e); } } }
定義ViewModel基類NavigationViewModelBase
通過定義ViewModel的基類,來實現行為約束和基本的實現,同時也可以直接獲取並使用NavigationService和NavigationContext對象。
public abstract class NavigationViewModelBase : ViewModelBase { protected bool RemoveBackEntry { get; set; } public NavigationService NavigationService { get; set; } public NavigationContext NavigationContext { get; set; } public virtual void OnNavigatedTo(NavigationEventArgs e) { } public virtual void OnNavigatingFrom(NavigatingCancelEventArgs e) { } public virtual void OnNavigatedFrom(NavigationEventArgs e) { if (RemoveBackEntry) { RemoveBackEntry = false; NavigationService.RemoveBackEntry(); } } }
具體用法
- 修改頁面的XAML,使之繼承自PhoneApplicationPageBase,同時修改.cs文件;
- 所有作為頁面DataContext的ViewModel繼承基類NavigationViewModelBase;
- 在ViewModel重寫基類虛方法,實現相關處理邏輯。
第三種解決方案Demo: