Windows Phone——MVVM模式下页面导航及导航事件处理

问题

在使用MVVM模式(使用了MvvmLight框架)开发Windows Phone应用的时候,遇到如下问题:

  1. ViewModel中如何实现导航?
  2. ViewModel中如何处理OnNavigatedTo事件?
  3. 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方法。

具体步骤是:

  1. 定义NavigationHelper类,实现真正的导航功能以及导航消息的发送。
    NavigationHelper类的GetPhoneFrameRoot方法其实是实现了PhoneApplicationFrame的单例模式,通过注册PhoneApplicationFrameNavigated事件,达到了截获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方法
        }
    }
  2. 定义INavigation接口,来约束ViewModel实现Navigated行为。
    public interface INavigation
    {
            /// <summary>
            /// 获取对应的View的Url
            /// </summary>
            /// <returns></returns>
            string GetViewUrl();
            /// <summary>
            /// 导航完成后发生
            /// </summary>
            /// <param name="uri"></param>
            void Navigated(Uri uri);
    }
  3. 定义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();
            }
        }
    }

具体用法

  1. 修改页面的XAML,使之继承自PhoneApplicationPageBase,同时修改.cs文件;
    xaml
    cs
  2. 所有作为页面DataContext的ViewModel继承基类NavigationViewModelBase;
    viewmodel
  3. 在ViewModel重写基类虚方法,实现相关处理逻辑。
    override

第三种解决方案Demo:

download

 

posted @ 2013-01-24 15:56  brycezhang  阅读(2845)  评论(8编辑  收藏  举报