ASP Nested Tags in a Custom User Control
I wrote a blog post about this some time ago. In brief, if you had a control with the following markup:
<Abc:CustomControlUno runat="server" ID="Control1"> <Children> <Abc:Control1Child IntegerProperty="1" /> </Children></Abc:CustomControlUno>
You'd need the code in the control to be along the lines of:
[ParseChildren(true)][PersistChildren(true)][ToolboxData("<{0}:CustomControlUno runat=server></{0}:CustomControlUno>")]public class CustomControlUno : WebControl, INamingContainer{ private Control1ChildrenCollection _children; [PersistenceMode(PersistenceMode.InnerProperty)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public Control1ChildrenCollection Children { get { if (_children == null) { _children = new Control1ChildrenCollection(); } return _children; } }}public class Control1ChildrenCollection : List<Control1Child>{}public class Control1Child{ public int IntegerProperty { get; set; }}
I followed Rob's blog post, and made a slightly different control. The control is a conditional one, really just like an if-clause:
<wc:PriceInfo runat="server" ID="PriceInfo"> <IfDiscount> You don't have a discount. </IfDiscount> <IfNotDiscount> Lucky you, <b>you have a discount!</b> </IfNotDiscount></wc:PriceInfo>
In the code I then set the HasDiscount
property of the control to a boolean, which decides which clause is rendered.
The big difference from Rob's solution, is that the clauses within the control really can hold arbitrary HTML/ASPX code.
And here is the code for the control:
using System.ComponentModel;using System.Web.UI;using System.Web.UI.WebControls;namespace WebUtilities{ [ToolboxData("<{0}:PriceInfo runat=server></{0}:PriceInfo>")] public class PriceInfo : WebControl, INamingContainer { private readonly Control ifDiscountControl = new Control(); private readonly Control ifNotDiscountControl = new Control(); public bool HasDiscount { get; set; } [PersistenceMode(PersistenceMode.InnerProperty)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public Control IfDiscount { get { return ifDiscountControl; } } [PersistenceMode(PersistenceMode.InnerProperty)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public Control IfNotDiscount { get { return ifNotDiscountControl; } } public override void RenderControl(HtmlTextWriter writer) { if (HasDiscount) ifDiscountControl.RenderControl(writer); else ifNotDiscountControl.RenderControl(writer); } }}
I ended up with something very similar to the answer by Rob (in wayback archive) @gudmundur-h, but I used ITemplate
to get rid of that annoying "You can't place content between X tags" in the usage. I'm not entirely sure what is actually required or not, so it's all here just in case.
The partial/user control markup: mycontrol.ascx
Note the important bits: plcChild1
and plcChild2
.
<!-- markup, controls, etc --><div class="shell"> <!-- etc --> <!-- optional content with default, will map to `ChildContentOne` --> <asp:PlaceHolder ID="plcChild1" runat="server"> Some default content in the first child. Will show this unless overwritten. Include HTML, controls, whatever. </asp:PlaceHolder> <!-- etc --> <!-- optional content, no default, will map to `ChildContentTwo` --> <asp:PlaceHolder ID="plcChild2" runat="server"></asp:PlaceHolder></div>
The partial/user control codebehind: mycontrol.ascx.cs
[ParseChildren(true), PersistChildren(true)][ToolboxData(false /* don't care about drag-n-drop */)]public partial class MyControlWithNestedContent: System.Web.UI.UserControl, INamingContainer { // expose properties as attributes, etc /// <summary> /// "attach" template to child controls /// </summary> /// <param name="template">the exposed markup "property"</param> /// <param name="control">the actual rendered control</param> protected virtual void attachContent(ITemplate template, Control control) { if(null != template) template.InstantiateIn(control); } [PersistenceMode(PersistenceMode.InnerProperty), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public virtual ITemplate ChildContentOne { get; set; } [PersistenceMode(PersistenceMode.InnerProperty), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public virtual ITemplate ChildContentTwo { get; set; } protected override void CreateChildControls() { // clear stuff, other setup, etc // needed? base.CreateChildControls(); this.EnsureChildControls(); // cuz...we want them? // using the templates, set up the appropriate child controls attachContent(this.ChildContentOne, this.plcChild1); attachContent(this.ChildContentTwo, this.plcChild2); }}
Important bits (?):
ParseChildren
-- so stuff shows up?PersistChildren
-- so dynamically created stuff doesn't get reset?PersistenceMode(PersistenceMode.InnerProperty)
-- so controls are parsed correctlyDesignerSerializationVisibility(DesignerSerializationVisibility.Content)
-- ditto?
The control usage
<%@ Register Src="~/App_Controls/MyStuff/mycontrol.ascx" TagPrefix="me" TagName="MyNestedControl" %><me:MyNestedControl SomeProperty="foo" SomethingElse="bar" runat="server" ID="meWhatever"> <%-- omit `ChildContentOne` to use default --%> <ChildContentTwo>Stuff at the bottom! (not empty anymore)</ChildContentTwo></me:MyNestedControl>