Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An article on sorting ItemsControl (with some bonuses)

0.00/5 (No votes)
29 May 2008 1  
An article on sorting ItemsControl (with some bonuses).

Introduction

When I started out this app, I wanted to create a simple application that demonstrated how to do sorting within the ItemsControl in WPF. I have to say that my mind has gone a bit mad on this one, and I've kind of created a bit more (not a lot more, but a bit more) than I originally set out to do. What we have in this article is the following:

  • How to create a scrollable design area
  • Designing a custom panel
  • How to use Adorners in a neat way
  • How to sort an ItemsControl using a CollectionView

I will explain each of these in more detail along the way.

Table of contents

A video showcasing the attached demo

Since I found out how to make videos and my hosting isn't that bad, I just think it's nice to add a video if it makes sense to the article. So this one also has a video:

Click the image or here to view the video.

I would suggest waiting for the entire video to finish streaming then watch it. It will make most sense that way.

Architectural overview

The overall class diagram is as follows:

I shall not explain each of these classes, but rather shall be highlighting the points of interest. I think the main bullet points listed above in the table of contents are worth covering, so I will be spending a little bit of time on those, but other than that, just dip into the code if you are interested.

Creating a scrollable design surface (With Friction)

Have you ever had a requirement that called for the user to be able to scroll around a large object, such as a diagram? Well, this is exactly the requirement I had for this article. I wanted to create a custom Panel where I didn't know how big it would be, so I wanted the user to be able to scroll around using the mouse.

We probably all know that WPF has a ScrollViewer control which allows users to scroll using scrollbars, which is fine, but it just looks ugly. What I wanted was for the user to not really ever realize that there is a scroll area; I want them to just use the mouse to pan around the large area.

To this end, I set about looking around, and I have pieced together the following subclass of the standard ScrollViewer control:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace ScrollControl
{
    /// <summary>
    /// Provides a scrollable ScrollViewer which
    /// allows user to apply friction, which in turn
    /// animates the ScrollViewer position, giving it
    /// the appearance of sliding into position
    /// </summary>
    public class FrictionScrollViewer : ScrollViewer
    {
        #region Data

        // Used when manually scrolling.
        private DispatcherTimer animationTimer = new DispatcherTimer();
        private Point previousPoint;
        private Point scrollStartOffset;
        private Point scrollStartPoint;
        private Point scrollTarget;
        private Vector velocity;

        #endregion

        #region Ctor
        /// <summary>
        /// Overrides metadata
        /// </summary>
        static FrictionScrollViewer()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
            typeof(FrictionScrollViewer),
            new FrameworkPropertyMetadata(typeof(FrictionScrollViewer)));
        }

        /// <summary>
        /// Initialises all friction related variables
        /// </summary>
        public FrictionScrollViewer()
        {
            Friction = 0.95;
            animationTimer.Interval = new TimeSpan(0, 0, 0, 0, 20);
            animationTimer.Tick += HandleWorldTimerTick;
            animationTimer.Start();
        }
        #endregion

        #region DPs
        /// <summary>
        /// The ammount of friction to use. Use the Friction property to set a 
        /// value between 0 and 1, 0 being no friction 1 is full friction 
        /// meaning the panel won’t "auto-scroll".
        /// </summary>
        public double Friction
        {
            get { return (double)GetValue(FrictionProperty); }
            set { SetValue(FrictionProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Friction.
        public static readonly DependencyProperty FrictionProperty =
            DependencyProperty.Register("Friction", typeof(double), 
            typeof(FrictionScrollViewer), new UIPropertyMetadata(0.0));
        #endregion

        #region overrides 
        /// <summary>
        /// Get position and CaptureMouse
        /// </summary>
        /// <param name="e"></param>
        protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
        {
            if (IsMouseOver)
            {
                // Save starting point, used later when determining how much to scroll.
                scrollStartPoint = e.GetPosition(this);
                scrollStartOffset.X = HorizontalOffset;
                scrollStartOffset.Y = VerticalOffset;
                // Update the cursor if can scroll or not. 
                Cursor = (ExtentWidth > ViewportWidth) || 
                    (ExtentHeight > ViewportHeight) ? 
                    Cursors.ScrollAll : Cursors.Arrow;
                CaptureMouse();
            }
            base.OnPreviewMouseDown(e);
        }

        /// <summary>
        /// If IsMouseCaptured scroll to correct position. 
        /// Where position is updated by animation timer
        /// </summary>
        protected override void OnPreviewMouseMove(MouseEventArgs e)
        {
            if (IsMouseCaptured)
            {
                Point currentPoint = e.GetPosition(this);
                // Determine the new amount to scroll.
                Point delta = new Point(scrollStartPoint.X - 
                    currentPoint.X, scrollStartPoint.Y - currentPoint.Y);
                scrollTarget.X = scrollStartOffset.X + delta.X;
                scrollTarget.Y = scrollStartOffset.Y + delta.Y;
                // Scroll to the new position.
                ScrollToHorizontalOffset(scrollTarget.X);
                ScrollToVerticalOffset(scrollTarget.Y);
            }
            base.OnPreviewMouseMove(e);
        }

        /// <summary>
        /// Release MouseCapture if its captured
        /// </summary>
        /// <param name="e"></param>
        protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
        {
            if (IsMouseCaptured)
            {
                Cursor = Cursors.Arrow;
                ReleaseMouseCapture();
            }
            base.OnPreviewMouseUp(e);
        }
        #endregion

        #region Animation timer Tick
        /// <summary>
        /// Animation timer tick, used to move the scrollviewer incrementally
        /// to the desired position. This also uses the friction setting
        /// when determining how much to move the scrollviewer
        /// </summary>
        private void HandleWorldTimerTick(object sender, EventArgs e)
        {
            if (IsMouseCaptured)
            {
                Point currentPoint = Mouse.GetPosition(this);
                velocity = previousPoint - currentPoint;
                previousPoint = currentPoint;
            }
            else
            {
                if (velocity.Length > 1)
                {
                    ScrollToHorizontalOffset(scrollTarget.X);
                    ScrollToVerticalOffset(scrollTarget.Y);
                    scrollTarget.X += velocity.X;
                    scrollTarget.Y += velocity.Y;
                    velocity *= Friction;
                }
            }
        }
        #endregion
    }
}

Which I can use in XAML as follows:

<UserControl.Resources>

    <!-- scroll viewer -->
    <Style x:Key="ScrollViewerStyle" TargetType="{x:Type ScrollViewer}">
        <Setter Property="HorizontalScrollBarVisibility" Value="Hidden" />
        <Setter Property="VerticalScrollBarVisibility" Value="Hidden" />
    </Style>

</UserControl.Resources>
....
....

<!-- Content Start-->
<local:FrictionScrollViewer x:Name="ScrollViewer" 
          Style="{StaticResource ScrollViewerStyle}">
    <ItemsControl x:Name="itemsControl" 
          Style="{StaticResource mainPanelStyle}">
    ....
    ....
    </ItemsControl>

</local:FrictionScrollViewer>

All we are doing here is creating an instance of my FrictionScrollViewer that hosts a single ItemsControl. You may notice the name of this class is FrictionScrollViewer, so naturally, there is some friction involved with the scroll movement. What actually happens is that the user's mouse movements are tracked, and a DispatchTimer is used to update the subclassed ScrollViewer Horizontal/Vertical offsets, every time period tick. It looks quite nice.

When the user is scrolling, the mouse cursor changes to a scrolling cursor.

You still need to Style the FrictionScrollViewer such that the scrollbars are hidden. I could have done this in code, but I didn't know what people would want, so left that option as a Style fixable thing. I personally didn't want scrollbars, but some may, so just change the Style shown above in the XAML.

Creating a custom panel in WPF

Creating custom panels in WPF is quite cool actually. Suppose you just don't like the current layout options (StackPanel/Grid/Canvas/DockPanel); we will just write our own.

You know, sometimes you want a custom job. Whilst it's probably true that you make most creations using a combination of the existing layouts, it's sometimes just more convenient to wrap this into a custom panel.

One of my fellow WPF Disciples, Rudi Grobler, has recently published a single application with loads of different custom panels from loads of different sources in one contained demo app. This is available at Rudi's blog. Have a look there if you want more panels:

Now when creating custom panels, there are just two methods that you need to override, these are:

  • Size MeasureOverride(Size constraint)
  • Size ArrangeOverride(Size arrangeBounds)

One of the best articles I've ever seen on creating custom panels is the article by Paul Tallett over at CodeProject, Fisheye Panel; paraphrasing Paul's excellent article:

To get your own custom Panel off the ground, you need to derive from System.Windows.Controls.Panel and implement two overrides: MeasureOverride and LayoutOverride. These implement the two-pass layout system where during the Measure phase, you are called by your parent to see how much space you'd like. You normally ask your children how much space they would like, and then pass the result back to the parent. In the second pass, somebody decides on how big everything is going to be, and passes the final size down to your ArrangeOverride method where you tell the children their size and lay them out. Note that every time you do something that affects layout (e.g., resize the window), all this happens again with new sizes.

What I wanted to achieve for this article was a column based Panel that wrapped to a new column when it ran out of space in the current column. Now, I could have just used a DockPanel that contained loads of vertical StackPanels, but that defeats what I am after. I want the panel to work out how many items are in a column based on the available size.

So I set to work exploring, and I found an excellent start within the superb Pro WPF in C# 2008: Windows Presentation Foundation with .NET 3.5, by Mathew McDonald, so my code is largely based on Mathew's book example.

The most important methods of my ColumnedPanel are shown below:

#region Measure Override
// From MSDN : When overridden in a derived class, measures the 
// size in layout required for child elements and determines a
// size for the FrameworkElement-derived class
protected override Size MeasureOverride(Size constraint)
{
    Size currentColumnSize = new Size();
    Size panelSize = new Size();

    foreach (UIElement element in base.InternalChildren)
    {
        element.Measure(constraint);
        Size desiredSize = element.DesiredSize;

        if (GetColumnBreakBefore(element) ||
            currentColumnSize.Height + desiredSize.Height > constraint.Height)
        {
            // Switch to a new column (either because
            // the element has requested it
            // or space has run out).
            panelSize.Height = 
              Math.Max(currentColumnSize.Height, panelSize.Height);
            panelSize.Width += currentColumnSize.Width;
            currentColumnSize = desiredSize;

            // If the element is too high to fit
            // using the maximum height of the line,
            // just give it a separate column.
            if (desiredSize.Height > constraint.Height)
            {
                panelSize.Height = 
                  Math.Max(desiredSize.Height, panelSize.Height);
                panelSize.Width += desiredSize.Width;
                currentColumnSize = new Size();
            }
        }
        else
        {
            // Keep adding to the current column.
            currentColumnSize.Height += desiredSize.Height;

            // Make sure the line is as wide as its widest element.
            currentColumnSize.Width = 
              Math.Max(desiredSize.Width, currentColumnSize.Width);
        }
    }

    // Return the size required to fit all elements.
    // Ordinarily, this is the width of the constraint, and the height
    // is based on the size of the elements.
    // However, if an element is higher than the height given to the panel,
    // the desired width will be the height of that column.
    panelSize.Height = Math.Max(currentColumnSize.Height, panelSize.Height);
    panelSize.Width += currentColumnSize.Width;
    return panelSize;
}
#endregion

#region Arrange Override
//From MSDN : When overridden in a derived class, positions child
//elements and determines a size for a FrameworkElement derived
//class.
protected override Size ArrangeOverride(Size arrangeBounds)
{
    int firstInLine = 0;

    Size currentColumnSize = new Size();

    double accumulatedWidth = 0;

    UIElementCollection elements = base.InternalChildren;
    for (int i = 0; i < elements.Count; i++)
    {

        Size desiredSize = elements[i].DesiredSize;

        if (GetColumnBreakBefore(elements[i]) || currentColumnSize.Height 
            + desiredSize.Height > arrangeBounds.Height)
            //need to switch to another column
        {
            arrangeColumn(accumulatedWidth, currentColumnSize.Width, 
                          firstInLine, i, arrangeBounds);

            accumulatedWidth += currentColumnSize.Width;
            currentColumnSize = desiredSize;

            //the element is higher than
            // the constraint - give it a separate column     
            if (desiredSize.Height > arrangeBounds.Height)                
            {
                arrangeColumn(accumulatedWidth, desiredSize.Width, i, ++i, 
                    arrangeBounds);
                accumulatedWidth += desiredSize.Width;
                currentColumnSize = new Size();
            }
            firstInLine = i;
        }
        else //continue to accumulate a column
        {
            currentColumnSize.Height += desiredSize.Height;
            currentColumnSize.Width = 
               Math.Max(desiredSize.Width, currentColumnSize.Width);
        }
    }

    if (firstInLine < elements.Count)
        arrangeColumn(accumulatedWidth, currentColumnSize.Width, 
            firstInLine, elements.Count, arrangeBounds);

    return arrangeBounds;
}

#endregion

#region Private Methods
/// <summary>
/// Arranges a single column of elements
/// </summary>
private void arrangeColumn(double x, double columnWidth, 
    int start, int end, Size arrangeBounds)
{
    double y = 0;
    double totalChildHeight = 0;
    double widestChildWidth = 0;
    double xOffset = 0;

    UIElementCollection children = InternalChildren;
    UIElement child;

    for (int i = start; i < end; i++)
    {
        child = children[i];
        totalChildHeight += child.DesiredSize.Height;
        if (child.DesiredSize.Width > widestChildWidth)
            widestChildWidth = child.DesiredSize.Width;
    }

    //work out y start offset within a given column
    y = ((arrangeBounds.Height - totalChildHeight) / 2);


    for (int i = start; i < end; i++)
    {
        child = children[i];
        if (child.DesiredSize.Width < widestChildWidth)
        {
            xOffset = ((widestChildWidth - child.DesiredSize.Width) / 2);
        }

        child.Arrange(new Rect(x + xOffset, y, child.DesiredSize.Width, columnWidth));
        y += child.DesiredSize.Height;
        xOffset = 0;
    }
}
#endregion

I can then use this panel anywhere I could use a standard Panel. I am actually using it as a ItemsControl panel, as shown below, where the ItemsControl is the one that is used by my FrictionScrollViewer mentioned above:

<ItemsControl x:Name="itemsControl" 
                   Style="{StaticResource mainPanelStyle}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <local:ColumnedPanel IsItemsHost="True"
                                 Loaded="OnPanelLoaded" 
                                 MinHeight="360" Height="360" 
                                 VerticalAlignment="Center" 
                                 Background="CornflowerBlue"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

Using Adorners

If you have come from a WinForms world (as I have), the concept of Adorners will probably be a little bit alien. Here is a brief low down on Adorners, from MSDN:

Adorners are a special type of FrameworkElement, used to provide visual cues to a user. Among other uses, Adorners can be used to add functional handles to elements or provide state information about a control.

An Adorner is a custom FrameworkElement that is bound to a UIElement. Adorners are rendered in an AdornerLayer, which is a rendering surface that is always on top of the adorned element or a collection of adorned elements. Rendering of an adorner is independent from rendering of the UIElement that the adorner is bound to. An adorner is typically positioned relative to the element to which it is bound, using the standard 2-D coordinate origin located at the upper-left of the adorned element.

Common applications for adorners include:

  • Adding functional handles to a UIElement that enable a user to manipulate the element in some way (resize, rotate, reposition, etc.)
  • Provide visual feedback to indicate various states, or in response to various events
  • Overlay visual decorations on a UIElement
  • Visually mask or override part or all of a UIElement

The attached project actually uses two Adorners, one for the main ScrollerControl (which is ScrollerControlAdorner), and one for an ItemHolder (ItemHolderAdorner). These are described below.

ScrollerControlAdorner

Provides two extra buttons that are created and managed on the AdornerLayer. These buttons allow the Adorner to call the Scroll(Point delta) method in AdornedElement (the ScrollerControl). There is a DispatchTimer that is used to call Scroll(Point delta) while the mouse is over the buttons within the Adorner. The most important part of this Adorner is the constructor/timer, which is as follows:

#region Constructor

public ScrollerControlAdorner(ScrollerControl sc)
    : base(sc)
{

    timer.Interval = new TimeSpan(0, 0, 0, 0, 100);
    timer.IsEnabled = false;
    timer.Tick += new EventHandler(timer_Tick);
    this.sc = sc;

    host.Width = (double)this.AdornedElement.GetValue(ActualWidthProperty);
    host.Height = (double)this.AdornedElement.GetValue(ActualHeightProperty);
    host.VerticalAlignment = VerticalAlignment.Center;
    host.HorizontalAlignment = HorizontalAlignment.Left;
    host.Margin = new Thickness(0);

    Button btnLeft = new Button();
    Style styleLeft = sc.TryFindResource("leftButtonStyle") as Style;
    if (styleLeft != null)
        btnLeft.Style = styleLeft;
    btnLeft.MouseEnter += new System.Windows.Input.MouseEventHandler(btnLeft_MouseEnter);
    btnLeft.MouseLeave += new System.Windows.Input.MouseEventHandler(MouseLeave);

    Point parentTopleft = sc.TranslatePoint(new Point(0, 0), sc.Parent as UIElement);
    double top = ((host.Height / 2) - (GLOBALS.leftRightButtonScrollSize / 2) - 
        (parentTopleft.Y / 2) - (GLOBALS.footerPanelHeight/2));

    btnLeft.SetValue(Canvas.TopProperty, top);
    btnLeft.SetValue(Canvas.LeftProperty, (double)spacer);

    Button btnRight = new Button();
    Style styleRight = sc.TryFindResource("rightButtonStyle") as Style;
    if (styleRight != null)
        btnRight.Style = styleRight;
    btnRight.MouseEnter += 
      new System.Windows.Input.MouseEventHandler(btnRight_MouseEnter);
    btnRight.MouseLeave += new System.Windows.Input.MouseEventHandler(MouseLeave);
    btnRight.SetValue(Canvas.TopProperty, top);
    btnRight.SetValue(Canvas.LeftProperty, 
        (double)(host.Width - (GLOBALS.leftRightButtonScrollSize + spacer)));

    host.Children.Add(btnLeft);
    host.Children.Add(btnRight);
    base.AddLogicalChild(host);
    base.AddVisualChild(host);
}

#endregion // Constructor

#region Private Methods
private void timer_Tick(object sender, EventArgs e)
{
    switch (currentDirection)
    {
        case ScrollDirection.Left:
            sc.Scroll(new Point(10, 0));
            break;
        case ScrollDirection.Right:
            sc.Scroll(new Point(-10, 0));
            break;
    }
}

/// <summary>
/// Sets currentDirection to None and disables scroll timer
/// </summary>
new private void MouseLeave(object sender, 
                 System.Windows.Input.MouseEventArgs e)
{
    currentDirection = ScrollDirection.None;
    timer.IsEnabled = false;
}

/// <summary>
/// Sets currentDirection to Left and enables scroll timer
/// </summary>
private void btnLeft_MouseEnter(object sender, 
             System.Windows.Input.MouseEventArgs e)
{
    currentDirection = ScrollDirection.Left;
    timer.IsEnabled = true;
}


/// <summary>
/// Sets currentDirection to Right and enables scroll timer
/// </summary>        
private void btnRight_MouseEnter(object sender, 
             System.Windows.Input.MouseEventArgs e)
{
    currentDirection = ScrollDirection.Right;
    timer.IsEnabled = true;
}
#endregion

ItemHolderAdorner

Provides a visual copy of the AdornedElement (ItemHolder), which is then scaled up a bit. The visual copy is achieved my making a VisualBrush of the AdornedElement. The most important part of this Adorner is the constructor, which is as follows:

public ItemHolderAdorner(ItemHolder adornedElement, Point position)
    : base(adornedElement)
{
    host.Width = adornedElement.Width;
    host.Height = adornedElement.Height;
    host.HorizontalAlignment = HorizontalAlignment.Left;
    host.VerticalAlignment = VerticalAlignment.Top;
    host.IsHitTestVisible = false;
    this.IsHitTestVisible = false;


    Border outerBorder = new Border();
    outerBorder.Background = Brushes.White;
    outerBorder.Margin = new Thickness(0);
    outerBorder.CornerRadius = new CornerRadius(5);
    outerBorder.Width = adornedElement.Width;
    outerBorder.Height = adornedElement.Height;
    
    Border innerBorder = new Border();
    innerBorder.Background = Brushes.CornflowerBlue;
    innerBorder.Margin = new Thickness(1);
    innerBorder.CornerRadius = new CornerRadius(5);

    outerBorder.Child = innerBorder;
    innerBorder.Child = new Grid
        {
            Background = new VisualBrush(adornedElement as Visual),
            Margin = new Thickness(1)
        };

    double scale = 1.5;

    host.RenderTransform = new ScaleTransform(scale, scale, -0.5, -0.5);
    double hostWidth = host.Width * scale;
    double diff = (double)(adornedElement.Width / 4);
    Thickness margin = new Thickness();
    margin.Top = diff * -1;
    margin.Left = diff * -1;
    host.Margin = margin;

    host.Children.Add(outerBorder);
    base.AddLogicalChild(host);
    base.AddVisualChild(host);
}

Sorting an ItemsControl using a CollectionView

As I stated at the start of this article, this was actually the main reason to write this article... But I got carried away and wrote the rest.

Sorting in WPF when using ItemsControl (or subclasses, such as ListBox) can be achieved through either XAML or code-behind. I am using code-behind, but I shall demonstrate an example of XAML also.

The first thing you need to be aware of is what sort of mode you are using with the ItemsControl; if you are using ItemsSource, then you are in what I will call NonDirect mode; if you are adding items using Items.Add(), you are in what I will call Direct mode.

If you have an ItemsControl, such as a ListBox that has content, you can use the Items property to access the ItemCollection, which is a view. Because it is a view, you can then use the view-related functionalities such as sorting, filtering, and grouping. Note that when ItemsSource is set, the view operations delegate to the view over the ItemsSource collection. Therefore, the ItemCollection supports sorting, filtering, and grouping only if the delegated view supports them.

The following example shows how to sort the content of a ListBox named myListBox. In this example, Content is the name of the property to sort by.

myListBox.Items.SortDescriptions.Add(new SortDescription("Content", 
                                     ListSortDirection.Descending));

When you do this, the view might or might not be the default view, depending on how the data is set up on your ItemsControl. For example, when the ItemsSource property is bound to a CollectionViewSource, the view that you obtain using the Items property is not the default view.

If your ItemsControl is bound (you are using the ItemsSource property), then you can do the following to get the default view:

CollectionView myView myView = 
  (CollectionView)CollectionViewSource.GetDefaultView(myItemsControl.ItemsSource);

In the attached demo project, I am setting up new sorts based on public properties which are available on the ItemHolder controls that I am storing in the ObservableCollection<ItemHolder> (ItemList) which is used as the ItemsSource for the ItemsControl.

public void SortItems(SortingType sort)
{
    CollectionView defaultView =
        (CollectionView)CollectionViewSource.
        GetDefaultView(itemsControl.ItemsSource);

    switch (sort)
    {
        case SortingType.Normal:
            defaultView.SortDescriptions.Clear();
            SetItemsCurrentSort(sort);
            break;
        case SortingType.ByDate:
            defaultView.SortDescriptions.Add(new SortDescription("FileDate", 
                ListSortDirection.Descending));
            SetItemsCurrentSort(sort);
            break;
        case SortingType.ByExtension:
            defaultView.SortDescriptions.Add(new SortDescription("FileExtension", 
                ListSortDirection.Descending));
            SetItemsCurrentSort(sort);
            break;
        case SortingType.BySize:
            defaultView.SortDescriptions.Add(new SortDescription("FileSize", 
                ListSortDirection.Descending));
            SetItemsCurrentSort(sort);
            break;
        default:
            defaultView.SortDescriptions.Clear();
            SetItemsCurrentSort(SortingType.Normal);
            break;
    }
}

To do something like this in XAML, we could simply do something like:

The following example creates a view of the data collection that is sorted by the city name and grouped by the state:

<Window.Resources>
  <src:Places x:Key="places"/>
  <CollectionViewSource Source="{StaticResource places}" x:Key="cvs">
    <CollectionViewSource.SortDescriptions>
      <scm:SortDescription PropertyName="CityName"/>
    </CollectionViewSource.SortDescriptions>
    <CollectionViewSource.GroupDescriptions>
      <dat:PropertyGroupDescription PropertyName="State"/>
    </CollectionViewSource.GroupDescriptions>
  </CollectionViewSource>

</Window.Resources>

The view can then be a binding source, as in the following example:

<ListBox ItemsSource="{Binding Source={StaticResource cvs}}"
         DisplayMemberPath="CityName" Name="lb">
  <ListBox.GroupStyle>
    <x:Static Member="GroupStyle.Default"/>
  </ListBox.GroupStyle>
</ListBox>

Conclusion

I started this article while I was waiting for an answer to come for my other last article, but you know, I am pretty pleased with the results of this one. I think it shows a nice set of things like:

  • How to create a scrollable design area
  • Designing a custom panel
  • How to use Adorners in a neat way
  • How to sort a list using a CollectionView

So hopefully, there is still something in here for you to use.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here