WPF中文网

ReactiveUI之ReactiveCommand

ReactiveUI 是集成了 .Net 的 ReatIve 扩展的 MVVM 框架,用来创建运行与任何移动设备或者桌面平台的优雅的可测试的用户接口。它支持 Xamarin.iOS,Xamarin.Android,Xamarin.Mac, WPF,Windows Forms,Windows Phone 8 和 Windows Store 应用程序。

ReactiveUI是一个可组合的跨平台模型 - 视图 - 视图模型框架,适用于所有.NET平台,受功能性反应式编程的启发。它允许您在一个可读位置围绕功能表达想法,从用户界面抽象出可变状态,并提高应用程序的可测试性

反应式编程简介

很久以前,当计算机编程首次出现时,机器必须手动编程。如果技术人员以正确的顺序输入正确的机器代码序列,则生成的程序行为将满足业务要求。而不是告诉计算机如何完成它的工作,哪个容易出错并且过分依赖于程序员的无懈可击,为什么我们不告诉它它的工作是什么,让它把剩下的工作弄清楚了?

ReactiveUI的灵感来自功能反应式编程的范例,它允许您将用户输入建模为随时间变化的函数。这非常酷,因为它允许您从用户界面中抽象出可变状态,并在一个可读位置表达功能,同时提高应用程序可测试性。

如何安装ReactiveUI组件?在VisualStdio的nuget包管理器中搜索关键字,请安装ReactiveUI.WPF版本,安装结束后,会在项目的引用中看到ReactiveUI和ReactiveUI.WPF。

如何在ReactiveUI中使用命令?ReactiveCommand是使用静态工厂方法创建的,该方法允许您创建同步或异步执行的命令逻辑。在ReactiveCommand类型中提供了一系列的以Create开头的静态方法,这些方法可以创建不同的命令。ReactiveCommand命令继承了一个带泛型的命令基类ReactiveCommandBase<TParam,TResult>,所以它的回调函数都是带有参数和返回值的,如果无需传参及返回值,可将泛型实参写成Unit。另外,ReactiveUI采用了大量的观察者模式,所以在创建命令时,还可以使用IObservable执行逻辑,并去订阅当前命令,当命令执行后,再去执行订阅回调函数。下面我简单罗列一下ReactiveUI命令的创建方式。

Create()创建一个命令,执行同步Func或Action。

CreateCombined()创建一个合并命令,可一次性执行多个命令。

CreateFromObservable()创建一个命令,使用IObservable执行逻辑。

CreateFromTask()创建一个命令,执行C#任务,允许使用C#async/await运算符。

CreateRunInBackground()创建一个背景命令。

ReactiveCommand示例

首先,我们创建一个MainViewModel,并在其中声明一些命令。

internal class MainViewModel:ReactiveObject
{
    public ICommand GeneralCommand { get; }
    public ICommand ParameterCommand { get; }
    public ICommand TaskCommand { get; }
    public ICommand CombinedCommand { get; }
    public ReactiveCommand<Unit,DateTime> ObservableCommand { get; }
    public MainViewModel()
    {
        GeneralCommand = ReactiveCommand.Create(General);
        ParameterCommand = ReactiveCommand.Create<object, bool>(Parameter);
        TaskCommand = ReactiveCommand.CreateFromTask(RunAsync);

        var childCommand = new List<ReactiveCommandBase<Unit,Unit>>();
        childCommand.Add(ReactiveCommand.Create<Unit, Unit>((o) => 
        {
           
            MessageBox.Show("childCommand1");
            return Unit.Default; 
        }));
        childCommand.Add(ReactiveCommand.Create<Unit, Unit>((o) =>
        {
            MessageBox.Show("childCommand2");
            return Unit.Default;
        }));
        childCommand.Add(ReactiveCommand.Create<Unit, Unit>((o) =>
        {
            MessageBox.Show("childCommand3");
            return Unit.Default;
        }));

        CombinedCommand = ReactiveCommand.CreateCombined(childCommand);

        ObservableCommand = ReactiveCommand.CreateFromObservable<Unit, DateTime>(DoObservab
        ObservableCommand.Subscribe(v => ShowObservableResult(v));

    }

    private void RunInBackground()
    {
        throw new NotImplementedException();
    }

    private IObservable<DateTime> DoObservableCommand(Unit arg)
    {
        //todo 业务代码

        var result = DateTime.Now;

        return Observable.Return(result).Delay(TimeSpan.FromSeconds(1));
    }

    private void ShowObservableResult(DateTime v)
    {
        MessageBox.Show($"时间:{v}");
    }

    private async Task RunAsync()
    {
        await Task.Delay(3000);
    }

    private bool Parameter(object arg)
    {
        MessageBox.Show(arg.ToString());
        return true;
    }

    private void General()
    {
        MessageBox.Show("ReactiveCommand!");
    }       
}

在这个示例中,并分演示了ReactiveCommand的普通命令、带参命令、Task命令、合并命令和观察者命令的用法。接下来创建XAML前端控件对象,将这些命令绑定到Button上面。

<Window x:Class="HelloWorld.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:HelloWorld"
        mc:Ignorable="d"
        Title="WPF从小白到大佬 - 命令" Height="350" Width="500">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <StackPanel>
        <TextBlock Text="ReactiveUI之ReactiveCommand课程" FontSize="28" Margin="5"/>
        <StackPanel Orientation="Horizontal">
            <Button Margin="5" Content="普通命令" Command="{Binding GeneralCommand}"/>
            <Button Margin="5" Content="参数命令" Command="{Binding ParameterCommand}" 
                    CommandParameter="Hello,Parameter"/>
            <Button Margin="5" Content="子线程命令" Command="{Binding TaskCommand}"/>
            <Button Margin="5" Content="合并命令" Command="{Binding CombinedCommand}"/>
            <Button Margin="5" Content="Observable命令" Command="{Binding ObservableCommand}"/>
        </StackPanel>
    </StackPanel>
</Window>

当前课程源码下载:(注明:本站所有源代码请按标题搜索)

文件名:077-《ReactiveUI之ReactiveCommand》-源代码
链接:https://pan.baidu.com/s/1yu-q4tUtl0poLVgmcMfgBA
提取码:wpff

——重庆教主 2023年10月17日

MVVMLight是一个实现MVVM模式的轻量级框架(相对于Prism),适用于Net framework版本下的WPF程序,目前已经停止维护,但是作为轻量及的开发框架,完全可以适用中小型的项目开发。它提供了IOC容器、MVMM模式、消息机制、对话框、命令等功能。本文将介绍如何安装mvvmlight以及使用它的Command命令。

一、下载安装mvvmlight框架

在nuget管理器中搜索mvvmlight,找到后下载安装即可。安装结束后,它会为我们的项目创建一个ViewModel的文件夹,并在其中创建两个类:ViewModelLocator和MainViewModel

由于本文只演示它的命令用法,所以就不展开mvvmlight的完整使用。需要注意的是,默认安装后,需要解决一个问题。那就是ViewModelLocator类报错问题。

这个问题就是ServiceLocator的命令空间已经不在Microsoft.Practices.ServiceLocation之中,而在CommonServiceLocator之中,所以需要将其替换一下。

然后,我们就可以在前面的示例的MainViewModel中,新建一条命令。

public class MainViewModel : ObservableObject
{
    public GalaSoft.MvvmLight.Command.RelayCommand<string> MvvmlightCommand { get; } 
    public MainViewModel()
    {
        MvvmlightCommand = new GalaSoft.MvvmLight.Command.RelayCommand<string>((message) =>
        {
            MessageBox.Show(message);
        });
    }
}

最后,我们在前端用一个Button来引用这个命令

<Button Content="mvvmlight" 
        Margin="5" 
        Command="{Binding MvvmlightCommand}" 
        CommandParameter="Hello,Mvvmlight"/>

mvvmlight框架的命令也是叫RelayCommand,同时它还有带泛型参数的RelayCommand命令。所以,大多数时候, 我们会直接安装mvvmlight、prism、ReactiveUI或者CommunityToolkit.Mvvm框架,这一类框架都是帮助我们更好的实现软件架构,所提供的功能也是大同小异,起码最基本的MVVM功能都得到了实现。

如果您理解了在前面的课程中如何实现ICommand接口,那么,这些框架中提供的命令,使用起来就变得简单许多。

当前课程源码下载:(注明:本站所有源代码请按标题搜索)

文件名:075-《mvvmlight框架之RelayCommand》-源代码
链接:https://pan.baidu.com/s/1yu-q4tUtl0poLVgmcMfgBA
提取码:wpff

通过前面的学习,我们发现Button拥有Command属性,开发者可以为其设置一条命令,当用户单击按钮的时候便执行这条命令。但是,一个控件往往不止一个事件,比如UIElement基类中便定义了大量的事件,PreviewMouseDown表示鼠标按下时引发的事件。

如何在PreviewMouseDown事件触发时去执行一条命令呢?这时候就需要用到WPF提供的一个组件,它的名字叫Microsoft.Xaml.Behaviors.Wpf,我们可以在nuget管理器中找到并下载安装它。

安装Microsoft.Xaml.Behaviors.Wpf。

然后,我们在window窗体中引入它的命名空间。

xmlns:i="http://schemas.microsoft.com/xaml/behaviors"

最后,我们以TextBox为例,因为TextBox也是UIElement基类的子类控件,所以,它也有PreviewMouseDown事件。

<TextBox x:Name="textbox" Grid.Row="1" Margin="5" TextWrapping="Wrap">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="PreviewMouseDown">
            <i:InvokeCommandAction Command="{Binding MouseDownCommand}"
                                   CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=TextBox}}">
            </i:InvokeCommandAction>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TextBox>

从上面的例子中,我们会发现,在TextBox 控件中增加了一个Interaction.Triggers附加属性,这个属性是一个集合,表示可以实例化多个Trigger触发器,于是,我们实例化了一个EventTrigger ,并指明它的EventName为PreviewMouseDown事件,关联的命令要写在InvokeCommandAction 对象中,命令绑定的方式采用Binding即可。然后我们来看看MouseDownCommand的执行代码:

public RelayCommand<TextBox> MouseDownCommand { get; set; } = new RelayCommand<TextBox>((textbox) =>
{
    textbox.Text += DateTime.Now + " 您单击了TextBox" + "\r";
});

在TextBox控件上单击鼠标时,执行MouseDownCommand 命令,同时将TextBox控件传入到命令的匿名回调函数中。

当前课程源码下载:(注明:本站所有源代码请按标题搜索)

文件名:074-《WPF事件转命令》-源代码
链接:https://pan.baidu.com/s/1yu-q4tUtl0poLVgmcMfgBA
提取码:wpff

——重庆教主 2023年10月12日

ICommand是WPF命令的代码协定,也就是说,WPF中所有的命令都要继承这个接口,不管是预定义命令还是自定义命令。

一、ICommand的定义

public interface ICommand
{
    //
    // 摘要:
    //     当出现影响是否应执行该命令的更改时发生。
    event EventHandler CanExecuteChanged;

    //
    // 摘要:
    //     定义确定此命令是否可在其当前状态下执行的方法。
    //
    // 参数:
    //   parameter:
    //     此命令使用的数据。 如果此命令不需要传递数据,则该对象可以设置为 null。
    //
    // 返回结果:
    //     如果可执行此命令,则为 true;否则为 false。
    bool CanExecute(object parameter);
    //
    // 摘要:
    //     定义在调用此命令时要调用的方法。
    //
    // 参数:
    //   parameter:
    //     此命令使用的数据。 如果此命令不需要传递数据,则该对象可以设置为 null。
    void Execute(object parameter);

}

这个接口比较关键的是CanExecute和Execute两个方法成员。前者表示当前命令是否可以执行,如果可以的话,WPF命令系统会自动帮我们去调用Execute方法成员。那么,我们要实现这个接口的话,通常只需要在CanExecute编写一些判断逻辑,在Execute调用一个委托就行了。至于这个委托的的签名和具体的代码内容,则是在实际应用时由开发者去编写不同的业务代码。接下来,我们来实现一个ICommand接口。

二、ICommand的实现

public class RelayCommand : ICommand
{
    public event EventHandler CanExecuteChanged;
    
    private Action action;

    public RelayCommand(Action action)
    {
        this.action = action;
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        action?.Invoke();
    }
}

在上面的例子中,我们自定义了一个叫RelayCommand的Command类,非常重要的一点是,它的构造函数要求传入一个Action,这个委托传进来后,将来在Execute成员中被执行。

接下来,我们看看它的具体使用。

public class MainViewModel : ObservableObject
{
    public RelayCommand OpenCommand { get; set; } = new RelayCommand(() =>
    {
        MessageBox.Show("Hello,Command");
    });
}

我们在MainViewModel中编写了一个叫OpenCommand的属性,而属性的类型就是RelayCommand,在构造这个RelayCommand时,我们传入了一个匿名函数。如此,前端就可以绑定这个OpenCommand命令了。

前端代码

<Window x:Class="HelloWorld.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:HelloWorld" 
        xmlns:forms="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
        mc:Ignorable="d" FontSize="14"
        Title="WPF中文网 - 命令 - www.wpfsoft.com" Height="350" Width="500">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Window.Resources>
        
    </Window.Resources>
    <Grid>
        <Button Width="100" Height="30" Content="打开" Command="{Binding OpenCommand}" />
    </Grid>
</Window>

单击按钮,弹出对话框,说明OpenCommand所指向的匿名函数代码块被执行。这便是ICommand接口最简单的自定义实现之一,如果我们需要在单击按钮时传入一些参数该怎么办呢?这就需要稍微改一下RelayCommand的代码了。

三、ICommand带参数的实现

public class RelayCommand : ICommand
{
    public event EventHandler CanExecuteChanged;
    
    private Action action;
    private Action<object> objectAction;
    public RelayCommand(Action action)
    {
        this.action = action;
    }

    public RelayCommand(Action<object> objectAction)
    {
        this.objectAction = objectAction;
    }
    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        action?.Invoke();
        objectAction?.Invoke(parameter);
    }
}

在这里,我们增加了一个带Action<object>参数的构造函数,将来定义命令时,就可以将一个带有object参数的方法传到这个RelayCommand中来。

MainViewModel代码如下

public class MainViewModel : ObservableObject
{
    public RelayCommand OpenCommand { get; set; } = new RelayCommand(() =>
    {
        MessageBox.Show("Hello,Command");
    });

    public RelayCommand OpenParamCommand { get; set; } = new RelayCommand((param) =>
    {
        MessageBox.Show(param.ToString());
    });
    
}

在MainViewModel中,我们增加了一个OpenParamCommand命令,并传入了一个带参数的匿名函数。最后看看前端代码如何调用。

<StackPanel VerticalAlignment="Center">
    <Button Width="100" Height="30" Content="打开" Command="{Binding OpenCommand}" />
    <Button Width="100" Height="30" 
            Content="打开" 
            Command="{Binding OpenParamCommand}" 
            CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self}}"/>
</StackPanel>

由此可见,我们利用CommandParameter属性将前端的button对象传递到了命令的回调函数中。虽然RelayCommand已经具备了参数的传递,但是,我们还有更优雅的方式去实现它。

四、ICommand的泛型参数实现

public class RelayCommand<T> : ICommand
{
    public event EventHandler CanExecuteChanged;
    public Action<T> Action { get; }
    public RelayCommand(Action<T> action)
    {
        Action = action;
    }
    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        Action?.Invoke((T)parameter);
    }
}

我们利用泛型来简化了RelayCommand的定义,而在定义泛型RelayCommand和使用时,与前面带object参数的RelayCommand的用法是差不多的。

public RelayCommand<object> OpenTParamCommand { get; set; } = new RelayCommand<object>((t) =>
{
    MessageBox.Show(t.ToString());
});

当前课程源码下载:(注明:本站所有源代码请按标题搜索)

文件名:071-《ICommand接口》-源代码
链接:https://pan.baidu.com/s/1yu-q4tUtl0poLVgmcMfgBA
提取码:wpff

官方的说法是:命令是WPF 中的一种输入机制,与设备输入相比,它提供的输入处理更侧重于语义级别。我觉得这样的解释有点抽象,在实际的使用过程中我们会发现,传统的事件驱动模式下,比如一个按钮的Click单击事件,我们会给它订阅一个回调函数,以便触发该事件时,去执行这个回调函数的业务逻辑代码。

Button button = new Button();
button.Click += (s, e) =>
{
    MessageBox.Show("HelloWorld");
};

或者在XAML前端实例化一个Button,然后订阅Click事件。

//XAML代码
<Grid>
    <Button Content="确定" Click="Button_Click"/>
</Grid>

//C#代码
private void Button_Click(object sender, RoutedEventArgs e)
{
    MessageBox.Show("HelloWorld");
}

事件驱动模式有一个不太好的地方是,它的前端XAML代码和后端的C#代码建立了一种强关联的耦合关系,无法体现WPF的MVVM开发模式的优势, 于是,WPF提供了一个ICommand接口,以及一个强大的命令系统,将控件的各种事件都能转换成一个命令。这个命令依然像绑定依赖属性一样,将命令定义成一个ViewModel中的命令属性,而ViewModel又被赋值到控件或窗体的DataContext数据上下文中,于是,窗体内的所有控件的事件都可以绑定这个命令,只要控件的事件被触发,命令将会被执行。

WPF的命令是通过ICommand接口创建的,并且微软已经为我们预定义了一部分命令,这些我们都称为预定义命令,它们的类型是RoutedUICommand或RoutedCommand。RoutedUICommand继承于RoutedCommand,而RoutedCommand继承于ICommand。

事实上,在WPF应用开发过程中,光靠这些预定义命令是不足以满足我们的开发需求的,所以我们肯定要另写一些命令类型,当然,自定义命令必须继承于ICommand。

厘清这一点之后,我们就明白了接下来要学习的几个方面:

  • 第一点:命令的4个主要概念,它解决了我们如何正确运用命令知识。
  • 第二点:预定义命令的熟悉与使用。
  • 第三点:自定义命令的定义与应用。
  • 第四点:常见框架(例如Prism,mvvmlight,ReactiveUI)中的命令应用。
  • 第五点:控件的普通事件转Command命令

第一点,WPF 命令中的四个主要概念

WPF 中的路由命令模型可分解为四个主要概念:命令、命令源、命令目标和命令绑定:

  • 命令是要执行的操作。
  • 命令源是调用命令的对象。
  • 命令目标是在其上执行命令的对象。
  • 命令绑定是将命令逻辑映射到命令的对象。

如上所述,命令其实就是ICommand接口的实现,命令源是调用命令的对象,这些对象一定会继承ICommandSource接口,而命令绑定就像是一座桥梁,它将命令与逻辑代码建立一种映射,这座桥梁就是CommandBinding。最后,命令如何与命令源建立绑定?当然就是Binding对象了。

较形象的总结一下命令的路线:ICommandSource命令源(或控件的事件)->绑定一个ICommand命令->CommandBinding桥梁->真正的业务逻辑代码块。

下一节,我们以这个路线先介绍命令源。

——重庆教主 2023年10月11日

copyright @重庆教主 WPF中文网 联系站长:(QQ)23611316 (微信)movieclip (QQ群).NET小白课堂:864486030 | 本文由WPF中文网原创发布,谢绝转载 渝ICP备2023009518号-1