Click here to Skip to main content
15,883,831 members
Articles / Desktop Programming / WPF

WPF UIEventHub – MultiSelect and DragnDrop

Rate me:
Please Sign up or sign in to vote.
4.86/5 (3 votes)
26 Dec 2013LGPL34 min read 14.8K   9   2
I hope this rewrite can reduce the effort needed to implement other touch based code and other gesture.

FileExplorer3.DropnDrop

WPF UIEventHub register a number of events and distributed to registered UIEventProcessor, available UIEventProcessor included MultiSelectEventProcessor and DragDropEventProcessor, which is an update for SelectionHelper and FileDragDropHelper static class.

How to use?

UIEventHub is created to support ListBox/View and TreeView, you can use xaml syntax or cs code to register the UIProcessors.

Xaml:

<ListView Grid.Column="1" Grid.Row="2" >
<bc:UIEventAdapter.Processors>
<bc:DragDropEventProcessor EnableDrag="True" EnableDrop="True" />
<bc:MultiSelectEventProcessor UnselectAllCommand="{Binding UnselectAllCommand}" />
</bc:UIEventAdapter.Processors>
</ListView>

Cs:

treeView.RegisterEventProcessors(new DragDropEventProcessor());
listView.RegisterEventProcessors(new DragDropEventProcessor(), new MultiSelectEventProcessor(vm1.UnselectAllCommand));

Please noted that TreeView does not support Multi-Select.

For Drag and Drop, in addition to register the UIProcessor, you also need to implement ISupportDrag/Drop/DragHelper/DropHelper for the system to work, it will be described below.

UIEventProcessor

///
/// Allow one application (e.g. dragging) to handle events from an control.
/// 

public interface IUIEventProcessor
{
   IEnumerable<RoutedEvent> ProcessEvents { get; }
   IScriptCommand OnEvent(RoutedEvent eventId); 
   IScriptCommand OnMouseDrag { get; }  //Previous method
   IScriptCommand OnMouseDragOver { get; }
   ....
}

public interface IScriptCommand
{
    string CommandKey { get; }

    IScriptCommand Execute(ParameterDic pm);
    bool CanExecute(ParameterDic pm);
}

UIEventProcessor includes a number of IScriptCommands  has OnEvent() method. When certain event is triggered, a ScriptRunner is created and run the appropriate IScriptCommand (e.g. Control.MouseMove -> OnEvent() -> ContinueDrag).  Because IScriptCommand.Execute() can return another IScriptCommand, complex operations are broken up into multiple IScriptCommands.

public class ScriptRunner : IScriptRunner
{
   public void Run(Queue<IScriptCommand> cmds, ParameterDic initialParameters)
   {
      ParameterDic pd = initialParameters;

      while (cmds.Any())
      {
         var current = cmds.Dequeue();
         if (current.CanExecute(pd))
         {
             var retCmd = current.Execute(pd);
             if (retCmd != null)
                cmds.Enqueue(retCmd);
         }
         else throw new Exception(String.Format("Cannot execute {0}", current));
      }
   }
}

 

Using UIEventHub, Drag and Drop Helper and MultiSelect Helper (static classes) in FileExplorer is converted to DragDropEventProcessor and MultiSelectEventProcessor.

DragDropEventProcessor

DragDropUIEventProcessor

DragDropEventProcessor enable dragging one or multi object from one DataContext (which implements ISupportDrag) to another DataContext (which implements ISupportDrop).

Control does not have Drag event, it’s trigger when MouseMove (while a mouse button is pressed) more than SystemParameters.MinimumHorizontalDragDistance  or MinimumVerticalDragDistance.

public interface ISupportDragHelper
{
    ISupportDrag DragHelper { get; }
}

public interface ISupportDrag
{        
    bool HasDraggables { get; }
    IEnumerable<IDraggable> GetDraggables();
    DragDropEffects QueryDrag(IEnumerable<IDraggable> draggables);
    IDataObject GetDataObject(IEnumerable<IDraggable> draggables);        
    void OnDragCompleted(IEnumerable<IDraggable> draggables, IDataObject da, DragDropEffects effect);
}

So whenever user trying to drag, this processor will check if DataContext supports ISupportDrag or ISupportDragHelper, and if HasDraggables equals true, it then calls GetDraggables() and GetDataObject() to get an dataobject, and initialize the drag  (using System.Windows.DragDrop.DoDragDrop).

Because it uses DoDragDrop, you can create a DataObject for shell file drop (to Windows Explorer, VS, but doesn’t work on notepad).  For files that's not exists in the file system, you can use the included VirtualDataObject to delay creation of the files as well.

That’s the drag operation, then we have drop operation.

Whenever items (DataObject) is dragged over or dropped on a registered control, the processor looks up the LogicalTree for a DataContext that implements ISupportDrop and it’s IDroppable equals true, so If DataContext of the current item, for examples, ListViewItem, does not support ISupportDrop, it will lookup the ListView. 

public interface ISupportDropHelper
{
    ISupportDrop DropHelper { get; }
}

public interface ISupportDrop
{
    bool IsDraggingOver { set; }
    bool IsDroppable { get; }
    string DropTargetLabel { get; }
    QueryDropResult QueryDrop(IDataObject da, DragDropEffects allowedEffects);
    IEnumerable<IDraggable> QueryDropDraggables(IDataObject da);
    DragDropEffects Drop(IEnumerable<IDraggable> draggables, IDataObject da, DragDropEffects allowedEffects);
}

DragDropEventProcessor show an adorner (DragAdorner) under your mouse cursor, it has an ItemsControl which loaded from ISupportDrop.QueryDropDraggables().  You have to specify a ItemTemplate for it, without template it shows the items in text.

<DataTemplate x:Key="dragnDropHintTemplate">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding Value}" />
        <TextBlock Text="[drag]" Foreground="Gray" />
    </StackPanel>
</DataTemplate>
<Style TargetType="{x:Type ListView}" BasedOn="{StaticResource {x:Type ListBox}}" >
    <Setter Property="ItemTemplate" Value="{StaticResource dragnDropItemTemplate}" />
    <Setter Property="ItemsSource" Value="{Binding Items}" />
    <Setter Property="AllowDrop" Value="True" />
    <Setter Property="bc:AttachedProperties.DragItemTemplate" 
            Value="{StaticResource dragnDropHintTemplate}" />
</Style>

The drag adorner also contains a textblock that shown “Copy n items to {target}”, the target text is loaded from ISupportDrop.DropTargetLabel.

This drag adorner is reside in an adorner layer named PART_DragDropAdorner, this adorner can be shared, so it’s ideal to put it under root control like Windows or Page.

<AdornerDecorator x:Name="PART_DragDropAdorner" />

QueryDrop allow you to return multiple supported DragDropEffects and one preferred DragDropEffect.  If user initiate the drag and drop in the same application using right mouse, a context menu is shown (ShowAdornerContextMenu) for choosing the desired DragDropEffect.  In this case, ISupportDrag.OnDragCompleted() is called after user select a DragDropEffect, instead of immediately after Drop.

MultiSelectEventProcessor

MultiSelectUIEventProcessor

MultiSelectEventProcessor allow you to choose multiple items by dragging with mouse on ListView, this is very basic functionality and it’s wired that the framework still doesn’t provide it.

When dragging, an adorner (SelectionAdorner) is shown, it has 0.5 opacity so the items below are visible.  The adorner is reside in adorner layer inside the control.  The items under the SelectionAdorner have it’s AttachedProperties.IsSelecting set to true, you can highlight your items when selecting.

<DataTemplate x:Key="dragnDropItemTemplate">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding Value}" />
        <TextBlock Text="[ing]" Foreground="Blue"
                Visibility="{Binding (bc:AttachedProperties.IsSelecting),
                    RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}},
                    Converter={StaticResource btvc}}" />
        <TextBlock Text="[ed]" Foreground="Green"
                Visibility="{Binding IsSelected, Converter={StaticResource btvc}}" />
    </StackPanel>
</DataTemplate>

The selection lookup routines (FindSelectedItems) is a combination of the first and second articles, it uses first/last selected item for GridView, IChildInfo for panels that support IChildInfo, otherwise HitTest.

public interface IChildInfo
{
    Rect GetChildRect(int itemIndex);
}

If HitTest, MultiSelectEventProcessor cannot correctly return all selected items (they cannot be hit test if destroyed or not shown), so Highlight/SelectItemsByUpdate are used, which add or remove items from the selection list.  For the another two methods, HighlightItems/SelectItems are used, which replace the selection list.

 

Conclusion

The reason for rewriting this two static class is to simplify it so I can implement touch related code.  I cannot implement them yet because I don’t have a machine that support touch right now, but I found implement the code in a scriptable way (learned from REST in Pratice) can make the code reusable and easy to read.

I hope this rewrite can reduce the effort needed to implement other touch based code and other gesture. 

This article has been posted on . You can find a list of my articles here.

This article was originally posted at http://quickzip.org/news/2013/12/wpf-uieventhub

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Founder
Hong Kong Hong Kong

Comments and Discussions

 
GeneralAdd source code, please! Pin
dyma27-Dec-13 3:07
dyma27-Dec-13 3:07 
GeneralRe: Add source code, please! Pin
Leung Yat Chun29-Dec-13 18:27
Leung Yat Chun29-Dec-13 18:27 
Please find them on Codeplex:
https://fileexplorer.codeplex.com/[^]
They are released under MIT license.
Regards
Joseph Leung

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.