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

Enable MultiSelect in WPF ListView (2)

0.00/5 (No votes)
19 Jul 2010 1  
Make your ListView support item selection by dragging

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)
          • HighlightItems
  • MouseUp - If IsDragging...
    • UpdateSelection(final)
      • Update SelectedItems
    • 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
    • Initial version
  • 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.

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