Month: July 2010

Silverlight Grid Row Gradients

I just had to implement a design where a table of data, in Silverlight, had to have cell backgrounds where each cell is slightly darker than the cell above. Here it is in situ:

As you can see, not only does each row have a descending gradient, but each column has a different gradient.

The easy way

The simple way is to just put in a border or background rectangle into each cell. This is what I did originally. But it’s a lot of work no matter how you do it. Neither Blend nor Visual Studio lend themselves to this kind of thing, and even hand-editing the Xaml can lead to cut&paste errors.

The even easier way

I decided to have some kind of automated way to do this. I needed something that you could call that would put in the right rectangles in the right places with the right colours but was easy to manage, so I wrote a behavior. If you’re unfamiliar with Silverlight Behaviors (apologies for the American spelling, but that’s what they’re called) then read this post by Christian Schormann from the Blend team.

I realised that all I needed was something where I can specify which column I’m interested in, and what the start and end colours are. So this is the code I came up with:


/// <summary>
 /// A behaviour which targets a Grid to put background rectangles
 /// into column cells creating a gradient.
 /// </summary>
 public class GridGradientBehavior : Behavior<Grid>
 {
   /// <summary>
   /// Somewhere to keep track of the items we've added to the grid
   /// </summary>
   List<Rectangle> rectangles = new List<Rectangle>();

   /// <summary>
   /// Called when this behaviour is attached to the grid.
   /// </summary>
   protected override void OnAttached()
   {
     base.OnAttached();

     /// Just in case, clear any rectangles we've already put in
     foreach (var rect in rectangles)
       {
         AssociatedObject.Children.Remove(rect);
       }
       rectangles.Clear();

       // Check the RowDefinitions. If there aren't any, just put in a single rectangle
       if (AssociatedObject.RowDefinitions == null
           || AssociatedObject.RowDefinitions.Count <= 1)
       {
         InsertRectangle(new SolidColorBrush(StartColour), 0);
       }
       else
       {
         // We need to interpolate between the two colours. I make use
         // of the SolidColorBrushInterpolator from System.Windows.Controls.DataVisualization
         // since my project already uses the toolkit.
         var interpolator = new SolidColorBrushInterpolator();
         interpolator.From = StartColour;
         interpolator.To = EndColour;
         interpolator.DataMinimum = 0;
         interpolator.DataMaximum = AssociatedObject.RowDefinitions.Count - 1;
         for (int i = 0; i < AssociatedObject.RowDefinitions.Count; i++)
         {
           InsertRectangle((Brush)interpolator.Interpolate(i),i);
         }
       }
     }

     /// <summary>
     /// For each grid row, insert a rectangle of the appropriate colour into the ]
     /// appropriate row and column slot.
     /// </summary>
     /// <param name="color">The brush to use, interpolated between Start and End</param>
     /// <param name="i"></param>
     private void InsertRectangle(Brush color, int i)
     {
       Rectangle rect = new Rectangle();
       rect.HorizontalAlignment = HorizontalAlignment.Stretch;
       rect.VerticalAlignment = VerticalAlignment.Stretch;
       rect.Fill = color;
       Grid.SetRow(rect, i);
       Grid.SetColumn(rect, Column);

       // This should ensure these rectangles are behind everything else in the grid
       Canvas.SetZIndex(rect, -1);

       // and if it doesn't, we insert our rectangles at the lowest point
       // in the visual tree
       AssociatedObject.Children.Insert(0, rect);
       rectangles.Add(rect);
     }

     /// <summary>
     /// Called when the behavior detaches. Remove all our rectangles from
     /// the grid.
     /// </summary>
     protected override void OnDetaching()
     {
       base.OnDetaching();
       foreach (var rect in rectangles)
       {
         AssociatedObject.Children.Remove(rect);
       }
       rectangles.Clear();
     }

     /// <summary>
     /// Dependency property telling us which column to apply this behaviour to
     /// </summary>
     public int Column
     {
       get { return (int)GetValue(ColumnProperty); }
       set { SetValue(ColumnProperty, value); }
     }
     public static readonly DependencyProperty ColumnProperty =
       DependencyProperty.Register("Column", typeof(int), typeof(GridGradientBehavior),
         new PropertyMetadata(0));

     /// <summary>
     /// Dependency property for the start colour of our gradient
     /// </summary>
     public Color StartColour
     {
       get { return (Color)GetValue(StartColourProperty); }
       set { SetValue(StartColourProperty, value); }
     }
     public static readonly DependencyProperty StartColourProperty =
       DependencyProperty.Register("StartColour", typeof(Color), typeof(GridGradientBehavior),
         new PropertyMetadata(Colors.White));

     /// <summary>
     /// Dependency property for the end colour of our gradient
     /// </summary>
     public Color EndColour
     {
       get { return (Color)GetValue(EndColourProperty); }
       set { SetValue(EndColourProperty, value); }
     }
     public static readonly DependencyProperty EndColourProperty =
       DependencyProperty.Register("EndColour", typeof(Color), typeof(GridGradientBehavior),
         new PropertyMetadata(Colors.Black));

   }

The easiest way to apply this to a grid is to drag and drop it from the Behaviors section of the Assets pane. Then set the three parameters (StartColour, EndColour and Column) as appropriate. If (as in my example) you have two columns you want this to affect, you ust have to drop a second behavior onto the same grid.

There are a million ways you might want to extend this – letting you style rows instead of columns, or changing the rectangle to a border and having the ability to define different borders for top and bottom for example. Or you could use this to do alternating row colours. There’s quite a lot of scope for such a simple idea.

What I really want, though, is a way to get the same effect in an ItemsControl, but that’s a whole different problem.

Feel free to use this if it’s helpful.