MVVM trong WPF (Part 1/4): INotifyPropertyChanged và INotifyCollectionChanged

WPF - Binding Notification ExampleKhi sử dụng data binding, các control hiển thị dữ liệu sẽ động được cập nhật mỗi khi dữ liệu bị thay đổi. Để làm được điều này, các đối tượng dữ liệu được hiện thực interface INotifyPropertyChanged và INotifyCollectionChanged.

INotifyPropertyChanged

Namespace: System.ComponentModel

INotifyPropertyChanged chỉ có duy nhất một thành viên là event mang tên PropertyChanged. Khi định nghĩa một class để dùng cho binding, bạn cần kích hoạt event này trong setter cho mỗi property trong class đó.
Ví dụ:

class Person:INotifyPropertyChanged
{
    private string _name;

    public string Name
    {
        get { return _name; }
        set
        {
            if (_name != value)
            {
                _name = value;
                OnPropertyChanged("Name");
            }
        }
    }

    protected void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
    public event PropertyChangedEventHandler PropertyChanged;
}

Trong trường hợp dự án của bạn có nhiều class cần hiện thực interface này, tốt nhất là nên tạo một lớp abstract để khỏi phải định nghĩa lại event PropertyChanged trong mỗi class.

abstract class PropertyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

}

class Person : PropertyChangedBase
{
    private string _name;

    public string Name
    {
        get { return _name; }
        set
        {
            if (_name != value)
            {
                _name = value;

                // comment the following line to see the difference when you edit an item
                OnPropertyChanged("Name");
            }
        }
    }
}

Tuy nhiên cách trên vẫn chưa hiệu quả và vẫn còn khá dài dòng. Vậy làm cách nào để có thể khai báo các property một cách đơn giản hơn. Sau một thời gian nghiên cứu một số phương pháp thực hiện, tôi nhận thấy cách đơn giản nhất là sử dụng kĩ thuật reflection và lập trình dynamic của .NET 4. Nếu quan tâm, bạn có thể đọc bài giới thiệu về phương pháp này tại đây:

WPF – Tự động hiện thực INotifyPropertyChanged với DynamicObject.

INotifyCollectionChanged và ObservableCollection(Of T)

Namespace:

System.Collections.Specialized

System.Collections.ObjectModel

Tương tự như interface trên, bạn có thể đoán được công dụng của INotifyCollectionChanged
là cung cấp chức năng thông báo để cập nhật lại giao diện mỗi khi một collection bị thay đổi, thông qua event NotifyCollectionChangedEventHandler CollectionChanged. Các thay đổi này có thể là thêm, xóa, di chuyển, thay thế các phần tử của collection. Trong trường hợp thay đổi các property của một phần tử trong collection, thì phần tử đó phải được hiện thực INotifyPropertyChange thì giao diện mới được cập nhật.

Tham số NotifyCollectionChangedEventArgs của event này xác định kiểu tác động đến collection thông qua enum NotifyCollectionChangedAction. Với mỗi kiểu tác động cần phải sử dụng một constructor tương ứng để tạo NotifyCollectionChangedEventArgs. Bạn sẽ thấy rõ hơn điều này khi tôi làm ví dụ trong phần sau.
Enum NotifyCollectionChangedAction bao gồm các giá trị sau:

Member name

Description

Add One or more items were added to the collection.
Remove One or more items were removed from the collection.
Replace One or more items were replaced in the collection.
Move One or more items were moved within the collection.
Reset The content of the collection changed dramatically.

.NET cung cấp class ObservableCollection(Of T) hiện thực sẵn interface này. Vì thế khi sử dụng data binding trong WPF, bạn hãy sử dụng kiểu collection này thay vì các kiểu truyền thống như ArrayList, List(Of T),…
Trong trường hợp cần chuyển đổi một collection sang kiểu ObservableCollection(Of T), hãy khởi tạo một đối tượng ObservableCollection(Of T) và truyền collection cần chuyển đổi vào constructor của nó.

Tạo một Notifiable Collection

Để hiểu thêm về cách hoạt động của ObservableCollection(Of T), cách tốt nhất là tạo một collection có chức năng tương tự. Tôi sẽ tạo một generic collection thừa kế từ class System.Collections.ObjectModel.Collection(Of T), dĩ nhiên cũng cần phải hiện thực INotifyCollectionChanged.

Class Collection(Of T) chứa sẵn các phương thức public cần thiết để thao tác với các phần tử. Các phương thức public hay indexer của nó đều được hoạt động bằng cách gọi các phương thức protected. Do đó, bạn chỉ cần override các phương thức protected để thêm việc kích hoạt event CollectionChanged.

class NotifiableCollection<T> : Collection<T>, INotifyCollectionChanged
{
    public NotifiableCollection() { }

    protected override void InsertItem(int index, T item)
    {
        base.InsertItem(index, item);
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
    }

    protected override void RemoveItem(int index)
    {
        T item = base[index];
        base.RemoveItem(index);
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item));
    }

    protected override void ClearItems()
    {
        base.ClearItems();
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    public void Move(int oldIndex, int newIndex)
    {
        T item = base[oldIndex];
        base.SetItem(newIndex, item);
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move,
            item, newIndex, oldIndex));
    }
    protected override void SetItem(int index, T item)
    {
        T oldValue = base[index];
        base.SetItem(index, item);

        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace,
                item, oldValue));
    }

    #region INotifyCollectionChanged Members

    protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (CollectionChanged != null)
            CollectionChanged(this, e);
    }

    public event NotifyCollectionChangedEventHandler CollectionChanged;

    #endregion
}

Ví dụ minh họa

MainWindow.xaml:

<Window x:Class="DataChangedNotificationDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Binding Notification" Height="320" Width="300">

    <StackPanel Margin="5">
        <TextBox Height="24"
                 Name="textBox1" Text="[Your name]" Width="180" />
        <Grid HorizontalAlignment="Center">
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Button Content="Add" Height="30" Width="80" Margin="5"
                Name="addButton" Click="addButton_Click" />
            <Button Content="Update" Height="30" Width="80" Margin="5"
                    Grid.Column="1"
                Name="updateButton" Click="updateButton_Click" />
            <Button Content="Replace" Height="30" Width="80" Margin="5"
                    Grid.Row="1"
                Name="replaceButton" Click="replaceButton_Click" />
            <Button Content="Clear" Height="30" Width="80" Margin="5"
                    Grid.Column="1" Grid.Row="1"
                Name="clearButton" Click="clearButton_Click" />
        </Grid>

        <ListBox Name="listBox1" ItemsSource="{Binding}"
                 Height="150" Width="200" Margin="5">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <DockPanel>
                        <Button Content="X" Width="20" Height="20"
                                Click="deleteButton_Click"
                                Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}"/>
                        <TextBlock Text="{Binding Name}" Margin="5,3,0,0"/>
                    </DockPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </StackPanel>
</Window>

MainWindow.xaml.cs:

public partial class MainWindow : Window
{
    NotifiableCollection<Person> _persons;

    public MainWindow()
    {
        InitializeComponent();

        _persons = new NotifiableCollection<Person>
        {
            new Person{Name="Eros"},
            new Person{Name="Tethys"},
            new Person{Name="Atlas"},
            new Person{Name="Apollo"},
            new Person{Name="Hades"},
        };
        this.DataContext = _persons;
    }

    private void deleteButton_Click(object sender, RoutedEventArgs e)
    {
        var p = (sender as Button).DataContext as Person;
        _persons.Remove(p);
    }

    private void addButton_Click(object sender, RoutedEventArgs e)
    {
        _persons.Add(new Person { Name = textBox1.Text });
    }

    private void updateButton_Click(object sender, RoutedEventArgs e)
    {
        if (listBox1.SelectedItem != null)
            _persons[listBox1.SelectedIndex].Name  = textBox1.Text;
    }

    private void clearButton_Click(object sender, RoutedEventArgs e)
    {
        _persons.Clear();
    }

    private void replaceButton_Click(object sender, RoutedEventArgs e)
    {
        if (listBox1.SelectedItem != null)
        {
            _persons[listBox1.SelectedIndex] = new Person { Name = textBox1.Text };
            listBox1.Items.Refresh();
        }
    }
}

Giao diện:
WPF - Binding Notification Example

Tổng kết

Ví dụ đơn giản trên là cách thông thường để viết ứng dụng và nó không theo mô hình nào cả. Sự trộn lẫn giữa các tầng giao diện, xử lý, dữ liệu khiến cho ứng dụng thiếu linh hoạt và khó phát triển.

Trong phần tiếp theo, bạn sẽ thấy cách tôi sử dụng Command để thay thế phương thức xử lý sự kiện. Việc ứng dụng Command giúp cho các phần của ứng dụng được tách biệt rõ ràng hơn. Lý do là các Command được định nghĩa trong phần ViewModel, và theo nguyên tắc chúng không thể truy xuất được các thành phần của View (giao diện).

http://yinyangit.wordpress.com

Bài liên quan

Cơ bản về MVVM (Model – View – ViewModel) Pattern

Posted in WPF. Thẻ: . 12 phản hồi »

12 phản hồi tới “MVVM trong WPF (Part 1/4): INotifyPropertyChanged và INotifyCollectionChanged”

  1. Án Bình Trọng nói:

    Sự kiện OnPropertyChanged làm sao gắn kết được PropertyChange. Bạn có thể giải thích rõ cơ chế bên trong được không?

  2. Án Bình Trọng nói:

    À, mình lờ mờ hiểu ra vấn đề. Khi thay đổi giá trị gì, thì nó gọi sự kiện OnPropertyChanged. Do nhiều trang ví dụ giống nhau quá, mình lại tưởng nó là implement nào đó của INotifyPropertyChanged

  3. Yin Yang nói:

    Thực ra không phải giống nhau mà do cách thực hiện như vậy đã thành một “chuẩn” hay thói quen khi lập trình:
    - Không kích hoạt event trực tiếp mà phải thông qua một phương thức. Phương thức này thường được đặt tên là OnEventName() hoặc RaiseEventName().

    - Việc gọi phương thức giúp cho code gọn hơn, ngoài ra thì bạn có thể debug hoặc làm việc gì đó mỗi khi gọi event.

    - Các phương thức này được đặt là protected để chỉ có subclass mới có thể sử dụng. Trong WinForms nếu bạn gõ override trong Form cũng sẽ thấy rất nhiều phương thức tương tự như OnLoad, OnClick, OnClosed,…

  4. diepdc nói:

    thanks ban, Tai khoan Yin Yang nay quen quen tren congdongcviet thi phai

  5. Yin Yang nói:

    oh đúng rồi, mình có 1 thời gian tham gia bên đó.

  6. Phi nói:

    Cho mình hỏi các sự kiện OnpropertyChanged, RaisePropertyChanged, NotifyPropertyChanged,FirePropertyChanged nó khác nhau ra sao và được dùng khi nào ?

    • Yin Yang nói:

      Đây chỉ là các tên bạn lập trình viên tự đặt. Như tên gọi, các phương thức này có mục đích kích hoạt một event nào đó. Bạn có thể đọc bài sau để tìm hiểu về event:

      Tạo, sử dụng và quản lý Event trong C#

      • hai_cs0110 nói:

        abstract class PropertyChangedBase : INotifyPropertyChanged
        {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
        {
        if (PropertyChanged != null)
        {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        }
        }

        Tại sao phải là abstract class vậy anh YIN nếu mình không dùng abstract có được không ? Và tại sao

  7. hai_cs0110 nói:

    Nhờ anh giải thích dùm

    • Yin Yang nói:

      Tất nhiên là bạn có thể ko dùng abstract class. Nhưng điều này ko nên vì class mình ra ko muốn nó có khả năng tạo ra được instance. Điều này giúp logic chương trình rõ ràng và tránh được các rủi ro về sau.

  8. youngj nói:

    Cho mình hỏi, nếu đã dùng NotifiableCollection thì ở Person k cần implement INotifyPropertyChanged, và mình demo ở updateButton, listBox không tự refresh lại Items, giống như khi Update item k sinh ra sự kiện OnPropertyChanged. MÌnh mới tìm hiểu về WPF…

  9. Yin Yang nói:

    Không thấy câu hỏi của bạn nhưng có vẻ bạn nhầm lẫn giữa Notify của một đối tượng và Notify của một collection. Như trong bài đã viết thì INotifyCollectionChanged chỉ có tác dụng với các sự kiện làm thay đổi các item của collection. Còn bản thân các property của item thay đổi thế nào thì phải dựa vào INotifyPropertyChanged của chính item đó.


Gửi phản hồi

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Thay đổi )

Twitter picture

You are commenting using your Twitter account. Log Out / Thay đổi )

Facebook photo

You are commenting using your Facebook account. Log Out / Thay đổi )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 141 other followers