Friday, January 16, 2015

INotifyPropertyChanged

There are many examples of using INotifyPropertyChanged on the web but I could not find one example written in VB so here is my explanation of how you know you need it and how to implement it in VB.

Let's start with a very simple page.

<Window x:Class="MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" DataContext="{Binding RelativeSource={RelativeSource self}}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="auto"></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <DataGrid Name="NumbersDataGrid" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" ItemsSource="{Binding Numbers}" AutoGenerateColumns="False" IsReadOnly="true">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Number" Binding="{Binding Number}"/>
            </DataGrid.Columns>
        </DataGrid>
        <Button Name="AddButton" Grid.Row="1" Grid.Column="0" Content="Add Row" Click="AddButton_Click"></Button>
        <Button Name="DeleteButton" Grid.Row="1" Grid.Column="1" Content="Delete Row" Click="DeleteButton_Click"></Button>
        <Button Name="IncrementButton" Grid.Row="1" Grid.Column="2" Content="Increment" Click="IncrementButton_Click"></Button>
    </Grid>
</Window>

This XAML defines a window whose DataContext is its own code behind. The window contains a DataGrid that is bound to a public property called Numbers. It also has three buttons to add and delete rows, and increment the number in the first row. The code behind looks like this...

Class MainWindow
    Public Class cNumber
        Public Property Number As Integer
        Public Sub New(Number As Integer)
            Me.Number = Number
        End Sub
    End Class

    Public Property Numbers As New System.Collections.Generic.List(Of cNumber) From {New cNumber(1)}

    Private Sub AddButton_Click(sender As System.Object, e As System.Windows.RoutedEventArgs)
        Numbers.Add(New cNumber(Numbers.Count + 1))
    End Sub

     Private Sub DeleteButton_Click(sender As System.Object, e As System.Windows.RoutedEventArgs)
        If Numbers.Count > 0 Then
            Numbers.RemoveAt(Numbers.Count - 1)
        End If
    End Sub

     Private Sub IncrementButton_Click(sender As System.Object, e As System.Windows.RoutedEventArgs)
        If Numbers.Count > 0 Then
            Numbers(0).Number += 1
        End If
     End Sub

End Class

If you run this example you will see none of the buttons work. Even though the Numbers collection is being modified, the grid does not reflect the changes without being explicitly rebound every time the collection changes.

We can get the Add and Delete buttons working by simply changing the Generic.List to an ObjectModel.ObservableCollection. The ObservableCollection is really just a List that implements INotifyPropertyChanged whenever a property of the collection is changed ie a row is added or deleted or a member of the list is changed.

Public Property Numbers As New System.Collections.ObjectModel.ObservableCollection(Of cNumber) From {New cNumber(1)}

Change the List to an ObservableCollection and try the application again.

The [Add] and [Delete] work as promised, but [Increment] does not.

If this was an ObservableCollection of Integer everything would work right now. But in real life we will have more complex DataGrids that need to be bound to a collection of classes which is why I used the cNumber class in this example. Now pay attention.

When a collection contains structures or objects it doesn't actually contain the objects, it contains pointers to those objects. When you modify the contents of the object, you don't change its address so the collection doesn't change and does not notify the bound control that something changed. So to deal with modifications to objects, the object itself has to be responsible for change notification which means it has to implement INotifyPropertyChanged.

We start off by changing the cNumber class to implement INotifyPropertyChange like this...

Public Class cNumber
    Implements System.ComponentModel.INotifyPropertyChanged

Once you have added this implementation VisualStudio will automatically declare a PropertyChanged event in your code. You will see this line added to the cNumber class...

Public Event PropertyChanged(sender As Object, e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged

Underneath this event add this new method. It will be called whenever a property setter detects a change.

Private Sub NotifyPropertyChanged(Name As String)
    RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(Name))
End Sub

The following steps need to be done for every bound, modifiable property - this can be a pain.

We are going to have to modify the setter for the Number property which means we need to explicitly declare the private member. Add the declaration for _Number and also add the explicit getter and setter for the Number property like this...


Private _Number As Integer

Public Property Number As Integer
    Get
        Return _Number
    End Get
    Set(value As Integer)
        If _Number <> value Then
            _Number = value
            NotifyPropertyChanged("Number")

        End If
    End Set
End Property

As you can see, raising the PropertyChanged event simply says "The value of this property changed, go tell everyone." If you run the application now you can see that clicking [Increment] increments the value of the first row.

No comments:

Post a Comment