Windows Phone开发:「优雅」地将NavigationHelper和Frame对象传入ViewModel中

最近因为项目需要接触了Windows Phone 8.1的开发,熟悉了基本的概念后,发现微软默认给了几个Common的类帮助构建应用,分别是NavigationHelper用于帮助页面之间的导航,然后其通过和SuspensionManager这个类来实现页面状态的保存和恢复。当然SuspensionManager类给出的默认序列化器效率并不是很好,但是很容易定制自己的解决方案,在此不再多言。

因之前做过WPF和Silverlight的开发,所以打算继续采用MVVM模型来进行开发。根据习惯,采用MVVMLight库。该库轻量而且相关组件都比较直接,可定制性很强。然后开始尝试开发,主要遇到了问题:

  1. 页面切换一般通过Frame来进行,如果涉及到页面切换的逻辑,就需要在ViewModel能拿到该页面对应的Frame对象。
  2. 每个页面的状态保存的时机一般通过监听NavigationHelper的LoadState和SaveState事件来完成,就MVVM而言,也就是需要将ViewModel的状态序列化或反序列化。NavigationHelper一般在Page初始化时构造,然后传入该Page对象,因为在其构造函数中,要通过监听Page对象的几个事件来完成其功能。也就是说,如果要想在ViewModel中处理页面状态的序列化和反序列化工作,也就需要能拿到每个页面对应的NavigationHelper对象。

然后我想达成的是:

  1. 通过一种通用的形式来「优雅」将Frame对象和NavigationHelper对象传入ViewModel中,而不需要将一部分逻辑写在页面的.cs文件中。
  2. ViewModel模型的绑定在xaml中完成,力求清晰直观。
  3. 尽量沿用标准的NavigationHelper和SuspensionManager提供的模式。

然后经过思考,我自己给出了一个解决方案,基本上解决了这个问题,自认为效果还不错,所以写出来分享下。

具体思路就是借助控制反转(IoC)和附加属性(Attached Property)来完成ViewModel的绑定。根据习惯,我采用的IoC框架是Autofac。

首先是ViewLocator的代码,熟悉MVVMLight框架的应该比较明白,这个类的默认实现就是将ViewModel注册在这里,然后通过属性绑定到具体的页面中去。这里我们做一些修改,加入附加属性:

    public class ViewModelLocator
    {
        private readonly IContainer _container;  // Autofac Container

        public ViewModelLocator()
        {
            var builder = new ContainerBuilder();
            builder.RegisterType<TestMainModel>().Keyed<ExtendViewModelBase>("TestMain");   // 注册ViewModel
this._container = builder.Build();
        }

        public HiwedoViewModelBase GetValueModel(string key)
        {
            if (!this._container.IsRegisteredWithKey<HiwedoViewModelBase>(key))
            {
                throw new Exception("Unregistered viewmodel: " + key);
            }
            return this._container.ResolveKeyed<HiwedoViewModelBase>(key);
        }

        public static readonly DependencyProperty ViewModelKeyProperty = DependencyProperty.RegisterAttached(
            "ViewModelKey",
            typeof(string),
            typeof(ViewModelLocator),
            new PropertyMetadata(default(string))
            );

        public static void SetViewModelKey(ExtendPage page, string value)
        {
            page.SetValue(ViewModelKeyProperty, value);
            SetViewModelForPage(page, value);
        }

        public static string GetViewModelKey(ExtendPage page)
        {
            return (string)page.GetValue(ViewModelKeyProperty);
        }

        public static void SetViewModelForPage(ExtendPage page, string key)
        {
            var locator = YourServiceLocator.Current.Resolve<ViewModelLocator>();  // 通过全局的ServiceLocator获取单例的ViewModelLocator
            var viewModel = locator.GetValueModel(key);
            viewModel.SetNavigationHelper(page.NavigationHelper);     // 将Page的NavigationHelper传入到ViewModel中
            page.DataContext = viewModel;   // 将ViewModel传入到Page的DataContext中
        }

        public static void Cleanup()
        {

        }
    }

需要注意的是,在SetViewModelForPage方法中,我是通过全局的ServiceLocator来获取单例模式的ViwModelLocator的,当然这里也可以有别的方案,根据自己的需要定制即可。当然在我这种方案中,ViewModelLocator需要在App的OnLaunched()事件中完成初始化。

同时对于传入的Page会发现我并没有直接使用Page类,而是使用了一个ExtendPage类,这个类是我对于Page类的继承类,下面将会列出,这样做的原因是因为需要在该方法中拿到NavigationHelper,为此拓展了Page类,具体实现如下:

    public class ExtendPage : Page
    {
        private readonly NavigationHelper _navigationHelper;

        public ExtendPage()
        {
            this._navigationHelper = new NavigationHelper(this);
        }

        public NavigationHelper NavigationHelper
        {
            get { return this._navigationHelper; }
        }

        #region NavigationHelper registration

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            this._navigationHelper.OnNavigatedTo(e);
        }

        protected override void OnNavigatedFrom(NavigationEventArgs e)
        {
            this._navigationHelper.OnNavigatedFrom(e);
        }

        #endregion
    }

这个类的代码比较简单,基本就是微软给出的示例代码。只不过我增加了一个NavigationHelper的公开属性,具体原因已经在上面提到过了。

这样之后,在页面中绑定ViewModel就可以通过如下代码进行了:

<common:ExtendPage
    x:Class="Hiwedo.WindowsPhone.Views.TestMain"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Hiwedo.WindowsPhone.Views"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:common="using:Hiwedo.WindowsPhone.Common"
    xmlns:viewModel="using:Hiwedo.WindowsPhone.ViewModels"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
    xmlns:i="using:Microsoft.Xaml.Interactivity"
    xmlns:core="using:Microsoft.Xaml.Interactions.Core"
    viewModel:ViewModelLocator.ViewModelKey="TestMain">

最后一行即是通过依赖属性实现DataContext绑定的代码,这里的ViewModelKey应该和在ViewModelLocator注册ViewModel时用到的Key保持一致。同时还要注意,这个Page的类型已经修改成了ExtendPage,不光需要在xaml中修改,同时还要将对应的.cs文件的类型修改成ExtendPage。

最后,需要看一下拓展的ViewModelBase类,也就是将ViewModelBase类做了拓展。

public class ExtendViewModelBase : ViewModelBase
    {
        private NavigationHelper _navigationHelper;

        public  ExtendViewModelBase()
        {

        }

        public HiwedoViewModelBase(NavigationHelper navigationHelper)
        {
            _navigationHelper = navigationHelper;
        }

        public void SetNavigationHelper(NavigationHelper navigationHelper)
        {
            this._navigationHelper = navigationHelper;
            this._navigationHelper.LoadState += NavigationHelper_LoadState;
            this._navigationHelper.SaveState += NavigationHelper_SaveState;
        }

        private async void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
        {
            await LoadStateAsync(sender, e);
        }

        private async void NavigationHelper_SaveState(object sender, SaveStateEventArgs e)
        {
            await SaveStateAsync(sender, e);
        }

        protected virtual async Task LoadStateAsync(object sender, LoadStateEventArgs e) { }
        protected virtual async Task SaveStateAsync(object sender, SaveStateEventArgs e) { }

        protected NavigationHelper NavigationHelper
        {
            get { return _navigationHelper; }
            set { _navigationHelper = value; }
        }
    }

具体写ViewModel的时候,只需要继承ExtendViewModelBase,然后重载LoadStateAsync和SaveStateAsync方法来实现相关状态的加载和保存即可。然后Frame的获取可以通过NavigationHelper中存有的Page对象得到。

以上就是我的解决方案,基本上解决了我提出的几个问题,然后这个解决方案现有的一个问题就是,在开发过程中,因为具体ViewModel的类型并不清楚,所以无法在编写XAML的绑定语句时实现智能提示,不过有一种解决思路就是通过在设计时,通过d:DataContext='xxx'来制定一个设计时用的ViewModel。

当然如果您有更好的方案也欢迎提出来。欢迎指正。

posted on 2015-01-18 19:06  harryttt  阅读(1175)  评论(2编辑  收藏  举报

导航