WPF MVVM系统入门-下
CommandManager
接上文WPF MVVM系统入门-上,我们想把Command放在ViewModel中,而不是Model中,可以将CommandBase类改为
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分别是
//Model
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;
}
//ViewModel
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的宗旨。
<Window.CommandBindings>
<CommandBinding
CanExecute="CommandBinding_CanExecute"
Command="ApplicationCommands.Open"
Executed="CommandBinding_Executed" />
</Window.CommandBindings>
<!--使用-->
<!--RoutedUICommand-->
<Button
Command="ApplicationCommands.Open"
CommandParameter="123"
Content="Ok" />
但是经常使用复制、粘贴等内置命令
<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
可以修改触发行为。
<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
命令的用法:
<!--定义命令资源-->
<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"
<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>
上面的的用法需要绑定命令,也可以直接绑定方法使用
<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,则这样使用
<!--引用命名空间-->
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,该类具有注册委托和执行委托方法
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中注册
ActionManager<object>.Register("ShowSubWin", new Func<object, bool>(_ => {
WindowManager.ShowDialog(typeof(SubWindow).Name,null);
return true;
}));
在VM中执行
ActionManager<object>.Invoke("ShowSubWin", null);
V中注册子窗口,VM中打开
可以写一个WindowManager类,该类中可以注册窗口和打开窗口
public class WindowManager
{
//注册窗口存放
static Dictionary<string, WinEntity> _windows = new Dictionary<string, WinEntity>();
//注册,传入Type类型,因为注册的时候不需要实例,
//但是owner则需要传入Window,因为要设置owner说明已经有了实例
public static void Register(Type type,Window owner)
{
if (!_windows.ContainsKey(type.Name))
{
_windows.Add(type.Name, new WinEntity {Type = type,Owner = owner });
}
}
//使用string类型的winKey,因为调用showDialog方法往往是在VM中,如果使用Type类型,则要在VM中引用View
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来实现该功能?
- 定义菜单模型
public class MenuModel
{
public string MenuIcon { get; set; }
public string MenuHeader { get; set; }
public string TargetView { get; set; }
}
- 定义MainModel
public class MainModel : INotifyPropertyChanged
{
public List<MenuModel> MenuList { get; set; }
/// <summary>
/// 当前点击的页面实例
/// </summary>
private object _page;
public object Page
{
get => _page;
set
{
_page = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Page"));
}
}
public event PropertyChangedEventHandler? PropertyChanged;
}
- MainViewModel
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",// 如果存在数据库的话: e643 这个字符的编号
MenuHeader = "Dashboard",
TargetView = "MvvmDemo.Views.DashboardPage",// 反射 新建一个UserControl名字为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
{
// obj希望传进来的一个TargetView
DoExecute = new Action<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>