Model-View-Controller - seperation of concerns

by DotNetNerd 2. November 2007 16:19

Model-View-Controller Framework

Frameworks der bygger på et Model-View-Controller pattern er blevet meget populære indenfor webudvikling, især Ruby on Rails vil mange kunne nikke genkendende til.

En af de store fordele ved MVC frameworks er at de giver "seperation of concerns" hvilket vil sige at man får adskilt datamodellen og brugergrænsefladerne. ASP.NET har været kritiseret for at netop det at opnå en klar adskillelse kan være meget svært idet en url mapper direkte til en aspx, som indeholder både markup og codebehind.

En anden force ved MVC frameworks er at de gør det muligt - og ligefrem naturligt - at basere sin udvikling på unittests. Dette er vigtigt ved udviklingen af større systemer, da man derved kan refaktorere uden at være nervøs for ikke at kunne overskue konsekvenserne.

Som alternativ til den almindelige aspx model er Microsoft derfor gået igang med at udvikle deres eget MVC framerwork, som skal passes direkte ind i .NET frameworket og understøtte både CLR og DLR sprog. MVC frameworket designes udfra at det skal være "extensible and pluggable", hvilket vil sige at der skal være muligt for udviklere at skifte ALLE enkeltdele af modellen ud hvis de ønsker at lave deres egen implementation. Derudover bliver der gjort meget ud af at frameworket skal performe godt og være "mockable" - altså det skal være muligt at lave unittesting baseret på mockups.

Routing regler 101

Routing er et kernebegreb i MVC frameworks, som går ud på at mappe en url til en given controller der skal håndtere det aktuelle request. Som standard vil MVC frameworket understøtte to former for mapning:

<Controller>/<Action>/<Param>
<SomeName>/<Action>/<Param> => <Controller>

Eksempelvis kunne man dermed lave en struktur der tillader redigering af produkter, hvor de følgende vil mappes til "rediger produkt med Id 4".

ProductsController/Edit/4
Products/Edit/4 => ProductsController

Udover at mappe til Controllers er det også muligt at mappe til ControllerFactories, som giver et abstraktionslag der kan dirigere videre til forskellige controllere alt efter hvad der skal udføres.

Routing kan f.eks sættes op i ApplicationStart eventet på Global.asax, og i de eksempler der har været vist indtil videre gør man sådan her:

Router.Routes.Add(
new Route("Products", "/Products/[action]/[id]", typeof(ProductController));


Controllers - systemets knudepunkt

Som tidligere nævnt er målet at designet skal være "extensible" og ud fra den devise er der flere mulige klasser der kan extendes, når man skal implementer sin egen controller.

Hvis man vil lave sin helt egen kan man tage udgangspunkt i interfacet IController, som har én metode kaldet Execute der modtager en IHttpContext og RouteData. RouteData er en dictionary af tokens fra url'en der udgør de parametre siden kaldes med.

public void Execute(IHttpContext context, RouteData routeData)
{
   
}

Alternativt kan man bygge videre på klasserne ControllerBase, Controller, Controller<T> eller IControllerFactory.
Det mest oplagte er at arbejde med Controller<T>, som er en typestærk standard implementation af IController.

Hver metode mapper her til en action, og parametre til de efterfølgende tokens i urlen. ControllerAction attributten fortæller at der er tale om en action, og ved at sætte DefaultAction=true indikerer man at denne metode skal kaldes hvis ingen action er givet af url'en. Udover denne attribut kan man tilføje andre til f.eks at håndtere caching.

class Products : Controller<ProductData>
[ControllerAction(DefaultAction=true)]
[OutputCache(Duration=10)]
public void List(int? number)
{
    ViewData["number"] = number;
    ProductData viewData = new ProductData();
    viewData.Number = number;
    RenderView("ProductView", ViewData); 
}

public class ProductData
{
    public int Number { get; set; }
}

ViewData bruges til at holde de data der skal passes til viewet. Der er som udgangspunkt en Dictionary til dette, med ved at bruge Controller<T> kan man angive en type man selv definerer.

Metoden RenderView bruges til at kalde et specifikt view og passe data til viewet.

Komplekse objekter kommer også til at kunne bruges som parametre ved hjælp af Type Descriptors som kan plugges ind.

Views - brugerflader rensede for logik

Views kan være aspx eller ascx filer med alt hvad de normalt kan, inclusiv brug af masterpages, bortset fra postbacks ikke er mulige og dermed undgår man at bruge viewstate.

Dette skyldes at views skal tilgås igennem en Controller, hvilket man ville bryde med ved at tillade postbacks.

Til at understøtte links ud kommer der blandt andet en hjælpeklasse der hedder html samt nogle extention methods. Skal man generere et link kan man f.eks. gøre følgende.

string link = Html.Link("Products", new {Action="Edit", param="4"});
string link = Html.Link<Products>(controller => controller.Edit(4));

På samme vis findes der en .Url metode der udelukkende returnerer url'en.

I den lidt mere fikse afdeling findes der også en metode til at lave links til paging som en pæn xhtml ul/li liste. Dette kræver at den liste man arbejder på implementerer IPagedList, som indeholder properties der beskriver det samlede antal elementer og index.

string paginationLinks = Html.PaginationLinks(viewData, "Products", "List", "10");

Af andre smarte metoder jeg lige vil nævne kort er der RedirectToAction til at håndtere redirects, og en UpdateFrom extention method, til at tage data fra Request.Form, et dataset eller andet og udfore en update.

RedirectToAction<Products>(c => c.List(0))

product.UpdateFrom(Request.Form)
Data kan fra views tilgås som tidligere antydet igennem ViewData, som kan være en type man selv definerer ellers igennem en Dictionary. Det er muligt på views det implementerer ViewPage<T> at tilgå data både via indexer og ved at tilgå properties direkte.

public class ProductView : ViewPage<ProductData>
{
    int i;
    i = ViewData["Number"];
    i = ViewData.Number
}

Ønsker man at benytte ajax vil der blive lavet sådan at man kan lave et kald og indsætte det returnerede html i en ny type panel.

Who am I?

My name is Christian Holm Diget, and I work as an independent consultant, in Denmark, where I write code, give advice on architecture and help with training. On the side I get to do a bit of speaking and help with miscellaneous community events.

Some of my primary focus areas are code quality, programming languages and using new technologies to provide value.

Microsoft Certified Professional Developer

Microsoft Most Valuable Professional

Month List