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
Resizing
As I look at the project so far, it seems somehow more natural to want to move things before selecting or resizing, but the notion of Selection had to come first, which has a dependency on my Site metaphor. Since the selection border is already in place, why not move on to resizing next?
While contemplating resizing, rotation (which I plan on adding eventually), and selection I had a dilemma. I felt there were two main paths:
1. Draw adorners. This means the IDesignableControl lives on the surface mostly as itself, and when an interesting event happens (like selection) we have the surface draw a border with the familiar “grab knobs” at the corners.
2. Container/Site: I first thought of creating a Grid to hold the IDesignableControl within it, since this would provide the table structure needed to draw “knobs” as well as the familiar selection border.
I wrote the code for the Grid first while working in my third office (http://www.ale-house.com/alehouse/index.php?option=com_content&task=view&id=23&Itemid=57 ) without the source for the project, which lives on the workstation in my second office (http://www.flickr.com/photos/damonrpayne/528817886/ ) and ultimately didn’t’ like it, but I did learn enough to see that a Silverlight Border would make resizing incredibly easy. I have a vague notion of an IDesignSurfaceBehavior I can use for true Adorner flexibility later, but for selection and resizing using a UserControl with a Border was just so simple I had to start by going down this path.
DesignSite
The UserControl serving as border container is the DesignSite. It has a Width and Height of Auto and the Border has a Width and Height of Auto as well, to make resizing a piece of cake. The XAML couldn’t be easier.
<Border x:Name="SiteBorder" BorderThickness="0" BorderBrush="{StaticResource SelectionBorderBrush}" CornerRadius="7" Cursor="Hand" MouseLeftButtonDown="SiteBorder_MouseLeftButtonDown" MouseLeftButtonUp="SiteBorder_MouseLeftButtonUp" MouseMove="SiteBorder_MouseMove">
<Border.Child>
<TextBlock>Default Content</TextBlock>
</Border.Child>
</Border>
The code is fairly straightforward too. For most things we are using a pattern of delegation to the contained instance to make future designer interactions easier. In order to properly calculate resizing, I needed to be able to get at the actual design surface itself, so I created an IDesigner interface which is implemented by DesignSurface.
public interface IDesigner : IService
{
UserControl VisualRoot { get; }
Canvas Surface { get; }
}
I have a feeling I’ll be adding to that later. Here is the mouse code in DesignSite:
private void SiteBorder_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_resizing = true;
SiteBorder.CaptureMouse();
}
private void SiteBorder_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_resizing = false;
SiteBorder.ReleaseMouseCapture();
}
private void SiteBorder_MouseMove(object sender, MouseEventArgs e)
{
if (_resizing)
{
Point resizePoint = e.GetPosition(_designer.VisualRoot);
double left = SiteBorder.GetValue<double>(Canvas.LeftProperty);
double top = SiteBorder.GetValue<double>(Canvas.TopProperty);
double newWidth = left + resizePoint.X;
double newHeight = top + resizePoint.Y;
Control c = (Control)SiteBorder.Child;
if (newWidth > 5 && newHeight > 5)
{
c.Width = newWidth;
c.Height = newHeight;
}
}
}
And now, when I run it, everything goes to hell. Handling resizing seems to have become my first serious problem. In order to study the issue, I created a DummyButton IDesignableControl, which exposes even more problems.
A Silverlight control with size set to Auto (take up its whole container) returns NaN for Width and Height, {0,0} for RenderSize, 0.0 for ActualWidth and ActualHeight, {0,0} for DesiredSize, 0.0 for MinWidth and MinHeight. This makes it hard to measure or do any calculations on until it is actually placed in a container. Calling Control.Measure does not seem to fix the issue. I was unable to create my Drag representation of the DummyButton until I set its containing UserControl to have an actual size. Attempting to select the button on the surface reveals another issue: the button is swallowing the click event before my code can react to it. I’d like to handle this in a generic fashion.
I set about making a “glass” idea: placing an almost entirely transparent control in front of the real content in the Z order. Getting this to cover the entirety of the real content led me back to the same lack of ActualWidth and friends that started this mess.
Reality Blogging, cue RC0
OK, everything above this heading was written pre-RC0 and I was clearly floundering, I call time out. I need to restate and reform my goals:
· My DummyButton test shows that I need to refine IDesignableControl and how things ought to resize on the DesignSurface. I am so far setting width/height of the IDesignableControl.Visual when dragging the Border to resize. This is only appropriate in some cases. For a Button, sure, I’d want to set its bounds independent of its FontSize or other properties. For the furniture and speakers, I’d actually want to apply a Transform since a huge red square with a chair in the corner or a small red square showing part of a chair is obviously not my intent.
· Following from the above, IDesignableControl.Scalable and IDesignableControl.Transformable need to be re-thought. I need a property stating if the IDC{IDesignableControl} is to be resized by changing Width & Height, if not, Transformable should be checked. Both these properties being false means we should not resize the IDC at all.
· There’s too much code in DesignSite. Event wire up related to sizing is possibly fine, but we should keep things in whatever IDesignTypeCreator implementation wherever possible.
Now I feel like I’ve got a better plan, even if the Measuring issue is still waiting. First I refactor IDC.Scalable and include an appropriate comment.
/// <summary>
/// Return true if this IDesignableControl should be resized by changing Width & Height
/// </summary>
bool IsBoundsResizable { get; }
Next, IDC.Transformable:
/// <summary>
/// Can a ScaleTransform etc. be used to resize this control?
/// </summary>
bool IsTransformable { get; }
Next I setup a test. The DummyButton will be IsBoundsResizable=true, and the Red Chair will be IsBoundsResizable=fasle/IsTransformable=true.
Debugging Events
The next step I take is to set up some code to debug the various mouse position, sizing, and layout events for the Border in DesignSite. I tend to do this by instrumenting the code rather than using the debugger. What good is a MouseLeftButtonDown event on a visual control when the debugger will receive the MouseLeftButtonUp event? I have to use the ILogService and my message window to see when/how some things are happening.
Layout Cycle Detected
If you subscribe to a LayoutUpdated event, it’s best not to touch the GUI in any way from within this event handler. During my testing, I would use the ILogService to send a message stating that my DesignSite had its layout updated. This, in turn, fires the LayoutUpdated event which is counterintuitive. One might think that LayoutUpdated means “Layout of the current control” but in fact means a layout or property change of any item in the visual tree of the Silverlight plug-in. Obviously touching the visual tree from here will melt circuits.
Resizing
With a more clear idea of precisely what I was trying to do, I went back to working on my resizing code. When my Border has mouse capture, I can choose how to resize its content:
private void SiteBorder_MouseMove(object sender, MouseEventArgs e)
{
if (_resizing)
{
if (HostedContent.IsBoundsResizable)
{
ResizeContent(e);
}
else
{
TransformContent(e);
}
}
}
I have to admit it took quite a while to get the ResizeContent method right. At the end of the day, this was just facing reality and doing what worked rather than refusing to accept how”Auto”, “Stretch”, and “Fill” did not live up to expectations. I have to set five different Controls’ sizes manually to make my bounds resizing work:
private void ResizeContent(MouseEventArgs e)
{
Point resizePoint = e.GetPosition(DesignParent);
//Determine where the DesignSite is on parent
double left = this.GetValue<double>(Canvas.LeftProperty);
double top = this.GetValue<double>(Canvas.TopProperty);
double newWidth = resizePoint.X - left;
double newHeight = resizePoint.Y - top;
//Take border into account
double innerWidth = newWidth - (2 * UNIFORM_BORDER_THICKNESS);
double innerHeight = newHeight - (2 * UNIFORM_BORDER_THICKNESS);
if (newWidth > 5 && newHeight > 5)
{
_content.Visual.Width = innerWidth;
_content.Visual.Height = innerHeight;
Glass.Width = innerWidth;
Glass.Height = innerHeight;
LayoutRoot.Width = newWidth;
LayoutRoot.Height = newHeight;
ContentCanvas.Width = innerWidth;
ContentCanvas.Height = innerHeight;
SiteBorder.Width = newWidth;
SiteBorder.Height = newHeight;
}
}
Having accepted this reality, my “transform” resizing worked the first time:
private void TransformContent(MouseEventArgs e)
{
Point resizePoint = e.GetPosition(DesignParent);
//Determine where the DesignSite is on parent
double left = this.GetValue<double>(Canvas.LeftProperty);
double top = this.GetValue<double>(Canvas.TopProperty);
double newWidth = resizePoint.X - left;
double newHeight = resizePoint.Y - top;
//We need to attain a render size equal to our new border inner bounds,
//so we'll calculate what scale transform would get us there
double innerWidth = newWidth - (2 * UNIFORM_BORDER_THICKNESS);
double innerHeight = newHeight - (2 * UNIFORM_BORDER_THICKNESS);
SiteBorder.Width = newWidth;
SiteBorder.Height = newHeight;
//
Glass.Width = innerWidth;
Glass.Height = innerHeight;
//
ContentCanvas.Width = innerWidth;
ContentCanvas.Height = innerHeight;
//
ScaleTransform st = new ScaleTransform();
st.ScaleX = innerWidth / HostedContent.Visual.ActualWidth;
st.ScaleY = innerHeight / HostedContent.Visual.ActualHeight;
HostedContent.Visual.RenderTransform = st;
}
Now, I can finally have large buttons, small couches, and huge speakers on my design surface:

… All perfectly scaled according to their needs by naturally grabbing and dragging the border. Hooray for vector graphics. Note to self: every single flaw in artwork is readily apparent when scaled far larger than I drew it.
Conclusion
This article was a bit of a mess since Silverlight 2 RC0 happened right in the middle of it and my vision was not re-adjusted early enough when I started realizing there were issues. There are also still cases where I am obviously thinking like a WinForms developer. I kept thinking to myself how easy this would be if I could do work by overriding an OnPaint virtual method. Still, I finally got where I wanted to be and learned a few more things about Silverlight in the process. Since I’ve already given thought to one more bit of sauce before I write code to move things around on the surface I will visit the “rectangular lasso” next.
Source code: DamonPayne.AGT[12].zip (1.3 MB)