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

A Silverlight WidgetZone Control

0.00/5 (No votes)
21 Oct 2009 1  
Widget developing is more and more in fashion today, drag and drop support 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.

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.

///  <summary>
/// Provides the behavior for the "measure" pass of Silverlight layout.
///  </summary>
///  <param name="availableSize">The size that this object
/// should use to measure its child objects.  </param>
///  <returns>The actual size used. </returns>
protected sealed override Size MeasureOverride(Size availableSize)
{
    // Calc columns width and left
    CalcColumnLeftAndWidth(availableSize.Width);

    // First of all, create the column based dictionary
    BuildDictionary(this.Children);

    // The used height of each column
    double[] usedHeights = new double[Partitions.ColumnsCount];

    // we measure each item based on it's column and row
    foreach (var item in columnDictionary)
    {
	// Calc the column width
	double columnWidth = columnWidths[item.Key];

	// for each item in a column
	foreach (UIElement element in item.Value)
	{
  	    // Attach event handle
	    AttachEventHandle(element);
	    // Arrange
	    if (GetIsMoving(element))
	    {
		// If the element in moving
		// The moving item dose not consume the height
		element.Measure(new Size(columnWidth,
			availableSize.Height - usedHeights[item.Key]));
	    }
	    else
	    {
		try
		{
		    // We measure the desired size based on each column
		    element.Measure(new Size(columnWidth, availableSize.Height -
			usedHeights[item.Key]));
			usedHeights[item.Key] += (element.DesiredSize.Height +
			RowSpacing);
		}
		catch
		{
		}
	    }
	}
    }

    // The final desired size
    double desiredHeight = 0;
    if (this.VerticalAlignment == VerticalAlignment.Stretch)
	desiredHeight = availableSize.Height;
    // Get Max height of the columns
    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.

/// <summary>
/// Provides the behavior for the "arrange" pass of Silverlight layout.
///  </summary>
///  <param name="finalSize">The size that this object should use
/// to arrange its child objects.  </param>
///  <returns>The actual size used. </returns>
protected sealed override Size ArrangeOverride(Size finalSize)
{
	// The used height of each column
	double[] usedHeights = new double[Partitions.ColumnsCount];

	// We arrange each item based on it's column then row
	foreach (var item in columnDictionary)
	{
		// for each UIElement in a column
		foreach (UIElement element in item.Value)
		{
			if (GetIsMoving(element))
			{
				Canvas.SetZIndex(element, 10);
				// If the element is in moving state
				// we arrange it like Canvas
				double left = Canvas.GetLeft(element);
				double top = Canvas.GetTop(element);
				// The moving item dose not eat height
				Rect finalRect = new Rect(left, top,
				columnWidths[item.Key], element.DesiredSize.Height);
				element.Arrange(finalRect);
			}
			else
			{
				// set Canvas left and top for moving
				Canvas.SetLeft(element, columnLefts[item.Key]);
				Canvas.SetTop(element, usedHeights[item.Key]);
		
				// Calc the final rect
				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);
			}
		}
	}

	// The final size
	double finalHeight = 0.0;
	if (this.VerticalAlignment == VerticalAlignment.Stretch)
	finalHeight = finalSize.Height;
	// Gets the max height of the columns
	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:

// The mouse left button down
void element_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    if (EditMode)
    {
        // if in edit mode, user can drag move the item from one place to other
        UIElement element = sender as UIElement;
        currentColumn = GetColumn(element);
        currentRow = GetRow(element);
        if (placeholder == null)
        {
            placeholder = CreatePlaceholder();
            this.Children.Add(placeholder);
        }
        
        // Set placeholder property
        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.

// The mouse move
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)
        {
            // column changed
            orgPoint.X = orgPoint.X * columnWidths[columnIndex] /
					columnWidths[currentColumn];
            placeholder.Width = columnWidths[columnIndex];
            SetColumn(placeholder, columnIndex);
            SetColumn(element, columnIndex);
            currentColumn = columnIndex;
        }
        if (currentRow != rowIndex)
        {
            // Row changed
            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.

// The mouse left button up
void element_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    UIElement element = sender as UIElement;
    SetIsMoving(element, false);
    if (placeholder != null)
    {
        // place the moving element to dest
        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

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