Custom Server Controls - Base Classes and Usage
I hope someone out there can shed some light on the topic of creating server controls, base control classes, and using those classes.
Here is an example of what I want to achieve. I want to create a custom panel control that can be created in ASPX markup like this:
<acme:Panel ID="MyPanel" runtat="server" Scrolling="true">
<Header>My Panel Header</Header>
<Toolbars>
<acme:Toolbar ID="Toolbar1" runat="server"/>
<acme:Toolbar ID="Toolbar2" runat="server"/>
</Toolbars>
<Contents>
<%-- Some Content for the Contents section --%>
</Contents>
<Footer>
<%-- Some Content for the Footer section --%>
</Footer>
</acme:Panel>
It should display the following HTML:
<div id="MyPanel" class="panel scroll-contents">
<div class="panel-header">
<div class="panel-header-l"></div>
<div class="panel-header-c">
<div class="panel-header-wrapper">My Panel Header</div>
</div>
<div class="panel-header-r"></div>
</div>
<div class="panel-toolbars">
// HTML of Toolbar control
</div>
<div class="panel-body">
<div class="panel-body-t">
<div class="panel-body-tl"></div>
<div class="panel-body-tc"></div>
<div class="panel-body-tr"></div>
</div>
<div class="panel-body-m">
<div class="panel-body-ml"></div>
<div class="panel-body-mc">
<div class="panel-body-wrapper">
// Contents
</div>
</div>
<div class="panel-body-mr"></div>
</div>
<div class="panel-body-b">
<div class="panel-body-bl"></div>
<div class="panel-body-bc"></div>
<div class="panel-body-br"></div>
</div>
</div>
<div class="panel-footer">
<div class="panel-footer-l"></div>
<div class="panel-footer-c">
<div class="panel-footer-wrapper">
// Footer contents
</div>
</div>
<div class="panel-footer-r"></div>
</div>
</div>
The developer should be able to omit any of the sections except the "Contents" section. The HTML code of the excluded sections should not be displayed. In addition, the user should be able to create / add a Panel control in the code behind the page and add additional controls to various sections of the control panel, for example:
ACME.Panel MyPanel = new ACME.Panel();
MyPlaceHolder.Controls.Add(MyPanel);
MyPanel.Header = "My Panel Header";
MyPanel.Toolbars.Controls.Add(new ACME.Toolbar());
MyPanel.Footer.Controls.Add(new Literal());
MyPanel.Contents.Controls.Add(new GridView());
I read the following article: Conversations in the Communication Sector: Creating Custom ASP.NET Server Controls (Which Base Class?)
Since I don't want the developer to change the style of my control, the base System.Web.UI.Control class was enough, but I also need to use the INamingContainer interface. So my control should look like this:
using System.Web;
using System.Web.UI;
using System.Web.UI.Design;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Security.Permissions;
namespace ACME
{
[ToolboxData("<{0}:Panel runat=server></{0}:Panel >")]
[ParseChildren(true)]
public class Panel : Control, INamingContainer
{
private string _header;
private ITemplate _toolbars;
private ITemplate _contents;
private ITemplate _footerContents;
public DialogBox()
{
}
[Browsable(false),
PersistenceMode(PersistenceMode.InnerProperty)]
public virtual ITemplate Toolbars
{
get { return _toolbars; }
set { _toolbars = value; }
}
[Browsable(false),
PersistenceMode(PersistenceMode.InnerProperty)]
public virtual ITemplate Contents
{
get { return _contents; }
set { _contents= value; }
}
[Browsable(false),
PersistenceMode(PersistenceMode.InnerProperty)]
public virtual ITemplate Footer
{
get { return _footerContents; }
set { _footerContents = value; }
}
}
}
I've read a lot of tutorials, but they either don't cover my intended implementation, or they explain WHY some approach has been taken. They also confused me so much that I used JavaScript to dynamically render the required HTML. If there is any Managing Guru, could you please explain how you would handle this task?
a source to share
This will be long, a lot of code:
Since you have a lot of common elements that you want to render, I started with BaseControl, which defined some common methods for creating all the sections that you use. This inherits from System.Web.UI.Control - like document state:
This is the main class you get when you develop custom ASP.NET controls. The control has no specific user interface (UI) functionality. If you create a control that does not have a user interface, or bundles other controls that display their own interface, are retrieved from Control .
So, the basic control looks like this:
/// <summary>
/// Provides some common methods.
/// </summary>
public class BaseControl: Control
{
protected Control[] TempControls;
/// <summary>
/// Clears the child controls explicitly, and stores them locally.
/// </summary>
protected void ClearControls()
{
if (HasControls())
{
TempControls = new Control[Controls.Count];
Controls.CopyTo(TempControls, 0);
}
Controls.Clear();
}
/// <summary>
/// Creates a new panel (HTML div) with the requested CSS
/// and containing any controls passed in.
/// </summary>
/// <param name="cssClass">The CSS class to be applied</param>
/// <param name="controls">Any controls that should be added to the panel</param>
protected Panel NewPanel(string cssClass, params Control[] controls)
{
// Create a new Panel, assign the CSS class.
var panel = new Panel { CssClass = cssClass };
// Loop through the controls adding them to the panel.
foreach (var control in controls)
{
panel.Controls.Add(control);
}
return panel;
}
/// <summary>
/// Creates a new row of panels (HTML div), based on the CSS class prefix.
/// The center panel holds the controls passed in.
/// </summary>
/// <param name="cssClassPrefix"></param>
/// <param name="controls"></param>
protected Panel NewRow(string cssClassPrefix, params Control[] controls)
{
// Expaned for clarity, but could all be passed in on one call.
var row = NewPanel(cssClassPrefix);
row.Controls.Add(NewPanel(cssClassPrefix + "-l"));
row.Controls.Add(NewPanel(cssClassPrefix + "-c", controls));
row.Controls.Add(NewPanel(cssClassPrefix + "-r"));
return row;
}
}
Then you need to create some controls that will handle each of your templates, I created two here.
First, the control that will generate one line is used by both the header and footer:
public class AcmeSimple : BaseControl, INamingContainer
{
private string m_CssPrefix;
public AcmeSimple(string cssPrefix)
{
m_CssPrefix = cssPrefix;
}
protected override void CreateChildControls()
{
ClearControls();
Panel wrapper = NewPanel(m_CssPrefix + "-wrapper", TempControls);
Panel simple = NewRow(m_CssPrefix, wrapper);
Controls.Add(simple);
base.CreateChildControls();
}
}
It creates a new bar to hold the controls and then creates a new row of divs to hold the wrapper.
Then a little more complex content management, which works in the same way as the title:
public class AcmeContents: BaseControl, INamingContainer
{
protected override void CreateChildControls()
{
ClearControls();
Panel wrapper = NewPanel("panel-body-wrapper", TempControls);
Panel contents = NewPanel("panel-body");
contents.Controls.Add(NewRow("panel-body-t"));
contents.Controls.Add(NewRow("panel-body-m", wrapper));
contents.Controls.Add(NewRow("panel-body-b"));
Controls.Add(contents);
base.CreateChildControls();
}
}
So this just created three lines, the middle one containing the controls.
Finally, the actual control you are placing on the page:
[ParseChildren(true)]
[ToolboxData("<{0}:AcmeControl runat=server></{0}:AcmeControl>")]
public class AcmeControl: BaseControl, INamingContainer
{
public bool Scrolling { get; set; }
[TemplateContainer(typeof(AcmeSimple))]
public ITemplate Header { get; set; }
[TemplateContainer(typeof(AcmeContents))]
public ITemplate Contents { get; set; }
[TemplateContainer(typeof(AcmeSimple))]
public ITemplate Footer { get; set; }
protected override void CreateChildControls()
{
Controls.Clear();
string cssClass = "panel";
if (Scrolling)
{
cssClass += " scrollContents";
}
Panel panel = NewPanel(cssClass);
panel.ID = ID;
Controls.Add(panel);
if (Header != null)
{
var header = new AcmeHeader("panel-header");
Header.InstantiateIn(header);
panel.Controls.Add(header);
}
if (Contents != null)
{
var contents = new AcmeContents();
Contents.InstantiateIn(contents);
panel.Controls.Add(contents);
}
else
{
// Possibly a little harsh, as it a runtime exception.
throw new ArgumentNullException("Contents", "You must supply a contents template.");
}
if (Footer != null)
{
var footer = new AcmeSimple("panel-footer");
Footer.InstantiateIn(footer);
panel.Controls.Add(footer);
}
}
}
So, we define the templates that we support as properties of the control as well as the Scrollable property that you wanted. Then in CreateChildControls, we start creating the body of the control using the controls we created in the beginning and the methods in BaseControl.
It happens on the page as follows:
<cc1:AcmeControl ID="AcmeControl1" runat="server">
<Header>
<b>Here a header</b>
</Header>
<Contents>
<i>Here some controls in the content.</i>
</Contents>
</cc1:AcmeControl>
And it outputs like this:
<div id="AcmeControl1_AcmeControl1" class="panel">
<div class="panel-header">
<div class="panel-header-l">
</div>
<div class="panel-header-c">
<div class="panel-header-wrapper">
<b>Here a header</b>
</div>
</div>
<div class="panel-header-r">
</div>
</div>
<div class="panel-body">
<div class="panel-body-t">
<div class="panel-body-t-l">
</div>
<div class="panel-body-t-c">
</div>
<div class="panel-body-t-r">
</div>
</div>
<div class="panel-body-m">
<div class="panel-body-m-l">
</div>
<div class="panel-body-m-c">
<div class="panel-body-wrapper">
<i>Here some controls in the content.</i>
</div>
</div>
<div class="panel-body-m-r">
</div>
</div>
<div class="panel-body-b">
<div class="panel-body-b-l">
</div>
<div class="panel-body-b-c">
</div>
<div class="panel-body-b-r">
</div>
</div>
</div>
</div>
So the only difference is that the content styles are tl and not tl.
However (and this may be a big problem for you), template controls are not really meant to be filled from code - you'll notice that you are trying to write:
AcmeControl1.Footer.Controls.Add([...]);
will not compile.
However, you can call:
AcmeControl1.Footer = Page.LoadTemplate([...])
and pass the path to the ascx file .
Further reading on creating templated controls can be found:
- System.Web.UI.ITemplate
- How to create templated ASP.NET custom controls
- How to dynamically create templates in DataList web server controls
I said it would be long.
a source to share