MVC

·         Model - Data & Business Rules

·         View - HTML response templates

·         Controller - Handles incoming requests, retrieve data from model, specify response template

Resources / Sources:

·         http://www.asp.net/mvc/tutorials/getting-started-with-mvc3-part1-cs

·         http://www.books24x7.com/assetviewer.aspx?bookid=35573&chunkid=172838318&noteMenuToggle=0&leftMenuState=1

 

Simple Example

(Note: unused default files and packages deleted from the zip to cut size from 2.2M to 20K. These don't affect the sample, but do prevent use of things like jQuery, entity framework etc).

Controllers

A controller is a class deriving from System.Web.Mvc.Controller that containing one of more public methods (called Actions). MVC's default routing automatically wires up URLs to these methods based on a standard naming convention:

http://localhost/Fred/Welcome?name=dave&whatever=42

à [http://localhost/ (not used)]/Controller/Action?Parameters

à FredController.Welcome("dave", 42);

·         When Parameters are present on the action method prototype but not on the incoming URL, an exception of the form "The parameters dictionary contains a null entry for parameter 'parameterName'..." is thrown. Default values on the prototype (int id=0) can prevent this.

·         To prevent a public method on a controller being exposed, mark it with the [NonAction] attribute

Action Return types

·         Actions normally return views (ViewResult), but can return HTML (string) or actions (ActionResult). ActionResult is a subclass of ViewResult, so a mixture or views or actions can be returned from the same method.

·         There instance helper methods on the Controller base class to return HTTP status codes (e.g. return this.HttpNotFound();)

·         Returning View() returns the default view for the controller/action combination. The file containing this view is stored under Views/Controller/Action.cshtml

·         Returning RedirectToAction("Action") redirects to the specified Action.

·         Returning Content(stringContent[, mimeType]) returns the specific text without decoration.

POST vs GET

·         A GET and POST with the same action name can have separate handlers by attributing one [HttpPost, ActionName("Action")] public ActionResult ActionOnPost(...)

·         HTTP GET operations should not cause data to be modified, so Create, Edit and Delete operations should be marked [HttpPost]. Plus you don't want Google following your delete links J

·         Controller code for edit:

[HttpPost]

public ActionResult Edit(Movie movie)

{

    if (ModelState.IsValid)

    {

        db.Entry(movie).State = EntityState.Modified;

        db.SaveChanges();

        return RedirectToAction("Index");

    }

    return View(movie);

}

·         The [Authorize] attribute requires the user to be logged in to call the action method. Depending on the auth method, this may redirect them to a login page. [Authorize(Roles="Admin,Sales")] is legal.

Error Handling

·         The [HandleError] attribute (with no parameters) on an action method or class redirects to the view stored at Views\Controller\Error.cshtml if an error occurs.

·         If the error view does not exist, it falls back to Views\Shared\Error.cshtml.

·         <System.Web><customErrors mode="On|RemoteOnly" /> needs to set for HandleError to work)

·         The optional parameters for a [HandleError] attribute are:

o   View: A different view can be specified by [HandleError(View="ViewName")]

o   Order: A priority order (the higher the number the lower the priority). This is typically used to set a lower priority default error view at controller class level and specific error handlers at the action method class. The default order is -1.
(Book says that if neither attribute specifies an Order, the class handler will 'win'. MSDN says the order

o   ExceptionType: Only exceptions derived from this type will trigger this ExceptionHandler. E.g. [HandleError(View="FileNotFoundErrror", ExceptionType=typeof(FileNotFound))]

Exceptions populate the following values in the ViewData.Model object  (which needs to be case to HandleErrorInfo first):

o   ActionName, ControllerName, Exception

<h2>Error</h2>

You are in error

<p>

    @{var errorInfo = (HandleErrorInfo)ViewData.Model;}

    Controller=@errorInfo.ControllerName<br />

    Action=@errorInfo.ActionName<br />

    Exception=@errorInfo.Exception.Message

</p>

 

Exceptions that occur in the Views themselves are handled by the default ASP.NET error handling.

Authorization

·         ?? TODO ??

Views

·         Razor file extension is .cshtml

·         Can be added by right-clicking in the body of a controller action and selecting Add View. This automatically creates a containing folder under Views with the same name as the controller and places a .cshtml file in it with the name specified in the Add View wizard (which defaults to the Action name).

·         View/Shared/_Layout.cshtml is the 'shared shell' used by other pages. This is defined in _ViewStart.schtml

o   @RenderBody() is the line that pulls in the View specific content

o   Code in the View appears to run before code in _Layout (i.e. a ViewBag property set in the View is seen by _Layout)

·         Views beginning with an underscore are protected and will not be directly served up by ASP.NET

Razor

·         To display a value from the ViewBag: @ViewBag.PropertyName (Extenso magic)

·         All strings are escaped automatically. To display HTML, use HtmlString, e.g.
@(new HtmlString(ViewBag.Notes))

[setting the ViewBag property directly (ViewBag.Notes = new HtmlString("<p/>");) also works]

·         @foreach (var item in model) { <tr><td>@Html.DisplayFor(modeItem => item.Prop2) }

·         @model keyword (at the very top of the file) specifies the type of object the view expects (this can be a single entity instance or a IEnumerable). This doesn't provide compile-time checking on the caller, but it does allow strong typing in the page itself. References to magic variable name Model are now of the type specified in the @model line.

·         Multiple code lines can be used if enclosed in a {...} block:
@{
    int x = 0;
    int x2 = x + 1;
}

Variables declared in the manner above do not go out of scope at the close brace.

·         @RenderPage("otherPage.cshtml") renders 'otherPage' at that point. For example, to include a header or footer.

·         @if (myBool) {
    <p> That was true</p>
} else
    <p> That was false</p>
}

 

·         @Html.ActionLink("DisplayText","Action" [, routeData e.g. new { id=item.Id} ])
Creates a hyper link to Site/CurrentController/Action/id

·         @Html.LabelFor - Displays name of the field from the model (as set by the [Display(name=...)] attribute / data annotation)

·         @Html.EditorFor - Creates HTML input element for the field. The action tag of the <form> is set by the '@using (Html.BeginForm())' line to something quite detailed (i.e. Controller/Action/ID). The using simply appears to be there to ensure the </form> tag gets written.

·         @Html.ValidationMessageFor - Creates display area for validation messages for the field

·         @Html.DropDownList("myProp", "All") - Creates a drop down populated from ViewBag.myProp with the additional entry 'All'

Models

·         Data Annotations on the entity class

o   [DataType(DataType.X)]

o   [DisplayFormat(DataFormatString = "{0:d}")]
[DisplayFormat(DataFormatString = "{0:c}")]

o   [Required(ErrorMessage="...")]        // ErrorMessageResourceString

o   [Range(1,100, ErrorMessage="...")]

o   [StringLength(5)]

·         Limited strong typing is available in the View by adding @model to the top of the file and specifying the desired type. This must either be fully qualified (as there doesn't appear to be the equivalent to a using-namepsace command in cshtml files) or be in a namespace listing in web.config under <pages><namespaces>.
The type declared with @model is then available through the Model keyword. The value for Model is specified when creating the View in the controller, using one of the View(..., object model) overrides.
The type declared with @model is often a collection (e.g. IList<entityType>).

Routing

·         URL form is Site/Controller/Action[Parameters]

·         Controller: The controller's name automatically maps to the URL (FredController -> http://localhost/Fred)

o   Default Controller (if none supplied on the URL) is Home

·         Action: A public method on the controller. Maps (by name) to the part of the URL after the controller name http://localhost/Fred/Welcome à FredController.Welcome() )

o   Default Action (if none supplied on the URL) is Index

·         Parameters: Defined through the Action parameters. Maps (by name) to the part of the URL after the controller and action names (http://localhost/Fred/Welcome?name=dave&whatever=42 à FredController.Welcome("dave", 42))

o   Special case: If the URL contains a slash and value after the action name (e.g. Site/Controller/Action/5), it is treated as if the parameter name was ID (e.g. Site/Controller/Action?ID=5)

·         UrlRoutingModule HTTP module directs the incoming request to the first matching item in the route map. If nothing matches, the request is passed through to normal ASP.NET. http://msdn.microsoft.com/en-us/library/dd381612(v=VS.100).aspx

·         Default routing is set up in the Global.asax.cs::RegisterRoutes.

o   It's the default implementation of this method that causes the default controller to be Home, the default action to be Index and the Parameters to be optional.

o   Additional routes can be added to change the default do-it-all-by-naming-convention mappings. For example, to map all actions of type Detail to a single controller.

o   Routes are evaluated in the order they are added, and stop once the first match is found.

Annoyances

·         Significant dependency on reflection to wire things up (e.g. return View("ViewName"), @Html.ActionLine(displayText, "ActionName") means poor compile time validation.

Deployment

The MVC3 runtime is xcopy deployable. Copying the following assemblies to the bin directory:

·         Microsoft.Web.Infrastructure

·         System.Web.Helpers

·         System.Web.MVC

·         System.Web.WebPages

·         System.Web.Razor

·         System.Web.WebPages.Razor

·         System.Web.WebPages.Deployment

(Easiest way to find / deploy these dlls is to add references for any that missing to them to the MVC3 project, set copy local to true on them all, and they'll appear in the bin directory when you build / publish)

Example Controller + View

Simple page with two action links that do nothing but Debug.WriteLine they were clicked:

 

Views\Logging\Index.cshtml:

@{ ViewBag.Title = "Logging"}

<h2>Logging</h2>

@Html.ActionLink("Disable Logging""Logging"new { enable = false}) &nbsp;

@Html.ActionLink("Enable Logging""Logging"new { enable = true })

 

Controllers\LoggingController.cs:
public class LoggingController : Controller

{

    // GET: /Logging/

    public ActionResult Index()  { return View();}

 

    // GET: /Logging/Logging?enable=bool

    public ActionResult Logging(bool enable)

    {

        Debug.WriteLine(enable);

 

        // Override return view name else we'd be looking for 

        // Views\Logging\Logging.cshtml (et al) rather than Views\Logging\Index.cshtml

        return View("Index");

    }

}

 

Azure 1.4 Logging Example

Uses a Model, View and Controller

Turns the Azure 1.4 $log features on and off, and displays BLOBs from $log.

Models\LogDescriptor.cs

using System;

using System.Data.Entity;

 

namespace Mandlebrot.Models

{

    public class LogDescriptor

    {

        public string Name { getset; }

        public string Url { getset; }

        public DateTime LastModified { getset; }

        public int ContentLength { getset; }

    }

 

    public class LogDescriptorContext : DbContext

    {

        public DbSet<LogDescriptor> Context { getset; }

    }

}

Note how the DbContext is a separate class to the DTO, and how lightweight the DTO is. Optional attributes for validation, form labels etc omitted for brevity.

Views\Logging\Index.cshtml

@{ ViewBag.Title = "Logging"}

<h2>Logging</h2>

 

<p>

@Html.ActionLink("Get Table Logging Configuration""Load"new { serviceName =  "table"}) <br />

@Html.ActionLink("Get Load Blob Logging Configuration""Load"new { serviceName =  "blob" }) <br />

@Html.ActionLink("Get Load Queue Logging Configuration""Load"new { serviceName =  "queue" }) <br />

</p><p>

@Html.ActionLink("Enable Table Logging""Save"new { serviceName =  "table", enable = true}) &nbsp;

@Html.ActionLink("Enable Blob Logging""Save"new { serviceName =  "blob", enable = true }) &nbsp;

@Html.ActionLink("Enable Queue Logging""Save"new { serviceName =  "queue", enable = true })

<br />

@Html.ActionLink("Disable Table Logging""Save"new { serviceName =  "table", enable = false}) &nbsp;

@Html.ActionLink("Disable Blob Logging""Save"new { serviceName =  "blob", enable = false }) &nbsp;

@Html.ActionLink("Disable Queue Logging""Save"new { serviceName =  "queue", enable = false })

</p><p>

@Html.ActionLink("Show Logs""Summary")

</p>

 

Views\Logging\Summary.cshtml

@model IEnumerable<Mandlebrot.Models.LogDescriptor>

@{

    ViewBag.Title = "LogView";

}

 

<h2>LogView</h2>

 

<table class="logGrid">

    <thead><tr><th>Name</th><th>Url</th><th>Last Modified</th><th>Content-Length</th></tr></thead>

@foreach (var row in Model) { 

    <tr>

        <td>@Html.ActionLink(row.Name, "Detail"new {path= row.Name})</td>

        <td>@row.Url</td>

        <td>@row.LastModified</td>

        <td>@row.ContentLength</td>

    </tr>

}

</table>

 

Views\Logging\Detail.cshtml

@{

    ViewBag.Title = "LogView";

}

 

<h2>LogView</h2>

<table class="logGrid">

    <thead><tr>

        <th>Log Version</th>

        <th>Transaction Start Time</th>

        <th>Reset Operation Type</th>

        <th>Request Status</th>

        <th>HTTP Status Code</th>

        <th>E2E Latency</th>

        <th>Server Latency</th>

        <th>Authentication Type</th>

        <th>Requestor Account Name</th>

        <th>Owner Account Name</th>

        <th>Service Type</th>

        <th>Request URL</th>

        <th>Object Key</th>

        <th>Request ID</th>

        <th>Operation Number</th>

        <th>Client IP</th>

        <th>Request Version</th>

        <th>Request Header Size</th>

        <th>Request Packet Size</th>

        <th>Request Content Length</th>

        <th>Request MD5</th>

        <th>Server MD5</th>

        <th>E-Tag</th>

        <th>Last Modified Time</th>

        <th>Conditions Used</th>

        <th>User Agent</th>

        <th>Referrer</th>

        <th>Client Request ID</th>

    </tr></thead>

@foreach (string[] row in ViewBag.Rows) { 

    <tr>

    @foreach (string field in row)

    {

        <td>@field</td>

    }

    </tr>

}

</table>

 

Controllers\LoggingController.cs:

using System;

using System.Collections.Generic;

using System.IO;

using System.Linq;

using System.Net;

using System.Text;

using System.Text.RegularExpressions;

using System.Web;

using System.Web.Mvc;

using System.Xml.Linq;

using Mandlebrot.Models;

using Microsoft.WindowsAzure;

using Microsoft.WindowsAzure.ServiceRuntime;

 

namespace Mandlebrot.Controllers

{

    public class LoggingController : Controller

    {

        // GET: /Logging/

        public ActionResult Index()

        {

            return View();

        }

 

        private static void ValidateServiceName(string serviceName)

        {

            if (serviceName == "table" || serviceName == "blob" || serviceName == "queue"return;

            throw new InvalidOperationException("Bad service name (must be service, blob or queue");

        }

 

        /// <summary>Gets the logging configuration XML for the blob, table or queue service</summary>

        /// <param name="serviceName">Name of the service (table, blob or queue)</param>

        public ActionResult Load(string serviceName)

        {

            ValidateServiceName(serviceName);

            CloudStorageAccount account = CloudStorageAccount.Parse(RoleEnvironment.GetConfigurationSettingValue(Mbot.StorageSettingName));

            Uri uri = new Uri(string.Format("https://{0}.{1}.core.windows.net/?restype=service&comp=properties", account.Credentials.AccountName, serviceName));

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);

            request.Headers["x-ms-version"] = "2009-09-19";

            if (serviceName == "table") { account.Credentials.SignRequestLite(request); } else { account.Credentials.SignRequest(request); }

            string textResponse = new StreamReader(request.GetResponse().GetResponseStream()).ReadToEnd();

            return this.Content(HttpUtility.HtmlEncode(textResponse));

        }

 

        /// <summary>Turns logging on or off for the blob, table or queue service</summary>

        /// <param name="serviceName">Name of the service (table, blob or queue)</param>

        /// <param name="enable">True to enable all logging options, false to disable all logging operations</param>

        [ValidateInput(false)]

        public ActionResult Save(string serviceName, bool enable)

        {

            ValidateServiceName(serviceName);

            CloudStorageAccount account = CloudStorageAccount.Parse(RoleEnvironment.GetConfigurationSettingValue(Mbot.StorageSettingName));

            Uri uri = new Uri(string.Format("https://{0}.{1}.core.windows.net/?restype=service&comp=properties", account.Credentials.AccountName, serviceName));

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);

            request.Method = "PUT";

            request.Headers["x-ms-version"] = "2009-09-19";

 

            string cmd = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +

                "<StorageServiceProperties>" +

                    "<Logging>" +

                        "<Version>1.0</Version>" +

                        "<Delete>{0}</Delete>" +

                        "<Read>{0}</Read>" +

                        "<Write>{0}</Write>" +

                        "<RetentionPolicy>" +

                            "<Enabled>{0}</Enabled>" +

                            "<Days>7</Days>" +

                        "</RetentionPolicy>" +

                    "</Logging>" +

                    "<Metrics>" +

                        "<Version>1.0</Version>" +

                        "<Enabled>{0}</Enabled>" +

                        "<IncludeAPIs>{0}</IncludeAPIs>" +

                        "<RetentionPolicy>" +

                            "<Enabled>{0}</Enabled>" +

                            "<Days>7</Days>" +

                        "</RetentionPolicy>" +

                    "</Metrics>" +

                "</StorageServiceProperties>";

            cmd = string.Format(cmd, enable);

            byte[] utf8 = Encoding.UTF8.GetBytes(cmd);

            request.ContentLength = utf8.Length;

 

            // GetRequestStream actually opens the connection to the servers, so we need to sign first.

            if (serviceName == "table") { account.Credentials.SignRequestLite(request); } else { account.Credentials.SignRequest(request); }

            using (Stream stream = request.GetRequestStream())

            {

                stream.Write(utf8, 0, utf8.Length);

                stream.Close();

            }

 

            try { 

                request.GetResponse(); 

                return Content("Success"); 

            }

            catch (WebException e) { 

                Response.StatusCode = 500; 

                Response.TrySkipIisCustomErrors = true

                return Content(new StreamReader(e.Response.GetResponseStream()).ReadToEnd()); 

            }

        }

 

        /// <summary>Gets a list of the log BLOBs avaliable for closer examination</summary>

        public ActionResult Summary()

        {

            CloudStorageAccount account = CloudStorageAccount.Parse(RoleEnvironment.GetConfigurationSettingValue(Mbot.StorageSettingName));

            Uri uri = new Uri(string.Format("https://{0}.blob.core.windows.net/$logs?restype=container&comp=list", account.Credentials.AccountName));

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);

            request.Method = "GET";

            request.Headers["x-ms-version"] = "2009-09-19";

 

            // GetRequestStream actually opens the connection to the servers, so we need to sign first.

            account.Credentials.SignRequest(request);

 

            XDocument doc;

            try

            {

                WebResponse wr = request.GetResponse();

                using (StreamReader sr = new StreamReader(wr.GetResponseStream()))

                {

                    doc = XDocument.Parse(sr.ReadToEnd());

                }

            }

            catch (WebException e)

            {

                Response.StatusCode = 500;

                Response.TrySkipIisCustomErrors = true;

                return Content(new StreamReader(e.Response.GetResponseStream()).ReadToEnd());

            }

 

            // Quick and non-scalible way to get the data into a displayable form. Note we don't handle the <NextMarker/> continuation token

            IEnumerable<LogDescriptor> query = from e in doc.Root.Descendants("Blob")

                                               let name = (string) e.Element("Name")

                                               let properties = e.Element("Properties")

                                               orderby name

                                               select new LogDescriptor { 

                                                   Name = name,

                                                   Url = (string) e.Element("Url"),

                                                   LastModified = (DateTime) properties.Element("Last-Modified"),

                                                   ContentLength = (int) properties.Element("Content-Length"),

                                                };

            

            return View(query.ToArray());

        }

 

        /// <summary>Gets the detail</summary>

        /// <param name="path">Relative path from the $log container (i.e. the BLOB name)</param>

        /// <returns></returns>

        public ActionResult Detail(string path)

        {

            // Check input (since we'll be passing it through to the Azure REST command API)

            Regex regex = new Regex(@"^(table|blob|queue)/\d{4}/\d{2}/\d{2}/\d{4}/\d{6}\.log$");

            if (!regex.IsMatch(path))

            {

                Response.StatusCode = 400;

                Response.TrySkipIisCustomErrors = true;

                return Content("Incorrect path format");

            }

 

            CloudStorageAccount account = CloudStorageAccount.Parse(RoleEnvironment.GetConfigurationSettingValue(Mbot.StorageSettingName));

            Uri uri = new Uri(string.Format("https://{0}.blob.core.windows.net/$logs/{1}", account.Credentials.AccountName, path));

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);

            request.Method = "GET";

            request.Headers["x-ms-version"] = "2009-09-19";

            account.Credentials.SignRequest(request);

 

            try

            {

                // We don't have to use the Model object, we can just pass everything through ViewBag if we like

                List<string[]> rows = new List<string[]>();

                WebResponse wr = request.GetResponse();

                using (StreamReader sr = new StreamReader(wr.GetResponseStream()))

                {

                    while (!sr.EndOfStream)

                        rows.Add(sr.ReadLine().Split(';'));

                }

                ViewBag.Rows = rows;

                return View();

            }

            catch (WebException e)

            {

                Response.StatusCode = 500;

                Response.TrySkipIisCustomErrors = true;

                return Content(new StreamReader(e.Response.GetResponseStream()).ReadToEnd());

            }

        }

    }

}

 

Example: MVC MandleBrot, $logging and $metrics