WPF MVVM系统入门-下
CommandManager
接上文WPF MVVM系统入门-上,我们想把Command放在ViewModel中,而不是Model中,可以将CommandBase类改为
publicclassCommandBase : ICommand
{
publiceventEventHandler?CanExecuteChanged
{
add { CommandManager.RequerySuggested+=value; }
remove { CommandManager.RequerySuggested+=value; }
}
publicFunc<object,bool>DoCanExecute { get; set; }
publicboolCanExecute(object?parameter)
{
returnDoCanExecute?.Invoke(parameter) ==true;
}
publicvoidExecute(object?parameter)
{
DoExecute?.Invoke(parameter);
}
publicAction<object>DoExecute { get; set; }
}
利用了CommandManager的静态事件RequerySuggested
,该事件当检测到可能改变命令执行条件时触发(实际上是一直不断的触发)。此时Model和ViewModel分别是
//Model
publicclassMainModel : INotifyPropertyChanged
{
publicdoubleValue1 { get; set; }
publicdoubleValue2 { get; set; }
privatedouble_value3;
publicdoubleValue3
{
get { return_value3; }
set
{
_value3=value;
PropertyChanged?.Invoke(this, newPropertyChangedEventArgs("Value3"));
}
}
publiceventPropertyChangedEventHandler?PropertyChanged;
}
//ViewModel
publicclassMainViewModel
{
publicMainModelmainModel { set; get; } =newMainModel();
publicvoidAdd(objectobj)
{
mainModel.Value3=mainModel.Value2+mainModel.Value1;
}
publicboolCanCal(objectobj)
{
returnmainModel.Value1!=0;
}
publicCommandBaseBtnCommand { get; set; }//命令
publicMainViewModel()
{
BtnCommand=newCommandBase() {
DoExecute=newAction<object>(Add),
DoCanExecute=newFunc<object, bool>(CanCal)
};
}
}
执行效果如下
内置命令
上面我们自定义了CommandBase
类,但其实WPF已经预定义了很多常用的命令
MediaCommands(24个) Play、Stop、Pause.......ApplicationCommands(23个) New、Open、Copy、Cut、Print.........NavigationCommands(16个) GoToPage、LastPage、Favorites...ComponentCommands(27个) ScrollByLine、MoveDown、ExtendSelectionDown.........EditingCommands(54个) Delete、ToggleUnderline、ToggleBold.........
命令绑定一般是这样做,此时使用预定义的命令,但是Execute等事件需要写在内置类中,不符合MVVM的宗旨。
<Window.CommandBindings>
<CommandBinding
CanExecute="CommandBinding_CanExecute"
Command="ApplicationCommands.Open"
Executed="CommandBinding_Executed"/>
</Window.CommandBindings>
<!--使用-->
<!--RoutedUICommand-->
<Button
Command="ApplicationCommands.Open"
CommandParameter="123"
Content="Ok"/>
但是经常使用复制、粘贴等内置命令
<TextBoxText="{Binding mainModel.Value1, UpdateSourceTrigger=PropertyChanged}">
<TextBox.ContextMenu>
<ContextMenu>
<MenuItemCommand="ApplicationCommands.Copy"Header="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>
<MenuItemCommand="ApplicationCommands.Paste"Header="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>
</ContextMenu>
</TextBox.ContextMenu>
</TextBox>
鼠标行为
一般Command都有默认触发的行为,如Button的默认触发行为是单机,那如果我想改成双击触发,那要如何实现?使用InputBindings
可以修改触发行为。
<ButtonContent="Ok">
<Button.InputBindings>
<MouseBinding
Command="ApplicationCommands.Open"
CommandParameter="123"
MouseAction="LeftDoubleClick"/>
<KeyBinding
Key="O"
Command="ApplicationCommands.Open"
CommandParameter="123"
Modifiers="Ctrl"/>
</Button.InputBindings>
</Button>
上面的案例可以实现双击按钮和Ctrl+o触发ApplicationCommands.Open
命令。
自定义RoutedUICommand
命令的用法:
<!--定义命令资源-->
<Window.Resources>
<RoutedUICommandx:Key="myCommand"Text="我的命令"/>
</Window.Resources>
<!--定义命令快捷键-->
<Window.InputBindings>
<KeyBinding
Key="Enter"
Command="{StaticResource myCommand}"
Gesture="Ctrl"/>
</Window.InputBindings>
<!--定义命令-->
<Window.CommandBindings>
<CommandBinding
CanExecute="CommandBinding_CanExecute_1"
Command="{StaticResource myCommand}"
Executed="CommandBinding_Executed_1"/>
</Window.CommandBindings>
<!--使用命令-->
<Button
Command="{StaticResource myCommand}"
CommandParameter="123"
Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>
任意事件的绑定
InputBindings
只能对KeyBinding
和MouseBinding
进行绑定,但如果我想要其他的事件,比如ComboBox的SelectionChanged
,此时可以使用System.Windows.Interactivity
。
- 使用行为需要nuget安装
Microsoft.Xaml.Behaviors.Wpf
,FrameWork版本安装System.Windows.Interactivity.WPF
- xaml中引用命名空间
xmlns:Behaviors="http://schemas.microsoft.com/xaml/behaviors"
<ComboBox
DisplayMemberPath="Value1"
ItemsSource="{Binding list}"
SelectedValuePath="Value2">
<Behaviors:Interaction.Triggers>
<Behaviors:EventTriggerEventName="SelectionChanged">
<Behaviors:InvokeCommandActionCommand="{StaticResource myCommand}"CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=ComboBox}, Path=SelectedValue}"/>
</Behaviors:EventTrigger>
</Behaviors:Interaction.Triggers>
</ComboBox>
上面的的用法需要绑定命令,也可以直接绑定方法使用
<ComboBox
DisplayMemberPath="Value1"
ItemsSource="{Binding list}"
SelectedValuePath="Value2">
<Behaviors:Interaction.Triggers>
<Behaviors:EventTriggerEventName="SelectionChanged">
<Behaviors:CallMethodActionMethodName="ComboBox_SelectionChanged"TargetObject="{Binding}"/>
</Behaviors:EventTrigger>
</Behaviors:Interaction.Triggers>
</ComboBox>
这样可以直接绑定ViewModel中定义的方法
本案例使用.net core进行测试,如果使用FrameWork,则这样使用
<!--引用命名空间-->
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ii="http://schemas.microsoft.com/expression/2010/interactions"
<!--使用-->
<i:EventTriggerEventName="SelectionChanged">
<ii:CallMethodActionTargetObject="{Binding}"
MethodName="ComboBox_SelectionChanged"/>
</i:EventTrigger>
MVVM中跨模块交互
跨模块交互经常会涉及到VM与V之间的交互,通常V绑定VM中的数据是非常简单的,直接使用Bind就可以
但是有时V中需要定义一些方法,让VM去触发,如果互相引用则违背了MVVM的原则(VM不要引用V),此时就需要一个管理类。
V中注册委托,VM中执行
写一个ActionManager,该类具有注册委托和执行委托方法
publicclassActionManager<T>
{
staticDictionary<string, Func<T, bool>>_actions=newDictionary<string, Func<T, bool>>();
//注册
publicstaticvoidRegister(stringname,Func<T,bool>func)
{
if (!_actions.ContainsKey(name))
{
_actions.Add(name, func);
}
}
//执行
publicstaticboolInvoke(stringname,Tvalue)
{
if (_actions.ContainsKey(name))
{
return_actions[name].Invoke(value);
}
returnfalse;
}
}
可以在V中注册
ActionManager<object>.Register("ShowSubWin", newFunc<object, bool>(_=> {
WindowManager.ShowDialog(typeof(SubWindow).Name,null);
returntrue;
}));
在VM中执行
ActionManager<object>.Invoke("ShowSubWin", null);
V中注册子窗口,VM中打开
可以写一个WindowManager类,该类中可以注册窗口和打开窗口
publicclassWindowManager
{
//注册窗口存放
staticDictionary<string, WinEntity>_windows=newDictionary<string, WinEntity>();
//注册,传入Type类型,因为注册的时候不需要实例,
//但是owner则需要传入Window,因为要设置owner说明已经有了实例
publicstaticvoidRegister(Typetype,Windowowner)
{
if (!_windows.ContainsKey(type.Name))
{
_windows.Add(type.Name, newWinEntity {Type=type,Owner=owner });
}
}
//使用string类型的winKey,因为调用showDialog方法往往是在VM中,如果使用Type类型,则要在VM中引用View
publicstaticboolShowDialog(stringwinKey ,objectdataContext)
{
if (_windows.ContainsKey(winKey))
{
Typetype=_windows[winKey].Type;
Window?win= (Window)Activator.CreateInstance(type);
win.DataContext=dataContext;
win.Owner=_windows[winKey].Owner;
returnwin.ShowDialog()==true;
}
returnfalse;
}
}
publicclassWinEntity
{
publicTypeType { get; set; }
publicWindowOwner { get; set; }
}
此时在主窗口的View中对子窗口进行注册WindowManager.Register(typeof(SubWindow), this);
在VM中打开子窗口WindowManager.ShowDialog("SubWindow", null);
页面切换
在单页面应用中,点击不同的菜单项会跳转到不同的页面,如何利用MVVM来实现该功能?
- 定义菜单模型
publicclassMenuModel
{
publicstringMenuIcon { get; set; }
publicstringMenuHeader { get; set; }
publicstringTargetView { get; set; }
}
- 定义MainModel
publicclassMainModel : INotifyPropertyChanged
{
publicList<MenuModel>MenuList { get; set; }
/// <summary>
/// 当前点击的页面实例
/// </summary>
privateobject_page;
publicobjectPage
{
get=>_page;
set
{
_page=value;
PropertyChanged?.Invoke(this, newPropertyChangedEventArgs("Page"));
}
}
publiceventPropertyChangedEventHandler?PropertyChanged;
}
- MainViewModel
publicclassMainViewModel
{
publicMainModelmainModel { get; set; }
publicMainViewModel()
{
mainModel=newMainModel();
mainModel.MenuList=newList<MenuModel>();
mainModel.MenuList.Add(newMenuModel
{
MenuIcon="\ue643",// 如果存在数据库的话: e643 这个字符的编号
MenuHeader="Dashboard",
TargetView="MvvmDemo.Views.DashboardPage",// 反射 新建一个UserControl名字为DashboardPage
});
mainModel.PageTitle=mainModel.MenuList[0].MenuHeader;
ShowPage(mainModel.MenuList[0].TargetView);
}
privatevoidShowPage(stringtarget)
{
vartype=this.GetType().Assembly.GetType(target);
this.MainModel.Page=Activator.CreateInstance(type);
}
//定义命令
publicCommandBaseMenuItemCommand
{
get=>newCommandBase
{
// obj希望传进来的一个TargetView
DoExecute=newAction<object>(obj=>
{
ShowPage(obj.ToString());
})
};
}
}
- View绑定MenuItemCommand
<!--ContentControl显示page页面-->
<ContentControl
Grid.Row="1"
Grid.Column="1"
Content="{Binding MainModel.Page}"/>
<!--GroupName是为了互斥-->
<ItemsControl
ItemsSource="{Binding MainModel.MenuList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.MenuItemCommand}"
CommandParameter="{Binding TargetView}"
Content="{Binding MenuHeader}"
GroupName="menu"
Tag="{Binding MenuIcon}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>