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