Introduction
Widgets are developing more and more fashion today, drag and drop supports is a basic function in a widget platform. If you want to develop a Silverlight widget platform, you may need a Panel
which supports drag and drop UIElement
on it. The WidgetZone
is a Panel that can be used in such cases. It supports drag and drop elements placed on it.
Background
In my options, the WPF/Silverlight control can be split into two categories. One is layout control and the other is display control. In many cases, the layout control does not have its own appearance and is just used to place the other control, such as Panel
(including StackPanel
, DockPanel
, etc.), Grid
and Canvas
. On the other hand, the most displayed control has its own appearance to display something for the user, such as Button
, TextBox
, etc.
If you are familiar with developing WPF/Silverlight controls, you may find that the control development mode of the two categories is very different. To implement a layout control, you always override the MeasureOverride
and ArrangeOverride
methods, and there is no default ControlTemplate
for this control. To develop a display control, you always provide a default ControlTemplate
for its default appearance. And your code should not interfere with the appearance of the control.
The WidgetZone
is a layout control to place an item on it and supports drag and drop the item. It's mostly like a Grid
control but supports drag and drop an element on it to other columns or rows.
Implement the WidgetZone Control
Firstly, we need to decide from where (or which control) our control derived? At first glance, it's something like a Grid
so maybe derived from Grid
is right. But if you carefully check its function, it acts more as a Panel
than others.
Secondly, how to define the columns which the WidgetZone
contains. If we support XAML binding, we must provide a TypeConverter
to convert string
to the columns definition type.
The simplest way to define the columns is to define each column's width in proportions like "3:2:1", which means if the total width is 600px, the first column is 300px and the second column is 200px and the last column is 100px width. We define this proportion representation in ColumnPartitions
class and use ColumnPartitionsConverter
to convert from string representation (in XAML file) to ColumnPartitions
object.
A panel uses MeasureOverride
to calculate each child element's size and uses ArrangeOverride
to arrange it. So a layout control's core implementation are the two functions. Let's look at them.
protected sealed override Size MeasureOverride(Size availableSize)
{
CalcColumnLeftAndWidth(availableSize.Width);
BuildDictionary(this.Children);
double[] usedHeights = new double[Partitions.ColumnsCount];
foreach (var item in columnDictionary)
{
double columnWidth = columnWidths[item.Key];
foreach (UIElement element in item.Value)
{
AttachEventHandle(element);
if (GetIsMoving(element))
{
element.Measure(new Size(columnWidth,
availableSize.Height - usedHeights[item.Key]));
}
else
{
try
{
element.Measure(new Size(columnWidth, availableSize.Height -
usedHeights[item.Key]));
usedHeights[item.Key] += (element.DesiredSize.Height +
RowSpacing);
}
catch
{
}
}
}
}
double desiredHeight = 0;
if (this.VerticalAlignment == VerticalAlignment.Stretch)
desiredHeight = availableSize.Height;
foreach (double height in usedHeights)
{
desiredHeight = Math.Max(desiredHeight, height);
}
return new Size(availableSize.Width, desiredHeight);
}
The MeasureOverride
first calculates each column's width and the left side position, then builds a dictionary based on each child element in which the column resides. Then it calculate each child element's size as it is placed in the desired column and row.
protected sealed override Size ArrangeOverride(Size finalSize)
{
double[] usedHeights = new double[Partitions.ColumnsCount];
foreach (var item in columnDictionary)
{
foreach (UIElement element in item.Value)
{
if (GetIsMoving(element))
{
Canvas.SetZIndex(element, 10);
double left = Canvas.GetLeft(element);
double top = Canvas.GetTop(element);
Rect finalRect = new Rect(left, top,
columnWidths[item.Key], element.DesiredSize.Height);
element.Arrange(finalRect);
}
else
{
Canvas.SetLeft(element, columnLefts[item.Key]);
Canvas.SetTop(element, usedHeights[item.Key]);
Rect finalRect = new Rect(columnLefts[item.Key],
usedHeights[item.Key],
columnWidths[item.Key], element.DesiredSize.Height);
usedHeights[item.Key] +=
(element.DesiredSize.Height + RowSpacing);
element.Arrange(finalRect);
}
}
}
double finalHeight = 0.0;
if (this.VerticalAlignment == VerticalAlignment.Stretch)
finalHeight = finalSize.Height;
foreach (double height in usedHeights)
{
finalHeight = Math.Max(finalHeight, height);
}
return new Size(finalSize.Width, finalHeight);
}
The ArrangeOverride
places each child element in the proper column and row. We use Column
and Row
attached property to indicate which column and row an element resides in. An attached property is a property that can attach to another element. For more information about attached property, please refer to MSDN. If an element is moving, which means the element is just dragged by the user, it can overlay the other element.
Now let's look at how to drag and drop an element. We always drag an element using the mouse, so we need to handle mouse left button down and mouse move event. We attach those events and handle to implement drag and drop.
When mouse left button is down in an element, we do:
void element_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (EditMode)
{
UIElement element = sender as UIElement;
currentColumn = GetColumn(element);
currentRow = GetRow(element);
if (placeholder == null)
{
placeholder = CreatePlaceholder();
this.Children.Add(placeholder);
}
placeholder.Height = element.RenderSize.Height + 2;
placeholder.Width = columnWidths[currentColumn];
SetColumn(placeholder, currentColumn);
SetRow(placeholder, currentRow);
placeholder.Visibility = Visibility.Visible;
SetIsMoving(element, true);
orgPoint = e.GetPosition(element);
element.CaptureMouse();
}
}
In the code, we create a placeholder to indicate the element will be moved and set the element in moving state by attaching the IsMoving
attached property to it.
When the mouse is moving, the selected element should move with the mouse.
void element_MouseMove(object sender, MouseEventArgs e)
{
UIElement element = sender as UIElement;
if (GetIsMoving(element))
{
Point point = e.GetPosition(this);
int columnIndex = GetColumnByPosition(point);
int rowIndex = GetRowByPosition(columnIndex, point);
if (currentColumn != columnIndex)
{
orgPoint.X = orgPoint.X * columnWidths[columnIndex] /
columnWidths[currentColumn];
placeholder.Width = columnWidths[columnIndex];
SetColumn(placeholder, columnIndex);
SetColumn(element, columnIndex);
currentColumn = columnIndex;
}
if (currentRow != rowIndex)
{
SetRow(placeholder, rowIndex);
currentRow = rowIndex;
}
Canvas.SetLeft(element, point.X - orgPoint.X);
Canvas.SetTop(element, point.Y - orgPoint.Y);
this.InvalidateArrange();
}
}
When the element is moving, it may move to another column or row, so we indicate if we drop now, where the moving element will be placed. We calculate the current column and row by GetColumnByPosition
and GetRowByPosition
.
When the left button is up, we just place the element on placeholder indicated column and row by attaching the Column
and Row
property to it.
void element_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
UIElement element = sender as UIElement;
SetIsMoving(element, false);
if (placeholder != null)
{
SetColumn(element, currentColumn);
SetRow(element, currentRow);
placeholder.Visibility = Visibility.Collapsed;
}
element.ReleaseMouseCapture();
}
Using the WidgetZone
If you want to use the WidgetZone
in your SL app, first reference the Cokkiy.Widgets.Widget
assembly, then in your XAML file, you do as follows:
<UserControl x:Class="WidgetZoneTest.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:widgets="clr-namespace:Cokkiy.Widgets;assembly=Cokkiy.Widgets.Widget"
xmlns:zone="clr-namespace:Cokkiy.Widgets;assembly=Cokkiy.Widgets.WidgetZone"
xmlns:local="clr-namespace:WidgetZoneTest"
Width="Auto" Height="480">
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<zone:WidgetZone VerticalAlignment="Stretch" Name="myZone" Partitions="3:2:1">
<Grid Height="40" Background="Yellow" zone:WidgetZone.Column="1"/>
<Grid Background="Red" Height="50" zone:WidgetZone.Column="0">
</Grid>
<Grid Background="Beige" Height="30" zone:WidgetZone.Column="0"
zone:WidgetZone.Row="1"/>
<Grid Background="CadetBlue" Height="40"
zone:WidgetZone.Column="0" zone:WidgetZone.Row="2"/>
<Grid Background="Chartreuse" Height="30"
zone:WidgetZone.Column="1" zone:WidgetZone.Row="1"/>
<Grid Background="DarkMagenta" Height="80"
zone:WidgetZone.Column="1" zone:WidgetZone.Row="2"/>
<widgets:Widget Title="My Widget" ShowTitleBar="False"
zone:WidgetZone.Column="1">
<widgets:Widget.Editor>
<local:MyEditor/>
</widgets:Widget.Editor>
</widgets:Widget>
</zone:WidgetZone>
<Button Grid.Row="1" Content="Goto Edit Mode"
x:Name="editModeButton" Click="editModeButton_Click"></Button>
</Grid>
</UserControl>
History
- 21st October, 2009: First post