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

Generic Multi-Select MVVM ListBox Drag and Drop Helper with Custom Feedback for Silverlight 4.0

0.00/5 (No votes)
11 Apr 2011 1  
This article focuses on developing an MVVM compatible ListBox-to-ListBox drag/drop helper for Silverlight.

Sample Image

Contents

Introduction

This article focuses on developing an MVVM compatible ListBox-to-ListBox drag/drop helper for Silverlight. There are already many good articles out there that describe the MVVM pattern and the benefits of it - we are not going to try to confuse anyone any further.

Background

We needed a highly flexible drag and drop library for our Silverlight LOB application that was:

  1. MVVM pattern compliant
  2. Supports the Single, Multiple, and Extended ListBox selection modes
  3. Minimal coding required
  4. Drag hover tooltip with blendable templating support
  5. Low CPU usage
  6. Light weight
  7. Free

Looking around, we couldn't find a solution that met the above criteria. Those that were close either had too many features (too bulky) or didn't support MVVM (code-behind only).

We've used .NET 4.0 for the solution, but it wouldn't take much to convert it to .NET 3.5.

Prerequisites

We would have liked to have kept this solution non-reliant on any third-party framework; however, we required a wrapper for the MouseMove event for the Listbox control.

So the solution makes use of an MVVM Framework - MVVM Light by Laurent Bugnion (GalaSoft). We use EventToCommand and RelayCommand to wire up the XAML to the ListDragDropHelper class.

I'm sure that everyone has their favorite framework; but for this article, we wanted to keep the focus on the solution. However, it would be very easy to change this as there is only one property on the ListDragDropHelper class, the RelayCommand property named ChildMoveCommand, that needs to be adjusted, and the MouseMove event for each ListBox in the XAML code for your favorite MVVM framework equivalents.

Project Design

To help ensure that the code meets the MVVM pattern requirements, even though this is a small sample application, it is a good policy to break out the code out into separate project modules:

  • SL MVVM DragDropHelper Sample - Main application containing the Views only
  • Models - All data models
  • ViewModels - All View Models and the View Model locator
  • Services - Mock Data Service
  • Helpers - Support classes
  • ListDragDropSL - Drag drop service and the DragDropToolTip control

Below is a class diagram of the sample project that's been included. The sample project is an example of using the ListDragDropHelper class to re-tag and group products either individually or in bulk (extended multi-select).

ListDragDropHelper Class Overview

When we built the ListDragDropHelper class, we wanted to limit the amount of work to wire up the View <-> ViewModel <-> ListDragDropHelper. So we've reduced the process to the following steps:

  1. Drop the DragDropToolTip control on a parent Panel control (Grid/Canvas/etc..) that contains all the ListBoxes that will be involved in the drag/drop operation and give it a name;
  2. Declare a ListDragDropHelper property on the ViewModel for each ListBox that can initiate a drag/drop operation;
  3. Add a Trigger to each of the listboxes' MouseMove event;
  4. Code the hover & drop handlers in the ViewModel;
  5. Style the DragDropToolTip control.

We could have generated the DragDropToolTip control totally in the ListDragDropHelper class; however, we use it as a marker to quickly identify the Panel control for the listboxes for the drag/drop operation. The upside is that this makes it easy for us to style the control in the Visual Studio XAML editor or Expression Blend.

How the ListDragDropHelper Class Works

We have selected the important parts of the class code to highlight the key functionality. To see the entire code, please download the solution from the above link included with this article. All source code is fully commented and easy to follow.

On initiation of a ListBox MouseMove event, the ListDragDropHelper class will:

  1. Checks if the mouse button is pressed;
  2. Figure out the XAML structure based on the positioning of the DragDropToolTip control;
  3. Position the DragDropToolTip control's Z-Index (for all panel types);
  4. Wire up the parent's (UserControl/Navigation Page) MouseMove and LeftMouseButtonUp events;

Why didn't we use the ListBoxes' LeftMouseButtonDown event? This event never fires. We've seen some solutions that use the SelectedItem or GotFocus events, but these events have issues. The SelectedItem event will only fire when the item is initially selected. The other issue with the SelectedItem event is that the drag operation will initiate on the LeftButtonMouseDown even when the user doesn't want to drag. You could get around this by using a Timer or watch the drag distance before initiating a drag/drop operation; however, you still have the initial issue of the SelectedItem event only firing for a new selection. The GotFocus event will only fire once for the ListBox control.

To get around these issues, as mentioned above, we monitor the MouseMove event of the ListBox and use the CaptureMouse (System.Windows.UIElement) method to identify if the mouse button is pressed or not. There is a 'gotcha' with doing this, but we have a simple solution that is described further in the article that demonstrates how we manage this.

Private Function IsMouseDown() As Boolean
    If _dragListBox IsNot Nothing AndAlso _dragListBox.CaptureMouse() Then
        _dragListBox.ReleaseMouseCapture()
        Return True
    End If
    Return False
End Function

The parent host control's MouseMove and LeftMouseButtonUp events will then manage the drag/drop operation to completion. During the drag/drop operation, the handlers in the ViewModel have total control over what and how the information is displayed in the DragDropToolTip control giving the user feedback.

One of the requirements was to minimise CPU usage. Monitoring the MouseMove event rapidly fires whilst the mouse is inside the bounding control that the event belongs to. When a drag/drop operation is in progress, we monitor the parent control's MouseMove event so that we can bridge the XAML layers from one ListBox to the next. If you have multiple listboxes, like the included sample project, then the CPU will be heavily taxed as multiple ListDragDropHelper classes track the position of the mouse in the same bounding panel control. To overcome this, we only monitor the parent MouseMove once the drag/drop operation begins, and upon completion, we remove the handler. This way, only one ListDragDropHelper class is tracking the mouse's position at any time during a drag/drop operation. The other benefit of using this approach is that it minimises the amount of coding to a single Trigger and per ListBox and a single entry point into the helper class.

'-- Start Drag
Private Sub ChildContainerMouseMove(e As MouseEventArgs)
    '-- Are we about to Drag?
    If (Not _isBusy) AndAlso (Not _isDragging) Then
        If Not _isDragMode Then
            '-- Control discovery only when we really need it
            If _dragListBox Is Nothing Then
                _dragListBox = FindListBoxContainer(e.OriginalSource)
            End If
            _isDragMode = IsMouseDown()
            If _isDragMode Then
                ...
                '-- Only allow DragDrop operation if 
                If _DragDropToolTip IsNot Nothing Then
                    ...
                    '-- Only wire up the parent mouse events
                    '   if/when we require them to reduce CPU usage
                    AddHandler _Host.MouseMove, AddressOf HostContainerMouseMove
                    AddHandler _Host.MouseLeftButtonUp, _
                               AddressOf HostContainerMouseLeftButtonUp
                End If
            End If
        End If
    End If
End Sub

'-- Stop Drag
Private Sub HostContainerMouseLeftButtonUp(Sender As Object, e As MouseEventArgs)
    ...
    '-- Cleanup after Drag/Drop operation
    _isDragMode = IsMouseDown()
    If _isDragging Then
        ...
        '-- Don't need to keep listening to parent mouse event. Free up resources
        RemoveHandler _Host.MouseMove, AddressOf HostContainerMouseMove
        RemoveHandler _Host.MouseLeftButtonUp, _
                      AddressOf HostContainerMouseLeftButtonUp
    End If
End Sub

A potential issue when you hook up several listboxes for drag drop operation (capturing the MouseDown with CaptureMouse during a MouseMove event of a ListBox) using a single ListDragDropHelper class is that more than one ListBox will fire a drag/drop operation. To overcome this, we have a shared (static) variable that tracks if we are in a drag/drop operation or not. This ensures that only one instance of the ListDragDropHelper class is active at any time during a drag/drop operation.

'-- Prevent Race condition with multiple handlers
Private Shared _isBusy As Boolean = False

...

'-- Start Drag
Private Sub ChildContainerMouseMove(e As MouseEventArgs)
    '-- Are we about to Drag?
    If (Not _isBusy) AndAlso (Not _isDragging) Then
    ...
    End If
End Sub

'-- Main Drag/Hover Logic
Private Sub HostContainerMouseMove(Sender As Object, e As MouseEventArgs)
    '-- Dragging
    If _isDragMode Then
    ...
        If Not _isDragging Then '-- Initiate Drag/Drop operation
            ...
            '-- Set Local and global dragging states - 
            '       only one local can be active at any time
            _isDragging = (_isBusy = False)
            If _isDragging Then _isBusy = _isDragging
            ...
        End If
    End If
End Sub

'-- Stop Drag
Private Sub HostContainerMouseLeftButtonUp(Sender As Object, e As MouseEventArgs)
    ...
    '-- Cleanup after Drag/Drop operation
    _isDragMode = IsMouseDown()
    If _isDragging Then
        _isBusy = False
       ...
    End If

End Sub

The DragDropToolTip control is displayed during a drag/drop operation; however, the control can be placed anywhere in the XAML code. Rather than force a specific order of placement, we control the Z-Index from code. The Canvas control has a Z-Index setter that makes it easy to bring a control to the front. For other panel controls like the Grid control, the Z-Index setter does not exist. The DragDropToolTip control normally is hidden when not in use. So to overcome the lack of an Z-Index setter for panel controls like the Grid control, we change the position of the DragDropToolTip control in the panel control's children - effectively bringing the DragDropToolTip control to the front.

...
If _DragDropToolTip IsNot Nothing Then
    '-- Bring to front _DragDropToolTip (DragTip)
    With CType(_DragDropToolTip.Parent, Panel).Children
        '-- only Canvas has ZIndex Setter.
        '   So we need to reorder the controls manually just in case
        '   the DragTip is not on top...
        .Remove(_DragDropToolTip)
        .Add(_DragDropToolTip)
    End With
    ...
End If
...

The last thing we need to do for the ListDragDropHelper class is to expose a property for the ListBox MouseMove Trigger to bind to.

#Region "Relay Commands"

    '-- App View Event Hook
    Public Property ChildMoveCommand() As RelayCommand(Of MouseEventArgs)

    Private Sub _InitCommands()
        ChildMoveCommand = New RelayCommand(Of MouseEventArgs)(_
                               Sub(e) ChildContainerMouseMove(e))
    End Sub

#End Region

Configuring a ViewModel for ListBox Drag/Drop

To enable the View to talk to the ListDragDropHelper class, we need to expose a property for each ListBox that can initiate a drag/drop operation.

#Region "View Binding Properties"

    '-- Drag Drop Services
    Property ProductsDragDropHelper As ListDragDropHelper
    Property TagsDragDropHelper As ListDragDropHelper
    Property GroupsDragDropHelper As ListDragDropHelper

#End Region

#Region "Constructor"

    Sub New()

        '-- Wiring up Drag Drop Services
        ProductsDragDropHelper = New ListDragDropHelper("DragDropToolTip", _
           Function(s, d, fb) HandleHover(s, d, fb), Sub(s, d) HandleDrop(s, d))
        TagsDragDropHelper = New ListDragDropHelper("DragDropToolTip", _
           Function(s, d, fb) HandleHover(s, d, fb), Sub(s, d) HandleDrop(s, d))
        GroupsDragDropHelper = New ListDragDropHelper("DragDropToolTip", _
           Function(s, d, fb) HandleHover(s, d, fb), Sub(s, d) HandleDrop(s, d))

    End Sub

#End Region

#Region "Event Handlers: Drag Hover / Drop"

    Private Function HandleHover(SelectedItems As Object,_
                                 DestItem As Object,_
                                 DragFeedback As ListDragDropHelper.FeedbackStates) _
                                 As ListDragDropHelper.FeedbackStates

        '-- Code goes here for controlling DragDropToolTip display
        '   and feedback icon and tip (see below sample code)
        ...

    End Sub

    Private Sub HandleDrop(ByRef SelectedItems As Object, DestItem As Object)

        '-- Code goes here for Processing the Drop Event (see below sample code)
        ...

    End Sub
    
#End Region

Wiring up the XAML

Lastly, we need to set up a trigger to fire the ListBoxes' MouseMove event.

<ListBox SelectionMode="Extended">
    <i:Interaction.Triggers>
        <!-- Trigger for Start Drag (LeftMouseDown does not fire). -->
        <i:EventTrigger EventName="MouseMove">
            <cmd:EventToCommand 
               Command="{Binding DragDropHelper.ChildMoveCommand, Mode=OneWay}"
               PassEventArgsToCommand="True" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ListBox>

Sample Application

The sample code included demonstrates how to use both the Single and Extended selection modes for listboxes. We are using three listboxes: Products, Tags, and Groups. A product can have multiple tags and only one group. The drag/drop operation allows the following rules:

  1. Multiple products can be selected and dragged onto a single tag or group.
  2. Multiple tags can be dragged onto a single product or group - Applying a set of tags to single/multiple products via the group link.
  3. Dragging a single group onto a single product.

Here's the code in the ViewModel that handles the hover event, giving feedback to the user based on the above rules. It identifies who started the operation based on the source's model type (ListBoxItem(s) only) and decides on what is displayed by the destination model type. If no destination model type is found (ListBoxItem only), then the operation must be illegal. By passing the models from the ListDragDropHelper class to the ViewModel, the ViewModel has no knowledge of what or how the models are being displayed in the View.

Private Function HandleHover(SelectedItems As Object,_
        DestItem As Object,_
        DragFeedback As ListDragDropHelper.FeedbackStates) _
        As ListDragDropHelper.FeedbackStates

    '-- Drag View Feedback

    Dim Move As New ListDragDropHelper.FeedbackStates With _
            {.Drag = ListDragDropHelper.DragFeedbackState.Move, 
            .Drop = ListDragDropHelper.DropFeedbackState.Move}
    Dim Allowed As New ListDragDropHelper.FeedbackStates With _
                {.Drag = ListDragDropHelper.DragFeedbackState.Copy, 
                .Drop = ListDragDropHelper.DropFeedbackState.Add}
    Dim NotAllowed As New ListDragDropHelper.FeedbackStates With _
                   {.Drag = ListDragDropHelper.DragFeedbackState.NotAllowed, 
                   .Drop = ListDragDropHelper.DropFeedbackState.None}

    If SelectedItems Is Nothing Then Return NotAllowed

    Dim Items As ObservableCollection(Of Object) = _
              CType(SelectedItems, ObservableCollection(Of Object))
    If Items.Count = 0 Then Return NotAllowed

    Dim SrcType As Type = SelectedItems(0).GetType

    Select Case True

        Case SrcType Is GetType(ProductModel)
            '-- Products require a Destination Tag/group
            If DestItem IsNot Nothing Then
                Select Case True
                    Case DestItem.GetType Is GetType(SummaryTagModel)
                        With Allowed
                            .HoverMessage = "BULK RETAG"
                            .DropMessage = _
                                String.Format("Set '{0}'{2}for '{1} Products'", 
                                   CType(DestItem, SummaryTagModel).Tag, 
                                   SelectedItems.count, 
                                   vbNewLine)
                            '.Drag = ListDragDropHelper.DragFeedbackState.None
                            ' Uncomment if no drag icon is required

                            '.Drop = ListDragDropHelper.DropFeedbackState.None
                            ' Uncomment if no drop tooltip is required
                        End With
                        Return Allowed

                    Case DestItem.GetType Is GetType(SummaryGroupModel)
                        With Allowed
                            .HoverMessage = "CHANGE GROUP"
                            .DropMessage = _
                              String.Format("Set '{0}'{2}for '{1} Products'", 
                                 CType(DestItem, SummaryGroupModel).title, 
                                 SelectedItems.count, 
                                 vbNewLine)
                        End With
                        Return Allowed

                    Case DestItem.GetType Is GetType(ProductModel)
                        With Move
                            .HoverMessage = "SELECT"
                            .DropMessage = _
                              String.Format("Drag '{0} Products' to a " & _ 
                                 "Tag to{1}Bulk Tag Change or{1}to a Group...", 
                                 SelectedItems.count, 
                                 vbNewLine)
                        End With
                        Return Move

                End Select

            End If

        Case SrcType Is GetType(SummaryTagModel)
            '-- Tags require a Destination product/group
            If DestItem IsNot Nothing Then
                Select Case True

                    Case DestItem.GetType Is GetType(ProductModel)
                        With Allowed
                            .HoverMessage = "BULK TAG"
                            .DropMessage = _
                                String.Format("Set '{0}'{2}for '{1} Tags'", 
                                    CType(DestItem, ProductModel).title, 
                                    SelectedItems.count, 
                                    vbNewLine)
                        End With
                        Return Allowed

                    Case DestItem.GetType Is GetType(SummaryTagModel)
                        With Move
                            .HoverMessage = "SELECT"
                            .DropMessage = String.Format("Drag '{0}' to a " & _ 
                              "product {1}to set the Tag{1}or " & _ 
                              "to a Group to Bulk change Groups...", 
                              CType(SelectedItems(0), SummaryTagModel).Tag, 
                              vbNewLine)
                        End With
                        Return Move

                    Case DestItem.GetType Is GetType(SummaryGroupModel)
                        With Allowed
                            .HoverMessage = "BULK CHANGE GROUPS"
                            .DropMessage = _
                                 String.Format("Set '{0}'{2}for '{1}' Products", 
                                 CType(DestItem, SummaryGroupModel).title, 
                                 CType(SelectedItems(0), SummaryTagModel).num_products, 
                                 vbNewLine)
                        End With
                        Return Allowed

                End Select

            End If

        Case SrcType Is GetType(SummaryGroupModel)
            '-- Groups require a Destination Product
            If DestItem IsNot Nothing Then
                Select Case True
                    Case DestItem.GetType Is GetType(ProductModel)
                        With Allowed
                            .HoverMessage = "CHANGE GROUP"
                            .DropMessage = _
                               String.Format("Set '{0}'{2}for '{1}' Products", 
                                 CType(SelectedItems(0), SummaryGroupModel).title, 
                                 CType(DestItem, ProductModel).title, 
                                 vbNewLine)
                        End With
                        Return Allowed

                    Case DestItem.GetType Is GetType(SummaryGroupModel)
                        With Move
                            .HoverMessage = "SELECT"
                            .DropMessage = _
                              String.Format("Drag '{0}' to a product " & _ 
                              "{1}to set the Group...", _
                              CType(SelectedItems(0), SummaryGroupModel).title, 
                              vbNewLine)
                        End With
                        Return Move

                End Select

            End If

    End Select

    Return NotAllowed

End Function

And below is the code that handles the drop operation. Again, the ListDragDropHelper class only passes the model data to the ViewModel.

Private Sub HandleDrop(ByRef SelectedItems As Object, DestItem As Object)

    '-- Drop Data handler

    If SelectedItems Is Nothing Then Return

    '-- Reset trackers
    ProductsNoTagRoom.Clear()
    ProductsUpdated.Clear()
    ProductsSkipped.Clear()

    Dim Items As ObservableCollection(Of Object) = _
        CType(SelectedItems, ObservableCollection(Of Object))
    If Items.Count = 0 Then Return

    Dim SrcType As Type = SelectedItems(0).GetType

    Select Case True

        Case SrcType Is GetType(ProductModel)
            '-- Products require a Destination Tag/group
            If DestItem IsNot Nothing Then
                ProcessProductMove(Items, DestItem)
                Return
            End If

        Case SrcType Is GetType(SummaryTagModel)
            '-- Tags require a Destination product/group
            If DestItem IsNot Nothing Then
                ProcessTagsMove(Items, DestItem)
            End If

        Case SrcType Is GetType(SummaryGroupModel)
            '-- Groups require a Destination Tag/group
            If DestItem IsNot Nothing Then
                ProcessGroupMove(Items, DestItem)
            End If

    End Select

    Return

End Sub

Private Sub ProcessProductMove(SelectedItems As _
            ObservableCollection(Of Object), DestItem As Object)

    Select Case True
        Case DestItem.GetType Is GetType(SummaryTagModel)

            Dim DestTag As TagModel = CType(DestItem, TagModel)

            For Each item As ProductModel In SelectedItems

                Dim product As ProductModel = _
                    Products.Where(Function(l) l.product_id = _
                    item.product_id).SingleOrDefault
                Dim Tag As TagModel = product.tags.Where(Function(t) t.Tag = _
                        DestTag.Tag).SingleOrDefault
                If Tag Is Nothing Then
                    If product.tags.Count > 10 Then
                        '-- too many
                        ProductsNoTagRoom.Add(product)
                    Else
                        '-- we have room...
                        product.tags.Add(DestTag)
                        ProductsUpdated.Add(product)
                    End If
                Else
                    '-- already set
                    ProductsSkipped.Add(product)
                End If
            Next

            '-- Refresh
            UpdateTagSumaryList(New ObservableCollection(Of Object) From {DestItem})

        Case DestItem.GetType Is GetType(SummaryGroupModel)

            Dim Destgroup As GroupModel = CType(DestItem, GroupModel)

            For Each item As ProductModel In SelectedItems

                Dim product As ProductModel = _
                    Products.Where(Function(l) l.product_id = _
                    item.product_id).SingleOrDefault

                If product.group.group_id = Destgroup.group_id Then
                    '-- already set
                    ProductsSkipped.Add(product)
                Else
                    '-- Ok to change...
                    product.group = Destgroup
                    ProductsUpdated.Add(product)
                End If
            Next

            '-- Refresh
            UpdateGroupSumaryList(Destgroup)

        Case Else
            '-- Invalid ListBox - Shouldn't get here!

    End Select

    Return

End Sub

Private Sub ProcessTagsMove(SelectedItems As _
        ObservableCollection(Of Object), DestItem As Object)

    Select Case True
        Case DestItem.GetType Is GetType(ProductModel)

            For Each item As TagModel In SelectedItems

                Dim product As ProductModel = 
                  Products.Where(Function(l) l.product_id = 
                  CType(DestItem, ProductModel).product_id).SingleOrDefault
                Dim Tag As TagModel = 
                  product.tags.Where(Function(t) t.Tag = item.Tag).SingleOrDefault
                If Tag Is Nothing Then
                    If product.tags.Count > 10 Then
                        '-- too many
                        ProductsNoTagRoom.Add(product)
                    Else
                        '-- we have room...
                        product.tags.Add(item)
                        ProductsUpdated.Add(product)
                    End If
                Else
                    '-- already set
                    ProductsSkipped.Add(product)
                End If
            Next

            '-- Refresh
            UpdateTagSumaryList(SelectedItems)

        Case DestItem.GetType Is GetType(SummaryGroupModel)

            Dim Destgroup As GroupModel = CType(DestItem, GroupModel)

            For Each item As TagModel In SelectedItems

                For Each product As ProductModel In Products

                    If (From t As TagModel In product.tags _
                             Where t.Tag = item.Tag).Count Then
                        If product.group.group_id = Destgroup.group_id Then
                            '-- already set
                            ProductsSkipped.Add(product)
                        Else
                            '-- Ok to change...
                            product.group = Destgroup
                            ProductsUpdated.Add(product)
                        End If
                    End If
                Next

            Next

            '-- Refresh
            UpdateGroupSumaryList(Destgroup)

        Case Else
            '-- Invalid ListBox - Shouldn't get here!

    End Select

    Return

End Sub

Private Sub ProcessGroupMove(SelectedItems As _
        ObservableCollection(Of Object), DestItem As Object)

    If DestItem.GetType Is GetType(ProductModel) Then

        Dim srcGroup As SummaryGroupModel = _
            CType(SelectedItems(0), SummaryGroupModel)
        Dim product As ProductModel = _
            Products.Where(Function(l) l.product_id = _
            CType(DestItem, ProductModel).product_id).SingleOrDefault

        If product.group.group_id = srcGroup.group_id Then
            '-- already set
            ProductsSkipped.Add(product)
        Else
            '-- Ok to change...
            product.group = srcGroup
            ProductsUpdated.Add(product)
        End If

        '-- Refresh
        UpdateGroupSumaryList(SelectedItems(0))

    End If

End Sub

The sample project has no conflict or mapping resolution for tags that either are already used or exceeds the number allowed. I'll leave this exercise to you.

I have however added a feedback control to the main form that shows the results of the drag/drop operation. Here's the code that updates the SummaryTagModel and SummaryGroupModel collections to refresh the tag and group listboxes. The code below only updates those that have changes rather than update the entire lists. The feedback control is bound to these collections and reflects any changes made.

'-- Tag Drop Management
ProductsNoTagRoom = New ObservableCollection(Of ProductModel)
ProductsUpdated = New ObservableCollection(Of ProductModel)
ProductsSkipped = New ObservableCollection(Of ProductModel)

...

Private Sub UpdateTagSumaryList(Items As ObservableCollection(Of Object))

    Dim tmpTags As ObservableCollection(Of SummaryTagModel) = _
                   MockDataProvider.GetTags(Products)

    '-- Only update changes
    For Each tag As SummaryTagModel In Items
        Dim destTag As SummaryTagModel = _
            Tags.Where(Function(t) t.Tag = tag.Tag).SingleOrDefault
        destTag.SetData(tmpTags.Where(Function(t) t.Tag = _
                        tag.Tag).SingleOrDefault)
    Next

End Sub

Private Sub UpdateGroupSumaryList(Item As SummaryGroupModel)

    '-- Only update change
    Dim tmpGroups As ObservableCollection(Of SummaryGroupModel) = _
        MockDataProvider.GetGroups(Products)
    Dim destSection As SummaryGroupModel = Groups.Where(Function(ss) _
                    ss.group_id = Item.group_id).SingleOrDefault
    destSection.SetData(tmpGroups.Where(Function(ss) ss.group_id = _
                destSection.group_id).SingleOrDefault)

End Sub

The ListDragDropHelper class can also support reordering a listbox and other operations; however, these are not included in the sample application. Reordering a listbox, for example, is straightforward - SelectedItems in the HandleDrop method would be the item(s) to be moved and the DestItem would be the new position. Remove the SelectedItems, and .InsertAt(x) the destItem index. To move the SelectedItems to the end of the list, the destItem would be nothing. Use .Add(x) instead of .InsertAt(x).

Special Mention

The icons used in the DragDropToolTip control were originally from the Silverlight Toolkit on CodePlex.

Final Comments

This is my first submission and I hope that you've found it useful.

History

  • 11th April 2011 - Initial release.

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