.NET - Advanced databinding with ViewModels in Silverlight
- Date:
- Author: Stefan Cruysberghs
At my work we are building a large enterprise application and I’m using Silverlight 3, RIA Services and the Model-View-ViewModel pattern. Silverlight and RIA Services are great technologies and the M-V-VM pattern is a nice approach to keep UI and logic separated. But using them together can be a laborious task. The databinding features in Silverlight are quite limited and several Silverlight controls are not fully worked out yet. This makes that using the combination of view models with item controls, dataforms and commands causes several difficulties. In this article I will try to deal with some issues and provide some solutions.
- Access ViewModel properties in a ListBoxItem
- Access ListBoxItem properties in a ListBoxItem
- Access Commands in a ListBoxItem
- Access ViewModel properties and Commands in a DataForm
- INotifyPropertyChanged vs. DependencyProperty in a ViewModel
Access ViewModel properties in a ListBoxItem
When you bind a collection to the ItemsSource of a ListBox or ItemsControl, the DataContext of the ListBoxItems will be set to an individual item of the collection. Therefore you won’t have access to your ViewModel anymore.
e.g. You have a ViewModel with a collection of Persons, a property which contains the font size and a property which specifies if the first name of the person should be displayed. The FontSize and IsFirstNameVisible properties from the ViewModel can’t be accessed in a ListBoxItem by the normal approach.
public class ViewModel : ViewModelBase
{
private int fontSize;
public int FontSize
{
get
{
return fontSize;
}
set
{
fontSize = value;
RaisePropertyChanged(() => FontSize);
}
}
public ObservableCollection<Person> Persons ...
public bool IsFirstNameVisible ...
public ViewModel()
{
var collection = new ObservableCollection<Person>();
collection.Add(new Person() { FirstName = "Jan", LastName = "Jansen"});
collection.Add(new Person() { FirstName = "Piet", LastName = "Pieters" });
collection.Add(new Person() { FirstName = "Els", LastName = "Van Elsen" });
Persons = collection;
IsFirstNameVisible = false;
FontSize = 10;
}
}
Silverlight 3 offers a new ElementName binding feature but it does not offer a RelativeSource FindAncestor binding mode like WPF does. Fortunately I found a great solution to solve this problem on Colin Eberhardt’s blog. He created a handy BindingHelper class which supports several relative source capabilities. Colin’s control provides attached properties which call the VirtualTreeHelper internally to set up a TwoWay binding to relative parent controls.
By using this BindingHelper class you can access the DataContext of the ListBox which refers to your ViewModel. This is what the solution will look like:
<ListBox ItemsSource="{Binding Persons}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding FirstName}" Margin="2"></TextBlock>
<TextBlock Text="{Binding LastName}" Margin="2"></TextBlock>
<TextBlock Text="{Binding FontSize}" FontSize="{Binding FontSize}" Margin="2">
<Framework:BindingHelper.Binding>
<Framework:BindingProperties
SourceProperty="DataContext"
TargetProperty="DataContext"
RelativeSourceAncestorType="ListBox">
</Framework:BindingProperties>
</Framework:BindingHelper.Binding>
</TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Additionally I created a small DataHolder control. This ContentControl will help you to simplify the XAML code and it is also handy if you don’t want to change the DataContext of the control. The DataHolder has an extra Data property and it requires the BindingHelper to find the ViewModel and assign it to the Data property. Then you have to use the ElementName binding to refer to the DataHolder. Make sure to refer to the Data property in the Path. This approach works really good and it can be used for a number of issues.
public class DataHolder : ContentControl
{
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(DataHolder),
new PropertyMetadata(null));
public DataHolder()
{
HorizontalContentAlignment = HorizontalAlignment.Stretch;
VerticalContentAlignment = VerticalAlignment.Stretch;
}
}
<TextBlock Text="Font size:" Margin="0,5,0,0"></TextBlock>
<Toolkit:NumericUpDown Value="{Binding FontSize, Mode=TwoWay}"></Toolkit:NumericUpDown>
<TextBlock Text="First name visible:" Margin="0,5,0,0"></TextBlock>
<CheckBox IsChecked="{Binding IsFirstNameVisible, Mode=TwoWay}"></CheckBox>
<ListBox
ItemsSource="{Binding Persons}"
Margin="0,5,0,0">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Framework:DataHolder x:Name="DataHolder">
<Framework:BindingHelper.Binding>
<Framework:BindingProperties
SourceProperty="DataContext"
TargetProperty="Data"
RelativeSourceAncestorType="ListBox">
</Framework:BindingProperties>
</Framework:BindingHelper.Binding>
</Framework:DataHolder>
<TextBlock
Text="{Binding ElementName=DataHolder, Path=Data.FontSize}"
FontSize="{Binding ElementName=DataHolder, Path=Data.FontSize}"
Margin="2">
</TextBlock>
<TextBlock
Text="{Binding FirstName}"
Visibility="{Binding ElementName=DataHolder, Path=Data.IsFirstNameVisible,
Converter={StaticResource VisibilityConverter}}"
Margin="2">
</TextBlock>
<TextBlock Text="{Binding LastName}" Margin="2"></TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
If you want to find more info about the VisibilityConverter or other value converters, you should check out one of my older articles.
Access ListBoxItem properties in a ListBoxItem
When you supply a DataTemplate to a ListBox, then the ListBox will automatically render your visual elements inside a ListBoxItem control. This ListBoxItem control has an interesting IsSelected property. Normally there is no way to bind properties of the ListBoxItem via an element name or otherwise. However by using the BindingHelper you can reach the ListBoxItem by navigating up the virtual tree using a relative source binding.
<Button
Text="Delete">
<Framework:BindingHelper.Binding>
<Framework:BindingProperties
SourceProperty="IsSelected"
TargetProperty="Visibility"
Converter="{StaticResource VisibilityConverter}"
RelativeSourceAncestorType="ListBoxItem">
</Framework:BindingProperties>
</Framework:BindingHelper.Binding>
</Button>
Access Commands in a ListBoxItem
When using the M-V-VM pattern and ViewModels you will need to implement commands to avoid event handling implementation (e.g. button clicks) in the code behind your XAML. Silverlight provides an ICommand interface but it lacks an implementation of this interface. Fortunately there are several good command implementations like the Composite Silverlight (Prism) DelegateCommand class, the Telerik RoutedCommand class or other open source implementations.
Using a command in an ItemsControl/ListBox DataTemplate will not work because the command can’t be found in the DataContext which is only an individual item of a collection. To solve this you need to use the same BindingHelper solution.
<Button
Text="Delete"
Commands:Click.Command="{Binding ElementName=DataHolder, Path=Data.DeleteCommand}"
Commands:Click.CommandParameter="{Binding}">
</Button>
Prism only offers a ClickCommand implementation but it is not so difficult to create your own commands for event handlers of Silverlight controls. Andrea Boschin posted a handy code snippet to generate all the code needed to implement a command behavior.
Call the code snippet and fill in the command name and the attached control type. Now you only have to change the implementation of the constructor. Because it is easy to create command behaviors I would recommend not to implement commands in your custom controls. Just add RoutedEvenHandlers in your custom controls and create command behaviors for the events you need to capture in a ViewModel. The advantage is that your custom controls will also work when the Prism or another Command implementation is not available.
Here are some alternative examples of command behaviors:
public class DoubleClickDataGridCommandBehavior : CommandBehaviorBase<DataGrid>
{
private long previousTicks;
public DoubleClickDataGridCommandBehavior(DataGrid targetObject) : base(targetObject)
{
targetObject.MouseLeftButtonDown += OnMouseLeftButtonDown;
}
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if ((DateTime.Now.Ticks - previousTicks) < 5000000)
{
ExecuteCommand();
previousTicks = 0;
}
previousTicks = DateTime.Now.Ticks;
}
}
<Controls:DataGrid
ItemsSource="{Binding DataSource.Data}"
Framework:DoubleClickDataGrid.Command="{Binding GotoDetailCommand}"
Framework:DoubleClickDataGrid.CommandParameter="{Binding}">
</Controls:ExtDataGrid>
public class EnterUpPasswordBoxCommandBehavior : CommandBehaviorBase<PasswordBox>
{
public EnterUpPasswordBoxCommandBehavior(PasswordBox targetObject)
: base(targetObject)
{
targetObject.KeyUp += OnKeyUp;
}
private void OnKeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
ExecuteCommand();
}
}
}
Access ViewModel properties and Commands in a DataForm
The new Silverlight DataForm control has the same limitation as all items controls. Once the CurrentItem property is set, the DataContext refers to an individual item of the collection. So you can’t access other properties from your ViewModel like collection properties for ComboBoxes or Commands. The ElementName binding can only refer to items within the DataForm.
Here as well, the solution for this problem is the same. Add my DataHolder control and the BindingHelper in the DataTemplate and use the ElementName binding to refer to the DataHolder control.
<TextBlock Text="Font size:" Margin="0,5,0,0"></TextBlock>
<Toolkit:NumericUpDown Value="{Binding FontSize, Mode=TwoWay}"></Toolkit:NumericUpDown>
<TextBlock Text="First name visible:" Margin="0,5,0,0"></TextBlock>
<CheckBox IsChecked="{Binding IsFirstNameVisible, Mode=TwoWay}"></CheckBox>
<DataFormToolkit:DataForm
Margin="0,5,0,0"
CurrentItem="{Binding Persons[0]}">
<DataFormToolkit:DataForm.EditTemplate>
<DataTemplate>
<StackPanel>
<Framework:DataHolder
x:Name="DataHolder">
<Framework:BindingHelper.Binding>
<Framework:BindingProperties
TargetProperty="Data"
SourceProperty="DataContext"
RelativeSourceAncestorType="DataForm"/>
</Framework:BindingHelper.Binding>
</Framework:DataHolder>
<DataFormToolkit:DataField
Label="First name"
Visibility="{Binding ElementName=DataHolder, Path=Data.IsFirstNameVisible,
Converter={StaticResource VisibilityConverter}}">
<TextBox Text="{Binding FirstName, Mode=TwoWay}"/>
</DataFormToolkit:DataField>
<DataFormToolkit:DataField Label="Last name">
<TextBox Text="{Binding LastName, Mode=TwoWay}"/>
</DataFormToolkit:DataField>
<DataFormToolkit:DataField Label="Font size">
<TextBox
Text="{Binding ElementName=DataHolder, Path=Data.FontSize}"
FontSize="{Binding ElementName=DataHolder, Path=Data.FontSize}">
</TextBox>
</DataFormToolkit:DataField>
<DataFormToolkit:DataField Label="City">
<ComboBox
DisplayMemberPath="Name"
ItemsSource="{Binding ElementName=DataHolder, Path=Data.Cities}">
</ComboBox>
</DataFormToolkit:DataField>
</StackPanel>
</DataTemplate>
</DataFormToolkit:DataForm.EditTemplate>
</DataFormToolkit:DataForm>
INotifyPropertyChanged vs. DependencyProperty in a ViewModel
Should you use INotifyPropertyChanged or DependencyProperties in your ViewModel? If you want a strict separation between UI and logic, you should use INotifyPropertyChanged. This will give you the ability to have getter/setter logic for each property. Setters can also be made private to disable TwoWay binding.
public class ViewModelBase : INotifyPropertyChanged
{
protected void RaisePropertyChanged<TViewModel>(Expression<Func<TViewModel>> property)
{
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class ViewModel : ViewModelBase
{
private int fontSize;
public int FontSize
{
get
{
return fontSize;
}
set
{
fontSize = value;
RaisePropertyChanged(() => FontSize);
}
}
}
DependencyProperties can’t be serialized and they have a small performance overhead. On the other hand, dependency properties offer robust features such as advanced data binding, animation, styles, … They also support change events and default values. Updating a DependencyProperty is reflected immediately in all bounded controls.
public class ViewModel : DependencyBase
{
public int FontSize
{
get { return (int)GetValue(FontSizeProperty); }
set { SetValue(FontSizeProperty, value); }
}
public static readonly DependencyProperty FontSizeProperty =
DependencyProperty.Register("FontSize", typeof(int), typeof(ViewModel),
new PropertyMetadata(10));
}
Personally I prefer the INotifyPropertyChanged interface but I use a pragmatic approach. My ViewModelBase class inherits from DependencyObject so it can support DependencyProperties, it implements the INotifyPropertyChanged interface and it provides a RaisePropertyChanged method that uses a lambda to pass the property name. Default all properties have setters and getters and call the RaisePropertyChanged method. Only for exceptional cases I use a DependencyProperty.
public class ViewModelBase : DependencyBase, INotifyPropertyChanged
{
protected void RaisePropertyChanged<TViewModel>(Expression<Func<TViewModel>> property)
{
var expression = property.Body as MemberExpression;
var member = expression.Member;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(member.Name));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
I hope that this article provides some useful information to help you set up bindings to ViewModels. Besides these problems I have described, I’m still struggling with multiple select in ListBoxes and DataGrids, with the limited features of ValueConverters and with ComboBoxes that hold lookup entities. It seems that several Silverlight controls were not implemented with advanced databinding in mind. Hopefully Microsoft will improve the binding features of Silverlight in the next version so it will become easier to implement the M-V-VM pattern. For now Colin Eberhardt’s BindingHelper is a great addition. If you have good solutions for multiple select or binding ComboBoxes, please send me an email. And if you have any other remarks or suggestions, please let me know.