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().Keyed("TestMain");   // 注册ViewModel
this._container = builder.Build();
		}
		public HiwedoViewModelBase GetValueModel(string key)
		{
			if (!this._container.IsRegisteredWithKey(key))
			{
				throw new Exception("Unregistered viewmodel: " + key);
			}
			return this._container.ResolveKeyed(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();  // 通过全局的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。

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

博客园-原创精华区稿源:博客园-原创精华区 (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 综合技术 » Windows Phone开发:「优雅」地将NavigationHelper和Frame对象传入ViewModel中

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录