Monday, January 18, 2010

Dragging and dropping controls on a templated ASP.Net control

Whenever you build a more complex ASP.Net control you get to build templates. Stuff like collapsing panels, headered controls or custom repeaters are all usually working with templates, by having properties of the ITemplate type which then are instantiated into a simple control like a Placeholder or Panel. There are many tutorials on how to do this, so I won't linger on the subject.

What I find very difficult to achieve was to have a templated control that would just accept dropped controls and place them into a template, without having to enter in "template mode". First I will copy the entire code of a very simple control, then I will explain.

using System.ComponentModel;
using System.ComponentModel.Design;
using System.Web.UI;
using System.Web.UI.Design;
using System.Web.UI.WebControls;

namespace Web.Controls
{
[Designer(typeof (TestControlDesigner), typeof (IDesigner))]
[ParseChildren(true)]
[PersistChildren(false)]
public class TestControl : WebControl
{
#region Properties

[EditorBrowsable(EditorBrowsableState.Never)]
[Browsable(false)]
[PersistenceMode(PersistenceMode.InnerProperty)]
[DefaultValue(typeof (ITemplate), "")]
[TemplateInstance(TemplateInstance.Single)]
public ITemplate MainTemplate
{
get;
set;
}

#endregion

#region Private Methods

protected override void CreateChildControls()
{
base.CreateChildControls();
if (MainTemplate != null)
{
MainTemplate.InstantiateIn(this);
}
}

#endregion
}

public class TestControlDesigner : ControlDesigner
{
#region Member data

private TestControl mTestControl;

#endregion

#region Public Methods

public override void Initialize(IComponent component)
{
base.Initialize(component);
SetViewFlags(ViewFlags.TemplateEditing, true);
mTestControl = (TestControl) component;
}

public override string GetDesignTimeHtml()
{
mTestControl.Attributes[DesignerRegion.DesignerRegionAttributeName] = "0";
return base.GetDesignTimeHtml();
}

public override string GetDesignTimeHtml(DesignerRegionCollection regions)
{
var region = new EditableDesignerRegion(this, "MainTemplate");
region.Properties[typeof (Control)] = mTestControl;
region.EnsureSize = true;
regions.Add(region);
string html = GetDesignTimeHtml();
return html;
}


public override string GetEditableDesignerRegionContent(
EditableDesignerRegion region)
{
if (region.Name == "MainTemplate")
{
var service = (IDesignerHost) Component.Site.GetService(typeof (IDesignerHost));
if (service != null)
{
ITemplate template = mTestControl.MainTemplate;
if (template != null)
{
return ControlPersister.PersistTemplate(template, service);
}
}
}
return string.Empty;
}


public override void SetEditableDesignerRegionContent(
EditableDesignerRegion region,string content)
{
if (region.Name == "MainTemplate")
{
if (string.IsNullOrEmpty(content))
{
mTestControl.MainTemplate = null;
}
else
{
var service = (IDesignerHost)
Component.Site.GetService(typeof (IDesignerHost));
if (service != null)
{
ITemplate template = ControlParser.ParseTemplate(service, content);
if (template != null)
{
mTestControl.MainTemplate = template;
}
}
}
}
}

#endregion

#region Properties

public override TemplateGroupCollection TemplateGroups
{
get
{
var collection = new TemplateGroupCollection();

var group = new TemplateGroup("MainTemplate");
var template = new TemplateDefinition(this,
"MainTemplate", mTestControl, "MainTemplate", true);
group.AddTemplateDefinition(template);
collection.Add(group);
return collection;
}
}

#endregion
}
}


The above code is a pretty common way of making a templated control. We have a Control (TestControl) with one or more ITemplate properties (MainTemplate) and a ControlDesigner (TestControlDesigner) that decorates the TestControl class via the Designer attribute. The trick for the drag and drop to work with this scenario is in the last two methods.

First look at the GetDesignTimeHtml() override. There I did a little hack in order to set for the target control an attribute with the name DesignerRegion.DesignerRegionAttributeName and with the value of "0". This actually declares the entire control in the Visual Studio designer as the first (and only) "editable region". The region is registered in the GetDesignTimeHtml(DesignerRegionCollection regions) override and named MainTemplate.

Secondly, look at the TemplateGroups property override. This is in order for the "Edit Templates" action to appear in design mode. Since drag and drop would work very nicely, technically we don't need it. However, I noticed that if one tries to drag a control out of the templated one, it doesn't really work :). We still need Template Editing mode, but only in unlikely scenarios. Even if the template group is called "MainTemplate" it has no connection with the editing region.

Thirdly, look at the lots of attributes decorating the TestControl class and the MainTemplate property. An important one is [TemplateInstance(TemplateInstance.Single)], which says that there will be only one instance of the template, in other words, the controls can be treated as in a Panel and accessible from the code as protected members.

Finally, let's look at the methods that actually permit the drag and drop operation: GetEditableDesignerRegionContent, which has a region as a parameter, and SetEditableDesignerRegionContent, which also has a region, but also a string content parameter.

The trick lies in the conversion from this unwieldy string variable to a control tree and viceversa. The string is necessary because it is the XML/HTML as interpreted by the Visual Studio designer.

As you can see, in the get method the static class ControlPersister is used to transform the control tree into a string, while in the set method the static ControlParser class is used to do the reverse.

Hope this helps someone.

0 comments: