L'interface ITemplate lors de la compilation d'une page en ASP.net
L'interface ITemplate est souvent mal comprise des développeurs, cette interface permet de faire des contrôles ayant la notion de template, c'est à dire que le développeur peut modifier certaines parties du code HTML généré par le contrôle, c'est le cas du Repeater, GridView ... Voyons comment l'utiliser dans un simple contrôle d'exemple.
[PersistChildren(false)]
[ParseChildren(true)]
public class TestTemplate : WebControl
{
private ITemplate _myTemplate;
[PersistenceMode(PersistenceMode.InnerProperty)]
public ITemplate MyTemplate
{
get { return _myTemplate; }
set { _myTemplate = value; }
}
protected override void CreateChildControls()
{
if (_myTemplate != null)
{
_myTemplate.InstantiateIn(this);
}
base.CreateChildControls();
}
}
on peut utiliser ce contrôle ainsi :
<test:testtemplate runat="server" id="tt">
<MyTemplate>
Coucou du template
</MyTemplate>
</test:testtemplate>
L'attribut PersistChildren permet de dire que le contrôle ne prend pas de contrôles enfant, comme c'est le cas par exemple pour un panel, l'attribut ParseChildren indique que le contenu du contrôle correspond aux valeurs de propriétés et enfin l'attribut PersistenceMode spécifie que cette propriété sera renseigné à l'interieur du contrôle.
Bien sur cet exemple est complétement inutile, les templates servent surtour lorsque vous avez des contrôles qui sont liés à une liste d'éléments, comme le repeater ou alors pour personaliser une partie du rendu d'un controle comme pour le Wizard
On peut également définir la propriété dynamiquement en utilisant la méthode Page.LoadTemplate("montemplate.ascx") ou alors en créant une classe qui implémentera ITemplate avec sa méthode InstanciateIn qui rajoutera les contrôles enfants dans le contrôle passé en paramètre de cette méthode. J'ai d'ailleurs parlé de la méthode LoadTemplate ici : Page.LoadTemplate - charger un UserControl dans une propriété ITemplate
Mais regardons plutôt ce qui se passe lors de la compilation de cette page avec Reflector :
private TestTemplate __BuildControltt()
{
TestTemplate template = new TestTemplate();
base.tt = template;
template.ApplyStyleSheetSkin(this);
template.MyTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control4));
template.ID = "tt";
return template;
}
private void __BuildControl__control4(Control __ctrl)
{
IParserAccessor accessor = __ctrl;
accessor.AddParsedSubObject(new LiteralControl("\r\n Coucou du template\r\n "));
}
Et voici le code de CompiledTemplateBuilder
public sealed class CompiledTemplateBuilder : ITemplate
{
// Fields
private BuildTemplateMethod _buildTemplateMethod;
// Methods
public CompiledTemplateBuilder(BuildTemplateMethod buildTemplateMethod)
{
this._buildTemplateMethod = buildTemplateMethod;
}
public void InstantiateIn(Control container)
{
this._buildTemplateMethod(container);
}
}
Lors de la compilation, la propriété MyTemplate prend une instance de CompiledTemplateBuilder qui ne fait qu'appeler la méthode passé en paramètre de son constructeur lorsqu'on appelle la méthode InstantiateIn. Autrement dit, lorsque l'on appelle la méthode InstantiateIn de la propriété de type ITemplate cela va rajouter le code HTML à la collection de contrôles de l'élément qu'on passe en paramètre. Dans notre exemple lors de l'appel de la méthode CreateChildControls, on appelle la méthode InstantiateIn qui va rajouter un new LiteralControl("\r\n Coucou du template\r\n ") à la collection de contrôle de notre contrôle.
Modifions un peu notre code afin d'utiliser l'expression "<%# Container", pour cela nous devons être dans un contexte de Binding, cela se produit lorsque l'on appel la méthode DataBind.
protected override void CreateChildControls()
{
if (_myTemplate != null)
{
_myTemplate.InstantiateIn(this);
this.DataBind();
}
base.CreateChildControls();
}
<test:testtemplate runat="server" id="tt">
<MyTemplate>
<%# "le type de container est " + Container.ToString() %>
</MyTemplate>
</test:testtemplate>
Comment est compilé cette page aspx, pour le savoir regardons le résultat de la publication avec Reflector.
private TestTemplate __BuildControltt()
{
TestTemplate template = new TestTemplate();
base.tt = template;
template.ApplyStyleSheetSkin(this);
template.MyTemplate = new CompiledTemplateBuilder(
new BuildTemplateMethod(this.__BuildControl__control4));
template.ID = "tt";
return template;
}
private void __BuildControl__control4(Control __ctrl)
{
DataBoundLiteralControl control = this.__BuildControl__control5();
IParserAccessor accessor = __ctrl;
accessor.AddParsedSubObject(control);
}
private DataBoundLiteralControl __BuildControl__control5()
{
// attention syntaxe C#3
DataBoundLiteralControl control = new DataBoundLiteralControl(2, 1) {
TemplateControl = this
};
control.SetStaticString(0, "\r\n ");
control.SetStaticString(1, "\r\n ");
control.DataBinding += new EventHandler(this.__DataBind__control5);
return control;
}
public void __DataBind__control5(object sender, EventArgs e)
{
DataBoundLiteralControl control2 = (DataBoundLiteralControl) sender;
Control bindingContainer = control2.BindingContainer;
control2.SetDataBoundString(0,
Convert.ToString("le type de container est " + bindingContainer.ToString(),
CultureInfo.CurrentCulture));
}
La différence par rapport à tout à l'heure et que la compilation créer un nouveau contrôle de type DataBoundLiteralControl qui contiendra tous le code html du template, ce code html est dynamique puisqu'il est modifié lors de l'événement DataBinding. Si on regarde la méthode __DataBind__control5, on voit que notre objet Container correspond à la propriété BindingContainer du DataBoundLiteralControl. Voici comment cette propriété est définit.
public Control BindingContainer
{
get
{
Control namingContainer = this.NamingContainer;
while (namingContainer is INonBindingContainer)
{
namingContainer = namingContainer.BindingContainer;
}
return namingContainer;
}
}
Cette propriété de type Control, ne fait que rechercher le NamingContainer du DataBoundLiteralControl, qui est lui même rajouté à notre contrôle. Dans notre cas le NamingContainer correspond donc à la page. Mais généralement le parent de la propriété ITemplate implémente INamingContainer. Pour rappel cette interface n'est qu'un marqueur qui permet d'avoir des ID de contrôles uniques, elle est donc indispensable si l'on utilise plusieurs fois le template dans notre contrôle, ce qui est le cas pour le Repeater.
On sait maintenant vers quel objet pointe l'objet Container. On peut donc l'utiliser un peu plus astucieusement, par exemple en rajouter une propriété à notre Container et ainsi y accéder directement dans la page aspx :
<test:TestTemplate runat="server" ID="tt" MyProperty="toto">
<MyTemplate>
<%# ((TestTemplate)Container).MyProperty %>
</MyTemplate>
</test:TestTemplate>
Nous avons vu tout à l'heure que l'objet Container était du type Control, il nous faut donc caster Container en TestTemplate pour avoir accès à notre propriété. L'attribut TemplateContainer permet justement de modifier le type de cet objet !
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(TestTemplate))]
public ITemplate MyTemplate
{
get { return _myTemplate; }
set { _myTemplate = value; }
}
nous permet alors d'écrire
<test:TestTemplate runat="server" ID="tt" MyProperty="toto">
<MyTemplate>
<%# Container.MyProperty %>
</MyTemplate>
</test:TestTemplate>
Nous verrons dans un prochain post un exemple d'utilisation plus concrète des Itemplate et de l'attribut TemplateContainer au sein d'un CompositeDataBoundControl.
Pour ceux qui sont interressé par savoir comment connaitre le résultat de la compilation d'une page ASP.net, j'en ai parlé ici : Obtenir le resultat de la compilation d'une page ASP.net