Thursday, February 2, 2017

Binding to an indexed property in VB

This is for framework 4.0

We know that we can have indexed properties but how do we bind to them and handle INotifyPropertyChanged? In this post, we will explore binding labels, textboxes, and datagridtextcolumns.

Start a new Visual Basic WPF Application and call it BindingToIndexedProperty.


We will create two labels, two textboxes, and a datagrid with two columns. The labels, textboxes and column headings will be dynamically populated as the application starts. There will also be a button that allows them to be changed to show INotifyPropertyChanged working. We will use an MVVM type model but simplified to use the window's class instead of a separate class.

Let's start with the XAML for the labels and textboxes. Replace the contents of MainWindow.xaml with the XAML below.

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:BindingToIndexedProperty"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"></RowDefinition>
            <RowDefinition Height="auto"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="auto"></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Label Grid.Column="0" Grid.Row="0" Content="{Binding Text[0]}"></Label>
        <Label Grid.Column="0" Grid.Row="1" Content="{Binding Text[1]}"></Label>
        <TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Text[0]}"></TextBox>
        <TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Text[1]}"></TextBox>
    </Grid>
</Window>

We see in this XAML that the labels and textboxes are bound to the same indexed property. Note the use of square brackets in the paths.

In MainWindow.xaml.vb add the private and public implementations of the Text property inside the class.

Class MainWindow

    Private _Text(2) As String
    Public Property Text(index) As String
        Get
            Return _Text(index)
        End Get
        Set(value As String)
            If _Text(index) <> value Then
                _Text(index) = value
            End If
        End Set
    End Property

End Class

We can run the application and everything will bind but it won't be very interesting. We can initialize the text in the New method like this.

Public Sub New()
    PreInitialize()
    InitializeComponent()
End Sub

Private Sub PreInitialize()
    Text(0) = "Hello"
    Text(1) = "Goodbye"

End Sub

Running the application now shows the binding to the indexed property has been successful.


Now let's look at implementing INotifyPropertyChanged. Add a button to the XAML with a RoutedCommand. Here is definition of the Routed Command which goes before the opening tag of the Grid.

<Window.Resources>
    <RoutedCommand x:Key="PostInitialize"></RoutedCommand>
</Window.Resources>
<Window.CommandBindings>
    <CommandBinding Command="{StaticResource PostInitialize}" Executed="PostInitialize_Executed"/>
</Window.CommandBindings>

And the button is inserted just before the closing tag for the Grid and looks like this.

<Button Grid.Column="0" Grid.Row="3" Content="Post Initialize" Command="{StaticResource PostInitialize}"/>

We need to add the PostInitialize function to our code like this...

Private Sub PostInitialize_Executed(sender As Object, e As ExecutedRoutedEventArgs)
    Text(0) = "Bonjour"
    Text(1) = "Au Revior"
End Sub

None of this will work until we implement INotifyPropertyChanged. Start by adding an Imports statement...

Imports System.ComponentModel

and an implements clause to the class definition so that it looks like this...

Class MainWindow
    Implements INotifyPropertyChanged

Now that we have said we implement INotifyPropertyChanged we have to add the PropertyChanged event and a method to raise it. We've all seen this code before, I'm sure.

Public Event PropertyChanged(sender As Object, e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
Private Sub NotifyPropertyChanged(Name As String)
    RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(Name))
End Sub

and finally we call NotifyPropertyChanged from the setter of the Text Property. Note we call this function with "Text". Not "Text(0)" or "Text[0]". The setter now looks like this.

Set(value As String)
    If _Text(index) <> value Then
        _Text(index) = value
        NotifyPropertyChanged("Text")
    End If
End Set

Now when we click the [Post Initialize] button the text changes which demonstrates that INotifyPropertyChanged is working.


Now let's see how we could bind the headings of a datagrid to an indexed property. Binding anything other that the Binding property in a DataGridColumn is difficult because they do not exist in the Visual Tree so they don't have a DataContext. Let's start by defining our DataGrid and it's two columns.

Add the following XAML after the <Button... > element

<DataGrid Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="2" IsReadOnly="true" AutoGenerateColumns="false">
    <DataGrid.Columns>

        <DataGridTextColumn Header="{Binding Text[0]}" Width="*"></DataGridTextColumn>
        <DataGridTextColumn Header="{Binding Text[1]}" Width="*"></DataGridTextColumn>
    </DataGrid.Columns>
</DataGrid>

If DataGridColumns were in the Visual Tree this would work. But they're not so it doesn't. Give it a try if you don't believe me. There's an old trick using a Proxy Element that is commonly used to overcome this enormous flaw in WPF.

We start by adding the proxy element before the DataGrid like this. It will inherit it's DataContext from the first parent that has a DataContext defined which happens to be the window.

<FrameworkElement x:Name="ProxyElement" Visibility="Collapsed"></FrameworkElement>

In the DataGridColumn's binding we can reference the proxy element using the x:Reference form even though it's not in the same visual tree as the DataGridColumn. Bear in mind that our source is the proxy element so our path is DataContext.<PropertyName>[index]. Our DataGridColumns now looks like this.

<DataGridTextColumn Header="{Binding DataContext.Text[0], Source={x:Reference ProxyElement}}" Width="*"></DataGridTextColumn>
<DataGridTextColumn Header="{Binding DataContext.Text[1], Source={x:Reference ProxyElement}}" Width="*"></DataGridTextColumn>

At this point the editor may highlight the Source clause with an error. However the compiler will not generate an error. This bug has been fixed in Framework 4.5

Note you can now use the same proxy element to bind any dependency property of any DataGridColumn on the page.


No comments:

Post a Comment