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.

Radio buttons in DataGrids

You have probably noticed there is no <DataGridRadioButtonColumn> class in WPF. If you search online you will see people recommend using a <DataGridTemplateColumn> like this...

<DataGridTemplateColumn Header="Include">
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
           <RadioButton HorizontalAlignment="Center" IsChecked="{Binding IsIncluded}"/>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>


Which is fine. But radio buttons normally come in groups. How do you create a group for each row? If we assume the DataGrid is bound to a collection that contains a uniquifier (a single column that uniquely identifies the row) then we can bind the GroupName property to that uniquifier like so...

GroupName="{Binding ID}"

Wednesday, January 14, 2015

Blank when Zero

I have a common requirement to replace zero values in a data grid text column with spaces. This can be done easily with a converter or a trigger, but I found a way to do it with a StringFormat using a little known feature called conditional formatting. This allows you to define up to three different formats depending on whether the number is positive, negative, or zero.

In the example XAML below, I want the Orphaned ID to be blank if the bound value is zero (which indicates that there is no orphan).

<DataGridTextColumn Header="Orphaned ID" Binding="{Binding OrphanedChangeID, StringFormat='{}{0:0;;#}'}"></DataGridTextColumn>


In this example, the format for negative numbers is not defined so it defaults to the format for positive numbers. The # symbol suppresses leading zeros - effectively leaving the column blank for zeros.