Windows Phone开发:「优雅」地将NavigationHelper和Frame对象传入ViewModel中
最近因为项目需要接触了Windows Phone 8.1的开发,熟悉了基本的概念后,发现微软默认给了几个Common的类帮助构建应用,分别是NavigationHelper用于帮助页面之间的导航,然后其通过和SuspensionManager这个类来实现页面状态的保存和恢复。当然SuspensionManager类给出的默认序列化器效率并不是很好,但是很容易定制自己的解决方案,在此不再多言。
因之前做过WPF和Silverlight的开发,所以打算继续采用MVVM模型来进行开发。根据习惯,采用MVVMLight库。该库轻量而且相关组件都比较直接,可定制性很强。然后开始尝试开发,主要遇到了问题:
- 页面切换一般通过Frame来进行,如果涉及到页面切换的逻辑,就需要在ViewModel能拿到该页面对应的Frame对象。
- 每个页面的状态保存的时机一般通过监听NavigationHelper的LoadState和SaveState事件来完成,就MVVM而言,也就是需要将ViewModel的状态序列化或反序列化。NavigationHelper一般在Page初始化时构造,然后传入该Page对象,因为在其构造函数中,要通过监听Page对象的几个事件来完成其功能。也就是说,如果要想在ViewModel中处理页面状态的序列化和反序列化工作,也就需要能拿到每个页面对应的NavigationHelper对象。
然后我想达成的是:
- 通过一种通用的形式来「优雅」将Frame对象和NavigationHelper对象传入ViewModel中,而不需要将一部分逻辑写在页面的.cs文件中。
- ViewModel模型的绑定在xaml中完成,力求清晰直观。
- 尽量沿用标准的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。
当然如果您有更好的方案也欢迎提出来。欢迎指正。