requestWindowFeature 小做了一下总结,附件里面是源码。
一般的采用View和后台cs中绑定Event的模式会导致view层和逻辑层耦合过紧,所以在开发Wp7客户端的时候我用了MVVM模式,一个VM对应了一个view,对于UI重构后后台逻辑基本不用变化就能够绑定Event。
但是原本后台逻辑中的页面跳转事件在松耦合的view和vm下就变的有点麻烦了。
原来我们可以直接在xaml.cs中重写下面两个方法来达到页面跳入跳出的逻辑的处理。
比如下面的代码,屎一般的代码啊。。。
public partial class DetailPage : PhoneApplicationPage { public DetailPage() { InitializeComponent(); } Parameters parame = new Parameters(); protected override void OnNavigatedTo(NavigationEventArgs e) { string id = null; string uid = null; if (!NavigationContext.QueryString.TryGetValue("id" ,out id) || !NavigationContext.QueryString.TryGetValue("uid" ,out uid)){ Deployment.Current.Dispatcher.BeginInvoke(() => MessageBox.Show("参数错误!")); } this.parame.Add("id", id); this.parame.Add("uid", uid); this.DetailPivot.DataContext = new DetailViewModel(this.parame); } protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) { base.OnNavigatingFrom(e); this.parame.Clear(); } }
这段代码是类似于从ListPage跳转到DetailPage的过程,这个过程中我们肯定要从上一个页面接收到ID来从后端获取信息,这样就出现了一段奇葩一样的代码
this.DetailPivot.DataContext = new DetailViewModel(this.parame);
我们的VM是在跳转后初始化的,而不是直接在View的Ctor中生成,这样的耦合是非常不自然的。
接下来我们要用MVVMLight中的Messenger消息通信机制来实现完全的view和vm的松耦合。
我们要用到ViewModelLocator这样vm反向定位器,并且在app.xml应用初始化的时候进行注册。
类似于这样:
public class ViewModelLocator { public ViewModelLocator() { ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default); SimpleIoc.Default.Register<FeatureViewModel>(); } //Singleton [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This non-static member is needed for data binding purposes.")] public FeatureViewModel FeaturePage { get { return ServiceLocator.Current.GetInstance<FeatureViewModel>(); } } }
由于消息通信时必须是每一个Vm对于与View是Singleton模式,我们用ServiceLocator自带的IOC容器注册这些vm,这里的ServiceLocator对象时取自Microsoft.Practices.ServiceLocation这个MS的Practices项目的DLL。
接下来我们把ViewModelLocator注册到app.xml中
<vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
在view中注入VM
DataContext="{Binding FeaturePage, Source={StaticResource Locator}}"
接下来在注册一个导航的控制器并在初始化的时候生成:
public class NavigationController { public NavigationController() { Messenger.Default.Register<Uri>(this, MsgToken.Navigation, Navigation); } private void Navigation(Uri uri) { NavigationHelper.NavigationTo(uri); } }
app.xml
<nav:NavigationController x:Key="NavCtr"/>
这里的控制器就是一个拦截消息并分发的作用。
每个与view绑定的VM需要实现INavigation这个接口:
public interface INavigation { string GetViewUrl(); void Navigated(Uri uri); }
这个接口是注册执行方法的规范接口。
我们在Helper类中定义两个关键的方法:
public static void NavigationMsgSend(string pageUrl) { Messenger.Default.Send(CreateUri(pageUrl), MessageToken.Navigation); } public static void NavigatedMsgReg(object recipient) { INavigation navigation = recipient as INavigation; if (navigation != null) { Messenger.Default.Register<Uri>(recipient, navigation.GetViewUrl(), navigation.Navigated); } }
最后我们把上面的所有代码运用到vm中。
public class DetailViewModel : BaseViewModel, INavigation { public DetailViewModel() { NavigationHelper.NavigatedMsgReg(this); } public string GetViewUrl() { return "/View/DetailPage.xaml"; } public void Navigated(Uri uri) { this.userId = NavigationHelper.GetQueryString(uri, "uid"); this.postId = NavigationHelper.GetQueryString(uri, "id"); this.Cursor = 0L; this.LoadDetail(); } }
我们在List页面中导航是这样的:
void TapStoryItemAction(object sender) { LongListSelector selector = sender as LongListSelector; PostViewModel o = selector.SelectedItem as PostViewModel; string id = o.PostItem.Id; string uid = o.PostItem.Publisher.Id; NavigationHelper.NavigationMsgSend(String.Format("/View/DetailPage.xaml?id={0}&uid={1}", id, uid)); selector.SelectedItem = null; }
好,大体的过程就是vm在初始化时NavigationHelper.NavigatedMsgReg(this)注册在了消息列表中,当在listpage发起导航时进行Messenger.Default.Send动作,第一件事是触发了控制器中的导航实际动作,这个动作是真正的导航到detail页面,但这个时候和vm是毫无关系的,第二步由于在相应的vm中用自己对应的uri来注册了消息通知,所以这个消息传递到了vm中,同时执行了vm中的Navigated方法,所以我们可以直接在vm中获得到上一个页面传进来的queue字段进操作,这样就完成了整套导航的动作,并且保持view和vm松耦合。
上一篇是讲messager通信机制下的导航,但这种导航方式有诸多问题:
首先需要注册在IOC容器中的VM必须是Singleton模式,因为假如不是单例则每一次生成vm,每一次都会去消息列表中注册一个观察者,这样会导致当出现导航动作的时候,这个VM下的Navigated方法多次执行,你并不知道这个vm应该什么时间点去注销消息列表。
第二点是由于是单例模式下的vm,所以每次的跳出都要执行cleanup操作来重置当前view的数据,会带来一些不必要的麻烦,就好像C#的垃圾回收要你自己做一样。
第三点是有一些情况会出现这个view自跳转本身的view,就像一个ProfileView(个人主页)中会有好友列表,而这个好友列表又要调转到另一个ID的ProfileView,因为前一个view 跳出时已经清空了数据,在新view中重新生成数据,这就导致了返回的时候又要重新的去请求后台并渲染UI,除非你自己维护一个cache来储存这些数据,但是cache的维护导致应用的复杂度提高而且cache何时被合适的销毁又是一个问题。
所以我们对于有些像detailview的view就要采用非单例的模式,每一次的跳转都会生成一个新的vm,而垃圾回收让C#的runtime自己去执行。
好,先从ViewModelLocator开始入手。每次注入vm都采用全新的vm对象。
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This non-static member is needed for data binding purposes.")] public DetailViewModel DetailPage { get { return new DetailViewModel(); } }
属性每一次不会去IOC容器中取单例,而是new一个新的对象,(IOC的完整代码可以看前一篇博文http://leyteris.iteye.com/blog/1539608)
好,这下每次注入的都是新的对象,但是问题来了,要是像上面说的这样每个vm每次都去注册一个消息观察者,一会导致这些vm得不到垃圾回收,二是导致方法重复执行。
我们要看来要放弃这种以消息通信机制为基础的导航。
重新写一个接口取名为NavigationViewModel
public interface INavigationBase { }
public interface INavigatedViewModel : INavigationBase { void OnNavigatedTo(NavigationEventArgs e); void OnNavigatedFrom(NavigationEventArgs e); }
这个接口是要有导航动作的vm实现的。
现在我们改一下控制器:
public class NavigationController { private INavigationBase currentVM; private static PhoneApplicationFrame frame; public NavigationController() { } public NavigationController(PhoneApplicationFrame frame) { frame.Navigated += onRootFrameNavigated; frame.Navigating += onRootFrameNavigating; } private void onRootFrameNavigated(object sender, NavigationEventArgs e) { if (currentVM != null) { if (currentVM is INavigatedViewModel) { (currentVM as INavigatedViewModel).OnNavigatedFrom(e); } currentVM = null; } var page = e.Content as PhoneApplicationPage; if (page != null && page.DataContext is INavigationBase) { currentVM = page.DataContext as INavigationBase; if (currentVM is INavigatedViewModel) { (currentVM as INavigatedViewModel).OnNavigatedTo(e); } } } private void onRootFrameNavigating(object sender, NavigatingCancelEventArgs e) { if (currentVM is INavigatingViewModel) { (currentVM as INavigatingViewModel).OnNavigatingFrom(e); } } private static void GetPhoneFrameRoot() { if (frame == null) { frame = Application.Current.RootVisual as PhoneApplicationFrame; if (frame == null) { throw new Exception("获取 ApplicationRootVisual 失败!"); } } } public static void NavigationTo(string url) { if (frame == null) GetPhoneFrameRoot(); if (frame != null) { var pageUri = new Uri(url, UriKind.Relative); frame.Navigate(pageUri); } } public static void NavigationTo(Uri pageUri) { if (frame == null) GetPhoneFrameRoot(); if (frame != null) { frame.Navigate(pageUri); } } }
再在定位器中写一个初始化这个控制器的静态方法;
private static NavigationController navController; public static void InitNavigationController(PhoneApplicationFrame frame) { navController = new NavigationController(frame); }
在app.xaml.cs 中初始化,将RootFrame传入
ViewModelLocator.InitNavigationController(RootFrame);
最后直接在vm中实现NavigationViewModel即可:
public class DetailViewModel : BaseViewModel, INavigatedViewModel //, INavigation { public DetailViewModel() { //NavigationHelper.NavigatedMsgReg(this); } public void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e) { Uri uri = e.Uri; if (e.NavigationMode == System.Windows.Navigation.NavigationMode.Back) return; this.userId = NavigationHelper.GetQueryString(uri, "uid"); this.postId = NavigationHelper.GetQueryString(uri, "id"); //this.Cursor = 0L; this.LoadDetail(); } public void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e) { } }
我们可以取消任何的reset操作,现在已经不是单例了。
下面是导航代码
NavigationController.NavigationTo(String.Format("/View/ProfilePage.xaml?uid={0}", this.PostItem.Publisher.Id));
好,再大体上说一下过程,这次的过程比消息通信机制的导航要简单很多,最最重要的代码就下面这些
var page = e.Content as PhoneApplicationPage; if (page != null && page.DataContext is INavigationBase) { currentVM = page.DataContext as INavigationBase; if (currentVM is INavigatedViewModel) { (currentVM as INavigatedViewModel).OnNavigatedTo(e); } }
就直接将当前的跳转后的DataContent取出来执行里面的OnNavigatedTo等方法,控制器这次的作用主要还是导航路由和触发导航事件后的执行路由分发事件方法。
这种导航方式在单例和非单例的vm中都可以使用,因为它不依赖与vm,而只是关联了view对应的作为DataContent的vm。目前的项目我都是采用这种导航方式,比较方便