草庐IT

WPF MVVM系统入门-下

步、步、为营 2023-04-12 原文

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只能对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>

有关WPF MVVM系统入门-下的更多相关文章

  1. 电脑0x0000001A蓝屏错误怎么U盘重装系统教学 - 2

      电脑0x0000001A蓝屏错误怎么U盘重装系统教学分享。有用户电脑开机之后遇到了系统蓝屏的情况。系统蓝屏问题很多时候都是系统bug,只有通过重装系统来进行解决。那么蓝屏问题如何通过U盘重装新系统来解决呢?来看看以下的详细操作方法教学吧。  准备工作:  1、U盘一个(尽量使用8G以上的U盘)。  2、一台正常联网可使用的电脑。  3、ghost或ISO系统镜像文件(Win10系统下载_Win10专业版_windows10正式版下载-系统之家)。  4、在本页面下载U盘启动盘制作工具:系统之家U盘启动工具。  U盘启动盘制作步骤:  注意:制作期间,U盘会被格式化,因此U盘中的重要文件请注

  2. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  3. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  4. 微信小程序开发入门与实战(Behaviors使用) - 2

    @作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors    1、什么是behaviors    2、behaviors的工作方式    3、创建behavior    4、导入并使用behavior    5、behavior中所有可用的节点    6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors    1、什么是behaviorsbehaviors是小程序中,用于实现

  5. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

  6. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  7. ES基础入门 - 2

    ES一、简介1、ElasticStackES技术栈:ElasticSearch:存数据+搜索;QL;Kibana:Web可视化平台,分析。LogStash:日志收集,Log4j:产生日志;log.info(xxx)。。。。使用场景:metrics:指标监控…2、基本概念Index(索引)动词:保存(插入)名词:类似MySQL数据库,给数据Type(类型)已废弃,以前类似MySQL的表现在用索引对数据分类Document(文档)真正要保存的一个JSON数据{name:"tcx"}二、入门实战{"name":"DESKTOP-1TSVGKG","cluster_name":"elasticsear

  8. ruby - 在没有基准或时间的情况下用 Ruby 测量用户时间或系统时间 - 2

    因为我现在正在做一些时间测量,我想知道是否可以在不使用Benchmark类或命令行实用程序time的情况下测量用户时间或系统时间。使用Time类只显示挂钟时间,而不显示系统和用户时间,但是我正在寻找具有相同灵active的解决方案,例如time=TimeUtility.now#somecodeuser,system,real=TimeUtility.now-time原因是我有点不喜欢Benchmark,因为它不能只返回数字(编辑:我错了-它可以。请参阅下面的答案。)。当然,我可以解析输出,但感觉不对。*NIX系统的time实用程序也应该可以解决我的问题,但我想知道是否已经在Ruby中实

  9. ruby - 以毫秒为单位获取当前系统时间 - 2

    在Ruby中,以毫秒为单位获取自纪元(1970)以来的当前系统时间的正确方法是什么?我试过了Time.now.to_i,好像不是我想要的结果。我需要结果显示毫秒并且使用long类型,而不是float或double。 最佳答案 (Time.now.to_f*1000).to_iTime.now.to_f显示包含十进制数字的时间。要获得毫秒数,只需将时间乘以1000。 关于ruby-以毫秒为单位获取当前系统时间,我们在StackOverflow上找到一个类似的问题:

  10. ruby-on-rails - 如何构建复杂的 Rails 系统 - 2

    关闭。这个问题需要更多focused.它目前不接受答案。想改进这个问题吗?更新问题,使其只关注一个问题editingthispost.关闭8年前。Improvethisquestion我们有以下(以及更多)系统,我们将数据从一个应用推送/拉取到另一个:托管CRM(InsideSales.com)Asterisk电话系统(内部)横幅广告系统(openx,我们托管)潜在客户生成系统(自行开发)电子商务商店(spree,我们托管)工作板(本土)一些工作网站抓取+入站工作提要电子邮件传送系统(如Mailchimp,自主开发)事件管理系统(如eventbrite,自主开发)仪表板系统(大量图表和

随机推荐