Wednesday, June 10, 2009

Adventures while building a Silverlight Enterprise application part #12

In this episode we have a closer look at the behavior of the Silverlight 2 DataGrid and especially when databinding and in relation to selecting items.

One of the UI elements that we have in our application is that whenever you select an item in DataGrid A, this updates DataGrid B. Not a very difficult peace of code on itself. However, if you need to select an item from DataGrid B right after this update, that's where trouble begins.
Debugging my source I realized that it had to do with the timing between setting the ItemsSource property to supply new data, and setting the SelectedItem property to select the correct item.
DataGrid A is only a UI element that happens to trigger the refreshing of the data, so it's not important in our problem.

With this information I wrote a small testapplication that demonstrates the problem I had.
The XAML looks like this:

<UserControl xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data" x:Class="DataGridSelection.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300" Loaded="UserControl_Loaded">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="25"/>
</Grid.RowDefinitions>
<data:DataGrid x:Name="dataGrid"
SelectionChanged="dataGrid_SelectionChanged"
ItemsSource="{Binding}"
LoadingRow="dataGrid_LoadingRow"/>
<Button x:Name="updateItemsSourceButton"
Content="Update ItemsSource"
Click="updateItemsSourceButton_Click"
Grid.Row="1"/>
</Grid>
</UserControl>

As you can see I have a single DataGrid to place data in. I subscribed to the SelectionChanged and LoadingRow events so I can demonstrate what happens.
I also use a Button that allows me to refresh the data easily and finally I have a Loaded event to handle the initial data creation.

Next is the code behind:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading;

namespace DataGridSelection
{
public partial class Page : UserControl
{
private ObservableCollection<string> _data;
private bool _loadingRow;

public Page()
{
InitializeComponent();
RecreateData();
}

private void RecreateData()
{
_data = new ObservableCollection<string>();
_data.Add("Item 1");
_data.Add("Item 2");
}

private void dataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Debug.WriteLine("Selection changed to index {0}", dataGrid.SelectedIndex);
if (_loadingRow)
{
Debug.WriteLine("Changed because of loading row");
}
_loadingRow = false;
}

private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
dataGrid.ItemsSource = _data;
}

private void updateItemsSourceButton_Click(object sender, RoutedEventArgs e)
{
int selectedIndex = dataGrid.SelectedIndex;
RecreateData();

dataGrid.ItemsSource = _data;
dataGrid.SelectedIndex = selectedIndex;
Debug.WriteLine("Exiting click event");
}

private void dataGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
Debug.WriteLine("Loading row");
_loadingRow = true;
}
}
}


So for test data I've simply used an ObservableCollection of strings, to which I add two items in the RecreateData method. This allows me to reinstantiate the collection at any given moment.

The problem code is in the Buttons Click event. I first retrieve the SelectedItem index, so I can later try and restore the selection. I then reinstantiate the data collection and reasign the data to the ItemsSource property of the DataGrid. After that I set the SelectedIndex property to restore the original selection. It seems like this should work, right? Wrong!

If you would test this, you'd find out that after reloading the data the first item is always selected. It doesn't matter what value I put in SelectedIndex. I could do exactly the same thing with SelectedItem and it would have the same result.
To make it more clear, as to what happens when, I added the debug output to the source, ran the application, selected the second item (SelectedIndex = 1) and pressed the button.

Here is the debug output:

Loading row
Loading row
Selection changed to index 0
Changed because of loading row
Selection changed to index 1
Selection changed to index 1
Exiting click event
Loading row
Loading row
Selection changed to index 0
Changed because of loading row

Now, the first four lines are from starting the application. The fifth line is from me, selecting the second item in the grid by clicking it. The two lines after that are from the last two lines of code of the click event, first setting the SelectedIndex property and then exiting the event. As you can see, the actual loading of the data happens after we exited the click event. As a result the selection is reset after that as well.

To find out more about this, and maybe find a way to get around this, I fired up Reflector and started digging around in the DataGrid's code. I found out that it uses the SetValue method of it's base class (DependencyObject), but I couldn't exactly find out why the data gets loaded after the user code is executed, nor did I find any indication as to how I could get around this behavior. My best guess so far is that at some point the control of loading the data is passed trough a Dispatcher instance and gets transfered out the main thread (which makes sense as you don't want your user to wait for this process).

I did find a solution direction in my test application however. The _loadingRow field actually gave me some inspiration as to how to solve this in my application.

I use two methods and two fields for this solution. To update the _loadingRow field I wrote a method SetLoadingRow so I have some point where I can handle changes in the field.
I also wrote another method called SelectItem, backed by a field to store the selected item, independent of the DataGrid. In the SelectItem method optionally I also set the SelectedItem property of the DataGrid.
In the SetLoadingRow method I only update the SelectedItem of the DataGrid if I was loading rows, but I'm now done (so the new value passed is false, but the current value is true) and if I have a selected item already and it's different from the selected item in the DataGrid.

SelectItem is called whenever I need to select an item from code. SetLoadingRow(true) is called whenever the LoadingRow event of the DataGrid is triggered and SetLoadingRow(false) is called at the end of the SelectionChanged event of the DataGrid.

This workes like a charm (altough I had some very specific other issues concerning state between different parts in my application, but I won't bother you with that :-) ).
I hope this can be helpful to you as well, even if only to know your not the only one struggling with this tricky side effect. If you have any questions or comments, please leave them below. I always look forward to them.

2 comments: