BigBasti's Blog About Me & my Digital Lifestyle

25Jan/124

ASP.NET MVC: HTML5 Elemente mit jQuery Fallback nutzen

Soo, nachdem ich die Überschrift mit all den hippen Begriffen vollgepackt habe die es in Sachen Web momentan so gibt möchte ich euch kurz erklären worum es in diesem Artikel gehen soll.

Ihr werdet sicher mitbekommen haben, dass HTML5 auf dem Vormarsch ist und auch wenn es immer noch nicht offiziell fertig ist findet man immer häufiger den HTML5-Header <!DOCTYPE html> auf diversen Seiten.

Eins der meiner Meinung nach nützlichsten Features die wir mit HTML5 erhalten sind die neuen Input-Typen. Denn bisher hatten wir keine Wahl und mussten type="text" benutzen!

Die Eingabefelder vom Typ Text konnten natürlich alles enthalten, sind aber nicht gerade Benutzerfreundlich wenn man etwas komplexere Daten eingeben soll wie zum Beispiel ein Datum.

In HTML5 wurden deswegen spezifische Typen für solche immer wieder einkehrende Eingaben eingeführt. Eine von ihnen ist &quot;Date&quot;:


Birthday: <input type="date" name="bday" />

Der große Vorteil hier ist, dass die Browser nun eine eigene Implementierung für diesen Typ von Eingabefeldern machen können. Falls der Browser nun also diesen neuen Eingabe Typ unterstützt blendet er automatisch einem (mehr oder weniger) schönen Date-Picker ein in dem man bequem das Datum wählen kann:

Ansicht des neuen Typen in verschiedenen Browsern

Ein weiterer großer Vorteil ist, dass auch die mobilen Browser wie z.B. auf dem iPhone diese neuen Elemente ebenfalls unterstützen und einen passenden Picker einblenden:

Date-Picker auf dem iPhone

Wie ihr in dem ersten Screenshot sehen könnt unterstützt der FireFox (zumindest zum Zeitpunkt des Screenshots) den neuen Typ nicht. In diesem Fall blendet er ein gewöhnliches Textfeld ein wo wir nun gezwungen sind das Datum auf die gewohnte (unbequeme) Weise einzutragen.

Übrigens, ich glaube ich muss euch nicht sagen warum der Internet Explorer hier nicht mit aufgeführt ist oder? ;-)

Und genau das ist das Problem um das es in diesem Blogpost geht. Wenn wir die tollen neuen Elemente nutzen gehen wir das Risiko ein, dass Nutzer die einen älteren Browser nutzen nicht in den Genuss einer bequemen Eingabe kommen.

jQuery Date-Picker

Wir könnten auch komplett auf das HTML5 Element verzichten und direkt alles mit jQuery machen, welches auch einen Date-Picker bietet, aber das ist auch nicht das gelbe vom Ei, denn dann wären die Mobilen Nutzer in Nachteil, da dort die Browser diese Elemente für gewöhnlich unterstützen und eine gewohnte Oberfläche für die Eingabe bieten.

Die Optimale Lösung ist offensichtlich eine Mischung beider Welten. Wenn der Benutzer die Seite mit einem kompatiblen Browser aufruft soll der Date-Picker des Browsers genutzt werden, wenn der Browser aber veraltet ist soll stattdessen der jQuery Fallback greifen und der JavaScript Date-Picker (siehe links) genutzt werden.

Zu unserem Glück macht uns ASP.NET MVC die Umsetzung dieses Plans sehr leicht, da es an den nötigen Stellen sehr einfach erweitert werden kann.

Schauen wir uns mal ein kleines Beispiel an. Wir haben eine Klasse Person, und wollen auch dessen Geburtstag speichern, hier wollen wir die tolle neue Funktionalität nutzen. Implementieren wir das Ganze jedoch erstmal wie gewohnt:


    public class Person
    {
        public int ID { get; set; }
        public String Name { get; set; }
        public DateTime Geburtstag { get; set; }
    }

Wenn wir die Model-Klasse haben können wir das MVC Scaffolding nutzen um für uns eine View zum Erstellen neuer Personen anzulegen:

MVC Scaffolding kommt uns zu Hilfe

Schauen wir uns nun doch mal an was da für uns tolles generiert wurde:


    <div class="editor-field">
        @Html.EditorFor(model => model.Geburtstag)
        @Html.ValidationMessageFor(model => model.Geburtstag)
    </div>

Auffällig hierbei ist, dass hier @Html.EditorFor genutzt wird und nicht etwa @Html.TextBoxFor! Der Hintergrund ist der, dass MVC bei der Generierung versuchen wird ein passendes Element für den angegebenen Datentyp (DateTime) zu bestimmen, aber da MVC standardmäßig nur den Typ Text kennt wird hier eigentlich immer ein Input-Element vom Typ text generiert.

Hier kommt uns die Erweiterbarkeit von MVC zu gute, denn wir können ganz einfach NuGet nutzen um uns die nötige Funktionalität zu verschaffen. Wir installieren das Packet MvcHtml5Templates.

Installation über NuGet

Neue Templates

Nachdem NuGet alles erledigt hat werdet ihr feststellen, dass ihr ein paar neue Dateien in eurem Views/Shared Verzeichnis habt (siehe Bild links). Und wie ihr schon an dem Namen erkennen könnt sind das die neuen Input-Typen aus HTML5.

Diese Templates machen im Grunde nichts anderes als die MVC-Eigenen zu überschreiben und versehen die mit dem passendem Type-Attribut.

Hier sind euch keine Grenzen gesetzt ihr könnt natürlich auch eire eigenen Templates definieren mit euren eigenen Typen. (Auch wenn man das eher selten benötigt)

Das Tolle: mehr müssen wir nicht machen. MVC wird nun zur Laufzeit statt einem Text ein Date Input-Element für uns anlegen. Beachtet, dass das nur geht wenn wir in der View die allgemeine Funktion @Html.EditorFor nutzen und keinen Spezifischen Typ angeben.

Schauen wir mal in den Sourcecode der zur Laufzeit für das Geburtstagsfeld generiert wird:


<input class="text-box single-line"
data-val="true"
data-val-required="Das Feld 'Geburtstag' ist erforderlich"
id="Geburtstag"
name="Geburtstag"
type="datetime"
value=""; />

Wie ihr seht wird nun der Korrekte Typ, nämlich datetime verwendet. Wenn wir die Seite nun also mit einem kompatiblen Browser aufrufen können wir ganz bequem das Datum wählen.

Leider ist der Opera Browser momentan wohl der einzige Desktop Browser der uns hier einen Benutzerfreundlichen Dialog einblendet (siehe erstes Bild oben), somit müssen wir dafür sorgen, dass die Benutzer mit anderen oder alten Browsern nicht benachteiligt werden.

JavaScript Magie

Auch für diese Problematik bringt MVC bereits alles mit was nötig ist sie zu lösen. Werfen wir doch mal einen Blick in unseren Scripts Ordner finden wir alle nötigen jQuery und jQuery UI Skripte, dazu kommt noch die modernizr Bibliothek die uns auch zu Gute kommen wird.

Die jQuery Bibliotheken liefern und die nötige Funktionalität die wir benötigen um diesen hübschen Date-Picker einzublenden.

Die modernizr Bibliothek dagegen hilft uns herauszufinden ob der Browser, den der Benutzer momentan verwendet die gewünschten HTML5 Features unterstützt.

Nun müssen wir also bei unseren Formularen prüfen, ob der Browser die nötigen Funktionen kennt und bei Bedarf die jQuery Klassen einbinden. Das Ganze ist dank der Einfachheit von Modernizr und jQuery ziemlich simpel.

Wechseln wir zu unserer View und fügen die Referenzen auf die nötigen Skripte und CSS Dateien ein:


<script src="@Url.Content("~/Scripts/jquery-1.5.1.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery-ui-1.8.11.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/modernizr-1.7.js")" type="text/javascript"></script>
<link href="@Url.Content("~/Content/themes/base/jquery.ui.all.css")" rel="stylesheet" type="text/css" />

Unten in der View fügen wir nun eine Modernizr Abfrage ein und prüfen, ob der Browser das DateTime-Feld unterstützt, und wenn nicht lassen wir jQuery das Ganze für uns regeln:


<script type="text/javascript">
    $(function () {
        if (!Modernizr.inputtypes.date) {
            $("input[type='datetime']").datepicker();
        }
    });
</script>

Mit dem Befehl $("input[type='datetime']").datepicker(); werden alle Input-Felder vom Typ DateTime durch den jQuery Picker ersetzt. Coole Sache!

Unser Browser kennt den Typ nicht :(

Unser Browser kennt den Typ nicht :(

Kleiner Hinweis am Rande: Das ist nicht wirklich best practice da die Skripte auch geladen werden wenn sie gar nicht benötigt werden (der Browser kennt das HTML5 Feld) ich habe das hier der Einfachheit geopfert.

Und das wars auch schon! Alle Browser die das Feld unterstützen blenden nun ihren Picker ein, ansonsten wird der jQuery Fallback genutzt.

Wir müssen uns auch nicht mit irgendwelchen UserAgent-Prüfungen herumschlagen und lassen das alles Modernizr erledigen, der das intern über JavaScript prüft und somit immer aktuell ist. Das heißt dass wenn irgendwann der IE plötzlich die neuen Typen kennt werden diese auch funktionieren!

Genauso könnt ihr auch bei all den anderen neuen HTML5 Typen vorgehen und euren Usern ein bestmögliches Bedienerlebnis bereiten ;-)

kick it on dotnet-kicks.de

17Jan/122

Konsolenausgaben in einer WindowsForms Applikation

Manchmal will man eine WindowsForms Anwendung auch über die Konsole aufrufen und dann möchte man natürlich auch Ausgaben der Anwendung in dem Konsolenfesnter sehen. Doch leider ist das nicht so einfach, da alle Aufrufe über die Console-Klasse werden ignoriert.

Doch warum ist das so? Das ist ganz einfach und liegt daran, dass das Konsolenfenster, das unser Programm startet zu einem anderem Prozess gehört, nämlich cmd.exe und somit nicht verbunden ist.

Was wir also tun müssen ist, unsere Ausgabe an das Konsolenfenster zu heften, das unsere Anwendung gestartet hat. Dafür kann man die Win32-Methode AttachConsole nutzen.

Diese ist sehr einfach aufgebaut und sollte vor der ersten Ausgabe an die Konsole aufgerufen werden:

Definition des Win32-Aufrufs: (Beispiele in VB.NET)


    <DllImport("kernel32.dll")> _
    Private Shared Function AttachConsole(dwProcessId As Integer) As Boolean
    End Function

Um die Ausgabe an den Prozess zu geben der uns aufgerufen hat (Parent-Process) kann man hier als Wert -1 übergeben:


    Private Const ATTACH_PARENT_PROCESS As Integer = -1

Was nun noch bleibt ist es die Methode aufzurufen, zum Beispiel in der Main-Methode der Applikation:


    Private Sub frmMain_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        AttachConsole(ATTACH_PARENT_PROCESS)
        //[...]

Das wars auch schon, nun werden eure Ausgaben an das Konsolenfester umgeleitet. Um die Konsole wieder von eurem Prozess zu lösen kann die paremeterlose Methode FreeConsole genutzt werden.

Es lohnt sich hier natürlich dann auch eine Unterscheidung zu machen wie die App gestartet wurde, also ob über Konsole oder über Doppelklick wie immer. Wenn sie über die Konsole gestartet wird, sollte man vielleicht die Oberfläche nicht mehr anzeigen. Das kann man dann zB. daran erkennen, dass Environment.GetCommandLineArgs.Length > 1 ist.

Probleme

Leider gibts ein paar Nebeneffekte die nicht unbeachtlich sind.

Ein Problem ist, dass Konsolenfuntionen die die Ausgabe von Programmen umleiten zB. so: "myapp.exe -test >test.txt" nicht funktionieren, die Ausgabe erfolgt weiterhin auf der Konsole. Hierfür konnte ich leider keine Lösung finden, wenn euch was einfällt einfach kommentieren.

Das zweite ist nur eine Schönheitssache, nämlich wenn das Programm beendet wird wird erst nicht die gewohnte Konsoleneingabezeile angezeigt, sondern nur der blinkende Cursor, so dass es aussieht als ob das Programm noch laufen würde, was es aber nicht mehr tut. (Wie gesagt Schönheitssache) Das kann man zB. dadurch umgehen, dass man vor dem Beenden des Programms noch ein [Enter] auf der Konsole ausgibt.

Weitere Infos und auch eine kleine Diskussion gibts hier und hier.

5Jan/120

ASP.NET MVC: Zugriff auf Dateien einschraenken

Oft kommt es vor dass man bestimmte Inhalte nicht der breiten Öffentlichkeit präsentieren möchte. So will man beispielsweise bestimmte Inhalte nur autorisierten Benutzern oder nur Admins zur Verfügung stellen, andere User die nicht eingeloggt sind sollen diese Inhalte nicht abrufen können. Ein weiterer Anwendungsfall wäre auch hotlinking zu verhindern.

Dieses Verhalten kann man in ASP.NET MVC über verschiedene Wege erreichen. Eine beliebte Möglichkeit ist es

die Dateien über einen Controller zu verteilen.

Hierbei erstellt man einen Controller der eine Action hat in der die gewünschte Datei geladen und an den Browser zurückgegeben wird. Hier hat man dann die Bequemlichkeit, dass man problemlos prüfen kann ob der User eingeloggt ist und über die nötigen Rechte für das Bild verfügt. Das geht zum Beispiel mit dem Authorized-Attribut für die Actions.

Implementiert würde so eine Action dann wohl ca so aussehen:


public ActionResult Image(string id)
{
    var dir = Server.MapPath("/Private/Images");
    var path = Path.Combine(dir, id + ".jpg");
    return base.File(path, "image/jpeg");
}

Das Bild kann man dann so abrufen:


<img src="@Html.Content("~/Content/Image/" + model.pictureID)" />

Dieser Ansatz funktioniert problemlos hat aber den kleinen Nachteil, dass er nicht so performant ist wie der direkte Zugriff auf die Ressource.

Eine weitere (und auch meine bevorzugte) Möglichkeit eine solche Zugriffskontrolle zu implementieren ist es durch

einen eigenen RouteHandler.

Eine Beispielimplementierung wie man auf diese Weise Hotlinking verhindern kann habe ich in Mike Brinds Blog gefunden.

Hier sind im Grunde drei kleine Schritte nötig um einen eigenen RouteHandler zum Laufen zu bekommen. (Code von Mikes Blog)

Zunächst müssen wir die RouteHandler Klasse anlegen, die das IRouteHandler Interface und dessen einzige Methode GetHttpHandler implementiert:


public class ImageRouteHandler : IRouteHandler
{
  public IHttpHandler GetHttpHandler(RequestContext requestContext)
  {
    return new ImageHandler(requestContext);
  }
}

Als nächstes erstellen wir die oben genutzte ImageHandler Klasse in der wir das das IHttpHandler Interface implementieren:


public class ImageHandler : IHttpHandler
{
  public ImageHandler(RequestContext context)
  {
    ProcessRequest(context);
  }

  private static void ProcessRequest(RequestContext requestContext)
  {
    var response = requestContext.HttpContext.Response;
    var request = requestContext.HttpContext.Request;
    var server = requestContext.HttpContext.Server;

    var validRequestFile = requestContext.RouteData.Values["filename"].ToString();
    const string invalidRequestFile = "thief.gif";
    var path = server.MapPath("~/graphics/");

    response.Clear();
    response.ContentType = GetContentType(request.Url.ToString());

    if (request.ServerVariables["HTTP_REFERER"] != null &&
        request.ServerVariables["HTTP_REFERER"].Contains("yourdomain.com"))
    {
      response.TransmitFile(path + validRequestFile);
    }
    else
    {
      response.TransmitFile(path + invalidRequestFile);
    }
    response.End();
  }

  private static string GetContentType(string url)
  {
    switch (Path.GetExtension(url))
    {
      case ".gif":
        return "Image/gif";
      case ".jpg":
        return "Image/jpeg";
      case ".png":
        return "Image/png";
      default:
        break;
    }
    return null;
  }

  public bool IsReusable
  {
    get { return false; }
  }
}

Zu Schluss müssen wir diesen neuen Handler mit einer Route in der Global.asax registrieren:


routes.Add("ImagesRoute",
                 new Route("images/{filename}", new ImageRouteHandler()));

Hierbei ist noch wichtig, dass ihr die Route an der passenden Stelle registriert, sodass diese nicht ungewollt von einer anderen Route blockiert wird weil diese auch auf dieses Pattern matcht.

Nun werden alle Requests die auf die Route "images/{filename}" gehen und von einer anderen Domain stammen als eurer eigenen auf eine andere Grafik umgeleitet.

Nun bleibt noch ein kleines Problem, denn wenn das Verzeichnis mit den Bildern in eurem Content Verzeichnis liegt kann immer noch darauf verlinkt werden wenn man den vollen Pfad nimmt, also statt dem virtuellem Pfad

http://localhost/images/myimg.jpg

die URL benutzt unter der tatsächlich alle Bilder liegen:

http://localhost/Content/graphics/myimg.jpg

Dies liegt daran, dass der neue RouteHandler in diesem Fall nicht greift und die Default Route genutzt wird. Am einfachsten kann man dies verhindern wenn man den Zugriff für Unbefugte über die web.config sperrt:


  <location path="Content/graphics">
    <system.web>
      <authorization>
        <allow roles="admin" />
        <deny users="*" />
      </authorization>
    </system.web>
  </location>

Das war dann auch schon, ich nutze gern den zweiten Weg, da dieser mir einfacher und sauberer erscheint. Wenn euch noch andere Möglichkeiten einfallen wie man das anstellen kann dann immer her damit!

13Dez/110

SSL Teil 4: Serverseitige Authentifizierung mit Java

Nachdem wir in den letzen Teilen mehr die Theorie durchgenommen haben, möchte ich jetzt zu dem praktischen Teil kommen und euch zeigen wie man diese Theorie in Code umsetzen kann am Beispiel Java. (.NET wird auch noch folgen)

Wir wollen einfach einsteigen und implementieren erst mal nur die eine Serverseitige Authentifizierung, also muss der Server sich uns gegenüber ausweisen und wir sind in der Pflicht sein Zertifikat anzunehmen oder abzulehnen.

Wie in Teil 1 erklärt benötigen wir also zunächst einen Ort wo wir die Zertifikate sichern, denen wir vertrauen. Hier können wir (da wir in Java unterwegs sind) nicht die Windows-eigenen nehmen sondern müssen die so genannten Keystores von Java Nutzen.

Ein Keystore, wie der Name schon sagt ist nichts anderes als ein Speicher für Zertifikate. Wenn ihr Java installiert dann legt Java bereits einen Keystore für euch an, diesen findet ihr dann in eurem Java Verzeichnis Java\jre6\lib\security\cacerts die Datei „cacerts“ enthält hier bereits ähnlich wie bei Windows bereits die CA-Zertifikate von den großen Zertifizierungsstellen.

Es wir desweiterhin in Java unterschieden zwischen  Keystore und Truststore, beides sind im Grunde Keystores und unterscheiden sich nur im Namen und in ihrem Inhalt. Eine einzige Datei kann auch beide beinhalten, ist aber eher unüblich, da man die eigenen (privaten) Zertifikate von den CA-Zertifikaten separieren will.

Zuständigkeiten

So enthalten die Keystores für gewöhnlich die eigenen Privaten Schlüssel und die eigenen von anderen CAs signierte Zertifikate, die bei Verbindungen als Serverzertifikate (Public Keys) genutzt werden. Der Truststore ist das Gegenstück und enthält die Zertifikate denen man vertraut. So müssen wir die Zertifikate die wir vom Server beim Verbindungsaufbau erhalten gegen einen Truststore prüfen.

Fangen wir mit unserem Code an, wir wollen einen Client erstellen, der eine Socket-basierte Verbindung zu einem Server über SSL aufbaut und das vom Server zurückgegebene Zertifikat prüft.

Java ist auf diesem Gebiet sehr entgegenkommend und bietet für fast alle Anwendungsfälle fertige APIs und Klassen die uns fast die ganze Arbeit abnehmen.

Um den Prozess besser aufzeigen zu können schreiben wir uns eine Eigene SSLSocketFactory die für uns den Aufbau der Verbindung übernimmt und unserem Programm im Prinzip ein fertiges Socket liefert.

Dazu erstellen wir eine neue Klasse und lassen diese von SSLSocketFactory erben. Da das eine abstrakte Klasse ist müssen eir einige Methoden implementieren. Wenn das erledigt ist sieht unsere Klasse schon so aus:


/**
 * Stellt eine SSLSocketFactory bereit die einen angegebenen TrustStore nutzt
 * @author Sebastian Gross
 */
public class ServerSSLAuthSocketFactory extends SSLSocketFactory{

    @Override
    public String[] getDefaultCipherSuites() {

    }

    @Override
    public String[] getSupportedCipherSuites() {

    }

    @Override
    public Socket createSocket(Socket socket, String string,
            int i, boolean bln) throws IOException {

    }

    @Override
    public Socket createSocket(String destinationAddress,
            int destinationPort) throws IOException, UnknownHostException {

    }

    @Override
    public Socket createSocket(String string, int i,
            InetAddress ia, int i1) throws IOException, UnknownHostException {

    }

    @Override
    public Socket createSocket(InetAddress ia, int i) throws IOException {

    }

    @Override
    public Socket createSocket(InetAddress ia, int i,
            InetAddress ia1, int i1) throws IOException {

    }
}

Nun müssen wir diesen Methoden Leben einhauchen, aber keine Angst das ist nicht so schlimm wie es aussieht, denn die einzige Änderung die wir machen müssen ist es der SSLSocktFactory beizubringen mit unserem Truststore zu arbeiten, der Rest der Funktionalität soll unberührt bleiben.

Fügen wir also einen Konstruktor zu unserer Klasse hinzu der das Initialisieren unserer Internen SSLSocketFactory mit unserem Truststore übernimmt.


/** Delegate {@link SSLSocketFactory}. */
private transient SSLSocketFactory factory;

private File caFile;
private KeyStore ks;

private SSLContext context;
private TrustManagerFactory tmf;
private X509TrustManager defTrustManager;
private SavingTrustManager tm;

/**
 * Neue Truststore-basierte SSL-SocketFactory
 * @param truststoreLocation Vollqualifizierter Pfad zum Truststore oder
			leer lassen für Java-Default-Truststore
 * @param truststorePass Passwort für den Truststore. Wenn leer wird
			"changeit" ala Passwort benutzt.
 */
public ServerSSLAuthSocketFactory(String truststoreLocation,
								  String truststorePass){
	super();

	//Wenn ein Pfad zum TrustStore angegeben wurde diesen benutzen
	if(!truststoreLocation.isEmpty()){
		caFile = new File(truststoreLocation);
	}else{
		//Pfad zum default Java TrustStore finden
		caFile = new File("cacerts");
		if (!caFile.exists() || !caFile.isFile()) {
			char SEP = File.separatorChar;
			File dir = new File(System.getProperty("java.home") + SEP
					+ "lib" + SEP + "security");
			caFile = new File(dir, "cacerts");
			if (!caFile.exists() || !caFile.isFile()) {
				caFile = new File(dir, "cacerts");
			}
		}
	}

	//Prüfen, ob ein TrustStore Password angegeben wurde
	String truststorePassword = truststorePass.isEmpty() ? "changeit" : truststorePass;

	//TrustStore öffnen
	try {
		InputStream in = new FileInputStream(caFile);
		ks = KeyStore.getInstance(KeyStore.getDefaultType());
		ks.load(in, truststorePassword.toCharArray());
		in.close();
	} catch (KeyStoreException ex) {
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	} catch (NoSuchAlgorithmException ex){
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	} catch (CertificateException ex) {
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	} catch (FileNotFoundException ex) {
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	}catch (IOException ex) {
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	}

	try {
		context = SSLContext.getInstance("TLS");
		tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
		tmf.init(ks);
		defTrustManager = (X509TrustManager)tmf.getTrustManagers()[0];
		context.init(null, new TrustManager[] {defTrustManager}, null);
		factory = context.getSocketFactory();
	} catch (NoSuchAlgorithmException ex) {
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	} catch (KeyStoreException ex) {
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	} catch (KeyManagementException ex) {
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	}

}

Das sieht auf den ersten Blick recht komplex aus, ist aber im Grunde recht simpel.

  • In den Zeilen 23 - 28 prüfen wir ob der Konstruktor einen Pfad zu einem Truststore übergeben bekommen hat, und wenn dies nicht der Fall ist, wird der Pfad zu dem Standard-Java-Truststore in eurem JRE-Verzeichnis ermittelt.
  • Direkt danach in Zeile 41 prüfen wir ob ein Passwort übergeben wurde. Wenn keins übergeben wurde wird "changeit" genutzt. "changeit" ist das Defaultpasswort für den Standard-Java-Truststore.
  • In Zeile 45 - 48 öffnen wir den Keystore und laden dessen Inhalt in unsere Kokale KeyStore Variable.
  • Das Wichtigste geschieht nun in Zeile 67 - 72 hier holen wir uns einen Verweis auf den SSLContext (67) und erstellen uns mit Hilfe unseres KeyStores einene TrustManagerFactory(68-69). Anschließend lassen wir die TrustManagerFactory für uns einen neuen X509TrustManager "produzieren" der dann den Verweis auf unseren eigenen Keystore trägt. (70)
  • Nun sagen wir dem SSLContext, dass es unseren Trustmanager nutzen soll (71) für die darüber laufende Verbindungen und erstellen uns eine SSLSocketFactory (72)

Nun haben wir intern eine SocketFactory, die unseren eigenen (über den Konstruktor angegebenen) Keystore nutzt. Der Rest der Klasse die wir erstellt haben dient hierbei eigentlich nur als Wrapper und Durchreiche, der Wichtigste Teil war der Konstruktor.

Die restlichen Methoden können nun also alle Anfragen direkt an die produzierte SSLSocketFactory weiterleiten. So sieht dann die fertige Klasse aus:


import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

/**
 * Stellt eine SSLSocketFactory bereit die einen angegebenen TrustStore nutzt
 * @author Sebastian Gross
 */
public class ServerSSLAuthSocketFactory extends SSLSocketFactory{

    /** Delegate {@link SSLSocketFactory}. */
    private transient SSLSocketFactory factory;

    private File caFile;
    private KeyStore ks;

    private SSLContext context;
    private TrustManagerFactory tmf;
    private X509TrustManager defTrustManager;
    private SavingTrustManager tm;

    /**
     * Neue Truststore-basierte SSL-SocketFactory
     * @param truststoreLocation Vollqualifizierter Pfad zum
	 *			Truststore oder leer lassen für Java-Default-Truststore
     * @param truststorePass Passwort für den Truststore.
	 *			Wenn leer wird "changeit" ala Passwort benutzt.
     */
    public ServerSSLAuthSocketFactory(String truststoreLocation,
				String truststorePass){
        super();

        //Wenn ein Pfad zum TrustStore angegeben wurde diesen benutzen
        if(!truststoreLocation.isEmpty()){
            caFile = new File(truststoreLocation);
        }else{
            //Pfad zum default Java TrustStore finden
            caFile = new File("cacerts");
            if (!caFile.exists() || !caFile.isFile()) {
                char SEP = File.separatorChar;
                File dir = new File(System.getProperty("java.home") + SEP
                        + "lib" + SEP + "security");
                caFile = new File(dir, "cacerts");
                if (!caFile.exists() || !caFile.isFile()) {
                    caFile = new File(dir, "cacerts");
                }
            }
        }

        //Prüfen, ob ein TrustStore Password angegeben wurde
        String truststorePassword =
				truststorePass.isEmpty() ? "changeit" : truststorePass;

        //TrustStore öffnen
        try {
            InputStream in = new FileInputStream(caFile);
            ks = KeyStore.getInstance(KeyStore.getDefaultType());
            ks.load(in, truststorePassword.toCharArray());
            in.close();
        } catch (KeyStoreException ex) {
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        } catch (NoSuchAlgorithmException ex){
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        } catch (CertificateException ex) {
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        } catch (FileNotFoundException ex) {
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        }catch (IOException ex) {
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        }

        try {
            context = SSLContext.getInstance("TLS");
            tmf = TrustManagerFactory.getInstance(TrustManagerFactory
													.getDefaultAlgorithm());
            tmf.init(ks);
            defTrustManager = (X509TrustManager)tmf.getTrustManagers()[0];
            context.init(null, new TrustManager[] {defTrustManager}, null);
            factory = context.getSocketFactory();
        } catch (NoSuchAlgorithmException ex) {
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        } catch (KeyStoreException ex) {
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        } catch (KeyManagementException ex) {
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        }

    }

    @Override
    public String[] getDefaultCipherSuites() {
        return factory.getDefaultCipherSuites();
    }

    @Override
    public String[] getSupportedCipherSuites() {
        return factory.getSupportedCipherSuites();
    }

    @Override
    public Socket createSocket(Socket socket, String string,
					int i, boolean bln) throws IOException {
        return factory.createSocket(socket, string, i, bln);
    }

    @Override
    public Socket createSocket(String destinationAddress,
					int destinationPort) throws IOException, UnknownHostException {
        return (SSLSocket)factory.createSocket(destinationAddress, destinationPort);
    }

    @Override
    public Socket createSocket(String string, int i,
					InetAddress ia, int i1) throws IOException, UnknownHostException {
        return factory.createSocket(string, i, ia, i1);
    }

    @Override
    public Socket createSocket(InetAddress ia, int i) throws IOException {
        return factory.createSocket(ia, i);
    }

    @Override
    public Socket createSocket(InetAddress ia, int i,
					InetAddress ia1, int i1) throws IOException {
        return factory.createSocket(ia, i, ia1, i1);
    }
}

Da wir nun eine Factory haben die uns Sockets liefert, die auf unseren Truststore zugreift, können wir diese nun nutzen um eine Verbindung zu SSL Servern aufzubauen. Das Schöne hierbei ist, dass Java an sehr vielen Stellen SocketFactories nutzt, so können wir fast in allen Szenarien unsere Klasse einsetzen.

So können wir unsere SSLFactory nun ganz simpel testen:


public static void main (String [] args) throws IOException{

    int port = 443; // default https port
    String host = "httpsurl";

    try {
      SSLSocketFactory factory
       = (SSLSocketFactory) new ServerSSLAuthSocketFactory("","");

      SSLSocket socket = (SSLSocket) factory.createSocket(host, port);

      // enable all the suites
      String[] supported = socket.getSupportedCipherSuites();
      socket.setEnabledCipherSuites(supported);

      Writer out = new OutputStreamWriter(socket.getOutputStream());
      // https requires the full URL in the GET line
      out.write("GET https://" + host + "/ HTTP/1.1\r\n");
      out.write("Host: " + host + "\r\n");
      out.write("\r\n");
      out.flush();

      // read response
      BufferedReader in = new BufferedReader(
                    new InputStreamReader(socket.getInputStream()));

      // read the header
      String s;
      while (!(s = in.readLine()).equals("")) {
          System.out.println(s);
      }
      System.out.println();

      // read the length
      String contentLength = in.readLine();
      int length = Integer.MAX_VALUE;
      try {
        length = Integer.parseInt(contentLength.trim(), 16);
      }
      catch (NumberFormatException ex) {
        // This server doesn't send the content-length
        // in the first line of the response body
      }
      System.out.println(contentLength);

      int c;
      int i = 0;
      while ((c = in.read()) != -1 && i++ < length) {
        System.out.write(c);
      }

      System.out.println();
      out.close();
      in.close();
      socket.close();

    }
    catch (IOException ex) {
      System.err.println(ex);
    }
}

Tragt in Zeile 4 einfach den gewünschten SSL-Host ein wie z.B. Google oder Facebook und startet das kleine Programm. Falls ihr keine Parameter beim Erzeugen der Factory (Zeile 7 und 8) angegeben habt solltet ihr auch keine Probleme beim Verbinden haben und wenn das Programm durchgelaufen ist seht ihr die Antwort des Servers im HTML Format.

Falls ihr doch einen eigenen Truststore eingetragen habt müsst ihr sicherstellen, dass dieser den Public Key des Servers zu dem ihr euch verbinden wollt beinhaltet, sonst wird die Verbindung nicht erfolgen.

Teil 1: Serverseitige Authentifizierung und Zertifikate
Teil 2: Beidseitige Authentifizierung
Teil 3: Der SSL Handshake
Teil 4: Serverseitige Authentifizierung mit Java

29Sep/111

Eine TreeView durchsuchen und den gefundenen Pfad aufklappen

Da ich heute wieder eine Mail mit der Frage bekommen habe wie man ein Item in einer TreeView finden kann und dieses dann anzeigen kann, dachte ich ich poste es gleich hier falls es noch jemand braucht.

Das Ganze funktioniert sehr einfach mit nur einer kleinen rekursiven Funktion:


   ''' <summary>
    ''' Diese Funktion durchläuft den ganzen Treenode bis ein Eintrag gefunden wird der dem Suchbegriff
    ''' gleicht. Danach wird der Pfad der zu diesem TreeNode führt (zur Visualisierung) aufgeklappt
    ''' </summary>
    ''' <param name="tn">Der zu durchsuchende Treenode
    ''' <param name="name">Der Suchbegriff
    Public Sub findNode(ByVal tn As TreeNode, ByVal name As String)
        For Each tnn As TreeNode In tn.Nodes
            If tnn.Text = name Then
                Dim t As TreeNode = tnn
                For i As Integer = tnn.Level To i = 0 Step -1
                    Try
                        t = t.Parent
                        t.Expand()
                    Catch ex As Exception
                    End Try
                Next
                tnn.Parent.Expand()
                Exit Sub
            End If
            findNode(tnn, name)
        Next
    End Sub

Hier laufen wir alle Nodes der TreeView durch bis wir auf einen Node stoßen der unserem Suchbegriff entspricht. Ab hier durchlaufen wir den ganzen Weg rückwärts und klappen alle übergeordneten Nodes auf, sodass wir unseren gesuchten Node zu Gesicht bekommen.

Diese Funktion kann man auch ganz gut als Extension Method umschreiben, sodass diese bequemer aufgerufen werden kann, aber das ist dann natürlich jedem selbst überlassen.

Demoapp in Aktion

Demoapp in Aktion

Hier noch ein Demoprojekt zum testen: Download [VS 2010]

17Sep/110

Nice2Know: Attribut-basierende Routen in ASP.NET MVC

Für Routen ist in ASP.NET MVC für gewöhnlich die Global.asax zuständig, so werden hier alle Routen definiert und gemappt. Doch wenn man viele verschiedene Routen nutzen will wird es schnell voll in der eigentlich sehr übersichtlichen Klasse.

Da wäre es doch eigentlich cool wenn wir die Routen direct in unseren Controllern definieren könnten. Genau das können wir mit der Erweiterung AttributeRouting machen.

Installation

Installation über NuGet

Dank NuGet ist die Installation in wenigen Sekunden abgeschlossen und wir können unsere Actions mit Routing-Attributen dekorieren.

Die Attribute sind hierbei sehr einfach gestaltet und bieten uns alles was wir benötigen.


    public class DemoController : Controller
    {
        [GET("Ist/Das/Cool/Oder/Was")]
        public ActionResult Start()
        {
            return View();
        }
    }

Nun muss man nur noch die Attribut-basierten Routen registrieren:


    public static void RegisterRoutes(RouteCollection routes) {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapAttributeRoutes(); //--- das wars!

        routes.MapRoute(
            "Default", // Route name
            "{controller}/{action}/{id}", // URL with parameters
            new { controller = "Home", action = "Start",
                id = UrlParameter.Optional } // Parameter defaults
        );
    }

Und fertig! Nun können wir die Seite starten und unsere Route testen:

Das Ergebnis

Das Ergebnis

Die Routen die wir definieren können, beschränken sich nicht darauf, so können diese auch problemlos parametrisiert werden, inklusive Defaultparameter:


    public class DemoController : Controller
    {
        [GET("Artikel/{artikelid}/{version?}")]
        [RouteDefault("artikelid", "120")]
        public ActionResult Start(int artikelid, string version)
        {
            ViewBag.ArtikelId = artikelid;
            ViewBag.Version = version;

            return View();
        }
    }

Hier wird der Parameter artikelid mit einem Defaultwert ausgestattet und der zweite optional gemacht. Das Ergebnis kann sich sehen lasen:

Läuft!

Läuft!

Hier hört das Framework nicht auf, so könnt ihr auch Regular Expressions in die Routen integrieren und auch mehrere Routen an eine Action heften. Die Möglichkeiten sind grenzenlos ;)

Für weitere Infos schaut einfach auf der Projekthomepage vorbei, es lohnt sich!

17Sep/112

Nice2Know: Errorlogging mit ELMAH

Errorlogging? Ist das nicht dieses Zeugs das die ganzen Java Programmierer und Linux Fuzies nutzen werdet ihr euch jetzt denken? Stimmt, aber auch in der .NET Welt ist es sehr nützlich ein Log zu führen und hier gibt es einige Tools für uns Entwickler die für uns diese unschöne Arbeit deutlich ersparen.

Das Tool dass ich euch heute vorstellen möchte ist ELMAH, was für Error Logging Modules and Handlers steht.

Was kann ELMAH? Hier ein Ausschnitt von der Hompage:

  • Logging of nearly all unhandled exceptions.
  • A web page to remotely view the entire log of recoded exceptions.
  • A web page to remotely view the full details of any one logged exception, including colored stack traces.
  • In many cases, you can review the original yellow screen of death that ASP.NET generated for a given exception, even with customErrorsmode turned off.
  • An e-mail notification of each error at the time it occurs.
  • An RSS feed of the last 15 errors from the log

 

Zusammengefasst gesagt schnappt ELMAH sich jede Exception die ihr nicht abgefangen habt und loggt diese für euch im Speicher oder in einer Datenbank und fasst diese auch noch schön zusammen auf einer Übersichtswebseite, in einem RSS Feed oder schickt euch diese direkt per Mail zu.

So könnt ihr jeder Zeit nach der Exception schauen was schiefgelaufen ist und euch den kompletten Stacktrace anschauen und (fast) jeden Yellow Screen of Death nachstellen.

Was mir an diesem Tool besonders gefällt ist die sehr einfach Integration in ein Projekt. Alles was ihr machen müsst ist es dafür zu sorgen dass die DLL in eurem bin-Verzeichnis und einen Handler in eure web.config einzutragen. Nun werden alle eure unbehandelten Exceptions geloggt.

Und es geht sogar noch einfacher, wenn ihr NuGet nutzt werden euch sogar diese Schritte abgenommen, einfach NuGet anwerfen, nach ELMAH suchen und installieren - fertig.

Das Schöne an NuGet ist, dass es nicht nur die Bibliothek herunterlädt sondern auch alle nötigen Einträge für uns anpasst, so werdet ihr nun auch ein paar neue Zeilen in eurer web.config entdecken:

Diese Arbeit blieb uns zum Glück erspart ;) Da nun eigentlich schon alles konfiguriert ist können wir fröhlich ein paar Fehler erzeugen:

Ich habe hier also eine URL aufgerufen für die kein Controller und keine View bestimmt werden konnte. Dass wir hier so “viele” Informationen angezeigt bekommen liegt daran, dass wir die Seite lokal aufrufen, würden wir das ganze von einem anderem System machen würden uns noch weniger Informationen zur Verfügung stehen.

Nun wollen wir mal gucken, was ELMAH für uns aufgezeichnet hat, das können wir machen, in dem wir diese Seite aufrufen: /elmah.axd

Alle aufgetretenen Exceptions für uns übersichtlich dargestellt. Wenn Bedarf besteht kann man natürlich auch tiefer bohren und die Details aufklappen:

Hier hat man nun den vollen Stacktrace und viele weitere Informationen wie z.B. die Servervariablen. Eine sehr coole Sache wie ich finde, da ihr nun auch von unterwegs eure Fehler untersuchen könnt.

Falls ihr nicht wollt, dass jeder eure Exceptions sehen kann ist dies natürlich auch möglich.

UPDATE: Ich habe hier die normale Version von ELMAH genutzt, für ein MVC Projekt würde ich aber ELMAH.MVC empfehlen (Ebenfalls auf NuGet verfügbar). Dieses integriert sich besser in eine MVC Umgebung da es einen AdminController anlegt und somit über /Admin/Elmah erreichbar ist.

ELMAH gibt es übrigens schon eine halbe Ewigkeit aber ich (Langschläfer) bin erst vor ein paar Wochen drüber gestolpert. Wie schaut es bei euch aus? Nutzt ihr ELMAH?

30Aug/112

Probleme mit Plesk Admin und MySQL

Es gibt Tage da zickt Plesk herum und man könnte verzweifeln weil plötzlich die Adminoberfläche, die gestern noch funktionierte, nun nicht mehr laden will und Plesk sich mit der Fehlermeldung verabschiedet:

Error: Test connection to the database server has failed because of network problems:
Failed to connect to database: Access denied for user 'admin'@'localhost' (using password: YES)

Lösung:

Zum Glück lässt sich das Problem ganz einfach aus der Welt schaffen, dazu muss man das Admin Passwort für MySQL neu setzen.

  1. Loggt euch auf dem Server über RemoteDesktop (oder SSH) ein
  2. Öffnet die my.ini aus eurem Plesk Verzeichnis ( %plesk_dir%databases\mysql\data )
  3. fügt den Parameter "add the parameter skip-grant-tables=1" hinzu und speichert.
  4. Startet den MySQL Service neu
  5. Verbindet euch zu der MySQL Datenbank ( "%plesk_dir%\mysql\bin\mysql.exe" mysql )
  6. Führt folgenden Befehl aus:
  7. mysql> update user set password=password('euer_passwort') where user='admin'; Wobei ihr hier euer Admin-Passwort eintragen solltet dass Plesk nutzt um sich anzumelden
  8. Entfernt den Parameter aus der my.ini den ihr im Schritt 3. hinzugefügt habt
  9. Startet dem MySQL Dienst erneu neu

Das Problem sollte sich nun erledigt haben.

8Aug/110

Anonyme Threaduebergreifende Eventhandler

Jeder von uns uns hatte schon mit Events und den dazugehörigen Handlern zu tun. Jeder Button auf einem Formular hat z.B. einen eigenen Eventhandler, der irgendwo in der Form.Designer.cs definiert und registriert wird. Dieser sieht dann meist ca so aus:


//Registrierung
this.button1.Click += new System.EventHandler(this.button1_Click);
//Verweis / Nutzung
private void button1_Click(object sender, EventArgs e) { }

So wird jedes Mal wenn des Click-Event der Button-Instanz ausgelöst wird die registrierte Methode button1_Click Ausgeführt. Eigentlich ein schönes und einfaches System, aber wenn man eigene Klassen hat und teilweise viele Instanzen davon bildet ist es unschön, da dabei sehr viele Methoden in der Klasse entstehen und somit  die Übersicht mit der Zeit verloren geht.

Deswegen gibt es für uns die Möglichkeit so genannte anonyme Methoden als EventHandler zu verwenden. Diese benötigen keine Deklaration und haben keine Referenz, somit gehen diese auch verloren, sobald diese zu Ende gelaufen sind. Benutzen kann man diese so:


this.button1.Click += delegate(object sender, EventArgs e) {
    //hier steht mein Code
};

Wie man sieht ist der Code um einiges übersichtlicher geworden und kürzer geworden. Wir übergeben nun eine anonyme Methode als Handler.

Was das ganze noch etwas interessanter macht, ist dass es noch mehr Ersparnis und noch mehr Übersicht bei Threadübergreifenden EventHandlern gibt. Schauen wir uns mal an wie es normalerweise ausschaut:


//Registrierung des EventHandler zum anderen Thread
man.NewInformationUpdate += new NewInformationUpdateArrived(update_received);

//Handlermethode
public void update_received(object sender, TestInformationUpdateEventArgs ev) {
    //Aufruf zu dem eigenen Thread deligieren
    this.BeginInvoke(new delegateupdate(update), new object[] { ev });
}

//Definition der delegaten Methode
public delegate void delegateupdate(TestInformationUpdateEventArgs ev);

//Eigentliche Methode zum Verarbeiten des Events
private void update(TestInformationUpdateEventArgs msg) {
//...
}

Wir haben nun drei Methoden definiert, nur um ein Event zu behandeln, dies ist von Nöten, da wir hier Events verarbeiten, die aus einem anderen Thread kommen.

Hier können wir ebenfalls viel einsparen wenn wir mit delegates und anonymen Methoden arbeiten:


fd1.CompletionInformation += delegate(object se, CompletionInformationArgs ci) {
    this.BeginInvoke(new EventHandler(delegate(object obj, EventArgs ea) {
        CompletionInformationArgs cea = (CompletionInformationArgs)ea;

        //Unser verabeitungs Code hier

    }), new object[] { se, ci });
};

Hier haben wir wieder aus drei Methoden eine gemacht, naja eine richtige zumindest ;) und zwei anonyme. Ich finde diese Variante deutlich lesbarer und übersichtlicher, da alle wichtigen Information an einer Stelle sind.

Doch Vorsicht, diese Methode hat einen gravierenden Nachteil, da wir hier anonyme Methoden benutzen haben wir keine Referenz darauf und können somit das registrierte Event nie wieder un-registrieren. Mit anderen Worten, man muss abwegen wann man diese Abkürzung gehen kann und wann nicht.

Meiner Erfahrung nach wird der Fall überwiegen wo man diesen Weg leider nicht gehen kann, aber dann kann man versuchen ein Zwischenweg zu finden:


    var handler = new EventHandler(delegate(object o, EventArgs e)
    {
        //handler Logik
    };

    Subject.NewEvent += handler;

    //Diesen können wir später dann auch entfernen
    Subject.NewEvent -= handler;

Auf diese Weise haben wir immernoch eine Ersparnis und behalten die volle Kontrolle über unsere Handler.

Als kleine (unschöne) Alternative können wir auch anonyme Handler loswerden, indem wir alle Handler entfernen:


        public event EventHandler SomeEvent;

        public void ClearEventHandlers() {
            Delegate[] delegates = SomeEvent.GetInvocationList();
            foreach (Delegate delegate in delegates) {
                SomeEvent -= (EventHandler) delegate;
            }
        }

10Mai/111

Benutzerregistrierung mit ASP.NET MVC und jQuery Teil 2

Im ersten Teil haben wir die Ajax-Funktionalität mit Hilfe von jQuery “zu Fuß” implementiert und haben nur die einfachsten Mittel verwendet. In diesem zweiten Teil wollen wir das nun mit Hilfe des ASP.NET MVC3 Frameworks implementieren, welches uns einiges an Arbeit abnimmt.

Das schöne an der dritten Version des MVC Frameworks ist, dass Microsoft hier schon einige helfende Elemente zum Validieren von Clientangaben eingebaut hat, die wir oft mit nur einer kleinen Zeile Code oder einem Attribut aktivieren können.

Fangen wir also mit unserem Model an und erweitern es um die neuen Attribute die uns das Framework zur Verfügung stellt:


public class RegisterModel2 {

    [Required(AllowEmptyStrings = false,
                ErrorMessage = "Bitte geben Sie einen Benutzernamen an!")]
    [DisplayName("Benutzername")]
    [Remote("ValidateUsername", "Validation")]
    public String UserName { get; set; }

    [Required(AllowEmptyStrings = false,
                ErrorMessage = "Bitte geben Sie eine Emailadresse an!")]
    [DisplayName("E-Mail Adresse")]
    [Remote("ValidateEmail", "Validation")]
    public String UserMail { get; set; }

    [Required(AllowEmptyStrings = false,
                ErrorMessage = "Bitte geben Sie ein Passwort an!")]
    [DisplayName("Passwort")]
    [DataType(DataType.EmailAddress)]
    [Remote("ValidatePassword", "Validation")]
    public String UserPass { get; set; }

    [Required(AllowEmptyStrings = false,
                ErrorMessage = "Bitte wiederholen Sie ihr Passwort!")]
    [Compare("UserPass", ErrorMessage="Die Passwörter stimmen nicht überein!")]
    [DisplayName("Passwort wiederholen")]
    public String UserPass2 { get; set; }
}

 

Wie man sieht sind nun einige Attribute dazugekommen, die Eigenschaften der Klasse an sich haben sich aber nicht verändert! Aber mal langsam, was bedeuten diese Attribute eigentlich?

  • Required
    • Markiert die Eigenschaft als Pflichtfeld. Durch setzen dieses Attributs wird beim Absenden des Formulars automatisch geprüft ob diesem Feld ein Wert zugewiesen wurde, falls nicht wird eine Fehlermeldung generiert.
    • Durch die Option AllowEmptyStrings = false sorgen wir dafür, dass auch Werte die nur als Leerzeichen bestehen ebenfalls Fehler auslösen.
    • Mit dem Setzen der ErrorMessage Eigenschaft können wir eine Eigene Fehlermeldung definieren, die statt der Standardmeldung angezeigt wird. Lässt man diese Eigenschaft weg wird eine Meldung generiert die ca. so ausschaut: “Das Feld "FELDNAME" ist erforderlich.”
  • DisplayName
    • Dieses Attribut haben wir auch schon in dem Letzten Teil benutzt. Dieses sorgt dafür dass wir den Namen für das Eingabefeld, dass der Benutzer später im Browser zu sehen bekommt bereits im Model definieren können.
  • Remote
    • Durch dieses Attribut definieren wir welche Methode und welcher Controller für das Validieren der Eigenschaft verantwortlich ist. Erst wird die Action und als zweiter Parameter der Controller angegeben. Wenn der Benutzer später Eingaben in das Textfeld tätigt, wird diese Action aufgerufen und der eingegebene Wert als Parameter übergeben, sehr ähnlich wie wir es in Teil 1 gemacht haben, aber später mehr dazu.
  • DataType
    • Damit kann man den Inhalt bestimmen was einem beim Validieren helfen kann.
  • Compare
    • Mit diesem Attribut können wir angeben, dass diese Eigenschaft den gleich Wert haben muss wie eine andere. Dies wird dann automatisch von jQuery überprüft, somit müssen wir nicht mehr machen als dieses Attribut zu setzen.
    • Als Parameter übergeben wir den Namen der Eigenschaft mit der der Wert verglichen werden soll und, als zweiten setzen wir eine eigene Fehlermeldung damit eine vernünftige Fehlermeldung angezeigt wird.

Damit haben wir unser Model fertiggestellt, nun schauen wir uns an was wir an unserer View verändern müssen.

Die gute Nachricht ist, dass wir den kompletten JavaScript/jQuery Teil streichen können, da das für uns alles automatisch geregelt wird, so sieht nun unsere View aus:

 


    @using (Html.BeginForm("Submit", "Validation")) {
        <table border="1">
            <tr><td>@Html.LabelFor(model => model.UserName):</td>
                <td>@Html.TextBoxFor(model => model.UserName)</td>
                <td>@Html.ValidationMessageFor(model => model.UserName)</td>
            </tr>
            <tr><td>@Html.LabelFor(model => model.UserMail):</td>
                <td>@Html.TextBoxFor(model => model.UserMail)</td>
                <td>@Html.ValidationMessageFor(model => model.UserMail)</td>
            </tr>
            <tr><td>@Html.LabelFor(model => model.UserPass):</td>
                <td>@Html.TextBoxFor(model => model.UserPass)</td>
                <td>@Html.ValidationMessageFor(model => model.UserPass)</td>
            </tr>
            <tr><td>@Html.LabelFor(model => model.UserPass2):</td>
                <td>@Html.TextBoxFor(model => model.UserPass2)</td>
                <td>@Html.ValidationMessageFor(model => model.UserPass2)</td>
            </tr>
            <tr><td> </td>
                <td><input id="submit_form_button" type="submit"
                            value="Registrierung abschicken" /></td>
                <td> </td>
            </tr>
        </table>
    }

 

Wenn ihr euch den Code anschaut, werdet ihr feststellen, dass sich eigentlich nicht getan hat. Das Einzige was sich verändert hat ist dass nun das Span in dem wir im letzten Teil die Fehler ausgegeben haben verschwunden ist und stattdessen

@Html.ValidationMessageFor(model => model.xxxxx)

aufgetaucht. Wie man sich auch schon von dem Namen ableiten kann wird hier eine Validierungsnachricht angezeigt, also mit anderen Worten die Fehlermeldung die bei der Validierung dieses Feldes entsteht. Diese Fehlermeldungen haben wir zum Teil schon über die Attribute im Model definiert.

Das wars auch schon mit Änderungen. Smiley super oder? Wir haben nun einen Haufen Code rausgeworfen, ein paar Attribute eingefügt und nicht eine Zeile JavaScript geschrieben - perfekt Zwinkerndes Smiley

Aber noch sind wir nicht ganz fertig, denn wir haben nun gar nicht die Regeln für die Validierung des Benutzernamen, der Emailadresse und des Passworts implementiert.

Wenn ihr aufgepasst habt werdet ihr euch daran erinnern, dass wir das Attribut [Remote] verwendet haben und dort als Parameter den Namen einer Action angaben, an die die Informationen zur Validierung gesendet werden sollten.

Diese Actions müssen wir nun natürlich in unserem Validation Controller implementieren, zu unserem Glück unterscheiden sich diese Methoden nur minimal von denen die wir bereits im letzten Teil erstellt haben:


public ActionResult ValidateUsername(string UserName) {
    //Wenn in unserer "Datenbank" der Name bei einem anderen Benutzer Auftaucht
    if (new UserDatabase().Users.Where(u => u.Username.Equals(UserName)).ToList().Count > 0) {
        //Eine Fehlermeldung zurückgeben
        return Json("Dieser Benutzername ist schon vergeben", JsonRequestBehavior.AllowGet);
    }

    //Wenn der Benutzername kürzer als 5 Zeichen ist ebenfalls Fehler ausgeben
    if (UserName == null || UserName.Length < 5) {
        //return "Der Benutzername ist zu Kurz!";
        return Json("Der Benutzername ist zu kurz!", JsonRequestBehavior.AllowGet);
    }

    //Falls es nichts zu meckern gibt geben wir true zurück
    return Json(true, JsonRequestBehavior.AllowGet);
}

 

Was ist passiert? Die Logik der Funktion ist gleich geblieben, lediglich der Rückgabetüp und die Rückgabemethode hat sich geändert. Wir geben nun ein ActionResult statt eines einfachen Strings zurück. Dazu rufen wir die Methode Json auf der wir als Parameter die Daten übergeben die übertragen werden sollen und als zweites erlauben wir GET Aufrufe.

Die Antwort die dadurch erzeugt wird wird automatisch auch in ein Format umgewandelt die jQuery versteht. da wir in diesem Fall aber nur ein Boolean oder einen String übergeben ist dies nicht nötig.

Wenn wir einen String ausgeben wird das als Ausgabe für die Fehlermeldung genutzt. Alternativ können wir auch ein false ausgeben, dann wird die Standardfehlermeldung angezeigt, die nicht allzu schön ist. Durch die Angabe von true sagen teilen wir jQuery auf der Clientseite mit, dass kein Fehler aufgetreten ist und dass es die eventuell angezeigte Fehler ausblenden soll.

Die anderen zwei Validierungsmethoden müssen genauso geändert werden um problemlos mit dem [Remote] Attribut genutzt zu werden wodurch wir den Vorteil erhalten, dass wir kein JavaScript Code schreiben müssen, alles wird von den Frameworks automatisch geregelt.

An dieser Stelle lohnt es sich mal anzuschauen was da eigentlich generiert wird für uns. Starten wir also unsere MVC Applikation und gucken mal in den Quelltext:


<td><label for="UserName">Benutzername</label>:</td>
<td><input data-val="true"
		data-val-remote="&amp;amp;amp;#39;Benutzername&amp;amp;amp;#39; is invalid."
		data-val-remote-additionalfields="*.UserName"
		data-val-remote-url="/Validation/ValidateUsername"
		data-val-required="Das Feld &amp;amp;amp;quot;Benutzername&amp;amp;amp;quot; ist erforderlich."
		id="UserName" name="UserName"
		type="text"
		value="" />
</td>
<td><span class="field-validation-valid"
		data-valmsg-for="UserName"
		data-valmsg-replace="true"></span>
</td>

Oben ist der Ausschnitt zu sehen, der für das Benutzername-Eingabefeld verantwortlich ist inklusive dem dazugehörigen Label und dem Validierungstext.

Auffällig ist, dass kein JavaScript generiert wurde, sondern das Input-Element mit zusätzlichen Tags versehen wurde, die aber nur für jQuery von Bedeutung sind. Andere Applikationen oder Frameworks werden dadurch also nicht beeinflusst, diese ignorieren einfach diese für sie unbekannte Attribute.

Wenn man die Attribute etwas genauer betrachtet wird man einiges aus unserem Model wiederfinden, wie zum Beispiel unsere dort definierte Fehlermeldung.

Nun bleibt nur noch eine Kleinigkeit zu tun, nämlich das nötige CSS zu schreiben dass mit der jQuery Validierung zusammenspielt und die nötigen JavaScript Dateien zu importieren.

Fangen wir mit dem Importieren der JavaScript Bibliotheken an. Wenn ihr ein neues MVC3 Projekt anlegt habt ihr schon alle Dateien die nötig sind bereits in eurem Scriptordner. Öffnet die “_Layout.cshtml” und fügt (falls noch nicht vorhanden) folgende Imports hinzu:


<script src="@Url.Content("~/Scripts/jquery-1.4.1.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

Diese Imports werden nun durch diese Mastervorlage in jede View eingebunden. Natürlich könnt ihr diese Imports auch explizit nur in die View schreiben in der diese auch benötigt werden, das wäre sogar vorteilhafter denke ich.

Zu guter Letzt kümmern wir uns um das CSS. Auch hier generiert euch Visual Studio das nötige CSS wenn ihr ein neues MVC Projekt anlegt, auch wenn ihr als Vorlage ein leeres MVC Projekt wählt. In dem Ordner Content werdet ihr eine Datei namens Site.css finden in der die CSS Klassen für die Validierung stehen:


/* Styles for validation helpers
-----------------------------------------------------------*/
.field-validation-error
{
    color: #ff0000;
}

.field-validation-valid
{
    display: none;
}

.input-validation-error
{
    border: 1px solid #ff0000;
    background-color: #ffeeee;
}

.validation-summary-errors
{
    font-weight: bold;
    color: #ff0000;
}

.validation-summary-valid
{
    display: none;
}

jQuery greift auf diese Klassen zurück um das Aussehen der Eingabefelder entsprechend dem Validierungsergebnis anzupassen. Hier könnt ihr nun eure Eigenen Änderungen vornehmen und die Visualisierung euren Wünschen anpassen.

Ich habe hier noch, um das Beispiel aus Teil 1 nicht aus den Augen zu verlieren die CSS Klasse etwas erweitert um das Aussehen anzugleichen:


.input-validation-error
{
    border: 1px solid #ff0000;
    background-color: #ffeeee;
    background-image: url("../Grafik/cancel.png");
    background-position: right;
    background-repeat: no-repeat;
}

Wenn man die Applikation nun startet funktioniert das Beispiel dem aus Teil 1 sehr ähnlich:

image

Damit haben wir es eigentlich schon geschafft. Zum Schluss noch ein kleiner Hinweis. Natürlich ist dieser Weg um einiges einfacher, aber es gibt auch einen kleinen Haken, nämlich dass das Framework begrenzt ist. Das bedeutet, dass man nicht alles darüber schnell und ohne selber Code zu schreiben lösen kann, viele Anforderungen müssen immer noch von Hand oder mit Hilfe von anderen Frameworks implementiert werden.

Das erweiterte Demoprojekt aus Teil 1 gibts hier: Download [VS2010 c#]

<< Teil 1

kick it on dotnet-kicks.de

 

Get Adobe Flash playerPlugin by wpburn.com wordpress themes