code it

Martins Tech Blog

HtmlHelper und Interfaces

MVC-Views an ein Modell zu binden ist eigentlich ganz einfach: Man definiert eine Klasse, und typisiert die View mit Hilfe von @model. Das Ganze funktioniert super mit konkreten Typen und auch mit Interfaces:

@using EmployeeApplication.Models
@model Employee

Name:  @Html.DisplayFor(m => m.LastName), @Html.DisplayFor(m => m.FirstName)
Salary: @Html.DisplayFor(m => m.Salary)

Mein Beispiel ist recht einfach umrissen: Es gibt eine Klasse Employee, die einen Mitarbeiter eines Unternehmens definiert, mit typischen Eigenschaften wie Name und Jahresgehalt. Mit Hilfe der Html-Helper-Methode DisplayFor werden die Daten des jeweiligen Mitarbeiters angezeigt.

So weit alles recht unproblematisch.

Nicht immer möchte man auf konkrete Typen binden. Aus diesem Grund ändere ich mein Modell etwas um: Es wird das neue Interface IPerson definiert. Dieses definiert die Eigenschaften einer Person - also Vorname und Nachname. Davon abgeleitet wird nun noch das Interface IEmployee definiert, das IPerson um die Eigenschaft Jahresgehalt erweitert.

Mehr...

Asynchrone Controller in MVC3

Die Zeiten in denen Anwender bereit waren, lange Wartezeiten in Kauf zu nehmen sind vorbei. Hat man eine Seite, die schwer zu beschaffende Informationen beinhaltet, so ist der Standardweg, dem Anwender zunächst schon einmal eine Seite auszuliefern, die dann mit Hilfe von asynchronen Requests (AJAX) die restlichen Seitenkomponenten nachlädt und im DOM ersetzt. Mit den aktuellen Frameworks und Bibliotheken ist das alles recht unproblematisch und schnell implementiert. Und so ist man auch in den meisten Fällen mit der Kombination aus synchronen Controller-Actions, Partial Views und AJAX recht gut bedient.

In bestimmten Fällen kann es aber auch sinnvoll sein, serverseitig asynchrone Prozesse zu verwenden. Auch wenn alles recht einfach implementiert ist (wie ich gleich zeigen werde), so ist im Vorab die Sinnhaftigkeit zu hinterfragen. Die Umstellung auf asynchrone Aktionen bringt nur dann etwas, wenn beispielsweise unabhängig voneinander mehrere Backend-Services abgefragt werden und die Backend-Systeme auch in der Lage sind, diese Anfragen parallel zu beantworten. In diesem Fall würde im Vergleich zur sequentiellen Abarbeitung in der sich die Ausführungszeit aus der Summe der Subrequests zusammensetzt die parallele Abarbeitung nur etwas länger als der längste Request dauern.

Das Vorgehen ist nicht kompliziert: Zunächst ändert man die Basisklasse des Controllers. Statt von Controller zu erben, verwendet man nun AsyncController.

public class HomeController : AsyncController
{
     // todo: add some views
}

Auch mit dieser Basisklasse sind nach wie vor synchrone Anfragen möglich - es wird ledigleich zusätzlich die Möglichkeit gegeben, serverseitig asynchrone zu arbeiten.

Der nächste Schritt betrifft die Action. Synchrone Controller-Actions bestehen aus einer Methode, die normalerweise den Namen der View haben und als Rückgabeparameter ein ActionResult haben. Das folgende Beispiel würde die View Index ausliefern.

public ActionResult Index()
{
    // do something
    return View();
}

Im Fall der asynchronen Controller-Action muss diese Methode gesplittet werden: Die erste Methode (im Beispiel IndexAsync) nimmt den Request entgegen und startet die asynchronen Hintergrundprozesse. Diese Methode hat keinen Rückgabewert. Dieser ist erst in der zweiten Methode vorhanden (im Beispiel IndexCompleted). Hier erfolgt die Verarbeitung der Rückgabewerte der Hintergrund-Requests und die Auslieferung der View. Wie häufig im ASP.NET MVC, so ist die konventionsgemäße Benennung der Methoden wichtig, damit die Magie funktioniert.

Wichtig ist, dass man nicht vergessen darf, dass man die Abarbeitung in den Thread-Pool übergibt und es dort ggf. einige Kontext-Objekte nicht mehr gibt und dass man die Kontrolle in einem statischen Kontext zurück bekommt. Zur Unterstützung der Abarbeitung steht im AsyncController ein AsyncManager zur Verfügung. Mit dessen Eigenschaft OutstandingOperations definiert man, wie viele offene Hintergrundprozesse es gibt. Wann immer ein Subrequest ein Ergebnis zurückliefert, schreibt man das Ergebnis in die Parameters-Auflistung des AsyncManagers zurück und verringert die OutstandingOperations.

Mein komplettes Beispiel sieht nun wie folgt aus:

public class HomeController : AsyncController
{

    public void IndexAsync()
    {
        AsyncManager.OutstandingOperations.Increment();
        var webRequest = WebRequest.Create(
                "http://api.twitter.com/1/statuses/user_timeline.xml"
                + "?screen_name=martin_hey");


        Observable
        .FromAsyncPattern<WebResponse>(webRequest.BeginGetResponse, 
            webRequest.EndGetResponse)()
        .Subscribe(response =>
                        {
                            var streamReader = new StreamReader(
                                response.GetResponseStream());
                            var responseText = streamReader.ReadToEnd();
                            AsyncManager.Parameters["TweetXml"] = responseText;
                            AsyncManager.OutstandingOperations.Decrement();
                        });
    }



    public ActionResult IndexCompleted(string tweetXml)
    {
        var model = new TweetViewModel
                        {
                            TimelineXml = tweetXml
                        };
        return View(model);
    }

}

In IndexAsync erfolgt ein Webrequest auf ein Drittsystem - im echten Leben wären hier mehrere Requests. Im der Callback-Action dieses Requests schreibe ich das erhaltene Ergebnis in die Parameter des AsyncManagers. Man könnte hier auch komplette Objekte oder Objektstrukturen ablegen, ich beschränke mich aber der Einfachheit halber auf den XML-String. Ist OutstandingOperations wieder auf 0, so wird IndexCompleted automatisch aufgerufen und die Parameter im AsyncManager auf die Parameter der Methode gemappt. Hier erfolgt dann die restliche Verarbeitung - in der Regel der Aufbau des Models zur Darstellung der View und die Rückgabe der View.

Einen kleinen Nachteil hat diese Umstellung noch: Kann man in der synchronen Verarbeitung problemlos mit Hilfe von Profiler-Werkzeugen (z.B. MVC Mini Profiler) eingreifen und beispielsweise die Laufzeit der Subrequests anzeigen, so wird das im asynchronen Kontext schon komplizierter bis unmöglich.

Englische Version Englische Version