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

WPF Behavior to Give Buttons the Capability to Add, Remove and Reorder Contents of a ListBox

0.00/5 (No votes)
3 Mar 2019 1  
This tip presents a behavior that will allow buttons to control the items in a WPF Selector (or ListBox) control.

Introduction

I had a situation where I wanted to be able to add items to a list, and move items around in the list, and it seemed the easiest way to use a ListBox. I immediately thought about how I could do this in a generic way, and then, maybe, use a behavior to do this. This seemed like a really useful idea. I decided to do it in a simple way for the application I was working on, but thought that I would create a demo project to explore the idea. This is the result.

Overview

The behavior actually has four independent parts to do different functions in a single class:

  • Add an item
  • Move a selected item up one spot
  • Move a selected item down one spot
  • Remove a selected item

The structure of the code for each of these functions is pretty much similar with only some details being different.

The code that will be examined is the code to Move Up function.

First, there is the definition of the DependencyProperty:

public static readonly DependencyProperty MoveItemUpProperty =
    DependencyProperty.RegisterAttached("MoveItemUp",
        typeof(Selector), typeof(ListHelperBehavior),
            new PropertyMetadata(null, OnMoveItemUpChanged));

public static Selector GetMoveItemUp(UIElement uiElement)
{ return (Selector)uiElement.GetValue(MoveItemUpProperty); }

public static void SetMoveItemUp(UIElement uiElement, Selector value)
{ uiElement.SetValue(MoveItemUpProperty, value); }

This is used to provide a Binding to the Selector (or ListBox) control that contains the list. It is used on a Button that is to perform the action, which in this case is to move a selected item one position up. For this action, the code needs to have access to the ItemsSource and SelectedIndex of the Selector control, the first to actually be able to do the move, and the second to know which item to move.

This code is pretty much the same for all actions, except that the Add Item does not need to monitor the SelectionChanged event of the Selector, and the Button is never disabled.

When this DependencyProperty changes, the event handler OnMoveUpItemChanged is executed. This event handler is specified in the FrameworkMetadata argument of the DependencyProperty RegisterAttached method.

private static void OnMoveItemUpChanged(DependencyObject d,
    DependencyPropertyChangedEventArgs e)
{
    if (e.OldValue is Selector Selector1)
    {
        Selector1.SelectionChanged -= SetMoveItemUpButtonIsEnabled;
    }
    if (e.NewValue is Selector Selector)
    {
        var Button = CheckForButtonBase(d);
        Button.Click -= MoveItemUpEvent;
        Button.Click += MoveItemUpEvent;
        Selector.SetValue(MoveUpButton, Button);
        Selector.SelectionChanged += SetMoveItemUpButtonIsEnabled;
        SetMoveItemUpButtonIsEnabled(Selector, null);
    }
}

This code attaches event handlers to the Button Click event, and the Selector SelectionChanged event. To ensure that the Button Click event is not double subscribed to, before subscribing to the event, and the event handler for SelectionChanged for the old Selector, if it exists, is removed. Also, the Button is saved in an attached DependencyProperty of the Selector so that it can be found for use by SelectionChanged event handler. Finally, the Button IsEnabled value is adjusted by use of the SelectionChanged event handler.

The code for the saving of the Button in the Selector is the following private DependencyProperty so that the Button be enabled and disabled can be found:

private static readonly DependencyProperty MoveUpButton =
    DependencyProperty.RegisterAttached("MoveUpButton",
        typeof(ButtonBase), typeof(ListHelperBehavior),
            new PropertyMetadata(null));

The Add Item code does not need to monitor the SelectionChanged event since the Button is never disabled.
The Click event of the Button for the Move Down function is as follows:

private static void MoveItemUpEvent(object sender, RoutedEventArgs e)
{
    Debug.Assert(sender is ButtonBase);
    var Button = (ButtonBase)sender;
    var Selector = GetMoveItemUp(Button);
    var IList = CheckForIList(Selector);
    var itemNumber = Selector.SelectedIndex;
    var item = IList[itemNumber];
    IList.RemoveAt(itemNumber);
    var type = IList.GetType().GetGenericArguments().Single();
    var castInstance = Convert.ChangeType(item, type);
    IList.Insert(itemNumber - 1, castInstance);
    if (itemNumber == 1) Button.IsEnabled = false;
    Selector.SelectedIndex = itemNumber - 1;
}

The sender argument has to be cast to a ButtonBase type, and then that is used to get the value for the Selector control saved as an attached property in the ButtonBase. This is then used to get IList that is bound to the Selector ItemsSource DependencyProperty and the SelectedItem value of the Selector. The selected item in the IList is then copied, cast to the correct type (getting the type with the Reflection GetGenericArgument method of the Type class, and then casting it with the Convert.ChangeType method), and then removed from the IList (RemoveAt method of the IList). The removed item is then inserted with the Selector Insert method.

Next, a check is done on if this is now the first item, disabling the Button if it is, and the Selector SelectedIndex is set so that it still points to the same item.

The Move Up code is almost identical, the Remove is much simpler since it does not have to save the removed item and then insert it back into the IList.

Finally, there is the code that will appropriately enable or disable the Button depending on if there is a SelectedItem, the SelectedItem is the first (for Move Up), or last item in the IList (for Move Down). This is an event handler that is called when for the SelectedItem event of the Selector is triggered:

private static void SetMoveItemUpButtonIsEnabled(object sender, RoutedEventArgs e)
 {
     Debug.Assert(sender is Selector);
     var Selector = (Selector)sender;
     var IList = CheckForIList(Selector);
     var itemNumber = Selector.SelectedIndex;
     var Button = (ButtonBase) Selector.GetValue(MoveUpButton);
     Button.IsEnabled = (itemNumber >= 1 && itemNumber < IList.Count);
 }

For this, you need the IList bound to the ItemsSource, the SelectedIndex, and you need to get the Button saved as an attached property for this function in the Selector. For the Remove function, you only have to know if SelectedIndex is equal to -1, so it is much simpler.

Using the Behavior

To use this behavior, you just need a list control that is derived from the Selector control, associate a Name value for this control, and define a Button for each function that should be implemented. Within each Button XAML, just include the ListHelperBahavior with the DependencyProperty and associate it with a Binding to the Selector:

<Grid Margin="10">
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <ListBox Name="TheList"
             ItemsSource="{Binding List}"
             HorizontalAlignment="Stretch"
             VerticalAlignment="Stretch"  >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="30"/>
                        <ColumnDefinition Width="200"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Text="{Binding ItemNumber}"/>
                    <TextBlock Grid.Column="1"
                               Text="{Binding TimeCreated}"/>
                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    <StackPanel Grid.Row="2"
                Margin="-5 5"
                Orientation="Horizontal"
                HorizontalAlignment="Right">
        <Button Content="Add"
                Width="70"
                Margin="5"
                local:ListHelperBehavior.AddToList="{Binding ElementName=TheList}"/>
        <Button Content="Remove"
                Width="70"
                Margin="5"
                local:ListHelperBehavior.RemoveFromList="{Binding ElementName=TheList}"/>
        <Button Content="Move Up"
                Width="70"
                Margin="5"
                local:ListHelperBehavior.MoveItemUp="{Binding ElementName=TheList}"/>
        <Button Content="Move Down"
                Width="70"
                Margin="5"
                local:ListHelperBehavior.MoveItemDown="{Binding ElementName=TheList}"/>
    </StackPanel>

Issues

There are some limitations on this behavior, some of which can be handled with additional code.
One of the issues is the behavior expects the Type bound to the Selector is of type IList, which means that both List and ObservableCollection can be used, but the Array Type cannot. This could be coded for but would require recreating the Array each time.

Another limitation is that the Add will only work if the Type of the IList is a class, and has a default constructor.

Of course, another limitation is that it handles only controls derived from the Selector control.

Conclusion

This is a really nice little behavior since it allows the order of a list to be changed and to add or remove items by just adding the behavior to each Button that is to implement the functionality. Nothing is required to be done in the ViewModel to provide this functionality.

History

  • 2019-03-03: Initial version

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