ASP.NET

UserControls mit Properties/Konstruktoren dynamisch laden (Server und Client)

...oder wie man ein komplexes ASP.NET-Benutzersteuerelement auch über einen WebService laden kann

UserControls sind herrlich, um Code-Redundanzen zu vermeiden. Wiederkehrenden HTML-Code lagert man in ein solches Benutzersteuelement aus und etwaige Änderungen müssen nur einmal an zentraler Stelle vorgenommen werden.

Programmatisch ein solches Steuerelement in die zu rendernde Seite einzufügen, ist hingegen nicht ganz so trivial. Vor allem, wenn man dem Control eine oder mehrere Eigenschaften verpasst hat, die den Inhalt oder das Aussehen steuern, oder wenn man gar einen Konstrukor mit Argumenten geschrieben hat. Aber ... wie immer gibt es einen Weg, bzw. mehrere, die ich hier aufzeigen möchte.

Das Element der Begierde

Das Steuerelement in den folgenden Beispielen ist ein simples DIV-Element, in dem ein Text dargestellt werden soll. Die Deklaration in der ASCX-Datei sieht folgendermaßen aus:

<%@ Control 
    Language="VB" 
    AutoEventWireup="false" 
    CodeFile="element.ascx.vb" 
    Inherits="ElementControl" 
    EnableViewState="false" %>

<div id="element<%=Me.ElementNo%>" class="<%=Me.ElementClass%>">
    <%=Me.ElementNo%>. <%=Me.ElementText%>
</div>

Die Klasse ElementControl der Code-Behind-Datei enthält, neben den drei Eigenschaften ElementNo, ElementClass und ElementText, u.a. folgende Konstruktoren:

Public Sub New()
	Me.ElementNo = 0
	Me.ElementText = ""
	Me.ElementClass = "style0"
End Sub

Public Sub New( _
	ByVal intElementNo As Integer, _
	ByVal strElementText As String, _
	ByVal strElementClass As String)
	
	Me.ElementNo = intElementNo
	Me.ElementText = strElementText
	Me.ElementClass = "style2"
End Sub

Registriert ist das Control in der web.config wie folgt:

...
<system.web>
	<pages>
		<controls>
			  <add tagPrefix="uct" tagName="Element" src="~/controls/element.ascx" />
...

Wie man sieht, ein sehr simples Beispiel, das allerdings nicht darüber hinwegtäuschen soll, dass mit dem folgenden Verfahren auch wesentlich komplexere UserControls verarbeitet werden können.

Der Klassiker - LoadControl und Setzen der Properties

Der Standardweg ein Benutzersteuerelement zu laden, ist der über die Methode LoadControl, die das Page-Objekt bereitstellt. Rückgabewert ist für das Beispiel eine Instanz der Klasse controls_element_ascx aus dem Namespace ASP, die vom Pre-Compiler zur Verfügung gestellt wird. Über Reflection kann man nun auf die in der Klasse enthaltenen Eigenschaften mittels SetValue zugreifen.

Am effizientesten ist es, wenn man den Code in eine allgemeingültige Extension-Method (Erweiterungsmethode) packt, die Page erweitert.

Als zweites Argument der Methode ist ein Dictionary sinnvoll, in der die einzustellenden Eigenschaften als Schlüssel/Wert-Paarungen abgelegt sind, wobei der einzustellende Wert einer Eigenschaft als Object übergeben wird, was einem beim Erstellen des Dictionaries diverse Konvertierungen erspart. Für die Methode selbst ist das egal, denn mit der wichtigste Aspekt dieser Implementierung ist die Überführung der Werte in den geforderten Typ der jeweiligen Eigenschaft der UserControl-Klasse mittels Convert.ChangeType. CType oder DirectCast funktionieren hier nicht, da man nicht den eigentlichen Typen Übergeben kann.

By the way: Erweiterungsmethoden in VB müssen immer in einem eigenen Modul abgelegt sein!

<Extension()> _
Public Function LoadControlWithProperties( _
    ByVal p As Page, _
	ByVal strVirtualPath As String, _
	ByVal dicProp As Dictionary(Of String, Object)) As UserControl
	
	'Neues Control-Objekt erzeugen
	Dim u As UserControl = _
		p.LoadControl(strVirtualPath)
	
	'Properties bestücken
	For Each s As String In dicProp.Keys
	
		'Property auslesen
		Dim pi As Reflection.PropertyInfo = _
			u.GetType().GetProperty(s)
	
		'Property-Typ ermitteln
		Dim t As System.Type = pi.PropertyType
	
		'Property-Wert setzen, inkl. Typumwandlung des Wertes
		pi.SetValue(u, Convert.ChangeType(dicProp(s), t), Nothing)
	
	Next
	
	Return u
	
End Function

Der Aufruf im Code einer ASPX-Seite sieht dann folgendermaßen aus:

Dim col As New NameValueCollection
col.Add("ElementNo", intElementNo)
col.Add("ElementText", strElementText)
col.Add("ElementClass", strElementClass)

Dim e As UserControl = _
    Me.LoadControlWithProperties("~/controls/element.ascx", _
                                 col)
'UserControl hinzufügen
Me.container.Controls.Add(e)

Die Alternative - LoadControl und Aufrufen eines Konstruktors

Beim Laden des Steuerelements mittels LoadControl wird zwar der parameterlose Konstrukor Sub New() aufgerufen, aber mehr auch nicht. .NET unterstützt hier keine Konstruktoren mit Argumenten. Abhilfe schafft widerum Reflection, denn hat man eine Liste der Typen der Parameter, kann man sich den passenden über GetConstructor heraussuchen und anschließend aufrufen. Vielen Dank Shahed für den guten Artikel.

Auch in diesem Fall ist es empfehlenswert dafür eine Erweiterungsmethode zu schreiben. Als zweites Argument kommt diesmal ein Parameter-Array zum Einsatz, mit dem man die Argumente für den Konstruktor übergibt. Wichtig ist hierbei die korrekte Reihenfolge der Parameter.

<Extension()> _
Public Function LoadControlWithConstructor( _
	ByVal p As Page, _
	ByVal strVirtualPath As String, _
	ByVal ParamArray arrArgs As Object()) As UserControl
	
	'Neues Control-Objekt erzeugen
	Dim u As UserControl = _
	    p.LoadControl(strVirtualPath)
	
	'Parameter-Typ-Liste
	Dim l As New Generic.List(Of Type)
	For Each a As Object In arrArgs
	    l.Add(a.GetType())
	Next
	
	'Konstruktor anhand der Typenliste ermitteln
	Dim c As Reflection.ConstructorInfo = _
	    u.GetType().BaseType.GetConstructor(l.ToArray)
	
	'Konstruktor aufrufen, wenn vorhanden
	If Not c Is Nothing Then
	    c.Invoke(u, arrArgs)
	End If
	
	'Rückgabe des Controls
	Return u

End Function

Im Code der aufrufenden Seite stellt sich der Code so dar:

Dim intElementNo As Integer = ...
Dim strElementText As String = ...
Dim strElementClass As String = ...

Dim e As UserControl = _
	Me.LoadControlWithConstructor( _
		"~/controls/element.ascx", _
		intElementNo, _
		strElementText, _
		strElementClass)

'UserControl hinzufügen
Me.container.Controls.Add(e)

Get the code

In den vorherigen Beispielen ging es immer darum eine Instanz des UserControls zu erzeugen und es dann zur Controls-Auflistung eines Containers hinzuzufügen.

Eine andere Möglichkeit das Steuerelement zu einer Seite hinzuzufügen ist, den Html-Code des bestückten Steuerelements auszulesen und zum Beispiel in ein Literal-Control zu schreiben.

Um an das Html heranzukommen, muss man programmatisch eine komplette Seite inklusive Form erzeugen, das Control hinzufügen und anschließend die nicht benötigten Teile wegschneiden. Dies erledigt die folgende Klasse UserControlHelper, die bereits die oben genannten Erweiterungsmethoden nutzt. Enhalten sind drei Überladungen der öffentlichen Methode GetHtml (siehe Download), die LoadControl, LoadControlWithProperties bzw. LoadControlWithConstructor aufrufen und die Hilfsmethode RetrieveHtml, die die eigentliche Arbeit verrichtet.

Private Function RetrieveHtml( _
    ByVal p As Page, _
    ByVal u As UserControl) As String

    'Neues HtmlForm zum Verarbeiten des Controls
    Dim f As New HtmlForm()

    'UserControl hinzufügen
    f.Controls.Add(u)

    'HtmlForm zur Seite hinzufügen
    p.Controls.Add(f)

    'Writer initialisieren
    Dim sw As New System.IO.StringWriter()

    'Seite in den Writer ausführen 
    HttpContext.Current.Server.Execute(p, sw, False)

    'Html aufräumen
    Dim strHtml As String = sw.ToString()
    strHtml = strHtml.Replace(vbCrLf, "")

    'nicht benötigte Elemente ausschneiden
    Dim a As New ArrayList
    a.Add("<[/]?(form)[^>]*?>")
    a.Add("<div>(\n)?(<input.*?__VIEWSTATE.*?/>)(\n)?</div>")

    For Each s As String In a
        strHtml = Regex.Replace(strHtml, _
                                s, "", _
                                RegexOptions.IgnoreCase)
    Next

    'Html ausgeben
    Return strHtml

End Function

Wichtig sind vor allem die beiden Code-Aufräumaktionen mittels Regex.Replace, denn erstens kann man kein zweites Form gebrauchen und zweitens muss das ViewState-Feld weg. Ein simples EnableViewState=False (egal wo) genügt leider nicht. ASP.NET schreibt dieses Feld IMMER. Warum kann man bei Scott Mitchell nachlesen.

Der Aufruf im Code der ASPX-Seite gestaltet sich widerum sehr einfach:

Dim intElementNo As Integer = ...
Dim strElementText As String = ...
Dim strElementClass As String = ...

'Html abrufen und in Literal-Control einfügen
Dim uch As New UserControlHelper
Me.litElement.Text = uch.GetHtml("~/controls/element.ascx", _
				 intElementNo, _
				 strElementText, _
				 strElementClass)

... und nu das Ganze mittels Javascript und WebService

Das i-Tüpfelchen ist nun, den so ermittelbaren Html-Code nicht nur server-seitig zu verarbeiten, sondern ihn auch mittels eines WebServices und Javascript auf dem Client zu konsumieren!

Den Grundstein dafür hat Sanjeev Agarwal in seinem Artikel Dynamically create ASP.NET user control using JQuery and JSON enabled Ajax Web Service gelegt, in dem er auch die Grundzüge der Ermittlung des Html-Codes aus dem letzten Abschnitt beschreibt.

Die Definition des WebServices ist, mit dem zuvor beschreibenen Mechanismus der Property-Übergabe, recht einfach, nur das hier nun die Dictionary-Variante der Helper-Klasse aufgerufen wird.

<WebMethod(EnableSession:=True)> _
Public Function GetControlHtmlWithProperties( _
	ByVal controlLocation As String, _
	ByVal properties As Dictionary(Of String, Object)) As String
	
	Dim uch As New UserControlHelper
	Return uch.GetHtml(controlLocation, properties)

End Function

Neben dem virtuellen Pfad des Benutzersteuerelements, erwartet die WebMethod die Übergabe des Dictionary's, wie es die Page-Extension LoadControlWithProperties vorgibt. Sie ist, zusammen mit der Hilfsklasse, im Grunde nicht anderes als ein simpler Wrapper für die Erweiterungsmethode.

Wer schon mal mit jQuery gearbeitet hat, weiß welch schlanken Javascript-Code man damit schreiben kann. Für einen XMLHTTP-Request auf einen Webserver benötigt man eigentlich nur $.ajax, das man mit den notwendigen Parametern bestückt.

Am interessantesten an folgendem Javascript ist wohl, dass ASP.NET in der Lage ist, aus einem Object mit drei Feldern über JSON das gewünschte Dictionary zu erzeugen.

function getElementHtml(iElementNo,
                        sElementText, 
                        sElementClass) {

    //Service-Url
    var sUrl = "service/ScriptService.asmx/" + 
	       "GetControlHtmlWithProperties"

	//Control-Pfad
	var sCtlPath = "~/controls/element.ascx";            
            
	//Properties
    var oProp = new Object();
    oProp["ElementNo"] = iElementNo;
    oProp["ElementText"] = sElementText;
    oProp["ElementClass"] = sElementClass;

    //JSON-String erzeugen
    var sData = JSON.stringify({
		controlLocation: sCtlPath, 
		properties: oProp 
	});            

    //Abruf des WebService
    $.ajax({
		type: "POST",
		url: sUrl,
		contentType: "application/json; charset=utf-8",
		data: sData,
		dataType: "json",                
		//Funktion für den Erfolgsfall
		success:
		    function(sMsg) {
				var sHtml = sMsg.d;
				$("#container").append(sHtml);
		    },
		//Funktion für den Fehlerfall
		error:
		    function(XMLHttpRequest,
				textStatus, 
				errorThrown) {
				alert(XMLHttpRequest.responseText);
		    }
    });            
}

Hier wird nun auch klar, warum die Typenüberführungen in der Erweiterungsmethode LoadControlWithProperties so wichtig ist, denn kann man vielleicht in VB.NET oder C# aus einem Object heraus noch den eigentlichen Typen ermitteln, spätestens hier schlägt es fehl. Das ist auch der Grund dafür, dass ich für den Service nicht den Konstruktor-Mechanismus gewählt habe, denn WebMethods unterstützen keine ParamArrays und somit kommt man nicht an die erforderlichen echten Datentypen ran, um sie mit den vorhandenen Konstruktoren zu vergleichen.

Fazit

Für mich persönlich eröffnen sich mit diesem Code ganz neue Möglichkeiten, weil ich generell ein Fan davon bin Code-Redundanzen mit der Entwicklung von unzähligen UserControls zu begegnen. Das macht das Ganze so schön modular. Diese so bereitgestellten HTML-Häppchen nun auch jederzeit mit Javascript in eine Seite injizieren zu können ist ein ordungstechnischer Traum. Einmal schreiben und gut...

Downloads

LoadControlWithParameter.zip


kick it on dotnet-kicks.de

0 wikio-Stimme(n) Trackback-Url...

Keine Kommentare bislang...

Dein Kommentar hierzu...


Kommentar-Feed für diesen Beitrag
Gravatare werden unterstützt .:. eMail-Adressen werden nicht veröffentlicht
 

RSS-Feed

Die URL des Standard-Newsfeed von zerbit.de lautet:

http://www.zerbit.de/rssfeed.aspx

Login


 

 

Statistik



kürzlich kommentiert

Artikel 296

  • Datum: 31.10.2009
    Kategorie: ASP.NET
    Zugriffe: 2.698
    Kommentare: 0
    Trackbacks: 0

Letzte Beiträge

Kategorien

Buttons & More

Blog-Roll

Banner Piraten-Partei