I’ve been playing around with the Silverlight 3 DataGrid and Silverlight Themes in an attempt to come up with a nice looking user interface. I’ve got to admit that I’m not the best graphic designer in the world so I’ve mostly been working with the out-of-the-box themes like ExpressionDark, BureauBlack, and ShinyBlue.
A couple of weeks ago, I ran into an interesting problem where rows in my DataGrid appeared to be missing. After some debugging, I found a bug in the Silverlight DataGrid where applying a Theme after setting the DataContext causes the current grid rows to become invisible.
Here are some pictures. This is what I expected my user interface to look like after applying the ExpressionDark theme:
Here’s what it actually looked like:
Notice that the grid appears to be empty in the second picture. I assure you that there are actually rows in the grid. What’s more interesting is that if I add more rows, they show up just fine but there are 3 blank rows in the grid.
See how there are those 3 “empty” rows and then 2 rows where the project name is a guid? Weird, huh? You can even try setting the DataContext on the DataGrid to null and then setting it to a brand new DataContext object instance and STILL you get those 3 “blank” rows. It seems that once the rows go blank, you have to get a new instance of a DataGrid to make them come back to visible again.
So. What’s going on here? Well, after some hunting around in Reflector, the DataGrid uses a class called DataGridRowPresenter. DataGridRowPresenter takes care of a lot of the logic of displaying your data in the grid and as part of this process, it re-uses instances of DataGridRow. What does this mean exactly? Well, re-using the DataGridRow instances means that even when your DataContext changes, the DataGrid instance never re-creates new rows.
As you’ve probably figured out by now, there’s a bug in the DataGrid. The bug is that DataGrid doesn’t handle Silverlight Themes all that well at the moment. Granted, it’s a lot better than Silverlight 2 but there are still a few issues. This issue is caused by applying a Theme to the DataGrid after you’ve set the DataContext to something.
Assuming that your DataContext object has some rows in it, it will go something like this:
1. An instance of your Silverlight control gets constructed 2. You set your DataContext to a value 3. The DataGrid is bound to the DataContext and populates it’s rows accordingly. 4. Some time after step 3, the Silverlight Theme gets applied. 5. (Here’s the bug) When the DataGrid gets notified about the Theme changing, it sets the visibility of all it’s current DataGridRow instances to invisible (collapsed).
From step #5 on, any existing DataGridRows get re-used and their visibility never gets set to ‘Visible’ again. If you create new rows (aka. row instances that aren’t being cached by the DataGridRowPresenter), they get the theme and the visibility applied correctly.
The Solution Workaround
Here’s the workaround. You’ve got to set the DataContext for your DataGrid AFTER the theme has been applied. For this, you need some kind of timer that fires the assignment to DataContext asynchronously. This is where the DispatchTimer class in System.Windows.Threading comes in handy.
Here’s the code that I put in my TimesheetUserControl.xaml.cs.
private System.Windows.Threading.DispatcherTimer m_timer = new System.Windows.Threading.DispatcherTimer();
public TimesheetUserControl() : this(true)
public TimesheetUserControl(bool withDelayedDataContextPopulate) { InitializeComponent(); this.LayoutUpdated += OnLayoutUpdated;
if (withDelayedDataContextPopulate == true) { RequestDelayedPopulateDataContext(); } else { PopulateDataContext(); } }
///
/// This gets around a bug in DataGrid. If DataGrid has rows when a theme is applied, /// then the existing row objects are marked as invisible. After the rows are marked as /// invisible, you'll always have blank rows in your DataGrid no matter what datasource you set. /// /// This method sets the data context 1 second after the form is started. /// private void RequestDelayedPopulateDataContext() { m_timer.Interval = new TimeSpan(0, 0, 0, 1); m_timer.Tick += new EventHandler(m_timer_Tick); m_timer.Start(); }void m_timer_Tick(object sender, EventArgs e) { m_timer.Stop();
PopulateDataContext(); }
private void PopulateDataContext() { TimesheetEditorViewModel model = new TimesheetEditorViewModel(); PopulateWithTestData(model); this.DataContext = model; } private void OnLayoutUpdated(object sender, EventArgs e) { LayoutUpdated -= OnLayoutUpdated; ImplicitStyleManager.SetApplyMode(LayoutRoot, ImplicitStylesApplyMode.Auto); ImplicitStyleManager.Apply(LayoutRoot); // redundant call to workaround datagrid style issue) }
In your control, create a member variable instance of DispatchTimer. In the code above, I call RequestDelayedPopulateDataContext() to configure the DispatchTimer. I use the DispatchTimer’s Tick event to fire off a call to PopulateDataContext() 1 second after the constructor for my control fires. This 1 second delay gives the control time to apply the theme and since the DataContext for the grid hasn’t been set, it means the grid hasn’t created any rows. No rows yet means that you don’t have any row visibility problems when the theme gets applied to the grid.
Click here to download the sample code.
The interesting code is located in TimesheetUserControl.xaml.cs in the Benday.Timesheet.SilverlightUI project.
-Ben