MVVM trong WPF (Part 3/4): Sử dụng RelayCommand cho các loại control

input devices settingsBởi vì chỉ có các class ButtonBase, MenuItem và Hyperling được hiện thực interface ICommandSource, nên chúng mới có các thuộc tính Command, CommandParameter, CommandTarget để có thể sử dụng binding cho đối tượng RelayCommand. Đối với các loại control khác, ta cần tìm giải pháp khác thay thế để hiện thực được mô hình MVVM với RelayCommand.

Tạo custom control hiện thực ICommandSource

Đây là cách thông thường nhất được lựa chọn, bạn cũng có thể tìm thấy một ví dụ trên MSDN về giải pháp này. Để có binding được, ta cần tạo các dependency property cho class thành viên của ICommandSource. Cuối cùng bạn chỉ cần override các phương thức protected tương ứng với sự kiện mà bạn muốn dùng nó để kích hoạt command.

Trong ví dụ tôi tạo một class CommandListBox thừa kế từ ListBox và dùng sự kiện double click để kích hoạt command. Với bất kì control nào, bạn cũng có thể sử dụng đoạn code tương tự:

public class CommandListBox : ListBox, ICommandSource
{
    public CommandListBox()
        : base()
    {
    }

    #region ICommand Interface Members

    // Make Command a dependency property so it can use databinding.
    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.Register(
            "Command",
            typeof(ICommand),
            typeof(CommandListBox),
            new PropertyMetadata((ICommand)null,
            new PropertyChangedCallback(CommandChanged)));

    public ICommand Command
    {
        get
        {
            return (ICommand)GetValue(CommandProperty);
        }
        set
        {
            SetValue(CommandProperty, value);
        }
    }

    public static readonly DependencyProperty ExecutedProperty =
        DependencyProperty.Register(
            "Executed",
            typeof(object),
            typeof(CommandListBox),
            new PropertyMetadata((object)null));

    public object Executed
    {
        get
        {
            return (object)GetValue(ExecutedProperty);
        }
        set
        {
            SetValue(ExecutedProperty, value);
        }
    }

    public static readonly DependencyProperty CanExecuteProperty =
        DependencyProperty.Register(
            "CanExecute",
            typeof(object),
            typeof(CommandListBox),
            new PropertyMetadata((object)null));

    public object CanExecute
    {
        get
        {
            return (object)GetValue(CanExecuteProperty);
        }
        set
        {
            SetValue(CanExecuteProperty, value);
        }
    }

    public static readonly DependencyProperty CommandTargetProperty =
        DependencyProperty.Register(
            "CommandTarget",
            typeof(IInputElement),
            typeof(CommandListBox),
            new PropertyMetadata((IInputElement)null));

    public IInputElement CommandTarget
    {
        get
        {
            return (IInputElement)GetValue(CommandTargetProperty);
        }
        set
        {
            SetValue(CommandTargetProperty, value);
        }
    }

    public static readonly DependencyProperty CommandParameterProperty =
        DependencyProperty.Register(
            "CommandParameter",
            typeof(object),
            typeof(CommandListBox),
            new PropertyMetadata((object)null));

    public object CommandParameter
    {
        get
        {
            return (object)GetValue(CommandParameterProperty);
        }
        set
        {
            SetValue(CommandParameterProperty, value);
        }
    }

    #endregion

    // Command dependency property change callback.
    private static void CommandChanged(DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        CommandListBox clb = (CommandListBox)d;
        clb.HookUpCommand((ICommand)e.OldValue, (ICommand)e.NewValue);
    }
    // Add a new command to the Command Property.
    private void HookUpCommand(ICommand oldCommand, ICommand newCommand)
    {
        // If oldCommand is not null, then we need to remove the handlers.
        if (oldCommand != null)
        {
            RemoveCommand(oldCommand, newCommand);
        }
        AddCommand(oldCommand, newCommand);
    }

    // Remove an old command from the Command Property.
    private void RemoveCommand(ICommand oldCommand, ICommand newCommand)
    {
        EventHandler handler = CanExecuteChanged;
        oldCommand.CanExecuteChanged -= handler;

    }

    // Add the command.
    private void AddCommand(ICommand oldCommand, ICommand newCommand)
    {
        EventHandler handler = new EventHandler(CanExecuteChanged);
        canExecuteChangedHandler = handler;
        if (newCommand != null)
        {
            newCommand.CanExecuteChanged += canExecuteChangedHandler;
        }
    }
    private void CanExecuteChanged(object sender, EventArgs e)
    {

        if (this.Command != null)
        {
            RoutedCommand command = this.Command as RoutedCommand;

            // If a RoutedCommand.
            if (command != null)
            {
                if (command.CanExecute(CommandParameter, CommandTarget))
                {
                    this.IsEnabled = true;
                }
                else
                {
                    this.IsEnabled = false;
                }
            }
            // If a not RoutedCommand.
            else
            {
                if (Command.CanExecute(CommandParameter))
                {
                    this.IsEnabled = true;
                }
                else
                {
                    this.IsEnabled = false;
                }
            }
        }
    }

    private static EventHandler canExecuteChangedHandler;

    protected override void OnMouseDoubleClick(MouseButtonEventArgs e)
    {
        base.OnMouseDoubleClick(e);

        if (this.Command != null)
        {
            RoutedCommand command = Command as RoutedCommand;

            if (command != null)
            {
                command.Execute(CommandParameter, CommandTarget);
            }
            else
            {
                ((ICommand)Command).Execute(CommandParameter);
            }
        }
    }
}

Sử dụng:

<local:CommandListBox x:Name="LayoutListBox" Height="100"
                        ItemsSource="{Binding Persons}" DisplayMemberPath="Name"
                        Command="{Binding DeleteCommand}"
                        CommandParameter="{Binding SelectedItem,ElementName=LayoutListBox}"/>

Sử dụng InputBinding

Là một class hiện thực ICommandSource, InputBinding cũng có các property Command, CommandParameter, CommandTarget. Tuy nhiên bạn không thể sử dụng property Command để binding bởi vì nó không phải là một dependency property. Điều này có vẻ sẽ được giải quyết trong các phiên bản WPF mới hơn.

Hiện tại bạn có thể sử dụng một trong hai giải pháp sau để thực hiện:

Cách 1: Tạo sub class của MouseBinding hoặc KeyBinding để tạo một dependency property để binding.

Như ví dụ sau tôi tạo một class MyKeyBinding và thêm property CommandBinding:

public class MyKeyBinding : KeyBinding
{
    public static readonly DependencyProperty CommandBindingProperty =
        DependencyProperty.Register("CommandBinding", typeof(ICommand),
        typeof(MyKeyBinding),
        new FrameworkPropertyMetadata(OnCommandBindingChanged));

    public ICommand CommandBinding
    {
        get { return (ICommand)GetValue(CommandBindingProperty); }
        set { SetValue(CommandBindingProperty, value); }
    }

    private static void OnCommandBindingChanged(
        DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var keyBinding = (MyKeyBinding)d;
        keyBinding.Command = (ICommand)e.NewValue;
    }
}

Sử dụng trong ListBox:

<ListBox.InputBindings>
        <local:MyKeyBinding CommandBinding="{Binding DeleteCommand}"
                                CommandParameter="{Binding SelectedItem, ElementName=listBox1}"
                                Key="Delete" Modifiers="Shift"/>

</ListBox.InputBindings>

Cách 2: Lưu trữ command trong resource

Tạo một class để lưu trữ command trong resource, sau đó ta có thể binding đến resource đó từ các InputBinding. Tuy nhiên, trường hợp ViewModel được lưu trong DataContext thì chỉ có các thành phần thuộc element tree mới thừa kế được dữ liệu ViewModel. Để giải quyết vấn đề này, ta cần cho class thừa kế từ Freezable. Đây là một thủ thuật mà ít người biết đến tuy nhiên lại rất cần thiết trong nhiều trường hợp.

public class DataCommand : Freezable, ICommand
{
    public DataCommand()
    {
    }

    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.Register("Command", typeof(ICommand), typeof(DataCommand),
        new PropertyMetadata(new PropertyChangedCallback(OnCommandChanged)));

    public ICommand Command
    {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }

    #region ICommand Members

    public bool CanExecute(object parameter)
    {
        if (Command != null)
            return Command.CanExecute(parameter);
        return false;
    }

    public void Execute(object parameter)
    {
        Command.Execute(parameter);
    }

    public event EventHandler CanExecuteChanged;

    private static void OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        DataCommand commandReference = d as DataCommand;
        ICommand oldCommand = e.OldValue as ICommand;
        ICommand newCommand = e.NewValue as ICommand;

        if (oldCommand != null)
        {
            oldCommand.CanExecuteChanged -= commandReference.CanExecuteChanged;
        }
        if (newCommand != null)
        {
            newCommand.CanExecuteChanged += commandReference.CanExecuteChanged;
        }
    }

    #endregion

    protected override Freezable CreateInstanceCore()
    {
        throw new NotImplementedException();
    }
}

Sử dụng:

<Window.DataContext>
    <local:MyViewModel />
</Window.DataContext>
<Window.Resources>
    <local:DataCommand x:Key="delCmd" Command="{Binding DeleteCommand}"/>
</Window.Resources>

<!-- ... -->

<ListBox.InputBindings>
    <KeyBinding Key="Delete" Modifiers="Shift"
                Command="{StaticResource delCmd}"
                CommandParameter="{Binding SelectedItem, ElementName=listBox1}"/>
</ListBox.InputBindings>

Thư viện Interactivity Microsoft Expression Blend SDK

Thư viện này cung cấp các chức năng giúp tương tác với ứng dụng thông qua các event và behavior. Tương tự như InputBinding,  tuy nhiên bạn có thể binding trực tiếp command vào thành phần InvokeCommandAction mà không cần tạo các class hỗ trợ.

Để sử dụng thư viện này, bạn cần cài đặt Microsoft Expression Blend SDK và add reference đến assembly System.Windows.Interactivity, sau đó khai báo thêm namespace trong tập tin xaml cần làm việc:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

Link download: Microsoft Expression Blend Software Development Kit (SDK) for .NET 4 (4.6 MB)

Sử dụng:

<ListBox Name="listBox1" ItemsSource="{Binding Persons}" DisplayMemberPath="Name"
    Height="150" Width="200" Margin="5">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseDoubleClick">
            <i:InvokeCommandAction
                    Command="{Binding DeleteCommand}"
                    CommandParameter="{Binding SelectedItem, ElementName=listBox1}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ListBox>

https://yinyangit.wordpress.com

Bài liên quan

Advertisements

10 thoughts on “MVVM trong WPF (Part 3/4): Sử dụng RelayCommand cho các loại control

  1. Theo mình được biết trên View không có code behind cua C# . Vây khi t muốn tạo một Form Register để thêm một thành viện khi đó dữ liệu sẽ được input từ bàn phím. T không biết cách nào để lấy dữ liệu cho vào ViewModel để thêm vào List. Vi du tớ đọc toàn add một cách trực tiếp

    Trả lời
  2. Em đã đọc lại các bài và làm được rồi , nhưng em sau khi thêm một phần tử vào ObserCollection hiển thị lên ListView ở ngay bên dưới form Add. Em muốn hiển thị lên form mới cái danh sách sau khi add thì không được. Anh có thể cho em mail em gửi project anh test giúp em vì em binding mãi mà không được.Thanks

    Trả lời
  3. @Yin Yang: Khi mình triển khai cái CommandListBox bên trên trong bài MVVM trong WPF (Part 2/4): RelayCommand. CommandListBox nó hiển thị lên nhưng nó bị disable là tại sao nhỉ?
    Mình debug thấy :
    private void CanExecuteChanged(object sender, EventArgs e)
    {
    if (this.Command != null)
    {
    RoutedCommand command = this.Command as RoutedCommand;
    //If a RoutedCommand
    if (command != null)
    {
    if (command.CanExecute(CommandParameter, CommandTarget))
    {
    this.IsEnabled = true;
    }
    else
    {
    this.IsEnabled = false;
    }
    }
    //If a not RoutedComand
    else
    {
    if (Command.CanExecute(CommandParameter)) <<<<<============ Nó ko chạy vào đây được CommandParameter luôn = null
    {
    this.IsEnabled = true;
    }
    else
    {
    this.IsEnabled = false;
    }
    }
    }
    }

    Giải đáp hộ mình với!
    ^^ : Cám ơn nha! Mấy chủ đề trong này rất hay!

    Trả lời
  4. ^^ Mình chạy được rồi! Vấn đề là nếu để nguyên code của Yin Yang. Thì khi chạy lần đầu ListView chưa có Item nào (= null). Nen CommandParameter = null. ListView bị disable. Mà sự kiện kích hoạt Command lại là DoubleClick. ListView ko DoubleClick đc(Nó bị Disable). Nên nó bị disable luôn. Nên Command ko Run đc. ^^

    Trả lời

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 Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s