April 2009 - Posts

Do you want your ASP.NET MVC application to auto redirect to a specific url after a certain interval, yes you can use javascript window.location, what about without javascript? Check this codes:

[AutoRefresh(ControllerName = "Home", ActionName = "About", DurationInSeconds = 10)]
public ActionResult Index1()

[AutoRefresh(ActionName = "About", DurationInSeconds = 15)]
public ActionResult Index2()

[AutoRefresh(RouteName = "ByFavoriteRoute", DurationInSeconds = 30)]
public ActionResult Index3()

[AutoRefresh(DurationInSeconds = 45)]
public ActionResult Index4()

If the browsers is idle for the specified period, it will automatically redirect to that Action. if you do not specify any action/controller/route (Index4) it will auto refresh the current url. How? Well I am just using/abusing some http header, check it out:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
[AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
public class AutoRefreshAttribute : ActionFilterAttribute
{
    public const int DefaultDurationInSeconds = 300; // 5 Minutes

    public AutoRefreshAttribute()
    {
        DurationInSeconds = DefaultDurationInSeconds;
    }

    public int DurationInSeconds
    {
        get;
        set;
    }

    public string RouteName
    {
        get;
        set;
    }

    public string ControllerName
    {
        get;
        set;
    }

    public string ActionName
    {
        get;
        set;
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        string url = BuildUrl(filterContext);
        string headerValue = string.Concat(DurationInSeconds, ";Url=", url);

        filterContext.HttpContext.Response.AppendHeader("Refresh", headerValue);

        base.OnResultExecuted(filterContext);
    }

    private string BuildUrl(ControllerContext filterContext)
    {
        UrlHelper urlHelper = new UrlHelper(filterContext.RequestContext);
        string url;

        if (!string.IsNullOrEmpty(RouteName))
        {
            url = urlHelper.RouteUrl(RouteName);
        }
        else if (!string.IsNullOrEmpty(ControllerName) && !string.IsNullOrEmpty(ActionName))
        {
            url = urlHelper.Action(ActionName, ControllerName);
        }
        else if (!string.IsNullOrEmpty(ActionName))
        {
            url = urlHelper.Action(ActionName);
        }
        else
        {
            url = filterContext.HttpContext.Request.RawUrl;
        }

        return url;
    }
}

Can’t think any proper usage of this action filter, may be you can use it for any iframe kind of page where you want to show some live statistics. Okay now let’s try one more time to abuse this Refresh header, this time it is a very common scenario.

You want your secured (marked as Authorized) actions to automatically redirect to login page when the session expires. Now try this:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
[AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
public class AutoRedirectToLogin : ActionFilterAttribute
{
    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        string url = FormsAuthentication.LoginUrl;
        int durationInSeconds = ((filterContext.HttpContext.Session.Timeout * 60) + 10); // Extra 10 seconds

        string headerValue = string.Concat(durationInSeconds, ";Url=", url);

        filterContext.HttpContext.Response.AppendHeader("Refresh", headerValue);

        base.OnResultExecuted(filterContext);
    }
}

When the session expires it will automatically redirect to the login page without requiring the extra click from the user.

Word of Caution: Do not use the Refresh meta header if you want your url to include in the search engine index (verified in Google, not sure about the others). For example, your Page A wants to auto redirect to Page B, the Page A only has this header, in that case Page A will not be indexed.

Roni Schuetz just informed me that he has started a new project in CodePlex to gather the mvc action filters, these two might be good candidates for his project.

Shout it

If you are familiar with YSlow recommendations, I guess you know that it recommends to put your CSS files at the top(#5)  and JavaScript files at the bottom(#6) of the pages. Placing the CSS files at the top is not an issue but putting the JavaScript files at the bottom of the pages has some gotchas, specially in heavily ajaxed based site with Master and Content page environment. When developing a Web 2.0 ajax site it is obvious that we will be using quite a number of plug-ins/widgets along with the core framework like jQuery, ExtJS, Prototype etc and of course there will be our hand coded javascript files, depending upon the size and the functionalities, the number of files can vary. This is the screenshot just to show you the usages of javascript files in DotNetShoutout/KiGG.

JSFiles

Certainly we can merge all these files into one large file and put it in the page bottom but it wont be very optimal solution, it will make our page unnecessary heavy and add delays prior making the page usable. Some will argue that it is just for the first time as the browser will cache the file and the visitor does not have to download it again. Yes true, but for a new site where a large percentage of visitors is visiting it for the first time, it is really important that it is lightening fast and reducing the number and size of the external files we can make our pages to load faster. So, instead of downloading a single gigantic file, we preferred to divide and merge those files in terms of functionality into different file sets and add specific sets to each page. The Master page contains the core javascript framework(jQuery), common parts/initialization that are reused in the content pages and the content pages have its own specific file sets and initialization scripts. And this is where it starts throwing javascript exceptions. Consider the following simple scenario:

In your master page at the bottom you have added the Utility.js with the jQuery framework:

    <script type="text/javascript" src="<%= Url.Content("~/Scripts/jquery-1.3.2.min.js")%>"></script>
    <script type="text/javascript" src="<%= Url.Content("~/Scripts/utillity.js")%>"></script>
    <script type="text/javascript">
        $(document).ready(
                            function()
                            {
                                utility.init();
                            }
                        );
    </script>
</body>

And in content page that is using the above master page you have added the dummyObject.js file at the bottom of the content page:

    <script type="text/javascript" src="<%= Url.Content("~/Scripts/dummyObject.js")%>"></script>
    <script type="text/javascript">
        $(document).ready(
                            function()
                            {
                                dummyObject.init();
                            }
                        );
    </script>
</asp:Content>

If you run the above in VS, you will find that VS enters into debug mode with a message “object expected”  like the following:

VS-Exception

Can you guess what is the problem in the above? yes, before the jQuery framework is downloaded the browser encounter the $ which is not yet defined and starts shooting exceptions.This is a very common gotchas working with Master and Content Pages and it is not ASP.NET MVC specific, the Web Forms also falls into this trap. I guess this is the reason why we have to put the ASP.NET AJAX ScriptManager prior any ajaxable control when working with the Web Forms.

There are quite a few things, we would like to solve:

  • Ensure that all script tags are rendered first no matter where it is located (Master Page/Content Page/User Control) before the initialization (document.ready of jQuery).
  • Merge all initialization statements and put into a single initialization block. The ordering should be Master –> Content Page –> User Control, again no matter how deep the nesting is.
  • Merge all cleanup statements and put into a single cleanup block ($(window).unload of jQuery) same as above initialization but the ordering should be reverse - User Control –> Content Page – > Master Page.

Now meet my tiny little component AssetManagement which solves the above issues very elegantly.

First we will see how the above issues can be solved with the controls of AssetManagement. Yes, it contains very similar controls like ASP.NET Ajax, but the key differences are, you can place it anywhere in the page, it does not generate any extra server tags, just the script tag, no view state, no hidden control nothing, you can easily use it in your ASP.NET MVC application. For the above, we will first put the jQueryScriptManager in the master page like the following:

    <readZ:jQueryScriptManager id="scriptManager" runat="Server">
        <Scripts>
            <readZ:JavaScriptReference Path="~/Scripts/jquery-1.3.2.min.js"/>
            <readZ:JavaScriptReference Path="~/Scripts/utility.js"/>
        </Scripts>
        <OnPageLoad>
            utility.init();
        </OnPageLoad>
    </readZ:jQueryScriptManager>
</body>

Next, in the Content Page:

    <readZ:JavaScriptManagerProxy id="scriptManagerProxy" runat="Server">
        <Scripts>
            <readZ:JavaScriptReference Path="~/Scripts/dummyObject.js"/>
        </Scripts>
        <OnPageLoad>
            dummyObject.init();
        </OnPageLoad>
    </readZ:JavaScriptManagerProxy>
</asp:Content>

Now, when you run, it will generate the following html output:

<script type="text/javascript" src="Scripts/jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="Scripts/utility.js"></script>
<script type="text/javascript" src="Scripts/dummyObject.js"></script>
<script type="text/javascript">
//<![CDATA[
jQuery(document).ready(function(){
            utility.init();
            dummyObject.init();
});
//]]>
</script>
</body>
</html>

You can also use the OnPageUnload property for cleanup. for example:

<readZ:jQueryScriptManager id="scriptManager" runat="Server">
    <Scripts>
        <readZ:JavaScriptReference Path="~/Scripts/jquery-1.3.2.min.js"/>
        <readZ:JavaScriptReference Path="~/Scripts/utility.js"/>
    </Scripts>
    <OnPageLoad>
        utility.init();
    </OnPageLoad>
    <OnPageUnload>
        alert('Cleanup for master page.');
    </OnPageUnload>
</readZ:jQueryScriptManager>

And

<readZ:JavaScriptManagerProxy id="scriptManagerProxy" runat="Server">
    <Scripts>
        <readZ:JavaScriptReference Path="~/Scripts/dummyObject.js"/>
    </Scripts>
    <OnPageLoad>
        dummyObject.init();
    </OnPageLoad>
    <OnPageUnload>
        alert('Cleanup for content page.');
    </OnPageUnload>
</readZ:JavaScriptManagerProxy>

It will generate:

<script type="text/javascript" src="Scripts/jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="Scripts/utility.js"></script>
<script type="text/javascript" src="Scripts/dummyObject.js"></script>
<script type="text/javascript">
//<![CDATA[
jQuery(document).ready(function(){
            utility.init();        
            dummyObject.init();
});

jQuery(window).unload(function(){
            alert('Cleanup for content page.');
            alert('Cleanup for master page.');
});
//]]>
</script>

Okay, if you have promised that you will not use any server control in your ASP.NET MVC application anymore, here is the fluent version of the exact same thing:

Master Page:

<%= Html.jQuery().Scripts(
                            script =>
                            {
                                script.Source("http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js");
                                script.Source("~/Scripts/utility.js");
                            }
                         )
                 .OnPageLoad("utility.init();")
                 .OnPageUnload("alert('Cleanup for master page.');")
%>

Content Page:

<% Html.jQuery().Scripts(script => script.Source("~/Scripts/dummyObject.js"))
                 .OnPageLoad("dummyObject.init();")
                 .OnPageUnload("alert('Cleanup for content page.');");
%>

And it will generate the same output as the controls. Note that for Content Page we are only using <% but for the master page we are using <%= which means we want to dump the result. Also the fluent syntax is progressive interface (inspired by this excellent post of  Jan Van Ryswyck) which means once you call a method, it will show the only available methods in that context like the following:

AssetFluentSyntax

The component also contains an HttpHandler which you can use to combine multiple css and javascript files, the above codes can also refer these assets. To use the asset handler, you have to first define the assets in the web.config like the following:

<configSections>
    <section name="assetSettings" type="ReadZ.AssetManagement.AssetSettingsSection, ReadZ.AssetManagement" requirePermission="false"/>
</configSections>
<assetSettings version="1.0.0.0" cacheDurationInDays="365" compress="true">
    <assets>
        <clear/>
        <add
            name="css"
            contentType="text/css"
            directory="~/assets/css"
            files="site.min.css;ui.jquery.min.css;marItUp.min.css;colorPicker.min.css"
        />
        <add
            name="js2"
            contentType="application/x-javascript"
            directory="~/assets/scripts"
            files="OpenID.min.js;jquery-1.2.6.min.js;jquery.form.min.js;jquery.validate.min.js;ui.core.min.js;ui.tabs.min.js;ui.draggable.min.js;ui.resizable.min.js;ui.dialog.min.js;Utility.min.js;Search.min.js;Tag.min.js;Membership.min.js;Story.min.js;Analytics.min.js"
        />
        <add
            name="js3"
            contentType="application/x-javascript"
            directory="~/assets/scripts"
            files="ui.autocomplete.min.js;jquery.markitup.min.js;showdown.js;colorpicker.min.js;RichEditor.min.js;ImageCode.min.js;Comment.min.js"
        />
    </assets>
    <system.web>
        <pages>
            <controls>
                <add tagPrefix="readZ" namespace="ReadZ.AssetManagement" assembly="ReadZ.AssetManagement"/>
                <add tagPrefix="readZ" namespace="ReadZ.AssetManagement.Controls" assembly="ReadZ.AssetManagement"/>
            </controls>
            <namespaces>
                <add namespace="ReadZ.AssetManagement"/>
                <add namespace="ReadZ.AssetManagement.HtmlHelpers"/>
            </namespaces>
        </pages>
        <httpHandlers>
            <add verb="GET,HEAD" path="asset.axd" validate="false" type="ReadZ.AssetManagement.AssetHandler, ReadZ.AssetManagement"/>
        </httpHandlers>
    </system.web>
    <system.webServer>
        <handlers>
            <remove name="AssetHandler"/>
            <add name="AssetHandler" preCondition="integratedMode" verb="GET,HEAD" path="asset.axd" type="ReadZ.AssetManagement.AssetHandler, ReadZ.AssetManagement"/>
        </handlers>
    </system.webServer>
</assetSettings>

As you can see, each asset has an unique name, the content type and number of files with the location. When an asset is requested it will merge the specified files into a single response, cache and compress it before sending the response. You can define global version, cache duration and compression (line 4) which will apply to all, you can also override the global settings as per asset. When referring these assets in your code you can use:

For Control:

<readZ:jQueryScriptManager id="scriptManager" runat="Server">
    <Scripts>
        <readZ:JavaScriptReference AssetName="js2"/>
    </Scripts>
</readZ:jQueryScriptManager>

For Fluent Html:

<%= Html.jQuery().Scripts(script => script.Asset("js2")) %>

When referring the CSS, you can use:

<head runat="server">
    <title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
    <link href="<%= Url.Asset("css")%>" rel="stylesheet" type="text/css"/>
</head>

The component has the extension methods for both UrlHelper and HtmlHelper, so that you can use it conveniently in your ASP.NET MVC views.

So far the codes that I have shown is for jQuery only, If you are thinking what about the others like ExtJS, prototype etc. The component has some nice extensibilities which you can use to support other popular libraries as well. For example, lets add the support for the ExtJS. First you have to create a class which implements the IScriptWrapper interface, next add codes for WrapOnPageLoad and WrapOnPageUnload methods like the following:

public class ExtJSScriptWrapper : IScriptWrapper
{
    public string WrapOnPageLoad(string scripts)
    {
        return string.IsNullOrEmpty((scripts ?? string.Empty).Trim()) ? string.Empty : string.Concat("Ext.onReady(function(){\r\n", scripts, "\r\n});");
    }

    public string WrapOnPageUnload(string scripts)
    {
        return string.IsNullOrEmpty((scripts ?? string.Empty).Trim()) ? string.Empty : string.Concat("Ext.EventManager.on(window, 'unload', function(){\r\n", scripts, "\r\n});");
    }
}

Control:

Next create a new control which inherits from base JavaScriptManager and create a new instance of the above wrapper in the constructor and that’s it.

public class ExtJSScriptManager : JavaScriptManager
{
    public ExtJSScriptManager() : base(new ExtJSScriptWrapper())
    {
    }
}

Now you will be able to use the ExtJSScriptManager like the same way as we did with jQueryScriptManager in the above.

Fluent Html:

First, create a class which inherits from base AbstractScriptHtmlHelper and create a new instance of the above wrapper in the constructor:

public class ExtJSScriptHtmlHelper : AbstractScriptHtmlHelper
{
    public ExtJSScriptHtmlHelper(HtmlHelper htmlHelper) : base(new ExtJSScriptWrapper(), htmlHelper)
    {
    }
}

Next create an extension method for the HtmlHelper which will return it.

public static class HtmlHelperExtension
{
    public static ExtJSScriptHtmlHelper ExtJS(this HtmlHelper helper)
    {
        return new ExtJSScriptHtmlHelper(helper);
    }
}

And that’s it, now you will be able to refer ExtJS in the mvc view.

The component also has the unit tests with the amazing duo (xUnit + Moq) and interestingly I was able to achieve 100% code coverage which is worth to check (Yes you will find some unnecessary tests, I did it intentionally to achieve more code coverage).

CodeCoverage

You can download the above sample code and the component from the bottom of this post.  If you have any feedback/bug report/enhancements, do let me know.

Happy YSlow scoring in your ASP.NET MVC App!!!

Download: Source Code

Shout it

Recently there has been quite some talk on HtmlHelper.RenderPartial() in ASP.NET MVC space which you will find over here and here. After reading those posts, I did an in depth analysis of the view location part, I am sharing the details with you so that it can help  both of us better understanding the internal details of the ASP.NET MVC framework. But before that I would like to suggest that the on going discussions should not be on partial view specific as there is no difference between the logic finding partial or regular view of default the WebFormViewEngine.

The default WebFormViewEngine is inherited from the base VirtualPathProviderViewEngine which in turns uses DefaultViewLocationCache for caching the location when running in web server in release mode. The DefaultViewLocationCache uses the ASP.NET built-in cache to cache the location. The following shows the code of DefaultViewLocationCache(I have excluded the other parts of the codes that are irrelevant in this discussion):

public class DefaultViewLocationCache : IViewLocationCache
{
    private static readonly TimeSpan _defaultTimeSpan = new TimeSpan(0, 15, 0);

    public static readonly IViewLocationCache Null = new NullViewLocationCache();

    public DefaultViewLocationCache() : this(_defaultTimeSpan)
    {
    }

    public void InsertViewLocation(HttpContextBase httpContext, string key, string virtualPath)
    {
        if (httpContext == null)
        {
            throw new ArgumentNullException("httpContext");
        }

        httpContext.Cache.Insert(key, virtualPath, null /* dependencies */, Cache.NoAbsoluteExpiration, TimeSpan);
    }
}

The first thing which I like to point is the default cache duration is 15 minutes(line 3 & 7) and this duration is never overridden in the ASP.NET MVC framework. Next, when caching it uses the relative expiration which means if the view is not accessed in this duration, the cache will become invalid (line 18).  So there should be a file exists check for each inactive view after the 15 minutes, so the maximum (15 x 4 x 24) = 1440 number of file checks in a day assuming that the view is accessed just after the cache becomes invalid. I did a small benchmark with the following code(FileExists method is exactly taken from the WebFormViewEngine)  and it took around 8.5 seconds for 1440 checks to complete.

public ActionResult Index()
{
    Stopwatch watch = new Stopwatch();

    watch.Start();

    for (int i = 1; i <= 1440; i++)
    {
        FileExists(ControllerContext, "~/Views/Home/TopMenu.ascx"); //Does not exist
        FileExists(ControllerContext, "~/Views/Shared/TopMenu.ascx");
    }

    watch.Stop();
    TimeSpan elapsed = watch.Elapsed;

    ViewData["elapsed"] = elapsed;

    return View();
}

private static bool FileExists(ControllerContext controllerContext, string virtualPath)
{
    try
    {
        object viewInstance = BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(object));

        return viewInstance != null;
    }
    catch (HttpException he)
    {
        if (he.GetHttpCode() == (int)HttpStatusCode.NotFound)
        {
            // If BuildManager returns a 404 (Not Found) that means the file did not exist
            return false;
        }
        else
        {
            // All other error codes imply other errors such as compilation or parsing errors
            throw;
        }
    }
    catch
    {
        return false;
    }
}

I don’t think 8.5 seconds for 1440 checks is a performance issue. But anyway, if you are wondering how to reduce the number of checks, here is the code that you can put in your global.asax or bootstrapper.

foreach (var viewEngine in ViewEngines.Engines.OfType<VirtualPathProviderViewEngine>())
{
    viewEngine.ViewLocationCache = new DefaultViewLocationCache(TimeSpan.FromHours(24));
}

The above code will cache the location for 24 hours that means there will be only one check for an inactive view in a day.

So if you are wondering why this post shows so many exceptions, the reason is as Simone pointed out that it was running in debug mode which means the path is never cached. There might be also some exceptions generated (which is gracefully handled by the ASP.NET MVC framework) when a view is accessed for the first time even running in release mode. The reason behind it, the view itself or the part of the view(partial view) is located under the shared folder instead of the controller specific view folder, the framework first tries to find the view in the controller view folder, when it does not find the view, it throws an exception, which the framework handles itself(line 31- 35 in the above FileExists method), then it again tries the shared folder and finds the view and caches the file path, so the next request for the same view in that cached period returns the path from the cache, instead of generating the exception in finding the view.

I hope the above will clarify the confusion related with view location performance concerns.

Before ending this post I would like to comment that the reason behind making the above thing bit complex(caching, handling exception internally etc etc) is due to a missing public method of asp.net BuildManager. It is obvious that the BuildManager must have the virtual path existence checking as well as file location caching and once again the Reflector proves that it does have these methods.

Reflector1

The GetVPathBuildResultInternal is called by the CreateInstanceFromVirtualPath method which is used by the WebFormViewEngine. Next, the caching, by default the ASP.NET comes with two built-in internal VirtualPathProvider: 1. MapPathBasedVirtualPathProvider and 2. ClientVirtualPathProvider. The following shows that the ClientVirtualPathProvider itself maintaining a cache:

Reflector2

So the question remain does the ClientVirtualPathProvider comes into action when resolving the virtual path which I would like to left for the ASP.NET Team to answer.

Shout it

I was looking  for an Ad Rotator for DotNetShoutout, as the sponsors are coming and most of them wants to show different images for the site and as well as in the feed. Certainly, I can use the built-in AdRotator control for the site, but it does not at all feels MVC-ish. So I decided to create a small class which mimics the same behavior as the original control.

namespace Kigg.Web
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web.Mvc;
    using System.Web.Routing;

    public class Ad
    {
        public string NavigateUrl
        {
            get;
            set;
        }

        public string Target
        {
            get;
            set;
        }

        public object LinkAttributes
        {
            get;
            set;
        }

        public string ImageUrl
        {
            get;
            set;
        }

        public string AlternateText
        {
            get;
            set;
        }

        public object ImageAttributes
        {
            get;
            set;
        }

        public string Keyword
        {
            get;
            set;
        }

        public int Impressions
        {
            get;
            set;
        }

        public static string Rotate(string keywordFilter, params Ad[] ads)
        {
            Ad ad = PickAd(keywordFilter, ads);

            string html = (ad == null) ? string.Empty : GenerateHtml(ad);

            return html;
        }

        public static string Rotate(params Ad[] ads)
        {
            return Rotate(null, ads);
        }

        private static Ad PickAd(string keywordFilter, params Ad[] ads)
        {
            Ad targetAd = null;

            IList<Ad> matchedAds = ads.Where(ad => string.Compare(ad.Keyword, keywordFilter, StringComparison.InvariantCultureIgnoreCase) == 0)
                                      .OrderBy(ad => ad.Impressions)
                                      .ToList();

            if (matchedAds.Count > 0)
            {
                int max = matchedAds.Sum(ad => ad.Impressions);
                int random = new Random().Next(max + 1);
                int runningTotal = 0;

                foreach(Ad ad in matchedAds)
                {
                    runningTotal += ad.Impressions;

                    if (random <= runningTotal)
                    {
                        targetAd = ad;
                        break;
                    }
                }

                if (targetAd == null)
                {
                    targetAd = matchedAds.Last();
                }
            }

            return targetAd;
        }

        private static string GenerateHtml(Ad ad)
        {
            Action<TagBuilder, object> merge = (builder, values) =>
                                               {
                                                   if (values != null)
                                                   {
                                                       builder.MergeAttributes(new RouteValueDictionary(values));
                                                   }
                                               };

            Action<TagBuilder, string, string> mergeIfNotBlank =    (builder, name, value) =>
                                                                    {
                                                                         if (!string.IsNullOrEmpty(value))
                                                                         {
                                                                             builder.MergeAttribute(name, value, true);
                                                                         }
                                                                    };

            TagBuilder imageBuilder = new TagBuilder("img");

            merge(imageBuilder, ad.ImageAttributes);
            mergeIfNotBlank(imageBuilder, "src", ad.ImageUrl);
            mergeIfNotBlank(imageBuilder, "alt", ad.AlternateText);

            if (!imageBuilder.Attributes.ContainsKey("alt"))
            {
                imageBuilder.Attributes.Add("alt", string.Empty);
            }

            TagBuilder linkBuilder = new TagBuilder("a");

            merge(linkBuilder, ad.LinkAttributes);
            mergeIfNotBlank(linkBuilder, "href", ad.NavigateUrl);
            mergeIfNotBlank(linkBuilder, "target", ad.Target);

            linkBuilder.InnerHtml = imageBuilder.ToString(TagRenderMode.SelfClosing);

            return linkBuilder.ToString();
        }
    }
}

And in the view I can use it like the following:

<%= Ad.Rotate(
                new Ad { NavigateUrl = "http://mysponsor1.com?tid=1", LinkAttributes = new { rel = "nofollow" }, ImageUrl = Url.Image("sponsors/mysponsor1/1.gif"), AlternateText = "My Cool Sponsor", ImageAttributes = new { style = "border:0"}, Impressions = 60 },
                new Ad { NavigateUrl = "http://mysponsor1.com?tid=2", LinkAttributes = new { rel = "nofollow" }, ImageUrl = Url.Image("sponsors/mysponsor1/2.gif"), AlternateText = "My Cool Sponsor", ImageAttributes = new { style = "border:0" }, Impressions = 40 },
                new Ad { NavigateUrl = "http://mysponsor1.com?tid=3", LinkAttributes = new { rel = "nofollow" }, ImageUrl = Url.Image("sponsors/mysponsor1/3.gif"), AlternateText = "My Cool Sponsor", ImageAttributes = new { style = "border:0"}, Impressions = 20 }
             )%>

It will randomly rotate based upon the impression value like the original control.  Though keyword filtering is not required for DotNetShoutout, but I have implemented it, maybe you can find it useful.

Shout it

[Edit 2: Kobe is now pulled off from the ASP.NET front page as well as from the news section, the msdn page also has a red notice – congrats MS for this step.]

[Edit: There are few people who mentioned that MS is not claiming it as a Best Practice, but I think they are wrong as the moment I am typing this the www.asp.net still has this tag

“The resources are intended to guide you with the planning, architecting, and implementing of Web 2.0 applications and services”

and I do have the objection when it is labeled as guide]

If you recently visited the home page of www.asp.net you will find that Microsoft has released a new Resource Kit for developing Web 2.0 Applications. The resource kit contains a sample reference application that is developed in ASP.NET MVC framework. Since both of these are my area of interest, I downloaded the codes and give a quick walkthrough. Prior getting to the main discussion, I just want to remind you that it is the second reference application (after Oxite) that is named under Microsoft, so you are suppose to get some quality architecture/design, codes etc etc. But unfortunately, it does not even qualify as an average application that is written by joe developer,  it is filled with serious flaws in both architectural design and coding and I would consider it as a worst practise of developing ASP.NET MVC application. Let me list the serious issues that I found:

Full of Magic Values in Controller

The controllers contains interesting magic values and left me wondering what's wrong with Enum. Consider the following Action of Home Controller:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Discovery(string query, string type, string section, string subSection, string page)
{
    query = Server.HtmlEncode(query);

    //Paging Section
    if (subSection != string.Empty)
    {
        if (type != "widget")
            return new ContentResult { Content = "" };

        int pageNo = 1;
        Int32.TryParse(page, out pageNo);
        if (pageNo == 0)
            pageNo = 1;

        if (section == "members")
        {
            List<UserSummary> users = null;

            switch (subSection)
            {
                case "members-mostactive":
                    users = _userService.GetMostActiveUsers(query, 8, pageNo);
                    break;
                case "members-all":
                    users = _userService.GetAllUsers(query, 8, pageNo);
                    break;
                default:
                    users = _userService.GetAllUsers(query, 8, pageNo);
                    break;
            }
            return View("MembersList", users);
        }
        else if (section == "groups")
        {
            List<Group> groups = null;

            switch (subSection)
            {
                case "groups-mostactive":
                    groups = _groupService.GetMostActiveGroupByKeyword(query, 8, pageNo);
                    break;
                case "groups-all":
                    groups = _groupService.GetAllGroupByKeyword(query, 8, pageNo);
                    break;
                default:
                    groups = _groupService.GetAllGroupByKeyword(query, 8, pageNo);
                    break;
            }
            return View("GroupsList", groups);
        }
        else if (section == "presentations")
        {

            List<PresentationSummary> presentations = null;
            switch (subSection)
            {
                case "presentations-all":
                    presentations = _presentationService.GetAllPresentationsByKewordTimeLine(query, "Day", "0", 10, pageNo);
                    break;
                case "presentations-mostpopular":
                    presentations = _presentationService.GetMostPopularPresentationsByKewordTimeLine(query, "Day", "0", 10, pageNo);
                    break;
                case "presentations-mostviewed":
                    presentations = _presentationService.GetMostViewedPresentationsByKewordTimeLine(query, "Day", "0", 10, pageNo);
                    break;
                case "presentations-mostdownload":
                    presentations = _presentationService.GetMostDownloadedPresentationsByKewordTimeLine(query, "Day", "0", 10, pageNo);
                    break;
                case "presentations-new":
                    presentations = _presentationService.GetNewPresentations(query, "Day", "0", 10, pageNo);
                    break;
                default:
                    presentations = _presentationService.GetAllPresentationsByKewordTimeLine(query, "Day", "0", 10, pageNo);
                    break;
            }
            return View("PresentationsList", presentations);
        }

        //    return new ContentResult { Content = "" };
    }

    return View();

}

Can you count the number of Magic string and integers in the above method?

Lack of Knowledge of ActionResult

Whenever the Action methods requires to return an empty result (which I think is also a design flaw) instead of EmptyResult it is returning new ContentResult { Content = "" }. Also when downloading a file, instead of using those nice FileResult, FilePathResult etc the people behind this application decided to modify the Response object directly (Check the Download method of Presentation Controller)  which clearly indicates the lack of knowledge of ASP.NET MVC Framework.

Action Methods are Populating Too Many View Data

Rather than populating the common view data with action filters or dividing the parts by Controller.RenderAction, the action methods are resposible to populate too many view data, consider the following action method:

        [AcceptVerbs(HttpVerbs.Get)]
        public ActionResult Index()
        {
            UserSummary memberOfDay = _communityService.GetUserOfDay();
            PresentationSummary presentationOfDay = _presentationService.GetPresentationOfDay();
            Group groupOfDay = _communityService.GetGroupOfDay();

            List<PresentationSummary> favouritePresentation = _communityService.GetCommunityFavorite(10,1); 
            List<PresentationSummary> userPresentationCount = _userService.GetAuthoredPresentations(memberOfDay.UserName);
            
            List<UserSummary> newUsers = _communityService.GetNewUsers(8,1);
            List<UserSummary> mostActiveUsers = _communityService.GetMostActiveUsers(8, 1);
            List<UserSummary> allUsers = _communityService.GetAllUsers(8, 1);
            int iNewMemberPageCount = decimal.Divide(_communityService.GetAllUsers().Count,8).ToInt();

            List<Group> newGroups = _communityService.GetNewGroups(8, 1);
            List<Group> mostActiveGroups = _communityService.GetMostActiveGroups(8, 1);
            List<Group> allGroups = _communityService.GetAllGroups(8, 1);
            int iNewGroupPageCount = decimal.Divide(_communityService.GetAllGroups().Count, 8).ToInt();
            int ifavouritesPageCount = 3;

            List<Group> groupsCommunity = _communityService.GetNewGroups(8,1);

            ViewData.Add("favouritePresentations", favouritePresentation.ToArray());
            ViewData.Add("UserOfDay", memberOfDay);
            ViewData.Add("GroupOfDay", groupOfDay);
            ViewData.Add("UserPresentationCount", userPresentationCount.Count);
            ViewData.Add("presentationsOfDay", presentationOfDay);            
            ViewData.Add("IsCommunityPage", true);

            ViewData.Add("members-new", newUsers);
            ViewData.Add("members-mostactive", mostActiveUsers);
            ViewData.Add("members-all", allUsers);
            ViewData.Add("members-totalcount", iNewMemberPageCount);

            ViewData.Add("groups-new", newGroups);
            ViewData.Add("groups-mostactive", mostActiveGroups);
            ViewData.Add("groups-all", allGroups);
            ViewData.Add("groups-totalcount", iNewGroupPageCount);

            ViewData.Add("favourites-totalcount", ifavouritesPageCount);


            ViewData.Add("GroupsListCommunity", groupsCommunity);


            return View();
        }

Same Action Method Returns Multiple Irrelevant View

Consider the following action:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Profile(string userId, string type, string section, string subSection, string page)
{
    string argUserName = userId;

    UserSummary _user = null;
    _user = _userService.GetUserByName(argUserName);

    if (argUserName == HttpContext.User.Identity.Name)
        ViewData["UserLogged"] = "Yes";
    else
        ViewData["UserLogged"] = "No";

    ViewData["User"] = _user;
    //Paging Section
    if (subSection != string.Empty)
    {
        if (type != "widget")
            return new ContentResult { Content = "" };

        int pageNo = 1;
        Int32.TryParse(page, out pageNo);
        if (pageNo == 0)
            pageNo = 1;

        switch (section)
        {
            case "profileContacts":
                switch (subSection)
                {
                    case "profile-contacts":
                        AddContactsToViewData(_user, pageNo);
                        return View("UserContactList");
                    case "profile-groups":
                        AddGroupsToViewData(_user, pageNo);
                        return View("UserGroupList");
                    case "profile-authors":
                        AddAuthorsToViewData(_user, pageNo);
                        return View("UserAuthorList");
                }
                break;
            case "messages":
                switch (subSection)
                {
                    case "wall-messages":
                        AddUserMessagesToViewData(_user, pageNo);
                        return View("Messages", ViewData["wallMessage"]);
                    case "wall-userinvites":
                        AddUserInvitationToViewData(_user, pageNo);
                        return View("UserInvites", ViewData["ContactInvites"]);
                    case "wall-groupinvites":
                        AddGroupInviteToViewData(_user, pageNo);
                        return View("GroupInvites", ViewData["GroupInvites"]);
                }
                break;
            case "profile":
                switch (subSection)
                {
                    case "profile-recommendedForYou":
                        AddSystemRecommendationToViewData(_user, pageNo);
                        return View("SystemRecommended", ViewData["profile-recommendedForYou"]);
                    case "profile-contactRecommendations":
                        AddContactRecommendationToViewData(_user, pageNo);
                        return View("ContactsRecommended", ViewData["profile-contactRecommendations"]);
                    case "profile-myFavorites":
                        AddFavoriteToViewData(_user, pageNo);
                        return View("FavoritePresentations", ViewData["profile-myFavorites"]);
                    case "profile-myPresentations":
                        AddPresentationAuthoredToViewData(_user, pageNo);
                        return View("AuthoredPresentations", ViewData["profile-myPresentations"]);
                }
                break;

        }
    }

    return null;
}

The same action is responsible for returning 11 different view!!!

VB6 Style variable naming

I thought we have already passed those days, but it proves me wrong:

string regStatus = _userService.RegisterUser(username, passwordRegistration, firstname, lastname, email, arrInterests);

if (regStatus == "Success")
{
    UserSummary user = _userService.GetUserByName(username);
    string callBackURL = Request.Url.ToString().Replace("Registration", "EmailConfirmation") + "/" + user.UserId.ToString();
    string strMessageBody = GetRegistrationConfirmationMailBody(firstname, callBackURL);
    string strMailSubject = "PlanetPPT - confirm you email";
    this.SendMail("admin@planetppt.com", user.Email, strMessageBody, strMailSubject);

	//More Codes
}

Check that the UserService is returning string to indicate success/failure and the controller itself is responsible for formatting and sending the email.

Still Provider Model

The Data Access is designed with the similar style of Oxite v1.0, which the community has rejected a long time ago.

Zero Unit Test

The application does not contain a single line of unit test and the worst thing is that the way it has been architect there is zero chance that you will be able to write it without refactoring the architecture.

This is so far I think it worth to investigate and I can assure you that you will be able to find more flaws if you have the time and patience.

At last, my urge to Microsoft, please take down this application from both www.asp.net and MSDN or put a notice that the application is not up to the standard and do not follow it anyway as it is full of bad practices. Do introduce a review board and get the approval before releasing this kind of application. Remember this is not the first time that this kind of controversy was born, so please ensure the quality and standard.

Shout it

ASP.NET MVC comes with different action results for various purpose, but some times, you will find those are not adequate for your scenario, lets consider the following screenshot:

Dashboard

When user navigates to a different tab or change the sort order or moves to a different page number, it will load the content as an ajax call, but if the user has JavaScript turned off, it will redirect to a regular url. One solution would be to create separate sets of controller actions that will return either the complete view (in case JavaScript is turned off) or the partial view and use the jQuery load to show the content. But the problem of this approach is that we will be duplicating the same logic for both the actions, moreover we will be returning the unnecessary html tags rather than pure json object that is sufficient for rendering the view. A better solution would be adaptive rendering. If you are not familiar with adaptive rendering then let me clarify it a bit, it is a process where the server responds differently depending upon the browser capability. So it has a broader scope(e.g. mobile devices, text only browsers etc) comparing  to our above JavaScript on/off scenario. But for the time being let us only focus on the above, so instead of creating pure html/ajax only version we would like to take the advantage of the browser capability and for this we will create a new action result, the beauty of the new action result is, the controller remains completely unaware of what kind of request it is serving. Lets take a look of the controller action that is serving both:

public ActionResult Dashboard(string userName, StoryListTab tab, OrderBy orderBy, int? page)
{
    StoryListViewModel viewModel = new StoryListViewModel
                                       {
                                           SelectedTab = tab,
                                           CurrentPage = page ?? 1,
                                           StoryPerPage = _storyPerPage,
                                           SelectedOrderBy = orderBy,
                                           UrlFormat = ((Route) RouteTable.Routes["Dashboard"]).Url
                                       };

    int start = PageCalculator.StartIndex(page, _storyPerPage);

    PagedResult<IStory> pagedResult = _dashBoardMethods[tab](_storyService, userName, start, _storyPerPage, orderBy);

    viewModel.TotalStoryCount = pagedResult.Total;
    viewModel.Stories = pagedResult.Result;

    return AdaptiveView(viewModel);
}

Check that we are not returning the ViewResult, instead we are returning a new action result AdaptiveViewResult. This action result is intelligent enough to decide what to return depending upon the request.

public class AdaptiveViewResult : ViewResult
{
    private readonly IEnumerable<JavaScriptConverter> _converters;

    public AdaptiveViewResult() : this(ServiceLocator.Current.GetAllInstances<JavaScriptConverter>())
    {
    }

    public AdaptiveViewResult(IEnumerable<JavaScriptConverter> converters)
    {
        _converters = converters;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context.HttpContext.Request.IsAjaxRequest())
        {
            ExecuteAjaxView(context);
        }
        else
        {
            ExecuteHtmlView(context);
        }
    }

    private void ExecuteAjaxView(ControllerContext context)
    {
        JavaScriptSerializer jsonSerializer = CreateJsonSerializer();

        //We can't serialize the modelState as it has deep nesting with objects that are not serializable.
        //So we need to convert it to serializable object.
        var modelStates = ViewData.ModelState.IsValid ?
                          null :
                          ViewData.ModelState.Select(ms => new
                                                                {
                                                                    ms.Key,
                                                                    Errors = ms.Value.Errors
                                                                                     .Select(error => (error.Exception == null) ? error.ErrorMessage : error.Exception.Message)
                                                                                     .Where(error => !string.IsNullOrEmpty(error))
                                                                                     .AsEnumerable()
                                                                }
                                                    ).Where(ms => ms.Errors.Count() > 0) //No need to include items that does not have errors
                                                    .ToList();

        var result = new
                        {
                            model = ViewData.Model,
                            modelStates
                        };

        string json = jsonSerializer.Serialize(result);

        HttpResponseBase response = context.HttpContext.Response;

        response.ContentType = "application/json";
        response.Write(json);
    }

    private void ExecuteHtmlView(ControllerContext context)
    {
        base.ExecuteResult(context);
    }

    private JavaScriptSerializer CreateJsonSerializer()
    {
        JavaScriptSerializer serializer = new JavaScriptSerializer();

        serializer.RegisterConverters(_converters);

        return serializer;
    }
}

The AdaptiveViewResult is inheriting from the regular ViewResult but when returning the result it will return based upon the type of request and when serializing, it serializes both the model and modelStates. Here is the client side code that updates content with the json result.

//Remove with # as we can do ajax request
$('#storyListTabs > ul.ui-tabs-nav > li > a').attr('href', '#')
$('#storyListTabs > ul.ui-tabs-nav > li').click(
                                                    function()
                                                    {
                                                        Story._selectTab($(this));
                                                    }
                                                );

_selectTab: function(target)
{
    var selectedTab = target.children('a').text();

    //No need to proceed further when clicking the same tab
    if ((Story._currentPage === 1) && (Story._currentTab === selectedTab))
    {
        return;
    }

    Story._switchTab(selectedTab);
},

_switchTab: function(selectedTab)
{
    Story._load(
                    selectedTab,
                    1,
                    Story._currentOrderBy,
                    function()
                    {
                        Story._currentTab = selectedTab;
                        //Reset to first page
                        Story._currentPage = 1;

                        $('#storyListTabs > ul.ui-tabs-nav > li').removeClass('ui-tabs-selected ui-state-active')
                                                                 .find('a').each(
                                                                                    function()
                                                                                    {
                                                                                        var a = $(this);

                                                                                        if (a.text() == selectedTab)
                                                                                        {
                                                                                            a.parent('li').addClass('ui-tabs-selected ui-state-active');
                                                                                        }
                                                                                    }
                                                                                );
                    }
                );
},

_load: function(tab, page, orderBy, callback)
{
    var url = Story._buildUrl(tab, page, orderBy);

    $U.blockUI($('#storyListTabs'));

    $.ajax(
                {
                    url: url,
                    type: 'GET',
                    dataType: 'json',
                    success: function(result)
                    {
                        $U.unblockUI();
                        Story._pageCount = result.model.pageCount;
                        Story._show(result.model);
                        callback();
                        Story._updateState();
                    },
                    error: function(e)
                    {
                        $U.unblockUI();
                    }
                }
            );
},

_show: function(model)
{
    var list = $('#list');

    if (model.stories.length > 0)
    {
        var content = '';

        for (var i = 0; i < model.stories.length; i++)
        {
            content += Story._createStoryHtml(model.stories[i]);
        }

        $('#sortBar').show();
        $('#notify').hide();
        list.empty().html(content);
        $('#actionBar').show();
    }
    else
    {
        $('#sortBar').hide();
        $('#notify').show();
        list.empty();
        $('#actionBar').hide();
    }

    if (model.pageCount > 1)
    {
        $('#pager').show();
    }
    else
    {
        $('#pager').hide();
    }
},

I have excluded other parts of the JavaScripts and yes you have to write some complex jQuery codes to achieve the smooth user experience and there is no alternate to it.

In the above AdaptiveViewResult we are also serializing the ModelStates, but it was not used, now lets take another example where it is used, the following is a typical signup form:

Signup

The validation are regular rules like all the fields are required, password and user name should not contain any special character and user name and email should be unique. We can use the jQuery validation plug-in to validate in the client side, but things getting complex when we want to ensure the unique user name and email, certainly we can use the jQuery validation remote, but it does not guarantee the uniqueness of those fields on the actual creation, moreover we would also like to ajaxify the form with the jQuery form plug-in. Now, lets first check the action method of the controller:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult SignUp(string userName, string password, string confirmPassword, string email)
{
    if (ValidateSignUp(userName, password, confirmPassword, email))
    {
        try
        {
            IUser user = _membershipService.Register(userName, password, email);
            _formsAuthentication.SetAuthticationCookie(user.UserName, false);

            return AdaptiveRedirect(Url.Dashboard());
        }
        catch (ReadZException e)
        {
            //Throws when User Name or Email is not unique
            ModelState.AddModelError(ModelStateException, e.Message);
        }
    }

    return AdaptiveView();
}

As you can see we are returning the same AdaptiveViewResult when the validation fails or when the user name and email is not unique, but when everything is okay we are returning another new action result AdaptiveRedirectResult rather than the regular RedirectResult. When using the jQuery forms plug-in there is no way to get the underlying XmlHttpRequest object in the JavaScript, so we cannot get the header which is set by the regular RedirectResult. The AdaptiveRedirectResult is inherited from the regular RedirectResult but for ajax request, it serializes the url rather than redirecting, so in the client side we can easily get the redirect url from the response.

public class AdaptiveRedirectResult : RedirectResult
{
    public AdaptiveRedirectResult(string url) : base(url)
    {
    }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context.HttpContext.Request.IsAjaxRequest())
        {
            AjaxRedirect(context);
        }
        else
        {
            base.ExecuteResult(context);
        }
    }

    private void AjaxRedirect(ControllerContext context)
    {
        JsonResult result = new JsonResult { Data = new { redirectUrl = Url } };

        result.ExecuteResult(context);
    }
}

Now let take a look at the client side jQuery code:

$.validator.addMethod(
                        'userName',
                        function(value, element)
                        {
                            return /^([a-zA-Z])[a-zA-Z_-]*[\w_-]*[\S]$|^([a-zA-Z])[0-9_-]*[\S]$|^[a-zA-Z]*[\S]$/.test(value);
                        },
                        'User name must be alphanumeric characters which starts with alphabet and can only contains special characters dash and underscore.'
                    );

$('#signUp').validate(
                            {
                                rules:
                                        {
                                            userName:
                                                                {
                                                                    required: true,
                                                                    minlength: Membership._minUserNameLength,
                                                                    userName: true
                                                                },
                                            password:
                                                                {
                                                                    required: true,
                                                                    minlength: Membership._minPasswordLength
                                                                },
                                            confirmPassword:
                                                                {
                                                                    required: true,
                                                                    minlength: Membership._minPasswordLength,
                                                                    equalTo: '#password'
                                                                },
                                            email:
                                                              {
                                                                  required: true,
                                                                  email: true
                                                              }
                                        },
                                messages:
                                            {
                                                userName:
                                                            {
                                                                required: 'User name cannot be blank.',
                                                                minlength: 'User name cannot be less than ' + Membership._minUserNameLength + ' characters.'
                                                            },
                                                password:
                                                            {
                                                                required: 'Password cannot be blank.',
                                                                minlength: 'Password cannot be less than ' + Membership._minPasswordLength + ' characters.'
                                                            },
                                                confirmPassword:
                                                                    {
                                                                        required: 'Confirm password cannot be blank.',
                                                                        minlength: 'Confirm password cannot be less than ' + Membership._minPasswordLength + ' character.',
                                                                        equalTo: 'Confirm password does not match with password.'
                                                                    },
                                                email:
                                                          {
                                                              required: 'Email cannot be blank.',
                                                              email: 'Email address format is not correct.'
                                                          }
                                            },
                                submitHandler: function(form)
                                {
                                    var options = {
                                                        dataType: 'json',
                                                        beforeSubmit: function()
                                                        {
                                                            $U.disableInputs($(form), true);
                                                        },
                                                        success: function(result)
                                                        {
                                                            $U.disableInputs($(form), false);

                                                            if (result.redirectUrl)
                                                            {
                                                                window.location.href = result.redirectUrl;
                                                            }
                                                            else
                                                            {
                                                                $U.mapErrors(result.modelStates);
                                                                $U.focus($('#userName'));
                                                            }
                                                        }
                                                };

                                    $(form).ajaxSubmit(options);
                                    return false;
                                },
                                errorContainer: 'div.validationErrors',
                                errorLabelContainer: 'div.validationErrors ul',
                                wrapper: 'li',
                                highlight: onHighlight,
                                unhighlight: onUnhighlight
                            }
                        );

mapErrors: function(modelStates, selector)
{
    var summary = selector ? $(selector) : $('div.validationErrors');

    var list = summary.children('ul');
    list.empty();

    for (var i = 0; i < modelStates.length; i++)
    {
        var e = $('#' + modelStates[i].Key);

        e.addClass('input-validation-error');
        var span = e.next('span.error');
        span.addClass('error').text('*').show();

        for (var j = 0; j < modelStates[i].Errors.length; j++)
        {
            list.append('<li><label class=\"error\" for=\"' + modelStates[i].Key + '\" generated=\"true\">' + modelStates[i].Errors[j] + '</label></li>');
        }
    }

    list.show();
    summary.show();
},

As you can see (line 73- 80) when the form post is successful it redirects to the url that the server is returning in json and when the post fails it shows the server side errors like user name or email is not unique.

That’s it.

In this post I have shown how you can implement adaptive rendering in ASP.NET MVC thus creating rich user experience. And of course it requires bit of work and it is not as painless as ASP.NET AJAX UpdatePanel. But it will give the complete control over the data that is transmitting and more control the way we are interacting with the user.

There is no attached source codes in this post, I am still working on it and this should be the reference implmentation of my ASP.NET MVC Best Practice.

Shout it

Just to let you know that I have uploaded the latest source of KiGG in Codeplex. Other than upgrading to ASP.NET MVC RTM, there are few enhancements:

  1. Implementing EventAggregator.
  2. Background Services like:
    • Broadcast in Twitter.
    • Ping different Feed Servers automatically.
  3. Restrict Story submit from specific domains.
  4. SQL Server Full Text search.
  5. Other minor enhancements and bug fixes.

And yes http://dotnetshoutout.com is currently running the same codebase. Those who are assuming whether KiGG is following the same practices which I have mentioned recently over here and here, let me tell you, no it is not following all the items, I am in a process of refactoring it following those rest of the items which I hope to deliver in a few weeks or I will post the tiny app that I am also developing.

Stay tuned.

Shout it

This is the second part of the series and may be the last, till I find some thing new. My plan was to start with routing, controller, controller to model, controller to view and last of all the view, but some how I missed one important thing in routing, so I will begin with that in this post.

15. Routing consideration

If you are developing a pure ASP.NET MVC application, turn off existing file check of routes, it will eliminate unnecessary file system check. Once you do it there are few more things you have to consider. Remember when you are hosting application in IIS7 integrated mode, your ASP.NET application will intercept all kind of request, no matter what the file extension is. So you have to add few more things in the ignore list which your ASP.NET MVC application will not process. This might include static files like html, htm, text file specially robots.txt, favicon.ico, script, image and css etc. This is one of the reason, why I do not like the default directory structure (Contents and Scripts folder) mentioned in my #2 in the previous post. The following is somewhat my standard template for defining routes when hosting in IIS7:

_routes.Clear();

// Turns off the unnecessary file exists check
_routes.RouteExistingFiles = true;

// Ignore text, html, files.
_routes.IgnoreRoute("{file}.txt");
_routes.IgnoreRoute("{file}.htm");
_routes.IgnoreRoute("{file}.html");

// Ignore axd files such as assest, image, sitemap etc
_routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

// Ignore the assets directory which contains images, js, css & html
_routes.IgnoreRoute("assets/{*pathInfo}");

// Ignore the error directory which contains error pages
_routes.IgnoreRoute("ErrorPages/{*pathInfo}");

//Exclude favicon (google toolbar request gif file as fav icon which is weird)
_routes.IgnoreRoute("{*favicon}", new { favicon = @"(.*/)?favicon.([iI][cC][oO]|[gG][iI][fF])(/.*)?" });

//Actual routes of my application

Next, few of my personal preference rather than guideline, by default ASP.NET MVC generates url like {controller}/{action} which is okay when you are developing multi-module application,  for a small application, I usually prefer the action name without the controller name, so instead of www.yourdomain.com/Story/Dashboard, www.yourdomain.com/Membership/SignIn it will generate www.yourdomain.com/Dashboard, www.yourdomain.com/Signin. So I add few more routes:

_routes.MapRoute("SignUp", "SignUp", new { controller = "Membership", action = "SignUp" });
_routes.MapRoute("SignIn", "SignIn", new { controller = "Membership", action = "SignIn" });
_routes.MapRoute("ForgotPassword", "ForgotPassword", new { controller = "Membership", action = "ForgotPassword" });
_routes.MapRoute("SignOut", "SignOut", new { controller = "Membership", action = "SignOut" });
_routes.MapRoute("Profile", "Profile", new { controller = "Membership", action = "Profile" });
_routes.MapRoute("ChangePassword", "ChangePassword", new { controller = "Membership", action = "ChangePassword" });

_routes.MapRoute("Dashboard", "Dashboard/{tab}/{orderBy}/{page}", new { controller = "Story", action = "Dashboard", tab = StoryListTab.Unread.ToString(), orderBy = OrderBy.CreatedAtDescending.ToString(), page = 1 });
_routes.MapRoute("Update", "Update", new { controller = "Story", action = "Update" });
_routes.MapRoute("Submit", "Submit", new { controller = "Story", action = "Submit" });

_routes.MapRoute("Home", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = string.Empty });

16. Create new ActionResult if required

ASP.NET MVC has quite a number of ActionResult for different purposes, but still we might need new ActionResult. For example xml, rss, atom etc. In those cases, I would suggest instead of using the generic ContentResult, create new ActionResult. The MVCContrib has an XmlResult which you can use for returning xml but no support for feed. Yes it is obviously tricky to convert an unknown object into rss/atom, in those cases you can create model specific ActionResult. For example:

public class AtomResult : ActionResult
{
    public AtomResult(string siteTitle, string feedTitle, IEnumerable<IStory> stories)
    {
        SiteTitle = siteTitle;
        FeedTitle = feedTitle;
        Stories = stories;
    }

    public string SiteTitle
    {
        get;
        private set;
    }

    public string FeedTitle
    {
        get;
        private set;
    }

    public IEnumerable<IStory> Stories
    {
        get;
        private set;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        string xml = Build(context);

        HttpResponseBase response = context.HttpContext.Response;
        response.ContentType = "application/atom+xml";
        response.Write(xml);
    }
}

And in Controller:

[AcceptVerbs(HttpVerbs.Get), OutputCache(CacheProfile = "Atom")]
public ActionResult Shared()
{
    IEnumerable<stories> stories = GetSharedStories();

    return new AtomResult("My Site", "My shared stories in atom", stories);
}

17. Split your View into multiple ViewUserControl

Split your view into multiple ViewUserControl when it is getting bigger, it really does not matter whether the same UserControl is reused in another page, it makes the very view much more readable. Consider the following view:

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    My Secret App : Dashboard
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <div id="heading"></div>
    <div class="columns">
        <div id="main" class="column">
            <div id="storyListTabs" class="ui-tabs ui-widget ui-widget-content ui-corner-all">
                <% Html.RenderPartial("TabHeader", Model);%>
                <div id="tabContent" class="ui-tabs-panel ui-widget-content ui-corner-bottom">
                    <div id="storyList">
                        <% Html.RenderPartial("SortBar", Model);%>
                        <div class="clear"></div>
                        <% Html.RenderPartial("NoLinkMessage", Model);%>
                        <form id="update" action="<%= Url.Update()%>" method="post">
                            <% Html.RenderPartial("List", Model);%>
                            <% Html.RenderPartial("ActionBar", Model);%>
                            <% Html.RenderPartial("Pager", Model);%>
                        </form>
                    </div>
                </div>
            </div>
            <%Html.RenderPartial("Submit", new StorySubmitViewModel());%>
        </div>
        <div id="sideBar" class="column"></div>
    </div>
</asp:Content>

18. HtmlHelper extension

First, read this great post of Rob Conery and I completely agree, you should create helper methods for each condition and I would also suggest to create helper methods for reusable UI elements like the ASP.NET MVC team did, but I have a different suggestion about the placing of these methods that we are currently practicing.

Application Developer

Do only create extension methods of HtmlHelper if you are using it in more than one view. Otherwise create a view specific helper and create an extension method of the HtmlHelper which returns the view specific helper. for example:

    public class DashboardHtmlHelper
    {
        private readonly HtmlHelper _htmlHelper;

        public DashboardHtmlHelper(HtmlHelper htmlHelper)
        {
            _htmlHelper = htmlHelper;
        }

        public string DoThis()
        {
            //Your Code
        }

        public string DoThat()
        {
            //Your Code
        }
    }

    public static class HtmlHelperExtension
    {
        public static DashboardHtmlHelper Dashboard(this HtmlHelper htmlHelper)
        {
            return new DashboardHtmlHelper(htmlHelper);
        }
    }

Now, you will able to use it in the view like:

<%= Html.Dashboard().DoThis() %>

UI Component Developer

If you are developing some family of UI components that will be reusable across different ASP.NET MVC application, create a helper with your component family name like the above, if you are a commercial vendor maybe your company name then add those methods in that helper. Otherwise there is a very good chance of method name collision.

The same is also applied if you are extending the IViewDataContainer like the MVCContrib.org.

19. Encode

Whatever you receive from the User always use Html.Encode(“User Input”) for textNode and Html.AttributeEncode(“User Input”) for html element attribute.

20. Do not put your JavaScript codes in your View

Do not intermix your javascript with the html, create separate js files and put your java script in those files. Some time, you might need to pass your view data in your java script codes, in those cases only put your initialization in the view. For example, consider you are developing Web 2.0 style app where you want to pass ajax method url, and some other model data  in the java script codes, in those cases you can use some thing like the following:

The View:

        <div id="sideBar" class="column"></div>
        <script type="text/javascript">
            $(document).ready(function(){
                Story.init('<%= Model.UrlFormat %>', '<%= Url.NoIcon() %>', <%= Model.PageCount %>, <%= Model.StoryPerPage %>, <%= Model.CurrentPage %>, '<%= Model.SelectedTab %>', '<%= Model.SelectedOrderBy %>');
            });
        </script>
    </div>
</asp:Content>

And JavaScript:

var Story =
{
    _urlFormat: '',
    _noIconUrl: '',
    _pageCount: 0,
    _storyPerPage: 0,
    _currentPage: 0,
    _currentTab: '',
    _currentOrderBy: '',

    init: function(urlFormat, noIconUrl, pageCount, storyPerPage, currentPage, currentTab, currentOrderBy)
    {
        Story._urlFormat = urlFormat;
        Story._noIconUrl = noIconUrl;
        Story._pageCount = pageCount;
        Story._storyPerPage = storyPerPage;
        Story._currentPage = currentPage;
        Story._currentTab = currentTab;
        Story._currentOrderBy = currentOrderBy;

		//More Codes
    }
}

And those who are not familiar with the above  JavaScript code, it is an example of creating static class in JavaScript. And one more thing before I forgot to mention, never hard code your ajax method url in your javascript file, no  matter Rob Conery or Phil Haack does it in their demo. It is simply a bad practice and demolish the elegance of ASP.NET Routing.

21. Use jQuery and jQuery UI

Use jQuery and jQuery UI, nothing can beats it and use Google CDN to load these libraries.

<link type="text/css" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.1/themes/{YOUR Prefered Theme}/jquery-ui.css" rel="stylesheet"/>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.1/jquery-ui.js"></script>

Or much better:

<link type="text/css" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.1/themes/{YOUR Prefered Theme}/jquery-ui.css" rel="stylesheet"/>
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript">
	google.load("jquery", "1.3.2");
	google.load("jqueryui", "1.7.1");
</script>

And that's it for the time being.

At the end, I just want congratulate the ASP.NET MVC Team for developing such an excellent framework and specially the way they took the feedback from the community and I look forward to develop few more killer apps in this framework.

Shout it

[Also check out the next part of this series]

In this post, I will share some of the best practices/guideline in developing ASP.NET MVC applications which I have learned in the hard way. I will not tell you to use DI or Unit Test instead I will assume you are already doing it and you prefer craftsmanship over anything.

1. Create Extension methods of UrlHelper to generate your url from Route

Avoid passing the controller, action or route name as string, create extension methods of UrlHelper which encapsulates it, for example:

public static class UrlHelperExtension
{
    public static string Home(this UrlHelper helper)
    {
        return helper.Content("~/");
    }

    public static string SignUp(this UrlHelper helper)
    {
        return helper.RouteUrl("Signup");
    }

    public static string Dashboard(this UrlHelper helper)
    {
        return Dashboard(helper, StoryListTab.Unread);
    }

    public static string Dashboard(this UrlHelper helper, StoryListTab tab)
    {
        return Dashboard(helper, tab, OrderBy.CreatedAtDescending, 1);
    }

    public static string Dashboard(this UrlHelper helper, StoryListTab tab, OrderBy orderBy, int page)
    {
        return helper.RouteUrl("Dashboard", new { tab = tab.ToString(), orderBy = orderBy.ToString(), page });
    }

    public static string Update(this UrlHelper helper)
    {
        return helper.RouteUrl("Update");
    }

    public static string Submit(this UrlHelper helper)
    {
        return helper.RouteUrl("Submit");
    }
}

Now, You can use the following in your view:

<a href="<%= Url.Dashboard() %>">Dashboard</a>
<a href="<%= Url.Profile() %>">Profile</a>

Instead of:

<%= Html.ActionLink("Dashboard", "Dashboard", "Story") %>
<a href="<%= Url.RouteUrl("Profile")%>">Profile</a>

And in Controller I can use:

return Redirect(Url.Dashboard(StoryListTab.Favorite, OrderBy.CreatedAtAscending, 1))

Instead of:

return RedirectToAction("Dashboard", "Story", new { tab = StoryListTab.Favorite, orderBy = OrderBy.CreatedAtAscending, page = 1 });

Of course you can use the strongly typed version which takes the controller, method and the parameters of the future assembly or create your own to avoid the future refactoring pain, but remember it is not officially supported and might change in the future. You can also use the above with the strongly typed version, certainly “adding another layer of indirection” (Scott Ha favorite quote) has some benefits. Another benefit when writing Unit Test you will only deal with the RedirectResult rather than RediretResult and RedirectToRouteResult.

2. Create Extension Method of UrlHelper to map your JavaScript, Stylesheet and Image Folder

By default ASP.NET MVC creates Content, Scripts folder for these things, which I do not like, Instead I like the following folder structure so that I can only set static file caching  on the Assets folder in IIS instead of going to multiple folders:

Assets
+images
+scripts
+stylesheets

No matter what the structure is, create some extension method of UrlHelper to map these folders, so that you can easily refer it in your view and later on if you need to change the structure, you do not have to do massive find/replace. I would also recommend to create extension methods for those assets which are often refereed in your views. For example:

public static string Image(this UrlHelper helper, string fileName)
{
    return helper.Content("~/assets/images/{0}".FormatWith(fileName));
}

public static string Stylesheet(this UrlHelper helper, string fileName)
{
    return helper.Content("~/assets/stylesheets/{0}".FormatWith(fileName));
}

public static string NoIcon(this UrlHelper helper)
{
    return Image(helper, "noIcon.png");
}

And when referring the assets you can use:

<link href="<%= Url.Stylesheet("site.css")%>" rel="stylesheet" type="text/css"/>

Instead of the default:

<link href="../../Content/Site.css" rel="stylesheet" type="text/css" />

3. Use Bootstrapper in Global.asax

I have already covered it in the past, basically if you are doing a lot things in Application_Start of global.asax e.g. Registering Routes, Registering Controller Factory, Model Binders, View Engine, starting application specific background services, create individual task for specific part, then do use Bootstrapper to execute those. This make your code lot more clean and testable. This will be also helpful when developing a portal kind of app in asp.net mvc where each module can have some startup initialization without affecting others. But if you are developing a small app where the above things will never be an issue you can surly go ahead with the default global.asax.

4. Do not make any hard dependency on the DI Container, use Common Service Locator

Do not clutter your code with any specific DI reference, instead use the Common Service Locator, it is an abstraction over the underlying DI and it has the support for all of the popular DI containers, so that you can replace the underlying DI without modifying your application code as each DI Container has some unique features over the others. Tim Barcz recently wrote a excellent post on this subject, how much obsessed we are with our favorite DI Container but I am not sure why he did not mention about it. The Common Service Locator has the support for most of the regular scenarios, but for specific case for example injecting dependency in already instantiated object which as per my knowledge StructureMap, Ninject and Unity supports you can call the static ServiceLocator.Current.GetInstance in the constructor instead of calling the underlying DI. And for those who do know, Common Service Locator is a joint effort of the DI Containers creators initiated by Jeremy D Miller.

Creating Controller Factory with Common Service Locator is very easy:

public class CommonServiceLocatorControllerFactory : DefaultControllerFactory
{
    protected override IController GetControllerInstance(Type controllerType)
    {
        return (controllerType == null) ? base.GetControllerInstance(controllerType) : ServiceLocator.Current.GetInstance(controllerType) as IController;
    }
}

I hope the MVCContrib guys will follow the same instead of creating separate Controller Factory for each Container.

5. Decorate your Action Methods with Proper AcceptVerbs Attribute

ASP.NET MVC is much more vulnerable comparing to Web Forms. Make sure the action methods that modifies the data only accepts HttpVerbs.Post. If security is too much concern use the ValidateAntiForgeryToken or you can use Captcha. Derik Whittaker has an excellent post as well as a screen cast on how to integrate reCaptcha with ASP.NET MVC application, which I highly recommend. (Side Note: Do not miss a single episode of DimeCasts.net, I have learnt a lot form those short screen casts). My rule of thumb is use HttpVerbs.Post for all data modification actions and HttpVerbs.Get for data reading operations.

6. Decorate your most frequent Action Methods with OutputCache Attribute

Use OutputCache attribute when you are returning the less frequent updated data, prime candidate may be your home page, feed etc etc. You can use it for both Html and Json data types. When using it, only specify the Cache Profile name, do not not specify any other thing, use the web.config output cache section to fine tune it. For example:

[AcceptVerbs(HttpVerbs.Get), OutputCache(CacheProfile = "Dashboard")]
public ActionResult Dashboard(string userName, StoryListTab tab, OrderBy orderBy, int? page)
{
}

And in web.config:

<system.web>
        <caching>
            <outputCacheSettings>
                <outputCacheProfiles>
                    <clear/>
                    <!-- 15 Seconds -->
                    <add
                        name="Dashboard"
                        duration="15"
                        varyByParam="*"
                        location="Client"
                    />
                </outputCacheProfiles>
            </outputCacheSettings>
        </caching>
</system.web>

7. Keep your controller free from HttpContext and its tail

Make sure your controller does not have to refer the HttpContext and its tail. it will make your life easier when unit testing your Controller. If you need to access anything from HttpContext like User, QueryString, Cookie etc use custom action filter or create some interface and wrapper and pass it in the constructor. For example, for the following Route:

_routes.MapRoute("Dashboard", "Dashboard/{tab}/{orderBy}/{page}", new { controller = "Story", action = "Dashboard", tab = StoryListTab.Unread.ToString(), orderBy = OrderBy.CreatedAtDescending.ToString(), page = 1 });

But the controller action methods is declared as:

[AcceptVerbs(HttpVerbs.Get), OutputCache(CacheProfile = "Dashboard"), UserNameFilter]
public ActionResult Dashboard(string userName, StoryListTab tab, OrderBy orderBy, int? page)
{
}

The UserNameFilter is responsible for passing the UserName:

public class UserNameFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        const string Key = "userName";

        if (filterContext.ActionParameters.ContainsKey(Key))
        {
            if (filterContext.HttpContext.User.Identity.IsAuthenticated)
            {
                filterContext.ActionParameters[Key] = filterContext.HttpContext.User.Identity.Name;
            }
        }

        base.OnActionExecuting(filterContext);
    }
}

[Update: Make sure you have decorate either the Action or the Controller with Authorize attribute, check the comments]

8. Use Action Filter to Convert to compatible Action Methods parameters

Use Action Filter to convert incoming values to your controller action method parameters, again consider the Dashboard method, we are accepting tab and orderBy as Enum.

[AcceptVerbs(HttpVerbs.Get), OutputCache(CacheProfile = "Dashboard"), StoryListFilter]
public ActionResult Dashboard(string userName, StoryListTab tab, OrderBy orderBy, int? page)
{
}

The StoryListFilter will be responsible to convert it to proper data type from route values/querystrings.

public class StoryListFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        const string TabKey = "tab";
        const string OrderByKey = "orderBy";

        NameValueCollection queryString = filterContext.HttpContext.Request.QueryString;

        StoryListTab tab =  string.IsNullOrEmpty(queryString[TabKey]) ?
                            filterContext.RouteData.Values[TabKey].ToString().ToEnum(StoryListTab.Unread) :
                            queryString[TabKey].ToEnum(StoryListTab.Unread);

        filterContext.ActionParameters[TabKey] = tab;

        OrderBy orderBy =   string.IsNullOrEmpty(queryString[OrderByKey]) ?
                            filterContext.RouteData.Values[OrderByKey].ToString().ToEnum(OrderBy.CreatedAtDescending) :
                            queryString[OrderByKey].ToEnum(OrderBy.CreatedAtDescending);

        filterContext.ActionParameters[OrderByKey] = orderBy;

        base.OnActionExecuting(filterContext);
    }
}

You can also use the custom Model Binder for the same purpose. In that case you will have to create two custom Model Binders for each Enum instead of one action filter. Another issue with the Model Binder is once it is registered for a type it will always come into action, but action filter can be selectively applied.

9. Action Filter Location

If you need the same action filter to all of your controller action methods,  put it in the controller rather than each action method. If you want to apply the same action filter to all of your controller create a base controller and inherit from that base controller, for example the story controller should be only used when user is logged in and we need to pass the current user name in its methods, also the StoryController should compress the data when returning:

[Authorize, UserNameFilter]
public class StoryController : BaseController
{
}

[CompressFilter]
public class BaseController : Controller
{
}

But if the inheritance hierarchy is going more than 2 /3 level deep, find another way to apply the filters. The latest Oxite code has some excellent technique applying filters dynamically which I highly recommend to check.

10. Use UpdateModel Carefully

I do not want to repeat what Justin Etheredge has mentioned in his post, be careful and do not fall into that trap.

11.Controller will not contain any Domain logic

Controller should be only responsible for:

  • Validating Input
  • Calling Model to prepare the view
  • Return the view or redirect to another action

If you are doing any other thing you are doing it in a wrong place, it is rather the Model responsibility which you are doing in Controller. If you follow this rule your action method will not be more than 20 – 25 lines of code. Ian Cooper has an excellent post Skinny Controller Fat Model, do read it.

12. Avoid ViewData, use ViewData.Model

Depending upon the dictionary key will not only make your code hard to refactor, also you will have to write the casting code in your view. It is completely okay even you end up per class for each action method of your controller.  If you think, creating these kind of classes is a tedious job, you can use the ViewDataExtensions of the MVCContrib project, it has some nice extension for returning strongly typed objects, though you still have to depend upon the string key if you have more than one data type in ViewData Dictionary.

13. Use PRG Pattern for Data Modification

Tim Barcz, Matt Hawley, Stephen Walther and even The Gu has blogged this over here, here, here and here. One of the issue with this pattern is when a validation fails or any  exception occurs you have to copy the ModelState into TempData. If you are doing it manually, please stop it, you can do this automatically with Action Filters, like the following:

[AcceptVerbs(HttpVerbs.Get), OutputCache(CacheProfile = "Dashboard"), StoryListFilter, ImportModelStateFromTempData]
public ActionResult Dashboard(string userName, StoryListTab tab, OrderBy orderBy, int? page)
{
	//Other Codes
    return View();
}

[AcceptVerbs(HttpVerbs.Post), ExportModelStateToTempData]
public ActionResult Submit(string userName, string url)
{
    if (ValidateSubmit(url))
    {
        try
        {
            _storyService.Submit(userName, url);
        }
        catch (Exception e)
        {
            ModelState.AddModelError(ModelStateException, e);
        }
    }

    return Redirect(Url.Dashboard());
}

And the Action Filers

public abstract class ModelStateTempDataTransfer : ActionFilterAttribute
{
    protected static readonly string Key = typeof(ModelStateTempDataTransfer).FullName;
}

public class ExportModelStateToTempData : ModelStateTempDataTransfer
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        //Only export when ModelState is not valid
        if (!filterContext.Controller.ViewData.ModelState.IsValid)
        {
            //Export if we are redirecting
            if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult))
            {
                filterContext.Controller.TempData[Key] = filterContext.Controller.ViewData.ModelState;
            }
        }

        base.OnActionExecuted(filterContext);
    }
}

public class ImportModelStateFromTempData : ModelStateTempDataTransfer
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        ModelStateDictionary modelState = filterContext.Controller.TempData[Key] as ModelStateDictionary;

        if (modelState != null)
        {
            //Only Import if we are viewing
            if (filterContext.Result is ViewResult)
            {
                filterContext.Controller.ViewData.ModelState.Merge(modelState);
            }
            else
            {
                //Otherwise remove it.
                filterContext.Controller.TempData.Remove(Key);
            }
        }

        base.OnActionExecuted(filterContext);
    }
}

The MVCContrib project also has this feature but they are doing it in a single class which I do not like, I would like to have more control which method to export and which to import.

14. Create Layer Super Type for your ViewModel and Use Action Filter to populate common parts.

Create a layer super type for your view model classes and use action filter to populate common things into it . For example the tiny little application that I am developing I need to know the User Name and whether the User is authenticated.

public class ViewModel
{
    public bool IsUserAuthenticated
    {
        get;
        set;
    }

    public string UserName
    {
        get;
        set;
    }
}

and the action filter:

public class ViewModelUserFilter : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        ViewModel model;

        if (filterContext.Controller.ViewData.Model == null)
        {
            model = new ViewModel();
            filterContext.Controller.ViewData.Model = model;
        }
        else
        {
            model = filterContext.Controller.ViewData.Model as ViewModel;
        }

        if (model != null)
        {
            model.IsUserAuthenticated = filterContext.HttpContext.User.Identity.IsAuthenticated;

            if (model.IsUserAuthenticated)
            {
                model.UserName = filterContext.HttpContext.User.Identity.Name;
            }
        }

        base.OnActionExecuted(filterContext);
    }
}

As you can see that it not replacing the model, if it is previously set in the controller, rather it populates the common part if it finds it compatible. Other benefit is, the views that only depends the layer super type you can simply return View() instead of creating the model.

That's it for today, I will post rest of the items tomorrow.

Stay tuned.

Shout it
More Posts