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 Panel
s. 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 ListViewItem
s 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
SelectedItem
s 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)
SelectedItem
s 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.