WPF MVVM系统入门-下
CommandManager
接上文WPF MVVM系统入门-上,我们想把Command放在ViewModel中,而不是Model中,可以将CommandBase类改为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class CommandBase : ICommand { public event EventHandler? CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested += value; } }
public Func<object,bool> DoCanExecute { get; set; } public bool CanExecute(object? parameter) { return DoCanExecute?.Invoke(parameter) == true; } public void Execute(object? parameter) { DoExecute?.Invoke(parameter); } public Action<object> DoExecute { get; set; } }
|
利用了CommandManager的静态事件RequerySuggested
,该事件当检测到可能改变命令执行条件时触发(实际上是一直不断的触发)。此时Model和ViewModel分别是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class MainModel : INotifyPropertyChanged { public double Value1 { get; set; } public double Value2 { get; set; }
private double _value3;
public double Value3 { get { return _value3; } set { _value3 = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value3")); } } public event PropertyChangedEventHandler? PropertyChanged; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class MainViewModel { public MainModel mainModel { set; get; } = new MainModel();
public void Add(object obj) { mainModel.Value3 = mainModel.Value2 + mainModel.Value1; }
public bool CanCal(object obj) { return mainModel.Value1 != 0; }
public CommandBase BtnCommand { get; set; } public MainViewModel() { BtnCommand = new CommandBase() { DoExecute = new Action<object>(Add), DoCanExecute = new Func<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的宗旨。
1 2 3 4 5 6 7 8 9 10 11 12 13
| <Window.CommandBindings> <CommandBinding CanExecute="CommandBinding_CanExecute" Command="ApplicationCommands.Open" Executed="CommandBinding_Executed" /> </Window.CommandBindings>
<Button Command="ApplicationCommands.Open" CommandParameter="123" Content="Ok" />
|
但是经常使用复制、粘贴等内置命令
1 2 3 4 5 6 7 8
| <TextBox Text="{Binding mainModel.Value1, UpdateSourceTrigger=PropertyChanged}"> <TextBox.ContextMenu> <ContextMenu> <MenuItem Command="ApplicationCommands.Copy" Header="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}" /> <MenuItem Command="ApplicationCommands.Paste" Header="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}" /> </ContextMenu> </TextBox.ContextMenu> </TextBox>
|

鼠标行为
一般Command都有默认触发的行为,如Button的默认触发行为是单机,那如果我想改成双击触发,那要如何实现?使用InputBindings
可以修改触发行为。
1 2 3 4 5 6 7 8 9 10 11 12 13
| <Button Content="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
命令的用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <Window.Resources> <RoutedUICommand x: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"
1 2 3 4 5 6 7 8 9 10
| <ComboBox DisplayMemberPath="Value1" ItemsSource="{Binding list}" SelectedValuePath="Value2"> <Behaviors:Interaction.Triggers> <Behaviors:EventTrigger EventName="SelectionChanged"> <Behaviors:InvokeCommandAction Command="{StaticResource myCommand}" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=ComboBox}, Path=SelectedValue}" /> </Behaviors:EventTrigger> </Behaviors:Interaction.Triggers> </ComboBox>
|
上面的的用法需要绑定命令,也可以直接绑定方法使用
1 2 3 4 5 6 7 8 9 10
| <ComboBox DisplayMemberPath="Value1" ItemsSource="{Binding list}" SelectedValuePath="Value2"> <Behaviors:Interaction.Triggers> <Behaviors:EventTrigger EventName="SelectionChanged"> <Behaviors:CallMethodAction MethodName="ComboBox_SelectionChanged" TargetObject="{Binding}" /> </Behaviors:EventTrigger> </Behaviors:Interaction.Triggers> </ComboBox>
|
这样可以直接绑定ViewModel中定义的方法
本案例使用.net core进行测试,如果使用FrameWork,则这样使用
1 2 3 4 5 6 7 8 9
| xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:ii="http://schemas.microsoft.com/expression/2010/interactions"
<i:EventTrigger EventName="SelectionChanged"> <ii:CallMethodAction TargetObject="{Binding}" MethodName="ComboBox_SelectionChanged"/> </i:EventTrigger>
|
MVVM中跨模块交互
跨模块交互经常会涉及到VM与V之间的交互,通常V绑定VM中的数据是非常简单的,直接使用Bind就可以
但是有时V中需要定义一些方法,让VM去触发,如果互相引用则违背了MVVM的原则(VM不要引用V),此时就需要一个管理类。
V中注册委托,VM中执行
写一个ActionManager,该类具有注册委托和执行委托方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class ActionManager<T> { static Dictionary<string, Func<T, bool>> _actions = new Dictionary<string, Func<T, bool>>(); public static void Register(string name,Func<T,bool> func) { if (!_actions.ContainsKey(name)) { _actions.Add(name, func); } }
public static bool Invoke(string name,T value) { if (_actions.ContainsKey(name)) { return _actions[name].Invoke(value); } return false; } }
|
可以在V中注册
1 2 3 4
| ActionManager<object>.Register("ShowSubWin", new Func<object, bool>(_ => { WindowManager.ShowDialog(typeof(SubWindow).Name,null); return true; }));
|
在VM中执行
1
| ActionManager<object>.Invoke("ShowSubWin", null);
|
V中注册子窗口,VM中打开
可以写一个WindowManager类,该类中可以注册窗口和打开窗口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public class WindowManager { static Dictionary<string, WinEntity> _windows = new Dictionary<string, WinEntity>();
public static void Register(Type type,Window owner) { if (!_windows.ContainsKey(type.Name)) { _windows.Add(type.Name, new WinEntity {Type = type,Owner = owner }); } }
public static bool ShowDialog(string winKey ,object dataContext) { if (_windows.ContainsKey(winKey)) { Type type = _windows[winKey].Type; Window? win = (Window)Activator.CreateInstance(type); win.DataContext = dataContext; win.Owner = _windows[winKey].Owner; return win.ShowDialog()==true; } return false; } } public class WinEntity { public Type Type { get; set; } public Window Owner { get; set; } }
|
此时在主窗口的View中对子窗口进行注册WindowManager.Register(typeof(SubWindow), this);
在VM中打开子窗口WindowManager.ShowDialog("SubWindow", null);
页面切换
在单页面应用中,点击不同的菜单项会跳转到不同的页面,如何利用MVVM来实现该功能?
- 定义菜单模型
1 2 3 4 5 6
| public class MenuModel { public string MenuIcon { get; set; } public string MenuHeader { get; set; } public string TargetView { get; set; } }
|
- 定义MainModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class MainModel : INotifyPropertyChanged { public List<MenuModel> MenuList { get; set; } private object _page;
public object Page { get => _page; set { _page = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Page")); } } public event PropertyChangedEventHandler? PropertyChanged; }
|
- MainViewModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| public class MainViewModel { public MainModel mainModel { get; set; } public MainViewModel() { mainModel = new MainModel(); mainModel.MenuList = new List<MenuModel>(); mainModel.MenuList.Add(new MenuModel { MenuIcon = "\ue643", MenuHeader = "Dashboard", TargetView = "MvvmDemo.Views.DashboardPage", }); mainModel.PageTitle = mainModel.MenuList[0].MenuHeader; ShowPage(mainModel.MenuList[0].TargetView); } private void ShowPage(string target) { var type = this.GetType().Assembly.GetType(target); this.MainModel.Page = Activator.CreateInstance(type); } public CommandBase MenuItemCommand { get => new CommandBase { DoExecute = new Action<object>(obj => { ShowPage(obj.ToString()); }) }; } }
|
- View绑定MenuItemCommand
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <ContentControl Grid.Row="1" Grid.Column="1" Content="{Binding MainModel.Page}" />
<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>
|