0
点赞
收藏
分享

微信扫一扫

WPF MVVM系统入门-下

SPEIKE 2023-02-18 阅读 124


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)
};
}
}

执行效果如下

WPF MVVM系统入门-下_wpf

内置命令

上面我们自定义了​​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>

WPF MVVM系统入门-下_.net_02

鼠标行为

一般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​​。

  1. 使用行为需要nuget安装​​Microsoft.Xaml.Behaviors.Wpf​​,FrameWork版本安装​​System.Windows.Interactivity.WPF​
  2. 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来实现该功能?

  1. 定义菜单模型

public class MenuModel
{
public string MenuIcon { get; set; }
public string MenuHeader { get; set; }
public string TargetView { get; set; }
}

  1. 定义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;
}

  1. 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());
})
};
}
}

  1. 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>


举报

相关推荐

0 条评论