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.
Friday, January 16, 2015
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}"
<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.

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.

Monday, December 29, 2014
Dynamically change column width in Reporting Services 2010
I have a requirement to hide/show columns in a Table depending on the type of document being reported and adjust another column's width to compensate. Changing a column's visibility according to a formula is easy, however there is no facility to change a column's width without using code, which is still a pain in the **** in Reporting Services.
Here are the three formats I need to support...
VENDOR requires an Item Number column and a wide Description
STORES requires a Stock Number column and a wide Description
VFS requires an Item Number column and a Stock Number column and a narrow Description.
Hiding a column is easy - you right click the column's tag, click on column visibility, check the 'Show or hide based on an expression', and then enter the Hidden value. I need to hide the Stock Number column for VENDOR documents so...
But how can you change the width of a column without a Dante-like descent into code?
Fortunately Item Number and Stock Number are both the same width (I made them 0.75" each). That means the Description column needs to be 0.75" narrower when the document is VFS.
I created a new column 0.75" wide to the right of the Description column and merged the header and body cells of the Description column with the header and body cells of the new column. With the Description column's tag selected the report designer looks like this...
Then I told Reporting Services to hide the new column when the document is VFS.
Here are the three formats I need to support...
VENDOR requires an Item Number column and a wide Description
STORES requires a Stock Number column and a wide Description
VFS requires an Item Number column and a Stock Number column and a narrow Description.
Hiding a column is easy - you right click the column's tag, click on column visibility, check the 'Show or hide based on an expression', and then enter the Hidden value. I need to hide the Stock Number column for VENDOR documents so...
But how can you change the width of a column without a Dante-like descent into code?
Fortunately Item Number and Stock Number are both the same width (I made them 0.75" each). That means the Description column needs to be 0.75" narrower when the document is VFS.
I created a new column 0.75" wide to the right of the Description column and merged the header and body cells of the Description column with the header and body cells of the new column. With the Description column's tag selected the report designer looks like this...
Then I told Reporting Services to hide the new column when the document is VFS.
Wednesday, December 3, 2014
Changing SelectedIndex on one combo box triggers SelectionChanged event on a different combo box
WPF 4.0
I just spent ten minutes trying to figure out why changing the selected index on one combo box triggers the SelectionChanged event on a different combo box.
The two combo boxes are defined thus...
<ComboBox Name="SearchTransactionTypeCombo" Binding="{StaticResource TransactionTypes}" SelectedValuePath="GLTransactionTypeID" DisplayMemberPath="Code" Style="{StaticResource DropDownList}"></ComboBox>
<ComboBox Name="EditTransactionTypeCombo" Binding="{StaticResource TransactionTypes}" SelectedValuePath="GLTransactionTypeID" DisplayMemberPath="Code" Style="{StaticResource DropDownList}" SelectionChanged="TransactionTypeCombo_SelectionChanged" ></ComboBox>
When I change SearchTransactionTypeCombo.SelectedIndex in code the TransactionTypeCombo_SelectionChanged event is raised and the sender object is EditTransactionTypeCombo. How can this be?
Eventually I thought to look at the DropDownList style in case I had declared an EventSetter in there. Here is the style definition...
As synchronization is not really needed, I removed it from the style. Alternatively I could have bound to different resources, or overridden synchronization in the control definitions.
I just spent ten minutes trying to figure out why changing the selected index on one combo box triggers the SelectionChanged event on a different combo box.
The two combo boxes are defined thus...
<ComboBox Name="SearchTransactionTypeCombo" Binding="{StaticResource TransactionTypes}" SelectedValuePath="GLTransactionTypeID" DisplayMemberPath="Code" Style="{StaticResource DropDownList}"></ComboBox>
<ComboBox Name="EditTransactionTypeCombo" Binding="{StaticResource TransactionTypes}" SelectedValuePath="GLTransactionTypeID" DisplayMemberPath="Code" Style="{StaticResource DropDownList}" SelectionChanged="TransactionTypeCombo_SelectionChanged" ></ComboBox>
When I change SearchTransactionTypeCombo.SelectedIndex in code the TransactionTypeCombo_SelectionChanged event is raised and the sender object is EditTransactionTypeCombo. How can this be?
Eventually I thought to look at the DropDownList style in case I had declared an EventSetter in there. Here is the style definition...
<Style TargetType="{x:Type ComboBox}" x:Key="DropDownList" BasedOn="{StaticResource {x:Type ComboBox}}">
<Setter Property="IsEditable" Value="False"/>
<Setter Property="IsSynchronizedWithCurrentItem" Value="True"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
</Style>
The problem is the IsSynchronizedWithCurrentItem property. Both combo boxes are bound to the same resource. When I changed SearchTransactionTypeCombo.SelectedIndex the current row of the collection was changed, which in turn changed the selected index of EditTransactionTypeCombo which raised the TransactionTypeCombo_SelectionChanged event.As synchronization is not really needed, I removed it from the style. Alternatively I could have bound to different resources, or overridden synchronization in the control definitions.
Subscribe to:
Posts (Atom)




