简体   繁体   中英

Create a custom server control with nested optional controls

I would like to create an custom server control, VersionedContentControl , which will allow me to specify different variations of the final markup.

Example usage:

<custom:VersionedContentControl runat="server" VersionToUse="2">
    <ContentVersions>
        <Content Version="1">
            <asp:HyperLink runat="server" ID="HomeLink" NavigateUrl="~/Default.aspx">Home</asp:HyperLink>
        </Content>
        <Content Version="2">
            <asp:LinkButton runat="server" ID="HomeLink" OnClick="GoHome">Home</asp:LinkButton>
        </Content>
        <Content Version="3">
            <custom:HomeLink runat="server" ID="HomeLink" />
        </Content>
    </ContentVersions>
</custom:VersionedContentControl>

Using the markup above, I would expect the only LinkButton control to be utilized on the page.

Long Story

I am having great difficulty trying to define this custom control. I haven't even been able to find a good example on the MSDN of using nested controls like this. Instead, I have had to resort to following these blog posts as examples:

Unfortunately, everything I have tried has failed miserably. Here is what I have currently:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.UI;

namespace CustomControls
{
    [ParseChildren(true)]
    [PersistChildren(false)]
    public class VersionedContentControl : Control, INamingContainer
    {
        public string VersionToUse { get; set; }

        [PersistenceMode(PersistenceMode.InnerProperty)]
        public IList<Content> ContentVersions { get; set; }

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            var controlToUse = ContentVersions.Single(x => x.Version == VersionToUse);
            Controls.Clear();
            controlToUse.InstantiateIn(this);
        }
    }

    public class Content : ITemplate
    {
        public string Version { get; set; }

        public void InstantiateIn(Control container)
        {
            // I don't know what this method should do
        }
    }

    public class ContentVersionsList : List<Content> {}
}

Even though I haven't implemented InstantiateIn , all 3 versions of my content appear on the page; it shows 3 links.

Also, I can't actually use the control unless I specify different ID property values for each nested control; I can't use "HomeLink" for all of them. I would like to be able to re-use the ID so that I can access the control from the code behind.

I realize that, normally, it is forbidden to specify duplicate ID values for multiple controls on a page. However, in the MSDN documentation for System.Web.UI.MobileControls.DeviceSpecific , the examples use duplicate ID values for nested controls. Infact, the example is very close to what I want to do; it differs content based on a mobile device compatibility filter.

<mobile:Form id="Form1" runat="server">
    <mobile:DeviceSpecific Runat="server">
        <Choice Filter="isHTML32">
            <HeaderTemplate>
                <mobile:Label ID="Label1" Runat="server">
                    Header Template - HTML32</mobile:Label>
                <mobile:Command Runat="server">
                    Submit</mobile:Command>
            </HeaderTemplate>
            <FooterTemplate>
                <mobile:Label ID="Label2" Runat="server">
                    Footer Template</mobile:Label>
            </FooterTemplate>
        </Choice>
        <Choice>
            <HeaderTemplate>
                <mobile:Label ID="Label1" Runat="server">
                    Header Template - Default</mobile:Label>
                <mobile:Command ID="Command1" Runat="server">
                    Submit</mobile:Command>
            </HeaderTemplate>
            <FooterTemplate>
                <mobile:Label ID="Label2" Runat="server">
                    Footer Template</mobile:Label>
            </FooterTemplate>
        </Choice>
    </mobile:DeviceSpecific>
</mobile:Form>

It would be nice to look at the source of those controls to see how they accomplish this but, unfortunately, it is not open-source.

My Question

How can I create a custom server control which contains a list of nested controls and only renders one of the nested controls based on a property? Ideally re-using IDs among separate nested controls.

tl;dr

I ended up using the MultiView control. It doesn't allow duplicate IDs but it satisfies all my other requirements and it allows me avoid having to maintain any custom code.

What I tried

I was finally able to get something working with this code:

using System.Collections.Generic;
using System.Linq;
using System.Web.UI;

namespace CustomControls
{
    [ParseChildren(true)]
    [PersistChildren(false)]
    public class VersionedContent : Control
    {
        public string VersionToUse { get; set; }

        [PersistenceMode(PersistenceMode.InnerProperty)]
        [TemplateContainer(typeof(ContentContainer))]
        [TemplateInstance(TemplateInstance.Multiple)]
        public List<Content> ContentVersions { get; set; }

        public override ControlCollection Controls
        {
            get
            {
                EnsureChildControls();
                return base.Controls;
            }
        }

        public ContentContainer ContentContainer
        {
            get
            {
                EnsureChildControls();
                return _contentContainer;
            }
        } private ContentContainer _contentContainer;

        protected override void CreateChildControls()
        {
            var controlToUse = ContentVersions.Single(x => x.Version == VersionToUse);
            Controls.Clear();
            _contentContainer = new ContentContainer();
            controlToUse.InstantiateIn(_contentContainer);
            Controls.Add(_contentContainer);
        }
    }

    public class Content : Control, ITemplate
    {
        public string Version { get; set; }

        public void InstantiateIn(Control container)
        {
            container.Controls.Add(this);
        }
    }

    public class ContentContainer : Control { }
}

This allows me to use the control like so:

<custom:VersionedContent ID="VersionedContentControl" runat="server" VersionToUse="1">
    <ContentVersions>
        <custom:Content Version="1">
            <custom:MyControlV1 runat="server" />
        </custom:Content>
        <custom:Content Version="2">
            <custom:MyControlV2 runat="server" CustomProperty="Foo" />
        </custom:Content>
    </ContentVersions>
</custom:VersionedContent>

Unfortunately, this solution had several drawbacks...

  • Even though I was using Template instances, I wasn't allowed to use duplicate IDs in separate Content sections
  • ViewState was not being handled properly
    • PostBack doesn't work properly
    • Validation doesn't work at all

After looking at the decompiled source code of the MultiView control (which is similar), I realized I would have had to make the code a lot more complicated to make it work as desired. The only thing I would gain over just using the MultiView control was possibly being able to use duplicate IDs, if I could even get that working. I decided it would be best to just settle for using the build-in MultiView control.

protected override void RenderContents(HtmlTextWriter output)
        {

            output.Write("<div><div class=\"UserSectionHead\">");

            Label l = new Label() { Text = Label };
            TextBox t = new TextBox() { Text = Text };
            l.AssociatedControlID = t.ID;
            l.RenderControl(output);

            output.Write("</div><div class=\"UserSectionBody\"><div class=\"UserControlGroup\"><nobr>");

            t.RenderControl(output);

            output.Write("</nobr></div></div><div style=\"width:100%\" class=\"UserDottedLine\"></div>");

        }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM