Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Panel Container with Styled and Region-based

4.73/5 (13 votes)
22 May 20076 min read 1   934  
Drag and drop control on Custom Panel Container with regions-based editable

Sample Image - Image1.gif

Introduction

I'll try to explain how to build this Simple Panel with Styled and Region-based editing with ASP.NET 2.0 using existing controls. We will call it shSimplePanel.

Framework.NET 2.0 provided us with new controls and base classes for building custom controls. System.Web.UI.WebControls.CompositeControl provides the base class for build custom control that implements INamingContainer and WebControl. System.Web.UI.WebControls.CompositeControlDesigner provides the base class for building a custom design-time editor with regions-based editing. When I wanted to build a custom control like the panel control, I didn't inherit from Panel because the behavior and look would be the same at the design-time: just a squared control. I wanted a custom control that enables drag and drop from the ToolBox and adding Title Text, Title Image and Content Image.

shSimplePanel performs these tasks at design-time. Also, I have implemented a class of "Image On the Fly" to allow shSimplePanel stretch-control of the Image at run-time only. Although this class isn't complete it is a good tool. At end of this article, I have provided some references.

Background

When I decided to make this control, I searched the web for documents and code that might help me. Mohammed Mahmoud wrote a WebTabControl with editable regions-based. It was a foundation for this simple control and, with a little help from MSDN Regions-based, it was an excellent reference about how to use the regions-based in design-time. Also, this control is based on the techniques of Styled type, written in the book "Developing Microsoft ASP.NET Server Controls and Components." This is a control built from Panel Web Control that implements Title Text, Title Image, Content-Background Image, Styles for Title and Content and regions editing on design-time. The composite of shSimplePanel is built with three panels.

Screenshot - Image2.gif

Using the code

This custom control implements properties that IntelliSense of VS.NET 2005 uses on design-time. It is easy to use this control; just drag and drop it on the ASPX page.

C#
<cc1:shSimplePanel ID="ShSimplePanel1" runat="server" Height="269px" 
    TitleText="Title shSimplePanel">
    <ContentTemplate>
        This is a Content of shSimplePanel. You can to add controls too.<br />
        <asp:HyperLink ID="HyperLink1" runat="server" 
            NavigateUrl="HypExample1.aspx" Width="225px">
            HyperLink Example 1</asp:HyperLink><br/> 
        <asp:HyperLink ID="HyperLink2" runat="server" 
            NavigateUrl="HypeExample2.aspx" Width="223px">
            HyperLink Example 2</asp:HyperLink><br/>
        <asp:Button ID="Button1" runat="server" Text="Button" 
            Width="93px" OnClick="Button1_Click"/>
    </ContentTemplate>
</cc1:shSimplePanel>

Building the code

First, you need create a WebControlLibrary Project from the Wizard of VS.NET 2005. After that, implement these references:

Adding References

Next, implement this skeleton for shSimplePanel:

C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Drawing;
using System.Collections;
using System.Web.UI.Design;
using System.Web.UI.Design.WebControls;
using System.Drawing.Design;
using System.ComponentModel.Design;

namespace shSimplePanel
{
    [ParseChildren(true)]
    [Designer(typeof(shSimplePanelControlDesigner))]
    [ToolboxData("<{0}:shSimplePanel runat=server 
        width=300px height=100px >")]
    [ToolboxBitmap(@"Please Put here some path of your 
        ToolboxBitmap 16x16 pixeles")]
    public class shSimplePanel : CompositeControl
    {..the code..}

All of the CompositeControl Custom Controls implement similar skeletons. We will define the Designer metadata later. ParseChildren(true) indicates that all of the child's controls will parse as properties. ToolboxBitmap allows addition of some bitmap to our custom control. Now we will implement our Panel control and the variables used in the control:

C#
internal List<IAttributeAccessor> regions;
private csImageOnFly IOFTitle = new csImageOnFly();
private csImageOnFly IOFContent = new csImageOnFly();
private ITemplate _contentTemplate;

private Style _StyleTitle;
private Style _StyleContent;

//Controls
private Panel cPanelMain;
private Panel cPanelTitle;
private Panel cPanelContent;
private Label cLabelTitle;

In this code, we defined our Panel Web Control and our Label as LabelTitle. The internal List<IAttributeAccessor> regions variable defines our regions component. It is declared as internal because we will use it in the shSimplePanelControlDesigner class. This variable has an attribute IAttributeAccessor because it will indicate that we'll add control in that List. Also defined is the csImageOnFly class, to enable stretching of the image at Title and Content both. The private ITemplate _contentTemplate is declared to enable a template to define a region editable. And finally are the Style for Title and Content Panels. Next, we will define the properties of this custom control:

C#
#region Properties of shSimplePanel

        .... for see all properties see the code please

        #region public ITemplate ContentTemplate
        [Browsable(false)]
        [MergableProperty(false)]
        [DefaultValue(null)]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [TemplateContainer(typeof(shSimplePanel))]
        [TemplateInstance(TemplateInstance.Single)]
        public ITemplate ContentTemplate
        {
            get{return _contentTemplate;}
            set{_contentTemplate = value;}
        }
        #endregion

        .... for see all properties see the code please

#endregion Properties of shSimplePanel

These properties may be familiar to you; the only special one is ContentTemplate. I declared Browsable(false) because didn't show in the properties explorer of VS.NET. I defined a TemplateContainer(typeof(shSimplePanel)) attribute as the principal container of the child controls. Finally, TemplateInstance(TemplateInstance.Single) enables access to the child's controls as-is. For example, a Button web control will be accessed as its NameID. Now we override the protected CreateChildControls method to create our control and set the region editable:

C#
protected override void CreateChildControls()
{
  cLabelTitle = new Label();
  cPanelTitle = new Panel();
  cLabelContent = new Label();
  cPanelContent = new Panel();
  cPanelMain = new Panel();

  if (_contentTemplate != null)
  {
    _contentTemplate.InstantiateIn(cPanelContent);
  }

  cPanelMain.Controls.Add(cPanelTitle);
  cPanelMain.Controls.Add(cPanelContent);

  regions = new List<IAttributeAccessor>();
  regions.Add(cPanelContent);
  Controls.Add(cPanelMain);
}

Here I only built the skeleton of shSimplePanel, instanced the ContentTemplate with the panel editable and finally added the regions at List<IAttributeAccessor>. The protected Render method will allow writing of the control on the page. Here is the code:

C#
protected override void Render(HtmlTextWriter writer)
{
  try
  {
    if (cPanelMain == null)
      CreateChildControls();
    //Label Titulo                
    cLabelTitle.Width = Width;
    cLabelTitle.Text = TitleText;
    //Panel Titulo
    cPanelTitle.Width = Width;
    cPanelTitle.Height = TitleHeight;
    cPanelTitle.Wrap = true;
    if ((ImageTitle != null) && (ImageTitle.Length > 0))
    {
      string resStretch;
      if (StretchImageTitle)
      {
        if (DesignMode)
        {
          cPanelTitle.BackImageUrl = ResolveClientUrl(ImageTitle);
        }
        else
        {
          IOFTitle.SourceFileName = 
              HttpContext.Current.Server.MapPath(ImageTitle);
          resStretch = 
              IOFTitle.StartConvertTo(
                  Convert.ToInt32(cPanelTitle.Width.Value), 
                  Convert.ToInt32(cPanelTitle.Height.Value), "_tempTitle");
          if (resStretch != null)
            cPanelTitle.BackImageUrl = "~/" + 
            resStretch.Substring(Page.Server.MapPath(
                HttpContext.Request.ApplicationPath).Length+1);
          else
          {
            cPanelTitle.BackImageUrl = ResolveClientUrl(ImageTitle);
            writer.Write("IOFTitle Error: " + IOFTitle.ErrorDescription + 
                "<br >");
          }
        }
      }
      else
        cPanelTitle.BackImageUrl = ResolveClientUrl(ImageTitle);
    }
    else //set empty string at ImageUrl of Panel Title
    {
      cPanelTitle.BackImageUrl = String.Empty;
    }
    cPanelTitle.Controls.Add(cLabelTitle); 
        //Add the Label Title at cPanelTitle
    if (_StyleTitle != null)               
        //Apply the Style at cPanelTitle 
      cPanelTitle.ApplyStyle(StyleTitle);
    cPanelTitle.HorizontalAlign = HorizontalAlignTitle;  
        //And the property HorizontalAlign for Title
     
    ... The same technics is applied at Content Panel. See the code

  }
  catch (Exception e)
  {
    writer.Write("Render Error: <br >" + e.Message);
  }
}

This code is simple and easy. First, ensure that all child controls are created with the CreateChildControls() method. Later, establish the same width at the panels. We ensure that an image is set and, if required, stretched. csImageOnFly is a simple class that creates a copy of original image on the same path but resized. To create the resized image it needs the final width and height values, the original full path and a text as postfix for image created.

To get the full path of an image, I used the HttpContext.Current.Server.MapPath method. The problem with this class is that it always creates the image resized and does not permit auto-deletion. :-( Finally, this method returns the path of the new image created or null if an error occurred. Afterwards, only apply some properties and styles at the Panel. The same techniques apply at the content panel. For this class, I used styles for content and title panels. These are the properties:

C#
#region Custom Styles
[
Category("Appearance"),
DefaultValue(null),
PersistenceMode(PersistenceMode.InnerProperty),
Description("PanelTitle Style"),
]
public virtual Style StyleTitle
{
    get
    {
        if (_StyleTitle == null)
        {
            _StyleTitle = new Style();
            if (IsTrackingViewState)
                ((IStateManager)_StyleTitle).TrackViewState();
        }
        return _StyleTitle;
    }
}
... for the title style see the code
#endregion

Those properties are declarations for the user only. The jobs for that style are managed for the protected methods LoadViewState, SaveViewState and TrackViewState. I will show how to work it:

C#
#region Custom state management
protected override void LoadViewState(object savedState)
{
    if (savedState == null)
    {
        base.LoadViewState(null);
        return;
    }
    else
    {
        object[] myState = (object[])savedState;
        if (myState.Length != 3)
        {
            throw new ArgumentException("Invalid view state");
        }
        base.LoadViewState(myState[0]);
        ((IStateManager)StyleTitle).LoadViewState(myState[1]);
        ((IStateManager)StyleContent).LoadViewState(myState[2]);
    }
}

protected override object SaveViewState()
{
    // Customized state management to save the state of styles.
    object[] myState = new object[3];

    myState[0] = base.SaveViewState();
    if (_StyleTitle != null)
        myState[1] = ((IStateManager)_StyleTitle).SaveViewState();
    if (_StyleContent != null)
        myState[2] = ((IStateManager)_StyleContent).SaveViewState();

    return myState;
}
protected override void TrackViewState()
{
    // Customized state management to track the state
    // of styles.
    base.TrackViewState();

    if (_StyleTitle != null)
        ((IStateManager)_StyleTitle).TrackViewState();
    if (_StyleContent != null)
        ((IStateManager)_StyleContent).TrackViewState();
}
#endregion

These techniques are implemented in the book "Developing Microsoft ASP.NET Server Controls and Components." Only save and load the style are saved in the State Manager. Check that it always saves in the pos zero base.SaveViewState().

The shSimplePanelControlDesigner class inherits CompositeControlDesigner to enable the region editable. First, we override the Initialize method:

C#
public class shSimplePanelControlDesigner : CompositeControlDesigner
{
  private shSimplePanel _shSimplePanel;
  private int _currentRegion = -1;
  private int _nbRegions = 0;

  public override void Initialize(IComponent component)
  {
    _shSimplePanel = (shSimplePanel)component;
    base.Initialize(component);
    SetViewFlags(ViewFlags.DesignTimeHtmlRequiresLoadComplete, true);
    SetViewFlags(ViewFlags.TemplateEditing, true);
  }
...
}

Here, we got the component shSimplePanel and enabled the design-time mode with SetViewFlags(ViewFlags.TemplateEditing, true);. Then the CreateChildControls method is overridden to set the name properties at the regions:

C#
protected override void CreateChildControls()
{
  base.CreateChildControls();
  if (_shSimplePanel.regions != null)
  {
    _nbRegions = _shSimplePanel.regions.Count;
    for (int i = 0; i < _nbRegions; i++)
    {
      _shSimplePanel.regions[i].SetAttribute(
          DesignerRegion.DesignerRegionAttributeName, i.ToString());
    }
  }
}

In this code, we get the regions of internal List of the shSimplePanel class and set the names. Then we override the OnClick event to get the current region selected:

C#
protected override void OnClick(DesignerRegionMouseEventArgs e)
{
  base.OnClick(e);
  _currentRegion = -1;
  if (e.Region != null)
  {
    for (int i = 0; i < _nbRegions; i++)
    {
      if (e.Region.Name == i.ToString())
      {
        _currentRegion = i;
        break;
      }
    }
    UpdateDesignTimeHtml();
  }
}

When the user makes a click on the content panel, OnClick is fired. Here we will get the current region and update the design-time HTML. For that, we override the GetDesignTimeHtml method:

C#
public override string GetDesignTimeHtml(DesignerRegionCollection regions)
{
  this.CreateChildControls();
  for (int i = 0; i < _nbRegions; i++)
  {
    DesignerRegion r;
    if (_currentRegion == i)
      r = new EditableDesignerRegion(this, i.ToString());
    else
      r = new DesignerRegion(this, i.ToString());
    regions.Add(r);
  }

  if ((_currentRegion >= 0) && (_currentRegion < _nbRegions))
    regions[_currentRegion].Highlight = true;
  return base.GetDesignTimeHtml(regions);
}

In this code, we set the designer editable region for the current region selected and later, we only make a highlight at that region. When you override the property TemplateGroups...

C#
public override TemplateGroupCollection TemplateGroups
{
  get
  {
    TemplateGroupCollection collection = new TemplateGroupCollection();
    TemplateGroup group = new TemplateGroup("ContentTemplate");
    TemplateDefinition definition = new TemplateDefinition(this, 
        "ContentTemplate",  _shSimplePanel, "ContentTemplate", false);
    group.AddTemplateDefinition(definition);
    collection.Add(group);
    return collection;
  }
}

...you can get a collection of region groups for editing in design-time, like the image shown:

Editing on design-time

Finally, override the GetEditableDesignerRegionContent and SetEditableDesignerRegionContent:

C#
public override string GetEditableDesignerRegionContent(
    EditableDesignerRegion region)
{
  IDesignerHost host = (IDesignerHost)Component.Site.GetService(
      typeof(IDesignerHost));
  if (host != null)
  {
    ITemplate contentTemplate;
    if (_currentRegion == 0)
    {
      contentTemplate = _shSimplePanel.ContentTemplate;
      return ControlPersister.PersistTemplate(contentTemplate, host);
    }
  }
  return String.Empty;
}

public override void SetEditableDesignerRegionContent(
    EditableDesignerRegion region, string content)
{
  if (content == null)
    return;
  IDesignerHost host = (IDesignerHost)Component.Site.GetService(
      typeof(IDesignerHost));
  if (host != null)
  {
    ITemplate template = ControlParser.ParseTemplate(host, content);
    if (template != null)
    {
      if (_currentRegion == 0)
      {
        _shSimplePanel.ContentTemplate = template;
      }
    }
  }
}

With this method we get the current template for editing in design-time within ControlParser and ControlPersister.

Conclusion

There are so many documents that will help us. The book referred to and other articles are excellent for beginners in developing custom controls.

I hope this will be useful for you. If I have some error with my English or code, please accept my apology and notify me. Thanks for this space and time.

History

  • 22/05/2007 Article edited and posted to main CodeProject.com article base.
  • 09/11/2006 ViewState in properties.
    • At run time, the properties lost values because the ViewState was not implemented. The private variables used in properties have been deleted.

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