April 2009 - Posts

Jag har tidigare skrivit några artiklar om bl.a. hur man kommer igång med Windows Cardspace och hur man använder det med Javascript. De har dock tagit upp hur det fungerar med ASP.NET Web Forms, men för att få det att fungera med ASP.NET MVC på serversidan så måste man tänka lite annorlunda.

Det vi ska göra är att använda standardsajten som följer med ASP.NET MVC och bygga ut den med CardSpace-stöd. Den har från början inbyggt stöd för ASP.NET Membership, och den vill vi helt enkelt bara bygga ut. När en person har registrerat sig och loggat in så skall vi göra det möjligt för denne att associera ett InfoCard med sitt konto. Efter det skall det vara möjligt för användaren att logga in på sidan genom att endast skicka in sitt InfoCard.

Då det redan finns funktionalitet för att skapa användare och låta dem logga ut på sidan så behöver vi inte ta med det nu, men vi behöver däremot skapa en ny Controller som jag i exemplet kallar för CardSpaceController. Vi behöver även en mapp under Views med namn CardSpace.

Controllern skall ha action-metoder för att logga in samt associera ett InfoCard med kontot. Jag tog grunden av koden från den befintliga AccountController-klassen för att få alla nödvändiga properties, men har skrivit om den för att få den här strukturen:

[HandleError]
public class CardSpaceController : Controller
{
    public CardSpaceController() : this(null, null) { }
 
    public CardSpaceController(IFormsAuthentication formsAuth, IMembershipService service)
    {
        FormsAuth = formsAuth ?? new FormsAuthenticationService();
        MembershipService = service ?? new AccountMembershipService();
    }
 
    public IFormsAuthentication FormsAuth
    {
        get;
        private set;
    }
 
    public IMembershipService MembershipService
    {
        get;
        private set;
    }
 
    public ActionResult LogOn()
    {
        //Visa vy för inloggning
    }
 
    [AcceptVerbs(HttpVerbs.Post)]
    [ValidateInput(false)]
    public ActionResult LogOn(string xmlToken)
    {
        //Använda det inskickade xmlToken för att hämta den användaren som har detta.
    }
 
    public ActionResult AssociateInfoCardWithAccount()
    {
        //Visa en sida där användaren kan associera kontot genom att klicka på en knapp.
    }
 
    [AcceptVerbs(HttpVerbs.Post)]
    [Authorize]
    [ValidateInput(false)]
    public ActionResult AssociateInfoCardWithAccount(string token)
    {
        //Spara token för det aktuella kontot
    }
}

Det vi har här är två olika typer av action-metoder, vi har en för att visa en inloggningssida samt genomföra inloggningen, samt en för att visa associeringssidan samt associera kontot.

Då det är en XML-fil som skickas genom Windows CardSpace så kommer ValidateRequest att sätta stopp för detta. Med ASP.NET Web Forms så fick vi då sätta validateRequest till false i antingen page-direktivet eller i page-elementet i web.config, men då vi kör ASP.NET MVC här så fungerar det annorlunda. Då man kan nå en aspx-fil genom flera olika action-metoder, samtidigt som en action-metod kan rendera olika vyer så kan man inte längre sätta detta. Istället så får vi använda ValidateInput(false), vilket gör detsamma.

I de metoder där vi skickar in data har vi även satt AcceptVerbs så att bara postningar kan komma till dessa metoder. Om det är ett get-anrop så visar vi istället formulärsvyerna.

Vi vill även att man bara skall kunna associera ett konto om man är inloggad, så på de metoderna har vi Authorize-attributet.

För att hämta ut det PPID som skickas med i XML-filen så kommer jag att använda samma metoder som jag har med i tidigare artiklar.

För att logga in så hämtar jag i LogOn-metoden (den som man postar till) en Xml-token och hämtar därifrån e-postadress samt PPID. Sedan kollar jag om det finns någon användare med det PPID:t och loggar då in med denne.

[AcceptVerbs(HttpVerbs.Post)]
[ValidateInput(false)]
public ActionResult LogOn(string xmlToken)
{
XmlDocument doc = new XmlDocument(); 
doc.LoadXml(Request.Form["xmlToken"]);
 
string email = doc.SelectSingleNode("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@AttributeName='emailaddress']", GetNamespaces(doc)).InnerText;
string ppid = doc.SelectSingleNode("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@AttributeName='privatepersonalidentifier']", GetNamespaces(doc)).InnerText;
 
MembershipUserCollection users = Membership.FindUsersByEmail(email);
 
foreach (MembershipUser mu in users)
{ 
    if (mu.Comment != null && mu.Comment.Equals(ppid))
        FormsAuthentication.RedirectFromLoginPage(mu.UserName, false);
}
 
return View();
}

Om användaren inte kan loggas in så visas återigen LogOn-vyn.

För att associera ett InfoCard med ett konto så sparar jag undan PPID:t i användarens kommentarsfält. Detta leder till att man bara kan ha ett InfoCard associerat till kontot, men det är lätt att bygga ut.

[AcceptVerbs(HttpVerbs.Post)]
[Authorize]
[ValidateInput(false)]
public ActionResult AssociateInfoCardWithAccount(string token)
{
    MembershipUser user = Membership.GetUser();
    user.Comment = GetPpid();
    Membership.UpdateUser(user);
 
    return RedirectToAction("Index", "Home");
}

När användaren har associerat kontot så kommer man återigen till startsidan.

Det här är ett enkelt exempel på hur man kan låta användaren logga in på en ASP.NET MVC-sida med ett InfoCard istället för användarnamn och lösenord. Just det här exemplet kräver att man kör utan SSL, vilket man aldrig bör göra. Om man har SSL aktiverat så kommer dock XML-filen att krypteras. Koden bör inte användas i produktion, utan är som sagt bara en demonstration på hur det kan fungera.

All kod med vyerna inkluderade finns att ladda ned här:
http://cid-4aa13e17331c7398.skydrive.live.com/self.aspx/Public/ASP.NET/MvcCardSpace.rar

När man skapar formulär i ASP.NET med web forms så finns det ett gäng olika kontroller som TextBox, DropDownList, Button m.m. Dessa kan sedan bindas mot olika event, vilket genererar olika javascript och HTML-element som vi inte kan styra över. Dessutom skapas olika id:n, vilka vi inte har kontroll över (förändras dock i ASP.NET 4.0, där vi äntligen kan anpassa dessa).

All denna autogenererade HTML och Javascript-kod leder till vissa stora nackdelar:

  • Stor ViewState, vilken är nästan omöjlig att bli av med. Den innehåller en Base64-kodad sträng (kan dock krypteras) med information om olika element, så att informationen skall bevaras mellan postbacks.
  • Ingen kontroll över HTML-koden som renderas.
  • Ingen kontroll över id-attributet, vilket försvårar vid användning av javascript och css (blir bättre i ASP.NET 4.0).
  • Postbacks som sker via javascript är ej tillgänglighetsanpassade och blockerar många användare. LinkButton är ett exempel på en kontroll som inte fungerar utan javascript.

Det har funnits mängder av önskemål som helt enkelt pekar mot:

  • Ingen ViewState.
  • Full kontroll över HTML-koden som renderas.
  • Full kontroll över id-attributet.
  • Inget postback-javascript.

Med andra ord så vill vi ha ett enkelt och smidigt sätt att skapa hemsidor på, där vi har full kontroll över vad som renderas, men samtidigt kan ta nytta av det enorma klassbibliotek vi får genom .NET Framework.

Lösningen på detta finns i ASP.NET MVC. Vi kan här skapa formulär och samtidigt ha 100% kontroll över vad som renderas hos klienten. De traditionella webbkontrollerna fungerar inte här då vi inte längre har postbacks och ViewState, men istället så har vi helper-klasser som kan underlätta vid skapandet av kontrollerna. Vi behöver dock inte dessa för att skapa formulär, men de underlättar mycket om vi har t.ex. databindning mot en dropdown-lista.

Till att börja med så har jag skapat upp ett nytt ASP.NET MVC-projekt (dock utan testprojekt då artikeln inte tar upp det). Nästa steg är att skapa en modell i form av Customer.cs i Models-mappen:

namespace Forms.Models
{
    public class Customer
    {
        public string Firstname { get; set; }
        public string Lastname { get; set; }
        public int Age { get; set; }
    }
}

Det här är en enkel Customer-klass som vi kommer att använda i formuläret. Klassen kommer att användas i formuläret.

Nästa steg är att skapa upp en Controller som skall ta hand om all interaktion med användaren. Högerklicka på Controllers-mappen, Välj Add –> Controller, ge den namnet CustomerController och välj att lägga till metoder för att skapa, uppdatera och visa detaljer, även om vi inte kommer att använda alla dessa – det enda vi kommer att använda här är just den delen som skapar nya poster.

Med lite modifieringar så har vi kvar det här:

using System.Web.Mvc;
 
namespace Forms.Controllers
{
    public class CustomerController : Controller
    {
        // GET: /Customer/
        public ActionResult Index()
        {
            return View();
        }
 
        // GET: /Customer/Create
        public ActionResult Create()
        {
            return View();
        } 
 
        // POST: /Customer/Create
        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create(FormCollection collection)
        {
            return RedirectToAction("Index");
        }
    }
}

Vi har dels en metod “Index” som är startsidan, men även två Create-metoder. Den utan parametrar är den som används för att helt enkelt bara visa sidan (dvs vid GET), den andra används när man istället har postat mot /Customer/Create. ASP.NET MVC känner automatiskt igen vilken som skall användas vid det aktuella scenariot.

Det vi behöver nu är först och främst ett formulär att använda vid skapande av nya customers. Detta formulär skall ligga under GET-varianten av Create, så genom att högerklicka i metoden och sedan välja “Add View…” så kan vi skapa en vy för denna händelse.

1

Det vi får upp då är en ruta med inställningar för vyn. Vi kan välja om vi vill ha en partiell vy (ascx, fungerar som user controls med web forms), vi kan använda en starkt typad vy (underlättar mycket då vi enkelt kan använda en befintlig modell), vi kan skapa en genererad vy baserad på modellen och vi kan även välja att använda en master page.

I det här fallet så har vi redan en modell som vi vill använda oss utav, så vi väljer att skapa en starkt typad vy (ProjektNamn.Models.Customer, syns den inte så testa att kompilera om projektet) samt att det skall vara av typen “Create”. Dessa olika typer som kan autogenereras bygger på T4-mallar, vilket är standard i Visual Studio. De är dock relativt okända, men väldigt smidiga.

Det bör se ut i stil med det här:

2

När vi nu har skapat filen så kan vi se att vi faktiskt har fått ett genererat formulär med fält baserade på fälten från modellen.

I koden så har vi först och främst en Html.ValidationSummary(), vilket är en av många extension methods i Html-klassen, vilket är en helper med funktioner att använda när vi utvecklar ASP.NET MVC-sidor. Om något har gått fel vid skapandet av sidan så får vi informationen visad här.

Vi har även ett gäng olika Html.TextBox(), vilket renderar vanliga textrutor där vi kan fylla i datan. Dessa har även Html.ValidationMessage(), vilka visar om inputen är felaktig.

Runt dessa textrutor så har vi dock ett using-block med Html.BeginForm, vilket skapar upp ett HTML-formulär runt input-kontrollerna.

Längst ner på sidan så har vi även en Html.ActionLink(). De används för att skapa länkar baserade på den routen som passar bäst in på länken.

Om vi nu surfar in på /Customer/Create så kan vi se detta:

3

Vi har alltså ett formulär där vi kan skriva in vilken typ av data som helst. När vi klickar på Create-knappen så får vi dock ett fel då vi inte har någon Index-sida, vilket är helt korrekt. Anledningen till att den försöker hitta just den sidan är för att vi har den här raden i POST-metoden för Create:

return RedirectToAction("Index");

Allt stämmer alltså än så länge.

För att se om alla värden kommer in korrekt så lägger vi till dessa rader innan RedirectToAction(), samt lägger till en breakpoint på just den raden så att vi enkelt kan se om alla värden kom fram som de skall:

string firstname = collection["Firstname"];
string lastname = collection["Lastname"];
string age = collection["Age"];

Om vi startar debugging, fyller i alla fälten och väljer att skicka så bör vi ha fått upp alla värden korrekt:

4

När vi ser att allt har kommit in korrekt så kommer nästa problem. Vi har olika helpers för validering på sidan, men som det ser ut nu så aktiveras dem aldrig. Det vi behöver göra är att kolla så att alla fälten är ifyllda, och om något inte är det så tvingar vi användaren till att fylla i dessa.

Det som sker nu är att vi först och främst skall ändra i vyn så att vi inte längre tar emot en FormCollection, utan istället rena objekt. Förnamn och efternamn är strängar, så de vill vi ska vara string. Åldern vill vi ha som en int, vilken skall vara över 0 för att valideras.

Då ASP.NET MVC kan mappa om alla fältens värden i formuläret till POCO så kan vi sätta dessa direkt som parametrar. Om man inte anger någon ålder så kommer den dock att vara null, vilket gör att vi kommer att behöva använda en nullable int för åldern.

Sedan kommer vi att lägga till eventuella fel i ViewData.ModelState. ViewData innehåller data för den aktuella vyn, och ModelState innehåller information om statusen på modellen. I det här fallet så innehåller den fel om något fält saknas, vilket vi enkelt kan kontrollera med ViewData.ModelState.IsValid som är false om så är fallet.

Koden som den ser ut nu:

// POST: /Customer/Create
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(string firstname, string lastname, int? age)
{
    if (String.IsNullOrEmpty(firstname))
        ViewData.ModelState.AddModelError("Firstname", "Du måste fylla i ditt förnamn.");
 
    if (String.IsNullOrEmpty(lastname))
        ViewData.ModelState.AddModelError("Lastname", "Du måste fylla i ditt efternamn.");
 
    if (!age.HasValue || age < 0)
        ViewData.ModelState.AddModelError("Age", "Du måste fylla i din ålder.");
 
    if (ViewData.ModelState.IsValid)
        return RedirectToAction("Index");
    else
        return View();
}

Om vi nu kör den här koden och skippar efternamn och ålder så får vi upp detta:

5

Vi kan alltså automatiskt mappa om elementen mot objekt i koden, utan att egentligen behöva cast:a om typerna själva, då detta sker automatiskt av ASP.NET MVC.

Uppdatera modellen

Om vi istället vill skapa upp ett Customer-objekt med dessa värden så skulle vi kunna skapa en ny instans av det och sedan sätta alla properties manuellt baserat på parametrarna. Ett annat sätt att lösa det på är att göra det genom att använda UpdateMode-metoden. Den tar emot namnet på modellen, alla värden från postningen samt eventuellt en whitelist samt blacklist.

Vi kommer att behöva gå tillbaka till en FormCollection, skapa upp en instans och sedan automatiskt få fälten ifyllda.

6

Vi kan här se att alla fälten automatiskt fylldes i enligt vår whitelist. Det här gör det enkelt att generera ett objekt baserat på data från formuläret.

För att göra det ännu lättare så kan vi även se till att ta emot ett Customer-objekt som parameter, vilket gör att vi inte behöver gör något alls för att automatiskt få objektet med alla värden direkt från formuläret. Genom att bara ange en Customer så får vi det här:

7

Vi behöver med andra ord inte göra något alls, utöver att säga att vi vill ha tillbaka ett Customer-objekt från formuläret. Detta kan vi sedan spara ned direkt till databasen om så önskas.

Att direkt hämta ett objekt på detta sätt kan dock leda till vissa problem. Det kan hända att vi behöver exkludera fält, som t.ex. ett ID-fält, då detta inte skall läggas in manuellt. Genom att använda Bind-attributet för parametern så kan vi få in en whitelist, samt blacklist direkt på parametern.

// POST: /Customer/Create
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create([Bind(Include = "Firstname,Age", Exclude = "Lastname")]Customer customer)
{
    return RedirectToAction("Index");
}

Om vi har denna bindning så kan vi se att customer innehåller förnamn och ålder, men null för efternamnet. På så vis kan vi enkelt exkludera data som vi inte vill ha med. Bind-attributet innehåller även en prefix-property där vi kan ange om fälten i formuläret har något prefix. Om vi har “formAge” i formuläret så kan vi sätta Prefix till “form”, vilket gör att endast Age kommer med och behandlas.

Absolut klockren video om Twitter:

http://www.youtube.com/watch?hl=sv&v=PN2HAroA12w&gl=SE

Microsoft har släppt källkoden till ASP.NET July 2007 Futures.

Medföljande delar:

Bridge
The Bridge enables Ajax applications to call Web services from different domains, something that is in theory not possible using only the browser’s XMLHttpRequest API available in most current browsers. The Bridge acts as a proxy for the remote service, enabling the Ajax application’s client code to query the local application’s Bridge, which will query the actual remote service and then forward the results to the client.
We decided not to include this functionality into ASP.NET Ajax out of concerns for security and performance, but are making the code freely available here.

Search
This project first enables Web applications to build and expose standard XML SiteMap files in order to help achieve efficient search engine crawling and indexing. Second, it makes it easy to integrate Live Search into a Web application, enabling full-text search over the public sections of the application.

Xml-Script
Xml-Script is a client-side declarative way of instantiating components that was developed as part of early previews of ASP.NET Ajax. It is accompanied by components that enable the declarative instantiation of JavaScript representations of common elements such as inputs and buttons, as well as by a simple animation framework.
This project is not under development today, but we’re making the source code available here. The features in this project have been replaced by the simpler declarative syntax in ASP.NET Ajax 4.0 and by the animation frameworks in Ajax Control Toolkit and jQuery, which are the recommended equivalent technologies today.

Diagnostics
Client-side errors on client machines usually get unnoticed by the application developer because there isn’t a good way to detect them remotely and get them back to the server for logging. This project makes this possible by enabling client code to send error reports back to the server.

ExtractJSFromAssembly
For better performance or to be able to patch JavaScript files, it is often preferable to work with static JavaScript files in the web application’s file system rather than with resource-bound, dynamically served JavaScript. This command-line tool extracts static JavaScript files from an assembly’s embedded resources. Debug and release versions are handled, as well as localized versions.

Ladda ned här.

Microsoft släppte ganska nyligen ett tillägg till ASP.NET 3.5 SP 1, vid namn ASP.NET MVC. Det här kan ses som ett alternativt sätt att skriva ASP.NET-applikationer på, vid sidan av ASP.NET Web Forms, som tidigare var det enda sättet att skriva ASP.NET-applikationer på. MVC-mönstret är inget nytt, utan har funnits sedan 1979, alltså i 30 års tid.

Det som gör att Microsoft har fått bra respons vad gäller ASP.NET MVC är att det är mycket lättare att testa, det har ingen ViewState (alla ASP.NET-utvecklare vet hur kolossal den kan bli, det är dock förbättrat i ASP.NET 4.0, men mer om det i någon annan post), inga postbacks, ingen code behind m.m. ASP.NET MVC är egentligen ASP.NET Web forms utan allt lull-lull runtomkring. Då det inte finns någon viewstate så försvinner även möjligheten att använda webbkontroller, vilket kan låta lite skrämmande. Det kan dock ses som en fördel, då det faktiskt tvingar oss att skriva koden som den skall se ut, vi kan alltså inte vara för lata och därmed öka möjligheten att rendera dålig HTML. Om man ser på till exempel GridView, calendar, LinkButton osv så kan man se att de slänger ur sig riktigt dålig HTML. Något system där man tydligt kan se vad webbkontrollsberoende kan leda till är SharePoint (HTML:en där är fantastiskt dålig).

Men förutom just på klientsidan så har vi även möjlighet att påverka allt som sker på servern. MVC är en akronym för Model-View-Controller, vilket förklarar hur applikationen skall byggas upp.

En förklaring på de olika stegen

Model

Modellen tar hand om datan och returnerar den som objekt, vilka sedan kan användas av de andra lagren. Det kan ses som en person som åker till IKEA och köper en möbel. Den är inte ihopskruvad när personen köper den, men allting finns strukturerat då IKEA redan har pakterat allt och gjort det möjligt att hämta ut det man vill.

View

I vyn så presenteras modellen på ett sätt som gör det möjligt för användaren att arbeta med det som kommer fårn modellen. Här har personen skruvat ihop möbeln och ställt fram den. Man kan nu se slutresultatet.

Controller

Controllern innehåller all funktionalitet som krävs för att vi skall kunna använda och jobba mot vyn. Om vår möbel är en bokhylla med olika skåp så använder vi controllern för att fylla bokhyllan med böcker, samt hämta en lista på böckerna och presentera dem för användaren. Vi kan här även välja att fylla bokhyllan med enbart de böcker som är skrivna av H.C. Andersen.

Skapa en ASP.NET MVC-applikation

Vad förväntas av dig?

Innan du läser vidare så tar jag förgivet att du sitter med antingen Visual Studio (2008 SP 1 eller senare) eller Visual Web Developer 2008 SP 1 (eller senare), eller motsvarande. Jag antar även att du har åtminstonde grundläggande kunskap inom webbutveckling, och gärna även kunskap om programmering på serversidan (helst ASP.NET).

Om du inte uppfyller dessa förväntningar så kan du få det svårt att förstå vad det handlar om, och bör börja med att komma igång med grunderna.

Skapa mallsidan av ASP.NET MVC

När man har installerat ASP.NET MVC så följer det med en standardmall i Visual Studio (även Visual Web Developer, men jag kommer att referera till Visual Studio i fortsättningen). Denna standardmall kan användas för att man enkelt skall kunna komma igång med en ASP.NET MVC-sida och kunna gå vidare med det riktiga arbetet.

ASP.NET MVC fungerar enbart med Web Application Project, och inte Web Site, vilket är en av anledningarna till varför man måste ha Service Pack 1 till Visual Web Developer om man använder det.

Det vi gör är att vi börjar med att skapa ett nytt projekt, väljer Web och sedan ASP.NET MVC Web Application, väljer ett namn på projektet och klickar på OK.

1

Det som sker när vi klickar å OK är att vi får en fråga om vi vill skapa ett unit testing-projekt tillsammans med ASP.NET MVC-projektet. Som standard kan man välja Visual Studio Unit Test, men om man har MBUnit, xUnit eller annat så kan man även använda dessa, då det finns en möjlighet för tredjepartsprodukter att integrera sig enkelt i processen.

2

Jag kommer att välja nej på denna fråga då posten handlar om ASP.NET MVC i sig, men om man skall arbeta med ett projekt som skall användas skarpt så bör man absolut testa det för att vara helt säker (nåja, åtminstonde nästan helt säker) på att allt fungerar som det ska.

När vi har klickat på OK även här så har vi fått ett nytt projekt skapat med alla filer, mappar referenser och så vidare som behövs för att vi skall kunna använda ASP.NET MVC.

Projektet som är skapat bör se ut i stil med det här:

3

Vi kan se att det bl.a. finns tre olika mappar för just MVC, det vill säga Model, View och Controller. I View så har vi sedan olika mappar för olika routes (kommer till det snart). Utöver det så har vi vanliga filer som web.config, default.aspx (dock enbart en placeholder, används inte - egentligen), global.asax osv. Vi kan även se att vi får med jQuery 1.3.2 med tillhörande intellisense i projektet.

Bland referenserna har vi dels System.Web.Mvc, men även System.Web.Routing som kom med i .NET 3.5 SP 1, det vill säga innan ASP.NET MVC. Routing är det som ASP.NET MVC använder sig för att kunna få till URL:erna så att de bli vänligare för besökaren. Det används även bl.a. i ASP.NET Dynamic Data som även det kom med ASP.NET 3.5 SP 1. Just vad gäller Routing så är det många missförstånd, det finns många som tror att det är en del utav ASP.NET MVC, men så är inte fallet. Det går utmärkt att använda det tillsammans med ASP.NET Web Forms för att ge möjligheten att få snygga URL:er även till sidor baserade på Web Forms.

De delar som är viktigast nu till en början är mapparna Models, View och Controller, samt Global.asax.

Vi kan se att Models-mappen är tom. Vi kommer inte att använda denna direkt, utan den kommer i ett senare skede. I Controllers-mappen så kan vi se att vi har två olika controllers – AccountController och HomeController. Alla controllers måste ha “Controller” som suffix! Om vi sedan kikar i Views-mappen så kan vi se att vi har dessa filer:

4

Account och Home används av sina aktuella controllers. Shared kan användas av alla controllers. Det gäller alltså att redan här få en bra struktur på hur innehållet skall ligga. För att göra det lättare för oss så plockar vi bort Account-mappen här då vi inte kommer att behöva den i exemplet. Den har Views som kan användas för att skapa användare och låta dem logga in på sidan. Vi tar även bort AccountController från Controllers-mappen. När de är borta så kan vi även plocka bort LogOnUserControl.aspx i Shared-mappen. Då vi redan nu vet att vi inte kommer att behöva dessa så finns det ingen anledning till att låta dem ligga där.

Det vi har kvar nu är:

  • /Home/Index.aspx – Startsidan
  • /Home/About.aspx – Info om sidan
  • /Shared/Error.aspx – Sida som visas om något går fel.
  • /Shared/Site.master – Precis som med Web Forms så kan vi använda master pages för att styra hur sidan skall vara uppbyggd.

Då vi har plockat bort bitar ur projektet så måste vi även redigera lite i standardsidorna som följde med. Rad 18-20 i Site.Master måste tas bort.

Om vi nu trycker på F5 i Visual Studio och väljer att kompilera och köra sidan så får vi fram det här:

5

För den uppmärksamme så kan ni se att jag var har klickat i compatibility view-knappen då sidan inte är helt perfekt med IE 8. ;-)

Om vi sedan klickar på About-länken i menyn så kan vi se att vi faktiskt kommer till /Home/About. Är man van vid vanlig Web Forms-utveckling så kan det här se helknas ut. Kollar vi i katalogstrukturen så ligger ju ändå About-sidan som /Views/Home/About.aspx.

Anledningen till detta är att ASP.NET MVC använder ASP.NET Routing. Om vi kikar i Global.asax.cs så kan vi se bl.a. det här:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
    routes.MapRoute(
    "Default",                                              // Route name
    "{controller}/{action}/{id}",                           // URL with parameters
    new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
    );
}

Vad som sker här är att vi vid Application_Start registrerar olika routes som skall användas. Just i det här fallet så har vi bara en route. Vi kan se att den har ett namn (en route måste ha ett unikt namn), en placeholder för URL:en som visar hur den skall vara uppbyggd, och sedan standardvärden som används om inget värde skickas in.

Vi kan här se att när vi går till /Home/About så går vi mot controllern “Home” och händelsen (vyn) “About”.

Det som är så fantastiskt med ASP.NET Routing, jämfört med traditionell URL Rewriting är att vi inte bara kan säga att en viss url skall göra att besökaren kommer till en viss sida, utan vi kan hämta den bäst lämpade URL:en för en viss sida.

Säg att vi bara vill använda en viss controller, och då vill slippa “/Home/” i URL:en, utan vi vill att alla sidor skall gå mot /About direkt. Då räcker det med att vi skapar en Route som pekar mot denna:

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

Genom att lägga in den här i Global.asax och kompilera om så räcker det med att vi nu anger Action, dvs vi kan surfa direkt till “/About” istället för “/Home/About”. Dessutom så skrivs våra länkar om automatiskt (om vi har använt ActionLink), vilket vi kan se här:

6

Magic!

För att vi automatiskt skall få länkarna pekade rätt så måste vi som sagt använda Action Link. Vi kan se ett bra exempel på det om vi ser hur länken i menyn är skapad:

<%= Html.ActionLink("About", "About", "Home")%>

Här skickar vi med texten på länken, action samt controller och får automatiskt en länk skapad med rätt URL. Kan det bli mycket smidigare?

För att allt skall fungera smidigt med andra controllers så tar vi dock bort den nya routern, men den kan anpassas hur mycket som helst utan att länkarna slutar fungera.

Liva upp sidan med en gästbok

För att få lite liv i sidan så vill vi nu även ha en gästbok, med en sida för att lista inlägg, och en sida för att skapa nya inlägg.

Det vi behöver för att kunna lagra inläggen från besökarna är en databas, en modell, vyer för presentation och skapande av inlägg, samt en controller som ser till att vi hamnar rätt.

Databasen jag använder här är en enkel SQL Server Express-databas i form av en mdf-fil i App_Data och modellen kommer att bygga på Linq to Entities.

Så det första vi gör nu är att högerklicka på App_Data, och välja att skapa en ny SQL Server Database. Vi ska göra den här så enkel som möjligt och skapar bara upp en tabell med tre fält för id, namn och meddelande och kallar denna tabellen för Entries.

Nästa steg är att i Models-katalogen skapa upp en ny ADO.NET Entity Data Model vid namn Guestbook. I guiden som kommer upp så väljer vi vår mdf-fil, samt väljer att hämta Entries-tabellen.

7

Det som sker när vi klickar på Finish är att vi får några nya referenser till ADO.NET Entity Framework-dll:er, samt att en datamodell skapas upp åt oss.

Då modellen visar ett enskilt inlägg så byter vi namn på tabellen till “Entry”, vilket visar på att det är just ett inlägg.

Då vi har en modell med tillhörande databas så kan vi gå vidare med att skapa en Controller som skall användas. Om vi högerklickar på Controllers, och väljer “Add” så kan vi se att Controller dyker upp automatiskt som ett alternativ.

8

Vi väljer att skapa en controller vid namn “GuestbookController”, samt bockar i rutan som frågar om vi vill ha metoder för att lägga till, ändra, samt ta bort inlägg.

9

Det vi får nu är ett gäng metoder för de olika händelserna. Om vi ser Index-metoden så kan vi se att den returnerar “View()” utan parametrar eller annat. Det är egentligen index-sidan, och det är där vi kommer att lista alla inlägg. Det finns vissa metoder vi kommer att plocka bort här då vi inte behöver dem.

Så här bör vår ändrade controller se ut:

using System.Web.Mvc;
 
namespace StartMvc.Controllers
{
    public class GuestbookController : Controller
    {
        //
        // GET: /Guestbook/
 
        public ActionResult Index()
        {
            return View();
        }
 
        //
        // GET: /Guestbook/Create
 
        public ActionResult Create()
        {
            return View();
        } 
 
        //
        // POST: /Guestbook/Create
 
        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create(FormCollection collection)
        {
            try
            {
                // TODO: Add insert logic here
 
                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }
    }
}

Det vi har är Index() för startsidan, Create() för formuläret, samt Create(FormCollection collection) för själva sparandet av inlägg. Vi kan se att den sistnämnda har ett attribut vid namn AcceptVerbs() och som tar emot HttpVerbs.Post som parameter. Det innebär att den här metoden enbart kommer att köras om den anropas via en postning. Är det istället GET så körs den andra Create-metoden.

För att det öht skall hända något nu så skall vi skapa upp två views i en katalog vid namn Guestbook, Index och Create.

Om vi i controllern högerklickar i Index-metoden så får vi upp det här:

image

Vi kan alltså skapa upp en vy för det aktuella ActionResult som vi står på, bara genom att högerklicka i metoden.

11

När vi väljer att skapa en ny vy så väljer vi även att ha den starkt typad, och pekar den mot Entry-objektet vi har i vår modell. Vi specifierar även att det här är en listning.

Det som sker nu är att vi får en ny sida av typen System.Web.Mvc.ViewPage<IEnumerable<StartMvc.Models.Entry>>, vilket tyder på att det är en listning av Entries. Vi har även fått en tabell automatiskt genererad med bl.a. detta:

 

    <% foreach (var item in Model) { %>
    
        <tr>
            <td>
                <%= Html.ActionLink("Edit", "Edit", new { id=item.id }) %> |
                <%= Html.ActionLink("Details", "Details", new { id=item.id })%>
            </td>
            <td>
                <%= Html.Encode(item.id) %>
            </td>
            <td>
                <%= Html.Encode(item.name) %>
            </td>
            <td>
                <%= Html.Encode(item.message) %>
            </td>
        </tr>
    
    <% } %>

Det här hämtar ut den aktuella modellen (i det här fallet Entry), och låter oss hämta informationen från den. Den har dock ej ännu fått någon data bunden till sig, utan det får vi lägga till i controllern:

public ActionResult Index()
{
    StartMvcEntities ent = new StartMvcEntities();
    IEnumerable<Entry> entries = ent.EntriesSet.OrderByDescending(e => e.id);
 
    return View(entries);
}

Det som sker här är att vi hämtar ut alla inlägg från vår modell och fyller vår vy med dessa. Då vi fortfarande inte har några inlägg så får vi börja med att lägga till vår sida som vi använder för att skapa inlägg.

Vi gör på samma sätt som tidigare och högerklickar i Create-metoden och väljer att skapa en ny vy. Vi väljer samma modell som innan, men den här gången väljer vi Create istället för List i den andra dropdown-listan:

12

Det som händer nu är att vi får en ny sida skapad åt oss med ett färdigt formulär för att skapa inlägg, baserat på fälten i modellen. Vi måste dock ta bort id härifrån, då det inte kan skapas upp av användaren.

För att enkelt kunna hitta till gästboken så väljer vi att lägga till en ActionLink i menyn:

 

<li><%= Html.ActionLink("Guestbook", "Index", "Guestbook")%></li>

Vi bör nu få med gästboken i menyn. Klickar vi på den så får vi upp detta:

13

Börjar likna något! Det vi gör härnäst är att klicka på “Create New”, för att få upp formuläret. Vi ser här att vi har ett formulär med de fält som vi vill att besökaren fyller i. Om vi fyller i båda fälten och klickar på knappen så märker vi att vi skickas tillbaka till gästboken utan att något har sparats. Det beror på att vi inte har något i controllern som säger att det som står i rutorna skall sparas ned i databasen.

Om vi sätter en breakpoint i den Create-metoden som bara används för postningar, och postar formuläret så kan vi se att våra två fält har skickats med i den FormCollection som tas emot som parameter:

14

För att nu lagra den här datan så använder vi oss utav den här koden:

StartMvcEntities ent = new StartMvcEntities();
Entry entry = new Entry();
 
TryUpdateModel(entry, new string[] { "name", "message" },collection.ToValueProvider());
 
ent.AddToEntriesSet(entry);
ent.SaveChanges();
 
return RedirectToAction("Index");

Det vi gör är att vi använder TryUpdateModel för att fylla entry med värdena från formuläret. Entity Framework har en begränsning här som säger att vi måste vitlista de fält som kan matas in, vilket sker i den andra parametern. I den tredje parametern så skickar vi in värdena.

Sen sparar vi ned texten till databasen och skickar besökaren till gästboken.

Här har vi resultatet:

15

Då vi inte har vyer för Edit och Details så tar vi bort dessa. Vi tar även bort id då det inte är relevant, och får då detta:

 16

Så här enkelt var det att komma igång med ASP.NET MVC och skapa upp en sida med en gästbok. Jag kommer även att ta upp andra delar av MVC-ramverket, men tills vidare så är det bara att experimentera och kika på bloggar.

More Posts