Maarten Balliauw {blog}

ASP.NET MVC, Microsoft Azure, PHP, web development ...

NAVIGATION - SEARCH

Creating a generic Linq to SQL ModelBinder for the ASP.NET MVC framework

You are right! This is indeed my third post on ASP.NET MVC ModelBinders. The first one focussed on creating a ModelBinder from scratch in an older preview release, the second post did something similar trying to do some dirty ViewState-like stuff. Good news! There's more of this dirty stuff coming!

How about this action method, using a Person class which is a Linq to SQL entity type:

[code:c#]

public ActionResult PersonDetails(Person id)
{
    if (id == null)
        return RedirectToAction("Index");

    return View(id);
}

[/code]

This action method is called from a URL which looks like Home/PersonDetails/2. Nothing special about this? Read this whole post again, from the beginning! Yes, I said that the Person class is a Linq to SQL entity type, and yes, you are missing the DataContext here! The above method would normally look like this:

[code:c#]

public ActionResult PersonDetails(int id)
{
    using (ApplicationDataContext context = new ApplicationDataContext())
    {
        Person person = context.Persons.Where(p => p.Id == id).SingleOrDefault();

        if (person == null)
            return RedirectToAction("Index");

        return View(person);
    }
}

[/code]

Using the ASP.NET MVC ModelBinder infrastructure, I am actually able to bind action method parameters to real objects, based on simple query string parameters like, in this case, id. A custom ModelBinder maps this string id to a real Person instance from my Linq to SQL DataContext. Let me show you how I've created this ModelBinder.

Registering the LinqToSqlBinder<T>

As with any custom ModelBinder, the LinqToSqlBinder should be registered with the ModelBinder infrastructure:

[code:c#]

protected void Application_Start()
{
    // ...

    LinqToSqlBinder<ApplicationDataContext>.Register(ModelBinders.Binders);
}

[/code]

The above piece of code registers every entity type (or table, whatever you like to call it) in my Linq to Sql data contextwith a new LinqToSqlBinder<ApplicationDataContext> instance.

[code:c#]

public static void Register(IDictionary<Type, IModelBinder> bindersDictionary)
{
    using (T context = new T())
    {
        foreach (var table in context.Mapping.GetTables())
        {
            ModelBinders.Binders.Add(table.RowType.Type, new LinqToSqlBinder<T>());
        }
    }
}

[/code]

The LinqToSqlBinder<T> source code

The LinqToSqlBinder<T> will make use of a small utility class, TableDefinition, in which some information about the entity type's table will be stored. This class looks like the following:

[code:c#]

private class TableDefinition
{
    public TableDefinition()
    {
        ColumnNames = new List<string>();
    }

    public string TableName;
    public string PrimaryKeyFieldName;
    public List<string> ColumnNames { get; set; }
}

[/code]

My LinqToSqlBinder<T> overloads ASP.NET MVC's DefaultModelBinder class, of which I'll override the BindModel method:

[code:c#]

public class LinqToSqlBinder<T> : DefaultModelBinder
{
    public override ModelBinderResult BindModel(ModelBindingContext bindingContext)
    {
        // ...
    }
}

[/code]

First of all, the LinqToSqlBinder<T> has to determine if it can actually perform binding for the requested model type. In this case, this is determined using the metadata my Linq to SQL data context provides. If it does not support mapping the requested type, model binding is further processed by the base class.

[code:c#]

// Check if bindingContext.ModelType can be delivered from T
MetaTable metaTable = context.Mapping.GetTable(bindingContext.ModelType);
if (metaTable == null)
{
    return base.BindModel(bindingContext);
}

[/code]

Next task for the model binder: checking whether a value is provided. For example, if my action method expects a parameter named "id" and I provide a parameter "borat" (whatever...) in the request, the model binder should not accept the task given. If everything succeeds, I should have an identity value which I can use in a query later on.

[code:c#]

// Get the object ID that is being passed in.
ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == null)
{
    return base.BindModel(bindingContext);
}
string objectId = valueProviderResult.AttemptedValue;

[/code]

Speaking of queries... Now is a good time to start filling my TableDefinition instance, on which I can generate a SQL query which will later retrieve the requested object. Filling the TableDefinition object is really an easy task when using the meta data Linq to SQL provides. Each member (or column) can be looped and queried for specific information such as type, name, primary key, ...

[code:c#]

// Build table definition
TableDefinition tableDefinition = new TableDefinition();
tableDefinition.TableName = metaTable.TableName;

foreach (MetaDataMember dm in metaTable.RowType.DataMembers)
{
    if (dm.DbType != null)
    {
        tableDefinition.ColumnNames.Add(dm.MappedName);
    }
    if (dm.IsPrimaryKey)
    {
        tableDefinition.PrimaryKeyFieldName = dm.MappedName;
    }
}

[/code]

With all this information in place, a SQL query can easily be built.

[code:c#]

// Build query
StringBuilder queryBuffer = new StringBuilder();
queryBuffer.Append("SELECT ")
                .Append(string.Join(", ", tableDefinition.ColumnNames.ToArray()))
           .Append(" FROM ")
                .Append(tableDefinition.TableName)
           .Append(" WHERE ")
                .Append(tableDefinition.PrimaryKeyFieldName)
                .Append(" = \'").Append(objectId).Append("\'");

[/code]

A nice looking query is generated using this code: SELECT id, name, email FROM dbo.person WHERE id = '2'. This query can now be executed on the Linq to SQL data context. The first result will be returned to the action method.

[code:c#]

// Execute query
IEnumerable resultData = context.ExecuteQuery(bindingContext.ModelType,
    queryBuffer.ToString());

foreach (object result in resultData)
{
    return new ModelBinderResult(result);
}

[/code]

Download the code

Feel free to download a working example based on this blog post: LinqModelBinderExample.zip (352.52 kb)

Note that this code may be vulnerable to SQL injection! This is not production code!

kick it on DotNetKicks.com

CarTrackr - Sample ASP.NET MVC application

CarTrackr - Sample ASP.NET MVC application Some people may have already noticed the link in my VISUG session blog post, but for those who didn't... I've released my sample application CarTrackr on CodePlex.

CarTrackr is a sample application for the ASP.NET MVC framework using the repository pattern and dependency injection using the Unity application block. It was written for various demos in presentations done by Maarten Balliauw.

CarTrackr is an online software application designed to help you understand and track your fuel usage and kilometers driven.

You will have a record on when you filled up on fuel, how many kilometers you got in a given tank, how much you spent and how much liters of fuel you are using per 100 kilometer.

CarTrackr will enable you to improve your fuel economy and save money as well as conserve fuel. Fuel economy and conservation is becoming an important way to control your finances with the current high price.

Go get the source code for CarTrackr on CodePlex! (note that it has been updated for ASP.NET MVC beta 1)

Technologies and techniques used

Here's a list of technologies and techniques used:

  • CarTrackr uses the Unity application block to provide dependency injection
  • The repository design pattern is used for building a flexible data layer
  • Controllers are instantiated by using a custom ASP.NET MVC ControllerBuilder, which uses Unity for dependency resolving
  • The testing project makes use of Moq to mock out parts of the ASP.NET runtime
  • Form validation is included on most forms, leveraging the ViewData.ModelState class
  • It is possible to sign in using OpenID, for which the ASP.NET MVC Membership starter kit was used
  • LinqToSQL is used as the persistence layer
  • CarTrackr uses my ASP.NET MVC sitemap provider
  • Configuration Section Designer was used to create a custom configuration section
  • Extension methods are created for including Silverlight charts (rendered with Visifire)
  • Web 2.0 logo creator was used to generate a classy logo

kick it on DotNetKicks.com

Introduction to ASP.NET MVC for VISUG - Presentation materials

VISUG Yesterday evening, I did a presentation on the ASP.NET MVC framework for VISUG (Visual Studio User Group Belgium). I really hope everyone got a good feel on what the ASP.NET MVC framework is all about and what it takes to build an ASP.NET MVC application. Thank you Pieter Gheysens for inviting me for this talk! And thank you audience for being interested for over an hour and a half!

A recorded version of this presentation will be available later, for the moment you'll have to do with the presentation materials. The download contains the slides, the Hello World application and the testing demo. The CarTrackr application can be found on CodePlex.

Downloads

Presentation materials: VISUG ASP.NET MVC materials.zip (5.63 mb)
CarTrackr sample application: http://www.codeplex.com/CarTrackr/

kick it on DotNetKicks.com

Using the ASP.NET MVC ModelBinder attribute - Second part

Just after the ASP.NET MVC preview 5 was released, I made a quick attempt to using the ModelBinder attribute. In short, a ModelBinder allows you to use complex objects as action method parameters, instead of just basic types like strings and integers. While my aproach was correct, it did not really cover the whole picture. So here it is: the full picture.

First of all, what are these model binders all about? By default, an action method would look like this:

[code:c#]

public ActionResult Edit(int personId) {
    // ... fetch Person and do stuff
}

[/code]

Now wouldn't it be nice to pass this Person object completely as a parameter, rather than obliging the controller's action method to process an id? Think of this:

[code:c#]

public ActionResult Edit(Person person) {
    // ... do stuff
}

[/code]

Some advantages I see:

  • More testable code!
  • Easy to work with!
  • Some sort of viewstate-thing which just passes a complete object back and forth. Yes, I know, ViewState is BAD! But I recently had a question about how to manage concurrency, and using a version id as an hidden HTML field or a complete object as a hidden HTML field should not be that bad, no?

Just one thing to do: implementing a ModelBinder which converts HTML serialized Persons into real Persons (well, objects, not "real" real persons...)

How to implement it...

Utility functions

No comments, just two utility functions which serialize and deserialize an object to a string an vice-versa.

[code:c#]

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace ModelBinderDemo.Util
{
    public static class Serializer
    {
        public static string Serialize(object subject)
        {
            MemoryStream ms = new MemoryStream();
            BinaryFormatter bf = new BinaryFormatter();
            bf.Serialize(ms, subject);

            return Convert.ToBase64String(ms.ToArray());
        }

        public static object Deserialize(string subject)
        {
            MemoryStream ms = new MemoryStream(Convert.FromBase64String(subject));
            BinaryFormatter bf = new BinaryFormatter();
            return bf.Deserialize(ms);
        }
    }
}

[/code]

Creating a ModelBinder

The ModelBinder itself should be quite simple to do. Just create a class which inherits DefaultModelBinder and have it ocnvert a string into an object. Beware! The passed in value might also be an array of strings, so make sure to verify that in code.

[code:c#]

using System;
using System.Globalization;
using System.Web.Mvc;
using ModelBinderDemo.Models;
using ModelBinderDemo.Util;

namespace ModelBinderDemo.Binders
{
    public class PersonBinder : DefaultModelBinder
    {
        protected override object ConvertType(CultureInfo culture, object value, Type destinationType)
        {
            // Only accept Person objects for conversion
            if (destinationType != typeof(Person))
            {
                return base.ConvertType(culture, value, destinationType);
            }

            // Get the serialized Person that is being passed in.
            string serializedPerson = value as string;
            if (serializedPerson == null && value is string[])
            {
                serializedPerson = ((string[])value)[0];
            }

            // Convert to Person
            return Serializer.Deserialize(serializedPerson);
        }
    }
}

[/code]

In order to use this ModelBinder, you'll have to register it in Global.asax.cs:

[code:c#]

// Register model binders
ModelBinders.Binders.Add(typeof(Person), new PersonBinder());

[/code]

Great View!

Nothing strange in the View: just a HTML form which generates a table to edit a Person's details and a submit button.

[code:c#]

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="ModelBinderDemo.Views.Home.Index" %>
<%@ Import Namespace="ModelBinderDemo.Models" %>

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
    <h2>Edit person</h2>
    <% using (Html.Form("Home", "Index", FormMethod.Post)) { %>
        <%=Html.Hidden("person", ViewData["Person"]) %>
        <%=Html.ValidationSummary()%>
        <table border="0" cellpadding="2" cellspacing="0">
            <tr>
                <td>Name:</td>
                <td><%=Html.TextBox("Name", ViewData.Model.Name)%></td>
            </tr>
            <tr>
                <td>E-mail:</td>
                <td><%=Html.TextBox("Email", ViewData.Model.Email)%></td>
            </tr>
            <tr>
                <td>&nbsp;</td>
                <td><%=Html.SubmitButton("saveButton", "Save")%></td>
            </tr>
        </table>
    <% } %>
</asp:Content>

[/code]

Wait! One thing to notice here! The <%=Html.Hidden("person", ViewData["Person"]) %> actually renders a hidden HTML field, containing a serialized version of my Person. Which might look like this:

[code:c#]

<input Length="340" id="person" name="person" type="hidden" value="AAEAAAD/////AQAAAAAAAAAMAgAAAEZNb2RlbEJpbmRlckRlbW8
sIFZlcnNpb249MS4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1udWxsBQEAAAAdTW9kZWxCaW5kZXJEZW1vLk1v
ZGVscy5QZXJzb24DAAAAEzxJZD5rX19CYWNraW5nRmllbGQVPE5hbWU+a19fQmFja2luZ0ZpZWxkFjxFbWFpbD5rX19CYWNraW5nRmllb
GQAAQEIAgAAAAEAAAAGAwAAAAdNYWFydGVuBgQAAAAabWFhcnRlbkBtYWFydGVuYmFsbGlhdXcuYmUL" />

[/code]

Creating the action method

All preparations are done, it's time for some action (method)! Just accept a HTTP POST, accept a Person object in a variable named person, and Bob's your uncle! The person variable will contain a real Person instance, which has been converted from AAEAAD.... into a real instance. Thank you, ModelBinder!

[code:c#]

[AcceptVerbs("POST")]
public ActionResult Index(Person person, FormCollection form)
{
    if (string.IsNullOrEmpty(person.Name))
    {
        ViewData.ModelState.AddModelError("Name", person.Name, "Plese enter a name.");
    }

    if (string.IsNullOrEmpty(person.Email))
    {
        ViewData.ModelState.AddModelError("Name", person.Name, "Plese enter a name.");
    }

    return View("Index", person);
}

[/code]

Make sure to download the full source and see it in action! ModelBinderDemo2.zip (239.87 kb)

kick it on DotNetKicks.com

ASP.NET MVC preview 5's AntiForgeryToken helper method and attribute

The new ASP.NET MVC preview 5 featured a number of new HtmlHelper methods. One of these methods is the HtmlHelper.AntiForgeryToken. When you place <%=Html.AntiForgeryToken()%> on your view, this will be rendered similar to the following:

[code:c#]

<input name="__MVC_AntiForgeryToken" type="hidden" value="Ak8uFC1MQcl2DXfJyOM4DDL0zvqc93fTJd+tYxaBN6aIGvwOzL8MA6TDWTj1rRTq" />

[/code]

When using this in conjunction with the action filter attribute [ValidateAntiForgeryToken], each round trip to the server will be validated based on this token.

[code:c#]

[ValidateAntiForgeryToken]
public ActionResult Update(int? id, string name, string email) {
    // ...
}

[/code]

Whenever someone tampers with this hidden HTML field's data or posts to the action method from another rendered view instance, this ValidateAntiForgeryToken will throw a AntiForgeryTokenValidationException.

kick it on DotNetKicks.com

Using the ASP.NET MVC ModelBinder attribute

ASP.NET MVC action methods can be developed using regular method parameters. In earlier versions of the ASP.NET MVC framework, these parameters were all simple types like integers, strings, booleans, … When required, a method parameter can be a complex type like a Contact with Name, Email and Message properties. It is, however, required to add a ModelBinder attribute in this case.

Here’s how a controller action method could look like:

[code:c#]

public ActionResult Contact([ModelBinder(typeof(ContactBinder))]Contact contact)
{
    // Add data to view
    ViewData["name"] = contact.Name;
    ViewData["email"] = contact.Email;
    ViewData["message"] = contact.Message;
    ViewData["title"] = "Succes!";

    // Done!
    return View();
}

[/code]

Notice the ModelBinder attribute on the action method’s contact parameter. It also references the ContactBinder type, which is an implementation of IModelBinder that also has to be created in order to allow complex parameters:

[code:c#]

public class ContactBinder : IModelBinder
{
    #region IModelBinder Members

    public object GetValue(ControllerContext controllerContext, string modelName, Type modelType, ModelStateDictionary modelState)
    {
        if (modelType == typeof(Contact))
        {
            return new Contact
            {
                Name = controllerContext.HttpContext.Request.Form["name"] ?? "",
                Email = controllerContext.HttpContext.Request.Form["email"] ?? "",
                Message = controllerContext.HttpContext.Request.Form["message"] ?? ""
            };
        }

        return null;
    }

    #endregion
}

[/code]

UPDATE: Also check Timothy's blog post on this one.
UPDATE: And my follow-up blog post.

kick it on DotNetKicks.com

Form validation with ASP.NET MVC preview 5

In earlier ASP.NET MVC previews, form validation was something that should be implemented "by hand". Since the new ASP.NET MVC preview 5, form validation has become more handy. Let me show you how you can add validation in such a ridiculously easy manner.

Here's an example controller:

[code:c#]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Ajax;

namespace ValidationExample.Controllers
{
    [HandleError]
    public class HomeController : Controller
    {
        // ... some other action methods ...

        [AcceptVerbs("GET")]
        public ActionResult Contact()
        {
            return View();
        }

        [AcceptVerbs("POST")]
        public ActionResult Contact(string name, string email, string message)
        {
            // Add data to view
            ViewData["name"] = name;
            ViewData["email"] = email;
            ViewData["message"] = message;

            // Validation
            if (string.IsNullOrEmpty(name))
                ViewData.ModelState.AddModelError("name", name, "Please enter your name!");
            if (string.IsNullOrEmpty(email))
                ViewData.ModelState.AddModelError("email", email, "Please enter your e-mail!");
            if (!string.IsNullOrEmpty(email) && !email.Contains("@"))
                ViewData.ModelState.AddModelError("email", email, "Please enter a valid e-mail!");
            if (string.IsNullOrEmpty(message))
                ViewData.ModelState.AddModelError("message", message, "Please enter a message!");

            // Send e-mail?
            if (ViewData.ModelState.IsValid)
            {
                // send email...
                return RedirectToAction("Index");
            }
            else
            {
                return View();
            }
        }
    }
}

[/code]

You may notice an starnge thing here... Why is Contact defined twice, and why is it with this strange AcceptVerbs attribute? The AcceptVerbs attribute determines which action method to call, based on the HTTP method of the request. In this case, when I do not post a form, the first action method will be called, simply rendering a view. When posting a form, the second action method will be called, allowing me to do some validations.

Speaking of validations... Notice that I can set errors on the ViewData.ModelState collection, and use this ViewData.ModelState.IsValid property to check if everything is OK.

UPDATE: You can also use the controller's UpdateModel method (which updates a model object with form values) for setting data on the model. If the model throws an exception, this will be added to the ViewData.ModelState dictionary too.

One thing left with validation: the view itself!

[code:html]

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Contact.aspx.cs" Inherits="ValidationExample.Views.Home.Contact" %>

<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
    <h2>Contact Us</h2>
    <p><%=Html.ValidationSummary()%></p>
    <% using (Html.Form<ValidationExample.Controllers.HomeController>( c => c.Contact("", "", ""), FormMethod.Post)) { %>
        <table border="0" cellpadding="2" cellspacing="0">
            <tr>
                <td>Name:</td>
                <td>
                    <%=Html.TextBox("name", ViewData["name"] ?? "")%>
                    <%=Html.ValidationMessage("name")%>
                </td>
            </tr>
            <tr>
                <td>Email:</td>
                <td>
                    <%=Html.TextBox("email", ViewData["email"] ?? "")%>
                    <%=Html.ValidationMessage("email")%>
                </td>
            </tr>
            <tr>
                <td colspan="2">Message:</td>
            </tr>
            <tr>
                <td colspan="2">
                    <%=Html.TextArea("message", ViewData["message"] ?? "")%>
                </td>
            </tr>
            <tr>
                <td colspan="2">
                    <%=Html.ValidationMessage("message")%>
                </td>
            </tr>
            <tr>
                <td>&nbsp;</td>
                <td>
                    <%=Html.SubmitButton("send", "Send e-mail")%>
                </td>
            </tr>
        </table>
    <% } %>
</asp:Content>

[/code]

Notice that there are 2 new HtmlHelper extension methods: ValidationMessage and ValidationSummary. The first one displays a validation message for one key in the ViewData.ModelState collection, while the latter displays a validation summary of all messages. Here's what my invalid post looks like:

Validation example

kick it on DotNetKicks.com

Building an ASP.NET MVC sitemap provider with security trimming

Warning!
Warning!
A new version of the source code provided in this post is available here. Use this blog post as reference only. 


Yes, it has been a while since my last post. A nice vacation to Austria, some work to catch up, ... All excuses, I know, but I'll make it up to you with a huge blog post!

If you have been using the ASP.NET MVC framework, you possibly have been searching for something like the classic ASP.NET sitemap. After you've played with it, you even found it useful! But not really flexible and easy to map to routes and controllers. Sounds familiar? Continue reading! Doesn't ring a bell? Well, continue reading, please!

Feel free to download the sample code.
UPDATE: A version for preview 5 can also be downloaded: MvcSitemapProvider.cs (19.46 kb)

The base concept of this class is based on someone else's version which supports dynamic nodes, populated by code instead of XML. Unfortunately, I forgot to write down the URL where I found it. So please, if you do find something like that, let me know so I can thank this person for the base concepts of his class...

kick it on DotNetKicks.com

The concept

What I would like, is having a web.sitemap file which looks like the following:

[code:c#]

<?xml version="1.0" encoding="utf-8" ?>
<siteMap>
  <siteMapNode id="Root" url="~/Index.aspx">
    <mvcSiteMapNode id="Home" title="Home" controller="Home" action="Index">
      <mvcSiteMapNode id="About" title="About Us" controller="Home" action="About" />
    </mvcSiteMapNode>

    <mvcSiteMapNode id="Products" title="Products" controller="Products">
      <mvcSiteMapNode id="Books" title="Books" controller="Products" action="List" category="Books" />
      <mvcSiteMapNode id="DVD" title="DVD's" controller="Products" action="List" category="DVD"/>
    </mvcSiteMapNode>
    <mvcSiteMapNode id="Account" title="Account" controller="Account">
      <mvcSiteMapNode id="Login" title="Login" controller="Account" action="Login" />
      <mvcSiteMapNode id="Register" title="Account Creation" controller="Account" action="Register" />
      <mvcSiteMapNode id="ChangePassword" title="Change Password" controller="Account" action="ChangePassword" />
      <mvcSiteMapNode id="Logout" title="Logout" controller="Account" action="Logout" />
    </mvcSiteMapNode>
  </siteMapNode>
</siteMap>

[/code]

That's right: regular siteMapNodes, but also mvcSiteMapNodes! I want my ASP.NET menu control and sitemap path to use both node types for determining the current locattion on my website. And since the ASP.NET MVC framework uses routing and allows extra parameters to build up a URL, I thought of creating an mvcSiteMapNode.

Each mvcSiteMapNode is structured like this:

id Id for the current node. Can only occur once!
title The title to show in menu's.
description Optional description.
controller The controller to map this node to. Will default to "Home" if it is not specified.
action The action on that controller to map this node to. Will default to "Index" if it is not specified.
* Well, any other attribute will be used as route data values. For example, if you add "category='Books'", it will correpond with new { category = "Books" } in your route definitions.
paramid Well, this one maps to new { id = ... }, since I already used id before...

Implementing it

Two options for this one... Option one would be extending the existing XmlSiteMapProvider class, but that seemed like a no-go because... well... I wanted to take the hard way :-) Option two it is! And that's extending StaticSiteMapProvider.

This MvcSiteMapProvider class will have to do some things:

  • Read the web.config settings
  • Cache my sitemap nodes for a specified amount of time
  • Do some mapping of the current HttpContext (which is not IHttpContext, unfortunately...) to the current route
  • Security trimming! The provider should check my controllers for AuthorizeAttribute and follow the directions of that attribute.

If you want to check the full source code, feel free to download it. I'll not go trough it completely in this blog post, but just pick some interesting parts.

MvcSiteMapNode

First things first! If I want to use a custom sitemap node, I'll have to create one! Here's my overloaded version of the SiteMapNode class which now also contains a Controller and Action property:

[code:c#]

/// <summary>
/// MvcSiteMapNode
/// </summary>
public class MvcSiteMapNode : SiteMapNode
{

    #region Properties

    public string Id { get; set; }
    public string Controller { get; set; }
    public string Action { get; set; }

    #endregion

    #region Constructor

    /// <summary>
    /// Creates a new MvcSiteMapNode instance
    /// </summary>
    public MvcSiteMapNode(SiteMapProvider provider, string key)
        : base(provider, key)
    {
        Id = key;
    }

    #endregion

}

[/code]

Reading the mvcSiteMapNode XML

That's actually a nice one! Here's the full snippet:

[code:c#]

/// <summary>
/// Maps an XMLElement from the XML file to a SiteMapNode.
/// </summary>
/// <param name="node">The element to map.</param>
/// <returns>A SiteMapNode which represents the XMLElement.</returns>
protected SiteMapNode GetMvcSiteMapNodeFromXMLElement(XElement node)
{
    // Get the ID attribute, need this so we can get the key.
    string id = GetAttributeValue(node.Attribute("id"));

    // Create a new sitemapnode, setting the key and url
    var smNode = new MvcSiteMapNode(this, id);

    // Create a route data dictionary
    IDictionary<string, object> routeValues = new Dictionary<string, object>();

    // Add each attribute to our attributes collection on the sitemapnode
    // and to a route data dictionary.
    foreach (XAttribute attribute in node.Attributes())
    {
        string attributeName = attribute.Name.ToString();
        string attributeValue = attribute.Value;

        smNode[attributeName] = attributeValue;

        if (attributeName != "title" && attributeName != "description"
            && attributeName != "resourceKey" && attributeName != "id"
            && attributeName != "paramid")
        {
            routeValues.Add(attributeName, attributeValue);
        }
        else if (attributeName == "paramid")

        {
            routeValues.Add("id", attributeValue);
        }
    }

    // Set the other properties on the sitemapnode,
    // these are for title and description, these come
    // from the nodes attrbutes are we populated all attributes
    // from the xml to the node.
    smNode.Title = smNode["title"];
    smNode.Description = smNode["description"];
    smNode.ResourceKey = smNode["resourceKey"];
    smNode.Controller = smNode["controller"];
    smNode.Action = smNode["action"] ?? "Index";

    // Verify route values
    if (!routeValues.ContainsKey("controller")) routeValues.Add("controller", "Home");
    if (!routeValues.ContainsKey("action")) routeValues.Add("action", "Index");

    // Build URL
    MvcHandler handler = HttpContext.Current.Handler as MvcHandler;
    RouteData routeData = handler.RequestContext.RouteData;

    smNode.Url = "~/" + routeData.Route.GetVirtualPath(handler.RequestContext, new RouteValueDictionary(routeValues)).VirtualPath;

    return smNode;
}

[/code]

Interesting part to note are the last 4 lines of code. I'm using the application's route data to map controller, action and values to a virtual path, which will be used by all sitemap controls to link to a URL. Coolness! If I change my routes in Global.asax.cs, my menu will automatically be updated without having to change my web.sitemap file.

Security trimming

Some more code. I told you it would be a long post!

[code:c#]

/// <summary>
/// Determine if a node is accessible for a user
/// </summary>
/// <param name="context">Current HttpContext</param>
/// <param name="node">Sitemap node</param>
/// <returns>True/false if the node is accessible</returns>
public override bool IsAccessibleToUser(HttpContext context, SiteMapNode node)
{
    // Is security trimming enabled?
    if (!this.SecurityTrimmingEnabled)
        return true;

    // Is it a regular node? No need for more things to do!
    MvcSiteMapNode mvcNode = node as MvcSiteMapNode;
    if (mvcNode == null)
        return base.IsAccessibleToUser(context, node);

    // Find current handler
    MvcHandler handler = context.Handler as MvcHandler;

    if (handler != null)
    {
        // It's an MvcSiteMapNode, try to figure out the controller class
        IController controller = ControllerBuilder.Current.GetControllerFactory().CreateController(handler.RequestContext, mvcNode.Controller);

        // Find all AuthorizeAttributes on the controller class and action method
        object[] controllerAttributes = controller.GetType().GetCustomAttributes(typeof(AuthorizeAttribute), true);
        object[] actionAttributes = controller.GetType().GetMethod(mvcNode.Action).GetCustomAttributes(typeof(AuthorizeAttribute), true);

        // Attributes found?
        if (controllerAttributes.Length == 0 && actionAttributes.Length == 0)
            return true;

        // Find out current principal
        IPrincipal principal = handler.RequestContext.HttpContext.User;

        // Find out configuration
        string roles = "";
        string users = "";
        if (controllerAttributes.Length > 0)
        {
            AuthorizeAttribute attribute = controllerAttributes[0] as AuthorizeAttribute;
            roles += attribute.Roles;
            users += attribute.Users;
        }
        if (actionAttributes.Length > 0)
        {
            AuthorizeAttribute attribute = actionAttributes[0] as AuthorizeAttribute;
            roles += attribute.Roles;
            users += attribute.Users;
         }

        // Still need security trimming?
        if (string.IsNullOrEmpty(roles) && string.IsNullOrEmpty(users) && principal.Identity.IsAuthenticated)
            return true;

            // Determine if the current user is allowed to access the current node
            string[] roleArray = roles.Split(',');
            string[] usersArray = users.Split(',');
            foreach (string role in roleArray)
            {
                if (role != "*" && !principal.IsInRole(role)) return false;
            }
            foreach (string user in usersArray)
            {
                if (user != "*" && (principal.Identity.Name == "" || principal.Identity.Name != user)) return false;
            }

            return true;
    }

    return false;
}

[/code]

Now read it again, it might be a bit confusing. What actually happens, is the following:

  1. Security trimming is not enabled? Well duh! Of cource you can access this node!
  2. If the curent node that a menu control or something similar tries to render is a regular sitemap node, simply use the base class to verify security
  3. If it is an MvcSiteMapNode that we're accessing, do some work...
    1. Find out the controller and action method that's being called
    2. Check for security attributes on the controller
    3. Check for security attributes on the action method
    4. Verify if the current IPrincipal complies with all previous stuff
  4. No access granted in the past few lines of code? return false!

I can now actually hide a sitemap node from unauthorized users by simply adding the [Authorize(...)] attribute to a controller action!

Using it

Feel free to download the sample code or check the live demo. It has been configured to use my custom sitemap provider by adding the following in web.config:

[code:xml]

<system.web>
    <!-- ... -->
    <siteMap defaultProvider="MvcSitemapProvider">
        <providers>
            <add name="MvcSitemapProvider"
                 type="MvcSitemapProviderDemo.Core.MvcSitemapProvider"
                 siteMapFile="~/Web.sitemap" securityTrimmingEnabled="true"
                 cacheDuration="10" />
        </providers>
    </siteMap>
    <!-- ... -->
</system.web>

[/code]

In short: I've told ASP.NET to use my sitemap provider in favor of the standard sitemap provider. Don't you just love this provider model!

Known issues

  • The root node should always link to url "~/Index.aspx"
  • A controller + action + values combination can only occur once (but that's the case with regular sitemaps too)

Note: based on ASP.NET MVC preview 4 - A version for preview 5 can also be downloaded: MvcSiteMapProvider.cs (19.90 kb)

kick it on DotNetKicks.com 

MSDN Chopsticks on ASP.NET MVC (screencasts)

A while ago, KatrienDG asked me to do some screencasts on the ASP.NET MVC framework for the MSDN Chopsticks page. I've been working on 2 screencasts: an introductory talk to the ASP.NET MVC framework and a Test Driven Development story. Feel free to leave some comments!

kick it on DotNetKicks.com

Introduction to ASP.NET's MVC framework

Abstract: "The ASP.NET MVC framework is a new approach to web development, based on the model-view-controller design pattern. Microsoft built this framework on top of ASP.NET to allow this alternative to work with existing features like membership caching, user controls... In this video, Maarten shows you some basics on the ASP.NET MVC framework like creating a new controller action and a view."

Test Driven Development with the ASP.NET MVC framework

Abstract: "This video explains you how to develop ASP.NET MVC web applications using 2 different approaches: regular development and test-driven development."

Example code: MvcTodoList.zip (503.21 kb)