Peregrine's View

Yet another C# / WPF / MVVM blog …

WPF General

WPF Behaviors – ListBox

Inappropriate behavior makes me laugh
Will Ferrell

One of the few gripes that I have with WPF is that Microsoft have failed to update many of the standard controls from one version to the next (it often seems that they’re overly worried about breaking legacy code). Many properties that ought to have been DependencyProperties from version 1.0 still aren’t – they’re just coded as standard C# properties instead. This means that they can’t be used as the target for data binding. A few examples such as PasswordBox.Password are understandable, but most just feel like laziness on Microsoft’s part. MediaElement is possibly the worst culprit – having to use a timer behind the scenes, just so you can bind to the current media position is shocking.

However, all is not lost. Microsoft introduced Behaviors in ExpressionBlend, as a mechanism for adding additional functionality to existing classes, even ones that you don’t have the source code for. A behavior can be attached to any DependencyObject, but is most commonly used with a Control descendent.

Over the next couple of posts, I’m going to break away briefly from the dicsussion on MVVM, to demonstrate behaviors for two commonly used WPF controls – ListBox and TreeView.

ListBox Behavior

The standard WPF ListBox control behaves pretty well in a MVVM context. SelectedItem is a dependency property, and can therefore be bound to. However, if the newly selected item is currently scrolled out of view, the U.I. doesn’t update. The Behavior I’m going to add will address that.

public class perListBoxHelper : Behavior<ListBox>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.SelectionChanged -= AssociatedObject_SelectionChanged;
    }

    private static void AssociatedObject_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        var listBox = sender as ListBox;

        if (listBox == null)
            return;

        Action action = () =>
            {
                var selectedItem = listBox.SelectedItem;

                if (selectedItem != null)
                    listBox.ScrollIntoView(selectedItem);
            };

        listBox.Dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle);
    }
}

The OnAttached method of the base behavior is overriden to set up the required functionality, most commonly by adding handlers for existing events. AssociatedObject is the item that this behaviour is attached to – in this case the ListBox instance. The OnDetaching() method is the place to remove the event handlers, helping to prevent memory leaks. The Dispatcher.BeginInvoke call ensures that any virtualised items are fully formed before we try to scroll them into view.

To use a behavior, just include a reference inside the appropriate control.

<ListBox ... >
    <i:Interaction.Behaviors>
        <vhelp:perListBoxScrollSelecionIntoViewBehavior />
    </i:Interaction.Behaviors>
</ListBox>

The associated demonstration application shows the behavior in action. Each ListBox control has its ItemSource bound to the same collection of strings, and its SelectedItem bound to the same string property on the ViewModel. The one on the right hand side of the View uses the perListBoxHelper behavior and will keep the selected item scrolled into view, however it is selected – either using one of the buttons, or by clicking in the other ListBox.

Theres is a further modification to the ListBox’s visual appearance through the inclusion of my standard application styles from the Peregrine.WPF.View library. The biggest difference from the default ListBox style is that the selected item(s) keeps the same background brush, even when the ListBox loses focus.

<Style TargetType="{x:Type ListBoxItem}">
    <Setter Property="Template">
        <!--  Revert to the "Windows 7" style template that used "SystemColors.HighlightBrushKey" etc  -->
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <Border x:Name="ItemBorder"
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        Padding="{TemplateBinding Padding}"
                        SnapsToDevicePixels="true">
                    <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                      VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                      SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                </Border>

                <ControlTemplate.Triggers>
                    <!--  Use the same colours for selected items, whether or not the control has focus  -->
                    <Trigger Property="IsSelected" Value="true">
                        <Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
                        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}" />
                    </Trigger>

                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

In my next post, I’ll be working on the TreeView control, but the behavior and style for that are a little more complicated than the case for the ListBox.

Don’t forget that all of the code samples for this blog are available on Github, along with my own personal C# / WPF library.

If you find this article useful, or you have any other feedback, please leave a comment below.

Leave a Reply

Your email address will not be published. Required fields are marked *