Onder de bezielende leiding van Scott Guthrie heeft Microsoft ‘s ASP.NET team in december 2007 de eerste preview van het ASP.NET MVC-Framework uitgebracht. Met het ASP.NET MVC-Framework is het mogelijk een webapplicatie in ASP.NET te ontwikkelen, gebaseerd op het Model View Controller design pattern.
Ontwerpprincipes
Dit framework is, naast het MVC design pattern, opgebouwd volgens een aantal principes:
- Convention over configuration: Het meest voorkomende patroon wordt gebruikt. Wil je iets anders bereiken, dan is dat te configureren.
- DRY (Don’t Repeat Yourself): Filosofie die tracht duplicatie te reduceren.
- Extensible: Het framework zal eenvoudig uitbreidbaar zijn. Kerncontracten zijn interface-gebaseerd.
- Seperation of concerns: Het scheiden van belangen zorgt ervoor dat elk component zijn eigen verantwoordelijkheid heeft en niet afhankelijk is van een of meerdere andere componenten.
- Facilitate Test Driven Development: Het framework moet het eenvoudig maken om applicaties die ermee ontwikkeld worden ,unit-testbaar te maken.
DRY: Don’t Repeat Yourself
Model View Controller
MVC (Model View Controller) is een design pattern bedoeld om business-logica te scheiden van userinterface-logica. Het resultaat van een juiste toepassing van het MVC-pattern zal robuuste, onderhoudbare en (unit)testbare applicaties opleveren.
- Model: Representeert de data en business-rules, verantwoordelijk voor handhaven van ‘State’.
- View: Definieert hoe de data aan de gebruiker gepresenteerd wordt (UI).
- Controller: Definieert hoe de UI reageert op ontvangen commando’s en requests. Deze componenten zijn verantwoordelijk voor de afhandeling van interactie met de eindgebruiker, het manipuleren van het Model en de uiteindelijke keuze van welke View aan de gebruiker wordt gepresenteerd.

Fig. 1:Schematische weergave Model View Controller
Voordelen
Het MVC-Framework biedt de volgende voordelen:
- Alle kerncontracten binnen MVC zijn interface-gebaseerd en daardoor uitwisselbaar (of mockable). Door het scheiden van belangen wordt een goede unit-testbaarheid verkregen.
- Het framework is uitbreidbaar. Het is bijvoorbeeld mogelijk IOC containers zoals Windsor, Spring.Net, NHibernate, etc., in te zetten met het Framework, maar ook view engines en unit test frameworks zijn uitwisselbaar.
- Door het introduceren van UrlRouting heb je veel controle over de URL’s van je applicatie. Je hoeft geen url rewriting meer in te zetten om URL’s te krijgen die afwijken van de WebForms manier.
Page-request flow
Figuur 2 toont een schematische weergave van het verloop van een page-request bij het MVC Framework:

Fig. 2: Verloop Page-request
- Request komt vanaf de client naar de controller.
- De controller roept het model aan om business-operaties uit te voeren (data ophalen, business-rules uitvoeren).
- Het model retourneert het resultaat van de operatie aan de controller.
- De controller beslist welke view getoond moet worden en stuurt deze view de benodigde data.
- View toont de output en stuurt response naar de client.
Unit-testen
De consensus binnen de moderne developer community is dat het gebruik van unit-tests belangrijk is.
Een unit-test moet autonoom, niet afhankelijk van externe resources of componenten, snel uit te voeren en herhaalbaar zijn. Dit is onder andere te bereiken door objecten die geen direct onderdeel uitmaken van de te testen unit te mocken en door het gebruik van “Inversion of Control”, ook wel het “Dependency Injection” design pattern genoemd. Het woord mocken betekent hier dat het gedrag en de eigenschappen van een ander object gesimuleerd worden.
Een unit-test moet autonoom, snel uit te voeren en herhaalbaar zijn
Het idee achter inversion of control is om externe afhankelijkheden naar objecten te faciliteren d.m.v. externe mechanismen in plaats van de objecten zelf de afhankelijkheden op te laten zetten, of zelfs de processen die de objecten gebruiken de benodigde afhankelijkheden op te laten zetten. Het einddoel is 'loose coupling', en het kan vrij eenvoudig bereikt worden door het parametriseren van de constructor met het object waarvoor je de aanroepende class verantwoordelijk laat zijn (zie listings 3, 4 en 5).
Omdat het met deze hulpmiddelen eenvoudig is om een unit (methode) te isoleren, is de mogelijkheid om alleen het gedrag van de unit te testen binnen handbereik. Het MVC-design pattern laat de controller de requests afhandelen en beslissen welk model er aan de view doorgegeven wordt om aan de gebruiker te tonen. Door deze scheiding van belangen is het unit-testen van een webapplicatie veel eenvoudiger. Het is mogelijk unit-tests te runnen zonder dat de webserver actief is, doordat de namespace System.Web.Abstractions, nadat het in het MVC framework zijn waarde bewezen heeft, is toegevoegd aan het .NET Framework. Deze namespace bevat HttpContextBase, waarmee je HttpContext kunt abstraheren en nabootsen, of mocken. Natuurlijk is het nog steeds van belang om principes als “een class heeft een verantwoordelijkheid” of “single responsibility principle” te volgen om een unit-testbare applicatie te krijgen.
MVC-Framework en unit-testen
Het ASP.NET MVC Web Application projecttype kan in Visual Studio toegevoegd worden door de installer te runnen; deze is te downloaden vanaf www.asp.net/mvc. Om duidelijk te maken hoe het MVC-Framework omgaat met unit-tests maken we een nieuw project aan.

Fig.: 3: Nieuw ASP.NET MVC Web Application project
We geven de gewenste naam en locatie op en klikken op ‘OK’. Dan verschijnt het scherm in figuur 4.

Fig. 4: Maak Unit Test Project aan
De eerste dialoog na het aanmaken van een ASP.NET MVC Web Application geeft de mogelijkheid tot het creëren van een unit-test project. Bij ‘Test Framework’ kan ieder unit-testframework gekozen worden dat integreert met het MVC-Framework. Wij maken gebruik van het Microsoft testframework dat standaard in Visual Studio 2008 aanwezig is.
Alternatief testframework is MbUnit, een open source initiatief
Een veelgebruikt alternatief testframework is MbUnit, dit is een open source initiatief. Bij installatie van MbUnit versie 3.x zet MbUnit zichzelf in de lijst van beschikbare testframeworks. Tevens worden er MVC/MbUnit Test Project Templates voor C# en VB.NET geinstalleerd. Mocht het testframework van jouw keuze dit (nog) niet doen, dan kun je op de Visual Web Developer Team Blog [3] lezen hoe dit werkt.
Projectstructuur
De wizard heeft twee projecten in de solution aangemaakt: de webapplicatie en een testproject. Zelf heb ik een class-library toegevoegd.
In de webapplicatie zijn de models, views en controllers gescheiden door aparte mappen.
In de View-map wordt voor iedere controller een bijbehorende subfolder verwacht. Doordat het convention over configuration principe gebruikt wordt, zal de controller direct werken wanneer men zich aan de conventie houdt. Hierdoor kun je snel aan de slag. Wil je ander gedrag, dan kun je dit configureren. In de sub folder worden de MVC-ASPX-views geplaatst.
In de View-map staat ook een Shared-submap waarin de MasterPages en errorpagina’s geplaatst worden.
De Content-map is voor de stylesheets en javascript–bestanden. De wizard plaatst een CSS-bestand en een MVC-variant van de ASP.NET AJAX Client library.
De wizard maakt een Home- en een Account-controller aan met bijbehorende views. Home is voor de start pagina van het project en account voor de aanmeld pagina. Voor de controllers geldt het convention over configuration principe dat iedere publieke methode als action-methode gezien wordt. De controller baseclass zal de betreffende action-methode uitvoeren volgens de URL routing regels die in de GLOBAL.ASAX van de applicatie zijn geconfigureerd.
In het testproject is een Controllers-map met een HomeControllerTest aangemaakt. In deze class zijn twee voorbeeldtests geplaatst, waarmee getest wordt of de controllers de juiste titel in de view plaatsen.

Fig. 5: Projectstructuur
Afhankelijkheden verwijderen
We willen een unit, een zo klein mogelijk stuk functionaliteit, testen. In dit geval nemen we de Index-methode op de HomeController als voorbeeld . De Index-methode (zie lisiting 3) haalt data op uit een database via de HomeModel-class en geeft deze door aan de view. Zodra we met een database werken, wil je bij het unit-testen niet afhankelijk zijn van het bestaan van de database. Door met interfaces te werken wordt het eenvoudiger om de datalaag te abstraheren en de database na te bootsen.
public interface IProductsDataSource
{
List<Product> GetProducts();
}
Listing 1: Interface IProductsDatasource
In listing 1 is een simpel interface zichtbaar. Dit interface dicteert het implementeren van een methode die een List van het type Product teruggeeft. Door een interface te gebruiken maken we het mogelijk de methodes in deze interface uit te wisselen, waardoor we in onze unit-test kunnen controleren wat deze retourneert.
We maken een eigen class genaamd MockDataStore. Deze class implementeert het IProductsDataSource -interface en geeft altijd dezelfde List<Product> terug (zie listing 2). Deze class zal alleen voor unit-testdoeleinden ingezet worden.
public class MockDatastore : IProductsDataSource
{
public List<Product> GetProducts()
{
List<Product> products = new List<Product>();
for (int i = 0; i < 3; i++)
{
Product product = new Product();
product.Name = "Productname " + i.ToString();
product.Category = "Testproduct";
product.Description = "This is product no: "
+ i.ToString();
product.Price = (Decimal)i * 10;
products.Add(product);
}
return products;
}
}
Listing 2: Mock Data class
TEST / REFACTOR / TEST
Test faalt
Test Driven Development (TDD) schrijft voor om te starten met het schrijven van een test, deze uit te voeren en te zien falen. De volgende stap is de methode zo simpel mogelijk in te vullen, zodat de test slaagt. Vervolgens refactor je de code en voer je de test weer uit; dit herhaal je totdat de test slaagt. Hierna start het proces weer van voren af aan. We starten met de test in listing 3.
[TestMethod]
public void HomeController_Get_All_Products_Index()
{
MockDatastore store = new MockDatastore();
HomeController controller = new HomeController(store);
controller.Index();
Assert.IsTrue(
((List<Product>)controller.ViewData.Model).Count == 3,
"ViewData.Model did not hold 3 Products");
}
Listing 3: Test HomeController
Deze test instantieert de HomeController, geeft in de constructor een instantie van onze zojuist gemaakte MockDatastore-class mee en voert vervolgens de Index-methode uit. De test controleert dat er drie Producten in het model zitten.
Na het uitvoeren van de test krijgen we het resultaat ‘Failed’, zoals getoond in figuur 6.

Fig. 6: Test failed
Refactor
Het Model krijgt een private read-only IProductsDatasource-property, die geïnstantieerd wordt met een nieuwe instantie van de datalaag-class genaamd Datastore. Dit is een object dat via de GetProducts-methode de producten uit de database haalt en retourneert. Door inversion of control te gebruiken kunnen we tijdens een test een ander object dat de IProductsDataSource interface implementeert, gebruiken om de data aan te leveren (zie listing 4).
public class HomeModel
{
private IProductsDataSource _DataSource;
private IProductsDataSource DataSource
{
get
{
if (_DataSource == null)
{
_DataSource = new Datastore();
}
return _DataSource;
}
}
public HomeModel()
{
}
/// <summary>
/// Constructs an instance of HomeModel
/// inversing control of the datasource to the caller
/// </summary>
/// <param name="dataSource"></param>
public HomeModel(IProductsDataSource dataSource)
{
_DataSource = dataSource;
}
public List<Product> GetProducts()
{
return DataSource.GetProducts();
}
}
Listing 4: Inversion of control op Model
In listing 5 zien we dat de controller (HomeController) een private member ModelData heeft van het type HomeModel. De standaard constructor zonder parameter instantieert het HomeModel type met behulp van zijn standaard constructor. Op de controller maken we een extra constructor waarbij een IProductsDataSource als parameter wordt verwacht. In deze constructor wordt de waarde van de parameter aan de private member ModelData toegewezen.
[HandleError]
public class HomeController : Controller
{
HomeModel ModelData;
public HomeController()
{
ModelData = new HomeModel();
}
public HomeController(IProductsDataSource productsData)
{
ModelData = new HomeModel(productsData);
}
public ActionResult Index()
{
return View("Index", ModelData.GetProducts());
}
}
Listing 5: Controller-class
In de Index-methode, die een ActionResult retourneert, wordt de member ModelData gebruikt om GetProducts uit te voeren. Door deze werkwijze zorgen we ervoor dat standaard, wanneer de constructors zonder parameter gebruikt worden, de data uit de database gebruikt wordt. Tijdens onze test in listing 4 kunnen we echter een eigen Data-object gebruiken, MockDatastore uit listing 3, die we in de constructor van de HomeController injecteren. Vervolgens zal dit MockDatastore object aan de private member DataSource toegewezen worden via de geparameteriseerde constructor van het HomeController object, waarna het in de GetProducts methode de producten kan retourneren die wij in onze test verwachten.
De class declaratie in listing 5 is met het attribuut HandleError gedecoreerd. Dit action- filter attribuut is sinds preview 4 aan het MVC Framework is toegevoegd
Test slaagt
Na deze refactoring voeren we de test uit listing 4 nogmaals uit.

Fig. 7: Test slaagt
In figuur 7 is te zien dat de test succesvol is uitgevoerd. We kunnen er nu vanuit gaan dat de methode Index op de HomeController-controller doet wat wij verwachten.
Conclusie
Wanneer jouw volgende webapplicatie meer is dan alleen CRUD, als je de mogelijkheid wilt om je processing- en business-logica te testen, misschien zelfs TDD wilt adopteren … dan is het ASP.NET MVC Framework misschien iets voor jou.
De aard van het MVC design pattern leent zich uitstekend voor unit-testen doordat de afzonderlijke onderdelen gescheiden zijn. Voeg daar het feit aan toe dat de kerncontracten binnen het ASP.NET MVC Framework interface-gebaseerd zijn, en het wordt alleen maar beter. De onderhoudbaarheid van een MVC-applicatie zal beter zijn door de ‘loosely coupled’ aard ervan. Overigens kan ook bij gebruik van het ASP.NET MVC Framework een slecht onderhoudbare applicatie gemaakt worden …
Ook als je het prettig vindt veel controle te hebben over bijvoorbeeld de HTML die in de browser gerenderd wordt, is MVC ideaal. Er wordt geen HTML gegenereerd; alleen datgene dat je zelf in de view plaatst wordt gebruikt. Voldoet de WebForms view-engine die standaard door het ASP.NET MVC-Framework gebruikt niet aan je wensen, gebruik dan de engine die je wel bevalt of maak er zelf een.
Referenties
Gebruikte URL’s: