I saw this tweet in my Windows Client tweetstream today:
I thought, "what a good idea for a sample." I know that binding
in WPF and
Silverlight
can be a challenge, especially if you're new to the technology. It
can be even more difficult when you're working with an edge case
like binding using indexers.
So, here's a quickie on binding to a Dictionary<TKey,
TValue> in WPF (note that the concept applies to Silverlight as
well).
Binding to Items by Key
To bind to an element by a string key, you need to include the
key inside brackets, just as you would in C# code. You omit any
quotes, though. For example, if you want to bind a textbox to the
dictionary item with a key of "field1", and support both display
and editing, you'd write this:
<TextBox Text="{Binding [field1], Mode=TwoWay}" />
That assumes your dictionary is in scope as the DataContext.
Let's say you have a view model with a dictionary called "Fields"
and you want to bind to field1 in that dictionary, but your view
model, not the dictionary, is the DataContext. Simple:
<TextBox Text="{Binding Fields[field1], Mode=TwoWay}" />
You can take a similar approach to use numeric indexers in
collections: just use a number instead of the string.
Example
Here's a quick example that shows binding to the dictionary,
using a simple code-behind approach.
Code-Behind
The code-behind is shown below. In a real application, I
recommend this (or similar) code be placed in a view model.
public partial class MainWindow : Window
{
Dictionary<string, object> _data = new Dictionary<string, object>();
public MainWindow()
{
InitializeComponent();
Loaded += new RoutedEventHandler(MainWindow_Loaded);
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
_data["field1"] = 100;
_data["field2"] = "Pete Brown";
_data["field3"] = new DateTime(2010, 03, 08);
this.DataContext = _data;
}
}
Note that the fields are all declared as object. If you have a
homogeneous set, you can more strongly type. One limitation of a
dictionary approach is that all the field values must be the same
type.
Xaml UI
The binding is as I described above. For grins, I threw in a
string formatter to show it's possible.
<Window x:Class="WpfDictionaryBinding.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">
<Window.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment"
Value="Right" />
<Setter Property="VerticalAlignment"
Value="Center" />
<Setter Property="Margin"
Value="0 0 10 0" />
</Style>
<Style TargetType="{x:Type TextBox}">
<Setter Property="HorizontalAlignment"
Value="Stretch" />
<Setter Property="VerticalAlignment"
Value="Stretch" />
<Setter Property="Margin"
Value="4" />
</Style>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="Field 1"
Grid.Row="0"
Grid.Column="0" />
<TextBlock Text="Field 2"
Grid.Row="1"
Grid.Column="0" />
<TextBlock Text="Field 3"
Grid.Row="2"
Grid.Column="0" />
<TextBox Text="{Binding [field1], Mode=TwoWay}"
Grid.Row="0"
Grid.Column="1" />
<TextBox Text="{Binding [field2], Mode=TwoWay}"
Grid.Row="1"
Grid.Column="1" />
<TextBox Text="{Binding [field3], Mode=TwoWay, StringFormat='D'}"
Grid.Row="2"
Grid.Column="1" />
<StackPanel Grid.Row="3"
Grid.Column="1">
<TextBlock Text="Raw Values" />
<TextBlock Text="{Binding [field1]}" />
<TextBlock Text="{Binding [field2]}" />
<TextBlock Text="{Binding [field3]}" />
</StackPanel>
</Grid>
</Window>
When run, it looks like this:
That's it. However, you may have noticed that the "raw values"
section never updates when you change values. Read on.
Change Notification
Of course, the problem comes about when you change the values in
the dictionary: the UI doesn't change. That's because the built-in
Dictionary class is not observable; it does't raise any change
events to notify the binding system that the values have
changed.
The community has come up with a few implementations of an
ObservableDictionary. This link is to one of the good ones, by Dr.
WPF. Here is another ObservableDictionary on
codeplex. Unfortunately, the first works only for ItemsControl
type scenarios (not field changing) and the latter is not really
geared towards WPF.
However, with one simple change, the one from Dr. WPF will work in our scenario
(WPF only, you'd need to spin off a version for Silverlight). Open
up the source code for the ObservableCollection, and find
public TValue this[TKey key] and change it like
this:
// old version
public TValue this[TKey key]
{
get { return (TValue)_keyedEntryCollection[key].Value; }
set { DoSetEntry(key, value);}
}
// new version
public TValue this[TKey key]
{
get { return (TValue)_keyedEntryCollection[key].Value; }
set
{
DoSetEntry(key, value);
OnPropertyChanged(Binding.IndexerName);
}
}
The change is simply to fire the PropertyChanged event for the item.
Note that I haven't tested the dictionary class to find out if
there are ways around this or edge cases, so there certainly are
likely to be some. That said, this will solve the main issue.
By simply using the ObservableDictionary with this modification,
rather than the regular Dictionary, your notification problems will
be solved. Please note, however, that there's a slight performance
penalty to pay for ObservableDictionary, so you'll want to be smart
about how you use them in your own applications.
I hope that helps you out in your apps.
Updated 2010-03-10: Replaced "Item[]" with the
Binding.IndexerName constant.