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

Developing a MultiSelector

0.00/5 (No votes)
3 Dec 2009 1  
A MultiSelector-derived control that supports multiple selection modes, scoped selection, customizable lasso-tool and selection logic

Introduction

.NET 3.5 SP1 introduced the MultiSelector class. The main benefit of having this primitive is that it exposes enough Selector internals for someone to actually be able to write a decent control that knows how to handle customized selection, without subclassing ListBox and deal with the heritage. The SelectionArea is such a control with a couple of enhancements that might come in handy in some cases. Firstly, let's briefly review the main features and components involved, and then we'll analyze the code.

The Item Container

The SelectionAreaItem is a very simple class. It derives from ContentControl and adds itself as owner for the Selector.SelectedEvent and Selector.UnselectedEvent routed events, and Selector.IsSelectedProperty property, respectively. When this property changes, one of the two events is raised, as appropriate.

It also overrides the OnMouseLeftButtonDown method, in order to inform its containing parent that it has been clicked, by calling an internal method of SelectionArea (the selection logic will be handled there).

internal SelectionArea ParentSelectionArea
{
   get
   {
      return ItemsControl.ItemsControlFromItemContainer(this) as SelectionArea;
   }
}

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
   base.OnMouseLeftButtonDown(e);

   var parent = ParentSelectionArea;

   if (parent != null)
   {
      parent.NotifyItemClicked(this);
   }

   e.Handled = true;
}

Selection Modes

There are four types of selection that SelectionArea can handle. These are defined by the SelectionType enum:

public enum SelectionType
{
   Single,
   Multiple,
   Extended,
   Lasso
}

The first three values are borrowed from SelectionMode (used by ListBox), and operate almost the same, with the exception that Extended mode doesn't handle the Shift modifier (for selecting consecutive items). In this article, we will not insist too much on these three modes, as they are very clear, both in usage and implementation. The fourth value enables you to select items using a lasso. This is the default value.

public static readonly DependencyProperty SelectionTypeProperty =
   DependencyProperty.Register(
      "SelectionType",
      typeof(SelectionType),
      typeof(SelectionArea),
      new PropertyMetadata(SelectionType.Lasso));

We will say that we are performing a 'direct-click selection' when selecting items by clicking on item containers, as opposed to 'lasso selection' - selecting items by capturing them using a lasso. The first three modes support only direct-click selection. The Lasso mode supports both. When doing direct-click selection, the Lasso mode functions exactly like the Extended mode. The direct-click selection logic is covered by the NotifyItemClicked method. The actual lasso selection is triggered when clicking over an empty area (that is, directly over the ItemsPanel), (optionally) moving the mouse while holding the button pressed and then release it. The OnMouseLeftButtonDown, OnMouseMove and OnMouseLeftButtonUp methods are overridden in order to handle this type of selection.

Scoped Selection

Sometimes you may require two or more Selectors to work together as a single unit, so that only one of them can have selected items at a given moment - you want selection to be considered per the entire unit. This is achieved in SelectionArea by using scopes. A scope is a set of SelectionAreas, where any two of them cannot have both selected items at the same time. When using scopes, depending on the SelectionType, selection can behave in different ways:

  • For non-additive* modes and also for additive modes with Ctrl not pressed, when an item is selected inside an area, if there exists another area that has selected items in the same scope, that area is cleared (all the selected items are unselected)
  • For additive modes with Ctrl pressed, if an area has at least one selected item, you can only select inside that area; selection in another area in the same scope will not be possible
* We will call 'additive' those types of selection that "add-up" items when Ctrl is pressed (select previously unselected items and keep the currently selected ones); in this respect, the Extended and Lasso modes are additive; conversely, Single and Multiple modes are non-additive.

Scoped selection can be activated by setting UseScopedSelection to true and specifying a Scope (this property has a default value of "Scope", so if you only have one scope you don't have to set it, unless you want to give a more significant denomination).

public static readonly DependencyProperty UseScopedSelectionProperty =
   DependencyProperty.Register(
      "UseScopedSelection",
      typeof(bool),
      typeof(SelectionArea));

public static readonly DependencyProperty ScopeProperty =
   DependencyProperty.Register(
      "Scope",
      typeof(string),
      typeof(SelectionArea),
      new PropertyMetadata("Scope", OnScopeChanged));

Direct-click Selection

When an item container is clicked, the SelectionAreaItem handles the event and notifies its parent by calling NotifyItemClicked.

internal void NotifyItemClicked(SelectionAreaItem item)
{
  clickedItem = null;
  
  switch (SelectionType)
  {
     case SelectionType.Single:
        if (!item.IsSelected)
        {
           ClearTargetArea();
           Select(item);
        }
        else if (IsControlKeyPressed)
        {
           Unselect(item);
        }
        break;
     case SelectionType.Multiple:
        if (UseScopedSelection && FocusedArea != this)
        {
           ClearTargetArea();
        }
        ToggleSelect(item);
        break;
     case SelectionType.Extended:
     case SelectionType.Lasso:
        if (!CanPerformSelection)
        {
           return;
        }

        if (IsControlKeyPressed)
        {
           ToggleSelect(item);
        }
        else if (!item.IsSelected)
        {
           ClearTargetArea();
           Select(item);
        }
        else
        {
           clickedItem = item;
        }
     break;
  }

  FocusedArea = this;
  Mouse.Capture(this);
}

The first two cases are self-explanatory. Single admits one item selected at a time. Multiple toggle-selects items as you click them. When using scoped selection, these conditions must be conjugated with the restriction that only one SelectionArea in a given scope can have selected items. The ClearTargetArea method unselects all the items in the 'target area'. This target area is either the current one (the parent of the clicked item) or the focused area, when using scoped selection (the area within the same scope as the current one upon which the last successful selection has been performed - which may or may not be the current one).

private void ClearTargetArea()
{
   var area = UseScopedSelection ? FocusedArea : this;

   if (area != null)
   {
      area.UnselectAll();
   }
}

When working with additive modes, the selection cannot always be performed. Specifically, when using scoped selection and Ctrl is pressed: in this case, you can only select items within the focused area. So either this area is the current one, either it does not have selected items (either it is null, which is the case when no selection has been made in this scope before). In any other situation, the selection is not permitted.

private bool IsControlKeyPressed
{
   get
   {
      return (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;
   }
}

private bool CanPerformSelection
{
   get
   {
      return !UseScopedSelection ||
             !IsControlKeyPressed ||
             FocusedArea == null ||
             FocusedArea == this ||
             FocusedArea.SelectedItems.Count == 0;
   }
}

When an item that is already selected is clicked and Ctrl is not pressed, a reference to this item is saved. We need this because, in SelectionArea the additive modes do not unselect the rest of the items at mouse down; they will be unselected at mouse up, so we need to know which item not to unselect. The logic behind this decision is that we may want to do something with (all) the selected items at mouse move (like moving them around using an attached behaviour, or whatever).

If a selection (or un-selection) is successfully performed, the current area becomes the focused one within its scope, if scoping is used.

Lasso Selection

In Lasso mode, if you click over the space between item containers, rather than directly on them, a lasso selection is started. The lasso is rendered using the AdornerLayer, so an AdornerDecorator is required (make sure you explicitly put one in your visual tree if you happen to redesign your top control's template). An exception will actually be thrown at initialization if an AdornerLayer object cannot be obtained. How the lasso actually looks and behaves is not built into the control. The user can customize these aspects by setting two properties: the LassoTemplate and the LassoGeometry.

The LassoTemplate defines how the lasso looks. Internally, the SelectionArea uses a TemplatedAdorner object to draw the lasso. The TemplatedAdorner class is very simple, we will not further detail the code: it has a single Control visual child; the SelectionArea supplies it with a template for this child - the LassoTemplate set by the user - and a Rect for arranging it.

public static readonly DependencyProperty LassoTemplateProperty =
   DependencyProperty.Register(
      "LassoTemplate",
      typeof(ControlTemplate),
      typeof(SelectionArea),
      new PropertyMetadata(OnLassoTemplateChanged));

private static void OnLassoTemplateChanged
	(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
   (target as SelectionArea).adorner.Refresh(e.NewValue as ControlTemplate);
}

When the LassoTemplate changes, the adorner updates its visual child with the new template.

The LassoGeometry defines the selection logic. It is a Geometry object used to hit test the items at mouse up. Any item that is fully inside or intersected by this geometry gets selected. This provides quite a lot of flexibility, allowing you to select the items in different ways. The most used type is the classic rectangular selection, but you can specify more "exotic" selection behaviours, if you so desire - any geometry will do.

public static DependencyProperty LassoGeometryProperty =
   DependencyProperty.Register(
      "LassoGeometry",
      typeof(Geometry),
      typeof(SelectionArea),
      new PropertyMetadata(OnLassoGeometryChanged));

private static void OnLassoGeometryChanged
	(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
   var area = target as SelectionArea;
   var geometry = e.NewValue as Geometry;

   if (area.LassoArrangeType == LassoArrangeType.LassoBounds && geometry != null)
   {
      area.adorner.Refresh(geometry.Bounds);
   }
}

The lasso template and geometry will most commonly depend on the mouse movement. In order to accommodate this fact, SelectionArea exposes two read-only DependencyProperties of type Point: StartPosition (which retains the position relative to the SelectionArea at the time when the left button was pressed) and CurrentPosition (which keeps track of the current position and is updated each time the mouse moves while the button is pressed). These two cover most of the needs when customizing the lasso tool.

The lasso adorner can be arranged in two ways: inside the Bounds of the LassoGeometry or inside the Bounds of the geometry that defines the clip region of the SelectionArea. You can select the type of arranging by setting the LassoArrangeType property to one of these values:

public enum LassoArrangeType
{
   LassoBounds,
   ClipBounds
}

If your template depends on StartPosition and/or CurrentPosition, you have to use ClipBounds for arranging - it makes sense to arrange relative to the SelectionArea, since the template depends on positions that are relative to it. If the template is not dependant on the SelectionArea in terms of geometric positioning, use LassoBounds - in this case the template will have to be designed in such a way that it changes appearance when the bounds for arrangement change, provided that you want a fluid lasso. You could have a static template that does not depend on mouse movement at all (don't bind it to any mouse-related position property and use ClipBounds to arrange it), but that wouldn't be very interesting.

public static readonly DependencyProperty LassoArrangeTypeProperty =
   DependencyProperty.Register(
      "LassoArrangeType",
      typeof(LassoArrangeType),
      typeof(SelectionArea),
      new PropertyMetadata(OnLassoArrangeTypeChanged));

private static void OnLassoArrangeTypeChanged
	(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
   var area = target as SelectionArea;
   Geometry geometry = null;
   Transform transform = null;

   switch ((LassoArrangeType)e.NewValue)
   {
      case LassoArrangeType.LassoBounds:
         geometry = area.LassoGeometry;
         break;
      case LassoArrangeType.ClipBounds:
         geometry = area.clipGeometry;

         if (geometry != null)
         {
            transform = (Transform)geometry.Transform.Inverse;
         }
         break;
      default:
         break;
   }

   if (geometry != null)
   {
      area.adorner.Refresh(geometry.Bounds, transform);
   }
}

Besides setting the Rect for arranging, when using ClipBounds we pass the adorner a Transform to apply to its child. That's because we are using positions relative to the SelectionArea, but the lasso is rendered on the AdornerLayer, so we need a transform to translate the coordinates. This transform is already computed, indirectly: the AdornerLayer object associated with a SelectionArea is clipped in order for the lasso not to fall outside the perimeter of the SelectionArea. The clip geometry is recomputed each time the area's size changes.

protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
   base.OnRenderSizeChanged(sizeInfo);

   clipGeometry = new RectangleGeometry(VisualTreeHelper.GetDescendantBounds(this));
   clipGeometry.Transform = (Transform)TransformToVisual(adornerLayer);
   clipGeometry.Freeze();

   if (LassoArrangeType == LassoArrangeType.ClipBounds)
   {
      adorner.Refresh(clipGeometry.Bounds, (Transform)clipGeometry.Transform.Inverse);
   }
}

We apply a transform to the clip geometry to translate coordinates from the SelectionArea to the AdornerLayer. For the lasso drawing, we need exactly the reversed effect: translate coordinates from AdornerLayer to SelectionArea, so we take the inverse of this transform and pass it to the adorner.

It's helpful to always have in mind this bounds inside of which the lasso adorner is arranged. If you want to break outside them, you can always apply transforms to your elements in the LassoTemplate, but be careful when doing so: if you have a visual representation for the lasso tool (as we will see shortly, this is not mandatory), it's natural to expect consistency between the geometric representation you see on screen and the geometric logic used for selection.

Here are the methods that handle the lasso selection:

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
   base.OnMouseLeftButtonDown(e);

   switch (SelectionType)
   {
      case SelectionType.Single:
      case SelectionType.Multiple:
      case SelectionType.Extended:
         break;
      case SelectionType.Lasso:
         if (!CanPerformSelection)
         {
            return;
         }

         if (!IsControlKeyPressed)
         {
            ClearTargetArea();
         }

         StartPosition = Mouse.GetPosition(this);
         CurrentPosition = StartPosition;
         
         clickedItem = null;
         FocusedArea = this;

         adornerLayer.Clip = clipGeometry;
         adornerLayer.Add(adorner);

         isMouseDown = true;
         e.Handled = true;

         Mouse.Capture(this);
         break;
      default:
         break;
   }
}

The lasso selection falls under the same scoping restriction we talked about earlier, same as direct-click selection. So if the selection cannot be performed, we simply return. If Ctrl is not pressed, we clear the target area. Otherwise, we leave the selected items untouched and the newly selected items via the geometry hit testing, if any, will be added to the result set. We also set the initial mouse position and clip the AdornerLayer object so the lasso stays inside the perimeter of the current SelectionArea.

protected override void OnMouseMove(MouseEventArgs e)
{
   base.OnMouseMove(e);

   if (!isMouseDown)
   {
      return;
   }

   CurrentPosition = Mouse.GetPosition(this);
   e.Handled = true;
}

When the mouse moves, we check if a valid lasso selection has been started. If so, we update the CurrentPosition. If your lasso template or geometry depends on the CurrentPosition, the adorner will be updated as well. This is done in the call-back handlers for the corresponding properties, as seen previously.

The Hit Testing

The selection logic is performed on mouse up.

protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
   base.OnMouseLeftButtonUp(e);
   Mouse.Capture(null);

   if (clickedItem != null)
   {
      UnselectAllExceptThisItem(clickedItem);
   }

   if (!isMouseDown)
   {
      return;
   }

   SelectionAreaItem item = null;

   if (LassoGeometry != null)
   {
      var capturedItems = new List<SelectionAreaItem>();

      VisualTreeHelper.HitTest(this,
         a =>
         {
            item = a as SelectionAreaItem;

            if (item != null && item.ParentSelectionArea == this)
            {
               return HitTestFilterBehavior.ContinueSkipChildren;
            }

            return HitTestFilterBehavior.ContinueSkipSelf;
         },
         a =>
         {
            switch (((GeometryHitTestResult)a).IntersectionDetail)
            {
               case IntersectionDetail.FullyInside:
               case IntersectionDetail.Intersects:
                  capturedItems.Add(a.VisualHit as SelectionAreaItem);
                  break;
            }

            return HitTestResultBehavior.Continue;
         },
         new GeometryHitTestParameters(LassoGeometry));

      Select(capturedItems);
   }

   if (SelectedItems.Count == 0)
   {
      item = ItemsControl.ContainerFromElement(null, this) as SelectionAreaItem;

      if (item != null)
      {
         var area = item.ParentSelectionArea;

         if (area != null)
         {
            area.NotifyItemClicked(item);
         }
      }
   }

   StartPosition = new Point(double.NegativeInfinity, double.NegativeInfinity);
   CurrentPosition = StartPosition;

   isMouseDown = false;
   adornerLayer.Remove(adorner);
}

If we clicked on an already selected item, without holding the Ctrl down, at mouse up we unselect all the other selected items except the clicked one. Otherwise, if a valid lasso selection was started, we make use of VisualTreeHelper.HitTest method and LassoGeometry to hit test the items. If after the hit testing we didn't select any item (and none was previously selected), we check to see if the SelectionArea is not part of the VisualTree of a SelectionAreaItem (in case of nested areas). If so, we just perform a usual direct-click selection on that item.

The hit testing deserves a little bit of attention. If you ever debugged a hit testing method against an arbitrary visual tree, the jumping back and forward between the filter call-back and the result call-back can get very confusing, unless you understand one very important rule: always override the HitTestCore method if you want your control to play well with VisualTreeHelper.HitTest. Your control will never make it to the result call-back if you don't. This is how it's implemented in SelectionAreaItem:

protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
   if (VisualTreeHelper.GetDescendantBounds(this).Contains(hitTestParameters.HitPoint))
   {
      return new PointHitTestResult(this, hitTestParameters.HitPoint);
   }

   return null;
}

protected override GeometryHitTestResult HitTestCore
		(GeometryHitTestParameters hitTestParameters)
{
   var geometry = new RectangleGeometry(VisualTreeHelper.GetDescendantBounds(this));
   return new GeometryHitTestResult
	(this, geometry.FillContainsWithDetail(hitTestParameters.HitGeometry));
}

Also, make sure you understand the difference between a Geometry and its Bounds. The two are geometrically equivalent only in the case of the RectangleGeometry. The bounds of a geometry is a Rect object large enough to fully contain that geometry, but the geometry can have an irregular shape. In the filter call-back, you get all the visuals that are inside the bounds of the geometry. At this point you can filter out visuals, in order to make hit testing faster, by pruning the visual tree. In SelectionArea we keep only the item containers of the current area, and do not go any deeper - but these are not the items that will be selected. Again, these merely fall within the bounds of the geometry, but it does not necessarily mean that they are intersected or contained by the geometry itself. This test is done in the result call-back, and this is why it is crucial to override the HitTestCore method.

Examples

Here are some snapshots of what you can do, and how to do it:

Elliptical selection over a Canvas as ItemsPanel.
Selection using an ellipse as lasso and an EllipseGeometry for hit testing

Selection using an ellipse as lasso and an EllipseGeometry for hit testing

<c:SelectionArea LassoArrangeType="LassoBounds">
...
   <c:SelectionArea.LassoTemplate>
     <ControlTemplate>
       <Ellipse
         Stroke="Gray"
         StrokeThickness="1"
         StrokeDashedArray="{Binding Source={x:Static DashStyles.Dash},
				Path=Dashes, Mode=OneTime}" />
     </ControlTemplate>
   </c:SelectionArea.LassoTemplate>

   <c:SelectionArea.LassoGeometry>
     <MultiBinding Converter="{c:EllipseGeometryConverter}">
       <Binding RelativeSource="{RelativeSource Self}" Path="StartPosition" />
       <Binding RelativeSource="{RelativeSource Self}" Path="CurrentPosition" />
     </MultiBinding>
   </c:SelectionArea.LassoGeometry>
</c:SelectionArea>

The LassoTemplate is just a gray Ellipse with a dashed stroke. For the LassoGeometry we use an IMultiValueConverter that returns an EllipseGeometry based on the two Points exposed by the SelectionArea.

public sealed class EllipseGeometryConverter : MarkupExtension, IMultiValueConverter
{
   public override object ProvideValue(IServiceProvider serviceProvider)
   {
      return this;
   }

   public object Convert(object[] values, Type targetType,
		object parameter, CultureInfo culture)
   {
      return new EllipseGeometry(new Rect((Point)values[0], (Point)values[1]));
   }

   public object[] ConvertBack(object value, Type[] targetTypes,
		object parameter, CultureInfo culture)
   {
      throw new NotImplementedException();
   }
}

That's it. You can define the selection logic in any way you want. The code for a rectangular selection is identical; just replace Ellipse with Rectangle and EllipseGeometry with RectangleGeometry.

These examples use data-bounded SelectionAreas, with two types of data items: type 1 has a simple TextBlock as DataTemplate, and type 2 has a SelectionArea as DataTemplate. Type 2 can have children of type 1 or 2, etc., so we can have nested SelectionAreas. When an item gets selected, its border's colour changes to red (in a real app you might want to do something more interesting than that).

You do not necessarily have to provide a ControlTemplate for the lasso in order to select items. In the example below, the template has been removed. The lasso geometry is a GeometryGroup composed of two LineGeometries - a vertical line and a horizontal line, their point of intersection being the CurrentPosition. Just click over an empty spot and any item intersected by one of them gets selected.

Single-point selection, with no ControlTemplate for the lasso, over a StackPanel as ItemsPanel.
Selection using a single click and a GeometryGroup for hit testing

Selection using a single click and a GeometryGroup for hit testing

In the elliptical selection we saw earlier, the ellipse is inscribed inside the rectangle defined by the StartPosition and CurrentPosition. Its center, major radius and minor radius are constantly changing as we move the mouse. We may want its center to be fixed and the radii to grow or shrink as we get farther or closer to the center. Below is the code for such a scenario, where the center coincides with the StartPosition and the two radii are both equal with the distance between the CurrentPosition and the StartPosition (of course you can set them to be different, for instance the major radius to be twice as the minor radius, or whatever suits you needs).

public sealed class EllipseGeometryConverter : MarkupExtension, IMultiValueConverter
{
   public override object ProvideValue(IServiceProvider serviceProvider)
   {
      return this;
   }

   public object Convert(object[] values, Type targetType,
		object parameter, CultureInfo culture)
   {
      var p1 = (Point)values[0];
      var p2 = (Point)values[1];
      var offset = p2 - p1;

      return new EllipseGeometry(p1, offset.Length, offset.Length);
   }

   public object[] ConvertBack(object value, Type[] targetTypes,
		object parameter, CultureInfo culture)
   {
      throw new NotImplementedException();
   }
}

We can do even better. With a little imagination we can handcraft a free-form selection tool.

Free-form selection over a WrapPanel as ItemsPanel.
Selection using a single click and a GeometryGroup for hit testing

Selection using a single click and a GeometryGroup for hit testing

We simulate the behaviour of an InkCanvas (although not as effective). For this to work, we build two converters: one for the template and one for the geometry. The binding will be set on the CurrentPosition.

public sealed class PolylineConverter : MarkupExtension, IValueConverter
{
   private List<point> points = new List<Point>();

   public override object ProvideValue(System.IServiceProvider serviceProvider)
   {
      return this;
   }

   public object Convert(object value, System.Type targetType,
	object parameter, System.Globalization.CultureInfo culture)
   {
      var p = (Point)value;

      if (p.X != double.NegativeInfinity && p.Y != double.NegativeInfinity)
      {
         points.Add(p);
      }
      else
      {
         points.Clear();
      }

      var template = new ControlTemplate(typeof(Control));
      template.VisualTree = new FrameworkElementFactory(typeof(Polyline));
      template.VisualTree.SetValue(Polyline.StrokeProperty,
			new SolidColorBrush(Colors.Gray));
      template.VisualTree.SetValue(Polyline.StrokeThicknessProperty, 1.0);
      template.VisualTree.SetValue(Polyline.StrokeDashArrayProperty,
			DashStyles.Dash.Dashes);
      template.VisualTree.SetValue(Polyline.PointsProperty, new PointCollection(points));

      return template;
   }

   public object ConvertBack(object value, System.Type targetType,
	object parameter, System.Globalization.CultureInfo culture)
   {
      throw new System.NotImplementedException();
   }
}

We need to store the list of points the mouse encounters as we move it around. With this list, we build a Polyline and set it as the VisualTree of the template. The geometry for hit test is a PathGeometry that adds little segments as we move the mouse:

public sealed class PolylineGeometryConverter : MarkupExtension, IValueConverter
{
   private PathGeometry pathGeometry;
   private PathFigure pathFigure;

   public PolylineGeometryConverter()
   {
      pathGeometry = new PathGeometry();
      pathFigure = new PathFigure();
      pathFigure.IsClosed = true;
      pathGeometry.Figures.Add(pathFigure);
   }

   public override object ProvideValue(System.IServiceProvider serviceProvider)
   {
      return this;
   }

   public object Convert(object value, System.Type targetType,
	object parameter, System.Globalization.CultureInfo culture)
   {
      var p = (Point)value;

      if (p.X != double.NegativeInfinity && p.Y != double.NegativeInfinity)
      {
         var lineSegment = new LineSegment(p, false);

         if (pathFigure.Segments.Count == 0)
         {
            pathFigure.StartPoint = p;
         }

         pathFigure.Segments.Add(lineSegment);
      }
      else
      {
         pathFigure.Segments.Clear();
      }

      return pathGeometry;
   }

   public object ConvertBack(object value, System.Type targetType,
	object parameter, System.Globalization.CultureInfo culture)
   {
      throw new System.NotImplementedException();
   }
}

In the converters above, we use a little "inside" knowledge: initially, and after each mouse up the StartPosition's and CurrentPosition's coordinates are set to double.NegativeInfinity. In this way we know when a selection has ended.

Limitations

  1. Although you can use the SelectionArea with any sort of ItemsPanel and any kind of DataTemplate for the item containers, you will not be able to do lasso selection if the containers have no space between them. You need an "empty spot" in order to do that. If this condition is not met and you don't want to take advantage of scoped selection either, you can do without it. Choose your controls wisely.
  2. When the LassoTemplate changes, the adorner drops its current template and picks up the new one. If you have a converter that returns a new template each time the CurrentPosition changes, like in the example with the two perpendicular lines or with the free-form selection, this switching will happen very often. Having a complex template will have a serious impact on performance in this scenario.
  3. The whole lasso arranging mechanism doesn't feel very solid. Looking for a better alternative.

Next

ItemsControls are all about data. And data has to be exchanged. In the next article, we will talk about drag and drop between data bounded ItemsControls.

Feedback

For any bug reports, suggestions or further improvement ideas, please drop a comment in order to alter the source code accordingly.

History

  • 3rd December, 2009 - Fix: clickedItem is nulled out at mouse down (both in NotifyItemClicked and OnMouseLeftButtonDown methods), instead of mouse up. This is needed because, in case the corresponding tunneling event for mouse up is handled by a third party (like an attached behaviour), you will have a pending unexisting clicked item, so the next direct-click selection will get corrupted and misbehave.
  • 27th November, 2009 - Created the article

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