WPF 命令(Command)使用探究

本文参考微软文档,但是他写得太乱了,不便于模仿学习。重新整理了一下。

创建标准 UI 命令

标准命令(StandardUICommand,如删除、编辑等),操作系统会自动帮我们完成翻译、图标设置等。使用方法:

1            <muxcontrols:MenuBarItem Title="Edit">
2                <MenuFlyoutItem x:Name="DeleteFlyoutItem"/>
3            </muxcontrols:MenuBarItem>
1            // Create the standard Delete command.
2            var deleteCommand = new StandardUICommand(StandardUICommandKind.Delete);
3            deleteCommand.ExecuteRequested += DeleteCommand_ExecuteRequested;
4
5            DeleteFlyoutItem.Command = deleteCommand;

其中 DeleteCommand_ExecuteRequested 是具体的执行逻辑,签名:

1        private void DeleteCommand_ExecuteRequested(
2            XamlUICommand sender, ExecuteRequestedEventArgs args)

需要我们自己实现。args.Parameter 中是执行的参数。

创建自定义 UI 命令

这种方法通过 XAML 定义命令,包括名称、图标,可以顺带设置快捷键:

1        <XamlUICommand x:Name="CustomXamlUICommand" ExecuteRequested="DeleteCommand_ExecuteRequested" Description="Custom XamlUICommand" Label="Custom XamlUICommand">
2            <XamlUICommand.IconSource>
3                <FontIconSource FontFamily="Wingdings" Glyph="&#x4D;"/>
4            </XamlUICommand.IconSource>
5            <XamlUICommand.KeyboardAccelerators>
6                <KeyboardAccelerator Key="D" Modifiers="Control"/>
7            </XamlUICommand.KeyboardAccelerators>
8        </XamlUICommand>

注意此处绑定了 DeleteCommand_ExecuteRequested 事件,需要在逻辑代码进行实现。

批量操作:

1            <muxcontrols:MenuBarItem Title="Edit">
2                <MenuFlyoutItem x:Name="DeleteFlyoutItem" Command="{StaticResource CustomXamlUICommand}"/>
3            </muxcontrols:MenuBarItem>

单独操作:

1                                        <SwipeItem x:Name="DeleteSwipeItem"
2                                                   Background="Red" 
3                                                   Command="{x:Bind Command}" 
4                                                   CommandParameter="{x:Bind Text}"/>

然后在添加列表项的时候绑定 Command:

1                collection.Add(new ListItemData { Text = "List item " + i.ToString(), Command = CustomXamlUICommand });

当然,这个列表项的模型需要我们定义:

1    public class ListItemData
2    {
3        public String Text { get; set; }
4        public ICommand Command { get; set; }
5    }

通过实现 ICommand 接口创建命令

这个例子,推荐在你的项目里参考使用。它更规范地实现了 ViewModel。并且,在这里例子中会看到著名的 RelayCommand 类。

让我康康

 1    /// <summary>
 2    /// A command whose sole purpose is to relay its functionality 
 3    /// to other objects by invoking delegates. 
 4    /// The default return value for the CanExecute method is 'true'.
 5    /// <see cref="RaiseCanExecuteChanged"/> needs to be called whenever
 6    /// <see cref="CanExecute"/> is expected to return a different value.
 7    /// </summary>
 8    public class RelayCommand : ICommand
 9    {
10        private readonly Action _execute;
11        private readonly Func<bool> _canExecute;
12
13        /// <summary>
14        /// Raised when RaiseCanExecuteChanged is called.
15        /// </summary>
16        public event EventHandler CanExecuteChanged;
17
18        /// <summary>
19        /// Creates a new command that can always execute.
20        /// </summary>
21        /// <param name="execute">The execution logic.</param>
22        public RelayCommand(Action execute)
23            : this(execute, null)
24        {
25        }
26
27        /// <summary>
28        /// Creates a new command.
29        /// </summary>
30        /// <param name="execute">The execution logic.</param>
31        /// <param name="canExecute">The execution status logic.</param>
32        public RelayCommand(Action execute, Func<bool> canExecute)
33        {
34            if (execute == null)
35                throw new ArgumentNullException("execute");
36            _execute = execute;
37            _canExecute = canExecute;
38        }
39
40        /// <summary>
41        /// Determines whether this <see cref="RelayCommand"/> can execute in its current state.
42        /// </summary>
43        /// <param name="parameter">
44        /// Data used by the command. If the command does not require data to be passed, this object can be set to null.
45        /// </param>
46        /// <returns>true if this command can be executed; otherwise, false.</returns>
47        public bool CanExecute(object parameter)
48        {
49            return _canExecute == null ? true : _canExecute();
50        }
51
52        /// <summary>
53        /// Executes the <see cref="RelayCommand"/> on the current command target.
54        /// </summary>
55        /// <param name="parameter">
56        /// Data used by the command. If the command does not require data to be passed, this object can be set to null.
57        /// </param>
58        public void Execute(object parameter)
59        {
60            _execute();
61        }
62
63        /// <summary>
64        /// Method used to raise the <see cref="CanExecuteChanged"/> event
65        /// to indicate that the return value of the <see cref="CanExecute"/>
66        /// method has changed.
67        /// </summary>
68        public void RaiseCanExecuteChanged()
69        {
70            var handler = CanExecuteChanged;
71            if (handler != null)
72            {
73                handler(this, EventArgs.Empty);
74            }
75        }
76    }

我们先看看 VM:

 1
 2namespace UICommand1.ViewModel
 3{
 4    public class ListItemData
 5    {
 6        public string ListItemText { get; set; }
 7        public Symbol ListItemIcon { get; set; }
 8    }
 9
10    public class UICommand1ViewModel
11    {
12        public RelayCommand MoveLeftCommand { get; private set; }
13        public RelayCommand MoveRightCommand { get; private set; }
14
15        public ObservableCollection<ListItemData> ListItemLeft { get; } = new ObservableCollection<ListItemData>();
16        public ObservableCollection<ListItemData> ListItemRight { get; } = new ObservableCollection<ListItemData>();
17
18        public ListItemData listItem;
19        
20        public UICommand1ViewModel()
21        {
22            MoveLeftCommand = new RelayCommand(new Action(MoveLeft), CanExecuteMoveLeftCommand);
23            MoveRightCommand = new RelayCommand(new Action(MoveRight), CanExecuteMoveRightCommand);
24
25            LoadItems();
26        }
27
28        public void LoadItems()
29
30        private bool CanExecuteMoveLeftCommand()
31        private bool CanExecuteMoveRightCommand()
32        public void MoveRight()
33        public void MoveLeft()
34        public event PropertyChangedEventHandler PropertyChanged;
35        protected void NotifyPropertyChanged(string propertyName)
36        {
37            PropertyChangedEventHandler handler = this.PropertyChanged;
38            if (handler != null)
39            {
40                PropertyChangedEventArgs args = new PropertyChangedEventArgs(propertyName);
41                handler(this, args);
42            }
43        }
44    }
45    
46    public class OpacityConverter : IValueConverter
47}

可见在 UICommand1ViewModel 中定义了相关的:

  1. 数据

  2. 命令的声明

  3. 命令的实现

而主类非常简单:

 1public sealed partial class MainPage : Page
 2    {
 3        public UICommand1ViewModel ViewModel { get; set; }
 4
 5        public MainPage()
 6        {
 7            this.InitializeComponent();
 8            ViewModel = new UICommand1ViewModel();
 9        }
10    
11        private void Page_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
12        {
13            var props = e.GetCurrentPoint(sender as UIElement).Properties;
14
15            if ((Window.Current.CoreWindow.GetKeyState(VirtualKey.Control) != 
16                CoreVirtualKeyStates.None) && !props.IsHorizontalMouseWheel)
17            {
18                bool delta = props.MouseWheelDelta < 0 ? true : false;
19
20                switch (delta)
21                {
22                    case true:
23                        ViewModel.MoveRight();
24                        break;
25                    case false:
26                        ViewModel.MoveLeft();
27                        break;
28                    default:
29                        break;
30                }
31            }
32        }
33    }

并且我们注意到 UICommand1ViewModel 下实际上有两个命令,可以学习这种封装方式。

从 UI 是如何触发命令的呢

1<Page
2    x:Class="UICommand1.View.MainPage"
3    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5    xmlns:vm="using:UICommand1.ViewModel"

此处进行了 vm 绑定,这是为了便于设计 ListView 的模板:

 1            <ListView.ItemTemplate>
 2                <DataTemplate x:DataType="vm:ListItemData">
 3                    <Grid VerticalAlignment="Center">
 4                        <AppBarButton Label="{x:Bind ListItemText}">
 5                            <AppBarButton.Icon>
 6                                <SymbolIcon Symbol="{x:Bind ListItemIcon}"/>
 7                            </AppBarButton.Icon>
 8                        </AppBarButton>
 9                    </Grid>
10                </DataTemplate>
11            </ListView.ItemTemplate>

另外在 MainPage 类中,已经创建了 ViewModel 的实例,因此可以通过这个实例引发命令:

1                <Button Name="MoveItemLeftButton" 
2                            Margin="0,10,0,10" Width="120" HorizontalAlignment="Center"
3                            Command="{x:Bind Path=ViewModel.MoveLeftCommand}">

我们自己的例子

让我们用最近简单的代码实现一个 Command:

UI:

<AppBarButton Icon="Upload" Label="Upload"  Command="{x:Bind Path=ViewModel.UploadCommand}"  /> 

Logic:

1        public MainPageViewModel ViewModel { get; set; }
2        public MainPage()
3        {
4            // ...
5            ViewModel = new MainPageViewModel();            
6        }

VM:

 1    public class MainPageViewModel
 2    {
 3        public RelayCommand UploadCommand {get;private set;}
 4        public MainPageViewModel()
 5        {
 6            UploadCommand = new RelayCommand(Upload);
 7        }
 8        public async void Upload()
 9        {
10            var messageDialog = new MessageDialog("Hi!");
11            await messageDialog.ShowAsync();
12        }
13    }

题外话

尴尬的是命令模式确实比事件 Handler 费精力(对于简单项目)。所以连官方自己都用古法写代码:PowerToys/MainWindow.xaml.cs at master · microsoft/PowerToys (github.com)