Introduction
Comment faire un controle riche permettant d’afficher un menu à plusieurs niveaux, en
utilisant les évènements, le databinding et les templates.
Connaître au préalable
Delegate et évènements
Le databinding
Les templated controles
Objectif
Définir le code source html pour
chacun des niveaux de l’arborescence, dans le gabarit.
Définir autant de niveau
différent que l’on souhaite sans modifier le contrôle.
Utiliser une source de donnée de
type datatable ou icollection
Modifier les éléments
crées
à la volée.
Hypothèse
La source de donnée sera déjà triè. On supposera que chaque élément de la source de donné est un noeud et qu’il est fils du précédent si l’identifiant de son père (idparent) est égal à
l’id de l’élément précédent.
Les différentes étapes
-
Définir et créer la classe
MenuControl
-
Créer la classe Item :
MenuControlItem
-
Ajouter le mécanisme de
template
.
-
Définir et créer le mécanisme de liaison aux données
-
Créer le gabarit et le code
behind
l’utilisant
Définir et créer la classe MenuControl
Créer une
nouvelle classe, la nommer MenuControl.
Hériter de
UserControl, pour bénéficier de l’ensemble des fonctionnalités
implémentés
pour cette classe.
Ajouter l’interface INamingContainer, afin de permettre à cette classe de définir un
namespace dans lequel chacun des contrôles crée aura un ClientID unique.
Voici le code source de ce contrôle.
///
<summary>
///
Summary description for
MenuControl
.
///
</summary>
public
class
MenuControl
:
UserControl,
INamingContainer
{
}
Créer la classe Item :
MenuControlItem
Il nous
faut pouvoir définir pour chaque niveau de l’arborescence:
-
le text pour afficher un libellé
-
l’image si image il faut afficher à la place du libellé
-
une url vers laquelle rediriger
-
un identifiant unique
-
l’identifiant parent pour établir la relation
père-fils.
-
le rang du contrôle
On aura besoin aussi plus tard de pouvoir :
-
y retrouver le conteneur interne sur lequel ajouter les
niveaux fils : Container
-
y ajouter l’objet provenant de la source de donnée
auquel est bindé le contrôle : DataItem
Code source :
///
<summary>
///
Represents
an item in the
MenuControl
control
///
</summary>
public
class
MenuControlItem
:
WebControl
,
INamingContainer
{
#region
Properties
///
<summary>
///
Gets or sets the text value for the control.
///
</summary>
public
string
Text
{
get
{
return
text;
}
set
{
text
=
value
;
}
}
private
string
text =
""
;
///
<summary>
///
Gets or sets the image value for the control.
///
</summary>
public
string
Image
{
get
{
return
image;
}
set
{
image
=
value
;
}
}
private
string
image =
""
;
///
<summary>
///
Gets or sets the rank value for the
control[
value
> 0].
///
</
summary
>
public
Int32 Rank
{
get
{
return
rank;
}
set
{
if
(
value
< 0)
throw
new
IndexOutOfRangeException
(
"Index
of a
MenuControlItem
can not be less than 0."
);
rank
=
value
;
}
}
private
Int32 rank = 0;
///
<
summary
>
///
Gets or sets the name of the inner container for the control.
///
</
summary
>
public
string
ContainerName
{
get
{
return
containerName;
}
set
{
containerName
=
value
;
}
}
private
string
containerName
=
""
;
///
<summary>
///
Gets
the inner container for the
control - if any.
///
</
summary
>
public
HtmlControl Container
{
get
{
return
(
HtmlControl
)
this
.FindControl
(
this
.ContainerName
);
}
}
///
<
summary
>
///
Gets or sets the
dataitem
bound to the
control.
///
</summary>
public
object
DataItem
{
get
{
return
dataItem
;
}
set
{
dataItem
=
value
;
}
}
private
object
dataItem
=
null
;
///
<summary>
///
Gets or sets the
url
value for the control
///
</summary>
public
string
Url
{
get
{
return
url
;
}
set
{
url
=
value
;
}
}
private
string
url =
""
;
#endregion
}
Ajouter le mécanisme de template
On voudrait permettre à l’utilisateur :
1.
de définir le code html de chaque niveau de
l’arborescence dans le gabarit
2.
de créer autant de modèle différent, qu’il aura de
niveau d’arborescence.
Pour le premier point il faut :
-
ajouter la propriété ItemTemplate – de type Itemplate -
à notre classe, pour recevoir les templates définis dans le gabarit
-
placer l’attribut TemplateContainer sur cette propriété
afin de caractèriser le type de template – ici : MenuControlItem.
-
ajouter à la classe MenuControl l’attribut
ParseChildren(
true
), afin que le lien entre les templates
définit dans le gabarit et l’attribut ItemTemplate de la classe se fasse.
Pour le second point :
- le
set
de la propriété ItemTemplate stockera non pas l’Itemplate obtenu dans une variable mais
plutôt dans une collection.
Voici le code source de la classe MenuControl modifié
///
<summary>
///
Summary description for
MenuControl
.
///
</summary>
[
ParseChildren
(
true
)]
public
class
MenuControl
:
UserControl,
INamingContainer
{
#region
Properties
public
event
MenuControlEventHandler
ItemDataBound
;
///
<summary>
///
Sets
the template for each level
of the control
///
</summary>
[
TemplateContainer
(
typeof
(
MenuControlItem
))]
public
ITemplate
ItemTemplate
{
set
{
if
(
itemTemplates
==
null
)
itemTemplates
=
new
ArrayList
();
itemTemplates.Add
(
value
);
}
}
private
ArrayList
itemTemplates
=
null
;
}
Définir et créer le
mécanisme de liaison aux données
On voudrait permettre à l’utilisateur du contrôle de réagir
à la création d’un élément :
-
définir une classe MenuControlEventArgs par laquel sera
transmis le contrôle crée
-
définir le delegate MenuControlEventHandler
-
créer un évènement ItemDataBound – de type
MenuControlEventHandler - auquel pourra s’abonner l’utilisateur du contrôle.
Voici le code ajouté en dehors du contrôle
///
<summary>
///
Represents
the
EventHandler
used
by the
MenuControl
control
///
</summary>
public
delegate
void
MenuControlEventHandler
(
object
sender,
MenuControlEventArgs
e);
///
<summary>
///
Represents
the
EventArgs
used by
the
MenuControl
control
///
</summary>
public
class
MenuControlEventArgs
:
EventArgs
{
///
<summary>
///
Gets or sets the
MenuControlItem
refered
to.
///
</summary>
public
MenuControlItem
Item
{
get
{
return
item;
}
set
{
item
=
value
;
}
}
private
MenuControlItem item =
null
;
}
De plus il faudra :
- avoir
une source de donnée : propriété DataSource.
///
<summary>
///
Gets or sets
the
DataSource
for the
control[
ICollection
or DataTable]
///
</summary>
public
object
DataSource
{
get
{
return
dataSource
;
}
set
{
dataSource
=
value
;
}
}
private
object
dataSource =
null
;
- permettre à l’utilisateur du contrôle de définir les champs de la source de donnée. Pour cela on créera la classe possédant l’ensemble des champs et on créera
une propriété public sur la classe MenuControl.
public
class
MenuControlDataField
{
///
<summary>
///
Get or sets the text property in the
datasource
for each template
///
</summary>
public
string
Text
{
get
{
return
text;
}
set
{
text
=
value
;
}
}
private
string
text =
"[Text]"
;
///
<summary>
///
Get or sets the image property in the
datasource
for each template
///
</summary>
public
string
Image
{
get
{
return
image;
}
set
{
image
=
value
;
}
}
private
string
image =
"[Image]"
;
///
<summary>
///
Get or sets the id property in the
datasource
for each template
///
</summary>
public
string
Id
{
get
{
return
id;
}
set
{
id
=
value
;
}
}
private
string
id =
"[Id]"
;
///
<summary>
///
Get or sets the
ParentID
property in the
datasource
for each template
///
</summary>
public
string
IdParent
{
get
{
return
idParent
;
}
set
{
idParent
=
value
;
}
}
private
string
idParent
=
"[
IdParent
]"
;
///
<summary>
///
Get or sets the
ParentID
property in the
datasource
for each template
///
</summary>
public
string
Url
{
get
{
return
url
;
}
set
{
url
=
value
;
}
}
private
string
url
=
"[
Url
]"
;
}
la
propriété publique sur le
contrôle MenuControl
public
MenuControlDataField
DataField
{
get
{
return
dataField
;
}
set
{
dataField
=
value
;
}
}
private
MenuControlDataField
dataField
=
new
MenuControlDataField
();
- réagir
à l’appel de la méthode DataBind
Important : lors du databind evaluer le type de source
de donné pour récuperer la bonne Collection.
///
<summary>
///
Create all
items from the
DataSource
///
</summary>
public
override
void
DataBind
()
{
base
.DataBind
(
);
ICollection
source =
null
;
if
(
DataSource
==
null
)
return
;
if
(
DataSource
is
DataTable)
source
=
((DataTable)DataSource).Rows;
object
lastObject
=
null
;
bool
DoWatch
=
false
;
foreach
(
object
o
in
source)
{
if
(
lastObject
==
null
||
DoWatch
)
lastObject
=
CreateItem
(source,
o, 0,
this
);
DoWatch
=
lastObject
== o;
}
}
Lors de la création de
chaque contrôle fils, il faut :
-
créer
le contrôle
-
y
placer les valeurs
obtenus
à partir de la source de
donnée
-
dans notre cas ajouter l’objet dataItem pour que l’utilisateur puisse s’en servir
dans son codebehind
-
récuperer
le template correspondant au rang et instantier le template dans le contrôle
-
l’ajouter
au contrôle courant pour que son clientID soit correcte
-
lever
l’évènement ItemDataBound pour permettre à l’utilisateur de le modifier
-
éventuellement
supprimer le contrôle si l’utilisateur l’a décidé.
-
et parcourir le reste de la source de donnée pour voir si ce contrôle n’a pas de
contrôle fils.
Voici le code source :
protected
object
CreateItem
(
ICollection
source,
object
dataItem
, Int32 rank, Control
parentControl
)
{
object
lastObject
=
dataItem
;
//define
the nested control
MenuControlItem
mci
=
new
MenuControlItem
(
);
mci.Text
=
DataBinder.Eval
(
dataItem
,
this
.DataField.Text
).
ToString
();
mci.Image
=
DataBinder.Eval
(
dataItem
,
this
.DataField.Image
).
ToString
();
mci.Url
=
DataBinder.Eval
(
dataItem
,
this
.DataField.Url
).
ToString
();
mci.DataItem
=
dataItem
;
mci.Rank
= rank;
mci.ContainerName
=
this
.ContainerName
+
rank.ToString
(
);
//find
the right template for the control
if
(
rank >
itemTemplates.Count
- 1)
throw
new
IndexOutOfRangeException
(
string
.Format
(
"Template
not defined for the object
MenuControlItem
[rank =
{0}]."
, rank));
ITemplate
it = (
ITemplate
)
itemTemplates
[rank];
it.InstantiateIn(mci);
mci.DataBind();
//adding the control
parentControl.Controls.Add
(
mci
);
//Give
user a view on the control before adding it to the collections off controls
MenuControlEventArgs
e =
new
MenuControlEventArgs
(
);
e.Item
=
mci
;
if
(
this
.ItemDataBound
!=
null
)
this
.ItemDataBound
(
this
, e);
//User
can disable the control and sons of this control
//by
setting the
Visible
property of the
MenuControlItem
to false
if
(
!
mci.Visible
)
parentControl.Controls.Remove
(
mci
);
//find
if there is any children control in the
datastream
bool
DoWatch
=
false
;
string
currentId
=
DataBinder.Eval
(
dataItem
,
this
.DataField.Id
).
ToString
();
Control
innerControl
=
null
;
foreach
(
object
o
in
source)
{
if
(
DoWatch
)
{
//if current
dataItem
is parent
for current element o
string
idparent
=
DataBinder.Eval
(o,
this
.DataField.IdParent
).
ToString
();
string
text =
DataBinder.Eval
(o,
this
.DataField.Text
).
ToString
();
HttpContext.Current.Trace.Warn
(
text +
":"
+
idparent
+
":"
+
currentId
);
if
(
currentId.Equals
(
idparent
))
{
if
(
innerControl
==
null
)
{
//finding the inner container
innerControl
=
mci.FindControl
(
mci.ContainerName
);
if
(
innerControl
==
null
)
throw
new
NullReferenceException
(
string
.Format
(
"Container control not defined for rank {0}"
,
rank));
}
//create element as son
lastObject
=
CreateItem
(source,
o, rank + 1,
innerControl
);
DoWatch
=
false
;
}
else
break
;
}
if
(
dataItem
== o ||
o ==
lastObject
)
DoWatch =
true
;
}
return
lastObject;
}
Créer le gabarit
Il faut ici importer dans le gabarit le contrôle
a
l’aide de l’instruction de pré-parsing :
<%@ Register
tagprefix
="control"
Namespace="
Website.Templated
"
assembly="General"%>
Puis
définir
le
contrôle
:
Utiliser le databinding pour le remplir dans le code behind
///
<summary>
///
Summary description for Menu.
///
</summary>
public
class
Menu :
System.Web.UI.Page
{
#region
Properties
protected
MenuControl
MenuMain
;
#
endregion
private
void
Page_Load
(
object
sender,
System.EventArgs
e)
{
DataTable
dt
=
GetDataSource
(
);
MenuMain.ItemDataBound
+=
new
MenuControlEventHandler
(
this
.MenuMain_ItemDataBound
);
MenuMain.DataSource = dt;
MenuMain.DataBind(
);
}
protected
void
MenuMain_ItemDataBound
(
object
sender,
MenuControlEventArgs
e)
{
if
(
e.Item.Rank
== 0)
{
e.Item.Attributes
[
"
onclick
"
]
=
string
.Format
(
"
javascript:Hide
('{0}')"
,
e.Item.Container.ClientID
);
}
}
public
DataTable
GetDataSource
(){}
}
Conclusion
Nous avons vu à travers cet exemple la puissance des templated controls. Ici la séparation
gabarit - présentation - et codebehind – logique –
prend
tout son sens, et il devient dès lors facile de modifier le rendu sans toucher
au code de logique.
Grace au DataBinding et aux évènements nous avons même permis à l’utilisateur du contrôle de modifier le rendu au cours du DataBinding dans le codebehind pour
une plus grande souplesse.
Vous êtes
maintenant prêt à créer vous même vos propre templated control.