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

Premier exemple

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

 

8 - Architecture des événements ASP côté serveur

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:

  1. Les événements SANS données de publication: par exemple un clic sur un bouton
  2. Les événements AVEC données de publication: Evénement déclenché lorsque l'état d'un composant change, SelectedChanged d'un CheckBox par exemple

Voyons les détails ensembles.

Les événements sans données de publication

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.

Premier exemple

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!

Cas où le composant doit générer le javascript d'appel de l'événement

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.

Cas où il y a plusieurs événements

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("&nbsp;");
 
        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("&nbsp;") 
 
   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.

Les événements avec données de publication

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

Cas d'un composant avec un seul contrôle

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).

Cas d'un composant avec plusieurs contrôles

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é.

Percolation d'un événement à travers la hiérarchie des composants

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:

  1. void RaiseBubbleEvent(Object sender,EventArgs e)
  2. bool virtual OnBubbleEvent(Object sender,EventArgs e)

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.Parent
    Loop
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.

9 - Les événements asynchrones

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:

  1. Un script pour lancer un appel asynchrone vers le serveur
  2. Un script pour traiter le résultat de l'appel asynchrone

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 asynchrone
function Click(nom,contexte)
{
    var controle = document.getElementById(contexte);
    UseCallback(nom + "," + controle.innerText,contexte);
}
 
// traitement du résultat du rappel
function 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("&nbsp;");
 
        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("&nbsp;") 
 
   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