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).

https://yinyangit.wordpress.com

Bài liên quan

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

Advertisements

22 thoughts on “MVVM trong WPF (Part 1/4): INotifyPropertyChanged và INotifyCollectionChanged

  1. 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,…

    Phản hồi
  2. 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…

    Phản hồi
  3. 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 đó.

    Phản hồi
  4. Hi cậu! Sao khi dùng cơ chế binding của wpf mà lại không dùng: ItemSource=”{ItemSource person}” mà list person sẽ nằm trong lớp data nhỉ.

    Khi dùng cơ chế của wpf thì mình nên nghĩ hạn chế viết code bedhind như thế các bạn khác mới thấy được cái hay của wpf

    Phản hồi
  5. Chào bạn, mình đang áp dụng MVVM cho dự án của mình, tuy nhiên đang gặp vấn đề khá đau đầu. Mình tìm mãi các trợ giúp nhưng vẫn không giải quyết được. Mong được hỗ trợ của bạn. Mình mô tả đại khái như thế này.
    Mình áp dụng MVVM + WCF + Entity Framework với
    (Model + Viewmodel) —– WCF —— Entity Framework (Model) – Database
    Khi Viewmodel yêu cầu WCF lấy các object từ EF xuống thì bị trục trặc với các Navigation Properties.
    Ví dụ mình có bảng Class quan hệ một – nhiều với bảng Student. Khi WCF trả object Student xuống cho Viewmodel thì bị thiếu dữ liệu liên kết tới bảng Class (Chẳng hạn mình muốn lấy Student.Class.Name của obj Student nhưng không được). Vậy bạn có cách giải quyết vấn đề này không?

    Phản hồi
  6. Vấn đề này mình ko có nhiều kinh nghiệm nhưng cũng góp ý bạn kiểm tra lại phần WCF kĩ rồi thực hiện các phần tiếp theo. Ko cần thiết phải gộp chung một nhóm dự án lại để xem xét, bạn nên coi chúng như các phần độc lập và ko phụ thuộc lẫn nhau.

    Phản hồi
  7. hi bạn! Bạn muốn lấy: Student.Class.Name thì hãy xem lại câu LINQ của bạn như thế nào? Nếu câu LINQ của bạn không hỗ trợ thì làm sao nó lấy được. Bạn có thể tìm hiểu “Include” đây là cách lấy table quan hệ! Mong có thể giúp được bạn!

    Phản hồi
  8. Lâu lâu lại trở lại blog của Yinyang.Hiện tại mình đọc lại bài viết về MVVM của bạn thấy 1 thắc mắc đó là: Nếu mình dùng Entity Frame Work code first để làm Model ,thì khi này mỗi thuộc tính lại phải khai báo kích hoạt sự kiện OnPropertyChanged ah.
    Và dùng EF mà hiện thực INotifyPropertyChaged thì báo lỗi .
    Bạn có thể cho mình giải pháp hay lời khuyên được không

    Phản hồi
  9. Theo như em thấy thì khi kích hoạt phương thức OnPropertyChanged thì có sử dụng cơ chế LazyLoading cho sự kiện PropertyChanged. nhưng em k thấy anh định nghĩa cho sự kiện PropertyChanged vậy thì nó thực hiện gì? Em bị mơ hồ chỗ này

    Phản hồi
    • PropertyChanged là một event thuộc interface INotifyPropertyChaged.
      Một event chỉ cần được xác định khi nào nó sẽ được kích hoạt, còn việc xử lý thì phụ thuộc vào nơi nó được sử dụng, bằng cách thêm (đăng kí) các event handler.
      Ví dụ như bạn viết xử lý cho event Click của Button. HandleButtonClick là một event handler của event click:

      btn.Click += HandleButtonClick;

      Ở đây thì PropertyChanged sẽ tự động được các lớp sử dụng nó (như các ObservableCollection) xử lý.

      Phản hồ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 Đă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