一般的采用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松耦合。