WPF MVVM系统入门-下

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

执行效果如下

img

内置命令

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

img

鼠标行为

一般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命令。

扫描二维码关注公众号,回复: 15212000 查看本文章

自定义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只能对KeyBindingMouseBinding进行绑定,但如果我想要其他的事件,比如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>

猜你喜欢

转载自blog.csdn.net/weixin_44064908/article/details/129064335