Peter Bucher - Mein Experiment, meine Spielereien, meine Welt...   ·   Stefan Falz   ·   Jürgen Gutsch   ·   Golo Roden   ·   ASP.NET Zone   ·   Microsoft ASP.NET
Willkommen bei ASP.NET Zone. Anmelden | Registrieren | Hilfe

Events für User- und CustomControls definieren und benutzen

In der Welt von ASP.NET gibt es - im Gegensatz zu Classic ASP - ein Eventsystem das in der ASP.NET Engine verwurzelt ist.
Dazugehörend haben viele ASP.NET Controls Events. Das bekannteste ist wohl Button.Click, über diesen Event kann festgestellt werden ob ein bestimmter Button gedrückt wurde, oder nicht.

Nun gibt es ja die Möglichkeit eigene User- und CustomControls zu schreiben.
Diese können mit Events ausgestattet werden, und genau um das geht es hier in diesem Artikel.

Wie funktionieren die Events in ASP.NET?

Grundsätzlich ereignen sich alle Aktionen auf dem Client, bspw. klickt eine Person an einem Computer in dessen Webbrowser auf einen Button.
Danach wird das Formular abgesendet und der Server empfängt dieses.

Der Server leitet den Http-Request (Die Http-Anfrage) an die ASP.NET Engine weiter.
Die Engine findet dann anhand der <Control>.UniqueID (Siehe: Identifizierung von Controls: Control.ID / .ClientID / .UniqueID) das Control zu dem die jeweiligen Daten gehören.

Wenn dieses Control gefunden wurde, leitet die Engine die gesamten POST-Daten (Formulardaten) an das bestreffende Control weiter.
Sind die Daten geändert worden, bzw. befinden sich die Daten in einem erwarteten Zustand, löst das Control den Event aus, ansonsten nicht.

Dies ist die normale Vorgehensweise von Events (Die Page Events laufen ähnlich ab), die über POST (Formular, Postback) ausgelöst werden.

Wie definiere ich einen Event, oder was ist ein Event überhaupt?

Für die Grundlagen zur Event-Erstellung, mit oder ohne nutzerspezifische Argumente, verweise ich hier auf den FAQ Thread des C#-Forums myCSHarp.de.

Die FAQ von myCSharp.de als solches ist auch für andere Sprachspezifische Angelegenheit sehr zu empfehlen.

Wie / Wann werden die Events ausgelöst und gibt es mehrere Auslösearten?

Das trickreiche an Events in ASP.NET ist nicht die Dekleration der Events, sondern der Zeitpunkt an dem sie ausgelöst werden sollen.
Es gibt tatsächlich mehrere Arten von Zustandsveränderungen bei denen ein Event ausgelöst werden kann.

Folgend die Möglichkeiten von Zustandsänderungen durch einen Benutzer der Web Applikation ausgelöst:

  • Werte die durch einen (oder mehrere) GET-Parameter (QueryString) empfangen werden, bspw. wenn der Benutzer auf einen Link klickt
  • Werte die durch einen (oder mehrere) POST-Parameter (Form) empfangen werden, bspw. wenn der Benutzer Formularfelder ausgefüllt hat und dann auf einen Button klickt, oder aber nur auf einen Button klickt

Zudem gibt es noch weitere Faktoren und Möglichkeiten, die als Indikatoren für eine Eventauslösung benutzt werden können:

  • Werte die durch einen (oder mehrere) POST-Parameter empfangen werden, die durch die __doPostback-Javascriptfunktion abgeschickt werden
  • Zustandsänderungen aller Art auf der Serverseite (Datei, Datenbank, Session, Zeit, Datum, etc...
  • Festgelegte Logik im Code, bspw. Vergleiche, etc...

Aller Anfang ist schwer leicht

Zuerst folgt ein Beispiel das auf einen GET-Parameter reagiert und bei Vorhandensein des Parameters einen Event auslöst.

C#:


namespace pb.Web.UI.WebControls.Examples
{
    public class TestControlGetSimple : WebControl 
    {
        public event EventHandler ParameterAvailable;

        protected virtual void OnParameterAvailable(object sender, EventArgs e) {
            if (this.ParameterAvailable != null)
                this.ParameterAvailable(sender, e);
        }

        protected override HtmlTextWriterTag TagKey {
            get {
                return HtmlTextWriterTag.Div;
            }
        }

        protected override void OnInit(EventArgs e) {
            base.OnInit(e);

            HyperLink link = new HyperLink();
            link.ID = "hlTest";
            link.Text = "Testlink";
            link.NavigateUrl = "?test=1234";
            this.Controls.Add(link);
        }

        protected override void OnLoad(EventArgs e) {
            base.OnLoad(e);

            string name = this.Context.Request.QueryString["test"];
            if(!string.IsNullOrEmpty(name)) {
                this.OnParameterAvailable(this, EventArgs.Empty);
            }
        }
    }
}

Was genau passiert da jetzt?

Das Beispiel ist einfach aufgebaut. Zuerst wird ein Event mit einem Standard-Eventhandler (in .NET verfügbar) implementiert.
Danach wird die eventauslösende OnEvent-Methode definiert, die aufgerufen wird, wenn der Event ausgelöst werden soll.

Anschliessend wird per überschriebenen TagKey-Eigenschaft angegeben, dass das Control ein umschliessenden Div-Tag besitzen soll. (Im Beispielprojekt wird das für die Formatierung benutzt).
In der OnInit-Methode wird ein Objekt vom Typ Hyperlink angelegt, ein Text und eine Url + GET-Parameter angegeben und anschliessend zum Control hinzugefügt.

Jetzt kommt der spannende Teil: Das Event wird ausgelöst.

Beim Laden des Controls wird ein evt. vorhandener Parameter über <HttpContext>.Request.QueryString(<Key>) abgerufen, und anschliessend überprüft ob dieser null oder ein Leerstring (String.Empty) ist.

Falls dies nicht der Fall ist, wird der Event ausgelöst. Das war schon der ganze "Zauber" auf der Control-Seite.

Die Benutzung des Controls in einer ASP.NET Seite ist sehr einfach und geht so von der Hand, wie ansonsten ein Klick-Event eines Buttons abonniert wird.

Wie das Control registiert wird, um auf der Seite zu benutzen, kann im folgendenen Blogpost nachgelesen werden:

Beispiel:

ASPX:



<fieldset>
    <legend>GET Beispiel (simple)</legend>
    <pb:TestControlGetSimple ID="testGetSimple" runat="server" />
    <br />
    <asp:Label ID="lblInfoGetSimple" Text="Parameter nicht verfügbar" runat="server" />
</fieldset>

Codebehind (C#):


protected void Page_Load(object sender, EventArgs e) {
    this.testGetSimple.ParameterAvailable += testGetSimple_ParameterAvailable;
}


void testGetSimple_ParameterAvailable(object sender, EventArgs e) {
    this.lblInfoGetSimple.Text = "Parameter ist <strong>vorhanden</strong>";
}

Zuerst werden auf der ASP.NET Seite das Test-Control und ein Label hinzugefügt.
Anschliessend abonnieren wir das Event im Page_Load Eventhandler und fügen dem zugewiesenen Handler Code hinzu, damit das Label anzeigt, ob der Event ausgelöst wurde oder nicht.

Wenn der Link angeklickt wird und infolgedessen ein GET-Parameter vorhanden ist, wird das Event ausgelöst und im Eventhandler den Text des Labels geändert.
Aus Benutzersicht ist nur der geänderte Text sichtbar.

Das Beispiel wurde mit Absicht sehr simpel gehalten, um einen einfach Einstieg zu ermöglichen.

Ein erweitertes Beispiel mit GET-Parametern

Folgend der Code eines erweiterten Beispiels, das mithilfe einer Session erkennen kann, ob der aktuelle Wert des GET-Parametes geändert wurde. Den ASPX-Teil wird nicht mehr gezeigt, da er praktisch identisch zum Beispiel (Siehe Code) ist.

C#:


namespace pb.Web.UI.WebControls.Examples
{
    public class TestControlGet : WebControl, INamingContainer
    {
        public delegate void NameChangedDelegate(object sender, NameChangedEventArgs e);
        public event NameChangedDelegate NameChanged;


        protected virtual void OnNameChanged(object sender, NameChangedEventArgs e) {
            if (this.NameChanged != null)
                this.NameChanged(sender, e);
        }

        protected override HtmlTextWriterTag TagKey {
            get {
                return HtmlTextWriterTag.Div;
            }
        }

        protected override void OnInit(EventArgs e) {
            base.OnInit(e);

            for (int i = 0; i < 5; i++) {
                HyperLink link = new HyperLink();
                link.ID = "testLink" + i.ToString();
                link.Text = "Patrick " + i.ToString();
                link.NavigateUrl = "?name=Patrick" + i.ToString();
                this.Controls.Add(link);
                this.Controls.Add(new LiteralControl("<br />"));
            }
        }

        protected override void OnLoad(EventArgs e) {
            base.OnLoad(e);

            string name = this.Context.Request.QueryString["name"];
            string nameBefore;
            object sessionValue = this.Context.Session["name"];

            if (sessionValue != null)
                nameBefore = sessionValue.ToString();

            bool flag = false;

            // Erste Änderung (ohne Session, erster Aufruf)
            if (name != null && sessionValue == null)
                flag = true;

            // Änderung mithilfe der Session herausfinden
            if (sessionValue != null && name != null && !sessionValue.Equals(name))
                flag = true;

            if(flag)
                this.OnNameChanged(this, new NameChangedEventArgs(name));

            this.Context.Session["name"] = name;
        }
    }

    public class NameChangedEventArgs : EventArgs {
        private string _newName;

        public NameChangedEventArgs(string newName) {
            this._newName = newName;
        }

        public string NewName {
            get { return this._newName; }
        }
    }
}

In diesem Beispiel wird eine Liste von Links mit GET-Parametern ausgegeben.
Wenn der empfangene Parameter ändert, wird der Event ausgelöst.

Beim ersten Empfangen eines Wertes wird dieser in die Session geschrieben und dieser Spezialfall berücksichtigt.
Wird jedoch ein Wert empfangen, wenn die Session schon ein Wert enthält, wird auf Ungleichheit geprüft, falls wahr wird der Event ausgelöst.

Zudem wird in diesem Beispiel ein eigener Delegate benutzt, um eigene EventArgs zu unterstützen.
Im Eventhandler kann der geänderte Namen einfach über e.NewName abgerufen werden.

(Der Einfachheit halber habe ich keinen generischen EventHandler benutzt, obwohl dieser ab .NET 2.0 natürlich zu empfehlen wäre. Wie es geht, siehe: Eigenen Event definieren / Information zu Events (Externer Link: myCSharp.de) )

Einen Event über POST-Werte mithilfe von IPostBackEventHandler und ClientScript.GetPostBackEventReference auslösen

Wie weiter oben schon angesprochen, stellt ASP.NET selber eine Architektur bereit, um mit POST-Daten Events über eine spezielle Infrastruktur auszulösen.
Die jetzt vorgestellte Lösung basiert auf der __doPostBack-Funktion, die ASP.NET im Normalfall in jeder ASP.NET Seite zur Verfügung stellt.

Folgend die Definition der Schnittstelle.

C#:

public interface IPostBackEventHandler
{
    void RaisePostBackEvent(string eventArgument);
}

Im Normalfall geschieht ein Postback über einen Html-Button, der das Formular absendet.
Das Formular kann aber auch per Javascript abgesendet werden, genau das nutzt die __doPostBack-Funktion.
Sie kann zusätzlich Parameter entgegennehmen und diese über HiddenFields (Versteckte Eingabefelder) zum Server schicken.

So ist nicht nur ein Abschicken und Prüfen von Form-Elementen möglich, sondern auch das übergeben beliebiger Elemente und zudem auch den Namen des Controls, von dem die Elemente bzw. Argumente kommen.

Über die Methode GetPostBackEventReference() der Klasse ClientScript finden sich viele Überladungen, die benutzt werden können um eine Javascript Postback-Referenz zu bekommen.
Dies ist nichts weiter als ein benutzerdefinierter Aufruf der __doPostBack-Funktion, mit eigenen Parametern.

ASP.NET stellt eine Schnittstelle Namens IPostBackEventHandler zur Verfügung, die implementiert werden kann, um über Postbacks mit der Möglichkeit der Übergabe von Elementen zu kommunizieren.
Dabei muss immer das Control selber (schlussendlich die UniqueID des Controls) übergeben werden, sowie Argumente nach Wunsch.

Vielleicht kennt der eine oder andere die __doostBack-Funktion aus dem GridView, wenn eine Zeile selektiert wird.
Dies wird genau nach diesem Schema erledigt.

Folgend nun das Beispiel (Wiederum nur das Control an sich, da der Rest praktisch gleich ist):

C#:


namespace pb.Web.UI.WebControls.Examples
{
    public class TestControlPost : WebControl, INamingContainer, IPostBackEventHandler
    {
        public delegate void NameSelectedDelegate(object sender, NameSelectedEventArgs e);
        public event NameSelectedDelegate NameSelected;


        protected virtual void OnNameChanged(object sender, NameSelectedEventArgs e) {
            if (this.NameSelected != null)
                this.NameSelected(sender, e);
        }

        protected override HtmlTextWriterTag TagKey {
            get {
                return HtmlTextWriterTag.Div;
            }
        }

        protected override void OnInit(EventArgs e) {
            base.OnInit(e);

            for (int i = 0; i < 5; i++) {
                HyperLink link = new HyperLink();
                link.ID = "testLink" + i.ToString();
                link.Text = "Patrick" + i.ToString();
                link.NavigateUrl = "#";
                link.Attributes.Add("onclick", this.Page.ClientScript.GetPostBackEventReference(this, "Patrick " + i.ToString()));
                this.Controls.Add(link);
                this.Controls.Add(new LiteralControl("<br />"));
            }
        }

        #region IPostBackEventHandler Members

        public void RaisePostBackEvent(string eventArgument) {
            if(this.NameSelected != null)
                this.NameSelected(this, new NameSelectedEventArgs(eventArgument));
        }

        #endregion
    }

    public class NameSelectedEventArgs : EventArgs
    {
        private string _selectedName;

        public NameSelectedEventArgs(string newName) {
            this._selectedName = newName;
        }

        public string SelectedName {
            get {
                return this._selectedName;
            }
        }
    }
}

Das Beispiel ist dem letzten Beispiel sehr ähnlich, jedoch geht es hier um das Selektieren eines Namens, nicht um die Änderung dessen.

Es werden auch HyperLinks erzeugt, und in dessen clientseitigem onclick-Eventhandler jeweils die PostBackEvent-Referenz mithilfe obengenannter Methode ein parameterisierter Javascript-Aufruf für das jeweilige Link erzeugt.
Dabei wird eine Referenz auf das Control selbst mitgegeben, damit ASP.NET das Control anhand der UniqueID identifizieren kann, und den Namen, der selektiert werden kann.

Die ASP.NET Engine merkt beim Durchgehen aller Controls, dass dieses Control die IPostBackEventHandler Schnittstelle implementiert und feuert dann ggf. bei Bestehen eines Events dass durch das Link gesendet wurde, das Event.

Dies geschieht dadurch, dass ASP.NET die Methode RaisePostBackEvent aufruft und ihr das Argument übergibt, dass vom Client gesendet wurde.
Aufgrund dieser Infos kann schlussendlich im Eventhandler auf das Argument zugegriffen werden.

Das Interface IPostBackDataHandler, Sinn und Nutzen

ASP.NET stellt noch ein weiteres Interface zur Verfügung, um Events auszulösen.
Dieses setzt zwei Methoden voraus, folgend die Definition:

C#:

public interface IPostBackDataHandler
{
    bool LoadPostData(string postDataKey, NameValueCollection postCollection);
    void RaisePostDataChangedEvent();
}

Im Beispiel auf der Seite von Microsoft, ist ein ziemlich sinnloses Beispiel zu finden, indem von einem WebControl ein Button nachgebaut wird.

Diese Schnittstelle wird nur existieren, damit Microsoft alle Form-Controls implementieren konnte.
In LoadPostData werden die UniqueID des Controls selber und die komplette Post-Collection angeboten.

Durch <Collection>[postDataKey] kann der Wert des Controls sehr einfach gelesen werden, der Rückgabetyp ist ein bool und bei einer Rückgabe von true wird die RaisePostDataChangedEvent()-Methode aufgerufen und der oder die Events können gefeuert werden.
ASP.NET ruft - auch durch die Interface-Markierung - jeweils die Methode LoadPostData automatisch auf.
Dies jedoch nur, wenn als TagKey bzw TagName des Controls ein bekanntes Form-Control gesetzt ist (Was auch Sinn macht).
Wenn die Methode bei einem eigenen Control trotzdem aufgerufen werden soll, so muss die Methode RegisterRequiresPostBack (Siehe auch: Understanding what Page.RegisterRequiresPostBack does) von der Klasse Page aufgerufen werden und das zu registrierende Control übergeben werden.

Für eine Implementation der Controls: Button, DropDownList, TextBox, etc... ein sehr sinnvolles Interface.
Bei der Implementation eines eigenen Controls, das kein Form-Tag darstellt geht es m.E. einfacher und besser ohne dieses Interface.

Siehe auch:

An dieser Stelle gibt es kein Beispiel, jedoch ist im Download auch ein solches Beispiel vorhanden, dass das Interface benutzt.

Inhalt des Downloads:

  • Alle hier darstellten Beispiele
  • IPostBackDataHandler-Beispiel
  • Alles in einer Solution mit einem Control-Library Projekt und einem Web Application Project zum Testen

Download:

Dank geht an:
Norbert Eder für das Review des Artikels

 

Ich freue mich über jegliche Kommentare!

Kommentare

# re: Events für User- und CustomControls definieren und benutzen

Hoi Peter, toller Artikel.

Siehst du, so lerne ich auch noch was von Dir ;-)

Danke. :-)

Freitag, 13. Juni 2008 08:48 by Jürgen Gutsch
Anonyme Kommentare sind nicht zugelassen