MVVM trong WPF (Part 2/4): RelayCommand

MVVM - Model View ViewModel PatternViệc tạo một custom RoutedCommand trong WPF rất đơn giản, bạn chỉ việc sử dụng CommandBinding là có thể tạo một command tùy ý. Tuy nhiên để ứng dụng mô hình MVVM, ta không thể sử dụng loại command thông thường, mà phải tạo một loại command mới có thể tách biệt được với giao diện. Loại command mà tôi muốn nói tới được gọi là Relay Command.

Interface ICommand

Một class command được định nghĩa bằng cách hiện thực interface ICommand và các thành viên của nó:

–         CanExecute(): Phương thức này xác định command có thể được thực thi hay không. Giá trị trả về của phương thức này (boolean) sẽ ảnh hưởng đến property Enabled của các control liên quan đến command.

–         Execute(): Phương thức này sẽ thực thi khi command được kích hoạt.

–         CanExecuteChanged: Event này kích hoạt mỗi khi có một sự thay đổi làm ảnh hưởng tới command. Các control sử dụng command sẽ được enable/disable tùy theo kết quả trả về của CanExecute().

(1) Event CanExecuteChanged này sẽ được tự động thêm vào các phương thức xử lý trong quá trình thực thi. Phương thức được thêm vào này được dùng để cập nhật lại giao diện (enable/disable các control liên quan đến command).

Theo cách thủ công, event CanExecuteChanged phải được kích hoạt khi cần cập nhật các thay đổi trên giao diện. Tuy nhiên cách mà WPF sử dụng là giao công việc này lại cho CommandManager, một class được tạo sẵn cho mục đích tự động thực hiện nhiệm vụ này.

Mỗi khi có sự thay đổi nào đó trên command target (thành phần mà command sẽ tác động đến khi được thực thi), (2) CommandManager sẽ tự động kích hoạt một static event của nó là RequerySuggested. Bạn cũng có thể kích hoạt event này bất kì lúc nào với phương thức static CommandManager.InvalidateRequerySuggested().

Vậy thì qua hai điểm (1) và (2) này, cách tốt nhất là ta sẽ thêm trực tiếp các phương thức xử lý vào event CommandManager.RequerySuggested thay vì ICommand.CanExecuteChanged. Đây cũng chính là cách thức mà RelayCommand hoạt động.

RelayCommand trong mô hình MVVM

Khác với RoutedCommand là một thành phần của WPF, RelayCommand là một class hiện thực interface ICommand và được giới thiệu bởi Josh Smith, trong quá trình tìm giải pháp để hiện thực mô hình MVVM. Đặc điểm khác biệt giữa RoutedCommand và RelayCommand là:

–         Do sử dụng CommandBinding để định nghĩa, các phương thức xử lý CanExecute() và Execute() phải được khai báo trong tập tin code-behind của View.

–         Với RelayCommand, các phương thức xử lý sẽ được lưu giữ trong delegate của class. Do đó có thể đặt bất kì ở đâu, và theo mô hình MVVM, chúng sẽ được đặt trong ViewModel.

Việc đặt RelayCommand trong ViewModel sẽ giúp chúng thao tác được dữ liệu chứa trong đó dễ dàng hơn.

Hình minh họa sau cho thấy cách mô hình MVVM hoạt động với command và data binding:

MVVM - Model View ViewModel Pattern

Tạo RelayCommand

Bằng cách sử dụng delegate để lưu giữ các phương thức CanExecute() và Execute() của command, ta sẽ truyền các phương thức cần thiết để command hoạt động thông qua constructor của nó. Hai delegate tương ứng để lưu giữ hai phương thức này là Predicate(Of T) và Action(Of T). Một cách tổng quát, bạn có thể sử dụng Func<> để lưu trữ bất kì loại phương thức nào.

class RelayCommand<T> : ICommand
{
    private readonly Predicate<T> _canExecute;
    private readonly Action<T> _execute;

    public RelayCommand(Predicate<T> canExecute, Action<T> execute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");
        _canExecute = canExecute;
        _execute = execute;
    }

    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute((T)parameter);
    }

    public void Execute(object parameter)
    {
        _execute((T)parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
}

RelayCommand có thể không cần đến phương thức CanExecute(), tức là command luôn có thể được thực thi bất kì lúc nào. Tuy nhiên việc thiếu phương thức Execute() là không được phép, vì thế cần kiểm tra giá trị của delegate Action(Of T) khi được truyền vào constructor của command này.

Khi làm ví dụ kiểm tra RelayCommand, bạn có thể thử bỏ phần accessor của event CanExecuteChanged. Lúc này, control được binding cho RelayCommand sẽ bị vô hiệu hóa và không sử dụng được.

Tạo ViewModel

Ta cần một class ViewModel chứa các dữ liệu và command cần thiết cho việc binding lên View. Để đơn giản, tôi chỉ tạo hai command dùng cho việc xóa và thêm phần tử:

class MyViewModel
{
    public ObservableCollection<Person> Persons { get; set; }
    public ICommand DeleteCommand { get; set; }
    public ICommand AddCommand { get; set; }

    public MyViewModel()
    {
        Persons = new ObservableCollection<Person>
        {
            new Person{Name="Eros"},
            new Person{Name="Tethys"},
            new Person{Name="Atlas"},
            new Person{Name="Apollo"},
            new Person{Name="Hades"},
        };

        DeleteCommand = new RelayCommand<Person>(
            (p)=>p!=null, // CanExecute()
            (p)=>Persons.Remove(p) // Execute()
            );
        AddCommand = new RelayCommand<string>(
            (s) => true, // CanExecute()
            (s) => Persons.Add(new Person { Name = s }) // Execute()
            );
    }
}

Chú ý rằng hai RelayCommand này sử dụng kiểu dữ liệu T làm tham số khác nhau. AddCommand sử dụng kiểu tham số là string, chứa dữ liệu được người dùng nhập vào từ TextBox. Bởi vì class Person tôi tạo ra (trong bài trước) chỉ có duy nhất một property là Name nên việc này rất đơn giản. Với kiểu dữ liệu phức tạp hơn, bạn cần phải sử dụng đến kĩ thuật MultiBinding.

Tạo View

Cuối cùng là thiết kế giao diện cửa sổ MainWindow. Trong ví dụ này, bạn không cần viết thêm bất kì dòng code nào trong tập tin MainWindow.xaml.cs cả.

MainWindow.xaml:

<Window x:Class="RelayCommandDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:RelayCommandDemo"
        Title="RelayCommand Demo" Height="300" Width="300">
    <Window.DataContext>
        <local:MyViewModel/>
    </Window.DataContext>
    <StackPanel Margin="5">
        <TextBox Name="textBox1" Text="[Your name]" Width="180" />
        <Grid HorizontalAlignment="Center">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Button Content="Delete" Width="80" Height="30"
                    Command="{Binding DeleteCommand}" CommandParameter="{Binding SelectedItem, ElementName=listBox1}" />

            <Button Content="Add" Height="30" Width="80" Margin="5"
                    Command="{Binding AddCommand}" CommandParameter="{Binding Text, ElementName=textBox1}"
                    Grid.Column="1" />
        </Grid>
        <ListBox Name="listBox1" ItemsSource="{Binding Persons}" DisplayMemberPath="Name"
                 Height="150" Width="200" Margin="5"/>
    </StackPanel>
</Window>

Tập tin xaml trên tạo một đối tượng MyViewModel vào Window.DataContext, khi đó mọi thành phần bên trong Window có thể binding được các property bên trong MyViewModel.

Giao diện của chương trình gồm hai Button được binding cho hai command tương ứng thông qua thuộc tính Command, đồng thời ta truyền tham số cho command bằng thuộc tính CommandParameter.

Giao diện khi thực thi:

MVVM - RelayCommand Example

Download sample project (22KB)

Tổng kết

Các thuộc tính Command, CommandParameter và CommandTarget nằm trong interface ICommandSource. Một điều không may là trong WPF, chỉ có các class ButtonBase, MenuItem và Hyperlink được hiện thực ICommandSource. Do đó, bạn chỉ có thực hiện binding các RelayCommand theo cách mà ví dụ trên thực hiện cho 3 loại control này.

Trong phần tiếp theo, ta sẽ cùng tìm hiểu một số phương pháp áp dụng mô hình MVVM và binding các RelayCommand cho các loại control bất kì.

https://yinyangit.wordpress.com

Bài liên quan

19 thoughts on “MVVM trong WPF (Part 2/4): RelayCommand

  1. Trong một số bài viết về MVVM em đọc em thấy có một số người họ dùng
    private readonly Func _canExecute;
    hoặc private readonly Func _canExecute;
    để thay thế cho private readonly Predicate _canExecute;
    Em không hiểu lắm, mong anh giải thích dùm

    Phản hồi
  2. Anh cho em hỏi sự khác nhau giữa RoutedCommand va ICommand ? Nên dùng chúng trong những trường hợp nào ? Ví dụ trong toolbar có các chức năng như New, Edit,Cancel, Delete, Search …. thì em nên dùng ICommand hay RoutedCommand với các predefined command có sẵn tương ứng như trên ..

    Phản hồi
  3. Câu hỏi bạn nên đặt là “sự khác biệt giữa Routed Command và Relay Command”. Còn ICommand chỉ là một interface và được dùng để tạo nên cả hai loại command trên. Bạn sử dụng Relay Command cho các trường hợp cần thay đổi dữ liệu ở model trong mô hình MVVM, còn lại thì dùng Routed Command.

    Phản hồi
  4. Em vẫn chưa hiểu thay đổi dữ liệu ở model là sao cả … Nếu như nút Save trên toolbar dùng để save lại các công việc của user lại thì có nghĩa là mình dùng RelayCommand , đúng không anh ? Nhưng trong bài của anh về RoutedCommand https://yinyangit.wordpress.com/2011/10/04/wpf-routed-command-co-ban/ có nói là những cái này được dùng cho Menu và Toolbar … Nhờ anh chỉ em thêm ạ

    Phản hồi
  5. Bạn cần phân biệt RoutedCommand và RelayCommand trước đã. Đừng quan trọng tên gọi của các command như Save, Cut, Paste, Add, … Đối với RoutedCommand thì đây là các command được tạo sẵn và thực hiện các công việc liên quan đến giao diện là chính. Còn đối với MVVM thì do được chia thành UI và Model nên cần loại command mới có thể liên kết được hai phần này (RelayCommand).

    Phản hồi
  6. Thực ra là có nhưng do cấu trúc chương trình khiến bạn khó nhận ra. CloseCommand là command dùng để đóng các TabPage của TabControl. Mỗi khi đóng sẽ cần thực hiện một vài công việc gì đó như save hoặc confirm, …
    CloseCommand được định nghĩa trong WorkspaceViewModel và được sử dụng trong MainWindowViewModel (thừa kế từ WorkspaceViewModel).

    Trong WorkspaceViewModel còn có một event RequestClose mà MainWindowViewModel sẽ đăng ký sự kiện vào đó để CloseCommand xử lý.

    Tức là dựa vào cơ chế của CloseCommand (trong class WorkspaceViewModel):
    _closeCommand = new RelayCommand(param => this.OnRequestClose());

    Mỗi khi command này được kích hoạt, sự kiện RequestClose sẽ được thực thi. Và bởi vì MainWindowViewModel đã đăng kí event này cho một phương thức xử lý (OnWorkspaceRequestClose), nên phương thức đó sẽ được thực thi và làm những công việc cần thiết.

    Bạn có thể tìm hiểu thêm về event nếu chưa hiểu rõ.

    Phản hồi
    • Bạn có thể tạo một property mới (ví dụ InputText) trong lớp MyViewModel để binding vào TextBox. Khi AddCommand được kích hoạt, bạn chỉ cần gán property InputText = String.Empty.

       class MyViewModel : INotifyPropertyChanged
      {
          public ObservableCollection<Person> Persons { get; set; }
          public ICommand DeleteCommand { get; set; }
          public ICommand AddCommand { get; set; }
      
          private string inputText = "[You Name]";
          public string InputText
          {
              get { return inputText; }
              set
              {
                  inputText = value;
                  this.OnPropertyChanged("InputText");
              }
          }
      
          public MyViewModel()
          {
              
      
              Persons = new ObservableCollection<Person>
              {
                  new Person{Name="Eros"},
                  new Person{Name="Tethys"},
                  new Person{Name="Atlas"},
                  new Person{Name="Apollo"},
                  new Person{Name="Hades"},
              };
      
              DeleteCommand = new RelayCommand<Person>(
                  (p)=>p!=null, // CanExecute()
                  (p)=>Persons.Remove(p) // Execute()
                  );
              AddCommand = new RelayCommand<string>(
                  (s) => true, // CanExecute()
                  (s) =>
                  {
                      Persons.Add(new Person { Name = s });
                      this.InputText = String.Empty;
                  }// Execute()
                  );
          }
      
          #region INotifyPropertyChanged Members
      
          public event PropertyChangedEventHandler  PropertyChanged;
          protected void OnPropertyChanged(string propertyName)
          {
              PropertyChangedEventHandler handler = PropertyChanged;
              if (handler != null)
                  handler(this, new PropertyChangedEventArgs(propertyName));
          }
          #endregion
      }
      

      XAML:
      TextBox Name=”textBox1″ Text=”{Binding Path=InputText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}” Width=”180″

      Phản hồi
    • Bạn cứ làm tương tự như property Name, phần quan trọng khi khi binding cho button, cần phải binding thêm tham số mới:

      <Button.CommandParameter>
              <MultiBinding Converter="{StaticResource YourConverter}">
                   <Binding Path="Text" ElementName="txt1"/>
                   <Binding Path="Text" ElementName="txt2"/>
              </MultiBinding>
          </Button.CommandParameter>
      
      Phản hồi
      • YY viết giúp mình AddCommand được không? , lúc này trên class Person của mình có 2 property là Age và Name. Theo đó, trên giao diện mình thiết kế 2 cái TextBox. Việc dùng MultiBinding mình hiểu OK rồi. Phiền bạn code cho mình command Addcommand.
        Thanks U,

  7. xin chào bạn Yin Yang
    Bạn vui lòng sửa nội dụng chỗ Tạo RelayCommand
    dòng code “public MyCommand(….” đổi thành “public RelayCommand(….”
    Mình bị lỗi chỗ này ngồi mò muốn chết mới ra lý do bị
    Thân

    Phản hồi
  8. Yin Yang cho mình hỏi làm thế nào để edit data. Ví dụ như khi mình click vào một item trong danh sách data thì item đó sẽ hiển thị nội dung lên một textbox. Khi nhấn save dữ lieu sẽ được lưu lại và cập nhật lên danh sách.

    Phản hồi
  9. Pingback: MVVM 1 INotifyPropertyChanged và INotifyCollectionChanged – Blog Dương Xuân Trà

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Đăng xuất / Thay đổi )

Connecting to %s