转载

MVVM-Sidekick 之SendToEventRouterAction使用

WP 开发中点击列表项跳转到详情页是一个很常用的功能,但是有可能项模板中还有其他的区域,比如点击标题跳转到详情页,点击 " " 图标送一个赞,点击 " " 图标踩一下,那如何处理同一个项的不同点击区域呢?

WP7 时代我是这么做的,先在 cs 代码中处理点击事件,然后判断 sender 是项模板里的哪个控件,根据不同控件来做不同的处理。感觉弱爆了!

后来使用了 MVVM-Sidekick 之后,实现这种目的变得方便多了!(韦恩卑鄙快给广告费!)

我会在以下的 Demo 里演示这种高大上的用法。

首先新建一个 WP8.1 MVVM-Sidekick 项目,我手头没有 Win10 的开发机,建 Win10 的也一样。

1. 建立 Model

在项目中添加 Models 文件夹,添加一个 UserInfoItem 类,继承于 BindableBase<UserInfoItem> ,代码如下:

public class UserInfoItem : BindableBase<UserInfoItem>

{

public string UserName

{

get { return _UserNameLocator( this ).Value; }

set { _UserNameLocator( this ).SetValueAndTryNotify(value); }

}

#region Property string UserName Setup

protected Property< string > _UserName = new Property< string > { LocatorFunc = _UserNameLocator };

static Func<BindableBase, ValueContainer< string >> _UserNameLocator = RegisterContainerLocator< string >( "UserName" , model => model.Initialize( "UserName" , ref model._UserName, ref _UserNameLocator, _UserNameDefaultValueFactory));

static Func< string > _UserNameDefaultValueFactory = () => { return default ( string ); };

#endregion

public int Age

{

get { return _AgeLocator( this ).Value; }

set { _AgeLocator( this ).SetValueAndTryNotify(value); }

}

#region Property int Age Setup

protected Property< int > _Age = new Property< int > { LocatorFunc = _AgeLocator };

static Func<BindableBase, ValueContainer< int >> _AgeLocator = RegisterContainerLocator< int >( "Age" , model => model.Initialize( "Age" , ref model._Age, ref _AgeLocator, _AgeDefaultValueFactory));

static Func< int > _AgeDefaultValueFactory = () => { return default ( int ); };

#endregion

}

之前已经说过了,使用 propvm 代码段可以快速生成以上的属性。

2. 初始化数据源

打开 MainPage_Model.cs 文件,使用 propvm 代码段添加一个 ObservableCollection 列表:

public ObservableCollection<UserInfoItem> UserInfoItemList

{

get { return _UserInfoItemListLocator( this ).Value; }

set { _UserInfoItemListLocator( this ).SetValueAndTryNotify(value); }

}

#region Property ObservableCollection<UserInfoItem> UserInfoItemList Setup

protected Property<ObservableCollection<UserInfoItem>> _UserInfoItemList = new Property<ObservableCollection<UserInfoItem>> { LocatorFunc = _UserInfoItemListLocator };

static Func<BindableBase, ValueContainer<ObservableCollection<UserInfoItem>>> _UserInfoItemListLocator = RegisterContainerLocator<ObservableCollection<UserInfoItem>>( "UserInfoItemList" , model => model.Initialize( "UserInfoItemList" , ref model._UserInfoItemList, ref _UserInfoItemListLocator, _UserInfoItemListDefaultValueFactory));

static Func<ObservableCollection<UserInfoItem>> _UserInfoItemListDefaultValueFactory = () => { return new ObservableCollection<UserInfoItem>(); };

#endregion

注意在 _UserInfoItemListDefaultValueFactory 里我改成了返回了一个 new 出来的 ObservableCollection<UserInfoItem> ,避免直接使用时因没有初始化而报错。这一步也可以放在 MainPage_Model 的构造函数里。

然后找到下面被注释掉的 OnBindedViewLoad 方法,初始化数据源:

///       <summary>        
      /// This will be invoked by view when the view fires Load event and this viewmodel instance is already in view's ViewModel property 
      ///        </summary>         
      ///        <param name="view">View that firing Load event</param>           
      ///        <returns>Task awaiter</returns>           
      protected        override Task OnBindedViewLoad(MVVMSidekick.Views.IView view) 
        { 
      if (!UserInfoItemList.Any()) 
            { 
                UserInfoItemList.Add(new UserInfoItem { UserName = "Jack", Age = 20 }); 
                UserInfoItemList.Add(new UserInfoItem { UserName = "Tom", Age = 21 }); 
                UserInfoItemList.Add(new UserInfoItem { UserName = "Lily", Age = 18 }); 
                UserInfoItemList.Add(new UserInfoItem { UserName = "Jim", Age = 20 }); 
                UserInfoItemList.Add(new UserInfoItem { UserName = "Bob", Age = 22 }); 
            } 
      return        base.OnBindedViewLoad(view); 
        } 

随便写上几个就好。

3. 绑定数据

为了方便使用 Blend ,还需要增加设计视图支持。把以上初始化数据的代码,加到 MainPage_Model 的构造函数里,放在 if (IsInDesignMode ) 里面:

public MainPage_Model()

{

if (IsInDesignMode )

{

Title = "Title is a little different in Design mode" ;

UserInfoItemList.Add( new UserInfoItem { UserName = "Jack" , Age = 20 });

UserInfoItemList.Add( new UserInfoItem { UserName = "Tom" , Age = 21 });

UserInfoItemList.Add( new UserInfoItem { UserName = "Lily" , Age = 18 });

UserInfoItemList.Add( new UserInfoItem { UserName = "Jim" , Age = 20 });

UserInfoItemList.Add( new UserInfoItem { UserName = "Bob" , Age = 22 });

}

}

然后编译一下,用 Blend 打开。

在MainPage中添加一个ListView,绑定ItemsSource属性:

MVVM-Sidekick 之SendToEventRouterAction使用

刚绑定上是这个样子:

MVVM-Sidekick 之SendToEventRouterAction使用

接下来需要设置项模板,具体步骤就不说了,能显示出内容就行:

MVVM-Sidekick 之SendToEventRouterAction使用

4. 添加详情页

添加一个UserInfoDetailPage,在 UserInfoDetailPage_Model 文件中添加属性:

public UserInfoItem CurrentUserInfoItem 
        { 
      get { return _CurrentUserInfoItemLocator(this).Value; } 
      set { _CurrentUserInfoItemLocator(this).SetValueAndTryNotify(value); } 
        } 
      #region Property UserInfoItem CurrentUserInfoItem Setup 
      protected Property<UserInfoItem> _CurrentUserInfoItem = new Property<UserInfoItem> { LocatorFunc = _CurrentUserInfoItemLocator }; 
      static Func<BindableBase, ValueContainer<UserInfoItem>> _CurrentUserInfoItemLocator = RegisterContainerLocator<UserInfoItem>("CurrentUserInfoItem", model => model.Initialize("CurrentUserInfoItem", ref model._CurrentUserInfoItem, ref _CurrentUserInfoItemLocator, _CurrentUserInfoItemDefaultValueFactory)); 
      static Func<UserInfoItem> _CurrentUserInfoItemDefaultValueFactory = () => { return          default(UserInfoItem); }; 
      #endregion       

然后修改 UserInfoDetailPage_Model 的构造函数,注意,每个VM必须有一个无参的构造函数,如果要传值的话,要手动把无参的构造函数也加上。同时别忘了把CurrentUserInfoItem绑定到页面上。

      public UserInfoDetailPage_Model() 
        { } 
      public UserInfoDetailPage_Model(UserInfoItem item) 
        { 
            CurrentUserInfoItem = item; 
        } 

5. 使用 InvokeCommandAction 导航到详情页面

接下来就要实现我们的目的,点击User列表的时候,导航到详情页。首先看第一种方式,使用InvokeCommandAction:

在Blend中编辑MainPage,拖一个InvokeCommandAction到ListView上:

MVVM-Sidekick 之SendToEventRouterAction使用

Behavior事件选择SelectionChanged:

MVVM-Sidekick 之SendToEventRouterAction使用

MainPage_Model 中添加一个 Command,使用propcmd代码段来生成:

      public CommandModel<ReactiveCommand, String> CommandNavToDetailByInvokeCommand 
        { 
      get { return _CommandNavToDetailByInvokeCommandLocator(this).Value; } 
      set { _CommandNavToDetailByInvokeCommandLocator(this).SetValueAndTryNotify(value); } 
        } 
      #region Property CommandModel<ReactiveCommand, String> CommandNavToDetailByInvokeCommand Setup 
      protected Property<CommandModel<ReactiveCommand, String>> _CommandNavToDetailByInvokeCommand = new Property<CommandModel<ReactiveCommand, String>> { LocatorFunc = _CommandNavToDetailByInvokeCommandLocator }; 
      static Func<BindableBase, ValueContainer<CommandModel<ReactiveCommand, String>>> _CommandNavToDetailByInvokeCommandLocator = RegisterContainerLocator<CommandModel<ReactiveCommand, String>>("CommandNavToDetailByInvokeCommand", model => model.Initialize("CommandNavToDetailByInvokeCommand", ref model._CommandNavToDetailByInvokeCommand, ref _CommandNavToDetailByInvokeCommandLocator, _CommandNavToDetailByInvokeCommandDefaultValueFactory)); 
      static Func<BindableBase, CommandModel<ReactiveCommand, String>> _CommandNavToDetailByInvokeCommandDefaultValueFactory = 
            model => 
            { 
      var resource = "NavToDetailByInvokeCommand";           // Command resource             
      var commandId = "NavToDetailByInvokeCommand"; 
      var vm = CastToCurrentType(model); 
      var cmd = new ReactiveCommand(canExecute: true) { ViewModel = model }; //New Command Core             
                cmd.DoExecuteUIBusyTask( 
                        vm, 
      async e => 
                        { 
      //Todo: Add NavToDetailByInvokeCommand logic here, or       
      await MVVMSidekick.Utilities.TaskExHelper.Yield(); 
      var item = e.EventArgs.Parameter as UserInfoItem; 
                            if(item != null) 
                            { 
                                await vm.StageManager.DefaultStage.Show(new UserInfoDetailPage_Model(item)); 
                            } 
                        }) 
                    .DoNotifyDefaultEventRouter(vm, commandId) 
                    .Subscribe() 
                    .DisposeWith(vm); 
      var cmdmdl = cmd.CreateCommandModel(resource); 
                cmdmdl.ListenToIsUIBusy( 
                    model: vm, 
                    canExecuteWhenBusy: false); 
      return cmdmdl; 
            }; 
      #endregion       

注意看红色的部分,首先获取Command的参数,然后使用StageManager去导航到详情页,并在详情页的构造函数里传递一个参数。

然后编译一下,在Blend里把Command绑定到Action上:

MVVM-Sidekick 之SendToEventRouterAction使用

Command的参数绑定到ListView的SelectedItem上:

MVVM-Sidekick 之SendToEventRouterAction使用

这样在Command里面就可以获取到点击的是哪个item了。

运行一下看看,可以根据点击项来导航了。

6. 使用 SendToEventRouterAction 来导航到详情页面

看了上面的大家觉得也太简单了,下面给大家介绍一个好东西,MVVM-Sidekick里的SendToEventRouterAction。

这个Action顾名思义就是将Event发送到一个 Router里来处理,Router可以是当前VM的,也可以是全局的,我一般喜欢使用全局的方式,比如在好多页面可能都有文章列表,这些列表点击后的动作都是相同的,都是导航到文章详情页面,使用全局的Router,只需要处理一次就可以了。

在MainPage里再添加一个ListView,也绑定到相同的数据源UserInfoItemList上,项模板复制一下之前的,命名为 SendToRouterUserInfoItemDataTemplate 两个ListView用的是不同的项模板,不要弄混了。

MVVM-Sidekick 之SendToEventRouterAction使用

这次我们不用InvokeCommandAction了,在项模板里做文章。编辑 SendToRouterUserInfoItemDataTemplate 项模板,拖一个SendToEventRouterAction到根Grid上 :

MVVM-Sidekick 之SendToEventRouterAction使用

Behavior的事件选择Tapped:

MVVM-Sidekick 之SendToEventRouterAction使用

EventRoutingName设置为NavToDetailByEventRouter,选中 IsEventFiringToAllBaseClassesChannels EventData 自定义表达式输入 { Binding } :

MVVM-Sidekick 之SendToEventRouterAction使用

在XAML里看起来是这样的:

<Interactivity:Interaction.Behaviors> 
                    <Core:EventTriggerBehavior EventName="Tapped"> 
                        <Behaviors:SendToEventRouterAction EventRoutingName="NavToDetailByEventRouter" IsEventFiringToAllBaseClassesChannels="True" EventData="{Binding}"/> 
                    </Core:EventTriggerBehavior> 
                </Interactivity:Interaction.Behaviors> 

然后来处理Router,在 MainPage_Model 里添加一个订阅命令的方法:

private       void SubscribeCommand() 
        { 
       //一般列表项点击事件      
            MVVMSidekick.EventRouting.EventRouter.Instance.GetEventChannel<Object>() 
                .Where(x => x.EventName == "NavToDetailByEventRouter") 
                     .Subscribe( 
      async e => 
                         { 
      var item = e.EventData as UserInfoItem; 
                             if (item != null) 
                             { 
                                 await StageManager.DefaultStage.Show(new UserInfoDetailPage_Model(item)); 
                             } 
                         } 
                     ).DisposeWith(this); 
        } 

注意看红色的部分,通过EventData来获取绑定的Model,进行下一步操作。

别忘了在Load事件中调用此方法。调用的时候注意最好加个flag避免重复订阅。

现在运行一下看看,下面的ListView也可以导航到详情页了。

需要说明的是,当前VM也有个EventRouter,如果要绑定到当前VM的EventRouter,XAML里要写明绑定到当前的EventRouter:

<Behaviors:SendToEventRouterAction EventRoutingName="NavToArticle" EventData="{Binding}" EventRouter="{Binding ElementName=LayoutRoot, Path=DataContext.EventRouter}" /> 

订阅的时候这样写:

this .LocalEventRouter.GetEventChannel< Object >() 。。。后面的一样

和全局的相比就是如果离开当前的 VM 此订阅就无效了。

7. 处理不同区域的点击事件

下面更进一步,实现点击项的不同区域分别进行处理。

在项模板里添加两个按钮,想实现这样的功能,点击一个按钮可以增加 Age ,点击另一个减少 Age

项模板改成这样:

MVVM-Sidekick 之SendToEventRouterAction使用

分别拖两个 Action 到按钮上, XAML 变成这样:

<StackPanel Grid.Row="2" Orientation="Horizontal"> 
                    <AppBarButton HorizontalAlignment="Stretch" Icon="Like" Label="Good" VerticalAlignment="Stretch"> 
                        <Interactivity:Interaction.Behaviors> 
                            <Core:EventTriggerBehavior EventName="Click"> 
                                <Behaviors:SendToEventRouterAction EventData="{Binding}" IsEventFiringToAllBaseClassesChannels="True" EventRoutingName="AddAge"/> 
                            </Core:EventTriggerBehavior> 
                        </Interactivity:Interaction.Behaviors> 
                    </AppBarButton> 
                    <AppBarButton HorizontalAlignment="Stretch" Icon="Dislike" Label="boo" VerticalAlignment="Stretch"> 
                        <Interactivity:Interaction.Behaviors> 
                            <Core:EventTriggerBehavior EventName="Click"> 
                                <Behaviors:SendToEventRouterAction EventData="{Binding}" EventRoutingName="RemoveAge" IsEventFiringToAllBaseClassesChannels="True"/> 
                            </Core:EventTriggerBehavior> 
                        </Interactivity:Interaction.Behaviors> 
                    </AppBarButton> 
                </StackPanel> 

订阅事件:

MVVMSidekick.EventRouting.EventRouter.Instance.GetEventChannel<Object>() 
                .Where(x => x.EventName == "AddAge") 
                     .Subscribe( 
                         e => 
                         { 
      var item = e.EventData as UserInfoItem; 
      if (item != null) 
                             { 
                                 item.Age++; 
                             } 
                         } 
                     ).DisposeWith(this); 
            MVVMSidekick.EventRouting.EventRouter.Instance.GetEventChannel<Object>() 
                .Where(x => x.EventName == "RemoveAge") 
                     .Subscribe( 
                         e => 
                         { 
      var item = e.EventData as UserInfoItem; 
      if (item != null) 
                             { 
                                 item.Age--; 
                             } 
                         } 
                     ).DisposeWith(this); 

跑一下试试,竟然点击按钮的时候也触发了导航事件,那需要把导航的 Action 从根 Grid 转移一下,和按钮的 Action 分开即可。

打完收工!

你们过节,我写博客……

正文到此结束
Loading...