MVC Music Store ist eine Beispielanwendung und Tutorial von Jon Galloway zum Einstieg in die Entwicklung von Web-Applikationen mit ASP.NET MVC 3 (auf der CodePlex-Seite des Projekts findet man auch noch die Version 1 des Tutorials, das auf ASP.NET MVC 2 basiert).

Wie jedes andere jemals geschriebene Programm jenseits von Hello, World! lauern sowohl in dem Tutorial als auch in dem Anwendungscode einige Fallstricke, die kleinere oder größere Schönheitskorrekturen erfordern. Dennoch bietet das Tutorial einen sehr brauchbaren und zudem völlig kostenlosen Einstieg in MVC 3.

Was mir an der Anwendung besonders gefällt: Sie bildet eine perfekte Ausgangsbasis zur Betrachtung einer Reihe spannender Fragestellungen, auf die man bei kleineren Beispielen so nicht stoßen würde oder die belanglos wären. Sprich, sie gibt genug Stoff her, um Theorie und Praxis hart kollidieren zu lassen. Zudem kann jeder Interessierte sich die fertige Anwendung herunterladen oder ihre Entwicklung Zeile für Zeile nachvollziehen kann.

Daher starte ich hier eine Blog-Serie rund um den MVC Music Store (in der aktuellen Version 3.0b) mit einer Reihe von Themen, die da anfangen, wo das Tutorial aufhört:

  • Software-Entwurf
  • Testing
  • Deployment auf IIS mit Web Deploy
  • Deployment auf Windows Azure

Doch bevor ich mit diesen inhaltlichen Themen der Blog-Serie beginne, möchte ich erst einmal eine Reihe von Änderungen bzw. Bugfixes vorstellen, die ich der Anwendung bei meinem persönlichen Durchgang durch das Tutorial verpasst habe.

Layout-Bug in allen Browsern

Je nach Fenstergröße des Browsers und der Anzahl der Alben (z.B. beim Genre Classical) offenbart der Browse-View unter Store einen unschönen Layout-Fehler. Anstatt sich links neben dem Genre-Menu auszurichten, rutschen die Alben unter das Menu. Dies betrifft alle von mir getesteten Browser, d.h. Internet Explorer 9 (s.u.), Firefox 5 und Chrome 12.

clip_image001

Der Fix hierfür lehnt sich direkt an die erste Version des MVC Music Store an:

  1. Man entfernt in die Eigenschaft float: left für #main in Site.css.
  2. Man umschließt in _Layout.cshtml das gesamte Markup unterhalb des Body mit einem zusätzlichen <div id="container></div>:
    <body>
        <div id="container">
            <div id="header">
            </div>
            <div id="main">
            </div>
            <div id="footer">
            </div>
        </div>
    </body>

Die CSS-Definition für #container existiert bereits in Site.css, sie wurde scheinbar nur vergessen.

Alben anlegen und ändern (S.62 ff)

Als deutschsprachiger Nutzer des Tutorials wird man unweigerlich über den folgenden Fehler stolpern: Versucht man bei einem vorhandenen oder neu anzulegenden Album den Preis anzugeben, verwirft die client-seitige JavaScript-Validierung die Eingabe, obwohl diese syntaktisch korrekt ist…

clip_image002

… aber nur für den Fall, dass man als Anwender das deutsche Zahlenformat verwendet. Tatsächlich erwartet das jQuery Validation-Plugin out-of-the-box aber das amerikanisch-englische Zahlenformat, welches jedoch anstelle eines Kommas den Punkt als Dezimaltrennzeichen verwendet. Gibt man daher "9.99" anstatt "9.99" ein, so folgt nun eine serverseitige Fehlermeldung…

clip_image003

… wenn man auf seinem Entwicklungsrechner die deutschen Kultureinstellungen in den Regional Settings von Windows gesetzt hat. Dieser Fehler rührt daher, dass der DefaultModelBinder von ASP.NET MVC 3 den String "9.99" anhand der aktuellen Kultureinstellungen (entnommen aus Thread.CurrentCulture) zu parsen versucht… aber nun eben anhand des deutschen Zahlenformats.

Der einfachste Fix hierfür ist das Festsetzen der Kultureinstellungen für die Applikation in web.config:

  <system.web>
     
    <globalization culture="en-US" />
        
  </system.web>

Dies ist natürlich nur ein Hack, aber zumindest kann man dann (unter Nutzung des amerikanisch-englischen Formats) Alben anlegen und ändern. Ich komme auf eine bessere Lösung später zurück.

Geschafft? Fast… denn ein Klick auf Save führt nun zu einem YSOD:

clip_image004

Dieser Fehler tritt auf, weil in der Edit-Action des StoreManagerController der Model Binder eine Album-Instanz erzeugt, ohne die Eigenschaft AlbumId (den Primary Key) zu setzen – diese wird nämlich explizit vom Model Binding per Attribut ausgeschlossen:

[Bind(Exclude = "AlbumId")]
public class Album
{
}

Das vom Entity Framework 4.1. generierte UPDATE-Statement scheitert also letztendlich an dem implizit generierten WHERE AlbumId = 0. Den Primary Key eines Objekts von der Datenbindung auszuschließen macht durchaus Sinn, weswegen ich die Edit-Action entsprechend der vorherigen Version des MVC Music Store implementiert habe:

[HttpPost]
public ActionResult Edit(int id, FormCollection
form)
{
   
Album
album = db.Albums.Find(id);
   
if
(TryUpdateModel(album))
    {
        db.SaveChanges();
       
return RedirectToAction("Index"
);
    }
    ViewBag.GenreId =
new SelectList(
db.Genres,
"GenreId", "Name"
, album.GenreId);
    ViewBag.ArtistId =
new SelectList(
db.Artists,
"ArtistId", "Name"
, album.ArtistId);
   
return View(album);
}

Anstatt den Model Binder das Objekt zu erzeugen und es dem DbContext unterzuschieben, lade und binde ich das Album hier explizit. Den FormCollection-Parameter benötigt die Methode nicht wirklich. Er dient nur dazu, die Überladung des Methodenbezeichners Edit zuzulassen, da GET- und POST-Action sonst dieselbe Methodensignatur hätten, was nicht funktioniert.

Don’t Just Try (S.116)

Der CheckoutController verwendet ein etwas befremdliches Kontrukt und ruft TryUpdateModel() auf, ohne den Rückgabewert zu prüfen. Da der anschließende Teil der Methode sowieso innerhalb eines try-Blocks ausgeführt wird, verwende ich stattdessen UpdateModel() in dem try-Block. Ferner kann man sich das Auslesen des Promo-Codes aus den Formular-Daten sparen, diesen Job kann der Model Binder erledigen.

[Nachtrag vom 10.07.] 
Ich habe die Methode nochmal geändert und lasse nun auch die Order-Instanz vom Model Binder erstellen, da die vorherige Fassung für Unit Tests mehr Aufwand hinsichtlich Test Doubles erfordert. Die Methode ist in dieser Hinsicht noch nicht perfekt, aber dazu kommen wir noch Winking smile.

[HttpPost]
public ActionResult AddressAndPayment(string promoCode, Order
order)
{
   
if
(!ModelState.IsValid)
    {
       
return
View(order);
    }
   
try
    {
       
if (!String.Equals(promoCode, PromoCode, StringComparison
.OrdinalIgnoreCase))
        {
           
return
View(order);
        }
       
else
        {
            order.UserName = User.Identity.Name;
            order.OrderDate =
DateTime
.Now;

           
// Save order
            storeDB.Orders.Add(order);
            storeDB.SaveChanges();

           
// Process the order
            var cart = ShoppingCart
.GetCart(HttpContext);
            cart.CreateOrder(order);

           
return RedirectToAction("Complete", new
{ id = order.OrderId });
        }
    }
   
catch
    {
       
// Invalid - redisplay with errors
        return View(order);
    }
}
[HttpPost]
public ActionResult AddressAndPayment(string
promoCode)
{
   
var order = new Order
();
   
try
    {
        UpdateModel(order);
       
if (!String.Equals(promoCode, PromoCode, StringComparison
.OrdinalIgnoreCase))
        {
           
return
View(order);
        }
       
else
        {
            order.UserName = User.Identity.Name;
            order.OrderDate =
DateTime
.Now;

           
// Save order
            storeDB.Orders.Add(order);
            storeDB.SaveChanges();

           
// Process the order
            var cart = ShoppingCart
.GetCart(HttpContext);
            cart.CreateOrder(order);

           
return RedirectToAction("Complete", new
{ id = order.OrderId });
        }
    }
   
catch
    {
       
// Invalid - redisplay with errors
        return
View(order);
    }
}

So viel zu den echten Bugs, im nächsten Teil beschreibe ich dann einige rein stilistische Änderungen.