Search code examples
datagriddatatemplatesilverlight-5.0controltemplatedatatrigger

In Silverlight 5, how do I bind each DataGrid's row background color to one of its bound item's properties?


Please note that this question is specific to Silverlight 5. There are lots of similar questions floating around, but often they are about WPF or previous Silverlight versions. Others target Silverlight 5, but are about controls other than DataGrid. I have been unable to find an existing question that asks for exactly the same thing as this one; at least none with an answer that works for me.

Let's assume I have this really simple DataGrid (XAML and C# code further down), which is bound to a collection of a INotifyPropertyChanged type having two properties, Selected and Text:

DataGrid screenshot

I would like to bind each row's background color to its data item's Selected property (using some bool-to-Brush converter). The usual row states' (normal, selected, mouse over, etc.) visual effects and transitions should not be affected; that is, normal rows will be colored, alternate rows will be a little lighter, mouse-over-ed rows will be slightly darker, selected rows will be even darker than that, etc.

Some apparent solutions seen elsewhere:

I have searched for a solution for hours, and there are many similar questions floating around, but none of the answers seem to apply to Silverlight 5, or they don't work. There appear to be several approaches to this:

  1. Override the DataGridRow control template and bind its BackgroundRectangle's Background property:

    <sdk:DataGrid …>
      <sdk:DataGrid.RowStyle>
        <Style TargetType="sdk:DataGridRow">
          <Setter Property="Template">
            <Setter.Value>
              <ControlTemplate>
                …
                <Rectangle x:Name="BackgroundRectangle" …
                           Background="{Binding Selected, Converter={StaticResource boolToBrush}}" />
                …
              </ControlTemplate>
            </Setter.Value>
          </Setter>
        </Style>
      </sdk:DataGrid.RowStyle>
      …
    </sdk:DataGrid>
    

    This works initially, and visual states are correctly rendered (because the DataGrid visual states only affect opacity of the BackgroundRectangle; see the default control template definition for details).

    The problem with this is that row background colors won't change when the bound items' Selected properties change value. It's as if the {Binding} had a Mode=OneTime.

    Also, I've read that data bindings in control templates should be avoided. TemplateBindings are fine in a control template, but regular Bindings perhaps don't belong here.

  2. Change all columns to <sdk:DataGridTemplateColumn> and use a data template:

    <sdk:DataGrid …>
      <sdk:DataGrid.Columns>
        <sdk:DataGridTemplateColumn Header="Selected">
          <sdk:DataGridTemplateColumn.CellTemplate>
            <DataTemplate>
              <Grid Background="{Binding Selected, Converter={StaticResource boolToBrush}}">
                <CheckBox IsChecked="{Binding Selected}" />
              </Grid>
            </DataTemplate>
          </sdk:DataGridTemplateColumn.CellTemplate>
          … <!-- repeat the above for each column -->
        </sdk:DataGridTemplateColumn>
      </sdk:DataGrid.Columns>
      …
    </sdk:DataGrid>
    

    This is even worse. The cells get the right background colors, but the data grid's various visual states are no longer properly applied. Also, each column has to be turned into a template column. And third, when the bound properties change value, the background colors are not updated.

  3. Use Expression Blend data triggers (types from the System.Windows.Interactivity and Microsoft.Expression.Interactivity.Core namespaces). To be honest, I haven't been able to work out how this is supposed to work at all.

Question:

I really don't want to resort to imperative code-behind where I do this manually. But how can I do what I want in XAML, in a proper way that also honors the DataGridRow visual states and behavior, and updates the row background when the bound property changes?

Could someone give me a XAML example how to bind each row's background color to one of its bound item's properties in such a way?

XAML and C# code for the above data grid:

<UserControl x:Class="SomeApplication.MainView"
  …
  xmlns:local="clr-namespace:SomeApplication"
  xmlns:sdk="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data">

  <!-- sample data context -->
  <UserControl.DataContext>
    <local:MainViewModel>
      <local:MainViewModel.Things>
        <local:ThingViewModel Text="A" />
        <local:ThingViewModel Text="B" />
        <local:ThingViewModel Text="C" />
        <local:ThingViewModel Text="D" Selected="True" />
        <local:ThingViewModel Text="E" />
      </local:MainViewModel.Things>
    </local:MainViewModel>
  </UserControl.DataContext>

  <Grid>
    <sdk:DataGrid ItemsSource="{Binding Things}" AutoGenerateColumns="False">
      <sdk:DataGrid.Columns>
        <sdk:DataGridCheckBoxColumn Header="Selected" Binding="{Binding Selected}" />
        <sdk:DataGridTextColumn Header="Text" Binding="{Binding Text}" />
      </sdk:DataGrid.Columns>
    </sdk:DataGrid>
  </Grid>
</UserControl>

namespace SomeApplication
{
    public interface IMainViewModel
    {
        ObservableCollection<ThingViewModel> Things { get; }
    }

    public interface IThingViewModel : INotifyPropertyChanged
    {
        bool Selected { get; set; }
        string Text { get; set; }
    }

    // straightforward implementations, omitted for brevity's sake:
    public partial class MainViewModel : IMainViewModel { }
    public partial class ThingViewModel : IThingViewModel { }
}


Solution

  • One thing that appears to work is a combination of overriding the control template and putting a Expression Blend data trigger inside the BackgroundRectangle.

    Start with the default control template for DataGridRow (which you can find on the MSDN page 'DataGrid Styles and Templates'). The only thing that needs to change is the <Rectangle x:Name="BackgroundRectangle">:

    <!-- 
      xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
      xmlns:ei="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expressions.Interactions" 
    -->
    
    <sdk:DataGrid …>
      <sdk:DataGrid.RowStyle>
        <Style TargetType="sdk:DataGridRow">
          <Setter Property="Template">
            <Setter.Value>
              <ControlTemplate>
                … <!-- use DataGridRow standard control template, except for this: -->
                <Rectangle x:Name="BackgroundRectangle" …
                           Background="{Binding Selected, Converter={StaticResource boolToBrush}}">
                  <i:Interaction.DataTriggers>
                    <ei:PropertyChangedTrigger Binding="{Binding Selected}" Value="True">
                      <ei:PropertyChangedTrigger.Actions>
                        <ei:ChangePropertyAction TargetName="BackgroundRectangle"
                                                 PropertyName="Fill" 
                                                 Value="{Binding Selected, Converter={StaticResource boolToBrush}}" />
                      </ei:PropertyChangedTrigger.Actions>
                    </ei:PropertyChangedTrigger>
                  </i:Interaction.DataTriggers>
                <Rectangle>
                …
              </ControlTemplate>
            </Setter.Value>
          </Setter>
        </Style>
      </sdk:DataGrid.RowStyle>
      …
    </sdk:DataGrid>
    

    Since a row's background rectangle is inside the control template, and that background should change with the data, it seems that it's actually unavoidable to put a data binding inside the control template.