June 2010 - Posts

Något som har funnits med ett tag, men som inte har använts särskilt mycket förrän när ASP.NET MVC introducerades är T4-templates. T4 är en akronym för ”Text Template Transformation Toolkit”, och gör det möjligt att skapa upp en mall som sedan kan användas för att generera färdig kod.

När man skapar templates med T4 så kan man använda antingen C# eller Visual Basic för syntaxen. All kod skrivs i kodblock, vilket påminner en del om hur vi normalt sett skapar vyer i ASP.NET MVC.

Visual Studio har inbyggt stöd för att skapa T4-templates och sedan generera kod från dem, men det finns inget inbyggt stöd för att få färgkodning eller IntelliSense. Det resulterar i att man sitter med vit bakgrund och svart text som om det vore ett vanligt textdokument. Det finns dock en enkel lösning på det, nämligen Tangible T4 Editor, vilket är en gratis-editor för T4 och finns att ladda ned för Visual Studio 2010 här:

http://visualstudiogallery.msdn.microsoft.com/en-us/60297607-5fd4-4da4-97e1-3715e90c1a23

Tangible T4 Editor gör arbetet så otroligt mycket enklare att det inte finns någon anledning till att inte ha det installerat.

Det jag kommer att göra nu är att börja med en enkel template för att visa vad som genereras, och hur det genereras. Sedan kommer jag att använda ASP.NET MVC för att kunna skriva en egen template för vyer som skapas.

Skapa en första template

Till att börja med så behöver vi ett vanligt klassbibliotek där vi plockar bort cs-filen som kommer som standard. Sedan behöver vi skapa upp en T4-template. Om man har installerat Tangible T4 Editor så kommer ett par projektmallar upp när vi tar Create -> New File, men vi kommer att skapa en tom template, vilken heter ”Text Template”. Vi kallar sedan filen för MyTemplate.tt. Just ”tt” är filändelsen för alla T4-templates.

T4-filen som skapas upp ser ut så här:

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".txt" #>

I Solution Explorer så kan vi även expandera MyTemplate.tt och under den filen ha en fil med namn MyTemplate.txt. Txt-filen är genererad från vår template, och vill vi göra ändringar i txt-filen så skall dessa göras i templaten, då de skrivs över annars.

I T4-filen så kan vi se att vi har ett par olika attribut. Debug är en boolean som avgör om vi vill ha möjlighet att debugga mallen eller inte. Hostspecific är en boolean som i det här fallet skall vara false då vi använder TextTemplateFileGenerator för att rendera filen (det kan vi se under Properties för tt-filen), och language som här är C#, men även kan vara VB visar vilken syntax vi vill använda i mallen. Vi kan även se att vi har ”.txt” som filändelse på filen vi renderar, och ändrar vi det till t.ex. ”.cs” så genereras en cs-fil istället.

En skillnad från när vi sitter i ASP.NET MVC är att koden finns inom <# och #> istället för <% och %> som vi är vana vid. Det här gäller överallt där vi vill ha kod i vår mall.

För att få in något innehåll i textfilen så börjar vi med att lägga till det här i mallen:

Hello, world!

När vi sparar tt-filen och tittar i txt-filen så kan vi se att texten har dykt upp där. Det här är dock ingen bra anledning till att använda T4-templates då vi lika gärna skulle kunna skriva det direkt i textfilen. Så hur tar vi nytta av C#?

Vi lägger nu till det här i filen:

Klockan är: <#= System.DateTime.Now.ToShortTimeString() #>

Lägg märke till ”=”, vilket skriver ut texten direkt, precis som i ASP.NET MVC. Sparar vi nu om filen så ser vi att vi har fått ut aktuell tid i txt-filen. Om vi vill uppdatera texten utan att spara om mallen så kan vi högerklicka på tt-filen och välja ”Run custom tool”.

Det som är så elegant med templates är att vi får tillgång till hela .NET, och enkelt kan generera filer. Kikar vi på POCO-stödet i Entity Framework 4.0 så kan vi se att T4-templates används för att generera klasser för modellen, baserat på edmx-filen. Det sker på samma sätt som vi nu skriver ut tiden, så det går att göra väldigt avancerade saker med det.

För att ta ett lite mer avancerat exempel så kommer vi nu att ha en loop där vi skriver ut siffror:

<#
for (int i = 1; i <= 10; i++)
    WriteLine(i.ToString());
#>

Sparar vi om filen så får vi fram siffrorna 1-10 på en varsin rad. Det är alltså väldigt smidigt att generera dessa filer, och vi behöver inte ens kompilera koden för det!

Templates i ASP.NET MVC

Så hur kan vi nu ta vara på den här kunskapen och generera mallar för våra vyer i ASP.NET MVC?

Först och främst så kommer jag att öppna projektet i artikeln jag skrev om att skapa en egen ViewEngine:

http://weblogs.asp.net/mikaelsoderstrom/archive/2010/06/17/skapa-en-egen-viewengine.aspx

När vi skapar nya vyer i ASP.NET MVC så har vi en del standardmallar som renderar kod som passar bra för WebFormViewEngine, men om vi nu vill göra detsamma för SimpleViewEngine?

Först och främst så kommer jag att bygga ut SimpleView för att ge den stöd för modeller. Det är inte den snyggaste koden för det, men den fungerar som den ska och gör det snabbt och enkelt. I metoden Render i klassen SimpleView så kommer jag att lägga till det här efter koden som byter ut – mot ett ViewData-värde (all kod kommer att finnas tillgänglig för nedladdning):

object model = viewContext.ViewData.Model;
 
if (model != null)
{
    Type t = model.GetType();
    PropertyInfo[] props = t.GetProperties();
 
    foreach (PropertyInfo item in props)
    {
        object o = item.GetValue(model, null);
 
        if (o != null)
            view = view.Replace("%%" + item.Name, o.ToString());
    }
}

Det vi gör i koden är att vi går igenom alla properties i modellen och ersätter %%PropertyNamn i vyn med dess värden.

Vi skapar sedan upp en modell:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Och så skickar vi med den i Index-metoden i HomeController:

public ActionResult Index()
{
    ViewData["Message"] = "Welcome to ASP.NET MVC!";
 
    Customer customer = new Customer() {
        Id = 1,
        Name = "Mikael"
    };
 
    return View(customer);
}

Jag kommer inte att gå igenom det här alltför noggrant, utan koden är ganska självförklarande.

Istället så ska vi nu skapa upp en T4-template som kan rendera rätt HTML åt oss.

Den HTML vi vill få ut är:

<h2>Index</h2>
    
<p> --Message </p>
 
<ul>    
    <li>Id: %%Id </li>
    <li>Name: %%Name </li>
</ul>

Så hur kan vi göra detta?

Först och främst så skapar vi upp en mapp i roten för projektet med namnet ”CodeTemplates”. Här kan vi sedan ha två mappar, ”AddController” som innehåller templates för nya controllers, och ”AddView”, vilken innehåller templates för nya views. Vi kommer bara att rendera nya views, så vi skapar upp AddView och där skapar vi en fil vid namn ”SimpleDetails.tt”.

Vi kan nu se att vi får en fil skapad åt oss automatiskt direkt under mallen. Den vill vi inte ha, så vi tar bort värdet i Custom tool.

1 - Add View

Vår template i sin helhet ser ut så här:

<#@ template language="C#" HostSpecific="True" #>
<#@ assembly name="System" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Reflection" #>
<#@ output extension=".html" #>
<#
var mvcHost = (MvcTextTemplateHost)Host;
var properties = new Dictionary<string, string>();
 
if (mvcHost.ViewDataType != null)
{
    foreach (var pi in mvcHost.ViewDataType.GetProperties())
        properties.Add(pi.Name, "%%" + pi.Name);
}
#>
<h2><#= mvcHost.ViewName #></h2>
    
<p> --Message </p>
 
<ul>    
    <# foreach(var property in properties) { #>
    <li><#= property.Key #>: <#= property.Value #> </li>
    <# } #>
</ul>

Det vi kan se här är att vi först har satt HostSpecific till true. Anledningen till det är att vi nu inte har något verktyg som genererar kod direkt, utan när vi skapar nya vyer så kommer MvcTextTemplateHost att användas. Den innehåller det vi behöver veta när vi skapar upp nya vyer.

Vi har sedan ett par referenser till de bibliotek vi behöver komma åt, samt sätter output extension till ”.html” då våra vyer är rena HTML-filer.

Efter det kommer koden, vilket är ren C#. Här sparar vi först undan hosten, vilken är en MvcTextTemplateHost, och sedan skapar vi upp en Dictionary<string, string> som kommer att innehålla namnet på vår property, samt det som krävs för att den skall renderas rätt i vyn.

ViewDataType som vi kollar efter är modellen som skickas från vår action-metod. I vårt fall kommer det att vara en Customer, men det skulle även kunna vara något annat.

Sedan har vi vår vanliga HTML, samt skriver ut de värden vi vill generera.

Nästa steg blir att använda mallen för att generera en ny Index.html. Om du redan har en Index.html i /Views/Home så ta bort den.

Öppna sedan upp HomeController, högerklicka i Index-metoden och välj Add View. Som View data class sätter vi Customer-modellen vi har skapat upp, och under View content så väljer vi SimpleDetails, vilket är den mallen vi har skapat upp nu.

När filen nu skapas upp så har den fått det här utseendet:

<h2>Index</h2>
    
<p> --Message </p>
 
<ul>    
    <li>Id: %%Id </li>
    <li>Name: %%Name </li>
</ul>

Den har alltså läst av vår T4-template, läst av alla properties i modellen och sedan genererat en vy anpassad för SimpleViewEngine!

Ser vi på hur sidan ser ut nu så kan vi se att allt renderas precis så som vi önskar.

Med T4-templates kan vi skapa upp alla typer av dokument snabbt och enkelt, och då med hjälp av allt .NET har att ge oss.

När man skapar nya vyer i ASP.NET MVC så skapas en aspx-fil upp, vilken påminner en del om klassisk ASP. När man sedan går in på sidan så kan man se att kodblocken är utbytta mot HTML. Den här transformeringen sker i vad som kallas en View Engine. Som standard i ASP.NET MVC används en View Engine vid namn WebFormViewEngine. WebFormViewEngine ärver klassen VirtualPathProviderViewEngine, som i sin tur implementerar IViewEngine.

Tack vare skalbarheten i ASP.NET MVC så kan vi byta ut denna helt och hållet mot en egen implementation mot antingen VirtualPathProviderViewEngine, eller direkt mot interfacet IViewEngine. I exemplet jag strax tar upp så kommer jag att gå direkt mot IViewEngine för att skapa en egen View Engine, samt skapa upp en implementation av Iview (ASP.NET MVC kör med WebFormView som standard).

Exemplet jag kommer att skapa är bara ett väldigt enkelt sådant, och bör ses som just ett exempel på hur man kan skapa en View Engine. Det kommer inte att vara optimalt, sexigt eller annat, utan är bara en väldigt enkel View Engine.

Skapa en View Engine

Det vår View Engine ska göra är att läsa av dels en Master.html, och sedan hämta in den vyn som vi returnerar i controllern och läsa in den. Även vyn kommer att vara en HTML-fil.

Det första steget är att skapa ett nytt vanligt ASP.NET MVC 2-projekt. Här skapar vi upp två klasser, SimpleView och SimpleViewEngine. SimpleView kommer att implementera IView och dess metoder.

namespace SimpleViewEngine
{
    public class SimpleView : IView
    {
        private string _masterPath;
        private string _viewPath;
 
        public SimpleView(string masterPath, string viewPath)
        {
            _masterPath = masterPath;
            _viewPath = viewPath;
        }
 
        public void Render(ViewContext viewContext, TextWriter writer)
        {
            //Läs in _masterPath och _viewPath.
        }
    }
}

I konstruktorn tar vi emot masterPath och viewPath från vår ViewEngine. Vi kommer sedan att läsa av masterPath och ersätta en viss del av den filen med viewPath.

I SimpleViewEngine som implementerar IViewEngine så kommer vi att hårdkoda sökvägen till Master.html för enkelhetens skull. Vi har även andra fasta sökvägar för att slippa tänka på det nu.

Klassen i sin helhet ser ut så här:

namespace SimpleViewEngine
{
    public class SimpleViewEngine : IViewEngine
    {
        private string pathPattern = @"{0}\views\{1}\{2}.html";
 
        public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
        {
            return FindView(controllerContext, partialViewName, "master", useCache);
        }
 
        public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
        {
            string viewPath = String.Format(pathPattern,
                controllerContext.HttpContext.Server.MapPath("~/"),
                controllerContext.RouteData.Values["controller"].ToString(),
                viewName);
 
            string masterPath = String.Format(pathPattern,
                controllerContext.HttpContext.Server.MapPath("~/"),
                "shared",
                "master");
 
            return new ViewEngineResult(new SimpleView(masterPath, viewPath), this);
        }
 
        public void ReleaseView(ControllerContext controllerContext, IView view)
        {
 
        }
    }
}

Det vi gör är att vi helt enkelt formaterar sökvägarna till filerna och skickar in i konstruktorn för SimpleView genom en klass kallad ViewEngineResult.

För att registrera vår ViewEngine lägger vi in det här i Application_Start i global.asax.cs:

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new SimpleViewEngine());

Det gör att vi plockar bort WebFormViewEngine, och istället kör med SimpleViewEngine. Varje gång vi returnerar View() i en action-metod kommer SimpleViewEngine att köras.

Det som återstår nu innan vi kan testköra sidan är att skapa upp våra html-sidor som kommer att vara view och master page.

Först börjar vi med Index.html som ligger i /Views/Home tillsammans med Index.aspx. Den får det här utseendet:

<b>En enkel vy. :-)</b>
<p>
    Klockan är: %TIME%
</p>
<p>
    --Message 
</p>

Det är väldigt enkel HTML. Vi kommer dock att ersätta %TIME% med den aktuella tiden, samt –Message med ViewData[”Message”]. Det kommer att bli en vanlig replace och vi kommer inte att ha stöd för att skicka in någon modell till vyn (det är dock möjligt att bygga vidare på vår SimpleView för att ge stöd för sådant).

Master.html som ligger i /Shared ser ut så här:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Test av ViewEngine</title>
</head>
<body>
    <h1>Master!</h1>
    <div>
        %VIEW%
    </div>
    <div>
        Coolt!
    </div>
</body>
</html>

%VIEW% kommer att ersättas med vyn som anropas.

Om vi försöker gå till /Home/Index nu så ser vi att skärmen blir blank. Anledningen till det är att vi fortfarande inte skriver ut något i Render-metoden i SimpleView.

Nästa steg blir att skriva logiken för att läsa in filerna (varning för fulkod):

public void Render(ViewContext viewContext, TextWriter writer)
{
    try
    {
        string view = File.ReadAllText(_viewPath);
        view = view.Replace("%TIME%", DateTime.Now.ToLongTimeString());
 
        if (view.Contains("--"))
        {
            int keyIndex = view.IndexOf("--");
            int length = view.IndexOf(" ", keyIndex) - keyIndex;
 
            string key = view.Substring(keyIndex, length);
            string value = viewContext.ViewData[key.Remove(0, 2)].ToString();
 
            view = view.Replace(key, value);
        }
 
        string master = File.ReadAllText(_masterPath);
        master = master.Replace("%VIEW%", view);
 
        writer.Write(master);
    }
    catch (Exception ex)
    {
        writer.Write("Oops..! Nu blev det knas!<div>" + ex.Message + "</div>");
    }
}

Det vi gör här är helt enkelt att läsa in filerna rakt av och sedan köra några replace för att få in all text.

Om vi surfar till startsidan nu så kan vi se att det här renderas på sidan:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Test av ViewEngine</title>
</head>
<body>
    <h1>Master!</h1>
    <div>
        <b>En enkel vy. :-)</b>
<p>
    Klockan är: 23:13:45
</p>
<p>
    Welcome to ASP.NET MVC! 
</p>
    </div>
    <div>
        Coolt!
    </div>
</body>
</html>

Utan att ändra något i controllern så har vi plockat fram ViewData och renderat vyerna baserat på HTML-filer.

Prestanda

Så hur blir det med prestandan? Då det inte blir ett lager ovanpå det befintliga, utan en ersättning, så påverkas inte prestandan negativt så länge som det inte är en dåligt skriven View Engine. Faktum är att det faktiskt kan gå snabbare om man skriver en View Engine som är optimerad för det aktuella syftet istället för att ha en som är anpassad för att vara generell.

Andra implementationer av ViewEngine

Så hur kan man gå vidare? Ett bra sätt är att kika på källkoden för ASP.NET MVC. Där finns allt tillgängligt och det är enkelt att se hur allt fungerar i bakgrunden.

Ett annat alternativ är att ta en titt på de olika varianter som finns tillgängliga på internet. En populär ViewEngine är Spark View Engine, vilket ger en HTML-liknande syntax i vyerna, men som även tillåter databindning och annat med samma syntax. Det är även open source, så man kan lätt se hur det fungerar.

Källkod, exempel och annat finns här:
http://sparkviewengine.com

I första versionen av Entity Framework som kom i .NET 3.5 SP 1 fanns till största delen grundläggande funktionalitet för att kunna koppla sig mot en databas snabbt och enkelt och utföra operationer. Dock saknades väldigt mycket, och på grund av en mängd beroenden bland de klasser som genererades så blev det svårt att enkelt separera på logiken. I .NET 4.0 introducerades en mängd funktioner för att lösa detta, och det är bland annat det jag kommer att använda mig utav här.

Projektet jag kommer att ta upp kommer att bli ett enkelt ”bibliotek” där man kan lista författare och böcker de har skrivit.

Förutom en vanlig installation av Visual Studio 2010 och SQL Server (2005 eller senare) så kommer dessa att användas:

Skapa upp projekten

Det första vi behöver göra nu är att skapa upp de projekt vi kommer att använda oss utav när vi testar all kod. För enkelhetens skulle skapar vi upp alla projekt på en gång (även de vi inte kommer att använda till en början).

  • De projekt vi kommer att ha är:
  • Data\Library.EntityRepository (Class Library)
  • Data\Library.TestRepository (Class Library)
  • UI\Library.ConsoleApplication (Console Application)
  • UI\Library.Mvc (ASP.NET MVC 2 Application)
  • Library.Interfaces (Class Library)
  • Library.Model (Class Library)

Jag har även med en Solution Folder med dependencies samt ett Modeling Project för att kunna generera ett Layer Diagram som enkelt kan visa dependencies mellan projekt. Dessa är dock ej nödvändiga för projektet.

Solution Explorer ser nu ut så här:

1 - Solution Explorer

Hur de olika projektens beroenden ser ut kan ni se här:

3 - Layer Diagram

Domain (Library.Model) och Interfaces (Library.Interfaces) har inga beroenden. De två datalagren har dock referensen till dessa två. UI-lagren har referenser till Domain, Interfaces och de olika datalagren.

Vi behöver även en databas att jobba mot. Då Entity Framework har funktionalitet för att skapa tabeller baserat på en modell så kommer vi att starta med en helt tom databas. Det som krävs är SQL Server 2005 eller senare (SQL Server 2000 har ingen support längre, och stödet är borttaget i Visual Studio 2010 för det). Jag kommer att använda SQL Server 2008 R2 Express, vilket fungerar utmärkt att använda. Man behöver alltså inte använda fulla versionen, utan kan utan problem köra med Express-versionen.

2 - Management Studio

Den skapade databasen, utan tabeller då de skapas av Entity Framework senare.

Skapa modellen

När vi nu har ett projekt och en databas så ska vi börja med att skapa Entity Framework-modellen, från vilken vi även skall skapa tabellerna i databasen. För att kunna ta nytta av allt jag tar upp nu så är det viktigt att ni har installerat stödet för det, vilket jag länkar till i början av artikeln.

I EntityRepository-projektet kommer vi att ha själva modellen, dess repository och andra filer som kommer att användas utav Entity Framework.

Börja med att välja Add -> New Item -> ADO.NET Entity Data Model. Vi ger filen namnet LibraryEntities.edmx och skapar den. Vi väljer sedan att skapa en Empty Model då vi vill skapa databasen utifrån modellen och inte tvärtom.

4 - Empty Model

De två entiteter vi kommer att använda nu är Author och Book. En Author kommer att kunna ha flera Books och en Book kommer att ha en Author. För att skapa en entitet så högerklicka på den tomma vita ytan och välj Add -> Entity.

5 - Add Entity

Den första entiteten heter Author. Se till att sätta Entity Set till Authors och inte AuthorSet som det står som standard. Vi ser även till att ha ett ID med namnet AuthorId. Klicka på OK och gör detsamma för Book. Det vi har nu är två entiteter utan egenskaper.

Nästa steg blir att skapa de olika egenskaperna. Högerklicka i Author och välj Add -> Scalar Property. Sätt namnet till Name. Gör sedan samma sak för Book där vi istället har Title.

Nu har vi två entiteter utan någon relation till varandra. Högerklicka nu i den vita ytan och välj Add -> Association. Det vi får upp nu är:

6 - Association

Ändra Book till Books för den Navigation Property som tillhör Author då vi ska ha flera böcker för en författare. Se även till att resten ser ut som på bilden och klicka på OK.

Nu har vi två entiteter med en relation mellan varandra, vilket är precis vad vi vill ha.

7 - Entities with association

Nästa steg blir att skapa tabellerna i databasen. Högerklicka nu återgen i den vita ytan och välj nu Generate Database from Model.

När Generate Database Wizard visas så skapa en connection string till databasen som kommer att användas och klicka på Next. Det som händer nu är att Visual Studio läser av edmx-filen (den som används av Entity Framework och innehåller entiteterna vi skapade), och sedan genererar en SQL-sats som kommer att användas för att generera databasen. SQL-satsen sparas sedan ned, vilket gör att vi enkelt kan skapa upp databasen på nytt, vilket är bra om vi vill skapa en likadan i skarpa miljön.

8 - DDL

I kommentar längst upp kan vi se att SQL-satsen fungerar för SQL Server 2005, 2008 och Azure. Det betyder att om vi nu vill använda Azure för vår applikation så kan vi använda samma modell även där.

När vi klickar på Finish visas SQL:en som just genererades , samt att sql-filen skapas och läggs i projektet. Tabellerna har dock ej skapats upp i databasen än, utan det vi får göra nu är att högerklicka i den öppnade filen med all SQL och välja Execute SQL. Kikar vi i databasen nu så kan vi se att tabellerna samt relationerna har skapats upp åt oss.

9 - Generated tables

Vi skulle redan nu kunna använda modellen för att jobba mot databasen, men då kan vi inte ta del av den nya POCO-funktionaliteten. Om vi kikar i Solution Explorer så kan vi se att vi inte bara har en edmx-fil, utan även en tillhörande Designer.cs-fil.

10 - edmxcs

Det cs-filen innehåller är all logik för modellen. Det vi vill göra istället är att skapa en egen ObjectContext-fil som kommer att användas. För att få bort cs-filen så väljer vi att högerklicka på edmx-filen och välja Properties. Här tar vi bort värdet för Custom Tool.

11 - Custom tool

När vi har gjort det så försvinner cs-filen och vi har bara modellen kvar.

Det vi ska göra nu är att skapa upp POCO-klasserna, så högerklicka på projektet, välj Add -> New Item och där ADO.NET POCO Entity Generator. Vi väljer LibraryPoco.tt som namn på filen och skapar upp den. Nu kan det hända att en varning dyker upp, välj då OK för att gå vidare.

Nu har två filer skapats upp, LibraryPoco.tt och LibraryPoco.Context.tt. Ta nu bort Context-filen och flytta LibraryPoco.tt till Library.Model-projektet. Öppnar vi nu upp LibraryPoco.tt så kan vi se att den har ett väldigt annorlunda utseende, och det är då det är en T4-template. T4-templates används för att generera kod, vilket vi kan se om vi expanderar den. Där kan vi se att det finns en cs-fil, vilken har genererats. Vi kommer även att få de genererade modellerna från Entity Framework-modellen här.

Nästa steg blir att sätta sökvägen till edmx-filen i tt-filen.

På rad 24 kan vi se detta:

string inputFile = @"$edmxInputFile$";

Byt ut $edmxInputFile$ mot den relativa sökvägen till din edmx-fil. I mitt fall blir det:

string inputFile = @"..\Library.Data\LibraryEntities.edmx";

Kompilera sedan om projektet. Det som har hänt nu är att T4-templaten har använts för att generera två nya cs-filer, en för varje entitet i modellen:

12 - Generated model

Öppnar vi upp dessa så kan vi se att vi har fått rena POCO-klasser med de propertys vi har, samt relationen mellan dessa. Ingen av dessa klasser har någon som helst relation till Entity Framework förutom det faktumet att de har genererats baserat på en modell.

Nu har vi en modell och klasser som används av modellen. Det vi behöver göra nu för att kunna ställa frågor mot databasen med dessa klasser är att skapa en ObjectContext-klass för det.

Skapa en ny klass i Library.EntityRepository med namnet LibraryContext och se till att den ärver ObjectContext.

I ObjectContext-klassen så behöver vi dels en konstruktor som skapar upp ObjectSets för de olika entiteterna, samt properties för dem.

Den fullständiga klassen ser ut så här:

using System.Data.Objects;
using Library.Model;
 
namespace Library.EntityRepository
{
    public class LibraryContext : ObjectContext
    {
        private IObjectSet<Author> _authors;
        private IObjectSet<Book> _books;
 
        public LibraryContext()
            : base("name=LibraryEntitiesContainer", "LibraryEntitiesContainer")
        {
            _authors = CreateObjectSet<Author>();
            _books = CreateObjectSet<Book>();
 
            ContextOptions.LazyLoadingEnabled = true;
        }
 
        public IObjectSet<Author> Authors
        {
            get
            {
                return _authors;
            }
        }
 
        public IObjectSet<Book> Books
        {
            get
            {
                return _books;
            }
        }
    }
}

Förutom att skapa upp ObjectSet för Book och Author så har vi satt ContextOptions.LazyLoadingEnabled till true, för att vi ska slippa ladda in det själv när vi hämtar datan. Det gör att vi direkt kan få tag i till exempel Author.Books. Vi har även i konstruktorn ett anrop till konstruktorn som finns i basklassen (ObjectContext), där vi sätter namnet på vår connectionstring samt namnet på containern. Vad vi har för connectionstring kan du se i app.config om du är osäker.

Skapa ett program som använder modellen

Nu när vi har en modell, klasser för denna samt en ObjectContext så är nästa steg att skriva ett program där vi kan arbeta mot modellen, för att se så att vi får fram datan.

Innan vi skriver programmet så ska vi se till att ha någon data att hämta. Kör SQL-satsen nedan för att få in några exempel.

INSERT INTO [Library].[dbo].[Authors] (Name)
VALUES ('Astrid Lindgren')
 
INSERT INTO [Library].[dbo].[Authors] (Name)
VALUES ('H.C. Andersen')
 
INSERT INTO [Library].[dbo].[Books] ([AuthorId], [Title])
VALUES (1, 'Pippi Långstrump')
 
INSERT INTO [Library].[dbo].[Books] ([AuthorId], [Title])
VALUES (1, 'Bröderna Lejonhjärta')
 
INSERT INTO [Library].[dbo].[Books] ([AuthorId], [Title])
VALUES (2, 'Den fula ankungen')

Nu är vi redo att hämta datan, så öppna upp console-programmet och lägg in det här:

using System;
using Library.EntityRepository;
using Library.Model;
 
namespace Library.ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            using (LibraryContext ctx = new LibraryContext())
            {
                foreach (Author author in ctx.Authors)
                {
                    Console.WriteLine(author.Name);
 
                    foreach (Book book in author.Books)
                    {
                        Console.WriteLine("\t{0}", book.Title);
                    }
                }
            }
 
            Console.ReadKey(true);
        }
    }
}

Då vi för tillfället går mot den ObjectContext vi har i Library.EntityRepository så behöver vi en referens till det projektet, samt till System.Data.Entity.dll för att kunna köra koden.

Vi behöver även rätt connection string i det här projektet. Lättaste sättet att lösa det på är att dra App.Config från Library.EntityRepository till Library.ConsoleApplication. Då får vi en kopia på filen med rätt connection string.

Kör vi nu programmet så ser vi det här:

13 - Console

Vi kan hämta datan genom våra POCO-objekt nu, men vi har fortfarande en koppling mot Entity Framework i Console-programmet. För att få bort den så ska vi använda ett repository som vi kan hämta datan genom.

Skapa ett repository för att hämta datan

Vi börjar med att skapa ett interface för repositoryt. Det kommer att vara ett väldigt enkelt repository, men vi kan självklart lägga till andra metoder om vi önskar det.

Högerklicka på Library.Interfaces och välj Add -> New Item. Här väljer vi Interface och sätter namnet till IRepository.cs.

Vårt interface kommer att se ut så här:

using System;
using System.Collections.Generic;
 
namespace Library.Interfaces
{
    public interface IRepository<T>
    {
        T First(Func<T, bool> where);
        List<T> GetAll();
        void Add(T entity);
        void Delete(T entity);
    }
}

Det är ett väldigt enkelt och generiskt repository där vi anger vilken typ vi vill använda.

För att skapa en implementation av interfacet så skapar vi en ny klass i Library. Vi kallar filen för EntityRepository.cs. Det repositoryt vi kommer att använda för Entity Framework kommer även det att vara generiskt, så att vi kan välja vilken modell vi vill arbeta med.

Repositoryt i sin helhet ser ut så här:

using System;
using System.Collections.Generic;
using System.Data.Objects;
using System.Linq;
using Library.Interfaces;
 
namespace Library.EntityRepository
{
    public class EntityRepository<T> : IRepository<T> where T : class
    {
        IObjectSet<T> _objectSet;
 
        public EntityRepository()
        {
            _objectSet = new LibraryContext().CreateObjectSet<T>();
        }
 
        public T First(Func<T, bool> where)
        {
            return _objectSet.Where(where).FirstOrDefault();
        }
 
        public List<T> GetAll()
        {
            return _objectSet.ToList<T>();
        }
 
        public void Add(T entity)
        {
            using (var ctx = new LibraryContext())
            {
                _objectSet.AddObject(entity);
                ctx.SaveChanges();
            }
        }
 
        public void Delete(T entity)
        {
            using (var ctx = new LibraryContext())
            {
                _objectSet.DeleteObject(entity);
                ctx.SaveChanges();
            }
        }
    }
}

För att få det så generellt som möjligt mot Entity Framework så går vi inte direkt mot objekten i sig, utan skapar ett ObjectSet baserat på modellen. På så vis kan vi använda vilken modell vi vill.

För att testa det så ändrar vi i console-programmet till:

using System;
using Library.EntityRepository;
using Library.Interfaces;
using Library.Model;
 
namespace Library.ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            IRepository<Author> _authors = new EntityRepository<Author>();
 
            foreach (Author author in _authors.GetAll())
            {
                Console.WriteLine(author.Name);
 
                foreach (Book book in author.Books)
                {
                    Console.WriteLine("\t{0}", book.Title);
                }
            }
 
            Console.ReadKey(true);
        }
    }
}

Vi har inte längre någon referens till Entity Framework, utan jobbar enbart med objekten i vår modell. Då vi använder Irepository<T> så kan vi även enkelt byta ut repositoryt mot något annat, vilket kan vara lämpligt om vi skall skriva enhetstester.

För att testa hur väl det fungerar att byta ut repositoryt så skapar vi en ny implementation av Irepository<T> i Library.TestRepository. Vi kallar repositoryt för FakeRepository och gör det tämligen simpelt:

using System;
using System.Collections.Generic;
using System.Linq;
using Library.Interfaces;
using Library.Model;
 
namespace Library.TestRepository
{
    public class FakeRepository : IRepository<Author>
    {
        public Author First(Func<Author, bool> where)
        {
            return GetAll().Where(where).FirstOrDefault();
        }
 
        public List<Author> GetAll()
        {
            var authors = new List<Author>()
            {
                new Author()
                {
                    AuthorId = 1,
                    Name = "Nisse"
                },
 
                new Author()
                {
                    AuthorId = 2,
                    Name = "Mikael"
                }
            };
 
            return authors;
        }
 
        public void Add(Author entity)
        {
            //Gör ingenting nu
        }
 
        public void Delete(Author entity)
        {
            //Gör ingenting nu
        }
    }
}

Det vi har här är två hårdkodade författare som fortfarande inte har skrivit någon bok. Om vi ändrar i console-programmet från:

IRepository<Author> _authors = new EntityRepository<Author>();

Till:

IRepository<Author> _authors = new FakeRepository();

Så får vi det här när vi kör:

14 - FakeRepository Console 

Vi har nu gjort det möjligt att enkelt byta repository vid behov, och vi är inte heller knutna till Entity Framework.

Nästa steg blir att skapa ett ASP.NET MVC-program där vi tar nytta av funktionaliteten, samt utöver det använder AutoMapper för att mappa om modellerna till view models, samt använder Ninject för att enkelt kunna injicera ett repository till våra controllers från ett och samma ställe.

Använd repositoryt i ett ASP.NET MVC-projekt

I ASP.NET MVC kan vi enkelt injicera repositorys och annat till våra controllers. Då vi har ett ASP.NET MVC-projekt redo så skapar vi upp en ny controller i detta vid namn ”LibraryController”. Här skapar vi en konstruktor som tar emot IRepository<Author>, samt en default-konstruktor som sätter ett standard-repository (det används normalt av ASP.NET MVC).

Controllern ser nu ut så här:

using System.Web.Mvc;
using Library.EntityRepository;
using Library.Interfaces;
using Library.Model;
 
namespace Library.Mvc.Controllers
{
    public class LibraryController : Controller
    {
        private IRepository<Author> _authors;
 
        public LibraryController() : this(new EntityRepository<Author>()) { }
 
        public LibraryController(IRepository<Author> authorRepository)
        {
            _authors = authorRepository;
        }
 
        // GET: /Library/
        public ActionResult Index()
        {
            return View();
        }
    }
}

Det vi returnerar i Index-metoden kommer att vara en vymodell med namnet AuthorViewModel. Vi skapar därför upp en ny klass i Models-mappen med det namnet.

Vymodellen har properties för Id på författaren, namn på författaren samt antalet böcker som denne har skrivit.

namespace Library.Mvc.Models
{
    public class AuthorViewModel
    {
        public int AuthorId { get; set; }
        public string Name { get; set; }
        public int BooksCount { get; set; }
    }
}

Vi vill sedan hämta alla författare, samt mappa om till den här vymodellen.

Om du inte redan har gjort det, så lägg till referenser till AutoMapper.dll, Ninject.dll och Ninject.Web.Mvc.dll.

För att enkelt mappa om datan så använder vi AutoMapper. Vi skapar därför en ny klass, AutoMapperBootstrapper, där vi skapar upp mappningarna.

using System.Linq;
using AutoMapper;
using Library.Model;
using Library.Mvc.Models;
 
namespace Library.Mvc
{
    public class AutomapperBootstrapper
    {
        public static void Initialize()
        {
            Mapper.CreateMap<Author, AuthorViewModel>()
                .ForMember(viewmodel => viewmodel.BooksCount,
                author => author.MapFrom(a => a.Books.Count()));
        }
    }
}

Förutom att bara mappa om objektet här så sätter vi egenskapen BooksCount till antalet böcker som finns för den aktuella författaren.

I Application_Start i global.asax anropar vi Initialize(), vilket gör att vi kan använda oss utav mappningen i vår controller.

protected void Application_Start()
{
    AutomapperBootstrapper.Initialize();
    ...
}

För att mappa om objektet och returnera vår vymodell så lägger vi nu till följande i Index-metoden i controllern:

// GET: /Library/
public ActionResult Index()
{
    var authors = _authors.GetAll();
    var authorViewModel = Mapper.Map<List<Author>, List<AuthorViewModel>>(authors);
 
    return View(authorViewModel);
}

Nästa steg blir att skapa en vy för Index. Vi väljer att skapa en hårt typad vy mot Library.Mvc.Models.AuthorViewModel, samt väljer List.

Innan vi testar att starta projektet så se till att återigen lägga in rätt connection string i config-filen. Kopiera den befintliga från App.Config och klistra in under ConnectionStrings i web.config.

Surfar vi nu till sidan så får vi upp det här:

15 - Mvc List

Vi kan enkelt hämta data utan att vara kopplad mot Entity Framework på något sätt, samt har gjort det möjligt att enkelt ändra repository. Det finns dock ett problem – vi måste ange repository direkt i controllern. Vi vill istället använda dependency injection för att injicera rätt repository på alla ställen där vi använder Irepository<T>. Det finns väldigt många ramverk för det, men jag har valt Ninject då det på ett smidigt sätt låter oss göra det.

Använd dependency injection för att ange repository

Det är relativt enkelt att injicera kod in i våra controllers. Ninject som vi kommer att använda nu har ett plugin som låter oss göra det direkt i global.asax.

För att kunna göra det så måste vi först och främst ändra från HttpApplication till NinjectHttpApplication i global.asax. Vi ska sedan byta ut Application_Start mot Ninjects metod OnApplicationStarted.

protected override void OnApplicationStarted()
{
    AutomapperBootstrapper.Initialize();
    AreaRegistration.RegisterAllAreas();
    RegisterRoutes(RouteTable.Routes);
}

När vi vill injicera kod med Ninject så kan vi välja en eller flera NinjectModule-klasser där vi har bundit olika interfaces till klasser. Det gör vi i en metod kallad CreateKernel som även den ligger i MvcApplication.

protected override IKernel CreateKernel()
{
    return new StandardKernel(new ServiceModule());
}

StandardKernel är en separat klass som vi har lagt i global.asax, men den kan lika gärna ligga i ett separat projekt om man så vill. Det är även möjligt att ladda in dessa dynamiskt.

internal class ServiceModule : NinjectModule
{
    public override void Load()
    {
        Bind<IRepository<Author>>().To<EntityRepository<Author>>();
    }
}

Här gör vi själva bindningen mellan Irepository<Author> och EntityRepository<Author>. På alla ställen där vi vill injicera Irepository<Author> kommer detta att slå igenom. Vi kan nu alltså ta bort default-konstruktorn från vår controller:

using System.Collections.Generic;
using System.Web.Mvc;
using AutoMapper;
using Library.Interfaces;
using Library.Model;
using Library.Mvc.Models;
 
namespace Library.Mvc.Controllers
{
    public class LibraryController : Controller
    {
        private IRepository<Author> _authors;
 
        public LibraryController(IRepository<Author> authorRepository)
        {
            _authors = authorRepository;
        }
 
        // GET: /Library/
        public ActionResult Index()
        {
            var authors = _authors.GetAll();
            var authorViewModel = Mapper.Map<List<Author>, List<AuthorViewModel>>(authors);
 
            return View(authorViewModel);
        }
    }
}

Byter vi ut EntityRepository<Author> mot FakeRepository så kommer vi att använda FakeRepository på alla ställen istället.

Sammanfattning

Vi har nu skapat en ASP.NET MVC-sida där vi enkelt kan injicera valfritt repository, samt även automatiskt mappar om detta till en vymodell. Hela lösningen är väldigt skalbar, och blir dessutom enkel att testa. Vi skulle även utöver detta kunna skapa andra typer av repositorys och mappningar mot andra objekt om vi skulle vilja det, och då göra dessa lika skalbara med hjälp av den funktionaliteten vi redan har.

Artikeln som pdf och xps, samt exempel koden finns att ladda ned här:
http://cid-4aa13e17331c7398.office.live.com/browse.aspx/Public/ASP.NET/POCO%20med%20Entity%20Framework

För att kunna skicka värden till våra action-metoder tvid vanliga GET-anrop så kan vi använda route-värden. Som standard i ett ASP.NET MVC-projekt finns ett sådant värde i routen vid namn ”id”. I global.asax ser det ut så här:

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

Det som anges där är att vi eventuellt kan ta in en parameter vid namn id. Den är dock satt som optional, så det finns inget krav på att något id ska skickas med.

För att se om något värde tas emot när vi surfar till /Home/Index/123 så ändrar vi på Index-metoden i HomeController till:

Public ActionResult Index(string id = "-1")
{
    ViewData["Message"] = "Welcome to ASP.NET MVC!";
    ViewData["id"] = id;
 
    return View();
}

Vi ändrar även i vyn till:

<%@PageLanguage="C#"MasterPageFile="~/Views/Shared/Site.Master"Inherits="System.Web.Mvc.ViewPage"%>
<asp:ContentID="Content1"ContentPlaceHolderID="TitleContent"runat="server">
    Home Page
</asp:Content>
<asp:ContentID="Content2"ContentPlaceHolderID="MainContent"runat="server">
<h2><%: ViewData["Message"] %></h2>
<p>
        ID: <%: ViewData["id"] %>
</p>
</asp:Content>

I Index-metoden så tar vi nu emot parametern id om den finns. Annars används standardvärdet -1. Surfar vi nu till /Home/Index/123 så får vi upp det här:

1 - LongRoute

Ett problem nu är att vi kan skicka in t.ex. ”123abc” som värde, vilket inte är vad vi har förväntat oss då vi vill ha ett heltal. Det vi kan göra för att försäkra oss om att det är just tal som skickas in så kan vi ändra direkt i action-metoden så att parametern för id är en int istället för string. Då ignoreras alla strängar och vi får istället standardvärdet.

Om vi nu istället vill ha mer kontroll, och enbart skicka användaren dit om det är just ett tal så kan vi använda Route Constraints. På så vis kan vi styra användaren till olika controllers helt baserat på vad som skickas in. För att tydligt visa detta så kan vi skapa en ny route-mappning som kollar om användaren anger ett heltal istället för controller-namnet (till exempel /12345 istället för /home).

För att lyckas med det så skapar vi en ny MapRoute i global.asax:

routes.MapRoute(
"Id",
"{id}",
new {controller = "Home", action = "Index" },
new { id = @"\d+" }
);

Det här är en väldigt enkel MapRoute som använder en route constraint i form av ett regular expression på sista raden. Det kollar om ett heltal har skickats in och körs i sådana fall. Om det inte är ett heltal så ignoreras routen och ASP.NET MVC väljer automatiskt den som passar bäst (vilket är standard-routen).

Surfar till /123 så ser vi på sidan att ID är 123, och surfar vi till /home så är ID satt till -1. Nu har vi angett samma controller och action, men vi kan självklart sätta till exempel ”Product” som controller och ”Details” som action om vi hade velat det. På så vis kan vi enkelt skapa nya routes med specifika regler. Alla länkar som vi har genererat med de medföljande html helpers som följer med kommer automatiskt att skickas till /{id} om den routen passar bäst.

Det här kan dock leda till ett problem. Vi får inte full kontroll, utan kan bara använda regular expressions för att ange regler som skall användas. Vad händer om vi vill ha logik bakom? Ett exempel på det skulle vara om vi skickar användaren till ”/billig-bil-till-salu” och då vill göra ett uppslag mot databasen för att se vilket id det skall mappas till (så kallad ”slug”).

För att göra det möjligt så kan vi skapa en egen route constraint genom att implementera IrouteConstraint och metoden ”bool Match(...)”. Här returnerar vi true eller false beroende på om requesten bör hanteras av vår route constraint.

I vårt fall ska vi ha en IdRouteConstraint som ska se om värdet dels är ett tal, men även om värdet är över 3.

En väldigt enkel implementation av det skulle kunna se ut så här:

using System;
using System.Web;
using System.Web.Routing;
 
namespace MvcRouteConstraints
{
public class IdRouteConstraint : IRouteConstraint
    {
        public bool Match(HttpContextBase httpContext,
            Route route, string parameterName,
            RouteValueDictionary values,
            RouteDirection routeDirection)
        {
            int id;
 
            if (!Int32.TryParse(values[parameterName] asstring, out id))
                return false;
 
            if (id > 3)
                return true;
 
            returnfalse;
        }
    }
}

För att använda vår route constraint så får vi ändra lite i global.asa:

routes.MapRoute(
  "Id",
  "{id}",
  new { controller = "Home", action = "Index" },
  new { id = newIdRouteConstraint() }
);

Surfar vi nu till /1 så får vi ett 404-meddelande då vi dels inte accepterar så låga tal i vår Route Constraint, samt då det inte finns någon Controller vid namn ”1”. Surfar vi däremot till /4 så får vi fram 4 som id. Vi kan även surfa till /Home, vilket ger oss -1 som id då vi inte skickade in något.

Det här gör det väldigt enkelt att skapa egna utseenden på routes, samt lägga in egen validering för dem.

Vad gäller ”slugs” som jag nämnde tidigare så kan jag rekommendera en titt på modelbinders i ASP.NET MVC för information om hur man kan skapa sådana.

More Posts