The AGT (Argentum Tela) series of articles is an effort to do two things. Usually an idea is presented only in its finished form. The first goal is to do some Reality Blogging, to show an idea evolve over time without pulling any punches. The second goal, and the example vehicle for the evolution aspect, is an extensible Design Surface for Silverlight similar to what we have in Visual Studio 2008. This type of application has all sorts of interesting uses. My example is a Home Theater layout tool. Read the entire saga: http://www.damonpayne.com/2008/09/14/RunTimeIsDesignTimeForAGT0.aspx
Rectangular Lasso
I find it incredibly useful to be able to draw a temporary rectangle on a design surface in order to select multiple Controls at once for moving or property editing. I’d like to support this feature in AGT as well; I call it the rectangular lasso. I had a very good notion of how I was going to do this: use a method somewhat similar to how I calculated render size in my resizing code, but for drawing a Rectangle object on my design surface. I’m going to introduce a new convention for these articles: a quick heading to summarize any time that I find I needed to refactor my design to meet current needs or refine thinking. Since refactoring is good, these will be green and easy to see.
Refactor: I made a static class called ControlExtensions and included a Generic Method to be more convenient with returning Dependency Property values. When I tried to use this with the Rectangle code below I realized (duh) that this should not be for Control, but DependencyObject. A DependencyObjectExtensions class has been introduced.
The stage is set when I click on the design surface Canvas.
private void LayoutRoot_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_isSelecting = true;
_selectingRect = new Rectangle();
_selectingRect.StrokeThickness = SELECT_RECT_STROKE_WIDTH;
_selectingRect.RadiusX = SELECT_RECT_RADIUS_X;
_selectingRect.RadiusY = SELECT_REDT_RADIUS_Y;
_selectingRect.Stroke = new SolidColorBrush(Colors.Red);
_selectingRect.Fill = new SolidColorBrush(Colors.Magenta);
Point clickLoc = e.GetPosition(LayoutRoot);
_selectingRect.Opacity = .65;
_selectingRect.SetValue(Canvas.ZIndexProperty, 100);
_selectingRect.SetValue(Canvas.LeftProperty, clickLoc.X);
_selectingRect.SetValue(Canvas.TopProperty, clickLoc.Y);
_selectAnchor = clickLoc;
LayoutRoot.Children.Add(_selectingRect);
LayoutRoot.CaptureMouse();
}
This sets up the Rectangle for resizing. It will be on top of all other controls and transparent so we can see the controls beneath it.
Note: since some Silverlight components cannot be extended, one is likely to be working with a lot of UserControls. I like the convention of keeping the top level content of each UserControl named LayoutRoot regardless of its type, it’s a good name.
We next need to handle the mouse move event as long as the left button is pressed. I have noticed that various programs with a design surface seem to allow resizing only to the east and south, but selecting in either direction. Since this seems to be a convention, I’ll follow it.
private void LayoutRoot_MouseMove(object sender, MouseEventArgs e)
{
_localMousePos = e.GetPosition(this);
if (_isSelecting)//southeast drag
{
double rectLeft = _selectingRect.GetValue<double>(Canvas.LeftProperty);
double rectTop = _selectingRect.GetValue<double>(Canvas.TopProperty);
if (rectLeft < _localMousePos.X)
{
double width = _localMousePos.X - rectLeft;
double height = _localMousePos.Y - rectTop;
_selectingRect.Width = width;
_selectingRect.Height = height;
}
else // northwest drag
{
double width = _selectAnchor.X - _localMousePos.X;
double height = _selectAnchor.Y - _localMousePos.Y;
if (width > 0 && height > 0)//need this safety here in case a resize rectangle "crosses" itself.
{
_selectingRect.Width = width;
_selectingRect.Height = height;
_selectingRect.SetValue(Canvas.LeftProperty, _localMousePos.X);
_selectingRect.SetValue(Canvas.TopProperty, _localMousePos.Y);
}
}
}
}
Refactor: When testing the drawing of the selection Rectangle, I found that when resizing controls on the surface I would also have a tag-along selection rectangle. The DesignSite resizing Border needed to mark the mouse click event has Handled=true in order to prevent this.
Now when the user releases the left mouse button, we destroy the selection Rectangle:
private void LayoutRoot_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_isSelecting = false;
LayoutRoot.ReleaseMouseCapture();
LayoutRoot.Children.Remove(_selectingRect);
_selectingRect = null;
}
This now allows me to lasso in several controls at once like so:

The last aspect of this feature is to hook into the ISelectionService to select any components that are within the Rectangle bounds as I resize the Rectangle. We need to create a SelectLassoComponents method and call it from within the MouseMove event. As soon as this is implemented, I find some new strange behavior. FindElementsInHostCoordinates does not work like I’d hoped. It seems to need a much bigger Rectangle than it appears to visually to call a “hit” for a control:

So, I add logging to see the size of the rectangle I’m drawing in my message window. I see nothing but extremely odd behavior. The code for selecting the lassoed controls uses the VisaulTreeHelper class as follows:
protected virtual void SelectLassoComponents(Rectangle lasso)
{
double left = lasso.GetValue<double>(Canvas.LeftProperty);
double top = lasso.GetValue<double>(Canvas.TopProperty);
double width = lasso.Width;
double height = lasso.Height;
Rect r = new Rect(left,top,width,height);
IEnumerable<UIElement> hits =
VisualTreeHelper.FindElementsInHostCoordinates(r, LayoutRoot);
VisualTreeHelper is not yet documented on MSDN, but my logging tells me that the Rect is being drawn with the correct bounds. I had to write my own simple hit test function which will register a hit if any of the four corners of a control falls within the lasso. This required a FrameworkElementExtensions class to contain a corner method:
public static Point[] GetCanvasCorners(this FrameworkElement u)
{
if (!(u.Parent is Canvas))
{
throw new ArgumentException("This only works if FrameworkElement is on a Canas!");
}
Point[] corners = new Point[4];
double left = u.GetValue<double>(Canvas.LeftProperty);
double top = u.GetValue<double>(Canvas.TopProperty);
double width = u.Width;
double height = u.Height;
if (double.IsNaN(width) || double.IsNaN(height))
{
width = u.RenderSize.Width;
height = u.RenderSize.Height;
}
corners[0] = new Point(left, top);
corners[1] = new Point(left + width, top);
corners[2] = new Point(left, top + height);
corners[3] = new Point(left + width, top + height);
return corners;
}
Following this, we can use a more basic hit testing algorithm like what is shown here, within the SelectLassoComponents implementation.
protected virtual void SelectLassoComponents(Rectangle lasso)//TODO: make hit test strategy pluggable
{
double left = lasso.GetValue<double>(Canvas.LeftProperty);
double top = lasso.GetValue<double>(Canvas.TopProperty);
double width = lasso.Width;
double height = lasso.Height;
Rect r = new Rect(left,top,width,height);