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