Tvorba templated controls v ASP.NET, včetně design time podpory

Templatovatelné prvky jsou způsob, jak lze v ASP.NET vytvářet server controls, které mohou v markupu obsahovat další controls. Základní postup pro jejich vytvoření je vcelku jednoduchý, ale pokud chcete, aby váš control byl stejně komfortní jako ty vestavěné, dá to trochu víc práce.

Poměrně často používám na svých stránkách "boxy", které mají zhruba následující HTML kód:

<div class="box">

    <div class="box-header">Text hlavičky</div>

    <div class="box-content">Text těla</div>

</div>

Uvedený kód může být samozřejmě jednodušší (pro jednoduché formátování, bez obalujícího DIVu) a nebo výrazně složitější (pro různé efekty s hlavičkou a obsahem), nicméně jeho logika je vždy stejná: hlavička a tělo, přičemž obojí může obsahovat buďto prostý text a nebo další server controls.

První generace

Budu tedy chtít vytvořit jednoduchý control Box, který bude mít vlastnosti HeaderText a ContentText a možnost specifikovat HeaderTemplate a ContentTemplate, pokud mi prostý text nebude stačit. Samotné vytvoření controlu je dost jednoduché, lze při něm vycházet například z návodu, který najdete na MSDN. První verze kódu – s minimální implementací - tedy vypadá takto:

using System;

using System.Web.UI;

using System.Web.UI.WebControls;

 

namespace MyControls {

 

    [ParseChildren(true)]

    public class Box : Control, INamingContainer {

        public class TemplateContainer : Control, INamingContainer { }

        private TemplateContainer headerTC, contentTC;

 

        public Box() {

            this.CssClass = "box";

            this.CssClassHeader = "box-header";

            this.CssClassContent = "box-content";

        }

 

        [TemplateContainer(typeof(TemplateContainer))]

        public ITemplate HeaderTemplate { get; set; }

 

        [TemplateContainer(typeof(TemplateContainer))]

        public ITemplate ContentTemplate { get; set; }

 

        public string HeaderText {

            get { return (string)this.ViewState["HeaderText"]; }

            set { this.ViewState["HeaderText"] = value; }

        }

 

        public string ContentText {

            get { return (string)this.ViewState["ContentText"]; }

            set { this.ViewState["ContentText"] = value; }

        }

 

        public string CssClass {

            get { return (string)this.ViewState["CssClass"]; }

            set { this.ViewState["CssClass"] = value; }

        }

 

        public string CssClassHeader {

            get { return (string)this.ViewState["CssClassHeader"]; }

            set { this.ViewState["CssClassHeader"] = value; }

        }

 

        public string CssClassContent {

            get { return (string)this.ViewState["CssClassContent"]; }

            set { this.ViewState["CssClassContent"] = value; }

        }

 

        protected override void OnDataBinding(EventArgs e) {

            this.EnsureChildControls();

            base.OnDataBinding(e);

        }

 

        protected override void CreateChildControls() {

            // Create core layout

            var boxPanel = new Panel { CssClass = this.CssClass };

            var headerPanel = new Panel { CssClass = this.CssClassHeader };

            var contentPanel = new Panel { CssClass = this.CssClassContent };

            boxPanel.Controls.Add(headerPanel);

            boxPanel.Controls.Add(contentPanel);

            this.Controls.Add(boxPanel);

 

            // Create header

            if (this.HeaderTemplate == null) {

                headerPanel.Controls.Add(new LiteralControl(this.HeaderText));

            }

            else {

                this.headerTC = new TemplateContainer();

                this.HeaderTemplate.InstantiateIn(this.headerTC);

                headerPanel.Controls.Add(this.headerTC);

            }

 

            // Create content

            if (this.ContentTemplate == null) {

                contentPanel.Controls.Add(new LiteralControl(this.ContentText));

            }

            else {

                this.contentTC = new TemplateContainer();

                this.ContentTemplate.InstantiateIn(this.contentTC);

                contentPanel.Controls.Add(this.contentTC);

            }

        }

 

    }

 

}

Kód testovací stránky vypadá následovně:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<%@ Page Language="C#" %>

<%@ Register TagPrefix="my" Namespace="MyControls" %>

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">

    <title></title>

    <style type="text/css">

        .box {

            border: solid 1px #990000;

            width: 300px;

        }

        .box-header {

            background-color: #990000;

            color: #ffffff;

            padding: 1ex;

        }

        .box-content {

            background-color: #eeeeee;

            padding: 1ex;

        }

    </style>

</head>

<body>

    <form id="form1" runat="server">

    <my:Box ID="Box1" runat="server" HeaderText="Text hlavičky">

        <ContentTemplate>

            <asp:TextBox ID="TextBox1" runat="server" />

        </ContentTemplate>

    </my:Box>

    </form>

</body>

</html>

Druhá generace: property grid a IntelliSense

Popsaný control funguje jak má. Což je bohužel asi tak všechno, co se o něm dá říct pozitivního. Design time podpora veškerá žádná. IntelliSense tag <ContentTemplate> nezná, takže editor se bude vztekat. V náhledovém režimu se zobrazí pouze prostý text [ Control: Box1] a víc z něj nedostanete… Řešení problému je přitom celkem snadné, druhá postupná verze vypadá následovně (změny jsou označené tučně):

using System;

using System.ComponentModel;

using System.Web.UI;

using System.Web.UI.WebControls;

 

namespace MyControls {

 

    [ParseChildren(true), PersistChildren(false)]

    public class Box : Control, INamingContainer {

        public class TemplateContainer : Control, INamingContainer { }

        private TemplateContainer headerTC, contentTC;

 

        public Box() {

            this.CssClass = "box";

            this.CssClassHeader = "box-header";

            this.CssClassContent = "box-content";

        }

 

        [TemplateContainer(typeof(TemplateContainer))]

        [Browsable(false), PersistenceMode(PersistenceMode.InnerProperty)]

        public ITemplate HeaderTemplate { get; set; }

 

        [TemplateContainer(typeof(TemplateContainer))]

        [Browsable(false), PersistenceMode(PersistenceMode.InnerProperty)]

        public ITemplate ContentTemplate { get; set; }

 

        [Category("Behavior"), Description("Text of box header if HeaderTemplate is not specified.")]

        public string HeaderText {

            get { return (string)this.ViewState["HeaderText"]; }

            set { this.ViewState["HeaderText"] = value; }

        }

 

        [Category("Behavior"), Description("Text of box content if ContentTemplate is not specified.")]

        public string ContentText {

            get { return (string)this.ViewState["ContentText"]; }

            set { this.ViewState["ContentText"] = value; }

        }

 

        [Category("Appearance"), Description("Box CSS class"), DefaultValue("box"), CssClassProperty]

        public string CssClass {

            get { return (string)this.ViewState["CssClass"]; }

            set { this.ViewState["CssClass"] = value; }

        }

 

        [Category("Appearance"), Description("Box header CSS class"), DefaultValue("box-header"), CssClassProperty]

        public string CssClassHeader {

            get { return (string)this.ViewState["CssClassHeader"]; }

            set { this.ViewState["CssClassHeader"] = value; }

        }

 

        [Category("Appearance"), Description("Box content CSS class"), DefaultValue("box-content"), CssClassProperty]

        public string CssClassContent {

            get { return (string)this.ViewState["CssClassContent"]; }

            set { this.ViewState["CssClassContent"] = value; }

        }

 

        protected override void OnDataBinding(EventArgs e) {

            this.EnsureChildControls();

            base.OnDataBinding(e);

        }

 

        protected override void CreateChildControls() {

            // Create core layout

            var boxPanel = new Panel { CssClass = this.CssClass };

            var headerPanel = new Panel { CssClass = this.CssClassHeader };

            var contentPanel = new Panel { CssClass = this.CssClassContent };

            boxPanel.Controls.Add(headerPanel);

            boxPanel.Controls.Add(contentPanel);

            this.Controls.Add(boxPanel);

 

            // Create header

            if (this.HeaderTemplate == null) {

                headerPanel.Controls.Add(new LiteralControl(this.HeaderText));

            }

            else {

                this.headerTC = new TemplateContainer();

                this.HeaderTemplate.InstantiateIn(this.headerTC);

                headerPanel.Controls.Add(this.headerTC);

            }

 

            // Create content

            if (this.ContentTemplate == null) {

                contentPanel.Controls.Add(new LiteralControl(this.ContentText));

            }

            else {

                this.contentTC = new TemplateContainer();

                this.ContentTemplate.InstantiateIn(this.contentTC);

                contentPanel.Controls.Add(this.contentTC);

            }

        }

 

    }

 

}

Provedené změny spočívají pouze v odekorování třídy a vlastností několika atributy. Převážná vtěšina z nich se zabývá zpřehledněním zobrazení v property gridu a nachází se v namespace System.ComponentModel:

  • Category určuje, do jaké kategorie se vlastnost zařadí při zobrazení v property gridu. Můžeme použít obvyklé kategorie, nebo si vytvořit vlastní. Výsledkem v každém případě bude lepší přehlednost gridu, že všechny vlastnosti nebudou nasypané na jedné hromadě v kategorii Misc.
  • Description je textový popis vlastnosti, který se zobrazí ve spodní části property gridu.
  • DefaultValue je výchozí hodnota. V gridu budou tučně označeny jenom ty vlastnosti, jejichž hodnoty se odlišují od defaultu.
  • Browsable(false) způsobí, že se takto označená metoda nebude v gridu zobrazovat vůbec, což se hodí mimo jiné právě v případě šablon.
  • CssClassProperty by měl způsobit, že bude hodnota vlastnosti rozpoznána jako CSS třída a bude se tedy nabízet seznam tříd v IntelliSense a rozbalovací seznam v gridu. Bohužel to z mně neznámých důvodů nefunguje, VS2008SP1 se orientuje pouze podle názvu vlastnosti (musí se jmenovat CssClass) a atribut ignoruje. Podle mne se jedná o chybu ve v Visual Studiu, budu to dále řešit.

Atributy PersistChildren a PersistenceMode slouží k indikaci, že daná vlastnost není realizována klasicky HTML atributem, ale právě vnořeným elementem. Vyřeší tedy problém s IntelliSense.

Třetí generace: Designer

Stále ale nemáme u tohoto controlu podporu pro design mode, tedy rozumné zobrazení prvku v designeru ani podporu pro vizuální editaci šablon. Tady už si s prostými atributy nevystačíme, a potřebujeme designer. Designer je speciální pomocná třída, poděděná od base class ControlDesigner. Nemá žádný vliv na vlastní funkčnost prvku jako takového, slouží čistě pro podporu v design time režimu.

Template editor smart tag

Designer využijeme pro dvě v podstatě nezávislé věci. Za prvé, přepíšeme v něm metodu GetDesignTimeHtml, která vrací HTML kód, který se použije pro zobrazení na ploše vizuálního návrhu. Za druhé, sdělíme Visual Studiu, jaké šablony náš prvek používá, aby mohlo vygenerovat odpovídající rozhraní ve smart tagu.Zdrojový kód bude vypadat následovně:

using System;

using System.ComponentModel;

using System.Web.UI;

using System.Web.UI.Design;

using System.Web.UI.WebControls;

 

namespace MyControls {

 

    [ParseChildren(true), PersistChildren(false), Designer(typeof(BoxDesigner))]

    public class Box : Control, INamingContainer {

        // Kód třídy samé je stejný jako v předchozím případě

    }

 

    public class BoxDesigner : ControlDesigner {

 

        #region Implementace podpory šablon

 

        private TemplateGroupCollection templateGroups;

 

        public override void Initialize(IComponent component) {

            base.Initialize(component);

            base.SetViewFlags(ViewFlags.TemplateEditing, true);

        }

 

        public override TemplateGroupCollection TemplateGroups {

            get {

                if (this.templateGroups == null) {

                    this.templateGroups = new TemplateGroupCollection();

                    TemplateGroup group = new TemplateGroup("Templates");

                    group.AddTemplateDefinition(new TemplateDefinition(this, "Header", this.Component, "HeaderTemplate", false));

                    group.AddTemplateDefinition(new TemplateDefinition(this, "Content", this.Component, "ContentTemplate", false));

                    this.templateGroups.Add(group);

                }

                return this.templateGroups;

            }

        }

 

        #endregion

 

        #region Implementace zobrazení v designeru

 

        public override string GetDesignTimeHtml() {

            var control = this.Component as Box;

            var sb = new System.Text.StringBuilder();

            sb.AppendFormat("<div class=\"{0}\"><div class=\"{1}\">", control.CssClass, control.CssClassHeader);

            sb.Append(this.GetHeaderDesignTimeHtml());

            sb.AppendFormat("</div><div class=\"{0}\">", control.CssClassContent);

            sb.Append(this.GetContentDesignTimeHtml());

            sb.Append("</div></div>");

            return sb.ToString();

        }

 

        private string GetHeaderDesignTimeHtml() {

            var control = this.Component as Box;

            if (control.HeaderTemplate != null) {

                using (var ph = new PlaceHolder()) {

                    control.HeaderTemplate.InstantiateIn(ph);

                    return RenderControlToString(ph);

                }

            }

            else if (!string.IsNullOrEmpty(control.HeaderText)) {

                return control.HeaderText;

            }

            else {

                return "[Header]";

            }

        }

 

        private string GetContentDesignTimeHtml() {

            var control = this.Component as Box;

            if (control.ContentTemplate != null) {

                using (var ph = new PlaceHolder()) {

                    control.ContentTemplate.InstantiateIn(ph);

                    return RenderControlToString(ph);

                }

            }

            else if (!string.IsNullOrEmpty(control.ContentText)) {

                return control.ContentText;

            }

            else {

                return "[Content]";

            }

        }

 

        private static string RenderControlToString(Control control) {

            if (control == null) throw new ArgumentNullException("control");

            using (var tw = new System.IO.StringWriter())

            using (var hw = new HtmlTextWriter(tw)) {

                control.RenderControl(hw);

                return tw.ToString();

            }

        }

 

        #endregion

 

    }

 

}

Vytvoříme třídu BoxDesigner, kterou podědíme od ControlDesigner. Původní control sám jenom odekorujeme atributem Designer, jinak kód zůstává nezměněn. Jinak je kód myslím dosti polopatický.

Na závěr se snad sluší jenom připomenout, že až budete experimentovat s design mode podporou server controls, nezapomeňte vždy zavřít a znovu otevřít ASPX soubor, protože jedině tak se obnoví cache a nebudete pracovat se starou verzí.

  • Altairis
  • Nemesis
  • Microsoft MVP
  • IIS
  • ASP.NET