Maîtriser les événements ASP.NET - Partie 4/5
Auteur : Frédéric De Lène Mirouze (amethyste16@hotmail.com)
8 - Architecture des événements ASP côté serveur
Les événements sans données de publication
Cas où le composant doit générer le javascript d'appel de l'événement
Cas où il y a plusieurs événements
Les événements avec données de publication
Cas d'un composant avec un seul contrôle
Cas d'un composant avec plusieurs contrôles
Percolation d'un événement à travers la hiérarchie des composants
9 - Les événements asynchrones
Nous avons vu comment ASP lève un événement du point de vue de la page HTML. Mais que se passe t'il côté serveur?
ASP s'appuie sur deux interfaces: IPostBackEventHandler et IPostBackDataHandler. Dès qu'un contrôle implémente l'une d'entre elle ou bien les deux, celui-ci est automatiquement inséré dans la chaîne de gestion des événements ASP. Il n'y a rien de plus à faire.
Il y a deux interfaces parce que l'on distingue deux types d'événement qui vont chacun correspondre à une plomberie spécifique:
Voyons les détails ensembles.
D'abord examinons notre
interface IPostBackEventHandler
. Elle ne déclare qu'une seule méthode: RaisePostBackEvent.
Cette méthode est automatiquement appelée par ASP pour demander au composant
d'exécuter la logique qui lève ou non ses événements.
Il appartient bien sûr au concepteur du composant de l'implémenter.
Nous allons développer un composant NewDdl qui hérite de DropDownList, mais implémente un nouvel événement. Cet événement se déclenche chaque fois que l'on sélectionne un article d'indice supérieur à 10 dans la liste.
Listing 8-1: Premier composant
Code C#
public sealed class NewDdl : DropDownList,IPostBackEventHandler
{public NewDdl()
{ } public event EventHandler ValeurElevee;
protected void OnValeurElevee(EventArgs e)
{if (ValeurElevee != null)
{ValeurElevee(this, e);
} } void IPostBackEventHandler.RaisePostBackEvent(string eventArgument)
{if (this.SelectedIndex > 10)
{ OnValeurElevee(EventArgs.Empty); } } protected override bool LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
{this.Page.RegisterRequiresRaiseEvent(this);
return base.LoadPostData(postDataKey, postCollection);
} protected override void OnPreRender(EventArgs e)
{base.OnPreRender(e);
this.Page.RegisterRequiresRaiseEvent(this);
} protected override void Render(HtmlTextWriter writer)
{
if (this.Page != null)
{// indispensable pour que les propriétés du contrôle soient postées
this.Page.VerifyRenderingInServerForm(this);
}base.Render(writer);
}}
Code VB
Public NotInheritable Class NewDdl
Inherits DropDownList
Implements IPostBackEventHandler
Public Sub New()
End Sub
Public Event ValeurElevee As EventHandler
Protected Sub OnValeurElevee(ByVal e As EventArgs)
RaiseEvent ValeurElevee(Me, e)
End Sub
Sub IPostBackEventHandler.RaisePostBackEvent(ByVal eventArgument As String)
If Me.SelectedIndex > 10 Then
OnValeurElevee(EventArgs.Empty) End If
End Sub
Protected Overloads Overrides Function LoadPostData(ByVal postDataKey As String, ByVal postCollection As System.Collections.Specialized.NameValueCollection) As Boolean
Me.Page.RegisterRequiresRaiseEvent(Me)
Return MyBase.LoadPostData(postDataKey, postCollection)
End Function
Protected Overloads Overrides Sub OnPreRender(ByVal e As EventArgs)
MyBase.OnPreRender(e)
Me.Page.RegisterRequiresRaiseEvent(Me)
End Sub
Protected Overloads Overrides Sub Render(ByVal writer As HtmlTextWriter)
If Not (Me.Page Is Nothing) Then
' indispensable pour que les propriétés du contrôle soient postées
Me.Page.VerifyRenderingInServerForm(Me)
End If
MyBase.Render(writer)
End Sub
End Class
On commence par implémenter un événement ValeurElevee selon le modèle standard avec la méthode On (voir l'annexe dans la première partie du tutoriel).
Le contrôle implémente IPostBackEventHandler, ASP va donc appeler RaisePostBack à un certain moment du cycle de vie de la page, nous verrons quand dans la dernière partie du tutoriel. Le code de cette méthode est simple, il exécute un test pour savoir s'il doit lever ou non l'événement. Dans ce cas il appelle la méthode OnValeurElevee.
Note:
Les appels à RegisterRequiresRaiseEvent
sont rendus nécessaires parce que le composant hérite d'un contrôle qui implémente
IPostBackDataHandler. Sans cet
appel RaisePostBack n'est pas
appelé, nous verrons pourquoi plus loin. Si IPostBackDataHandler
n'est pas implémenté, l'appel à RaisePostBack
nest pas nécessaire comme nous le constaterons aussi avec les deux exemples qui
suivent.
Ne me dites pas que c'est difficile!
Examinons un autre cas de figure, le composant NewLabel:
Listing 8-2: Composant NewLabel
Code C#
public class NewLabel : WebControl, IPostBackEventHandler
{public NewLabel()
{ } public string Text
{ get {if (this.ViewState["Text"] == null)
{return "Hello le monde";
}return this.ViewState["Text"] as string;
} set {this.ViewState["Text"] = value;
} } public event EventHandler Click;
protected void OnClick(EventArgs e)
{if (Click != null)
{Click(this,e);
} } public void RaisePostBackEvent(string eventArgument)
{this.OnClick(EventArgs.Empty);
} protected override void RenderContents(HtmlTextWriter writer)
{writer.Write(this.Text);
} protected override void Render(HtmlTextWriter writer)
{
if (this.Page != null)
{// indispensable pour que les propriétés du contrôle soient postées
this.Page.VerifyRenderingInServerForm(this);
}base.Render(writer);
}}
Code VB
Public Class NewLabel
Inherits WebControl
Implements IPostBackEventHandler
Public Sub New()
End Sub
Public Property Text() As String
Get
If Me.ViewState("Text") Is Nothing Then
Return "Hello le monde"
End If
Return CType(ConversionHelpers.AsWorkaround(Me.ViewState("Text"), GetType(String)), String)
End Get
Set
Me.ViewState("Text") = value
End Set
End Property
Public Event Click As EventHandler
Protected Sub OnClick(ByVal e As EventArgs)
RaiseEvent Click(Me, e)
End Sub
Sub IPostBackEventHandler.RaisePostBackEvent(ByVal eventArgument As String)
Me.OnClick(EventArgs.Empty)
End Sub
Protected Overloads Overrides Sub RenderContents(ByVal writer As HtmlTextWriter)
writer.Write(Me.Text)
End Sub
Protected Overloads Overrides Sub Render(ByVal writer As HtmlTextWriter)
If Not (Me.Page Is Nothing) Then
Me.Page.VerifyRenderingInServerForm(Me)
End If
MyBase.Render(writer)
End Sub
End Class
Le composant fonctionne correctement, il expose un événement Click auquel on peut s'abonner. Malheureusement rien n'est prévu pour le déclencher bien que nous ayons implémenté IPostBackEventHandler.
La raison est qu'il manque côté client le javascript nécessaire. Dans l'exemple précédent nous héritions de DropDownList et récupérions son événement standard SelectedIndexChanged. L'ajout du script client était donc déjà implémenté.
Dans notre cas il nous appartient de le faire. Nous avons vu au chapitre 7 comment s'y prendre, il n'y a rien de nouveau ici.
Listing 8-3: Ajout du déclenchement des événements
Code C#
protected override void AddAttributesToRender(HtmlTextWriter writer)
{base.AddAttributesToRender(writer);
string DoPostBack =
this.Page.ClientScript.GetPostBackEventReference(new PostBackOptions(this));
writer.AddAttribute(HtmlTextWriterAttribute.Onclick, DoPostBack);}
Code VB
Protected Overloads Overrides Sub AddAttributesToRender(ByVal writer As HtmlTextWriter)
MyBase.AddAttributesToRender(writer)
Dim DoPostBack As String =
Me.Page.ClientScript.GetPostBackEventReference(New PostBackOptions(Me))
writer.AddAttribute(HtmlTextWriterAttribute.Onclick, DoPostBack) End Sub
Nous disposons donc d'un contrôle de type Label qui lève un événement Click lorsque l'utilisateur clique dessus. Je ne vais pas jusqu'à dire que c'est le genre de comportement qu'il est normal de développer, mais je voulais un exemple à partir d'un contrôle que l'on ne puisse soupçonner d'être pollué par une plomberie pré existante.
Notez également que contrairement à l'exemple précédent, nous n'avons nul besoin de faire un appel à RegisterRequiresRaiseEvent.
Notre nouveau composant affiche une valeur numérique et en dessous deux labels Plus et Moins qui incrémentent ou décrémentent la valeur numérique.
![]()
Nous souhaitons disposer de deux événements: un pour chacune des actions. Tant que l'on y est on passera dans l'événement la valeur affichée.
Listing 8-4: Composant Compteur
Code C#
public class Compteur : WebControl, IPostBackEventHandler
{public Compteur()
{ } public int Total
{get
{if (this.ViewState["Total"] == null)
{return 0;
}return Convert.ToInt32(this.ViewState["Total"]);
} set {this.ViewState["Total"] = value;
} } public delegate void CompteurEventHandler(object sender, CompteurEventArgs e);
public event CompteurEventHandler Plus;
protected void OnPlus(CompteurEventArgs e)
{if (Plus != null)
{
Plus(this, e);
} } public event CompteurEventHandler Moins;
protected void OnMoins(CompteurEventArgs e)
{if (Moins != null)
{Moins(this, e);
} } void IPostBackEventHandler.RaisePostBackEvent(string eventArgument)
{
if (eventArgument == "plus")
{this.Total++;
this.OnPlus(new CompteurEventArgs(this.Total));
}else
{this.Total--;
this.OnMoins(new CompteurEventArgs(this.Total));
} } protected override void RenderContents(HtmlTextWriter writer)
{writer.Write(this.Total);
writer.WriteBreak(); // <br/>
string DoPostBack =
this.Page.ClientScript.GetPostBackEventReference(this, "moins");
writer.AddAttribute(HtmlTextWriterAttribute.Onclick, DoPostBack); writer.RenderBeginTag(HtmlTextWriterTag.Span);writer.Write("Moins");
writer.RenderEndTag(); writer.Write(" ");
DoPostBack = this.Page.ClientScript.GetPostBackEventReference(this, "plus");
writer.AddAttribute(HtmlTextWriterAttribute.Onclick, DoPostBack); writer.RenderBeginTag(HtmlTextWriterTag.Span);writer.Write("Plus");
writer.RenderEndTag(); } protected override void Render(HtmlTextWriter writer)
{
if (this.Page != null)
{// indispensable pour que les propriétés du contrôle soient postées
this.Page.VerifyRenderingInServerForm(this);
}base.Render(writer);
}
} public class CompteurEventArgs : EventArgs
{public CompteurEventArgs()
{ } public CompteurEventArgs(int total)
{this.Total = total;
} private int _Total;
public int Total
{ get {return _Total;
} set {_Total = value;
} }}
Code VB
Public Class Compteur
Inherits WebControl
Implements IPostBackEventHandler
Public Sub New()
End Sub
Public Property Total() As Integer
Get
If Me.ViewState("Total") Is Nothing Then
Return 0
End If
Return Convert.ToInt32(Me.ViewState("Total"))
End Get
Set
Me.ViewState("Total") = value
End Set
End Property
Public Delegate Sub CompteurEventHandler(ByVal sender As Object, ByVal e As CompteurEventArgs)
Public Event Plus As CompteurEventHandler
Protected Sub OnPlus(ByVal e As CompteurEventArgs)
RaiseEvent Plus(Me, e)
End Sub
Public Event Moins As CompteurEventHandler
Protected Sub OnMoins(ByVal e As CompteurEventArgs)
RaiseEvent Moins(Me, e)
End Sub
Sub IPostBackEventHandler.RaisePostBackEvent(ByVal eventArgument As String)
If eventArgument = "plus" Then
System.Math.Min(System.Threading.Interlocked.Increment(Me.Total),Me.Total-1)
Me.OnPlus(New CompteurEventArgs(Me.Total))
Else
System.Math.Max(System.Threading.Interlocked.Decrement(Me.Total),Me.Total+1)
Me.OnMoins(New CompteurEventArgs(Me.Total))
End If
End Sub
Protected Overloads Overrides Sub RenderContents(ByVal writer As HtmlTextWriter)
writer.Write(Me.Total)
writer.WriteBreak ' <br/>
Dim DoPostBack As String = Me.Page.ClientScript.GetPostBackEventReference(Me, "moins")
writer.AddAttribute(HtmlTextWriterAttribute.Onclick, DoPostBack) writer.RenderBeginTag(HtmlTextWriterTag.Span) writer.Write("Moins")
writer.RenderEndTag writer.Write(" ")
DoPostBack = Me.Page.ClientScript.GetPostBackEventReference(Me, "plus")
writer.AddAttribute(HtmlTextWriterAttribute.Onclick, DoPostBack) writer.RenderBeginTag(HtmlTextWriterTag.Span) writer.Write("Plus")
writer.RenderEndTag End Sub
Protected Overloads Overrides Sub Render(ByVal writer As HtmlTextWriter)
If Not (Me.Page Is Nothing) Then
Me.Page.VerifyRenderingInServerForm(Me)
End If
MyBase.Render(writer)
End Sub
End Class
Public Class CompteurEventArgs
Inherits EventArgs
Public Sub New()
End Sub
Public Sub New(ByVal total As Integer)
Me.Total = total
End Sub
Private _Total As Integer
Public Property Total() As Integer
Get
Return _Total
End Get
Set
_Total = value End Set
End Property
End Class
Globalement le code est similaire à celui de l'exemple précédent. Nous avons cette fois deux événements: Plus et Moins. RaisePostBackEvent doit donc décider lequel sera levé.
La décision est prise en fonction de l'argument passé à chaque événement dans l'appel à GetPostBackEventReference situé dans RenderContents. Cet argument permet de connaître le nom de l'événement levé.
Le reste est classique.
Cette fois nous souhaitons
déclencher un événement uniquement si des changements sont intervenus dans
l'état du contrôle.
Sur le principe c'est assez simple, il suffit de comparer ses propriétés avec
la valeur postée par HTTP. C'est en réalité ce que nous allons faire. ASP nous
simplifie la vie si le composant implémente IPostBackDataHandler
car dans ce cas:
· L'événement est traité au bon moment dans le cycle de vie de la page
· ASP effectue pour nous le travail de collecte des états avant/après
Prenons l'exemple d'un contrôle de type TextBox. On commence par une classe de base qui sera utilisée par la suite:
Listing 8-5: NewTextBox1
Code C#
public class NewTextBox1 : WebControl,ITextControl
{public NewTextBox1()
{ } public string Text
{ get {if (this.ViewState["Text"] == null)
{return string.Empty;
}return this.ViewState["Text"] as string;
} set {this.ViewState["Text"] = value;
} } protected override HtmlTextWriterTag TagKey
{ get {return HtmlTextWriterTag.Input;
} } protected override void AddAttributesToRender(HtmlTextWriter writer)
{writer.AddAttribute(HtmlTextWriterAttribute.Type, "Text");
// ne pas oublier!!!
writer.AddAttribute(HtmlTextWriterAttribute.Value, this.Text);
} protected override void Render(HtmlTextWriter writer)
{if (this.Page != null)
{// indispensable pour que les propriétés du contrôle soient postées
this.Page.VerifyRenderingInServerForm(this);
}base.Render(writer);
}}
Code VB
Public Class NewTextBox1
Inherits WebControl
Implements ITextControl
Public Sub New()
End Sub
Public Property Text() As String
Get
If Me.ViewState("Text") Is Nothing Then
Return String.Empty
End If
Return CType(ConversionHelpers.AsWorkaround(Me.ViewState("Text"), GetType(String)), String)
End Get
Set
Me.ViewState("Text") = value
End Set
End Property
Protected Overloads Overrides ReadOnly Property TagKey() As HtmlTextWriterTag
Get
Return HtmlTextWriterTag.Input
End Get
End Property
Protected Overloads Overrides Sub AddAttributesToRender(ByVal writer As HtmlTextWriter)
writer.AddAttribute(HtmlTextWriterAttribute.Type, "Text")
' ne pas oublier!
writer.AddAttribute(HtmlTextWriterAttribute.Value, Me.Text)
End Sub
Protected Overloads Overrides Sub Render(ByVal writer As HtmlTextWriter)
If Not (Me.Page Is Nothing) Then
Me.Page.VerifyRenderingInServerForm(Me)
End If
MyBase.Render(writer)
End Sub
End Class
Pour l'instant le contrôle se contente d'afficher un contrôle input. Si vous le testez vous constaterez qu'il ne restaure pas les saisies. Il ne lève pas d'événement non plus.
L'implémentation de IPostBackDataHandler va nous donner l'occasion de corriger ces deux défauts. L'interface expose deux méthodes:
1. bool LoadPostData(string postDataKey,NameValueCollection postCollection)
2. void RaisePostDataChangedEvent()
Lorsqu'une page est postée, ASP parcourt la liste des ID trouvés dans la collection postCollection. S'il existe sur la page un contrôle ayant un ID identique et que ce contrôle implémente IPostBackDataHandler alors ASP appelle sa méthode LoadPostData.
LoadPostData joue deux rôles distincts:
1. permettre au contrôle de retrouver les valeurs saisies par l'utilisateur et amorcer le mécanisme de levée des événements si les données ont été modifiées par l'utilisateur
2. alimenter les propriétés du contrôle avec les nouvelles valeurs en provenance de la page client
Le paramètre postCollection est une collection qui contient l'ensemble des données en provenance de Request.Form et Request.QueryString. C'est dans cette collection qu'un contrôle va puiser les données qu'il contient.
postDataKey est la clef contenant un ID qui, dans cet exemple, coïncide avec l'ID de l'unique composant de notre contrôle. Nous verrons plus loin qu'il y a une subtilité. Pour l'instant, cette clef va donc nous permettre de rechercher la valeur postée du composant dans postCollection.
LoadPostData doit retourner true
si les données postées diffèrent de celle du contrôle. Dans ce cas ASP appelle
automatiquement RaisePostDataChanged,
la deuxième méthode de l'interface.
Cette méthode va implémente la logique qui décide si on lève un événement selon
la valeur de la propriété AutoPostback
par exemple. C'est également là qu'on appelle à la validation du formulaire si
la propriété CauseValidation
est à true. Dans notre exemple
nous allons rien faire de tout cela.
Première étape, alimenter les propriétés du contrôle, nous traiterons l'événement plus loin:
Listing 8-6: NewTextBox2
Code C#
public class NewTextBox2 : NewTextBox1, IPostBackDataHandler
{public NewTextBox2()
{ } protected override void AddAttributesToRender(HtmlTextWriter writer)
{base.AddAttributesToRender(writer);
writer.AddAttribute(HtmlTextWriterAttribute.Name, this.ClientID);
} bool IPostBackDataHandler.LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
{if (! this.Text.Equals(postCollection[postDataKey]))
{
// la valeur a changée, on récupère la nouvelle saisie
this.Text = postCollection[postDataKey];
} // pas d'événements
return false;
} void IPostBackDataHandler.RaisePostDataChangedEvent()
{// pas utilisé
}}
Code VB
Public Class NewTextBox2
Inherits NewTextBox1
Implements IPostBackDataHandler
Public Sub New()
End Sub
Protected Overloads Overrides Sub AddAttributesToRender(ByVal writer As HtmlTextWriter)
MyBase.AddAttributesToRender(writer)
writer.AddAttribute(HtmlTextWriterAttribute.Name, Me.ClientID)
End Sub
Function IPostBackDataHandler.LoadPostData(ByVal postDataKey As String, ByVal postCollection As System.Collections.Specialized.NameValueCollection) As Boolean
If Not Me.Text.Equals(postCollection(postDataKey)) Then
' la valeur a changée, on récupère la nouvelle saisie
Me.Text = postCollection(postDataKey)
End If
' pas d'événement
Return False
End Function
Sub IPostBackDataHandler.RaisePostDataChangedEvent()
' pas utilisé
End Sub
End Class
Comme on le voit, la persistance n'est pas assurée par le viewstate, mais par le contrôle lui-même dans la méthode LoadPostData. Désactiver le viewstate ne l'empêchera pas de persister son état. Comme nous l'avons constaté dans l'article consacré au viewstate, ce comportement n'est pas celui observé par un contrôle Label par exemple. C'est parce que ce contrôle n'implémente pas IPostBackDataHanler qu'il doit utiliser le viewstate pour persister son état[40].
Pour que IPostBackDataHanler fonctionne il est essentiel que le contrôle dispose d'une valeur de Name qui soit unique. C'est la raison pour laquelle nous avons surchargé AddAttributesToRender.
Note:
J'ai bien dis Name, pas ID.
Maintenant comment lever un événement si la valeur de la zone de saisie a changée?
On fait comme d'habitude, on implémente un événement TextAChange. Cette fois on implémente en plus RaisePostDataChangedEvent, ce qui n'a pas été fait tout à l'heure:
Listing 8-7: Evénement TextAChange
Code C#
bool IPostBackDataHandler.LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
{if (! this.Text.Equals(postCollection[postDataKey]))
{// la valeur a changée, on récupère la nouvelle saisie
this.Text = postCollection[postDataKey];
return true;
} return false;
} public event EventHandler TextAChange;
protected void OnTextAChange(EventArgs e)
{if (this.TextAChange != null)
{this.TextAChange(this, e);
}} void IPostBackDataHandler.RaisePostDataChangedEvent()
{this.OnTextAChange(EventArgs.Empty);
}
Code VB
Function IPostBackDataHandler.LoadPostData(ByVal postDataKey As String, ByVal postCollection As System.Collections.Specialized.NameValueCollection) As Boolean
If Not Me.Text.Equals(postCollection(postDataKey)) Then
' la valeur a changée, on récupère la nouvelle saisie
Me.Text = postCollection(postDataKey)
Return True
End If
Return False
End Function
Public Event TextAChange As EventHandler
Protected Sub OnTextAChange(ByVal e As EventArgs)
If Not (Me.TextAChange Is Nothing) Then
Me.TextAChange(Me, e)
End If
End Sub
Sub IPostBackDataHandler.RaisePostDataChangedEvent()
Me.OnTextAChange(EventArgs.Empty)
End Sub
Le code ne présente aucune
difficulté particulière. Notez simplement que l'appel à la méthode On est le travail de RaisePostDataChanged, pas de LoadPostData.
On aurait pu aussi y implémenter du code pour par exemple lancer une validation
de la page si la propriété CauseValidation
du contrôle était activée. Mais c'est dans LoadPostData
que l'on devrait implémenter le code qui appelle la validation des événements
(voir partie 1 du tutoriel).
Certains contrôles peuvent être composés de plusieurs composant. C'est le cas de LoginControl qui propose deux zones de saisie. On commence par notre classe de base comme précédemment qui implémente un très rudimentaire composant login réduit à deux zones de saisie:
Listing 8-8: Un composant composé
Code C#
public class LoginContol : WebControl
{public LoginContol()
{ } public string MotPasse
{ get {if (this.ViewState["MotPasse"] == null)
{return string.Empty;
}return this.ViewState["MotPasse"] as string;
} set {this.ViewState["MotPasse"] = value;
} } public string Login
{ get {if (this.ViewState["Login"] == null)
{return string.Empty;
}return this.ViewState["Login"] as string;
} set {this.ViewState["Login"] = value;
} } protected override void RenderContents(HtmlTextWriter writer)
{writer.AddAttribute(HtmlTextWriterAttribute.Value, this.Login);
writer.RenderBeginTag(HtmlTextWriterTag.Input); writer.RenderEndTag(); writer.WriteBreak(); writer.AddAttribute(HtmlTextWriterAttribute.Value, this.MotPasse);
writer.RenderBeginTag(HtmlTextWriterTag.Input); writer.RenderEndTag(); } protected override void Render(HtmlTextWriter writer)
{if (this.Page != null)
{// indispensable pour que les propriétés du contrôle soient postées
this.Page.VerifyRenderingInServerForm(this);
}base.Render(writer);
}}
Code VB
Public Class LoginContol
Inherits WebControl
Public Sub New()
End Sub
Public Property MotPasse() As String
Get
If Me.ViewState("MotPasse") Is Nothing Then
Return String.Empty
End If
Return CType(ConversionHelpers.AsWorkaround(Me.ViewState("MotPasse"), GetType(String)), String)
End Get
Set
Me.ViewState("MotPasse") = value
End Set
End Property
Public Property Login() As String
Get
If Me.ViewState("Login") Is Nothing Then
Return String.Empty
End If
Return CType(ConversionHelpers.AsWorkaround(Me.ViewState("Login"), GetType(String)), String)
End Get
Set
Me.ViewState("Login") = value
End Set
End Property
Protected Overloads Overrides Sub RenderContents(ByVal writer As HtmlTextWriter)
writer.AddAttribute(HtmlTextWriterAttribute.Value, Me.Login)
writer.RenderBeginTag(HtmlTextWriterTag.Input) writer.RenderEndTag writer.WriteBreak writer.AddAttribute(HtmlTextWriterAttribute.Value, Me.MotPasse)
writer.RenderBeginTag(HtmlTextWriterTag.Input) writer.RenderEndTag End Sub
Protected Overloads Overrides Sub Render(ByVal writer As HtmlTextWriter)
If Not (Me.Page Is Nothing) Then
Me.Page.VerifyRenderingInServerForm(Me)
End If
MyBase.Render(writer)
End Sub
End Class
On va le dériver en un composant qui implémente IPostBackDataHandler . Beaucoup de développeurs se plaignent de ne pas y parvenir, on va voir ce qu'ils oublient:
Listing 8-9: LoginControl2
Code C#
public class LoginContol2 : LoginContol, IPostBackDataHandler
{public LoginContol2()
{ } private string LoginId
{ get {return this.UniqueID ;
} } private string PasseId
{ get {return this.ClientID + "_Passe";
} } protected override void RenderContents(HtmlTextWriter writer)
{
writer.AddAttribute(HtmlTextWriterAttribute.Name, this.LoginId);
writer.AddAttribute(HtmlTextWriterAttribute.Value, this.Login);
writer.RenderBeginTag(HtmlTextWriterTag.Input); writer.RenderEndTag(); writer.WriteBreak(); writer.AddAttribute(HtmlTextWriterAttribute.Name, this.PasseId);
writer.AddAttribute(HtmlTextWriterAttribute.Value, this.MotPasse);
writer.RenderBeginTag(HtmlTextWriterTag.Input); writer.RenderEndTag(); } bool IPostBackDataHandler.LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
{this.Login = postCollection[this.LoginId];
this.MotPasse = postCollection[this.PasseId];
// aucun événement d'état défini dans ce contrôle
return false;
} void IPostBackDataHandler.RaisePostDataChangedEvent()
{// non utilisé
}}
Code VB
Public Class LoginContol2
Inherits LoginContol
Implements IPostBackDataHandler
Public Sub New()
End Sub
Private ReadOnly Property LoginId() As String
Get
Return Me.UniqueID
End Get
End Property
Private ReadOnly Property PasseId() As String
Get
Return Me.ClientID + "_Passe"
End Get
End Property
Protected Overloads Overrides Sub RenderContents(ByVal writer As HtmlTextWriter)
writer.AddAttribute(HtmlTextWriterAttribute.Name, Me.LoginId)
writer.AddAttribute(HtmlTextWriterAttribute.Value, Me.Login)
writer.RenderBeginTag(HtmlTextWriterTag.Input) writer.RenderEndTag writer.WriteBreak writer.AddAttribute(HtmlTextWriterAttribute.Name, Me.PasseId)
writer.AddAttribute(HtmlTextWriterAttribute.Value, Me.MotPasse)
writer.RenderBeginTag(HtmlTextWriterTag.Input) writer.RenderEndTag End Sub
Function IPostBackDataHandler.LoadPostData(ByVal postDataKey As String, ByVal postCollection As System.Collections.Specialized.NameValueCollection) As Boolean
Me.Login = postCollection(Me.LoginId)
Me.MotPasse = postCollection(Me.PasseId)
' aucun événement d'état défini dans ce contrôle
Return False
End Function
Sub IPostBackDataHandler.RaisePostDataChangedEvent()
End Sub
End Class
Comme dans le cas précédent on
alimente la propriété Name des
deux zones de saisie avec une valeur unique.
Mais ce n'est pas tout. Un des composants rendus doit avoir son Name (pas l'ID) identique
au UniqueId du composant, LoginContol2 dans cet exemple. C'est cela
qui est en général oublié et amène à des échecs.
Le reste ne pose pas de difficultés particulières une fois ceci compris.
Pour l'instant notre composant ne fait rien de particulièrement passionnant. Il serai bien de lui ajouter un bouton pour faire un login. Pour ajouter la prise en charge d'un événement il suffit d'implémenter IPostbackEventHandler comme on l'a vu plus haut et définir un événement. Voyons comment faire cohabiter ces deux interfaces.
Listing 8-10: LoginControl3
Code C#
public class LoginContol3 : LoginContol, IPostBackDataHandler, IPostBackEventHandler
{public LoginContol3()
{ } private string LoginId
{ get {return this.ClientID + "_Login";
} } private string PasseId
{ get {return this.ClientID + "_Passe";
} } private string BoutonId
{ get {return this.UniqueID;
} } protected override void RenderContents(HtmlTextWriter writer)
{writer.AddAttribute(HtmlTextWriterAttribute.Name, this.LoginId);
writer.AddAttribute(HtmlTextWriterAttribute.Value, this.Login);
writer.RenderBeginTag(HtmlTextWriterTag.Input); writer.RenderEndTag(); writer.WriteBreak(); writer.AddAttribute(HtmlTextWriterAttribute.Name, this.PasseId);
writer.AddAttribute(HtmlTextWriterAttribute.Value, this.MotPasse);
writer.RenderBeginTag(HtmlTextWriterTag.Input); writer.RenderEndTag(); writer.WriteBreak(); writer.AddAttribute(HtmlTextWriterAttribute.Name, this.BoutonId);
writer.AddAttribute(HtmlTextWriterAttribute.Value, "Connecter");
writer.AddAttribute(HtmlTextWriterAttribute.Type, "submit");
writer.RenderBeginTag(HtmlTextWriterTag.Input); writer.RenderEndTag(); } bool IPostBackDataHandler.LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
{if (!string.IsNullOrEmpty(postCollection[this.BoutonId]))
{// oui, le bouton a été cliqué
this.Page.RegisterRequiresRaiseEvent(this);
} return false;
} void IPostBackDataHandler.RaisePostDataChangedEvent()
{// non utilisé
} public event EventHandler Logon;
protected void OnLogon(EventArgs e)
{if (this.Logon != null)
{this.Logon(this, e);
} } void IPostBackEventHandler.RaisePostBackEvent(string eventArgument)
{this.OnLogon(EventArgs.Empty);
} protected override void OnPreRender(EventArgs e)
{base.OnPreRender(e);
this.Page.RegisterRequiresRaiseEvent(this);
}}
Code VB
Public Class LoginContol3
Inherits LoginContol
Implements IPostBackDataHandler
Implements IPostBackEventHandler
Public Sub New()
End Sub
Private ReadOnly Property LoginId() As String
Get
Return Me.ClientID + "_Login"
End Get
End Property
Private ReadOnly Property PasseId() As String
Get
Return Me.ClientID + "_Passe"
End Get
End Property
Private ReadOnly Property BoutonId() As String
Get
Return Me.UniqueID
End Get
End Property
Protected Overloads Overrides Sub RenderContents(ByVal writer As HtmlTextWriter)
writer.AddAttribute(HtmlTextWriterAttribute.Name, Me.LoginId)
writer.AddAttribute(HtmlTextWriterAttribute.Value, Me.Login)
writer.RenderBeginTag(HtmlTextWriterTag.Input) writer.RenderEndTag writer.WriteBreak writer.AddAttribute(HtmlTextWriterAttribute.Name, Me.PasseId)
writer.AddAttribute(HtmlTextWriterAttribute.Value, Me.MotPasse)
writer.RenderBeginTag(HtmlTextWriterTag.Input) writer.RenderEndTag writer.WriteBreak writer.AddAttribute(HtmlTextWriterAttribute.Name, Me.BoutonId)
writer.AddAttribute(HtmlTextWriterAttribute.Value, "Connecter")
writer.AddAttribute(HtmlTextWriterAttribute.Type, "submit")
writer.RenderBeginTag(HtmlTextWriterTag.Input) writer.RenderEndTag
End Sub
Function IPostBackDataHandler.LoadPostData(ByVal postDataKey As String, ByVal postCollection As System.Collections.Specialized.NameValueCollection) As Boolean
If Not String.IsNullOrEmpty(postCollection(Me.BoutonId)) Then
' oui, le bouton a été cliqué
Me.Page.RegisterRequiresRaiseEvent(Me)
End If
Return False
End Function
Sub IPostBackDataHandler.RaisePostDataChangedEvent()
End Sub
Public Event Logon As EventHandler
Protected Sub OnLogon(ByVal e As EventArgs)
If Not (Me.Logon Is Nothing) Then
Me.Logon(Me, e)
End If
End Sub
Sub IPostBackEventHandler.RaisePostBackEvent(ByVal eventArgument As String)
Me.OnLogon(EventArgs.Empty)
End Sub
Protected Overloads Overrides Sub OnPreRender(ByVal e As EventArgs)
MyBase.OnPreRender(e)
Me.Page.RegisterRequiresRaiseEvent(Me)
End Sub
End Class
On dispose donc d'un bouton de type submit. Il reste à intercepter le renvoi lorsque l'on clique dessus. Le travail est fait dans LoadPostData. On vérifie d'abord que l'on a cliqué sur le bouton. Comme nous l'avons vu au chapitre au sujet de l'architecture des événements, cela se traduit par une information dans le paquet HTTP, ce que l'on teste.
On lance ensuite un appel à RegisterRequiresRaiseEvent. Cette méthode demande à ASP de lancer RaisePostBackEvent au moment voulu par l'architecture des événements ASP. L'appel de la même méthode dans OnPreRender est là pour être certain que RaisePostBackEvent sera lancé, même si le composant n'inscrit pas son id dans les données de publication du serveur.
On peut se demander pourquoi
implémenter IPostBackEventHandler
ne suffit pas comme on l'a fait en début de ce chapitre. La raison est que si IPostBackDataHandler est implémenté,
alors RaisePostBackEvent n'est
pas automatiquement appelé.
Si vous vous souvenez du premier exemple, vous devez avoir en tête que cet
appel est nécessaire même si IPostBackDataHandler
est implémenté par une des classes de base.
Note:
La situation est confuse car certains auteurs [46] mentionnent qu'en ASP 2.0
l'appel est automatiquement fait. J'ai testé l'exemple proposé dans l'article
et ne reproduit pas ce que Joteke a pu observer. Je pense que ce comportement a
du être testé par Microsoft dans une quelconque béta, puis abandonné.
Les exemples exposés jusqu'à présent interceptaient les événements simplement en s'y abonnant. Cela suppose que le contrôle client dispose d'une référence directe au contrôle cible. Ce n'est généralement pas le cas pour des contrôles situés dans des composants comme les DataList ou parfois les contrôles créés dynamiquement. Nous avons toutefois vu des solutions au cours de la partie 3 de ce tutoriel.
Nous allons exposer ici une méthode différente[41].
L'idée est très simple: le composant lève un événement. Cet événement est automatiquement intercepté par le contrôle parent qui décide ou non de le traiter. S'il ne le traite pas il le remonte à son propre contrôle parent.
ASP met en place un tel mécanisme appelé (par moi même!) percolation des événements (event bubbling). Nous l'avons déjà rencontré car c'est ainsi que fonctionne l'événement Command.
Nous allons reprendre cet exemple, mais en détaillant les coulisses. Le principe est facilement généralisable à d'autres contextes.
Listing 8-11: Exemple de traitement de Command
<html xmlns="http://www.w3.org/1999/xhtml" >
<body>
<form id="form1" runat="server">
<br />
<asp:gridview ID="GridView1" runat="server" AutoGenerateColumns="False">
<columns>
<asp:templatefield>
<itemtemplate>
<asp:button CommandName="Test" ID="Button1" runat="server" Text="Button" />
</itemtemplate>
</asp:templatefield>
</columns>
</asp:gridview>
</form>
</body>
</html>
Button implémente IPostBackEventHandler. Par conséquent un click sur ce composant appelle RaisePostBackEvent qui en retour appelle successivement OnClick puis OnCommand.
Listing 8-12: Button.RaisePostBackEvent
Code C#
protected virtual void RaisePostBackEvent(string eventArgument)
{base.ValidateEvent(this.UniqueID, eventArgument);
if (this.CausesValidation)
{this.Page.Validate(this.ValidationGroup);
}this.OnClick(EventArgs.Empty);
this.OnCommand(new CommandEventArgs(this.CommandName, this.CommandArgument));
}
Code VB
Protected Overridable Sub RaisePostBackEvent(ByVal eventArgument As String)
MyBase.ValidateEvent(Me.UniqueID, eventArgument)
If Me.CausesValidation Then
Me.Page.Validate(Me.ValidationGroup)
End If
Me.OnClick(EventArgs.Empty)
Me.OnCommand(New CommandEventArgs(Me.CommandName, Me.CommandArgument))
End Sub
OnCommand lève l'événement Command qui est un exemple d'événement destiné à percoler dans la hiérarchie des contrôles avant d'y être intercepté.
Le mécanisme de percolation est hérité de la classe de base Control dont héritent tous les contrôles. Celui-ci s'appuie sur deux méthodes protected:
On initie cette remontée par un appel à la méthode RaiseBubbleEvent. Par exemple dans le cas du contrôle Button, Reflector nous indique:
Listing 8-13: Button.OnCommand
Code C#
protected virtual void OnCommand(CommandEventArgs e)
{CommandEventHandler handler = (CommandEventHandler) base.Events[EventCommand];
if (handler != null)
{handler(this, e);
}base.RaiseBubbleEvent(this, e);
}
Code VB
Protected Overridable Sub OnCommand(ByVal e As CommandEventArgs)
Dim handler As CommandEventHandler = DirectCast(MyBase.Events.Item(Button.EventCommand), CommandEventHandler)
If (Not handler Is Nothing) Then
RaiseEvent handler(Me, e)
End If
MyBase.RaiseBubbleEvent(Me, e)
End Sub
Pour l'essentiel, RaiseBubbleEvent appelle la méthode OnBubbleEvent de toute la hiérarchie de ses contrôles parents. Ceux-ci peuvent surcharger OnBubbleEvent pour fournir un traitement. C'est le mécanisme de remontée. Comment s'arrête t'il?
Listing 8-14: Control.RaiseBubbleEvent
Code C#
protected void RaiseBubbleEvent(object source, EventArgs args)
{for (Control parent = this.Parent; parent != null; parent = parent.Parent)
{
if (parent.OnBubbleEvent(source, args))
{
return;
} }}
Code VB
Protected Sub RaiseBubbleEvent(ByVal source As Object, ByVal args As EventArgs)
Dim parent As Control = Me.Parent
Do While (Not parent Is Nothing)
If parent.OnBubbleEvent(source, args) Then
Return
End If
parent = parent.ParentLoop
End Sub
OnBubbleEvent a un type de retour: un booléen. Si OnBubbleEvent retourne false, alors alors on recommence l'opération avec le contrôle parent de niveau supérieur. Sinon le mécanisme de remontée s'arrête là. La percolation des événements s'arrête donc avec le premier contrôle qui accepte de le prendre en charge ou bien une fois que l'on est en haut de la hiérarchie.
On est pas obligé de remonter spécialement l'événement Command. Un composant situé dans la hiérarchie peut parfaitement le transformer en un événement différent en appelant la méthode On correspondante. C'est de cette manière qu'un DataGrid transforme l'événement OnCommand d'un Button en un événement UpdateCommand si CommandArgument contient "Update" (voir méthode DataGrid.OnBubbleEvent avec Reflector).
Comme on le voit l'exploitation de l'événement Command est très facile, tous les contrôles disposent en standard de la plomberie nécessaire.
Jusqu'à présent nous avons vu que les événements ASP sont implémentés comme des événements de renvoi à travers l'interface IPostBackEventHandler.
ASP 2.0 propose un autre modèle de prise en charge des événements dans lequel la page n'a plus besoin d'être régénérée à chaque requête. Ce modèle est appelé modèle par rappel (Callback).
Une nouvelle interface est crée:
ICallbackEventHandler. Comme
son aînée cette interface expose une méthode RaiseCallbackEvent
qui est très similaire à RaisePostBackEvent.
L'interface expose aussi GetCallbackResult
qui permet de récupérer le résultat du rappel. On retrouve également les même
facilités comme la propriété Page.IsCallback.
On peut dresser le tableau de correspondance suivant entre les deux modèles:
|
Modèle par publication |
Modèle par rappel |
|
IsPostBack |
IsCallback |
|
IPostBackEventHandler |
ICallbackEventHandler |
|
RaisePostBackEvent |
RaiseCallbackEvent |
|
GetPostBackEventReference |
GetCallbackEventReference |
Note:
Vous noterez une différence de syntaxe "intéressante", le b de Callback est en minuscule alors que dans PostBack il y a des majuscules partout.
Le schéma suivant est composé de copies du site Microsoft Japon que je trouve particulièrement esthétique. Il illustre les différences fondamentales entre le modèle par publication et le modèle par rappel[53].
|
Modèle par rappel |
Modèle de publication |
|
|
|
Le modèle de renvoi (publication) est bien connu maintenant. Le modèle par rappel est on le voit différent de par sa philosophie déjà puisqu'en général il ne se traduit pas par un changement de page.
Côté client on devra donc ajouter deux scripts:
Notez aussi que côté serveur certain événements n'ont pas lieu. Les plus notables sont les événements Render. Il sera donc à votre charge de rafraîchir la page après le rappel. Le modèle par rappel est en effet nettement plus verbeux. Notez aussi qu'il n'y a pas d'événements de sauvegarde des états.
Voyons un exemple ensemble, il
s'agit de la version asynchrone du composant Compteur
vu précédemment.
Côté aspx nous avons deux scripts à ajouter.
Listing 9-1: Exemple d'événement asynchrone
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<script language="javascript">
// lance un événement asynchronefunction Click(nom,contexte)
{var controle = document.getElementById(contexte);
UseCallback(nom + "," + controle.innerText,contexte);
} // traitement du résultat du rappelfunction TraitementRappel(resultat,controleID)
{var controle = document.getElementById(controleID);
controle.innerHTML = resultat;
}</script>
</head>
<body>
<form id="form1" runat="server">
<cc1:compteur FonctionDeclenchement="Click" FonctionRappel="TraitementRappel"
ID="Compteur1" runat="server" />
</form>
</body>
</html>
La méthode Click se charge du déclenchement asynchrone des événements de clic sur le contrôle. Nous souhaitons déclencher deux événements: Plus et Moins. Le premier paramètre indiquera le nom de l'événement. Le deuxième est le contexte, dans notre cas l'ID du composant contenant la valeur courante affichée par le compteur.
Pour l'essentiel Click donne la main à la méthode UseCallback. Cette méthode sera déclarée plus tard côté code behind. C'est elle qui lance l'appel asynchrone.
Elle attend deux paramètres,
l'un désigne le nom de l'événement (plus
ou moins) accompagné par la
valeur courante du compteur le tout séparé par une virgule. Cette syntaxe n'a
rien d'impérative, vous pouvez définir votre propre protocole. Ce paramètre
joue un rôle comparable à la classe EventArgs.
Le deuxième paramètre est le contexte dont on a déjà parlé.
La méthode est ensuite déclarée dans l'attribut FonctionDeclenchement du contrôle.
La méthode suivante a pour but de prendre en charge le retour de l'appel asynchrone. Il s'agit de TraitementRappel. Les arguments réclamés sont clairs je pense, notez juste sa déclaration dans l'attribut FonctionRappel. La méthode assure le rafraîchissement de la valeur du compteur.
Nous en avons terminé côté aspx, voyons maintenant le code behind.
Nous commençons par déclarer les deux propriétés FonctionDeclenchement et FonctionRappel que nous venons de voir.
Listing 9-2: Code behind
Code C#
public class Compteur : WebControl, ICallbackEventHandler
{public Compteur()
{ } public string FonctionDeclenchement
{get
{if (string.IsNullOrEmpty(this.ViewState["FonctionDeclenchement"] as string))
{
return "Click";
}return this.ViewState["FonctionDeclenchement"] as string;
} set {this.ViewState["FonctionDeclenchement"] = value;
} } public string FonctionRappel
{ get {if (string.IsNullOrEmpty(this.ViewState["FonctionRappel"] as string))
{
return "TraitementRappel";
}return this.ViewState["FonctionRappel"] as string;
} set {this.ViewState["FonctionRappel"] = value;
}}
protected override void OnInit(EventArgs e)
{base.OnInit(e);
if (string.IsNullOrEmpty(this.FonctionRappel))
{// pas de méthode de traitement, on en construit une par défaut
string FonctionRetour = @"function TraitementRappel(resultat,controleID)
{ var controle = document.getElementById(controleID); controle.innerHTML = resultat;}";
this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "FonctionRetour", FonctionRetour, true);
} // construction de la méthode UseCallback
string DoCallback =
this.Page.ClientScript.GetCallbackEventReference(this, "arg", this.FonctionRappel, "contexte");
string AppelDoCallback =
string.Format("function UseCallback(arg,contexte) {{ {0}; }}", DoCallback);
this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(),
this.FonctionDeclenchement, AppelDoCallback, true);
} protected override void RenderContents(HtmlTextWriter writer)
{string IdResultat = this.UniqueID + "_result";
writer.AddAttribute(HtmlTextWriterAttribute.Id, IdResultat); writer.RenderBeginTag(HtmlTextWriterTag.Div); writer.Write(0); writer.RenderEndTag(); writer.WriteBreak(); // <br/>
writer.AddAttribute(HtmlTextWriterAttribute.Onclick, string.Format("javascript:{0}('moins','{1}');",this.FonctionDeclenchement, IdResultat));
writer.RenderBeginTag(HtmlTextWriterTag.Span);writer.Write("Moins");
writer.RenderEndTag(); writer.Write(" ");
writer.AddAttribute(HtmlTextWriterAttribute.Onclick, string.Format("javascript:{0}('plus','{1}');",this.FonctionDeclenchement, IdResultat));
writer.RenderBeginTag(HtmlTextWriterTag.Span);writer.Write("Plus");
writer.RenderEndTag(); } private int Resultat;
string ICallbackEventHandler.GetCallbackResult()
{return this.Resultat.ToString();
} void ICallbackEventHandler.RaiseCallbackEvent(string eventArgument)
{string[] Commandes = eventArgument.Split(new Char[] { ',' });
int Total = Convert.ToInt32(Commandes[1]);
switch (Commandes[0])
{case "plus":
this.Resultat = ++Total;
break;
case "moins":
this.Resultat = --Total;
break;
} } protected override void Render(HtmlTextWriter writer)
{if (this.Page != null)
{// indispensable pour que les propriétés du contrôle soient postées
this.Page.VerifyRenderingInServerForm(this);
}base.Render(writer);
}}
Code VB
Public Class Compteur
Inherits WebControl
Implements ICallbackEventHandler
Public Sub New()
End Sub
Public Property FonctionDeclenchement() As String
Get
If String.IsNullOrEmpty(CType(ConversionHelpers.AsWorkaround(Me.ViewState("FonctionDeclenchement"), GetType(String)), String)) Then
Return "Click"
End If
Return CType(ConversionHelpers.AsWorkaround(Me.ViewState("FonctionDeclenchement"), GetType(String)), String)
End Get
Set
Me.ViewState("FonctionDeclenchement") = value
End Set
End Property
Public Property FonctionRappel() As String
Get
If String.IsNullOrEmpty(CType(ConversionHelpers.AsWorkaround(Me.ViewState("FonctionRappel"), GetType(String)), String)) Then
Return "TraitementRappel"
End If
Return CType(ConversionHelpers.AsWorkaround(Me.ViewState("FonctionRappel"), GetType(String)), String)
End Get
Set
Me.ViewState("FonctionRappel") = value
End Set
End Property
Protected Overloads Overrides Sub OnInit(ByVal e As EventArgs)
MyBase.OnInit(e)
If String.IsNullOrEmpty(Me.FonctionRappel) Then
' pas de méthode de traitement, on en construit une par défaut
Dim FonctionRetour As String =
"function TraitementRappel(resultat,controleID)" & Microsoft.VisualBasic.Chr(13)
& "" & Microsoft.VisualBasic.Chr(10) & " {" & Microsoft.VisualBasic.Chr(13) & ""
& Microsoft.VisualBasic.Chr(10) & " var controle = document.getElementById(controleID);"
& Microsoft.VisualBasic.Chr(13) & "" & Microsoft.VisualBasic.Chr(10)
& " controle.innerHTML = resultat;" & Microsoft.VisualBasic.Chr(13)
& "" & Microsoft.VisualBasic.Chr(10) & " }"
Me.Page.ClientScript.RegisterClientScriptBlock(Me.GetType,
"FonctionRetour", FonctionRetour, True)
End If
' construction de la méthode UseCallback
Dim DoCallback As String =
Me.Page.ClientScript.GetCallbackEventReference(Me, "arg", Me.FonctionRappel, "contexte")
Dim AppelDoCallback As String =
String.Format("function UseCallback(arg,contexte) {{ {0}; }}", DoCallback)
Me.Page.ClientScript.RegisterClientScriptBlock(Me.GetType,
Me.FonctionDeclenchement, AppelDoCallback, True)
End Sub
Protected Overloads Overrides Sub RenderContents(ByVal writer As HtmlTextWriter)
Dim IdResultat As String = Me.UniqueID + "_result"
writer.AddAttribute(HtmlTextWriterAttribute.Id, IdResultat) writer.RenderBeginTag(HtmlTextWriterTag.Div) writer.Write(0) writer.RenderEndTag writer.WriteBreak ' <br/>
writer.AddAttribute(HtmlTextWriterAttribute.Onclick, String.Format("javascript:{0}('moins','{1}');", Me.FonctionDeclenchement, IdResultat))
writer.RenderBeginTag(HtmlTextWriterTag.Span) writer.Write("Moins")
writer.RenderEndTag writer.Write(" ")
writer.AddAttribute(HtmlTextWriterAttribute.Onclick, String.Format("javascript:{0}('plus','{1}');", Me.FonctionDeclenchement, IdResultat))
writer.RenderBeginTag(HtmlTextWriterTag.Span) writer.Write("Plus")
writer.RenderEndTag End Sub
Private Resultat As Integer
Function ICallbackEventHandler.GetCallbackResult() As String
Return Me.Resultat.ToString
End Function
Sub ICallbackEventHandler.RaiseCallbackEvent(ByVal eventArgument As String)
Dim Commandes As String() = eventArgument.Split(New Char() {","C})
Dim Total As Integer = Convert.ToInt32(Commandes(1))
Select Commandes(0)
Case "plus"
Me.Resultat = System.Threading.Interlocked.Increment(Total)
Case "moins"
Me.Resultat = System.Threading.Interlocked.Decrement(Total)
End Select
End Sub
Protected Overloads Overrides Sub Render(ByVal writer As HtmlTextWriter)
If Not (Me.Page Is Nothing) Then
' indispensable pour que les propriétés du contrôle soient postées
Me.Page.VerifyRenderingInServerForm(Me)
End If
MyBase.Render(writer)
End Sub
End Class
Intéressons nous à OnInit. C'est là que nous construisons la méthode javascript UseCallback grâce à un appel de GetCallbackEventReference. Il s'agit de l'équivalent de GetPostBackEventReference dans le monde appel asynchrone.
L'un des paramètres est le nom de la fonction chargée de récupérer le traitement côté client (TraitementRappel). Notez bien que arg et contexte sont simplement le nom des paramètres de la méthode UseCallback(). L'argument args sera passé à RaiseCallbackEvent, il contient les paramètres de l'événement comme nous l'avons vu précédemment. On génère au final le code suivant:
Listing 9-3: UseCallback
<script type="text/javascript">
<!--function UseCallback(arg,contexte)
{ WebForm_DoCallback('Compteur1',arg,TraitementRappel,contexte,null,false);
}// --></script>
OnInit propose aussi de construire une méthode TraitementRappel par défaut si on n'en a pas déclaré. On aurait pu en faire de même avec Click. D'un point de vue technique il aurait été plus propre d'utiliser une ressource incluse[19].
L'étape suivante est l'appel à RaiseCallbackEvent qui réalise l'action demandée: incrémenter ou décrémenter le compteur. Ensuite GetCallbackResult retourne le résultat vers le client. ASP lance alors un appel à TraitementRappel qui est chargé de rafraîchir l'affichage. Cette méthode reçoit en paramètre le résultat du traitement et le contexte.
Notez un point important, lors d'un appel asynchrone aucun événement de sauvegarde des états n'est déclenché, c'est la raison pour laquelle nous sommes obligés de passer en paramètre la valeur courante du compteur afin de pouvoir la traiter. Une autre raison est que dans la mesure où il peut être altéré par du code purement javascript, on n'a pas d'autres moyens de connaître sa valeur.
Si vous testez le code, vous constaterez que le compteur répond aux deux événements Plus ou Moins, mais la page n'est jamais postée.
© 2007 DotNetGuru.org - Publié le 27/03/2007