Ja, ich gebe es offen und ehrlich zu! Ich habe sie noch nie wirklich genutzt, die ASP.NET WebForms ObjectDataSource. Irgendwie hatte ich ein Unbehagen bei der Vorstellung mir das alles bloß zusammen zu klicken und habe daher bisher einen Bogen um die ObjectDataSource gemacht. Außerdem habe ich bisher auch noch niemanden getroffen, der ernsthaft in Erwägung gezogen hätte die ObjectDataSource zu nutzen - oder sich zumindest getraut hätte, dies zuzugeben ;-)

Kürzlich war es dann aber soweit. meine Für eine kleine Demo startete ich damit, die serverseitige ASP.NET MVC Implementierung meiner Beispielanwendung für meinen jQuery Vortrag auf der dotnet Colgone nach Webforms zu konvertieren. Mein Ziel war es dabei, wo immer es nur geht, den WebForms "Baukasten" zu nehmen. Da ich bereits einen bestehenden Business Service hatte, der mir meine Objekte laden und persistieren konnte, kreuzte sie nun also meinen Weg, die ObjectDataSource.

Nach ein paar Klicks durch den Wizzard und einem beherztem F5 bestätigte sich vorerst mein initiales Vorurteil: "Totaler Mist!".

Mein Business Service hatte nämlich eine Abhängigkeit auf eine weitere Klasse, die für die  Datenhaltung zuständig war. Diese Abhängigkeit fand sich in meinem Quellcode in Form eines Konstruktor Parameters wieder. In meiner ASP.NET MVC Implementierung war der DI Container StructureMap für das Auflösen dieser Abhängigkeit zuständig.

Die WebForms Variante brach die Ausführung des Codes nun allerdings mit einer Exception ab und wies mich in freundlichem Gelb darauf hin, dass mein Business Objekt keinen parameterlosen Konstruktor hätte.

In der Hoffnung, eine Factory für mein Business Objekt angeben zu können durchsuchte ich also die Eigenschaften der ObjectDataSource. Leider wurde ich nicht fündig, fluchte ein wenig darüber, dass meine Anforderung doch gar nicht so ungewöhnlich wäre und beendete Visual Studio frustriert.

Glücklicherweise guckte ich ein wenig später doch noch mal nach einer Lösung. So kann zwar keine Eigenschaft für eine Factory angegeben werden, stattdessen wird jedoch ein Ereignis zur Verfügung gestellt, in dem ich das entsprechende Business Objekt erstellen und meiner ObjectDataSource zuweisen kann.

Konkret sieht dies wie folgt aus:

protected void AufgabenDataSource_ObjectCreating(object sender, ObjectDataSourceEventArgs e)
{
    AufgabenService service = ObjectFactory.GetInstance<AufgabenService>();
    e.ObjectInstance = service;
}

Nun habe ich über die ObjectFactory zwar einen direkten Verweis innerhalb meiner CodeBehind Datei auf den genutzten DI Container (in meinem Fall StructureMap), dies ist mir aber immer noch lieber, als die Abhängigkeit zur Persistenzschicht in meinem Business Service hart zu verdrahten.

Und die Moral von der Geschicht ...

... lautet: Erst ausprobieren und dann (gegebenenfalls) meckern ;-)


Kick it on dotnet-kicks.de
 
7/14/2010 - 07:08 AM | Comments [0] | Categories: .NET | ASP.NET | DotNetGerman Bloggers
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Passend zur Fußball WM konnte ich einen virtuellen Hattrick landen. Kürzlich ist nämlich mein dritter Artikel in Folge auf der deutschen ASP.NET Site http://www.asp.net/de veröffentlicht worden. Cool :-)

aspnet_de

Nachdem ich die Artikel des Tages nun also "gestürmt" habe, frage ich mich nur noch, wie ich mein Blog in den Feed links bekomme ...


Kick it on dotnet-kicks.de
 
6/14/2010 - 11:32 PM | Comments [1] | Categories: .NET | ASP.NET | Community | DotNetGerman Bloggers
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Das Beispielprojekt meines jQuery Vortrags während der dotnet Cologne kann ab sofort hier herunter geladen werden.

Es handelt sich dabei um ein kleines ASP.NET MVC 2 Projekt, in dem folgendes genutzt wurde:

  • ASP.NET MVC 2 ;-)
  • StructureMap als IOC Container
  • SQLite als leichtgewichtige In-Memory-DB
  • NHibernate für den Datenbankzugriff
  • jQuery für den Wow-Faktor ;-)

Die Solution liegt passend zur Veranstaltung im VS 2010 Format vor. Bei Bedarf kann ich aber auch eine VS 2008 Solution bereit stellen.

Hauptaugenmerk solltet ihr auf die Datei aufgabenlist.js setzen. Hier befindet sich der relevante jQuery / JavaScript Code. Der ganze Rest ist nur "Infrastruktur", damit ich jQuery an einem halbwegs realistischen Beispiel zeigen kann ;-)

An Feedback zu den Quellcodes bin ich immer interessiert. Am besten über das Kontaktformular, oder die während des Vortrags mitgeteilte E-Mail Adresse.

Ich weiß übrigens, dass das ASP.NET MVC Projekt keine Unit Tests beinhaltet. Angesichts der Projektgröße und der verfügbaren Zeit habe ich hier ein wenig geschludert. Thomas Bandt hat mich während des Vortrags übrigens auch darauf aufmerksam gemacht, dass mein JavaScript Code nicht via Unit Tests geprüft wurde. Auch hier gelobe ich Besserung :-)

Literaturempfehlungen

Im Anschluss an den Vortrag wurde ich außerdem gefragt, welche Literatur ich zu dem Thema empfehlen könnte.

Nun, da sich jQuery hauptsächlich mit der Modifikation des DOMs, insbesondere dem Ein- / Ausblenden sowie dynamischem CSS befasst, sollte man meiner Meinung nach zunächst über solide (X)HTML und CSS Kenntnisse verfügen (kein Witz).

Zu diesem Thema kann ich das Buch Head First HTML with CSS & XHTML empfehlen:

Außerdem können generelle JavaScript Kenntnisse auch nicht schaden ;-) Hier habe ich persönlich sehr gute Erfahrung mit dem Buch Professional JavaScript for Web Developers gemacht.

Speziell zum Thema jQuery hat mir das Buch jQuery in Action, Second Edition sehr gut gefallen. Ich habe es mir im Rahmen des Manning Early Access Program als E-Book bestellt. Wer lieber ein gedrucktes Exemplar haben möchte, muss sich noch ein wenig gedulden, kann es aber dann auch z. B. bei Amazon bestellen.


Kick it on dotnet-kicks.de
 
5/31/2010 - 11:24 PM | Comments [0] | Categories: .NET | ASP.NET | Community | DotNetGerman Bloggers | jQuery
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Letzten Freitag hatte ich die Freude, an der dotnet Cologne teilzunehmen. Mit über 300 Teilnehmern war es ein wirklich riesiges Event, dass sich vor "professionellen", oder besser gesagt kommerziellen Konferenzen nicht verstecken muss.

Der Teilnehmer

Vor Ort war ich in mehreren Rollen. Zum einen natürlich als Teilnehmer. In dieser Rolle nutzte ich die Möglichkeit, endlich mal die Gesichter zu einigen Bekannten aus der Community zu sehen und auch mal persönlich das ein oder andere Wort zu wechseln. Außerdem hörte ich mir auch spannende Vorträge, unter anderem von Jörg Krause zu Sharepoint als Entwicklungsplattform, Neues in Silverlight 4 von Stefan Lange sowie Neues in ASP.NET 4.0 von Jan Welker.

Irritierend fand ich, dass gefühlte 90 % der Besucher von Jörg Krauses Sharepoint Vortrag keinerlei Sharepoint Vorkenntnisse hatten und somit wohl nicht im geringsten wussten, wie so eine Site, eine Sitecollection, ein Web, eine Liste, ein Webpart ... an der Oberfläche wohl aussehen. Die gleiche Situation habe ich übrigens auch mehrfach schon auf der Shareconnect (Basta Sharepoint Days) beobachtet. Vielleicht wäre hier einfach mal ein "Was ich über Sharepoint wissen sollte, bevor ich Visual Studio aufmache" Vortrag angebracht. Leider habe ich einen solchen Vortrag bisher noch auf keiner (Entwickler-)Konferenz im Angebot gesehen. Nichts desto trotz fand ich Jörgs Vortrag prima! Auch die anderen besuchten Vorträge haben mir gut gefallen, daher mein Fazit als Teilnehmer:

Voller Erfolg! Gute Vorträge, gute Kontaktmöglichkeiten zur Community, was will man mehr.

Der Aussteller

Unter den Sponsoren des Events war unter anderem auch die Firma Infragistics. Wie auch auf anderen Konferenzen lies ich es mir als fleißiger Infragistics MVP  in den Pausen natürlich nicht nehmen, Kiril und Nils tatkräftig zu unterstützen. Dies war auch bitter nötig, da der Andrang am Stand weit höher war, als ich es von anderen Konferenzen gewohnt war. Kamen wir normalerweise zu zweit immer ganz gut zurecht, waren dieses Mal sogar drei Personen eigentlich schon fast zu wenig.

Fazit als Aussteller: Tolle Veranstaltung! Viele Kontakte, interessante und zum Teil sogar sehr trickreiche Fragen, genauso muss es sein!

Der User Group Leader

Jeder der schon mal ein User Group Treffen besucht hat wird sich sicherlich fragen:

Wo kommen eigentlich die Sprecher her?

Nun ja, als sie ganz klein waren, wird sie voraussichtlich einer der beiden hier unten gebracht haben.

image

Foto: Valter Jacinto | Portugal   http://www.flickr.com/photos/valter/87429062/sizes/m/
Creative Commons License

Irgendwann werden die Jungs und Mädels dann aber groß und spätestens dann stellt sich für einen User Group Leiter die Frage:

Wo bekomme ich eigentlich Sprecher her?

Ein besonders guter Ort, Sprecher für die eigene User Group zu finden ist selbstverständlich eine Konferenz, denn dort treten Speaker häufig in Rudeln auf ;-)

Also machte ich mich während der dotnet Cologne auf den Weg und zog Sprecher für die nächsten Treffen der .NET User Group Koblenz an Land.

Die Ausbeute war übrigens mit zwei definitiven, einer relativ verbindlichen und einer losen Zusage recht gut. Daher auch hier: dotnet Cologne, 12 Points ;-)

Der Sprecher

Zu guter letzt (und angesichts der Agenda ist dies wörtlich gemeint), war ich auch als Sprecher unterwegs. Mein Thema war die Einführung in jQuery unter dem spontan geänderten Titel:

jQuery - oder warum Sie JavaScript in Zukunft nicht mehr hassen werden.

Den Verlauf des Vortrags würde ich wie folgt beschreiben

  • Der Saal füllt sich, die Menge wird still. Ich will loslegen, aber mein Mikro überlegt sich, dass es sich lieber in meinem T-Shirt verdreht. Kein Mensch hört mich ... so ein Mist
  • Mikro Problem gelöst, schnell durch die Folien gejagt. Auf gehts zur Demo!
  • Meine ASP.NET MVC Anwendung reißt niemand vom Hocker und stößt kaum auf Interesse
  • Ist zum Glück nicht schlimm, schließlich ist mein Thema ja auch jQuery und nicht ASP.NET MVC ;-)
  • Die ersten UI Gimmicks (alternierende Tabellenzeilen, Hover Effekte) zaubern ein müdes Lächeln auf die Gesichter der Menge - da muss wohl noch mehr her
  • Ich erstelle mit einer Zeile jQuery Code auf- und zuklappbare Bereiche in der Sidebar der Anwendung. Im Publikum sehe ich die ersten funkelnden Augen
  • Auf der Welle muss ich weiter reiten, also jetzt schnell ein wenig Ajax;-)
  • In der Einleitung habe ich etwas vom Update Panel erzählt. Heißt dann wohl ich sollte auch ein wenig WebForms zeigen. Ich entschließe mich also, im Firebug mal zu zeigen, was über die Leitung geht wenn man Ajax mit dem Update Panel erlegt erledigt. Als ich zum ViewState scrolle scheinen einige Teilnehmer zu glauben ich hätte gerade die Matrix gehacked oder zumindest gedebugged.
  • Oh je, nur noch 3 Minuten Zeit und ich habe doch versprochen früher Schluss zu machen - jetzt muss schnell etwas großartiges her. Ich greife also noch mal in die Trickkiste und greife zu meinem größten Trumpf:
    runde Ecken;-)
    Puh, geschafft, die Zuschauer jubeln. Ein Glück, dass es runde Ecken gibt ;-)

Mein Fazit als Sprecher lautet also:

Wahnsinn! Auch wenn es nur ein Einsteiger Vortrag war und laut Handzeichen mindestens die Hälfte der Anwesenden jQuery bereits kannte und nutzte war das Publikum allem Anschein nach während des Vortrags voll dabei. So macht vortragen Spaß!

Der Grillfreund

Zum Abschluss fand für einige Teilnehmer, Sprecher und Sponsoren dann noch die durch Microsoft gesponsorte und durch Jan Welker gestifftete Grill-Party des dotnet Forums statt. Essen und Getränke waren sehr lecker, die Gespräche spannend, von daher auch hier mein Kompliment.

Fazit

Der Besuch der dotnet Cologne hätte in keinster Weise besser laufen können. Großes Lob und alle Achtung an die Organisatoren! Nächstes Jahr bin ich - in welcher Form auch immer - sicherlich auch wieder mit dabei!

 

Hey, du hast dir die Zeit genommen, den ganzen Beitrag zu lesen, oder zumindest bis hier hin zu scrollen. Nimm dir doch bitte auch noch die Zeit, ihn über den unten stehenden Button bei dotnet-kicks.de zu kicken!


Kick it on dotnet-kicks.de
 
5/31/2010 - 11:03 PM | Comments [0] | Categories: .NET | ASP.NET | Community | DotNetGerman Bloggers | jQuery | Vorträge
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Habe ich für meine Blog Beiträge gefunden. Zumindest die englischen, die einen Bezug zu den Infragistics NetAdvantage Controls haben.

Seit kurzem gibt auf der Infragistics Homepage nämlich einen MVP Blog, für das ich einer der Autoren bin.

Selbstverständlich gibt es auch schon einen ersten Blog Post von mir. Dieser beinhaltet neben einer kurzen (OK, langen ;-)) Vorstellung ein kleines jQuery Script, welches hilfreich beim Einsatz des Infragistics ASP.NET Aikido WebDropdown Controls ist.

igmvpblog


Kick it on dotnet-kicks.de
 
5/23/2010 - 04:24 PM | Comments [0] | Categories: .NET | ASP.NET | DotNetGerman Bloggers | Infragistics | jQuery
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Moderne Webseiten beinhalten in der heutigen Zeit einen hohen Anteil clientseitiger Quellcodes in Form von JavaScript. Für diesen Anwender führt dies im Idealfall zu einer verbesserten Benutzbarkeit der Applikation, da diese neben netten UI Effekten nun meist durch Ajax auch weitaus flüssiger bedienbar ist.

Bei vielen Entwicklern solcher dynamischer Webseiten führt der vermehrte JavaScript Anteil allerdings häufig zu Wein- und/oder Schreikrämpfen, denn nur die wenigsten mögen JavaScript wirklich. Die Ursachen für diese Antipathie liegen jedoch selten an JavaScript als Sprache selbst, sondern in den meisten Fällen eher an der unterschiedlichen Implementation des DOMs / BOMs durch die verschiedenen Browserhersteller. Diese Inkonsistenz führt nämlich dazu, dass JavaScript Code, der in einem Browser bzw. einer Version eines Browsers problemlos läuft im nächsten Browser Fehler verursacht und schlichtweg nicht läuft.

Weiter wird häufig bemängelt, das Selektionen fernab von einem einfachen document.getElementById(IdMeinesElements) relativ kompliziert sind. Als Beispiel wären hier zu nennen: alle Elemente mit einer bestimmten Klasse, ungerade Zeilen einer Tabelle, das aktivierte Element einer RadioButtonGroup usw.

Und nun?

Nun gibt es folgende Strategien mit diesen Problemen umzugehen:

  1. Man drückt sich immer davor JavaScript zu schreiben und wälzt dies auf einen Kollegen ab
  2. Man kopiert sich irgendwoher JavaScript Codeschnippsel, die angeben browserunabhängig zu sein und hofft, das dem auch so ist
  3. Man investiert Unmengen Zeit in die eigene browserübergreifende Implementierung diverser Funktionalitäten
  4. Man bedient sich eines der am Markt verfügbaren JavaScript Frameworks

Strategie Nr. 1 mag eine Weile ganz gut funktionieren, früher oder später wird aber der Zeitpunkt kommen, an dem man sich nicht mehr drücken kann (ich spreche da aus eigener Erfahrung ;-)).

Strategie Nr. 2 funktioniert immer dann, wenn der Umfang des benötigten JavaScript Codes überschaubar ist. Gemeinsam mit der Applikation wird aber auch irgendwann der zusammenkopierte Code wachsen und die Wartbarkeit entsprechend sinken.

Strategie Nr. 3 ist sicherlich eine prima Idee für alle die außerdem auch ein eigenes Logging Framework, einen eigenen OR Mapper usw implementiert haben, oder kurz gesagt für alle die gerne das Rad neu erfinden ;-)

Gangbar scheint also nur Strategie Nr. 4 zu sein. Schaut man sich nun am Markt um, stößt man unweigerlich auf jQuery. Die freie JavaScript Library adressiert unter anderem genau die zuvor genannten Probleme und hat in letzter Zeit einen wahren Hype verursacht. Dieser Rummel wurde sicherlich auch dadurch verstärkt, dass Microsoft jQuery offiziell supportet, mit Visual Studio ausliefert und die Arbeiten an der hauseigenen JavaScript Bibliothek ASP.NET Ajax Library zugunsten von jQuery eingestellt hat.

Wie legen wir nun aber mit jQuery los? Genau diese Frage möchte ich in diesem und den folgenden Blog Posts beantworten. Da jQuery in einem neuen ASP.NET MVC Projekt automatisch hinzugefügt wird und ASP.NET MVC Entwickler somit sowieso jQuery gewöhnt sind, soll als Beispiel zunächst eine Webforms Anwendung dienen.

Auf die Plätze, fertig, los!

Wenn man jQuery benutzen möchte, besteht der erste Schritt darin, die freie JavaScript Library in seine Seite einzubinden. Dazu lädt man unter http://www.jQuery.com einfach die aktuelle Version der Bibliothek herunter und kopiert diese anschließend zum Beispiel in einen Unterordner Scripts seiner Webanwendung. jQuery ist übrigens in einer für Menschen lesbaren (z. B. jQuery-1.4.2.js) und in einer verkleinerten Version (z, B. jQuery-1.4.2.min.js) verfügbar. In der verkleinerten Version sind unnötige Leerzeichen, Zeilenumbrüche und Kommentare entfernt. Außerdem wurden die Namen der Variablen und nicht öffentlichen Funktionen auf ein oder zwei Buchstaben verkürzt. Sinn dieser Maßnahme ist es die zum Client übertragene Datenmenge zu reduzieren. So ist die verkleinerte Version knappe 100 kb kleiner als die lesbare Variante. Für Produktivszenarien sollte demnach also in jedem Fall die .min Version genutzt werden, wohingegen während der Entwicklungszeit eher die lesbare Variante eingebunden werden sollte. So kann man den Scriptcode im Fall der Fälle nämlich noch debuggen.

So, genug der Vorrede und zurück zur Praxis. Tatsächlich einbinden können wir jQuery nun über folgende Zeile:

<script src="Scripts/jquery-1.4.2.js" type="text/javascript"></script>

Platzieren sollte man diese Zeile übrigens innerhalb des Kopfbereichs der Seite:

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <script src="Scripts/jquery-1.4.2.js" type="text/javascript"></script>
</head>

Prima, aber was mache ich nun damit?

Gute Frage! Jetzt haben wir jQuery zwar eingebunden, aber wie geht es jetzt weiter. Dazu sollten wir zunächst kurz einen Blick darauf werfen, was wir normalerweise mit JavaScript machen. In den meißten Fällen reduziert sich dies auf:

  • Elemente aus dem DOM zu selektieren
  • Selektierte Elemente zu manipulieren (ein- / ausblenden, Styledefinitionen zu verändern...)
  • Code bei bestimmten Ereignissen ausführen (z. B. click event)
  • Neue Elemente dem DOM hinzuzufügen
  • Elemente aus dem DOM zu löschen
  • AJAX Aufrufe zum Server zu machen und die Antwort zu Verarbeiten

Als kleinen Einstieg picken wir uns exemplarisch die ersten drei Punkte heraus. Wir werden also DOM Elemente selektieren und diese manipulieren. Geschehen wird dies bei dem Klick auf einen Button.

Dazu werden wir eine Meldungszeile, ähnlich wie man sie von Stackoverflow kennt nachbauen.

01_so_message

Der erste Schritt besteht in der Erstellung eines HTML und CSS Grundgerüsts:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>jQuery Demo</title>

    <style type="text/css">
        body
        {
            margin: 0px;
        }
        #message
        {
            background-color: #FFFF88;
            border-bottom: solid 1px #000000;
            font-weight: bold;
            text-align: center;
            padding: 8px;
            margin: 0px;
            display:none;
            font-family: Arial, Verdana, Sans-Serif;
        }
        #message a
        {
            float: right;
            border: solid 3px black;
            font-family: Arial, Verdana, Sans-Serif;
            font-weight: bold;
            text-decoration: none;
            color: Black;
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
    <div id="message"><span>Dummy Nachricht</span><a href="Default.aspx">X</a>
    </div>
    <div id="content">
        <p>
            Dies ist eine jQuery Demowebsite</p>
        <button id="showMessage">
            Klick mich</button>
    </div>
    </form>
</body>
</html>

Wie man sieht, wird ein DIV Element  mit dem Namen "message" erstellt, dass als Unterlemente ein SPAN Element und einen Link enthält. Über CSS wird dieses DIV Element noch ein wenig gestyled und initial ausgeblendet. Außerdem ist auf der Seite noch ein Button definiert. Ein Klick auf diesen Button soll der Auslöser sein, um die Meldung anzuzeigen.

Wie aber genau sieht nun der Code aus, um die Nachricht - mit verändertem Text - anzuzeigen?

Mit der Hilfe von jQuery ist dies garnicht so schwer:

$("#showMessage").click(function(evt) {
    evt.preventDefault();
    $("#message span").text("Du hast den Button geklickt. Klicke nun auf das X, um die Nachricht auszublenden.");
    $("#message").fadeIn(2000);
});
Auf den ersten Blick mag der Code etwas verwirrend aussehen. Vor allem durch die vielen $-Zeichen. Daher ein paar erklärende Worte:

In Zeile 1 selektieren wir mit dem Befehl: $("#showMessage") zunächst das DOM Element mit der ID showMessage, also unseren Button. Das $-Zeichen ist übrigens ein Alias für die jQuery Funktion. Statt $("message") hätte ich also auch jQuery("$message") schreiben können. Die Rückgabe des Aufrufs - unabhängig ob via jQuery(...) oder $(...) ist ein Objekt vom Typ jQuery. Dieses Objekt beinhaltet die selektieren DOM Elemente bzw. das selektierte DOM Element sowie einige weitere Funktionen.

In unserem Fall bekommen wir also ein Objekt vom Typ jQuery zurück, dass das DOM Element showMessage beinhaltet. Für dieses DOM Element wird nun eine anonyme Funktion als Handler für das Ereignis Click registriert.

In Zeile 2 wird mit dem Befehl evt.preventDefault(); nun die Standardaktion die der Browser bei diesem Ereignis, also z. B. das posten des Formulars nach Klick auf den Button, verhindert.

In Zeile 3 werden anschließend alle span Elemente innerhalb des DOM Elements mit der Id message selektiert. In unserem Fall ist dies also genau eins. Für dieses span Element wird mit der Funktion text jetzt ein neuer Text vergeben. Außerdem wird das Element mit der Id message in Zeile 4 langsam (über einen Zeitraum von zwei Sekunden) eingeblendet.

Der Quellcode zum Ausblenden der Nachricht sieht ähnlich aus:

$("#message a").click( function(evt) {
     evt.preventDefault();
     $("#message").fadeOut("slow");
 });

In Zeile 1 wird an alle A-Elemente innerhalb des DOM Elements mit der Id message ein Eventhandler für das Ereignis Click angehangen. Dieser verhindert in Zeile 2 die Standardaktion des Links und blendet in Zeile 3 unsere Nachrichtenzeile wieder aus. Dieses mal wird statt einer Angabe in Millisekunden der String "slow" als Argument übergeben. Dieser ist in den jQuery Quellcodes mit einem Wert von 600 ms hinterlegt.

Perfekt, aber wo schreibe ich den Code nun rein?

So, jetzt wo wir eigentlich den ganzen Quellcode fertig haben stellt sich natürlich die Frage, wie wir ihn in unsere Seite einbinden. Eine naive Implementierung sähe wie folgt aus:

<head>
<!-- ... -->
<script type="text/javascript">
    $("#showMessage").click(function(evt) {
        evt.preventDefault();
        $("#message span").text("Du hast den Button geklickt. Klicke nun auf das X, um die Nachricht auszublenden.");
        $("#message").fadeIn(2000);
    });
    $("#message a").click( function(evt) {
        evt.preventDefault();
        $("#message").fadeOut("slow");
    });
</script>
<!-- ... -->
</head>

Der Code würde also einfach in ein Scripttag innerhalb des Head Tags kopiert werden. Dies läuft so nicht! Der Grund ist, dass mit diesem Code versucht wird, ein Eventhandler an ein DOM Element zu binden, das es zu diesem Zeitpunkt noch garnicht gibt.

Wie sieht aber die Lösung für das Problem aus?

Alles zu seiner Zeit

Wie wir zuvor gesehen haben, ist unser Code wirkungslos, wenn wir ihn ausführen ehe es ein entsprechendes DOM Element gibt. Daher sollten wir ihn erst auslösen, sobald das DOM vollständig initialisiert ist.

Ein weg dies zu erreichen wäre es, den Code aufzurufen wenn das Ereignis window.onload eintritt:

window.onload = function() {
  $("#showMessage").click(function(evt) {
     evt.preventDefault();
     // Restlicher Code hier
  });
}

Diese Variante würde bereits fehlerfrei funktionieren. Allerdings wird das Ereignis onload erst ausgelöst, wenn das DOM vollständig initialisiert wurde und alle externen Ressourcen, wie zum Beispiel Bilder oder Stylesheets geladen wurden. Dies kann von Fall zu Fall recht lange dauern, so dass der Anwender den Button bereits anklicken könnte, ohne dass unser Script ausgeführt wird. Zum Glück bietet jQuery einen besseren Ansatz, nämlich $(document).ready. In dieser Variante wird das Ereignis ready ausgelöst, sobald das DOM vollständig initialisiert wurde, aber bevor externe Ressourcen geladen wurden.

Das vollständige Beispiel mit $(document).ready sieht dann wie folgt aus:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>jQuery Demo</title>

    <script src="Scripts/jquery-1.4.1.js" type="text/javascript"></script>

    <style type="text/css">
        body
        {
            margin: 0px;
        }
        #message
        {
            background-color: #FFFF88;
            border-bottom: solid 1px #000000;
            font-weight: bold;
            text-align: center;
            padding: 8px;
            margin: 0px;
            display:none;
            font-family: Arial, Verdana, Sans-Serif;
        }
        #message a
        {
            float: right;
            border: solid 3px black;
            font-family: Arial, Verdana, Sans-Serif;
            font-weight: bold;
            text-decoration: none;
            color: Black;
        }
    </style>

    <script type="text/javascript">
        $(document).ready(function() {
            $("#showMessage").click(function(evt) {
                evt.preventDefault();
                $("#message span").text("Du hast den Button geklickt. Klicke nun auf das X, um die Nachricht auszublenden.");
                $("#message").fadeIn(2000);
            });
            $("#message a").click(function(evt) {
                evt.preventDefault();
                $("#message").fadeOut("slow");
            }
        }
      });
    </script>

</head>
<body>
    <form id="form1" runat="server">
    <div id="message"><span>Dummy Nachricht</span><a href="Default.aspx">X</a>
    </div>
    <div id="content">
        <p>
            Dies ist eine jQuery Demowebsite</p>
        <button id="showMessage">
            Klick mich</button>
    </div>
    </form>
</body>
</html>

Ausblick und Fazit

Wie dieser Blog Eintrag gezeigt hat, ist jQuery eine recht komfortabel zu bediene JavaScript Library, mit der sich mit wenigen Zeilen Script Code interessante Effekte erzielen lassen. Neben der intuitiven und Browser unabhängigen API besticht die Bibliothek vor allem durch die Fülle an Funktionen und erhältlichen Plug-Ins. Selbstverständlich konnte ich hier nur einen kurzen (ersten) Einblick verschaffen. Da ich das Thema Clientseitige Entwicklung in (ASP.NET) Webanwendungen jedoch für sehr interessant halte, habe ich vor in der nächsten Zeit weitere Einträge zum Thema jQuery mit folgenden Schwerpunkten zu schreiben:

  • Nutzen von Content Delivery Networks (CDNs)
  • jQuery Selektoren
  • Möglichkeiten der DOM Manipulation
  • jQuery und Firebug
  • jQuery Plugins selber entwickeln
  • jQuery UI
  • jQuery Utility Funktionen
  • jQuery und Ajax für Webforms und ASP.NET MVC

Eventuell werde ich im Anschluss an die Artikel auch kurze Video Tutorials bereitstellen.

Bevor ich allerdings loslege würde mich natürlich interessieren, ob das Thema für euch überhaupt von Interesse ist. Am liebsten in Form eines kurzen Kommentars.

Sollte jemand von euch übrigens das Verlangen haben, das Thema jQuery, oder auch generell ASP.NET mit mir persönlich in lockerer Atmosphäre zu diskutieren: Am 28. Mai werde ich einen Einsteigervortrag zu jQuery auf der dotnet Cologne 2010 halten. Über zahlreiche Besucher des Vortrags und natürlich auch spannende Diskussionen danach würde ich mich sehr freuen.

In den Pausen findet man ihr mich übrigens wahrscheinlich im Ausstellerbereich am Stand der Firma Infragistics. Dort wäre ich dann zusätzlich auch für den ein oder anderen Plausch über die Infragistics NetAdvantage Komponenten zu haben.

War dieser Artikel hilfreich für dich? Dann kicke ihn doch bitte bei dotnet-kicks.de!


Kick it on dotnet-kicks.de
 
5/4/2010 - 07:47 PM | Comments [4] | Categories: .NET | ASP.NET | Community | DotNetGerman Bloggers | Infragistics | jQuery
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Startet man eine ASP.NET Anwendung zum ersten Mal, kommt man nicht gerade in einen Geschwindigkeitsrausch. Dies liegt zum Beispiel daran, dass der IIS den ASP.NET Worker Prozess hochfahren muss. Außerdem läuft eventuell im Ereignis Application_Start hinterlegter Initialisierungscode und schließlich müssen die vorliegenden Assemblies noch durch den JITer in nativen Code überführt werden.

All dies führt dazu, dass man relativ selten so etwas wie "rasend schnell" hört, wenn vom ersten Zugriff auf eine ASP.NET Anwendung gesprochen wird.

Glücklicherweise ist all dies nach dem ersten Request einer Seite kein Problem mehr. Der Worker Prozess ist da, der Initialisierungscode lief und die Just In Time Compilation lief auch.

Zu einem Problem wird die Situation jedoch, wenn man seine ASP.NET Anwendung während der Entwicklung als ein Bestandteil des Nightly Builds automatisch deployed und auch seinem Kunden Zugriff auf diesen täglich frischen Applikationsstand gibt.

Da Kunden morgens nämlich meist früher als Entwickler im Büro sind, sind sie auch die ersten, die die Webanwendung öffnen, um zu sehen, was am Vortrag umgesetzt wurde. Ist dieser erste Zugriff nun aus den oben genannten Gründen langsam, ist negatives Feedback des Kundens zur Applikationsperformance - oder sogar noch schlimmer: im Stillen sinkendes vertrauen in Applikation und Entwickler nicht selten die Folge.

Um dieses Problem zu umgehen, habe ich ein kleines Powershell Script geschrieben, das nach Angabe einer URL die einzelnen Seiten einer Webapplikation ansurft. 

function warmup-site( [string] $rootUrl){
	
	$proxy = New-Object System.Net.WebProxy("mein.firmen.proxy:8080")
	$proxy.UseDefaultCredentials = 1
	
	$wc = New-Object System.Net.WebClient
	$wc.Proxy = $proxy
	
	$pages = @("default.aspx", "seite1.aspx", "seite2.aspx", "subfolder/seite3.aspx")

	# Jede Seite 3 Mal ansurfen, um Sie "warmzuklicken"
	for ($i=0; $i -lt 3; $i++){
		foreach($page in $pages){
			trap [System.Net.WebException] {
			  write-error $("TRAPPED: " + $_.Exception.ToString());
			  continue;
		   	}
			$content = $wc.DownloadString($rootUrl+$page)
			write $("URL " + $rootUrl+$page + " angesurft");
		}
	}

}

warmup-site("http://meine.url/")

Bisher gebe ich die einzelnen Seiten die angefragt werden sollen noch manuell innerhalb des Arrays $pages an. Im nächsten Schritt würde ich jedoch lieber nur noch die URL der Sitemap angeben, diese auslesen und dann sämtliche in der Sitemap aufgeführten URLs anfragen. Da ich jedoch noch Powershell Neuling bin, könnte es noch ein wenig dauern, bis ich die Lösung so weit automatisiert habe. Sollte einer der Leser nun sagen: "Das ist doch ein Dreizeiler, den ich in der Kaffeepause aus dem Ärmel schütteln könnte", dann würde ich darum bitten genau diesen Dreizeiler über die Kommentarfunktion meines Blogs hier einzustellen ;-)

Was bringts?

Die Anwendung startet beim ersten Request nun bedeutend schneller. Somit ist der Kunde beruhigt und wir können uns auf die wichtigen Sachen des Projekts konzentrieren ;-)

Habe übrigens gesehen, dass es ab dem IIS 7 (oder 7.5?) von Hause aus eine Warmup gibt. Hat jemand von euch Erfahrung damit? Im Moment ist es für mich zwar noch nicht akut, da wir den IIS 6 nutzen, wäre aber Interessant für die Zukunft ein paar Erfahrungsberichte zur Hand zu haben.


Kick it on dotnet-kicks.de
 
4/13/2010 - 09:05 AM | Comments [1] | Categories: .NET | ASP.NET | DotNetGerman Bloggers
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Vor einiger Zeit habe ich ein kleines Einsteigertutorial zum Umgang mit WinDbg geschrieben. Etwas später folgte dann auch ein kurzes Video hierzu.

Sowohl im Tutorial als auch im Video war eine Windows Forms Anwendung das Ziel meiner Debuggingaktivitäten. In einem kurzen Nebensatz erwähnte ich, dass die Zielanwendung bei der gezeigten Vorgehensweise, nämlich dem direkten Anhängen an den Prozess der Wahl, zumindest zeitweise blockiert wird.

In einer Windows Forms Anwendung mag dies kein Problem sein, schließlich bin ich ja alleine auf dem System. Unschön wird es jedoch, wenn man Probleme in einer produktiven ASP.NET Anwendung sucht. Verständlicherweise reagieren hier nämlich die wenigsten Anwender erfreut, wenn die Website steht weil gerade jemand im Hintergrund daran herumfummelt. Außerdem ist es je nach IT Richtlinie auch problematisch den lokalen Administrator des Kundens davon zu überzeugen, dass man mal gerade physikalisch oder auch nur per Remote Desktop an seinen Webserver will.

Und nun?

Abhilfe schafft hier die VBScript Datei adplus.vbs innerhalb des WinDbg Verzeichnisses. Sie ermöglicht es, ein aktuelles Speicherabbild des problematischen Prozesses in Form einer *.DMP Datei zu erstellen. Diese kann wiederum in WinDbg geöffnet und analysiert werden.

Adplus kann in zwei verschiedenen Modi genutzt werden: hang und crash.

Im hang Modus wird einmalig ein aktueller Memory Dump des Prozesses zum Zeitpunkt der Ausführung des Scripts gezogen. Dieser Modus eignet sich besonders wenn die Applikation "hängt" ;-), also in Deadlock Szenarien, oder aber auch um Memory Leaks zu finden. Zur besseren Analyse sollten im Fall ein Memory Leaks jedoch mehrere Dumps bei verschieden hoher Speicherauslastung gezogen werden.

Der Befehl zum Erzeugen eines Dumps im hang Modus lautet übrigens:

adplus.vbs -hang -p processId

Die Id des Prozesses kann entweder über den Taskmanager, oder aber im Fall einer ASP.NET Anwendung die in einem IIS 6 gehostet wird, über den Befehl IISAPP gefunden werden.

Alternativ zum Parameter -p kann über den Parameter -pn auch ein Prozessname angegeben werden. Im Fall einer ASP.NET Anwendung wäre dies also W3WP.exe (Windows Server mit IIS) bzw. WebDev.WebServer.exe im Falle des lokalen Entwicklungsservers. Außerdem kann über den optionalen Parameter -o auch ein dediziertes Ausgabeverzeichnis angegeben werden. Standardmäßig wird nämlich sonst ein Unterverzeichnis, unterhalb des Ordners aus dem adplus heraus aufgerufen wurde, angelegt.

Innerhalb des Zielverzeichnisses wird nun eine Datei mit der Endung .DMP angelegt. Diese kann in WinDbg über den Menüpunkt File -> Open Crash Dump ... geöffnet und mit den bereits bekannten Befehlen analysiert werden.

Der zweite Modus neben hang ist der Modus crash.

Der Aufruf erfolgt hier ähnlich zum hand Modus, mit dem einzigen Unterschied, dass statt -hang -crash übergeben wird.

adplus.vbs -crash -p processId

Im Crash Modus bleibt der Debugger so lange am entsprechenden Prozess angehangen, bis dieser unfreiwillig beendet wird. Tritt dieser Fall ein, wird ein Dump auf die Festplatte geschrieben. Außerdem werden Dumps geschrieben, wenn ein zuvor definierter Breakpoint erreicht wird, oder aber eine access violation Exception auftritt.

Fazit

Mit adplus liegt ein leichtgewichtiges Skript zur Hand, dass bei der Problemanalyse innerhalb produktiver Umgebungen enorm helfen kann. Gerade in Umgebungen, in denen das Live-System nicht durch Debugging blockiert werden darf, oder aber der Administrator keinen Zugriff auf das System gewährt, kann das Skript seine stärken Ausspielen.


Kick it on dotnet-kicks.de
 
4/12/2010 - 10:07 PM | Comments [0] | Categories: .NET | ASP.NET | DotNetGerman Bloggers | WinDbg
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

In Teil 5 meiner ASP.NET Webforms und Ajax Serie wies René Drescher-Hackel mich darauf hin, dass ich einen Big Player vollkommen außer acht gelassen habe: AjaxPro von Michael Schwarz.

Das wollte ich natürlich nicht auf mir sitzen lassen. Daher befassen wir uns heute mit:

AjaxPro :-)

AjaxPro ist eine freie Library von Michael Schwarz, die Ajax unter ASP.NET ohne Postbacks ermöglicht. Vergleichbar ist AjaxPro somit am ehesten mit den in Teil 4 vorgestellten PageMethods. Ein großer Vorteil gegenüber diesen ist jedoch die Möglichkeit, AjaxPro auch unter ASP.NET 1.1 einsetzen zu können (ja - ich kenne wirklich noch aktive ASP.NET 1.1 Projekte ;-)). Angemerkt werden sollte noch, dass AjaxPro keiner Weiterentwicklung mehr unterliegt. Michael Schwarz schrieb in in seinem Blog, dass er die Arbeit an der Library einstellt. Als Alternative empfiehlt er den Einsatz von PageMethods.

Lost gehts

AjaxPro ist relativ einfach eingebunden. Es genügt einen Verweis auf die Datei AjaxPro.2.dll hinzuzufügen.

01addreference

Weiter muss AjaxPro in der Datei web.config des Webprojekts bekannt gemacht werden. Dies geschieht über das Hinzufügen eines entsprechenden Http-Handlers:

<httpHandlers>
   ...
   <add verb="POST,GET" path="ajaxpro/*.ashx" type="AjaxPro.AjaxHandlerFactory, AjaxPro.2"/>
</httpHandlers>

Anschließend müssen die Methoden einer Seite, die später via Ajax aufgerufen werden sollen, mit dem Attribut AjaxMethod annotiert werden. Dies funktioniert also ähnlich wie die bereits vorgestellten PageMethods. Der Quellcode dieses Beitrags basiert übrigens wieder auf dem Quellcode der vorherigen Teile dieser Serie.

public partial class Teil7 : Page
{
    [AjaxPro.AjaxMethod]
    public string ReadStaticFile()
    {
        string fileContent;
        using (var reader = 
            new StreamReader(
                System.Web.HttpContext.Current.Server.MapPath("~/static.html")))
        {
            fileContent = reader.ReadToEnd();
        }
        return fileContent;
    }
}

Ein netter Nebeneffekt im Vergleich zu Pagemethods ist übrigens, dass die entsprechende Methode zwar statisch sein kann, dies aber nicht muss. Außerdem muss sich die Methode noch nicht einmal zwingend innerhalb einer Webseite (ASPX-Datei) befinden. Stattdessen kann Sie Mitglied einer beliebigen öffentlichen Klasse sein.

Um diese Methode nun komfortabel per JavaScript aufrufen zu können, muss die zugehörige Klasse noch als AjaxTyp registriert werden. Dies geschieht in der Methode Page_Load der Klasse Teil7.

protected void Page_Load(object sender, System.EventArgs e)
{
    AjaxPro.Utility.RegisterTypeForAjax(typeof(Teil7));
}

Der Aufruf innerhalb der Seite kann nun wie folgt erfolgen:

<a href="#" id="StaticFileLink" onclick="Teil7.ReadStaticFile(ajaxdemo_callback)">Hier klicken zum Request einer statischen Datei</a><br />

Bei dem übergebenen Parametger ajaxdemo_callback handelt es sich um eine Callback Funktion, die die Antwort des Servers verarbeitet. In unserem Beispiel sieht sie wie folgt aus:

<script type="text/javascript">
    function ajaxdemo_callback(result) {
       var content = document.getElementById('content');
       content.innerHTML = result.value;
    }
</script>

Die Seite ist nun funktionsfähig und kann gestartet werden. Ein Blick in die Firebug Ausgabe zeigt, dass neben der eigentlichen Seite automatisch noch 4 JavaScript Dateien geladen wurden. Diese sind für die Serverkommunikation zuständig und haben eine Gesamtgröße von nur 25 kb! Im Vergleich zu den zuvor vorgestellten Lösungen ist dies das beste Ergebnis!

02_initialesladen

Auch beim Aufruf der serverseitigen Methode schlägt sich AjaxPro gut. Hier gehen lediglich 111 Byte über die Leitung:

03_ajaxcall

Der Aufruf selbst hat eine Datei mit der Endung *.ashx innerhalb des Pfads /ajaxpro zum Ziel. In meinem Beispiel lautet die URL: http://localhost:2800/AjaxDemo/ajaxpro/Teil7,App_Web_mke8f-yp.ashx. Er wird somit durch den zuvor in der web.config eingetragenen Http-Handler behandelt. Außerdem ist innerhalb der URL, wie man sehen kann, der Name der Klasse in der sich die entsprechende Methode befindet kodiert. Die Methode selbst wird im Header des Aufrufs als Attribut X-AjaxPro-Method übergeben.

Webservices

Nachdem sich PageMethods relativ einfach aufrufen ließen, stellt sich nun natürlich die Frage, wie die Methoden eines Webservices aufgerufen werden können.

Eine Variante wäre es, die Methoden innerhalb des Webservices nicht nur mit dem Attribut [WebMethod] zu anotieren, sondern zusätzlich auch mit [AjaxPro.AjaxMethod] zu versehen. Diese Variante ist selbstverständlich nur für eigene und nicht für externe Webservices möglich.

Ein allgemeingültiger Ansatz besteht darin, händisch Proxy-Methoden innerhalb der Seite für die gewünschten Webservice Methoden anzulegen.

[AjaxPro.AjaxMethod]
public string CallHelloWorldService()
{
    return new AjaxDemoService().HelloWorld();
}

[AjaxPro.AjaxMethod]
public string CallEchoService(int value)
{

    return new AjaxDemoService().Echo(value);
}

Auf dem Client birgt der zusätzliche Code keine Überraschungen. Der Aufruf der serverseitigen Methoden erfolgt analog des ersten Beispiels. Die einzige Besonderheit ist der Aufruf der Methode CallEchoService, da diese einen Parameter erwartet.

<script type="text/javascript">
    function callEchoService() {
        var echoTextBox = document.getElementById('EchoTextBox');
        var value = echoTextBox.value;
        Teil7.CallEchoService(value, ajaxdemo_callback);
    }

    function ajaxdemo_callback(result) {
        var content = document.getElementById('content');
        content.innerHTML = result.value;
    }
</script>

Wie der Beispielcode zeigt, wird der erwartete Parameter einfach vor die Angabe der JavaScript Callback Funktion gepackt.

Fazit

AjaxPro ist eine kleine und leichtgewichtige Ajax Library, die ohne unnötigen Ballast daher kommt. Durch die Ähnlichkeit zu Pagemethods und Scriptservices fiel mir die Einarbeitung ziemlich leicht und nahm kaum mehr als eine halbe Stunde in Anspruch.

Trotz dieser Vorteile kann ich mir nicht vorstellen, AjaxPro als Standardwerkzeug in aktuellen Projekten zu nutzen. Stattdessen werde ich wohl weiterhin auf jQuery setzen. Der Grund für diese Entscheidung liegt ganz einfach darin, dass ich jQuery zu UI Manipulationszwecken meist sowieso schon auf meinen Seiten referenziert habe. Daher ist es naheliegend für AjaxRequests auch zu jQuery zu greifen.

Sehr gut vorstellen könnte ich mir jedoch, AjaxPro in ASP.NET 1.1 Projekten zu nutzen, die sich noch in Pflege befinden. In diesen fehlt die Möglichkeit, PageMethods und Scriptservices einzusetzen, so das AjaxPro dort das perfekte Werkzeug wäre.

An dieser Stelle auch noch einmal vielen Dank an René für den hilfreichen Tipp.


Kick it on dotnet-kicks.de
 
3/29/2010 - 10:12 PM | Comments [0] | Categories: .NET | Ajax | ASP.NET | DotNetGerman Bloggers
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Von Administratoren geliebt, von Entwicklern verflucht: Das Transportsystem von SAP. Prinzipiell kann man sich das so vorstellen, dass dieses System vorsieht, dass der Quellcode nur an einer Stelle, nämlich im SAP Entwicklungssystem erstellt und modifiziert wird.

Anschließend wird ein sogenannter Transport (so was ähnliches wie ein Setuppaket) erstellt. Dieses kann dann je Konfiguration erst mal nur in das Testsystem eingespielt werden. Dort wird das Paket auf seine Funktionstüchtigkeit getestet. Wurden diese Tests bestanden, wird es in das Integrationssystem "transportiert". Dort wird das Zusammenspiel mit anderen Anpassungen getestet, es wird also geprüft, ob das Paket nichts anderes kaputt macht ;-)

War auch dieser Test erfolgreich, wird der Transport aus dem Integrationssystem in das Produktionssystem transportiert. Ein direktes Einspielen von Transporten in das Produktionssystem beziehungsweise sogar ein direktes Ändern der Quellcodes ist nicht vorgesehen.

Auch wenn diese Vorgehensweise zunächst recht streng, bürokratisch und viel zu aufwändig wirkt, hat sie durchaus ihren Sinn. Dieser zugegeben etwas starre Prozess sorgt dafür, dass SAP Produktionssysteme normalerweise immer stabil und möglichst fehlerfrei sein sollten. Er trägt also Maßgeblich zu SAPs gutem Ruf bei!

Warum erzähle ich das eigentlich?

Ok, nette Einführung, aber ist das hier nicht eigentlich ein auf .NET fokussierter Blog? Ja :-) Also keine Sorge, ich bin nicht ins Lager der SAP Consultants gewechselt ;-) Allerdings habe ich von ungefähr 2005 bis 2008 die SAP Schnittstelle meines ehemaligen Arbeitgebers gemeinsam mit einem Kollegen weiterentwickelt. Da blieb der ein oder andere Blick unter die Haube diverser SAP Systeme nicht aus.

Nun aber zurück zum Thema:

In meiner beruflichen Laufbahn war ich an einigen (ASP).NET Projekte beteiligt, die von einem SAP transportähnlichen System sehr profitiert hätten. Überspitzt gesagt sah es dort nämlich häufig so aus, dass ein Entwickler nach einer Quellcodeänderung Visual Studio genutzt hat um einen neuen Releasestand zu erstellen und diesen dann schnell auf den produktiven Webserver gepackt hat. Selbstverständlich wurde das ganze vorher nicht in einem Test- oder Integrationssystem installiert und unter Umständen wusste der Kunde und/oder Projektleiter sogar nichts von dem adhoc Update.

Agil != Chaos !!!

Der Entwickler handelte unter dem Deckmantel der agilen Entwicklung nach dem Prinzip "nach mir die Sinnflut", einfach drauf damit, wird schon klappen. In diesem Zusammenhang finde ich es übrigens immer wieder äußerst verwunderlich, wie viele Entwickler unkoordiniertes und schlampiges Vorgehen einfach als agil bezeichnen, um ihr Chaos damit zu entschuldigen. Agil ist allerdings alles andere als chaotisch, unprofessionell, unkoordiniert oder schlampig. Aber gut, dass ist ein anderes Thema - ich schweife schon wieder aus ;-)

In einem meiner aktuellen Projekte wollte ich einer solchen Vorgehensweise direkt einen Riegel vorschieben. Nicht dass die Entwickler dieses Projekts chaotisch wären, das ist ganz und gar nicht der Fall, aber wir wollen ja auch niemanden in Versuchung führen ;-)

Ich hatte einen Traum ...

Meine Ziele lautete also wie folgt:

  1. Releasestände einer Software sollen automatisch auf einem dedizierten Buildrechner und nicht auf einem Entwickler-PC erstellt werden.
  2. Jedes Release hat eine eindeutige Versionsnummer. Kompilate auf dem Entwicklerrechner haben jedoch immer die Nummer 1.0.0.0 (dazu später mehr)
  3. Jedes Release soll ein Set an Unit Tests durchlaufen haben.
  4. Jedes Release soll mit einer bestimmten Qualität als Metadatum versehen werden.
  5. In Abhängigkeit von dieser Qualität und der vorherigen Qualität soll die Software bei bestimmten Statusübergängen automatisch auf bestimmte Systeme verteilt werden.
  6. Das Qualitätsmetadatum wird durch einen dedizierten Personenkreis gesetzt (zum Beispiel QM-Manager oder Projektmanager, Softwarearchitekt oder Chefentwickler)

Realisiert werden sollte das ganze mit Visual Studio 2008 und dem Team Foundation Server. Die Punkte 1 - 3 stellen in einer solchen Umgebung kein Problem dar.

Punkt Nr. 1 erledigen wir durch Teambuild. Hier läuft ein Buildagent auf einem dedizierten Rechner. Durchgeführt werden sowohl Continuous Integration Builds, als auch Nightly Builds.

Punkt Nr. 2 haben wir durch ein einfaches Build Target in unserem Buildscript erledigt. Die Versionsnummer haben wir wie folgt aufgebaut:

  • Major: Fixer Wert
  • Minor: Fixer Wert
  • Build: Aktueller Changeset des Teamprojekts im TFS
  • Revision: Fortlaufende Nummer

Diese Nummer wird nur während des Builds durch Teambuild erzeugt. Entwicklerbuilds haben immer die Version 1.0.0.0. So fange ich ab, dass ein Entwickler doch einfach mal auf seinem Rechner eine Version baut und versucht zu deployen. Natürlich ist dies nur ein weicher Schutz. Mit ein wenig Aufwand lässt sich das System natürlich leicht umgehen indem ein Entwickler eine korrekte Versionsnummer erzeugt. Allerdings ist der Aufwand schon wieder so hoch, dass sich dann doch lohnt, den vorgesehenen Prozess einzuhalten ;-)

Sollte jemand Interesse an diesem Teil des Buildscripts haben: Einfach einen Kommentar posten.

Punkt Nr. 3 ist natürlich auch kein Problem. Aufgrund der guten VS Integration haben wir uns für MS Test für dieses Projekt entschieden. Diese Tests als Bestandteil des Buildprozess laufen zu lassen war demnach kein Problem.

Punkt Nr. 4 ist auch bereits von Haus aus mit dem TFS möglich. Jeder Build der durch Teambuild durchgeführt wird hat ein Metadatum Buildqualität. Die möglichen Werte der Buildqualität lassen sich frei definieren. Auch lässt sich über entsprechende Rechte einschränken, wer diese Qualität setzen darf. Wir haben bei uns die Werte "Entwicklung", "Test", "Integration", "Produktion" und "Abgelehnt" definiert.

Punkt Nr. 5 kann mit TFS Bordmitteln erst mal nicht realisiert werden. Hier kommt nun das Open Source Projekt TFS Deployer ins Spiel. TFS Deployer installiert man als Windowsdienst. Nach entsprechender Konfiguration lauscht er nun auf eine Änderung der Buildqualität. Dazu muss der Benutzer unter dem der Dienst läuft allerdings in der TFS Gruppe Valid Users sein und für das entsprechende Temaprojekt zumindest Leserechte haben. Bemerkt TFS Deployer nun einen Statusübergang, für den er ein Deploymentscript hat, führ er dieses automatisch aus.

Angelehnt an die Einführung von Dave Comfort und Scott Colestock kann man sich das ganze nun wie folgt vorstellen:

tfsdeployer

Konkret sieht dies so aus, dass innerhalb der TFS Quellcodeverwaltung unterhalb des Verzeichnisses der Builddefinition ein Verzeichnis Deployment angelegt wird, also zum Beispiel $/TeamProjekt/TeamBuildTypes/MeinProjektNightlyBuild/Deployment. In diesem Verzeichnis wird nun die Datei DeploymentMappings.xml angelegt. In dieser Datei wird für jeden gewünschten Statusübergang festgelegt, welches Powershellscript auf welchem Computer ablaufen soll. Die Datei DeploymentMappings kann zum Beispiel wie folgt aussehen:

<DeploymentMappings xmlns="http://www.readify.net/TFSDeployer/DeploymentMappings20061026">
  <Mapping xmlns=""
           Computer="tfsbuildserver"
           OriginalQuality="*"
           NewQuality="Entwicklung"
           Script="EntwicklerDrop.ps1"
           RunnerType="PowerShell"
           NotificationAddress="someone@mycompany.com;someoneelse@mycompany.com" />
  <Mapping xmlns=""
           Computer="tfsbuildserver"
           OriginalQuality="Entwicklung"
           NewQuality="Test"
           Script="EntwicklungNachTest.ps1"
           RunnerType="PowerShell"
           NotificationAddress="someone@mycompany.com;someoneelse@mycompany.com" />
  <Mapping xmlns=""
           Computer="tfsbuildserver"
           OriginalQuality="Test"
           NewQuality="Integration"
           Script="TestNachIntegration.ps1"
           RunnerType="PowerShell"
           NotificationAddress="someone@mycompany.com;someoneelse@mycompany.com" />
  <Mapping xmlns=""
           Computer="tfsbuildserver"
           OriginalQuality="Integration"
           NewQuality="Produktion"
           Script="TestNachIntegration.ps1"
           RunnerType="PowerShell"
           NotificationAddress="someone@mycompany.com;someoneelse@mycompany.com" />
</DeploymentMappings>

In diesem Script werden vier Statusübergänge definiert. Erst vom leeren Zustand nach Entwicklung, dann weiter nach Test, Integration und Produktion. Jeder dieser Status entspricht einem Build-Quality Eintrag im TFS. Diese sind wie angemerkt frei definierbar. Über die Eigenschaft Script wird nun definiert, welches Powershellscript ausgeführt werden soll. Diese Powershellscripte müssen übrigens im selben Ordner liegen, wie die Datei DeploymentMappings.xml.

Tritt nun einer der definierten Statusübergänge auf, ruft TFS Deployer das gewünschte Script im Kontext des Service Users auf und übergibt ein BuildData Objekt. Die verschiedenen des Eigenschaften BulidData Objekts können dann über die Variable $TfsDeployerBuildData zugegriffen werden.

In unserer Umgebung nutzen wir übrigens eine modifzierte Variante des Beispiels der Seite Team Foundation Server Build Recipes.

function publish-site( [string] $sourcepath, [string] $destinationpath, [bool] $renameconfig, [bool] $deleteexisting) {
	$droplocation = $TfsDeployerBuildData.DropLocation
	
	$websourcepath = $droplocation + $sourcepath
	$webdestinationpath = $destinationpath
	
	new-item -force -path $webdestinationpath -itemtype "directory"
	if ($deleteexisting) {
		get-childitem $webdestinationpath | remove-item -force -recurse
	}
	get-childitem $websourcepath | copy-item -force -recurse -destination $webdestinationpath

	if ($renameconfig) {
		$configFile = $webdestinationpath + "web.production.config"
		remove-item $configFile -force
		$configFile = $webdestinationpath + "web.development.config"
		remove-item $configFile -force
		$configFile = $webdestinationpath + integration.config"
		remove-item $configFile -force


		$configFile = $webdestinationpath + "web.test.config"
		$configFileDest = $webdestinationpath + "web.config"
		move-item $configFile $configFileDest -force
	}
}

publish-site "\Release\_PublishedWebsites\MyWebApplication\" "\\MeinWebServer\MeineFreigabe\" 1 1

Dies funktioniert in unserer Umgebung recht gut, da wir vom Build Server direkten Zugriff auf die verschiedenen Web Server (Entwicklung, Test, Integration, Produktion) haben. Für jede Webapplikation besteht auf den Servern eine Windows Freigabe, die direkt auf das Wurzelverzeichnis der Applikation zeigt. Zugriffsrechte hat hier nun der Benutzer, unter der TFS Deployer Dienst läuft. So stellen wir sicher, dass keiner der Entwickler "mal schnell" etwas patcht ;-) Noch sicherer wäre es natürlich gewesen, den TFS Deployer Dienst direkt auf dem Web Server laufen zu lassen, und ihm lesenden Zugriff auf die erstellen Binaries des Buildprozesses zu geben.

Als Erweiterung zum Standardscript wärmen wir nach dem Deployment Vorgang die Applikation noch auf, indem wir einige Seiten ansurfen. Somit ist sichergestellt, dass der Workerprozess beim ersten Zugriff durch einen Benutzer schon da ist und die entsprechenden Seiten auch schon kompiliert wurden.

Den Übergang von "Nichts" zu "Entwicklung" machen wir übrigens automatisch als letzten Schritt unseres Buildscripts. Somit wird der Nightly Build Automatisch in die Entwicklungsumgebung gejagt.

Und was hat man nun davon?

Mit dem TFS Deployer haben Nutzer des TFS eine einfache Möglichkeit, benannte Softwarestände automatisch in verschiedene Umgebungen zu verteilen. Die dadurch entstehende Nachvollziehbarkeit lässt aus meiner Erfahrung Kunden und somit auch die Projektverantwortlichen im internen Team besser schlafen.

Ein wenig schade ist, dass man innerhalb des TFS zwar verschiedene Buildqualitäten, jedoch keine Regeln für gültige Übergänge anlegen kann. Da unsere Scripte jedoch auf festen Übergängen definieren bedeutet dies, dass derjenige, der die Buildqualität setzt, genau wissen muss welche Übergänge gültig sind. Schaltet er nämlich zum Beispiel von Test direkt auf Produktion passiert nämlich nichts.

Angeregt zu diesem Blog Post hat mich übrigens Robert Mühsigs Posts Build / Deployment / WTF.


Kick it on dotnet-kicks.de
 
3/25/2010 - 07:11 AM | Comments [7] | Categories: .NET | ASP.NET | DotNetGerman Bloggers | TFS
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Cool, mein Blogeintrag zum Thema Viewstate hat es auf die offizielle Microsoft ASP.NET Seite geschaft. Nun ja, zumindest auf die deutsche Variante davon :-)


Kick it on dotnet-kicks.de
 
3/23/2010 - 03:20 PM | Comments [2] | Categories: .NET | ASP.NET | DotNetGerman Bloggers
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

In den letzten 5 Einträgen meines Blogs habe ich über verschiedene Möglichkeiten geschrieben Ajax in einer ASP.NET Webforms zu implementieren.

Angefangen mit dem manuellen Weg über das XmlHttpRequest Objekt ging es weiter zu Client Callbacks, dem Updatepanel, dem ASP.NET Ajax Framework sowie jQuery.

Der Fokus meiner Beiträge lag darauf nicht nur einfach zu zeigen welche Möglichkeiten es gibt, sondern zusätzlich auch zu zeigen, welche Datenmengen über die Leitung gehen und ob bzw. welche Teile des ASP.NET Page Life Cycles durchlaufen werden.

Ich hoffe dass ich bei dem ein oder anderen Leser für manchen Aha Effekt sorgen konnte. Zumindest ging es mir persönlich bei der ersten detaillierten Auseinandersetzung mit dem Thema so. Schließlich ist man als ASP.NET Webforms Entwickler traditionell doch eher auf dem Server zu Hause und realisiert vorerst garnicht welchen Overhead Client Callbacks oder das Updatepanel mit sich bringen.

Mein persönliches Fazit ist, dass Updatepanel und Client Callbacks korrekt eingesetzt in einigen Fällen vielleicht berechtigte Alternativen sind, in den meisten Fällen jedoch zum Ajax Framework oder jQuery gegriffen werden sollte.

Meine zurzeit favorisierte Lösung ist - ähnlich wie bei vielen anderen sicherlich auch - der Einsatz von jQuery.

Da mein letztes Beispiel noch ein wenig aufgebläht war, möchte ich an dieser Stelle zwei kurze Tipps geben, die die Arbeit mit jQuery und Ajax ein wenig erleichtern.

Der erste Punkt beschäftigt sich mit der großen Redundanz zwischen den verschiedenen Ajax Aufrufen:

$("#StaticFileLink").click(function(e) {
    e.preventDefault();
    $.ajax({
        type: "POST",
        url: "Teil5.aspx/ReadStaticFile",
        data: "{}",
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        success: function(msg) {
            $("#content").html(msg.d);
        }
    });
});

$("#HelloWorldLink").click(function(e) {
    e.preventDefault();
    $.ajax({
        type: "POST",
        url: "AjaxDemoService.asmx/HelloWorld",
        data: "{}",
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        success: function(msg) {
            $("#content").html(msg.d);
        }
    });
});

$("#EchoLink").click(function(e) {
    e.preventDefault();
    var number = $("#EchoTextBox").val();
    var jsonData = "{ 'number' : '" + number + "'}";
    $.ajax({
        type: "POST",
        url: "AjaxDemoService.asmx/Echo",
        data: jsonData,
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        success: function(msg) {
            $("#content").html(msg.d);
        }
    });
});

Bereits beim ersten Blick auf den Quellcode fällt auf, dass die Werte einiger Parameter statisch zu sein scheinen. So haben folgende Parameter stets einen fixen Wert:

  • type
  • contentType
  • dataType

Außerdem ist zumindest in den ersten beiden Aufrufen auch der Eintrag für data gleich.

Ein Weg um diese Redundanz herum zu kommen wäre eine eigene Funktion, die nur die Variablen Parameter entgegen nimmt.

Standardwerte setzen

Eine andere Alternative besteht darin, Standardwerte für jQuery Ajax Aufrufe zu setzen. Dies sähe dann so aus:

<script type="text/javascript">
    $(document).ready(function() {
        $.ajaxSetup({
            type: "POST",
            data: "{}",
            contentType: "application/json; charset=utf-8",
            dataType: "json"
        });
        $("#StaticFileLink").click(function(e) {
            e.preventDefault();
            $.ajax({
                url: "Teil6.aspx/ReadStaticFile",
                success: function(msg) {
                    $("#content").html(msg.d);
                }
            });
        });

        $("#HelloWorldLink").click(function(e) {
            e.preventDefault();
            $.ajax({
                url: "AjaxDemoService.asmx/HelloWorld",
                success: function(msg) {
                    $("#content").html(msg.d);
                }
            });
        });

        $("#EchoLink").click(function(e) {
            e.preventDefault();
            var number = $("#EchoTextBox").val();
            var jsonData = "{ 'number' : '" + number + "'}";
            $.ajax({
                url: "AjaxDemoService.asmx/Echo",
                data: jsonData,
                success: function(msg) {
                    $("#content").html(msg.d);
                }
            });
        });
    });
</script>

Neu hinzugekommen sind die Zeilen 2 - 8. Diese setzen Standardwerte für alle folgenden Ajax Aufrufe. Diese Variante kann einiges an Code sparen, ist allerdings mit Vorsicht zu genießen. Sollte nämlich zum Beispiel ein jQuery Plug-In auf der Seite genutzt werden, dass auch die $.ajax Funktion nutzt, könnte es zu Seiteneffekten kommen. Konkret wäre dies der Fall, wenn einer der per .ajaxSetup gesetzten Parameter nicht überschrieben, aber mit einem anderen Wert erwartet wäre. Typischerweise würde dies für den contentType oder oder dataType geschehen.

Serialisieren - einfach gemacht

Eine weitere Unschönheit des gezeigten Quellcodes besteht darin, dass die String Variante des in JSON notierten Objekts data von Hand zusammen gebaut wurde. Dies ist natürlich nicht sonderlich schick. Abhilfe schafft die Funktion stringify des Objekts JSON. Einige Browser wie Firefox ab Version 3.5 oder IE ab der Version 8 haben bereits ein eingebautes Objekt JSON. Für alle anderen gibt es unter http://www.json.org/js.html eine JavaScript Library zum Download, die entsprechenden Support nachrüstet, falls noch nicht vorhanden.

Konkret sähe dies dann wie folgt aus:

<script src="scripts/json2.js" type="text/javascript"></script>

[ ... ]


$("#EchoLink").click(function(e) {
    e.preventDefault();
    var number = $("#EchoTextBox").val();
    var jsonData = { 'number': number };
    var jsonString = JSON.stringify(jsonData);
    $.ajax({
        url: "AjaxDemoService.asmx/Echo",
        data: jsonString,
        success: function(msg) {
            $("#content").html(msg.d);
        }
    });

Wie man sieht, wird nun in Zeile 9 zuerst ein JavaScript Objekt jsonData in JSON Notation erzeugt. Dies wird anschließend über JSON.stringify in Zeile 10 in einen String konvertiert. Bei diesem konkreten Beispiel mag der Vorteil noch nicht auf der Hand liegen, spätestens bei komplexen Objekten lernt man die Funktion stringify jedoch schnell zu schätzen.

Bei der Recherche zu den Beiträgen dieser Serie bin ich übrigens auf einen sehr gut geschriebenen Eintrag von Roberto Bez gestolpert. Roberto stellt in seinem Blog Post die verschiedenen Varianten kompakt gegenüber. Die Lektüre des Artikels kann ich jedem Ajax interessierten Webforms Entwickler wärmstens empfehlen.

Weiter hat René Drescher-Hackel in einem Kommentar zu meinem letzen Beitrag, dass mit Ajax.NET Professional (AJAX.PRO) eine weitere effiziente Alternative zur Verfügung steht. Ich muss gestehen, dass mir die Existenz dieses Frameworks bis jetzt vollkommen entgangen ist. Gelobe allerdings Besserung und werde es mir gerne ansehen und mein Beispiel damit umsetzen.


Kick it on dotnet-kicks.de
 
3/11/2010 - 09:44 PM | Comments [0] | Categories: .NET | Ajax | ASP.NET | DotNetGerman Bloggers
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Eigentlich altbekannt, trotzdem bin ich gerade mal wieder darauf hereingefallen und schreibe des deshalb hier auf:

Ich wollte in einer ASP.NET Webforms Anwendung ein Servercontrol serverseitig über

meinControl.Visible = false;

ausblenden, um es nach einer Nutzeraktion auf dem Client wieder einzublenden:

$(Id$="MeinControl").show();

Leider funktioniert das allerdings nicht, da ein serverseitiges Visible = false dafür sorgte, dass das Control erst garnicht gerendert wurde und somit auf dem Client nicht verfügbar war.

Die simple Lösung im Codebehind:

meinControl.Style.Add(HtmlTextWriterStyle.Display, "none");

Dann klappts auch mit dem Einblenden ;-)

War dieser Beitrag hilfreich? Dann kick ihn doch bitte.


Kick it on dotnet-kicks.de
 
3/11/2010 - 07:41 PM | Comments [2] | Categories: .NET | ASP.NET | DotNetGerman Bloggers
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Im vierten Teil dieser Serie habe ich gezeigt, wie Pagemethods und Scriptservices mit dem ASP.NET Ajax Framework angesprochen werden können. Im Vergleich zu den vorherigen Teilen, die auf Client Callbacks bzw. das Updatepanel setzen, konnte über diesen Weg die übertragene Datenmenge erheblich verkleinert werden, da unter anderem der Viewstate nicht mehr übertragen werden musste. Außerdem wurde serverseitig nicht mehr der komplette Page Life Cycle durchlaufen, was weitere Performanceverbesserungen mit sich brachte.

Im Gegenzug zu diesen Verbesserungen mussten wir allerdings in Kauf nehmen, dass beim ersten Request drei zusätzliche JavaScript Dateien des Ajax Frameworks mit einer Gesamtgröße von 85 kb geladen wurden.

Hält man im Hinterkopf, dass der Viewstate einer Seite schnell 50 kb und mehr beträgt und dieser bei jedem Ajax Request der vorherigen Methoden hin und her übertragen wurde, lassen sich diese 85 kb jedoch sicherlich leicht verschmerzen.

Doch wie sieht es aus, wenn neben dem Ajax Framework auch jQuery in die Seite eingebunden wurde, um die Oberfläche zu tunen?

In diesem Fall stellt sich die Situation anders dar, nun ist das ASP.NET Ajax Framework clientseitig nämlich reiner Ballast. Zumindest wenn es darum geht Pagemethods und Scriptservcies aufzurufen. Das kann jQuery nämlich auch.

Also weg damit!

Wie müssen wir aber vorgehen, um Pagemethods und Scriptservices aus jQuery rufen zu können?

Zunächst kopieren wir den kompletten Code unseres letzten Beispiels. Wenn wir nun einen Blick auf den Code werfen sehen wir in der *.aspx Datei einen ScriptManager.

<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="True">
  <Services>
    <asp:ServiceReference Path="~/AjaxDemoService.asmx" />
  </Services>
</asp:ScriptManager>

Dieser war notwendig, damit das ASP.NET Ajax Framework die JavaScript Proxies erstellte, die uns Aufrufe in der Form PageMethods.Methodenname bzw. WebServiceName.Methodenname erlaubten.

Der ScriptManager ist allerdings überflüssig wenn man Pagemethods und Scriptservices via jQuery rufen möchte. Daher fliegt er raus. Weiter entfernen wir den bestehenden inline JavaScript Code der Seite und räumen Sie noch ein wenig auf. Das Ergebnis sieht wie folgt aus:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Teil5.aspx.cs" Inherits="Teil5" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head runat="server">
        <title>ASP.NET Webforms Anwendungen und Ajax (Teil 5): Scriptservices mit jQuery</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <h1>
                ASP.NET Webforms Anwendungen und Ajax (Teil 5): Scriptservices mit jQuery</h1>
            <p>
                <a href="#" id="StaticFileLink">Hier klicken zum Request einer statischen Datei</a><br />
                <a href="#" id="HelloWorldLink">Hier für Hello World WebService klicken </a><br />
                <a href="#" id="EchoLink">Hier für Echo WebService klicken. Geben Sie bitte vorher eine
                    Zahl in nebenstehendem Feld ein: </a>&nbsp;
                <input type="text" id="EchoTextBox" value="4711" />
            </p>
            <div id="content">
                Bitte klicken Sie auf einen der Links, damit dieser Bereich gefüllt wird.
            </div>
        </form>
    </body>
</html>

Der serverseitige Code des vorherigen Beispiels wurde übrigens nicht verändert.

Unsere Seite umfasst nun schlanke 1,6 kb, wie die folgende Abbildung zeigt:

01_Seite

Allerdings kann sie auch noch nichts ;-)

Darum werden wir uns aber in den nächsten Schritten kümmern.

jQuery let's go

Als erstes benötigen wir eine Referenz auf jQuery. Diese erhalten wir über folgenden Script Tag, den wir innerhalb des Head Bereichs der Seite einfügen:

<script src="scripts/jquery-1.3.2.min.js" type="text/javascript"></script> 

Als nächstes fügen wir den Code ein, um den Link zur Anzeige der statischen Datei zum fliegen zu bekommen. Dieser sieht wie folgt aus:

<script type ="text/javascript">
    $(document).ready(function() {
        $("#StaticFileLink").click(function(e) {
            e.preventDefault();
            $.ajax({
                type: "POST",
                url: "Teil5.aspx/ReadStaticFile",
                data: "{}",
                contentType: "application/json; charset=utf-8",
                dataType: "json",
                success: function(result) {
                    $("#content").html(result.d);
                }
            });
        });
	 });
</script>

Relevant für uns sind die Zeilen 5 - 14. Da es jedoch vielleicht den ein oder anderen Leser gibt, der noch nie mit jQuery gearbeitet hat, möchte ich auch zu den anderen Zeilen ein paar Worte verlieren.

Auffällig sind zunächst die vielen $-Zeichen innerhalb des Codes. Dabei handelt es sich um einen Alias für die Funktion jQuery. Überall wo ein $ steht, könnte man sich also auch jQuery denken.

Die Funktion jQuery nimmt ein DOM Element bzw. einen CSS Selektor als Argument entgegen. In Zeile 2 wird das DOM Element document, also eine Referenz auf unser eigentliches HTML Dokument übergeben. Anschließend wird an das Ereignis ready eine anonyme Funktion gehangen. Das Ereignis ready tritt übrigens auf, sobald das DOM vollständig initialisiert wurde, jedoch ehe weitere Inhalte wie Beispielsweise Bilder herunter geladen wurden. Somit hat man innerhalb des ready Ereignisses zwar Zugriff auf das vollständig initialisierte DOM, muss aber nicht den kompletten Seitenaufbau abwarten.

In Zeile 3 wird die jQuery Funktion mit dem CSS Selektor #StaticFileLink gefüllt, um das DOM Element mit der ID StaticFileLink, also unseren Link, zu selektieren. An das Ereignis click des Links wird wieder eine anonyme Funktion gehangen.

In Zeile 4 wird über e.preventDefault(); die Standardaktion des Links verhindert. In unserem Fall also, dass beim Klick auf dem Link dem Inhalt des href Attributs gefolgt wird. Stattdessen wird ab der Zeile 5 definiert, dass ein ajax Aufruf gefeuert werden muss.

Dabei werden folgende Werte an die verschiedenen Parameter der Funktion ajax übergeben:

Parameter Wert Erklärung
type POST (fix) Ajax Anfragen an Pagemethods oder Scriptsservices müssen aus Sicherheitsgründen immer vom Typ POST sein, um JSON hijacking Angriffe abzuwehren. Eine sehr gute Beschreibung hierzu gibt es im Blog von Phil Haack.
url {Seitenname.aspx}/{Methodenname} Ziel des Ajax Aufrufs. Im Falle einer Page Method nach dem Schema {Seitenname.aspx}/{Methodenname}, im Fall von ScriptServices {Servicename.asmx}/{Methodenname}. Wichtig ist, dass die entsprechende Methode im Falle von Pagemethods statisch ist und mit dem Attribut WebMethod dekoriert wurde. Im Falle eines ScriptServices ist auf die Dekorierung des Services mit dem Attribut ScriptService zu achten.
data JSON String Erwartet die Methode Parameter, sind diese hier als JSON String, und nicht als JSON Objekt zu übergeben! Falls die Methode keine Parameter erwartet, sollte stets ein leeres JSON Objekt übergeben werden (also "{}"). Andernfalls übergibt jQuery als contentType statt application/json automatisch text/html.
contentType application/json; charset=utf-8 (fix) Als Content Type ist stets application/json; charset=utf-8 zu übergeben. Dies ist das Zeichen für das ASP.NET Ajax Framework, serverseitig die Anfrage an den ScriptService Handler und nicht an den Soap Webservice Handler weiterzuleiten.
dataType json (fix). Gibt den Datentyp der Antwort an. Gültige Antworten sind unter anderem json, script, xml oder text. Im Fall von Pagemethods und ScriptServices kommt immer JSON zurück.
succes Anonyme Funktion bzw. Funktionsname Eine Anonyme Funktion bzw. der Name einer Funktion, die die Rückgabe der Methode verarbeitet.
Im Falle von PageMethods und ScriptServices wird die Antwort aus Sicherheitsgründen übrigens immer in ein Objekt Namens d verpackt (siehe auch Zeile 12 des Scripts). Die Ursache hierfür ist ähnlich wie beim Parameter type und wird auch näher in Phil Haacks Blog Eintrag erklärt.

Der Aufruf des Services sollte somit ausreichend erklärt sein. Einzig Zeile 12 bedarf noch einer kleinen Erklärung:

Via $("#Content") holt jQuery sich eine Referenz auf den Zielbereich in mithilfe der Funktion html die Antwort des Ajax Requests geschrieben wird.

Der Code zum Aufruf der beiden Webservice Methoden sieht erwartungsgemäß ähnlich aus:

            $("#HelloWorldLink").click(function(e) {
                e.preventDefault();
                $.ajax({
                    type: "POST",
                    url: "AjaxDemoService.asmx/HelloWorld",
                    data: "{}",
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    success: function(msg) {
                        $("#content").html(msg.d);
                    }
                });
            });

            $("#EchoLink").click(function(e) {
                e.preventDefault();
                var number = $("#EchoTextBox").val();
                var jsonData = "{ 'number' : '" + number + "'}";
                $.ajax({
                    type: "POST",
                    url: "AjaxDemoService.asmx/Echo",
                    data: jsonData,
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    success: function(msg) {
                        $("#content").html(msg.d);
                    }
                });
            });

Erklärenswert ist einzig Zeile 18. Hier wird ein Json String zusammen gesetzt, der später an die entsprechende Methode des Webservices übergeben wird. Dies geht im Falle eines Arguments noch ganz gut, wird aber spätestens bei zwei oder mehr Argumenten sehr lästig. Abhilfe schafft hier die Methode stringify des Objekts JSON. Die Methode überführt ein Json Objekt in einen entsprechenden String. Moderne Browser bringen dieses Objekt inkl. passender Methode gleich mit. In allen anderen Fällen hilft der JSON Parser / Stringifier Json2.

Das Ergebnis der Mühe sieht im Live-Betrieb dann übrigens wie folgt aus:

02_AjaxRequest

Zum Download der jQuery Library ist, wie auf der Abbildung gezeigt, ein weiterer Request notwendig, der knapp 58 kb umfasst. Dies sind fast 30 kb weniger als es im vorherigen Beispiel auf Basis des ASP.NET AJAX (client) Frameworks der Fall waren.

Fazit

Ajax Aufrufe werden mithilfe des jQuery Frameworks zum Kinderspiel. Gerade wenn man jQuery bereits für Oberflächenmanipulationen in einer Seite nutzt, lohnt sich die Anwendung von jQuery für Ajax Aufrufe anstatt der Nutzung des ASP.NET Ajax (Client) Frameworks.


Kick it on dotnet-kicks.de
 
3/1/2010 - 11:33 PM | Comments [2] | Categories: .NET | Ajax | ASP.NET | DotNetGerman Bloggers
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Nachdem ich in Teil 1 der Serie den manuellen Ansatz zur AJAX Implementierung gezeigt habe und in den Teilen 2 und 3 auf (halb-)automatische Alternativen einging, möchte ich dieses Mal einen anderen Weg zeigen, ASP.NET Scriptservices in Verbindung mit ASP.NET AJAX.

Rekapitulieren wir noch einmal:

Teil 1 der Serie zeigte einen manuellen Ansatz, der hauptsächlich damit kämpfte, dass man innerhalb des JavaScript Codes selbst darauf achten musste, die verschiedenen Besonderheiten der unterschiedlichen Browser zu berücksichtigen. Dies geschah in den Teilen 2 und 3 zwar automatisch hinter den Kulissen, dafür hatten wir bei diesen Alternativen jedoch das Problem, dass bei jedem AJAX Request sowohl der komplette ViewState übertragen wurde und vor allem auf dem Server auch der komplette Page Life Cycle durchlebt wurde.

Leichtgewichtig durch die Ajax Welt

Heute möchte ich deshalb einen Ansatz zeigen, der die Vorteile der bisher vorgestellten Ansätze vereint, jedoch nicht über die selben Nachteile verfügt.

Konkret bedeutet dies, dass die Implementierung auf Clientseite browserübergreifend stattfinden wird, zwischen Client und Server nur die wirklich notwendigen Daten übertragen werden und der Page Life Cycle auf der Serverseite nicht durchlaufen wird.

Page Methods

Der erste Schritt Richtung einer leichtgewichtigen Implementierung führt über die sogenannten Page Methods. Page Methods sind statische Methoden einer *.aspx Seite, die mit dem [WebMethod] Attribut annotiert werden. Für den Client erscheinen Page Methods wie normale Methoden eines Web Services.

Ausgehend vom Beispielcode der letzten drei Teile der Serie passe ich den Servercode für die Methode, die den Inhalt einer Datei zurückliefert also wie folgt an:

using System.IO;
using System.Web.UI;
using System.Web.Services;

public partial class Teil4 : Page
{
	[WebMethod]
    public static string ReadStaticFile()
    {

	     string fileContent;
         using (var reader =
             new StreamReader(
                 System.Web.HttpContext.Current.Server.MapPath("~/static.html")))
         {

             fileContent = reader.ReadToEnd();
         }
         return fileContent;
     }
}

Als einzige Neuerung im Vergleich zur vorherigen Version fällt das zuvor beschriebene Attribut [WebMethod] auf.

Um die Methode ReadStaticFile nun clientseitig über das ASP.NET Ajax Framwork nutzen zu können, müssen wir innerhalb der *.aspx Seite bei unserem ScriptManager noch die Eigenschaft EnablePageMethods mit dem Wert true versehen.

<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="True"/>

Als Ergebnis rendert der Server bei einem Aufruf der Seite nun folgenden Java Script Code inline in die Seite hinein:

 <script type="text/javascript">
         //<![CDATA[
         var PageMethods = function() {

            PageMethods.initializeBase(this);
            this._timeout = 0;
            this._userContext = null;
            this._succeeded = null;
            this._failed = null;
         }
         PageMethods.prototype = {
             _get_path: function() {
                 var p = this.get_path();
                 if (p) return p;
                 else return PageMethods._staticInstance.get_path();
             },
             ReadStaticFile: function(succeededCallback, failedCallback, userContext) {
                 /// <param name="succeededCallback" type="Function" optional="true" mayBeNull="true"></param>
                 /// <param name="failedCallback" type="Function" optional="true" mayBeNull="true"></param>
                 /// <param name="userContext" optional="true" mayBeNull="true"></param>
                 return this._invoke(this._get_path(), 'ReadStaticFile', false, {}, succeededCallback, failedCallback, userContext);
             } 
         }
         // und vieles mehr ;-)

Wie man sieht, wird ein JavaScript Objekt PageMethods angelegt, welches über eine Funktion ReadStaticFile verfügt. Die Funktion hat also genau denselben Namen wie unsere serverseitige Funktion. Weiter nimmt Sie drei optionale Parameter entgegen:

  • succeededCallBack: Funktion, die aufgerufen wird, wenn die Ajax Anfrage erfolgreich beendet wurde
  • failedCallBack: Funktion, die aufgerufen wird, wenn die Ajax Anfrage fehlerhaft beendet wurde
  • userContext: Der Inhalt dieser Variable wird an die Funktionen succeededCallBack bzw. faildCallBack durchgereicht.

Der clientseitige Aufruf der Page Method gestaltet sich nun relativ einfach. Als erstes fügen wir der *.aspx Seite einen neuen ClientScriptBlock hinzu:

<script type="text/javascript">       
  function getStaticFile() {
    PageMethods.ReadStaticFile(onSuccess, onError, "MeinKontext");
  }

  function onSuccess(result, userContext, methodName) {
    var contentDiv = $get('content');
    if (userContext) {
      result += "<p>Der UserContext war: <strong>" + userContext + "</strong></p>";
    }

    result += "<p>Der Methoden Name war: <strong>" + methodName + "</strong></p>";
    contentDiv.innerHTML = result;
  } 

  function onError(result) {
    var contentDiv = $get('content');
    contentDiv.innerHTML = result;
  }

</script>

Einstiegspunkt für unseren späteren Code (Click Ereignis unseres Links) ist die Funktion getStaticFile. Sie ruft die Methode ReadStaticFile des Objekts PageMethods. Als Argumente übergeben wir zum einen die Funktionen für die erfolgreiche bzw. fehlerhafte Behandlung der Ajax Anfrage sowie als drittes die Zeichenfolge "MeinKontext".

Die Funktion onSuccess ist nun die Funktion, die die (erfolgreiche) Antwort des Servers verarbeitet. Sie nimmt neben der eigentlichen Antwort des Servers, auch den zuvor übergebenen userContext entgegen. Außerdem wird der Name der aufgerufenen Servermethode im Parameter methodName übergeben. Da JavaScript relativ flexibel ist, was die Anzahl der Parameter angeht und Funktionen einzig durch ihren Namen und nicht anhand ihrer Signatur identifiziert, können die beiden letzten Parameter übrigens auch problemlos weg gelassen werden.

Innerhalb der Methode onSuccess holen wir uns via $get zunächst eine Referenz auf das Div Element in das wir später unser Ergebnis schreiben möchten. $get ist übrigens eine Funktion des ASP.NET Ajax Client Frameworks. Sie ist (vereinfacht gesagt) die Kurzschreibweise für die JavaScript Funktion document.getElementById.

Weiter werden noch userContext (falls vorhanden) und der aufgerufene Methodenname an das Ergebnis angehangen. Im letzten Schritt wird das komplette Ergebnis schließlich in unser Container Div geschrieben.

Der Aufruf  der Funktion getStaticFile geschieht schließlich im Event onclick eines HTML Links (a Tag).

   17     <p>

   18         <a href="#" onclick="getStaticFile()">Hier klicken zum Request einer statischen Datei

   19         </a>

  // ....

   27     </p>

   28     <div id="content">

   29         Bitte klicken Sie auf einen der Links, damit dieser Bereich gefüllt wird.

   30      </div>

Schweres Leichtgewicht

Ruft man die Seite nun im Browser auf, fällt auf, dass neben der eigentlichen Seite auch drei JavaScript Dateien des Microsoft ASP.NET Ajax Client Frameworks geladen werden.

Erster Request der Seite

Die erste Anfrage umfasst nun also 93 kb, von denen sich 85 kb auf die MS AJAX Script Dateien verteilen.

Seinen Vorteil spielt die Herangehensweise jedoch bei den Ajax Anfragen aus. Wie man im folgenden Screenshot sieht, gehen hier gerade einmal 127 Byte über die Leitung. Weiter erkannt man deutlich, dass während des Posts kein ViewState mehr zum Server übertragen wird.

02_ajaxpost

Auch die Antwort ist relativ schmal und beschränkt sich auf das nötigste:

03_ajaxantwort

Die Antwort ist übrigens im JSON (Java Script Object Notation) Format serialisiert. Hätte unser Post Daten in Form von Argumenten für unsere Page Method enthalten, wären diese auch automatisch im JSON Format serialisiert worden. Eine etwas besser lesbare Variante der JSON formatierten Antwort enthalt der Tab JSON des Firebugs:

04_ajaxjson

In unserem Beispiel ist die Antwort zwar noch recht übersichtlich, so dass wir den Tab JSON eigentlich nicht benötigen würden, spätestens bei der Rückgabe komplexer Objektstrukturen lernt an den Tab jedoch schnell zu schätzen ;-)

Es geht noch besser!

Obwohl die bisherige Implementierung bereits ganz gut ist, gibt es noch zwei Kritikpunkte. Zum einen erscheint die Plazierung einer statischen Methode innerhalb der *.aspx Seite ein wenig eigenartig. Da sie weder auf Instanz Exemplar-Felder, noch auf Exemplar-Methoden der Seite zugreifen kann, ist sie innerhalb der Seite eigentlich fehl am Platz.

Außerdem stört mich der automatisch generierte JavaScript Script Block innerhalb der Seite, der die Proxies für die PageMethod Aufrufe zur Verfügung stellt. Lieber wäre mir die Variante einer eingelinkten externen JavaScript Datei.

Beide Punkte können wir glücklicherweise recht einfach lösen, nämlich durch den Einsatz von Script Services. Script Services sind normale Web Services, die mit dem ScriptService Attribut annotiert werden. Erstellt man einen Web Service mit Visual Studio 2008, ist dieses Attribut sogar bereits über der Klasse enthalten. Allerdings wird es automatisch auskommentiert.

Um einen Web Service nun also als Script Service zur Verfügung zu stellen, genügt es den Kommentar, wie in Zeile 13 des folgenden Listings gezeigt, zu entfernen.

   11 [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]

   12 // To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line.

   13 [System.Web.Script.Services.ScriptService]

   14 public class AjaxDemoService : WebService {

   15 

   16     [WebMethod]

   17     public string HelloWorld() {

   18         return "Hello World";

   19     }

   20 

   21     [WebMethod]

   22     public string Echo(int number)

   23     {

   24         return string.Format("Sie haben {0} eingegeben.", number);

   25     }  

   26 }

Prinzipiell reicht dies aus, um den Service via Ajax anzusprechen. Um uns das Leben jedoch ein wenig einfacher zu machen, lassen wir das ASP.NET Ajax Framework für uns - analog zur Page Method Lösung - JavaScript Proxies für unsere Methoden Aufrufe erstellen. Dazu fügen wir innerhalb des ScriptManager Tags der Seite ein Kind Element <Services> ein. Diesem Element fügen wir als weiteres Kind Element eine ServiceReference ein, deren Pfad auf unseren Service verweißt.

   10     <asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="True">

   11         <Services>

   12             <asp:ServiceReference Path="~/AjaxDemoService.asmx" />

   13         </Services>

   14     </asp:ScriptManager>

An dieser Stelle sei noch einmal darauf hingewiesen, dass dieser Schritt reine Bequemlichkeit und nicht zwingend erforderlich ist. Ohne ihn könnten wir unseren Service genauso via JavaScript rufen und würden genauso JSON als Serialisierungsformat nutzen können. Allerdings müssten wir den clientseitigen Aufruf dann komplett selbst entwickeln und hätten keine Proxies zur Verfügung.

Da wir jedoch bequem sind, lassen wir uns Proxymethoden generieren. Diese können wir dann in unserem bestehenden Client Script Block innerhalb der Seite aufrufen.

Wie die folgende Abbildung zeigt, erhalten wir dann sogar Intellisense und erfahren somit, welche Argumente eine bestimmte Methode entgegen nimmt.

05_VS

Der Rest gestaltet sich ähnlich der Page Method Implementierung. Als wichtiger Unterschied wäre noch zu erwähnen, dass die generierten Proxies standardmäßig in eine externe JavaScript Datei geschrieben werden und somit nicht mehr als Client Script Block in der Seite auftauchen.

Die Kommunikation zwischen Client und Server läuft analog der Page Method Implementierung.

Der Client sendet in seiner Anfrage unter anderem einen Header Content-Type mit dem Wert "application/json; charset=utf-8". Dieser ist besonders wichtig, denn er steuert den Serialisierungsmechanismus des Servers. Hat dieser Header einen anderen Wert, wird die Antwort XML Serialisiert. Somit ist sichergestellt, dass JavaScript Clients JSON erhalten, wohingegen andere Clients XML erhalten.

06_ajaxheader2

Die nächste Abbildung zeigt, dass auch die Post Parameter via JSON serialisiert werden:

07_ajaxpost2

Für die Antwort ist dies natürlich auch weiterhin der Fall:

07_ajaxantwort2

Fazit:

Dieser Teil der Serie hat gezeigt, dass sich die Menge der übertragenen Daten während eines Ajax Requests mit der Hilfe von Page Methods, Script Services und dem ASP.NET Ajax Framework auf ein Minimum reduzieren lassen. Außerdem wird bei dieser Variante der ASP.NET Page Life Cycle im Gegensatz zu den vorherigen Beispielen nicht durchlaufen.

Beides in Kombination kann zu gewaltigen Performancevorteilen verglichen mit Client Callbacks oder dem Updatepanel führen.

Einziger Wermutstropfen ist die Übertragung der 85 kb großen ASP.NET Ajax Framework Java Script Dateien während des ersten Requests.

Eine Abhilfe für dieses Problem werden wir uns im nächsten Teil der Serie ansehen.

Wie immer freue ich mich natürlich über Kommentare zu diesem Blog Beitrag.

 

Dieser Beitrag war hilfreich? Dann bitte nicht vergessen Ihn zu kicken!


Kick it on dotnet-kicks.de
 
2/18/2010 - 10:14 PM | Comments [0] | Categories: .NET | Ajax | ASP.NET | DotNetGerman Bloggers
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Zu Anfang hoch gelobt, später ausgepfiffen und verpönt. Was sich im ersten Moment nach der typischen Geschichte deutscher Castingshow Teilnehmer anhört, ist in Wirklichkeit das Schicksal des ASP.NET AJAX Updatepanels.

Woll kaum ein anderes Control hat nach seiner Einführung einen solchen Hype verursacht und ist später so tief gefallen.

Der Reihe nach

Ehe wir im dritten Teil meiner ASP.NET Ajax Serie prüfen werden, ob das Updatepanel seinen schlechten Ruf zu recht oder unrecht hat, möchte ich zunächst jedoch kurz ein paar allgemeine Worte über das Steuerelement verlieren.

Das Updatepanel kam mit den ASP.NET Ajax Extensions in der Version 1.0 für das .NET Framework 2.0 auf den Markt und musste für Visual Studio 2005 noch separat heruntergeladen und installiert werden. Ab Visual Studio 2008 ist das UpdatePanel sofort von Hause aus in der Version 3.5 des ASP.NET Ajax Frameworks enthalten.

Ziel des Updatepanels ist es, ASP.NET Entwicklern die Möglichkeit zu geben Ajax zu implementieren, ohne selbst clientseitigen Code schreiben zu müssen. Im Gegensatz zu den Client Callbacks, die ich im Teil 2 der Serie vorgestellt habe, können ASP.NET Entwickler also in Ihrer Komfortzone - dem Server - bleiben.

Der klassische Ansatz über ASP.NET Postbacks

Basis für den heutigen Blogpost wird wieder eine leicht modifizierte Variante des Beispielcodes der vorherigen beiden Teile der Serie sein. Da das Updatepanel aus Entwicklersicht rein serverseitig arbeitet, sind zunächst einige Veränderungen am bestehenden Quellcode notwendig.

Zum einen sind sämtliche HTML Links (a Tags) durch ASP.NET LinkButtons zu ersetzen. Außerdem habe ich den HTML Div Tag, der das Ergebnis der Click-Aktionen anzeigte, durch ein ASP.NET Literal Control ersetzt. Den zuvor in die Seite integrierten JavaScript Code des letzten Beispiels habe ich komplett entfernt.

    1 <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Teil3.aspx.cs" Inherits="Teil2" %>

    2 

    3 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

    4 <html xmlns="http://www.w3.org/1999/xhtml">

    5 <head runat="server">

    6     <title></title>

    7 </head>

    8 <body>

    9     <form id="form1" runat="server">

   10     <h1>

   11         ASP.NET Webforms Anwendungen und Ajax (Teil3): Das Updatepanel</h1>

   12     <div>

   13         <p>

   14             <asp:LinkButton ID="StaticLinkButton" runat="server" OnClick="StaticLinkButton_Click">Hier klicken zum Request einer

   15                 statischen Datei</asp:LinkButton>

   16             <br />

   17             <asp:LinkButton ID="HelloWorldLinkButton" runat="server" OnClick="HelloWorldLinkButton_Click">Hier

   18                 für Hello World WebService klicken</asp:LinkButton>

   19             <br />

   20             <asp:LinkButton ID="EchoLinkButton" runat="server" OnClick="EchoLinkButton_Click">Hier für Echo WebService klicken.

   21                 Geben Sie bitte vorher eine Zahl in nebenstehendem Feld ein:</asp:LinkButton>

   22             &nbsp;<asp:TextBox ID="EchoTextBox" runat="server" Width="40px">4711</asp:TextBox>

   23         </p>

   24     </div>

   25     <asp:Literal ID="contentLiteral" runat="server">Bitte klicken Sie auf einen der Links,

   26     damit dieser Bereich gefüllt wird.</asp:Literal>

   27     </form>

   28 </body>

   29 </html>

Serverseitig hat der Code keine Überraschungen parat. Je HTML Link Button gibt es einen Handler für das Click Event, in dem der Inhalt des Literal Controls gesetzt wird.

    1     protected void StaticLinkButton_Click(object sender, EventArgs e)

    2     {

    3         this.contentLiteral.Text = this.ReadStaticFile();

    4     }

    5 

    6     protected void HelloWorldLinkButton_Click(object sender, EventArgs e)

    7     {

    8         this.contentLiteral.Text = this.CallHelloWorldService();

    9     }

   10 

   11     protected void EchoLinkButton_Click(object sender, EventArgs e)

   12     {

   13         this.contentLiteral.Text = this.CallEchoService();

   14     }

   15 

   16     private string ReadStaticFile()

   17     {

   18         string fileContent;

   19         using (var reader = new StreamReader(Server.MapPath("~/static.html")))

   20         {

   21             fileContent = reader.ReadToEnd();

   22         }

   23         return fileContent;

   24     }

   25 

   26     private string CallHelloWorldService()

   27     {

   28         return new AjaxDemoService().HelloWorld();

   29     }

   30 

   31     private string CallEchoService()

   32     {

   33         int value;

   34         int.TryParse(this.EchoTextBox.Text, out value);

   35         return new AjaxDemoService().Echo(value);

   36     }

Startet man die Seite nun, sieht man dass ein GET Request auf die Seite Teil3.aspx abgesetzt wird. Die Antwort umfasst 2.1 kb.

01ersterRequest

Klickt man nun auf einen der Link Buttons, wird ein Postback ausgeführt und die komlette Seite neu vom Server auf den Client übertragen. Dies überrascht auch nicht, schließlich wurde noch keinerlei Ajax Funktionalität implementiert.

02normalerPostback 

Ein erster Versuch mit dem Updatepanel

Um die Seite nun zu "ajaxifizieren", benötigen wir zwei Controls. Das erste ist der ASP.NET Scriptmanager. Seine Hauptaufgabe ist es, Scripte zum Client zu liefern. Er muss vor allen AJAX Controls innerhalb der Seite erscheinen. Außerdem ist noch anzumerken, dass der Scriptmanager nach dem Highlander prinzip arbeitet: "Es kann nur einen geben!" - nun ja, zumindest pro Seite. Andernfalls würde zur Laufzeit eine InvalidOperationException ausgelöst werden.

Dies bedeutet übrigens, dass ich beim Einsatz von Masterpages keinen ScriptManager auf einer Contentseite platzieren darf, falls  auf der Masterpage bereits ein Scriptmanager genutzt wurde. Stattdessen müsste ich auf der Contentseite einen ScriptmanagerProxy einsetzen. Diesen benötige ich allerdings nur, falls ich auf der Contentseite deklarativ noch weitere Scripte hinzufügen möchte, die auf der Masterpage nicht gebraucht werden.

Da ich langsam etwas ausschweifend werde: Zurück zum Thema!

Das zweite Control, das wir auf unserer Seite brauchen ist ein Updatepanel. Es dient als Container des Bereichs, der später aktualisiert werden soll. Updatepanels dürfen im Gegensatz zum Scriptmanager übrigens mehrfach auf einer Seite vorkommen.

 03toolbox

Nachdem ich Scriptmanager und Updatepanel auf die Seite gezogen, und die die Clientcontrols in das Updatepanel verschoben habe, sieht das Markup meiner Seite wie folgt aus.

    1 <body>

    2     <form id="form1" runat="server">

    3     <asp:ScriptManager ID="ScriptManager1" runat="server">

    4     </asp:ScriptManager>

    5     <h1>

    6         ASP.NET Webforms Anwendungen und Ajax (Teil3): Das Updatepanel</h1>

    7     <asp:UpdatePanel ID="UpdatePanel1" runat="server">

    8         <ContentTemplate>

    9             <p>

   10                 <asp:LinkButton ID="StaticLinkButton" runat="server" OnClick="StaticLinkButton_Click">Hier klicken zum Request einer

   11                 statischen Datei</asp:LinkButton>

   12                 <br />

   13                 <asp:LinkButton ID="HelloWorldLinkButton" runat="server" OnClick="HelloWorldLinkButton_Click">Hier

   14                 für Hello World WebService klicken</asp:LinkButton>

   15                 <br />

   16                 <asp:LinkButton ID="EchoLinkButton" runat="server" OnClick="EchoLinkButton_Click">Hier für Echo WebService klicken.

   17                 Geben Sie bitte vorher eine Zahl in nebenstehendem Feld ein:</asp:LinkButton>

   18                 <asp:TextBox ID="EchoTextBox" runat="server" Width="40px">4711</asp:TextBox>

   19             </p>

   20             <asp:Literal ID="contentLiteral" runat="server">Bitte klicken Sie auf einen der Links,

   21     damit dieser Bereich gefüllt wird.</asp:Literal>

   22         </ContentTemplate>

   23     </asp:UpdatePanel>

   24     </form>

   25 </body>

Da keine Anpassungen im Code Behind meiner Seite notwendig sind, kann ich die Anwendung nun direkt starten.

Ein Blick auf den Netzwerkverkehr zeigt, dass bei der ersten Anfrage der Seite neben der eigentlichen Seite noch drei JavaScript Dateien übertragen wurden. Die Verweise auf diese drei Script Dateien wurden durch den ScriptManager automatisch in die Seite eingetragen. Allerdings werden nun statt der initialen 2.1 kb knapp 90 kb zum Client übertragen.

04ersterRequestMitUpdatePanel

Den eigentlichen Vorteil sehen wir, sobald wir auf einen der Links klicken. Nicht nur, dass die Seite nicht komplett neu geladen wird und somit das lästige flackern beim Seitenneuaufbau entfällt, auch die Menge der übertragenen Daten hat sich auf 1,5 kb reduziert.

05AsyncRequest1Post

Anscheinend haben wir also alles richtig gemacht. Leider aber auch nur anscheinend. Ein näherer Blick auf die Antwort des Servers zeigt, dass neben dem aktualisierten Inhalt auch das Markup für unsere drei Aktionslinks sowie die Textbox erneut übertragen wurden.

06AsyncRequest1Antwort

Das liegt daran, dass wir einen klassichen Updatepanel Anfängerfehler gemacht haben. Wir haben den kompletten Inhalt der Seite in das Updatepanel geschoben. Somit wird dieser auch vollkommen korrekt wieder vollständig vom Server zum Client übertragen.

Das ist selbstverständlich nicht das, was wir wollten. Schließlich ist unsere Ajax Implementierung nur augenwischerei wenn im Hintergrund doch wieder (fast) alles zum Client übertragen wird.

Ein zweiter Anlauf

Was können wir also tun? Nun, da wir nur den Inhalt des Steuerelements contentLiteral aktualisieren möchten, sollten wir offensichtlich auch nur diesen in das Updatepanel aufnehmen. Dazu schieben wir einfach die drei Aktionslinks wieder über das Updatepanel.

Wenn wir die Seite nun starten, stellen wir jedoch fest, dass ein Klick auf einen der Aktionslinks wieder einen vollständigen Postback inklusive komplettem Rendern der Seite auslöst.

Die Ursache hierfür ist, dass ein Updatepanel standardmäßig nur einen partiellen Postback verursacht und sich somit erneuert, wenn eines seiner Childcontrols einen Postback initiert.

Glücklicherweise lässt sich dieses Verhalten jedoch leicht ändern. Für jedes Updatepanel können sogenannte Trigger registriert werden, die einen partiellen Postback auslösen. Ein Trigger ist dabei nichts anderes, als ein Event eines Servercontrols.

Das Registrieren eines solchen Triggers kann entweder über den Designer oder über das ASP.NET Markup geschehen. Um es über das Markup zu lösen, müssen wir unser Beispiel wie folgt anpassen:

    1     <asp:UpdatePanel ID="UpdatePanel1" runat="server">

    2         <ContentTemplate>

    3             <asp:Literal ID="contentLiteral" runat="server">Bitte klicken Sie auf einen der Links,

    4     damit dieser Bereich gefüllt wird.</asp:Literal>

    5         </ContentTemplate>

    6        <Triggers>

    7             <asp:AsyncPostBackTrigger ControlID="StaticLinkButton" EventName="Click" />

    8             <asp:AsyncPostBackTrigger ControlID="HelloWorldLinkButton" EventName="Click" />

    9             <asp:AsyncPostBackTrigger ControlID="EchoLinkButton" EventName="Click" />

   10         </Triggers>

   11     </asp:UpdatePanel>

Lädt man die Seite nun erneut, stellt man fest, dass die Aktionslinks wieder partielle Postbacks auslösen. Außerdem werden auch nur die notwendigen Daten übertragen.

07AsyncRequest2Antwort

Zu viel des Guten

Das Ergebnis sieht eigentlich schon ganz gut, so das man meinen könnte, dass wir nun fertig wären. Leider gibt es aber noch ein kleines Problem. Um dies zu verdeutlichen lege ich ein weiteres Updatepanel auf der Seite an. Dieses Panel erhält ein Literal-Control, welches serverseitig mit der aktuellen Uhrzeit gefüllt wird.

    1     <asp:UpdatePanel ID="UpdatePanel2" runat="server">

    2         <ContentTemplate>

    3             <asp:Literal ID="TimeLiteral" runat="server"></asp:Literal>

    4         </ContentTemplate>

    5     </asp:UpdatePanel>

 

    1     protected void Page_Load(object sender, EventArgs e)

    2     {

    3         this.TimeLiteral.Text = DateTime.Now.ToLongTimeString();

    4     }

Meine Erwartungshaltung wäre, dass bei einem Klick auf einen der drei Aktionslinks nur das ursprüngliche Updatepanel aktualisiert wird, da die Links nur hier als Trigger registriert wurden. Das neue Updatepanel sollte nicht aktualisiert werden.

Ein kurzer Test zeigt dass dem nicht so ist:

08_2UpdatePanels1 08_2UpdatePanels2

Wie die beiden Abbildungen zeigen, wurde nicht nur der Bereich des ersten Updatepanels, sondern auch der des zweiten Updatepanels aktualisiert.

Glücklicherweise lässt sich dieses Verhalten jedoch steuern! Das Updatepanel verfügt über eine Eigenschaft UpdateMode. Diese gibt an, wann sich das Updatepanel aktualisieren soll. Standardmässig ist diese Eigenschaft mit dem Wert Always belegt. Stellt man den Wert jedoch auf Contidional, wird das Updatepanel nur noch aktualisiert, wenn entweder eines seiner Childcontrols einen Postback initiert hat, einer der registrierten Trigger ausgelöst wurde, oder explizit die Methode Update serverseitig gerufen wurde.

Die folgenden Abbildungen zeigen die Auswirkung der Änderung des UpdateModes:

08_2UpdatePanels3

08_2UpdatePanels4

08_2UpdatePanels5

Fazit

Richtig eingesetzt ist das Updatepanel meiner Meinung nach besser als sein Ruf und für einfache Szenarien durchaus geeignet. Ajax Funktionalitäten können mit wenig Aufwand und ohne manuellen Scriptcode implementiert werden.

Achtet man darauf, nicht die komplette Seite in ein Updatepanel zu integrieren und den UpdateMode auf Conditional zu setzen, kann das Updatepanel in vielen Szenarien guten Gewissens genutzt werden. Man sollte sich allerdings darüber im Klaren sein, dass serverseitig bei jeder Anfrage der komplette Page-Lifecycle durchlaufen wird. Zeitaufwändige Operationen im Page_Load können - ähnlich wie bei den Client Callbacks - die Ajax Anfragen demnach erheblich verlangsamen!

Was man machen kann, wenn man den Pagelifecycle nicht komplett durchlaufen möchte, werde ich übrigens im nächsten Teil dieser Serie zeigen.


Kick it on dotnet-kicks.de
 
2/12/2010 - 12:51 AM | Comments [0] | Categories: .NET | Ajax | ASP.NET | DotNetGerman Bloggers
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

In meinem letzten Blog Eintrag habe ich erklärt, wie Ajax von Hand mit Hilfe des XMLHttpRequest Objekts implementiert werden kann.

Eine Alternative zu dieser händischen Implementierung ist die Nutzung des ASP.NET Client Callback Frameworks, welches mit der Version 2.0 von ASP.NET erschien.

Um Client Callbacks nutzen zu können muss eine ASP.NET Seite bzw. ein ASP.NET Control das Interface ICallbackEventHandler implementieren. Das Interface definiert die beiden Methoden:

RaiseCallbackEvent ist die Methode, den Aufruf des Clients entgegen nimmt. GetCallbackResult gibt das Ergebnis der serverseitigen Verarbeitung an den Client zurück.

Startet der Client eine Anfrage an eine Seite / ein Control welches ICallBackEventHandler implementiert, wird eine reduzierte Variante des ASP.NET Page Lifecycle / ASP.NET Control Lifecycle ausgeführt. Im wesentlichen entspricht die Callback Variante der "klassichen" Postback Variante, es fehlen jedoch die Methoden, die sich um das Rendering des Outputs kümmern. Außerdem werden keine Postback Events ausgeführt.

Das ganze sieht bezogen auf die Phasen also ungefähr so aus:

04lifecycle

Los gehts

Sehen wir uns das ganze nun einmal an einem konkreten Beispiel an. Als Ausgangsbasis soll eine modifizierte Kopie der Seite des letzten Beispiels mit folgendem Markup dienen:

    1     <div>

    2         <p>

    3             <a href="#">Hier klicken zum Request einer

    4                 statischen Datei </a>

    5             <br />

    6             <a href="#">Hier für Hello World WebService klicken </a>

    7             <br />

    8             <a href="#">Hier für Echo WebService klicken. Geben Sie bitte vorher eine Zahl in nebenstehendem Feld ein: </a>&nbsp;

    9             <asp:TextBox ID="TextBox1" runat="server" Width="40px">4711</asp:TextBox>

   10         </p>

   11     </div>

   12     <div id="content">

   13         Bitte klicken Sie auf einen der Links, damit dieser Bereich gefüllt wird.

   14     </div>

 

Der Server

Auf Serverseite müssen wir nun in unserer Seite das Interface ICallbackEventHandler implementieren. Ausgehend von unserem Markup gilt es drei Operationen zu unterstützen:

  • Zurückgeben des Inhalts einer statischen Datei
  • Aufruf einer parameterlosen Methode eines Webservices, die "Hello World" zurück gibt
  • Aufruf einer Methode Echo eines Webservices, welche einen numerischen Wert entgegen nimmt und einen String zurück gibt.

Welche Operation auszuführen ist, unterscheiden wir anhand des einzigen Arguments der Methode RaiseCallbackEvent: eventArgument.

    1 public partial class Teil2 : Page, ICallbackEventHandler

    2 {

    3     private string callbackResult;

    4 

    5     public void RaiseCallbackEvent(string eventArgument)

    6     {

    7         switch (eventArgument)

    8         {

    9             case "static":

   10                 this.ReadStaticFile();

   11                 break;

   12             case "HelloWorld":

   13                 this.CallHelloWorldService();

   14                 break;

   15             case "Echo":

   16                 this.CallEchoService();

   17                 break;

   18             default:

   19                 this.ReturnUnknownOperationError();

   20                 break;

   21         }

   22     }

   23 

   24     public string GetCallbackResult()

   25     {

   26         return this.callbackResult;

   27     }

Je nach Wert des Arguments eventArgument wird eine andere Methode zur weiteren Verarbeitung aufgerufen. Jede dieser Methoden schreibt ihr Ergebnis wiederum in das Feld callbackResult. Dieses wird wiederum als Ergebnis der Methode GetCallbackResult an den Client zurück gegeben.

Der Inhalt der verarbeitenden Methoden sieht wie folgt aus:

Auslesen der statischen Datei:

    1 private void ReadStaticFile()

    2 {

    3     using (var reader = new StreamReader(Server.MapPath("~/static.html")))

    4     {

    5         this.callbackResult = reader.ReadToEnd();

    6     }

    7 }

Aufruf der Methode HelloWorld des Webservices:

    1 private void CallHelloWorldService()

    2 {

    3     this.callbackResult = new AjaxDemoService().HelloWorld();

    4 }

Aufruf der Methode Echo des Webservices:

    1 private void CallEchoService()

    2 {

    3     int value;

    4     int.TryParse(this.TextBox1.Text, out value);

    5     this.callbackResult = new AjaxDemoService().Echo(value);

    6 }

Wie man beim Aufruf der Webservice Methoden sieht, geschieht dieser Aufruf nun vom Server aus und nicht vom Client. Dadurch dass im Gegensatz zum vorherigen Beispiel der Client nun nicht mehr direkt den Service ruft, musste ich mir einen Proxy auf Serverseite erstellen.

Schön, aber wie komme ich nun auf den Server?

Bisher haben wir zwar geklärt, was auf dem Server alles geschehen muss, damit Client Callbacks funktionieren, offen ist allerdings noch, wie man überhaupt vom Client auf den Server kommt.

Der Aufruf des Servers geschieht über die JavaScript Funktion WebForm_DoCallback. Einen entsprechenden Aufruf der Methode kann man sich über den ClientScriptManager der Seite anhand der Methode GetCallbackEventReference erzeugen lassen. Zeile 5 des folgenden Listing demonstriert dies.

    1 protected void Page_Load(object sender, EventArgs e)

    2 {

    3     var clientScript = Page.ClientScript;

    4     string callbackReference =

    5         clientScript.GetCallbackEventReference(this, "argument", "requestFinished", "context", true);

    6 

    7     string script = string.Format("function sendAjaxRequest(argument, context){{\n {0}; }}", callbackReference);

    8 

    9     if (!clientScript.IsClientScriptBlockRegistered("callback"))

   10     {

   11         clientScript.RegisterClientScriptBlock(this.GetType(), "callback", script, true);

   12     }

   13 }

Die Methode GetCallbackEventReference hat in der höchsten Überladung folgende Parameter:

  Parameter Bedeutung
control Servercontrol, dass den Callback behandelt.
argument Argument, das an die Methode RaiseCallbackEvent des behandelnden Controls geschickt wird.
clientCallback Name der Javascript Funktion, die die Rückgabe des Servers im Erfolgsfall behandelt.
context Javascsript Code, der ausgeführt wird, ehe die Anfrage an den Server geschickt wird. Achtung, hierbei handelt es sich nicht um einen JavaScript String, sondern um einen Befehl / eine Funktion
clientErrorCallback Name der Javascript Funktion, die die Rückgabe des Servers im Fehlerfall behandelt. In meinem Beispiel habe ich diesen Paramter nicht gesetzt.
useAsync Boolscher Wert, der angibt, ob die Anfrage asynchron (true) oder synchron (false) abgesetzt werden soll.

Der Einfachheit halber verpackt man den generierten Aufruf der Methode WebForm_DoCallback in eine eigene JavaScript Funktion, die nur noch die wirklichen variablen Parameter argument und context entgegen nimmt, wie in Zeile 7 geschehen. Anschließend registriert man wie in Zeile 9 - 11 noch einen ClientScriptBlock für diese Funktion, der wie folgt in die Seite gerendert wird.

    1 <script type="text/javascript">

    2     //<![CDATA[

    3     function sendAjaxRequest(argument, context) {

    4         WebForm_DoCallback('__Page', argument, requestFinished, context, null, true);

    5     } //]]>

    6 </script>

Die generierte Methode sendAjaxRequest kann man anschließend wie folgt aufrufen:

    1             <a href="#" onclick="sendAjaxRequest('static', showMessage());">Hier klicken zum Request einer

    2                 statischen Datei </a>

    3             <br />

    4             <a href="#" onclick="sendAjaxRequest('HelloWorld', '');">Hier

    5                 für Hello World WebService klicken </a>

    6             <br />

    7             <a href="#" onclick="sendAjaxRequest('Echo', null);">Hier

    8                 für Echo WebService klicken. Geben Sie bitte vorher eine Zahl in

    9             nebenstehendem Feld ein: </a>

Zeile 1 zeigt, wie der Parameter context genutzt wird. Ich übergebe einfach die JavaScript Funktion showMessage als Argument. Diese wird nun automatisch aufgerufen, ehe der Request gestartet wird. Dies ist zum Beispiel bei länger dauernden Ajax Aufrufen sinnvoll, bei denen man dem Benutzer einen Hinweis anzeigen möchte, wie folgendes Listing zeigt:

    1 <script type="text/javascript">

    2     function showMessage() {

    3         var contentDiv = document.getElementById('content');

    4         contentDiv.innerHTML = 'Lade...';

    5     }

    6 </script>

Wichtig ist, dass der Parameter keinen String entgegen nimmt (wie leider in vielen Beispielen fälschlicherweise gezeigt), sondern eine JavaScript Funktion!

Eigentlich Interessant für unser Beispiel ist jedoch nicht der Parameter context, sonder der Parameter argument. Der Inhalt dieses Parameters (und nur dieser!) wird an die Methode RaiseCallbackEvent des Servers übergeben. Er steuert somit die auszuführende Funktionalität.

Alles beim Alten?

Spielt man nun ein wenig mit der vorgestellten Lösung herum, bemerkt man schnell, dass der Aufruf der Methode Echo stets ausgibt, das man 4711 eingegeben hat - auch wenn der Wert der Textbox zwischenzeitlich geändert wurde.

Ursache hierfür ist, dass ASP.NET Client Callbacks für alle Formularfelder stets die initialen Werte zurücksendet.

Screenshot der Beispielanwendung

Abhilfe schafft folgender JavaScript Code:

    1 __theFormPostData = '';

    2 WebForm_InitCallback();

Diesen packt man einfach in eine JavaScript Funktion, z. B. mit dem Namen repostForm.

Gibt man diese nun als context während des Aufrufs an:

    1 <a href="#" onclick="sendAjaxRequest('Echo', repostForm());">Hier [...]

werden auch die geänderten Eingaben des Benutzers übertragen.

Screenshot zeigt dass die geänderten Werte übertragen wurden

Fazit

ASP.NET Client Callbacks sind eine schnelle und einfache Lösung um Ajax Funktionalitäten unter ASP.NET ab der Version 2.0 zu implementieren.

Bei aller Einfachheit sollte man jedoch beachten, dass diese Lösung auch einige Schwächen hat:

  1. Auf dem Server wird fast der komplette Page Lifecycle ausgeführt. Daher sollten Codebestandteile, die bei einem Callback nicht zwingend ausgeführt werden müssen immer durch ein if(!Page.IsCallback) ... geklammert werden, um unnötige Wartezeiten zu verhindern.
  2. Bei jedem Callback wird der komplette Viewstate zum Server übertragen. Gerade bei großem Viewstate und schlechter Internetanbindung verlangsamt dieser Umstand die Ajax Anfrage enorm.
  3. Es kann immer nur ein Argument an den Server übergeben werden. Möchte man mehr als eins übergeben, muss man die Werte durch ein Trennzeichen trennen und auf der Serverseite auseinanderpflücken
  4. Das übergebene Argument wird nicht wieder vom Server auf den Client zurück übertragen. Somit ist es in der verarbeitenden Javascript Funktion auf dem Client unmöglich herauszufinden, welche Operation ursprünglich gestartet wurde. Abhilfe schafft auch hier ein zusammengesetzter Rückgabewert, oder aber eine eigene verarbeitende Funktion je Operation.

Hat man diese Schwächen jedoch im Blick, lassen sich mit ASP.NET Client Callbacks wunderbare Ajax Applikationen implementieren.

Unter der Haube arbeiten diese Client Callbacks übrigens genauso wie das händische Beispiel aus dem letzten Blogpost - nämlich mit dem XMLHttpRequest Objekt:

Screenshot des JavaScript Debuggers, zeigt Zugriff auf XMLHttpRequest Objekt


Kick it on dotnet-kicks.de
 
2/5/2010 - 01:20 AM | Comments [7] | Categories: .NET | Ajax | ASP.NET | DotNetGerman Bloggers
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Das man mit statischen, oder auch dyanamischen Seiten, die bei jeder Anfrage die komplette Seite neu aufbauen, heute keinen Blumentopf mehr gewinnt, dürfte jedem klar sein. Verwöhnt durch Webanwendungen wie zum Beispiel Google-Maps oder Outlook Web Access erwarten Endanwender Webanwendungen, die einen ähnlichen Bedienkomfort wie Desktop Anwendungen aufweisen.

Als Entwickler solcher Webanwendungen bringt uns dies in die Situation, dass wir unsere Entwicklungstätigkeiten nun nicht mehr rein auf den Server beschränken können, sondern auch auf dem Client aktiv werden müssen, um die hohen Erwartungen unserer Anwender zu erfüllen.

Was "auf dem Client aktiv werden" konkret bedeutet möchte ich in einer kleinen Artikelreihe in meinem Blog beantworten.

Der Client - das unbekannte Wesen

Heutzutage sind eine Reihe Ajax Frameworks verfügbar, die uns die "schmutzige" Arbeit auf dem Weg zur dynamischen Webseite abnehmen wollen. Da es meiner Meinung nach aber nie schaden kann, wenn man weiss was diese magischen Toolsets und Frameworks unter der Haube machen, werden wir heute die Ärmel hoch krempeln und die "Drecksarbeit" auf dem Client selbst erledigen.

Ehe wir los legen, sollten wir zuvor jedoch einen Blick auf die notwendigen Zutaten werfen und kurz klären, worum es sich dabei im einzelnen handelt.

  1. JavaScript

    Das J in AJAX. Eine von vielen Entwicklern zu unrecht gefürchtete und/oder belächelte Client Script Sprache ;-) Der Scriptcode wird entweder direkt in eine Seite eingebettet, oder aber in eine externe Datei ausgelagert. JavaScript wird zur Manipulation des DOM genutzt

  2. das Document Object Model (DOM) des Browsers 

    Das DOM des Browsers ist eine Schnittstelle, die den Zugriff auf das zugrundeliegende (X)HTML Dokument bereitstellt. Über das DOM habe ich die Möglichkeit, Aussehen und Struktur der einer Webseite zu verändern.

  3. das XMLHttpRequest Objekt

    Eines der Objekte des window Objekts des Browsers(zumindest in modernen Browsern. Andernfalls ein ActiveX Objekt). Es ermöglicht einem Script das Absetzen einer (asynchronen) Anfrage an einen Webserver. 

Im Überblick sieht das Zusammenspiel der drei Komponenten ungefähr so aus:

Zusammenspiel zwischen JavaScript, DOM und XMLHttpRequest

Los gehts!

Wir wissen zwar jetzt, was wir alles brauchen, allerdings fehlt immer noch die Antwort, wie die Komponenten nun genutzt werden müssen, um AJAX in die eigene Webseite zu bekommen.

Starten wir dazu mit einem kleinen und sehr einfachen Beispiel.

Wir erstellen eine einfache ASPX Seite, die aus einem Link und einem Platzhalter Bereich bestehen soll. Sobald der Anwender auf den Link klickt, soll dieser Platzhalter mit dem Inhalt einer statischen Datei, welche auf dem Server liegt, gefüllt werden.

Der zugehörige HTML Code der ASPX Seite sieht wie folgt aus:

    1     <form id="form1" runat="server">

    3     <div>

    4         <p>

    5             <a href="#">Hier klicken zum Request einer

    6                 statischen Datei </a>

    7         </p>

    8     </div>

    9     <div id="content">

   10         Bitte klicken Sie den Link, damit dieser Bereich gefüllt wird.

   11     </div>

   12     </form>

Der Link, welcher später die Ajax Aktion initiieren soll, wird in den Zeilen 5 und 6 definiert. Der Bereich welcher später ersetzt werden soll, ist der Inhalt des DIV Tags aus Zeile 9.

Noch ist das ganze jedoch nicht sonderlich dynamisch. Dazu benötigen wir noch etwas JavaScript:

    1     <script type="text/javascript">

    2         var xmlHttp;

    3         window.onload = function() {

    4             initializeXmlHttp();

    5         }

    6 

    7         function initializeXmlHttp() {

    8             if (window.XMLHttpRequest) { // IE7, IE8, Mozilla, Safari, Opera

    9                 xmlHttp = new XMLHttpRequest();

   10             } else if (window.ActiveXObject) {

   11                 try {

   12                     xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); //IE 5.x, 6

   13                 }

   14                 catch (e) {

   15                     alert('Ajax Zugriffe sind aufgrund der aktuellen Sicherheitseinstellungen leider nicht möglich');

   16                 }

   17             }

   18         }

   19 

   20         function sendAjaxRequest(url) {

   21             if (xmlHttp) {

   22                 xmlHttp.open("GET", url, true);

   23                 xmlHttp.onreadystatechange = requestFinished;

   24                 xmlHttp.send(null);

   25             }

   26         }

   27 

   28         function requestFinished() {

   29             if (xmlHttp.readyState==4) {

   30               if (xmlHttp.status==200) {

   31                 var contentDiv = document.getElementById('content');

   32                 contentDiv.innerHTML = xmlHttp.responseText;

   33               }

   34             }

   35         }

   36    </script>

Das Script deklariert zunächst in Zeile 2 eine Variable, die einen Verweis auf das XMLHhtpRequest Objekt hält. Wie zuvor beschrieben ermöglicht dieses das Absetzen einer asynchronen Abfrage an den Server.

Die Funktion initializeXmlHttp ab Zeile 7 füllt dieses Objekt nun mit Leben. Der Code sollte recht selbsterklärend sein. Zeile 8 prüft, ob das XMLHttpRequest Objekt am window Objekt hängt. Falls ja, kann ein neues XMLHttpRequest Objekt erstellt werden. Ist dies nicht der Fall, muss es als ActiveX Objekt erstellt werden.

Die Funktion sendAjaxRequest ab Zeile 20 setzt die eigentliche Abfrage ab. Dazu prüft Sie in Zeile 21 zuerst, ob die Variable xmlHttp initialisiert werden konnte. Wäre dies nicht der Fall, könnte keine Abfrage abgesandt werden. Anschließend wird in Zeile 22 eine asynchrone Verbindung geöffnet mit der Methode open geöffnet. Diese Methode erhält als Parameter die Methode ("GET" oder "POST"), die URL, sowie ein Flag, ob die Anfrage asynchron abgesetzt werden soll.

Zeile 23 gibt eine CallBack-Funktion an (in unserem Fall requestFinished), die aufgerufen werden soll, sobald eine Antwort auf die Anfrage zurück gekommen ist.

Zeile 24 sendet die Anfrage nun schließlich ab.

Die Funktion requestFinished ab Zeile 28 verarbeitet die Antwort des Webservers. Da eine Anfrage verschiedene Phasen durchlebt, prüft die Funktion in Zeile 29 anhand der Eigenschaft readyState ab, ob die Anfrage tatsächlich komplett beendet wurde.

Da zu Ende nicht immer "hat auch geklappt" heißen muss, wird in der nächsten Zeile geprüft, ob die Anfrage auch erfolgreich beendet wurde (HTTP Statuscode 200).

Die eigentliche Verarbeitung findet nun in den Zeilen 31 und 32 statt. Zeile 31 holt sich eine Referenz auf den zu ersetzenden Bereich (unser Div mit der ID Content). Zeile 32 überschreibt diesen Inhalt schlussendlich mit der Antwort des Servers.

Damit das ganze auch funktioniert muss die Funktion sendAjaxRequest aus Zeile 20 noch aus unserem HTML Code heraus aufgerufen werden. Dazu erweitern wir unseren Link aus den Zeilen 5/6 einfach um das Attribut onclick.

    5             <a href="#" onclick="sendAjaxRequest('static.html');">Hier

    6                 klicken zum Request einer statischen Datei </a>

Ausgeführt sieht das ganze dann wie folgt aus:

Erster Aufruf der Seite Nach Aufruf des Links

Ganz nett ...

wir wissen nun also, wie wir statische Dateien nachladen können, aber steht das X in AJAX nicht für XML Webservices?

Genau! Deshalb habe ich das Demoprojekt auch um einen kleinen Webservice mit zwei sinnlosen Methoden erweitert:

    1 public class AjaxDemoService : System.Web.Services.WebService {

    2 

    3     [WebMethod]

    4     public string HelloWorld() {

    5         return "Hello World";

    6     }

    7 

    8     [WebMethod]

    9     public string Echo(int number)

   10     {

   11         return string.Format("Sie haben {0} eingegeben.", number);

   12     }  

   13 }

Wie rufen wir einen solchen Service nun aber per JavaScript auf?

Die Antwort findet sich relativ schnell, wenn man die URL des Webservices im Browser eingibt. In der nun erscheinenden Seite werden sämtliche Methoden des Webservices aufgelistet. Ein Klick auf eine dieser Methoden verrät schlussendlich, wie die Methode aufgerufen werden muss. Im Beispiel der Methode HelloWorld sieht die Ausgabe für eine HTTP Post Anfrage zum Beispiel wie folgt aus:

Beschreibung des POST Aufrufs der Webservice Methode

Für die Methode Echo wird folgende Ausgabe produziert:

4_echo_http_post

Im Vergleich zu unserer bisherigen Vorgehensweise beim dynamischen Nachladen der statischen Datei fällt auf, dass die Methode, über die die Anfrage gestartet wird, nun nicht mehr GET ein kann, sondern POST sein muss.

Außerdem wird nun auch ein bestimmter Content-Type, nämlich application/x-www-form-urlencoded benötigt. Speziell bei der Methode Echo fällt auf, dass außerdem nun auch Daten mit an den Server gesendet werden müssen.

Um die veränderten Anforderungen abdecken zu können, erweitere ich das bestehende JavaScript um eine neue Funktion:

    1         function postAjaxRequest(url, data) {

    2             if (xmlHttp) {

    3                 xmlHttp.open("POST", url, true);

    4                 xmlHttp.onreadystatechange = requestFinished;

    5                 xmlHttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');

    6                 xmlHttp.send(data);

    7             }

    8         }

 

Die neuen Funktion, postAjaxRequest ähnelt der vorherigen Funktion sendAjaxRequest. Die veränderten Anforderungen ließen sich relativ einfach integrieren. In Zeile 3 wurde aus einem GET ein POST. In Zeile 5 wird der zusätzlich benötigte Content-Type gesetzt. In Zeile 6 wird der Methode send nun nicht mehr fix null als Datenparameter übergeben. Stattdessen wird das neue Argument data weiter gereicht.

Der HTML Code musste auch nur unwesentlich erweitert werden. Er erhält zwei neue Links zum Aufruf der beiden Webmethoden:

    1             <a href="#" onclick="postAjaxRequest('AjaxDemoService.asmx/HelloWorld', null);">Hier

    2                 für Hello World WebService klicken </a>

    3             <br />

    4             <a href="#" onclick="postAjaxRequest('AjaxDemoService.asmx/Echo', 'number=1');">Hier

    5                 für Echo WebService klicken </a>

Das Ergebnis sieht anschließend wie folgt aus:

Abbildung zeigt den Rückgabewert des Hello World Webservices Abbildung zeigt die Rückgabe des Echo Web Services

Fazit

Ajax in eine Webanwendung zu integrieren ist aus technischer Sicht nicht sonderlich schwierig. Der vorliegende Blog Post hat gezeigt, dass sich selbst ohne "magische" Frameworks mit wenigen Zeilen JavaScript Ajax Funktionalitäten integrieren lassen. Natürlich ist die gezeigte Implementierung nur sehr rudimentär. So wurde das zurückgegeben XML des Webservices zum Beispiel noch nicht geparsed und es fand außerdem so gut wie keine Fehlerbehandlung statt. Dieses in das Beispiel zu integrieren hätte den Rahmen eines Blogposts jedoch vollständig gesprengt. Außerdem gibt es genau aus diesen Gründen auch bereits fertige Frameworks, wie ASP.NET AJAX oder jQuery, die einem genau diese Detailarbeit abnehmen.

Ehe man ein solches Framework einsetzt, sollte man jedoch die Grundlagen dessen kennen, was hinter den Kulissen geschieht. Und genau diese Grundlagen haben wir in diesem Blog Post geschaffen.

Das vollständige Beispiel werde ich übrigens am Wochenende zum Download bereit stellen.


Kick it on dotnet-kicks.de
 
1/29/2010 - 12:51 AM | Comments [4] | Categories: .NET | Ajax | ASP.NET | DotNetGerman Bloggers
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

vielen Missverständnissen. Eines dieser Missverständnisse ist die häufig anzutreffende Meinung, dass der ASP.NET ViewState für DropDownList Controls nicht abgeschaltet werden darf. Andernfalls würde die DropDownList den vom Anwender gewählten Wert beim Postback "verlieren".

Ein kleines Beispiel

Sehen wir uns zur Verdeutlichung ein kleines Beispiel an. Ein Entwickler - nennen wir ihn Herrn Brause -  hat die Aufgabe eine Webseite zu schreiben, auf der ein Anwender in einer DropDownListe auswählen kann, wie viele Zeilen Quellcode er heute schreiben kann. Nach der Bestätigung der Auswahl soll der entsprechende Wert auf der Webseite angezeigt werden.

Herr Brause macht sich schnell ans Werk und erstellt folgende Seite:

Screenshot der erstellen Webseite, bestehend aus Text und DropDownList 

Die DropDownListe füllt er mit folgendem Code.

    1 using System;

    2 using System.Web.UI.WebControls;

    3 

    4 public partial class _Default : System.Web.UI.Page

    5 {

    6     protected void Page_Load(object sender, EventArgs e)

    7     {

    8         if (!IsPostBack)

    9         {

   10             linesOfCodeDropDown.DataSource = GetLinesOfCodeListItems();

   11             linesOfCodeDropDown.DataBind();

   12         }

   13     }

   14 

   15     static ListItemCollection GetLinesOfCodeListItems()

   16     {

   17         var items = new ListItemCollection();

   18         for (int i = 0; i < 1000; i++)

   19         {

   20             items.Add(new ListItem(string.Format("{0} Codezeilen", i), i.ToString()));

   21         }

   22         return items;

   23     }

   24 

   25     protected void sendButton_Click(object sender, EventArgs e)

   26     {

   27         linesOfCodeLiteral.Text = linesOfCodeDropDown.SelectedItem.Text;

   28     }

   29 }

   30 

Ein erster kleiner Test auf Herrn Brauses Rechner ergibt, dass die Webseite zufriedenstellend läuft. Stolz übergibt er sein Arbeitsergebnis an seinen Chef.

Nicht so schnell!

Kurz nach der Veröffentlichung der Seite mehren sich die Beschwerden von Anwendern, die an Standorten mit schlechten Internetverbindungen arbeiten. Sie klagen über extrem lange Ladezeiten.

Herr Brause nimmt sich dem Problem an und findet schnell heraus, dass sich mehr als ein Drittel der übertragenen Daten aus dem ViewState der Seite zusammen setzt. Von den insgesamt knapp 107 kB der Seite verteilen sich 40 kB auf den ViewState.

Bild der Seiten- und Viewstategröße.

Diese 40 kB werden übrigens nicht nur zum Client heruntergeladen, sondern bei jedem PostBack auch wieder zurück zum Server geschickt. Sie belasten die Seite also doppelt!

Was war noch mal der ViewState?

Ehe wir den ViewState gleich eliminieren werden, möchte ich zuvor schnell erklären, was genau wir überhaupt versuchen los zu werden.

Der ViewState speichert Werte von ASP.NET Controls anhand ihres Namens. Er ist vergleichbar mit einer Hashtable. Diese Name/Wert Paare serialisiert er in ein verstecktes Formularfeld mit dem Namen __VIEWSTATE. Nach einem Postback wird das __VIEWSTATE Feld deserialisiert und der Viewstate somit wieder hergestellt.

In unserem Beispiel sieht das __VIEWSTATE Feld so aus:

Bild des Seitenquelltexts mit Viewstate. 

Der tatsächliche Viewstate der Seite ist um einiges größer! Die Abbildung zeigt nur einen Ausschnitt.

Der ViewState dient übrigens als Datenspeicher für die meisten Controleigenschaften. Die Eigenschaft "DataSource" der DropDownList könnte daher zum Beispiel wie folgt implementiert sein:

    1 public IEnumerable DataSource

    2 {

    3     get

    4     {

    5         return ViewState["DataSource"] as IEnumerable

    6     }

    7 

    8     set

    9     {

   10         ViewState["DataSource"] = value;

   11     }

   12 }

 

Also weg damit!

Um die übertragene Datenmenge seiner Seite zu reduzieren, schaltet Herr Brause also den ViewState der DropDownList ab. Da die DataSource der Liste nach einem PostBack nun nicht mehr automatisch gefüllt wird, entfernt er außerdem die Prüfung auf einen PostBack ehe er die DataSource der Liste füllt:

    1 protected void Page_Load(object sender, EventArgs e)

    2 {

    3     linesOfCodeDropDown.DataSource = GetLinesOfCodeListItems();

    4     linesOfCodeDropDown.DataBind();

    5 }

Die anschließende Prüfung ergibt, dass der ViewState nun nur noch 0,05 kB beträgt und die komplette Seite nun über 40 kB kleiner geworden ist.

Der ViewState beträgt nun nur noch 0.05 kB

Leider ergibt der zweite Test, dass die Seite nun nicht mehr funktioniert. Egal was der Anwender auswählt, die Seite gibt immer "0 Codezeilen" aus. Dies liegt daran, dass der "SelectedIndex" der Liste innerhalb des Button Click Events immer 0 ist.

Im folgenden Screenshot wurde zum Beispiel "5 Codezeilen" ausgewählt und anschließend auf Absenden geklickt.

Obwohl der Anwender den Wert 5 auswählte, wird nur eine 0 dargestellt.

Ist der ViewState also dafür zuständig, die Auswahl des Anwenders in das Servercontrol zu schreiben?

Nein!

Auch wenn diese Vermutung zunächst nahe liegt, ist sie vollkommen falsch. Um das Rätsel zu lösen, muss man sich kurz den ASP.NET Page Life Cycle vor Augen halten.

Dieser teilt sich in folgende Phasen auf:

  1. Page Request
  2. Start
  3. Page Initialization
  4. Load
  5. Validation
  6. Postback event handling
  7. Rendering
  8. Unload

Interessant für unsere Situation sind die Phasen PageInitialization und Load.

In der Phase PageInitialization werden die folgenden Methoden durchlaufen:

OnInit

Alle Controls der Seite sind bereits erstellt worden. Deklarativ in der ASPX Seite festgelegte Werte wurden zugewiesen. ViewState und durch den Anwender gepostete Werte wurden noch nicht in die Controleigenschaften geschrieben

OnInitComplete

Wird aufgerufen, nachdem das die Methode OnInit durchgelaufen ist.

OnPreLoad

Wird aufgerufen, ehe die Phase Load begonnen wird.

In der Phase Load werden unter anderem die folgenden Methoden durchlaufen

LoadViewState

In dieser Methode wird der ViewState zurück in die Seite und die Controls geschrieben. In unserem ursprünglichen Beispiel würde hier also die DataSource Eigenschaft der DropDownList gefüllt werden.

ProcessPostData

Diese Methode schreibt die durch den Benutzer geposteten Werte in die Controls. In unserem Beispiel wird hier also der selektierte Wert der DropDownList gesetzt.

PageLoad

Wenn diese Methode ausgeführt wird, ist die Seite vollständig initialisiert. In unserem Ursprünglichen Beispiel haben wir hier die DataSource der DropDownList gesetzt.

Die Events der einzelnen Controls, also zum Beispiel das Event Click unseres Buttons werden übrigens in einer späteren Phase, nämlich der Phase PostBack Event Handling ausgelöst.

Interessant, aber was hat es mit unserem Problem zu tun?

Auch wenn der kleine Exkurs in Richtung Page Life Cycle sicherlich (vielleicht ;-)) sehr interessant war, stellt sich die Frage, wo der Zusammenhang zu unserem Problem der fehlenden geposteten Daten besteht.

Nun, dies ist eigentlich ganz einfach. Ursprünglich lief unsere Seite wie folgt:

Erster Request:

Setzen der DataSource Eigenschaft in der Methode Page_Load.

PostBack

Automatisches Wiederherstellen der DataSource Eigenschaft in der Methode LoadViewState, anschließend automatisches Zuweisen des durch den Anwender ausgewählten Werts in ProcessPostData.

Durch unsere Änderungen, nämlich das Deaktivieren des ViewStates und der Entfernung der Prüfung auf einen PostBack in Page_Load haben wir nun folgendes Bewirkt:

Erster Request:

Setzen der DataSource Eigenschaft in der Methode Page_Load.

PostBack

Automatisches Zuweisen des durch den Anwender ausgewählten Werts in ProcessPostData. Da die DropDownListe jedoch noch keine DataSource hat, kann dies hier noch nicht funktionieren. Die DataSource wird erst anschließend in Page_Load gesetzt.

Und das bedeutet?

Das bedeutet, dass die "fehlenden" geposteten Werte eigentlich gar nichts mit dem deaktivierten ViewState zu tun hatten. Die Ursache ist einfach die Tatsache, dass wir die DataSource setzen, nachdem die Daten des Anwenders in das Control geschrieben wurden.

Und die Lösung?

Die Lösung ist recht einfach. Der Code, der die DataSource der DropDownList setzt muss einfach ausgeführt werden, ehe ProcessPostData durchlaufen wird. Typischerweise nimmt man dazu die Methode OnInit.

    1 protected override void OnInit(EventArgs e)

    2 {

    3     linesOfCodeDropDown.DataSource = GetLinesOfCodeListItems();

    4     linesOfCodeDropDown.DataBind();

    5 }

    6 

    7 protected void Page_Load(object sender, EventArgs e)

    8 {

    9 }

Fazit

Der ViewState einer DropDownList darf sehr wohl deaktiviert werden. Um das Problem der "fehlenden" geposteten Werte in den Griff zu bekommen, muss lediglich die Zuweisung der DataSource an einen früheren Zeitpunkt innerhalb des Page Life Cycle verschoben werden.

Wer mehr zu dem Thema wissen möchte, sollte sich übrigens in jedem Fall Dave Reeds Artikel Truly Understanding ViewState durchlesen. Eine umfangreichere und vor allem bessere Behandlung des Themas habe ich bisher an keiner anderen Stelle im Web gefunden.

Wer mehr über den ASP.NET Page Life Cycle wissen möchte, dem kann ich den Artikel Der Lebenszyklus einer ASP.NET 2.0 Seite empfehlen.

Tja und wer gar nichts mehr mit ViewState oder Page Life Cycle zu tun haben möchte, der sollte zum ASP.NET MVC Framework greifen;-)


Kick it on dotnet-kicks.de
 
1/22/2010 - 09:43 AM | Comments [3] | Categories: .NET | ASP.NET | DotNetGerman Bloggers
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Jeder der in einer ASP.NET Webforms Anwendung ein oder mehrere "Ajax-Enabled" Controls eines Komponentenherstellers nutzt kennt das Problem: Die Anzahl der zum Client übertragenen Scripte steigt explosionsartig an.

Dies liegt daran, dass die meisten Komponentenhersteller je Control ein eigenes Script zum Client senden. Generell ist das auch durchaus sinnvoll, denn nur so kann gewährleistet werden, dass der Browser nur die minimal benötigte Anzahl an Scriptcode vom Server herunterladen muss.

Wie der folgende Screenshot zeigt, wird dieser Segen jedoch auch schnell zum Fluch. Die Abbildung zeigt einen Mitschnitt des Netzwerktraffics beim Besuch einer Seite eines aktuellen Projekts. Inhalt der Seite sind ein Scriptmanager, zwei Infragistics Controls, einige ESRI WebADF Controls sowie ein paar eigene Servercontrols, die auch eigene Scripts rendern.

1_Vorher

Die Anzahl zu übertragenden Scripts beträgt laut Screenshot also 37 Stück.

Interessant, aber wo liegt nun das Problem?

Wir wissen zwar nun, dass wir 37 Scripte zum Client übertragen müssen, aber was ist daran so schlimm und warum sollte es uns überhaupt interessieren?

Um das Problem an der Situation zu erkennen, sollten wir uns kurz vor Auge führen, dass die Scripte nicht gemeinsam innerhalb des selben HTTP Requests wie die eigentliche Website übertragen werden. Stattdessen wird je Script (und auch Bild, CSS Stylesheet, etc.) eine eigene Anfrage an den Webserver gestellt.

Jede dieser Anfragen hat eine gewisse "Rüstzeit" oder auch Latenz. Damit ist die Zeitspanne gemeint, die zwischen dem Absetzen des Befehls, eine Anfrage zu starten und dem tatsächlichen Start der Anfrage vergeht. Je nach Entfernung zwischen Client und Server sowie der Qualität der Internetanbindung der beiden variiert der tatsächliche Wert dieser Reaktionszeit.

Und was bedeutet dies nun konkret?

Schauen wir uns zur Verdeutlichung ein kleines Beispiel an. Gehen wir von jemandem mit einer extrem schlechten Anbindung und einer Latenz von 500 ms aus (zugegeben, der Wert ist recht hoch, lässt sich aber schön einfach rechnen ;-))

Um unsere 37 Scriptdateien herunterzuladen, hätten wir nun also

37 Anfragen * 500 ms Latenz + 37 Antworten * 500 ms Latenz = 37 Sekunden.

Diese 37 Sekunden sind reine Wartezeit und kommen zur eigentlichen Übertragungszeit der Scripte hinzu. Nun ist es natürlich nicht so, dass sämtliche Anfragen sequentiell abgearbeitet werden. Allerdings sieht die Standardkonfiguration vieler Browser eine Beschränkung von zwei gleichzeitigen Verbindungen zu einem Hostnamen vor. Diese Anzahl resultiert aus einer Empfehlung der HTTP 1.1 Spezifikation. Heisst, wir müssen unsere 37 Sekunden noch durch zwei Teilen, da zwei gleichzeitige Verbindungen möglich sind. Bleiben immer noch 18,5 Sekunden übrig.

Was können wir dagegen tun?

Um dieses Problem, was tückischerweise während der Entwicklung nur selten auffällt, da die Latenz zwischen lokalem Webserver und lokalem Webbrowser nicht sonderlich hoch sein dürfte ;-), in den Griff zu bekommen, bietet ASP.NET ab der Version 3.51 das Element CompositeScript als Kindelement des Scriptmanagers an.

Dieses Element erlaubt es, einzelne Scripts anzugeben, welche anschließend automatisch zu einem Script zusammengefasst werden.

Das Ganze sieht ungefähr so aus:

<asp:ScriptManager runat="server" ID="sm1">
    <CompositeScript>
        <Scripts>
            <asp:ScriptReference Name="/Scripts/MeinScript1.js" />
            <asp:ScriptReference Name="/Scripts/MeinScript2.js" />
            <asp:ScriptReference Name="/Scrips/MeinScript3.js" />
        </Scripts>
    </CompositeScript>
</asp:ScriptManager>

Problematisch ist nur, dass man die URL der notwendigen Scripts nur für Dateibasierte Scripts kennt. Sobald ein Script aber als eingebettete Ressource über die Webresource.axd nach aussen gerendert wird, ist die URL unbekannt.

Um dieses Problem zu lösen, kann das auf Codeplex erhältliche Control ScriptReferenceProfiler genutzt werden. Wird es auf einer Seite eingesetzt, rendert es eine Liste aller genutzten Scripte heraus. Diese Liste kann 1:1 kopiert und in das Scripts Element des CompositeScript Elements eingefügt werden.

4_ScriptProfilerOutput

Die eingefügten Scripts werden anschließend serverseitig zu einem großen Script hinzugefügt und innerhalb einer einzigen Anfrage herunter geladen.

Nicht so schnell!

Der Versuch, alle 37 Scripte der zuvor genannten Seite in einem CompositeScript Element zusammen zu führen resultiert in folgendem Fehler:

5_ASP_Error

Die Ursache für diesen Fehler ist, dass der Scriptmanager jedes benötigte Script mit einem kryptischen Bezeichner an die URL anhängt. Dies sprengt schnell die maximale Begrenzung einer URL auf 1024 Zeichen.

Die Lösung

Um dieses Problem zu umgehen platziert man einfach mehrere CompositeScript Elemente auf der Seite und verteilt die Scripte auf diese. Da jedes CompositeScript Element einen eigenen Scriptmanager benötigt, nutzt man ab dem zweiten CompositeScript Element statt zusätzlichen Scriptmanagern, von denen es immer nur einen pro Seite geben darf einfach ScriptmanagerProxy Controls.

Die optimale Verteilung der Scripte auf die einzelnen CompositeScript Elemente ist schnell im Trial-and-Error-Verfahren gefunden. In meinem Beispiel sah sie übrigens wie folgt aus:

<asp:ScriptManagerProxy ID="ScriptManagerProxyAjax" runat="server">
    <CompositeScript>
        <Scripts>
            <asp:ScriptReference Name="MicrosoftAjax.js" />
            <asp:ScriptReference Name="MicrosoftAjaxWebForms.js" />
            <asp:ScriptReference Name="AjaxControlToolkit.Common.Common.js" Assembly="AjaxControlToolkit, Version=3.0.30512.20315, Culture=neutral, PublicKeyToken=28f01b0e84b6d53e" />
            <asp:ScriptReference Name="AjaxControlToolkit.Compat.Timer.Timer.js" Assembly="AjaxControlToolkit, Version=3.0.30512.20315, Culture=neutral, PublicKeyToken=28f01b0e84b6d53e" />
            <asp:ScriptReference Name="AjaxControlToolkit.Compat.DragDrop.DragDropScripts.js"                 Assembly="AjaxControlToolkit, Version=3.0.30512.20315, Culture=neutral, PublicKeyToken=28f01b0e84b6d53e" />
            <asp:ScriptReference Name="AjaxControlToolkit.Animation.Animations.js" Assembly="AjaxControlToolkit, Version=3.0.30512.20315, Culture=neutral, PublicKeyToken=28f01b0e84b6d53e" />
        </Scripts>
    </CompositeScript>
</asp:ScriptManagerProxy>

<asp:ScriptManagerProxy ID="ScriptManagerProxyInfragisticsCommon" runat="server">
    <CompositeScript>
        <Scripts>

...
        </Scripts>
    </CompositeScript>
</asp:ScriptManagerProxy>

...

Gerade wenn man sich mit einem Element nahe an der 1024 Zeichen Grenze befindet, sollte man jedoch im Hinterkopf halten, dass die tatsächliche Url der Seite später meist nicht mehr http://localhost ist, sondern eher http://meinesubdomain.meinedomain.de. Durch die Änderung des Hostnamens während der Produktivsetzung ändert sich also eventuell noch einmal die Länge der URL. Daher im Zweifelsfall lieber ein Script weniger als in der Entwicklungsumgebung möglich in die CompositeScript Tags einfügen.

Es müssen übrigens nicht alle Scripts in CompositeScript Tags gepackt werden. Solche, die nicht verpackt wurden, werden wie gehabt ganz normal weiter als einzelner Script Tag zum Browser herausgerendert.

Und was bringts nun?

Tja, was bringt der ganze Aufwand nun? Da Bilder bekanntlich mehr als Worte sagen, hier ein paar Vorher/Nachher Screenshots der zuvor vorgestellten Seite:

Initial hat die Seite 37 Scripts heruntergeladen.

 1_Vorher

Yahoo's YSlow gab der Seite eine Gesamtnote von E.

 2_Vorher_YSlowGrade

Die Ladezeit betrug knapp 15 s, die Gesamtgröße 734 kb.

 3_Vorher_YSlowSize

Nach den Änderungen wurden nur noch 9 Scripte referenziert.

 6_Firebug_Nachher

Die YSlow Benotung stieg immerhin von E auf D.

 7_Nachher_YSlowGrade

Die Ladezeit reduzierte sich auf knapp 5 s, die Gesamtgröße auf 655 kb.

8_Nachher_YSlowSize


Kick it on dotnet-kicks.de
 
1/14/2010 - 10:16 AM | Comments [0] | Categories: .NET | ASP.NET | DotNetGerman Bloggers | Tips und Tricks
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Wer in den letzten Jahren auch nur am Rande etwas mit der Entwicklung von Webapplikationen zu tun hatte, sollte wissen, dass der Einsatz von Tabellen zur Gestaltung des Layouts nicht mehr wirklich State-Of-The-Art sind. Schön und gut, aber wie sieht es mit Dateneingabefomularen aus? Diese sind häufig zweispaltig und mehrzeilig aufgebaut, so dass der Einsatz von Tabellen nicht komplett abwegig wäre und zumindest noch halbwegs sinnvoll erscheint.

Doch leider weit gefehlt. Jeder, der sich ein solches Eingabeformular von einem Screenreader vorlesen lässt, merkt schnell, dass auch hier Tabellen nicht das Mittel der Wahl sein sollten. Wie sieht nun aber die Alternative aus? In der Praxis stolpert man häufig über wüst zusammen geschusterte DIV Tags mit fixen Positionierungen, deren einziger Zweck in der Emulation von Tabellenzellen besteht. Dies mag zwar bereits ein Schritt in die richtige Richtung sein, aber wenn man ehrlich ist, wurde das Problem nur verlagert.

Weitaus interessanter finde ich Cameron Adams Ansatz, den ich seit einiger Zeit auch erfolgreich in einigen Projekten zum Einsatz gebracht habe. Wer also Eingabeformulare fürs Web erstellen muss, und keine Lust auf Tabellen hat, sollte sich auf jeden Fall diesen Link inklusive der verschiedenen Beispiele und der Quellcodes ansehen.


Kick it on dotnet-kicks.de
 
6/19/2009 - 02:19 PM | Comments [0] | Categories: .NET | ASP.NET | DotNetGerman Bloggers
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Seit über drei Jahren ist ASP.NET 2.0 nun schon in der Final Version auf dem Markt und bis Mitte letzter Woche habe ich sie übersehen: Die Eigenschaft AssociatedControlID des ASP.NET Label Controls. Bisher habe ich auf den Einsatz dieses Controls möglichst verzichtet, da es in meinen Augen "nur" einen unnötigen Span-Tag um meinen Text setzt.

Habe ich in meiner aspx Seite also folgenden Code:

<asp:Label ID="Label1" runat="server" Text="Ich bin ein Label"></asp:Label>
<asp:TextBox ID="TextBox1" runat="server">Ich bin eine Textbox</asp:TextBox><br />

wird dies zu folgendem Markup:

<span id="Label1">Ich bin ein Label</span>
<input name="TextBox1" type="text" value="Ich bin eine Textbox" id="TextBox1" /><br />

Alles in allem ist dies wenig spektakulär.

Erweitere ich meinen Code nun jedoch um die Eigenschaft AssociatedControlID

<asp:Label ID="Label2" runat="server" AssociatedControlID="TextBox2" Text="Ich bin auch ein Label"></asp:Label>
<asp:TextBox ID="TextBox2" runat="server">Ich bin auch eine Textbox</asp:TextBox></div>

wird das ASP.NET Label nicht mehr als Span-Tag, sondern als HTML Label-Tag gerendert:

<label for="TextBox2" id="Label2">Ich bin auch ein Label</label>
<input name="TextBox2" type="text" value="Ich bin auch eine Textbox" id="TextBox2" />

Schön und gut, aber wo liegt der Vorteil?

Zum einen schafft der HTML Code durch den Zusatz "for=<anderes Control>" eine logische Verknüpfung zwischen Eingabefeld und Beschriftung. Dies kann von Screenreadern ausgelesen werden und verbessert somit die Barrierefreiheit.

Dies ist jedoch nicht alles. Für den Standardbenutzer ergibt sich der Vorteil, dass er nun innerhalb des Browsers mit der Maus einfach auf das Label klicken kann und anschließend automatisch das assozierte Steuerlement den Fokus erhält.

Mein Fazit: ein tolles Feature, was sowohl Usability, als auch Barrierefreiheit mit sehr wenig Aufwand erhöht.

Näheres findet sich übrigens auch bei selfhtml.

Die Frage ist nur: Warum habe ich es so lange übersehen? Vielleicht liegt es daran, dass es unter ASP.NET 1.0 noch nicht existierte und die anderen Neuerungen einfach so viel aufregender waren?


Kick it on dotnet-kicks.de
 
4/30/2009 - 08:58 AM | Comments [0] | Categories: .NET | ASP.NET | DotNetGerman Bloggers
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Er ist ASP.NET MVP, er ist Co-Leader der Central-Jersey .NET User Group, für Microsoft hält Web-Casts direkt aus Redmond. Außerdem ist er der Leiter von Infragistics weltweiter Evangelism Gruppe. Er kommt direkt aus den USA und im November ist er bei uns!

Die .NET User Group Koblenz ist stolz, Anfang November mit Tony Lombardo, Lead Evangelist von Infragistics aus den USA, den ersten internationalen Sprecher zu Gast zu haben.
Tony wird über Line of Business Applications mit Silverlight 2.0 sprechen. Während des Vortrags wird er unter anderem auf alltägliche Aufgabenstellungen wie Datenbindung, Authentifizierung oder der Speicherung von Benutzereinstellungen zu sprechen kommen.

Über den Sprecher:

Tony Lombardo ist der leitende Evangelist von Infragistics Welt-Weiter Evangelisten Gruppe. Sein Spezialgebiet ist ASP.NET. Sein tiefer technischer ASP.NET Hintergrund sowie sein Community Engagement verschafften ihm die Auszeichnung zum Microsoft MVP für ASP.NET.
Tony ist Co-Leader der Central-Jersey .NET User Group. Zusätzlich schreibt er regelmäßig für online und print Medien wie zum Beispiel ASP.NET Pro, Dr. Dobb’s Journal, .NET Developers Journal und die Redmond Developer News.
Tony arbeitet seit fast einem Jahrzehnt für Infragistics. Diese Zeit startete er zunächst als Entwickler für ASP.NET, wurde anschließend Produkt Manager für die Infragistics ASP.NET Komponenten ehe er seine aktuelle Rolle als Lead-Evangelist einnahm. Bedingt durch seinen Werdegang verfügt er über "real-world" ASP.NET Erfahrung, die er stets mit freue teilt.
Der Vortrag wird ausnahmsweise in englischer Sprache abgehalten werden.

Anmeldung unter:
https://www.xing.com/app/events?op=detail;id=267741

English Version:

LOB 2.0 with Silverlight 2

This talk will cover building a next generation web application using Silverlight 2. In this talk we will discuss how to do some common tasks including databinding, authentication, and persistence of user defined settings.

Speaker: Tony Lombardo

In November we have Tony Lombardo visiting Europe from the USA. Tony is the lead Technical Evangelist in Infragistics' Worldwide Evangelism Group at Infragistics.
Tony has a deep technical background in ASP.NET and his community participation has earned him the ASP.NET MVP Award from Microsoft. Tony co-runs the Central Jersey .NET Users Group, and often writes for both online and printed publications, including ASP.NET Pro, Dr. Dobb’s Journal, .NET Developers Journal and Redmond Developer News.
In addition, Tony has been working for Infragistics for almost a decade, and in that time he spent his days working as a Developer on our ASP.NET product before becoming a Product Manager and eventually moving the role he has today. As such, Tony has first-hand experience in “real-world” issues and he will be very pleased to advise in a technical capacity on use of Infragistics controls.

Register at:
https://www.xing.com/app/events?op=detail;id=267741


Kick it on dotnet-kicks.de
 
10/27/2008 - 09:17 PM | Comments [0] | Categories: .NET | ASP.NET | DNUG Koblenz | DotNetGerman Bloggers | Infragistics | Silverlight
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Devin Rader, Produktmanager bei Infragistics für ASP.NET kündigte in seinem Blog eine neue CTP des Web User Interface Frameworks Aikido an.

Ziel des Aikido Frameworks ist - neben einer erheblichen Reduzierung der Client-Scripte im Vergleich zu den Standard NetAdvantage ASP.NET Controls - eine verbesserte Web Standard Unterstützung (CSS 2 + XHTML 1.0), ein einfacheres Objektmodell sowie die dadurch resultierende Performanceverbesserung.

Das Framework basiert auf Microsofts ASP.NET 2.0 AJAX Extensions 1.0.

Neu im März 2008 CTP sind:

  • WebDataGrid
  • WebDataTree
  • WebSlider
  • und die WebHierachicalDataSource.

Ob Infragistics die ehrgeizig gesteckten Ziele erreicht, kann jeder der möchte unter der Aikido Downloadseite selbst herausfinden.

Technorati tags: , ,

Technorati Profile


Kick it on dotnet-kicks.de
 
3/13/2008 - 09:20 AM | Comments [0] | Categories: Aikido Framework | ASP.NET | Infragistics
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)

Häufiges Problem z. B. auf Kundenservern:

Der Aufruf einer auf dem Server liegenden Website / WebService zeigt nur den Inhalt der ASPX Seite, statt diese auszuführen.

Grund: IIS wurde nach .NET Framework installiert. Daher sind die ASP.NET ISAPI Filter nicht im IIS registriert und er hat somit keine Ahnung, was er mit einer ASpx bzw asmx Seite anfangen soll

  1. Lösung (Sehr aufwendig)
    Format c:
    Neu installieren :-)
  2. Lösung (Auch nicht wirklich schnell)
    .NET Framework deinstallieren
    .NET Framework wieder installieren
    hoffen, dass alles klappt ..
  3. Lösung (mein Favorit :-))
    Start->Ausführen->"aspnet_regiis -i"->OK
    ----> Alles läuft :-)
Technorati tags: ,

Kick it on dotnet-kicks.de
 
11/2/2007 - 09:12 AM | Comments [0] | Categories: .NET | ASP.NET
© Andre Kraemer | RSS/Subscribe Feed your aggregator (RSS 2.0)