Introduction
As I am trying to code a WPF File Explorer, Multi-Select List View is very important to me. Two years ago, I developed a MultiSelection Helper which uses HitTest to do the tricks, but as Mickey Mousoff points out, it's an overcomplicated and non-effective approach, I agreed, but that's all I could offer at that moment.
The new approach is even more complicated, I rewrite everything except the ListView, but it is worth it as it can provide better performance.
Index
SelectionHelper class
- How to use?
- How it works?
- Scrolling issues
- Panels unable to report position
- Unable to draw the selection properly
- Incomplete items
- References
- Version history
SelectionHelper Class
How to Use?
You can enable multiselect by using SelectionHelper.EnableSelection attached property:
<ListView x:Name="listView" uc:SelectionHelper.EnableSelection="True" />
If you are not using GridView, you have to use IChildInfo interface supported Panels. It contains only one method:
Rect GetChildRect(int itemIndex);
VirtualWrapPanel and VirtualStackPanel already have this implemented.
You can define a view using something similar to the following:
<uc:VirutalWrapPanelView x:Key="ListView" ItemHeight="20" ItemWidth="100"
HorizontalContentAlignment="Left" Orientation="Vertical" >
<uc:VirutalWrapPanelView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image x:Name="img" Source="Generic_Document.png" Width="16"/>
<TextBlock Text="{Binding}" Margin="5,0" />
</StackPanel>
</DataTemplate>
</uc:VirutalWrapPanelView.ItemTemplate>
</uc:VirutalWrapPanelView>
You can change the ListViewItem ControlTemplate to trigger when SelectionHelper.IsDragging (demo not included):

Taken from VirtualWrapPanelView.xaml:
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Grid>
<Border Background="{TemplateBinding Background}" />
<Border Background="#BEFFFFFF" Margin="1,1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Border Margin="2,1,2,0" Grid.Row="0" Background="#57FFFFFF" />
</Grid>
</Border>
<ContentPresenter Margin="5,0" />
</Grid>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsSelected" Value="False"/>
</MultiTrigger.Conditions>
<Setter Property="Background" Value="{DynamicResource fileListHotTrackBrush}" />
</MultiTrigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{DynamicResource fileListSelectionBrush}" />
</Trigger>
<Trigger Property="uc:SelectionHelper.IsDragging" Value="True">
<Setter Property="Background" Value="Black" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
How It Works?
The structure of a ListView is shown above, which embedded a ScrollViewer, and a ScrollContentPresenter then ItemPresenter. The ItemPresenter contains the Panel specified in the View, in this case, VirtualWrapPanel, it's the Panel that hosts, measures and arranges the items.
Because the ScrollContentPresenter is the topmost control without the scrollbars, I attached most events here, as well as the adorner in the AdornerLayer shown above.
The SelectionHelper works this way:
PreviewMouseDown
SetStartPosition
SetStartScrollbarPosition
SetIsDragging
- Capture mouse, so it works if user moves outside the
listview
MouseMove - If IsDragging...
SelectionAdorner shown?
- (No) if (move distance > threshold) Show
SelectionAdorner
- (Yes)
GetMousePosition
GetScrollbarPostion
UpdateAdornerPosition
UpdateSelection (preview)
MouseUp - If IsDragging...
UpdateSelection(final)
- Hide
SelectionAdorner
SetIsDragging to False
- Release mouse
It looks very simple, but there are a number of issues that are required to be solved.
Scrolling Issues
Because most events are attached to ScrollContentPresenter, it's not aware of the scrolling position. So when drag started, I have to obtain the scrollbar position and record it. The position can be obtained by:
ScrollViewer scrollViewer = UITools.FindAncestor<ScrollViewer>(p);
return new Point(p.ActualWidth / scrollViewer.ViewportWidth *
scrollViewer.HorizontalOffset,
p.ActualHeight / scrollViewer.ViewportHeight * scrollViewer.VerticalOffset);
The reason that some calculation is required (instead of returning the offset directly) is that GridView storeViewportHeight and VerticalOffset differently store ViewportHeight as total number of items and VerticalOffset as the item scrolled to.
When user moves the mouse when dragging, both mouse position and scrollbar position is used to calculate the selected:
UpdateSelection(p, new Rect(
new Point(startPosition.X + startScrollbarPosition.X,
startPosition.Y + startScrollbarPosition.Y),
new Point(curPosition.X + curScrollbarPosition.X,
curPosition.Y + curScrollbarPosition.Y)));
Panels Unable to Report Position
For GridView, it's easy to deal with because I can just return the items between first and last selected item.
For other views, VirtualWrapPanel and VirtualStackPanel are designed for this purpose, as most listviews use these two panels (all file lists except gridview can be represented by VirtualWrapView), both panels are designed based on Dan Crevier's VirtualizingTilePanel. Because the panels are virtual, the listview items are generated when needed, and thus you must specify the item size. Both panels expose a method named GetChildRect() allowing SelectionHelper to obtain the position of individual ListViewItem.
VirutalWrapPanelView is a ViewBase, it allows the coder to set a number of properties of ListView at a time, so to change the list method all it takes is to assign the ListView.View to a new one instead of assigning a dozen properties. VirtualWrapPanelView exposes the ItemWidth, ItemHeight and Orientation properties of its VirtualWrapPanel.
Because VirtualPanel is used, not all ListViewItems are generated, thus I cannot signal ListView.SelectedEvent and ListView.UnselectedEvent. Listen to ListView.SelectionChangeEvent instead.
Unable to Draw the Selection Properly
SelectionAdorner is designed for this purpose, it is an adorner attached to ScrollContentPresenter (a control inside the ListView which holds the child items). It can display the drag area based on its three properties, IsSelecting (whether the adorner is visible), StartPosition and EndPosition.
References
Version History
- 11-03-10 - version 0.1
- 12-03-10 - version 0.2
- Handles shift / control button properly
- Handle drag outside the scroll control properly
- (Most events attached to the
listview now)
- 17-03-10 - version 0.3
SelectedItems is now only changed when drag is completed
SelectionHelper.GetIsDragging(aListViewItem) is true when the ListViewItem is inside the user-selected region (therefore you can theme the selection)
SelectedItems is now changed by adding / removing items, instead of clearing it and re-polling the list
- 19-07-10 - version 0.4
- Fixed click on GridView Header recognize as drag start. For GridView, only support selection if drag occur inside the first column Fixed
VirtualListView selection problem by adding IVirtualListView interface.