Windows Phone——MVVM模式下页面导航及导航事件处理
问题
在使用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:
Posted by Bryce Zhang
版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0