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 Selector
s 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 SelectionArea
s, 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
.
<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 Point
s 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 SelectionArea
s, 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 SelectionArea
s. 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
.
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
.
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
- 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. - 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. - The whole lasso arranging mechanism doesn't feel very solid. Looking for a better alternative.
Next
ItemsControl
s are all about data. And data has to be exchanged. In the next article, we will talk about drag and drop between data bounded ItemsControl
s.
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
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.