問題
在使用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:

