Maarten Balliauw {blog}

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

NAVIGATION - SEARCH

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)

ASP.NET MVC - Upcoming preview 4 release

ScottGu just posted that there's an upcoming preview 4 release of the ASP.NET MVC framework. What I immediately noticed, is that there are actually some community concepts being integrated in the framework, yay! And what's even cooler: 2 of these new features are things that I've already contributed to the community (the fact that it these are included in the MVC framework now could be coincidence, though...).

Thank you, ASP.NET MVC team! This preview 4 release seems like a great step in the evolution of the ASP.NET MVC framework. Thumbs up!

kick it on DotNetKicks.com

Article on ASP.NET MVC in .NET magazine #21

.NET magazine 21 Yesterday, I received the new Dutch edition of .NET magazine containing my article on the ASP.NET MVC framework. Since the article was written quite a while ago, soucre code is no longer up-to-date. Readers who are interested (or anyone else interested in ASP.NET MVC) can download up-to-date code examples on the ASP.NET MVC guestbook page.

kick it on DotNetKicks.com

Extending ASP.NET MVC OutputCache ActionFilterAttribute - Adding substitution

In my previous blog post on ASP.NET MVC OutputCache, not all aspects of "classic" ASP.NET output caching were covered. For instance, substitution of cached pages. Allow me to explain...

When using output caching you might want to have everything cached, except, for example, a user's login name or a time stamp. When caching a full HTTP response, it is not really possible to inject dynamic data. ASP.NET introduced the Substitution control, which allows parts of a cached response to be dynamic. The contents of the Substitution control are dynamically injected after retrieving cached data, by calling a certain static method which returns string data. Now let's build this into my OutputCache ActionFilterAttribute...

UPDATE: Also check Phil Haack's approach to this: http://haacked.com/archive/2008/11/05/donut-caching-in-asp.net-mvc.aspx

1. But... how?

Schematically, the substitution process would look like this:

ASP.NET MVC OutputCache

When te view is rendered, it outputs a special substitution tag (well, special... just a HTML comment which will be recognized by the OutputCache). The OutputCache will look for these substitution tags and call the relevant methods to provide contents. A substitution tag will look like <!--SUBSTITUTION:CLASSNAME:METHODNAME-->.

One side note: this will only work with server-side caching (duh!). Client-side could also be realized, but that would involve some Ajax calls.

2. Creating a HtmlHelper extension method

Every developer loves easy-to-use syntax, so instead of writing an error-prone HTML comment like <!--SUBSTITUTION:CLASSNAME:METHODNAME-->. myself, let's do that using an extension method which allows syntax like <%=Html.Substitution<MvcCaching.Views.Home.Index>("SubstituteDate")%>. Here's an example view:

[code:c#]

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

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
    <h2><%= Html.Encode(ViewData["Message"]) %></h2>

    <p>
        Cached timestamp: <%=Html.Encode(DateTime.Now.ToString())%>
    </p>

    <p>
        Uncached timestamp (substitution):
        <%=Html.Substitution<MvcCaching.Views.Home.Index>("SubstituteDate")%>
    </p>
</asp:Content>

[/code]

The extension method for this will look quite easy. Create a new static class containing this static method:

[code:c#]

public static class CacheExtensions
{
    public static string Substitution<T>(this HtmlHelper helper, string method)
    {
        // Check input
        if (typeof(T).GetMethod(method, BindingFlags.Static | BindingFlags.Public) == null)
        {
            throw new ArgumentException(
                string.Format("Type {0} does not implement a static method named {1}.",
                    typeof(T).FullName, method),
                        "method");
        }

        // Write output
        StringBuilder sb = new StringBuilder();

        sb.Append("<!--");
        sb.Append("SUBSTITUTION:");
        sb.Append(typeof(T).FullName);
        sb.Append(":");
        sb.Append(method);
        sb.Append("-->");

        return sb.ToString();
    }
}

[/code]

What happens is basically checking for the existance of the specified class and method, and rendering the appropriate HTML comment. Our example above will output <!--SUBSTITUTION:MvcCaching.Views.Home.Index:SubstituteDate-->.

One thing to do before substituting data though: defining the SubstituteDate method on the MvcCaching.Views.Home.Index: view codebehind. The signature of this method should be static, returning a string and accepting a ControllerContext parameter. In developer language: static string MyMethod(ControllerContext context);

Here's an example:

[code:c#]

public partial class Index : ViewPage
{
    public static string SubstituteDate(ControllerContext context)
    {
        return DateTime.Now.ToString();
    }
}

[/code]

3. Extending the OutputCache ActionFilterAttribute

Previously, we did server-side output caching by implementing 2 overrides of the ActionFilterAttribute, namely OnResultExecuting and OnResultExecuted. To provide substitution support, we'll have to modify these 2 overloads a little. Basically, just pass all output through the ResolveSubstitutions method. Here's the updated OnResultExecuted overload:

[code:c#]

public override void OnResultExecuted(ResultExecutedContext filterContext)
{
    // Server-side caching?
    if (CachePolicy == CachePolicy.Server || CachePolicy == CachePolicy.ClientAndServer)
    {
        if (!cacheHit)
        {
            // Fetch output
            string output = writer.ToString();

            // Restore the old context
            System.Web.HttpContext.Current = existingContext;

            // Fix substitutions
            output = ResolveSubstitutions(filterContext, output);

            // Return rendered data
            existingContext.Response.Write(output);

            // Add data to cache
            cache.Add(
                GenerateKey(filterContext),
                writer.ToString(),
                null,
                DateTime.Now.AddSeconds(Duration),
                Cache.NoSlidingExpiration,
                CacheItemPriority.Normal,
                null);
        }
    }
}

[/code]

Now how about this ResolveSubstitutions method? This method is passed the ControllerContext and the unmodified HTML output. If no substitution tags are found, it returns immediately. Otherwise, a regular expression is fired off which will perform replaces depending on the contents of this substitution variable.

One thing to note here is that this is actually a nice security hole! Be sure to ALWAYS Html.Encode() dynamic data, as users can inject these substitution tags easily in your dynamic pages and possibly receive useful error messages with context information...

[code:c#]

private string ResolveSubstitutions(ControllerContext filterContext, string source)
{
    // Any substitutions?
    if (source.IndexOf("<!--SUBSTITUTION:") == -1)
    {
        return source;
    }

    // Setup regular expressions engine
    MatchEvaluator replaceCallback = new MatchEvaluator(
        matchToHandle =>
        {
            // Replacements
            string tag = matchToHandle.Value;

            // Parts
            string[] parts = tag.Split(':');
            string className = parts[1];
            string methodName = parts[2].Replace("-->", "");

            // Execute method
            Type targetType = Type.GetType(className);
            MethodInfo targetMethod = targetType.GetMethod(methodName, BindingFlags.Static | BindingFlags.Public);
            return (string)targetMethod.Invoke(null, new object[] { filterContext });
        }
    );
    Regex templatePattern = new Regex(@"<!--SUBSTITUTION:[A-Za-z_\.]+:[A-Za-z_\.]+-->", RegexOptions.Multiline);

    // Fire up replacement engine!
    return templatePattern.Replace(source, replaceCallback);
}

[/code]

How easy was all that? You can download the full soure and an example here.

kick it on DotNetKicks.com