Tag Archives: attached behavior

A WPF behavior to improve grouping in your ListBox



Post updated Jan. 2nd 2014 to fix a bug when items have a background.

A couple of weeks ago I started prototyping with a friend a behavior in order to improve the grouping in the ListBox control. Today, I’m finally taking time to write a post about this adventure :-)

My overall goal was to have the group headers always visible and smoothly moving while the content of the control was scrolled. Here is a video showing the demo application running:

Get the Flash Player to see this content.

Download the code (VS2010 required)

Of course I wanted to have the cleanest implementation possible, and as I think you already know, this where the WPF behavior comes to the rescue ! Let’s see how this is possible.

Note:

  • You must be aware that enabling group in your ListBox will disable UI virtualization. This might not be a problem if the databound collections is small (<100/200) but if the collection is very large, you might get into performance problem. However this limitation might be fixed in the next release of WPF (see my post about it here).
  • This solution is not 100% compatible with keyboard selection and should be improved to work properly.

Anatomy of a ListBox when grouping is enabled

In order to create this behavior, I had to carefully look the Visual Tree of a ListBox when grouping is enabled. I used Snoop to do so, and I was able to get the following picture:

As you can see, for each group, we have a GroupItem control. However, we cannot direcly apply a transform to this element because it both contains the header (in a ContentPresenter) and the items in the group (in an ItemsPresenter).

Introducing the OverlayGroupingBehavior

The behavior actually walks in the visual tree to find out the interesting part. In our case, we want to attach a TranslateTransform to the headers of each GroupItem. Then, using simple mathematics, we can figure out the needed Y-translation to move the header at the top position of the ListBox.

Here are some details about the behavior:

  • the class inherits Behavior<ListBox>
  • we override the OnAttached method, and in this method we register an event handler for the LayoutUpdated of the associated ListBox
  • in the LayoutUpdated handler, we update the transforms
?View Code CSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
using System.Windows.Media;
using DemoAnimatedGroup.Helpers;
 
namespace DemoAnimatedGroup.Behaviors
{
    public class OverlayGroupingBehavior : Behavior<ListBox>
    {
        protected override void OnAttached()
        {
            this.AssociatedObject.LayoutUpdated += new EventHandler(this.OnLayoutUpdated);
        }
 
        private void OnLayoutUpdated(object sender, EventArgs e)
        {
            ListBoxItem topListBoxItem1;
            GroupItem topGroupItem1, topGroupItem2;
            ContentPresenter topPresenter1, topPresenter2 = null;
            double topOffset1, topOffset2 = -1;
 
            // find the first ListBoxItem which is at the top of the control
            topListBoxItem1 = this.GetItemAtMinimumYOffset<ListBoxItem>();
            if (topListBoxItem1 == null)
                return;
 
            // get all group items order by their distance to the top
            var groupItems = TreeHelper.FindVisualChildren<GroupItem>(this.AssociatedObject)
                                       .OrderBy(this.GetYOffset)
                                       .ToList();
 
            // from the GroupItem, find the ContentPresenter on which we can apply the transform
            topGroupItem1 = TreeHelper.FindVisualAncestor<GroupItem>(topListBoxItem1);
            topPresenter1 = TreeHelper.FindVisualChildren<ContentPresenter>(topGroupItem1).First(cp => cp.Name == "PART_GroupHeader");
            topOffset1 = this.GetYOffset(topPresenter1);
 
            // try to find the next GroupItem and its presenter
            var index = groupItems.IndexOf(topGroupItem1);
            if (index + 1 < groupItems.Count)
            {
                topGroupItem2 = groupItems.ElementAt(index + 1);
                topPresenter2 = TreeHelper.FindVisualChild<ContentPresenter>(topGroupItem2);
                topOffset2 = this.GetYOffset(topPresenter2);            
            }
 
            // update transforms
            if (topOffset2 < 0 || topOffset2 > topPresenter1.ActualHeight)
                this.SetGroupItemOffset(topPresenter1, topOffset1); 
 
            if(topPresenter2 != null)
                topPresenter2.RenderTransform = null;
        }
 
        private T GetItemAtMinimumYOffset<T>() where T : UIElement
        {
            var minOffset = double.MaxValue;
            T topItem = null;
            foreach (var item in TreeHelper.FindVisualChildren<T>(this.AssociatedObject))
            {
                var offset = this.GetYOffset(item);
                if (Math.Abs(offset) <= Math.Abs(minOffset))
                {
                    minOffset = offset;
                    topItem = item;
                }
            }
 
            return topItem;
        }
 
        private void SetGroupItemOffset(ContentPresenter groupHeader, double offset)
        {
            if (groupHeader.RenderTransform as TranslateTransform == null)
                groupHeader.RenderTransform = new TranslateTransform();
 
            ((TranslateTransform)groupHeader.RenderTransform).Y -= offset;
        }
 
        private double GetYOffset(UIElement uiElement)
        {
            var transform = (MatrixTransform) uiElement.TransformToVisual(this.AssociatedObject);
            return transform.Matrix.OffsetY;
        }
    }
}

Using the behavior

It’s more than easy to enable this functionality in any existing app. In the XAML, all you have to do is to attach the behavior to the targeted ListBox:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<ListBox ItemsSource="{Binding Cities}">
    <i:Interaction.Behaviors>
        <Behaviors:OverlayGroupingBehavior/>
    </i:Interaction.Behaviors>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <!-- data template... -->
        </DataTemplate>
    </ListBox.ItemTemplate>
    <ListBox.GroupStyle>
        <GroupStyle>
        <!-- group style -->
        </GroupStyle>
    </ListBox.GroupStyle>
</ListBox>

If you’re not already grouping items in your ListBox, here is how to do it:

?View Code CSHARP
1
2
var cv = CollectionViewSource.GetDefaultView(viewmodel.Cities);
cv.GroupDescriptions.Add(new PropertyGroupDescription("Country"));

Useful resources

Download the code (VS2010 required)

How to attach commands to any UIElement ?

In this blog, I already wrote 2 posts about attached properties. The more I play with this new WPF concept, the more I like it. Today at work, I found one new nice use of attached properties, and because it is this time reusable, I decided to share my experience here.

If you’re familiar with WPF, then you’re probably familiar with Commands. Commands are a new concept in WPF, here is the introduction from MSDN documentation:

Commanding is an input mechanism in Windows Presentation Foundation (WPF) which provides input handling at a more semantic level than device input. Examples of commands are the Copy, Cut, and Paste operations found on many applications.

Commands help you to decouple your UI from its execution logic and also simplify the process of enabling and disabling controls regarding the state of the command. If you want more details about Commands, you can check out this nice post from Marlon Grech.

You can attach a command to a Button using its Command property (you might also use CommandParameter and CommandTarget properties in some cases). To be more precise, elements that support Command must implement the ICommandSource interface:

?View Code CSHARP
1
2
3
4
5
6
public interface ICommandSource
{
ICommand Command { get; }
object CommandParameter { get; }
IInputElement CommandTarget { get; }
}

If you open Reflector and lookup for this interface, you’ll discover that 3 controls implement this interface:

  • MenuItem
  • ButtonBase
  • Hyperlink

In the project I’m working on at work, I had to find out a way to surpass 2 limitations:

  • Defining commands on other controls than MenuItem, ButtonBase and Hyperlink
  • Defining commands that could be triggered on other event than MouseLeftButtonUp

As you can imagine, I found a way to do that using… attached properties ! Basically I defined an attached property that I called MouseDoubleClickCommandProperty. This command enables you to attach a ICommand to ANY UIElement that will be triggered when the control is double clicked.

The MouseDoubleClickCommandProperty register a PropertyChangedCallback so that when the target changes, I can register on the UIElement.MouseDownEvent event. By looking the ClickCount property of the MouseButtonEventArgs parameter, I can check the MouseDownEvent comes from a DoubleClick event, and then trigger the associated command.

A nice example of this concept can be found in a TreeView. Imagine you want to start an action when a particular node in your TreeView is double clicked. The basic way to do that is to register the MouseDoubleClick event on the control. The new way to do the same thing is to use the MouseDoubleClickCommandProperty attached propery. Here is an example that shows how to do that in a hierarchical data template:

1
2
3
4
5
6
7
8
9
<HierarchicalDataTemplate
  DataType="{x:Type treeViewModel:Node}" 
  ItemsSource="{Binding Path=Children}">
    <StackPanel Orientation="Horizontal"
      attached:ClickBehavior.MouseDoubleClickCommand="{Binding Path=Command}">
        <Image x:Name="image" Width="16" Height="16" Margin="3,0" Source="..\Resources\Icons\Treeview\Default16.png" />
        <TextBlock Text="{Binding Path=Name}" />
    </StackPanel>
</HierarchicalDataTemplate>

As you can see, by using the MouseDoubleClickCommand attached property is becomes possible to attach the command to a StackPanel ! Moreoever, because we might need to pass parameter to the ICommand, I also defined another attached properties that can hold any parameter you want, this is the MouseEventParameterProperty.

Similarly, we can imagine to trigger Commands when a RightClick occurs (we would just have to define a new attached property to do so).

I did a sample application that demonstrates the concept of this article. Because I didn’t have too much time, I used a ListBox instead of a TreeView. but the concepts are equivalent. Please feel free to comment :-)

Thinking in WPF: more attached properties

In my last blog post, I wrote an article about attached properties. Today at work, I encountered a problem that can be solved in a nice way using an attached property. Because the functionality I wanted to implement is also very simple, I decided to blog about it to give a concrete example.

I wanted to use a TextBox to allow the user of my application to give a description for any item he selects in a TreeView. Because the TextBox’s content could be changed very frequently by the user, I thought it might be useful to select all the TextBox’s text when the user click in the control (so that as soon as he types something, the old content is cleared).

In this article, I will describe various way to implement this feature and I will detail the way I prefer, using of course an attached property !

Continue reading