Maarten Balliauw {blog}

ASP.NET, ASP.NET MVC, Windows Azure, PHP, ...

NAVIGATION - SEARCH

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