Alternating Row Styles in Silverlight

Update: There was an issue when using ItemsControl because of the way I was determining if one control was in another (I was using the DataContext.) This example code and attached ZIP have been updated.

Because the ItemsControl in Silverlight 4 doesn’t support alternating styles on the child items, I’ve made this set of attached properties to allow you to switch the style on any item contained within an ItemsControl or anything that inherits from it (e.g. ListBox.)

I’ve uploaded a sample project, but here’s the entirety of the code for those who don’t want to download anything.

SilverlightApplication1.zip

Here’s a sample of it’s usage:

<UserControl x:Class="SilverlightApplication1.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:SilverlightApplication1"
    mc:Ignorable="d" Loaded="UserControl_Loaded"
    d:DesignHeight="300" d:DesignWidth="400">

    <Grid x:Name="LayoutRoot" Background="White">
        <ListBox x:Name="ListItems">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border>
                        <Border.Style>
                            <Style TargetType="Border">
                                <Setter Property="Background" Value="White" />
                            </Style>
                        </Border.Style>
                        <local:ItemsControlAlternation.AlternateStyle>
                            <Style TargetType="Border">
                                <Setter Property="Background" Value="LightBlue" />
                            </Style>
                        </local:ItemsControlAlternation.AlternateStyle>
                        <ContentPresenter Content="{Binding}" />
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
    
</UserControl>

And here’s the main part of the code, separated into two classes. One for the attached properties, and one that’s the new behavior. Also I’ve included two extension methods in this example, that aren’t specific to this code, but do make it cleaner.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace SilverlightApplication1
{
    public static class ItemsControlAlternation
    {
        public static readonly DependencyProperty AlternateStyleProperty =
            DependencyProperty.RegisterAttached("AlternateStyle", typeof(Style), typeof(ItemsControlAlternation), new PropertyMetadata(AlternateStylePropertySet));

        public static readonly DependencyProperty AlternationIndexProperty =
            DependencyProperty.RegisterAttached("AlternationIndex", typeof(int), typeof(ItemsControlAlternation), new PropertyMetadata(2, AlternationIndexPropertySet));

        private static readonly DependencyProperty AlternationHelperProperty =
            DependencyProperty.RegisterAttached("AlternationHelper", typeof(ItemsControlAlternationHelper), typeof(ItemsControlAlternation), new PropertyMetadata(null));

        public static Style GetAlternateStyle(DependencyObject obj)
        {
            return (Style)obj.GetValue(AlternateStyleProperty);
        }

        public static void SetAlternateStyle(DependencyObject obj, Style value)
        {
            obj.SetValue(AlternateStyleProperty, value);
        }

        public static int GetAlternationIndex(DependencyObject obj)
        {
            return (int)obj.GetValue(AlternationIndexProperty);
        }

        public static void SetAlternationIndex(DependencyObject obj, int value)
        {
            obj.SetValue(AlternationIndexProperty, value);
        }

        private static ItemsControlAlternationHelper GetAlternationHelper(DependencyObject obj)
        {
            return (ItemsControlAlternationHelper)obj.GetValue(AlternationHelperProperty);
        }

        private static void SetAlternationHelper(DependencyObject obj, ItemsControlAlternationHelper value)
        {
            obj.SetValue(AlternationHelperProperty, value);
        }

        private static void AlternateStylePropertySet(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ItemsControlAlternationHelper helper = GetOrCreateAlternationHelper(sender);

            helper.AlternateStyle = (Style)e.NewValue;
        }

        private static void AlternationIndexPropertySet(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ItemsControlAlternationHelper helper = GetOrCreateAlternationHelper(sender);

            helper.AlternationIndex = (int)e.NewValue;
        }

        private static ItemsControlAlternationHelper GetOrCreateAlternationHelper(DependencyObject target)
        {
            ItemsControlAlternationHelper helper = GetAlternationHelper(target);

            if (helper == null)
            {
                helper = new ItemsControlAlternationHelper(target as FrameworkElement);
                SetAlternationHelper(target, helper);
            }

            return helper;
        }
    }

    internal class ItemsControlAlternationHelper
    {
        private FrameworkElement targetItem;
        private Style originalStyle;
        private Style alternateStyle;
        private int alternationIndex;

        private ItemsControl parentCollection;
        private INotifyCollectionChanged parentCollectionItems;
        private FrameworkElement[] parentPath;

        public ItemsControlAlternationHelper(FrameworkElement targetItem)
        {
            if (targetItem == null)
            {
                throw new ArgumentNullException("targetItem", "targetItem is null.");
            }

            this.targetItem = targetItem;
            this.originalStyle = targetItem.Style;
            this.alternationIndex = 2;

            // Check for loaded in case the element hasn't been added to the visual tree yet.
            targetItem.Loaded += TargetItemLoaded;

            LocateParents();
        }

        /// <summary>
        /// The style to apply to the alternate items.
        /// </summary>
        public Style AlternateStyle
        {
            get { return alternateStyle; }
            set
            {
                alternateStyle = value;
                UpdateAlernationStyle();
            }
        }

        /// <summary>
        /// At what frequency to apply the alternate style. Defaults to 2, which applies the style to every second item.
        /// </summary>
        public int AlternationIndex
        {
            get { return alternationIndex; }
            set
            {
                if (value == 0)
                {
                    throw new ArgumentOutOfRangeException("value", "value must be non-zero.");
                }

                alternationIndex = value;
                UpdateAlernationStyle();
            }
        }

        /// <summary>
        /// Parses the visual tree of the target element to find the ItemsControl
        /// </summary>
        private void LocateParents()
        {
            if (parentCollectionItems != null)
            {
                parentCollectionItems.CollectionChanged -= ParentCollectionChanged;
            }

            DependencyObject[] parents = targetItem.GetVisualParents().ToArray();

            parentCollection = parents.OfType<ItemsControl>().FirstOrDefault();

            if (parentCollection == null)
            {
                return;
            }

            parentPath = parents.TakeWhile(x => x != parentCollection).Skip(1).OfType<FrameworkElement>().ToArray();
            parentCollectionItems = parentCollection.Items as INotifyCollectionChanged;

            if (parentCollectionItems != null)
            {
                parentCollectionItems.CollectionChanged += ParentCollectionChanged;
            }
        }

        private void TargetItemLoaded(object sender, RoutedEventArgs e)
        {
            LocateParents();
            UpdateAlernationStyle();
        }

        private void ParentCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            UpdateAlernationStyle();
        }

        /// <summary>
        /// Update the Style of the target element.
        /// </summary>
        private void UpdateAlernationStyle()
        {
            if (IsAlternate())
            {
                targetItem.Style = AlternateStyle;
            }
            else
            {
                targetItem.Style = originalStyle;
            }
        }

        /// <summary>
        /// Checks to see if the item is at the alternate index
        /// </summary>
        private bool IsAlternate()
        {
            if (parentCollection == null)
            {
                return false;
            }

            Panel panel = parentCollection.GetVisualChildrenRecursive().OfType<Panel>().FirstOrDefault();

            if (panel == null)
                return false;

            int itemIndex = panel.Children.IndexOf(i => i.GetVisualChildrenRecursive().Contains(targetItem));

            return (itemIndex + 1) % AlternationIndex == 0;
        }

    }

    public static class Extensions
    {
        /// <summary>
        /// Walks the visual tree to find all of an items parents.
        /// </summary>
        public static IEnumerable<DependencyObject> GetVisualParents(this DependencyObject source)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source", "source is null.");
            }

            DependencyObject parent = VisualTreeHelper.GetParent(source);

            while (parent != null)
            {
                yield return parent;

                parent = VisualTreeHelper.GetParent(parent);
            }
        }

        /// <summary>
        /// Returns the first index of an item in the collection that matches the given predicate.
        /// </summary>
        public static int IndexOf<T>(this IEnumerable<T> source, Func<T, bool> predicate)
        {
            return source
                .Select((item, index) => new { item, index })
                .Where(i => predicate(i.item))
                .Select(i => i.index + 1)
                .FirstOrDefault() - 1;
        }

        /// <summary>
        /// Returns all the direct visual children of an object
        /// </summary>
        public static IEnumerable<DependencyObject> GetVisualChildren(this DependencyObject source)
        {
            return Enumerable.Range(0, VisualTreeHelper.GetChildrenCount(source))
                .Select(i => VisualTreeHelper.GetChild(source, i));
        }

        /// <summary>
        /// Returns the children of a dependancy object using a breadth first algorithm
        /// </summary>
        public static IEnumerable<DependencyObject> GetVisualChildrenRecursive(this DependencyObject source)
        {
            DependencyObject[] children = source.GetVisualChildren()
                .ToArray();

            while (children.Any())
            {
                foreach (var item in children)
                {
                    yield return item;
                }

                children = children.SelectMany(c => c.GetVisualChildren())
                    .ToArray();
            }
        }
    }
}

I’m not particularly happy with how it uses DataContext to determine if it’s a sub-item, so any suggestions on how to improve it would be appreciated 🙂

Phil

10 Responses to “ “Alternating Row Styles in Silverlight”

  1. Asgher says:

    This is exactly what I need.

    You say that this would work for ItemControls or any inherited controls such as ListBox. It works fine for a listbox, but I cannot get it to work with a simple ItemsControl.

  2. Surge001 says:

    Man, you are good! Very good code, quite useful. I actually learned something today from you. Thank you.

  3. geelius says:

    Nice code. Works great with a full-blown Listbox, but not with a lighter-weight ItemsControl as Asgher mentioned. Any tips on how to modify this to work with both? I’m an old-school procedural kind of guy – your functional coding style has me a little confused 😉

    Thanks

  4. Phil says:

    Hey Surge001 and Asgher,

    Looks like you where right, there was an issue with the code. The problem was that I was checking to DataContext to find the the childs index, and DataContext doesn’t seem to get set for ItemsControl, which is news to me!

    I’ve updated the example code as well as the attached zip, hopefully that’ll work better for you.

    Phil

  5. geelius says:

    Phil

    Thanks for taking the time to modify so that it works with a plain old ItemsControl.

    Works great and currently the best solution IMO to a surprisingly tricky problem!

    Thanks!

  6. Shannon says:

    Is it just me or does the updated version no longer work with ListBox? ie. the ListBox is displayed but the row items are not alternating. It still seems to work with the base ItemsControl.

    • Shannon says:

      OK, so I found out where the problem was and got this working with a ListBox.
      Replace:
      Panel panel = parentCollection.GetVisualChildrenRecursive().OfType().FirstOrDefault();

      With
      Panel panel = parentCollection.GetVisualChildrenRecursive().OfType().FirstOrDefault();

      However there is still one problem. Whenever you scroll through the list box the alternate rows get messed up. Seems that the list is refreshed with the scroll so the indexing gets mixed up.

      • Shannon says:

        sorry that didn’t come out right the angle brackets got wiped. Basically you need to replace OfType Panel with OfType VirtualizingStackPanel.

      • Shannon says:

        solution to the scrolling problem is to swap out the VitualizingStackPanel with a regular StackPanel as suggested here:

        *this apparently has a performance hit though.

        • Joseph says:

          When replacing the OfType() with StackPanel, all the rows get the alternative style… VirtualizingStackPanel still messes up when you scroll though. Did you get this to work?