這幾天使用MVVM重構這個應用,發現一個嚴重的問題,那就是導航。基於MVVM的思想,View跟ViewModel之間依靠綁定等技術通信,而且是View可以拿到ViewModel,ViewModel不可以拿到View。本來用CodeBehind的時候很容易的導航,到這里就無從下手了。當然也是有辦法把View傳遞到ViewModel的,不過這樣就破壞了MVVM的初衷了。
解決這個問題,首先需要解決怎么在ViewModel中得到NavgationServices來導航。以下是解決辦法:
root = Application.Current.RootVisual as PhoneApplicationFrame;
拿到這個root之后就可以導航了。
root.Navigate(pageUri); 不過這樣直接在ViewModel里導航總感覺比較唐突,而且有個重要的問題,那就是使用CodeBehind時,可以依靠重寫OnNavigatedTo等這種方法來處理的邏輯在ViewModel里如何來處理。當了解了MVVM Light的Message機制之后,我想到了一套解決方案。
MVVM Light的Message機制可以Send一個消息,它會被廣播出去,然后被register的對象接收,然后調用指定的方法。
思路:
當一個VM需要導航的時候,Send一個Message把導航的URL傳遞出去,這個消息被一個NavgationController截獲,執行導航操作,導航完成之后NavgationController會Send一個Message,通知導航到的View對應的ViewModel執行Navigated方法。
NavigationHelper:
using System; using System.Collections.Generic; using System.Windows; using Microsoft.Phone.Controls; using GalaSoft.MvvmLight.Messaging; namespace MvvmLightNavgation { public class NavigationHelper { private static PhoneApplicationFrame root; public static PhoneApplicationFrame GetPhoneFrameRoot() {if (root == null) { root = Application.Current.RootVisual as PhoneApplicationFrame; if (root == null) { throw new Exception("獲取 ApplicationRootVisual 失敗!"); } }return root;
} /// <summary> /// 根據url字符串導航 /// </summary> /// <param name="url"></param> public static void NavigationTo(string url) { if (root==null) GetPhoneFrameRoot(); if(root !=null) { var pageUri = new Uri(url, UriKind.Relative); root.Navigate(pageUri); } } /// <summary> /// 根據Uri導航 /// </summary> /// <param name="pageUri"></param> public static void NavigationTo(Uri pageUri) { if (root == null) GetPhoneFrameRoot(); if (root != null) { root.Navigate(pageUri); } } public static Uri CreateUri(string url) { return new Uri(url, UriKind.RelativeOrAbsolute); } /// <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); } /// <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); } } } }
這個類提供了一堆靜態方法來實現頁面之間的導航。其中最重要的方法是:
/// <summary> /// 發送導航Msg /// </summary> /// <param name="pageUrl"></param> public static void NavigationMsgSend(string pageUrl) { Messenger.Default.Send(CreateUri(pageUrl), MsgToken.Navigation); } 這個方法會發送一個導航的消息,這個消息會被導航控制器攔截到。
/// <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); } }
這個方法注冊一個導航完成的接受對象及方法,攔截導航控制器發出的完成消息。
NavigationController :
using GalaSoft.MvvmLight.Messaging; namespace MvvmLightNavgation { public class NavigationController { public NavigationController() { Messenger.Default.Register<Uri>(this, MsgToken.Navigation, Navigation);NavigationHelper.GetPhoneFrameRoot().Navigated += new System.Windows.Navigation.NavigatedEventHandler(RootNavigated); } private void Navigation(Uri uri) { NavigationHelper.NavigationTo(uri); }
private 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); }
這個方法是導航完成之后的回調方法。它將發送一個以導航到的View對應的Url為Token的Message。
} }
導航控制器,攔截所有導航的消息,然后導航,導航完成之后發送消息通知導航到的View對應的VM執行Navigated方法。
導航接口:
namespace MvvmLightNavgation { public interface INavigation { /// <summary> /// 獲取對應的View的Url /// </summary> /// <returns></returns> string GetViewUrl(); /// <summary> /// 導航完成后發生 /// </summary> /// <param name="uri"></param> void Navigated(Uri uri); } } 讓VM去實現這個接口,保證所有VM都具有這2個方法。
使用:
在App.xaml里添加一個NavigationController靜態資源
<Application.Resources> <vm:MvvmViewModelLocator xmlns:vm="clr-namespace:DBFM7" x:Key="Locator" /> <nav:NavigationController x:Key="NavCtr"/> </Application.Resources>
在需要導航的地方發送導航消息:
string pageUrl = "/View/MainPage.xaml?Channle=" + hubTitle; NavigationHelper.NavigationMsgSend(pageUrl); 在VM的構造函數中注冊接受導航完成消息的對象。
public ChannelTileViewModel()
{
NavigationHelper.NavigatedMsgReg(this);
}
VM去實現導航接口:
public class ChannelTileViewModel : ViewModelBase,INavigation {
。。。。。。。。。。。。。。。。。。。
/// <summary> /// 導航完成后發生 /// </summary> /// <param name="uri"></param> public void Navigated(Uri uri) { } /// <summary> /// 獲取對應的View的Url /// </summary> /// <returns></returns> public string GetViewUrl() { return "/View/ChannelTile.xaml"; } }
通過Message跟NavigationController這一層的過渡徹底的解決了因導航而帶來的View跟ViewModel的耦合問題。而且可以方便的擴展Navigating,NavigateFailed,NavigateStopped等邏輯。