Logo

Maarten Balliauw {blog}

ASP.NET, ASP.NET MVC, Azure, PHP, OpenXML, VSTS, ...

About the author

Maarten Balliauw is currently employed as .NET Technical Consultant at RealDolmen. His interests are mainly web applications developed in ASP.NET (C#) or PHP and the Windows Azure cloud platform.
More about me More about me
Send mail E-mail me


ASP.NET MVC Quickly Subscribe to my RSS feed Follow me on Twitter! View Maarten Balliauw's profile on LinkedIn
View Maarten Balliauw's MVP profile

Search

Latest Twitter

    Follow me on Twitter...

    My projects

    Disclaimer

    The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

    © Copyright Maarten Balliauw 2010

    MvcSiteMapProvider 2.1.0 released!

    MvcSiteMapProvider The release for MvcSiteMapProvider 2.1.0 has just been posted on CodePlex. MvcSiteMapProvider is, as the name implies, an ASP.NET MVC SiteMapProvider implementation for the ASP.NET MVC framework. Targeted at ASP.NET MVC 2, it provides sitemap XML functionality and interoperability with the classic ASP.NET sitemap controls, like the SiteMapPath control for rendering breadcrumbs and the Menu control.

    Next to a brand new logo, the component has been patched up with several bugfixes, the visibility attribute is back (in a slightly cooler reincarnation) and a number of new extension points have been introduced. Let’s give you a quick overview…

    kick it on DotNetKicks.com

    Extension points

    MvcSiteMapProvider is built wih extensibility in mind. All extension point contracts are defined in the MvcSiteMapProvider.Extensibility namespace. The sample application on the downloads page contains several custom implementations of these extension points.

    Global extension points (valid for the entire provider and all nodes)

    These extension points can be defined when Registering the provider.

      Node key generator

    Keys for sitemap nodes are usually automatically generated by the MvcSiteMapProvider core. If, for reasons of accessing sitemap nodes from code, the generated keys should follow other naming rules, a custom MvcSiteMapProvider.Extensibility.INodeKeyGenerator implementation can be written.

      Controller type resolver

    In order to resolve a controller type and action method related to a specific sitemap node, a MvcSiteMapProvider.Extensibility.IControllerTypeResolver is used. This should normally not be extended, however if you want to make use of other systems for resolving controller types and action methods, this is the logical extension point.

      Action method parameter resolver

    Action method parameters are resolved by using ASP.NET MVC's ActionDescriptor class. If you want to use a custom system for this, a MvcSiteMapProvider.Extensibility.IActionMethodParameterResolver implementation can be specified.

      ACL module

    To determine whether a sitemap node is accessible to a specific user, a MvcSiteMapProvider.Extensibility.IAclModule implementation is used. MvcSiteMapProvider uses two of these modules by default: access is granted or denied by checking for [Authorize] attributes on action methods, followed by the roles attribute that can be specified in the sitemap XML.

      URL resolver

    URLs are generated by leveraging a MvcSiteMapProvider.Extensibility.ISiteMapNodeUrlResolver implementation. If, for example, you want all URLs generated by MvcSiteMapProvider to be in lowercase text, a custom implementation can be created.

      Visibility provider

    In some situations, nodes should be visible in the breadcrumb trail but not in a complete sitemap. This can be solved using the MvcSiteMapProvider.Extensibility.ISiteMapNodeVisibilityProvider extension point that can be specified globally for every node in the sitemap or granularly on a specific sitemap node. A sample is available on the Advanced node visibility page.

    Node-specific extenion points (valid for a single node)

    These extension points can be defined when Creating a first sitemap.

      Dynamic node provider

    In many web applications, sitemap nodes are directly related to content in a persistent store like a database.For example, in an e-commerce application, a list of product details pages in the sitemap maps directly to the list of products in the database. Using dynamic sitemaps, a small class implementing MvcSiteMapProvider.Extensibility.IDynamicNodeProvider or extending MvcSiteMapProvider.Extensibility.DynamicNodeProviderBase can be provided to the MvcSiteMapProvider offering a list of dynamic nodes that should be incldued in the sitemap. This ensures the product pages do not have to be specified by hand in the sitemap XML.

    A sample can be found on the Dynamic sitemaps page.

      URL resolver

    URLs are generated by leveraging a MvcSiteMapProvider.Extensibility.ISiteMapNodeUrlResolver implementation. If, for example, you want the URL for a sitemap node generated by MvcSiteMapProvider to be in lowercase text, a custom implementation can be created.

      Visibility provider

    In some situations, nodes should be visible in the breadcrumb trail but not in a complete sitemap. This can be solved using the MvcSiteMapProvider.Extensibility.ISiteMapNodeVisibilityProvider extension point that can be specified globally for every node in the sitemap or granularly on a specific sitemap node. A sample is available on the Advanced node visibility page.

    Conclusion

    Only one conclusion: grab the latest bits and start playing with them! And feel free to bug me with feature requests and issues found.

    Also, follow me on Twitter for updates on this project.


    Categories: ASP.NET | C# | General | MVC | Projects

    ASP.NET MVC 3 and MEF sitting in a tree...

    As I stated in a previous blog post: ASP.NET MVC 3 preview 1 has been released! I talked about some of the new features and promised to do a blog post in the dependency injection part. In this post, I'll show you how to use that together with MEF.

    Download my sample code: Mvc3WithMEF.zip (256.21 kb)

    kick it on DotNetKicks.com

    Dependency injection in ASP.NET MVC 3

    First of all, there’s 4 new hooks for injecting dependencies:

    • When creating controller factories
    • When creating controllers
    • When creating views (might be interesting!)
    • When using action filters

    In ASP.NET MVC 2, only one of these hooks was used for dependency injection: a controller factory was implemented, using a dependency injection framework under the covers. I did this once, creating a controller factory that wired up MEF and made sure everything in the application was composed through a MEF container. That is, everything that is a controller or part thereof. No easy options for DI-ing things like action filters or views…

    ASP.NET MVC 3 shuffled the cards a bit. ASP.NET MVC 3 now contains and uses the Common Service Locator’s IServiceLocator interface, which is used for resolving services required by the ASP.NET MVC framework. The IServiceLocator implementation should be registered in Global.asax using just one line of code:

    MvcServiceLocator.SetCurrent(new SomeServiceLocator());

    This is, since ASP.NET MVC 3 preview 1, the only thing required to make DI work. In controllers, in action filters and in views. Cool, eh?

    Leveraging MEF with ASP.NET MVC 3

    First of all: a disclaimer. I already did posts on MEF and ASP.NET MVC before, and in all these posts, I required you to explicitly export your controller types for composition. In this example, again, I will require that, just for keeping code a bit easier to understand. Do note that are some variants of a convention based registration model available.

    As stated before, the only thing to build here is a MefServiceLocator that is suited for web (which means: an application-wide catalog and a per-request container). I’ll still have to create my own controller factory as well, because otherwise I would not be able to dynamically compose my controllers. Here goes…

    Implementing ServiceLocatorControllerFactory

    Starting in reverse, but this thing is the simple part :-)

    [Export(typeof(IControllerFactory))]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public class ServiceLocatorControllerFactory
        : DefaultControllerFactory
    {
        private IMvcServiceLocator serviceLocator;

        [ImportingConstructor]
        public ServiceLocatorControllerFactory(IMvcServiceLocator serviceLocator)
        {
            this.serviceLocator = serviceLocator;
        }

        public override IController CreateController(RequestContext requestContext, string controllerName)
        {
            var controllerType = GetControllerType(requestContext, controllerName);
            if (controllerType != null)
            {
                return this.serviceLocator.GetInstance(controllerType) as IController;
            }

            return base.CreateController(requestContext, controllerName);
        }

        public override void ReleaseController(IController controller)
        {
            this.serviceLocator.Release(controller);
        }
    }

    Did you see that? A simple, MEF enabled controller factory that uses an IMvcServiceLocator. This thing can be used with other service locators as well.

    Implementing MefServiceLocator

    Like I said, this is the most important part, allowing us to use MEF for resolving almost any component in the ASP.NET MVC pipeline. Here’s my take on that:

    [Export(typeof(IMvcServiceLocator))]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public class MefServiceLocator
        : IMvcServiceLocator
    {
        const string HttpContextKey = "__MefServiceLocator_Container";

        private ComposablePartCatalog catalog;
        private IMvcServiceLocator defaultLocator;

        [ImportingConstructor]
        public MefServiceLocator()
        {
            // Get the catalog from the MvcServiceLocator.
            // This is a bit dirty, but currently
            // the only way to ensure one application-wide catalog
            // and a per-request container.
            MefServiceLocator mefServiceLocator = MvcServiceLocator.Current as MefServiceLocator;
            if (mefServiceLocator != null)
            {
                this.catalog = mefServiceLocator.catalog;
            }

            // And the fallback locator...
            this.defaultLocator = MvcServiceLocator.Default;
        }

        public MefServiceLocator(ComposablePartCatalog catalog)
            : this(catalog, MvcServiceLocator.Default)
        {
        }

        public MefServiceLocator(ComposablePartCatalog catalog, IMvcServiceLocator defaultLocator)
        {
            this.catalog = catalog;
            this.defaultLocator = defaultLocator;
        }

        protected CompositionContainer Container
        {
            get
            {
                if (!HttpContext.Current.Items.Contains(HttpContextKey))
                {
                    HttpContext.Current.Items.Add(HttpContextKey, new CompositionContainer(catalog));
                }

                return (CompositionContainer)HttpContext.Current.Items[HttpContextKey];
            }
        }

        private object Resolve(Type serviceType, string key = null)
        {
            var exports = this.Container.GetExports(serviceType, null, null);
            if (exports.Any())
            {
                return exports.First().Value;
            }

            var instance = defaultLocator.GetInstance(serviceType, key);
            if (instance != null)
            {
                return instance;
            }

            throw new ActivationException(string.Format("Could not resolve service type {0}.", serviceType.FullName));
        }

        private IEnumerable<object> ResolveAll(Type serviceType)
        {
            var exports = this.Container.GetExports(serviceType, null, null);
            if (exports.Any())
            {
                return exports.Select(e => e.Value).AsEnumerable();
            }

            var instances = defaultLocator.GetAllInstances(serviceType);
            if (instances != null)
            {
                return instances;
            }

            throw new ActivationException(string.Format("Could not resolve service type {0}.", serviceType.FullName));
        }

        #region IMvcServiceLocator Members

        public void Release(object instance)
        {
            var export = instance as Lazy<object>;
            if (export != null)
            {
                this.Container.ReleaseExport(export);
            }

            defaultLocator.Release(export);
        }

        #endregion

        #region IServiceLocator Members

        public IEnumerable<object> GetAllInstances(Type serviceType)
        {
            return ResolveAll(serviceType);
        }

        public IEnumerable<TService> GetAllInstances<TService>()
        {
            var instances = ResolveAll(typeof(TService));
            foreach (TService instance in instances)
            {
                yield return (TService)instance;
            }
        }

        public TService GetInstance<TService>(string key)
        {
            return (TService)Resolve(typeof(TService), key);
        }

        public object GetInstance(Type serviceType)
        {
            return Resolve(serviceType);
        }

        public object GetInstance(Type serviceType, string key)
        {
            return Resolve(serviceType, key);
        }

        public TService GetInstance<TService>()
        {
            return (TService)Resolve(typeof(TService));
        }

        #endregion

        #region IServiceProvider Members

        public object GetService(Type serviceType)
        {
            return Resolve(serviceType);
        }

        #endregion
    }

    HOLY SCHMOLEY! That is a lot of code. Let’s break it down…

    First of all, I have 3 constructors. 2 for convenience, one for MEF. Since the MefServiceLocator will be instantiated in Global.asax and I only want one instance of it to live in the application, I have to do a dirty trick: whenever MEF wants to create a new MefServiceLocator for some reason (should in theory only happen once per request, but I want this thing to live application-wide), I’m giving it indeed a new instance which at least shares the part catalog with the one I originally created. Don’t shoot me for doing this…

    Next, you will also notice that I’m using a “fallback” locator, which in theory will be the instance stored in MvcServiceLocator.Default, which is ASP.NET MVC 3’s default MvcServiceLocator. I’m doing this for a reason though… read my disclaimer again: I stated that everything should be decorated with the [Export] attribute when I’m relying on MEF. Now since the services exposed by ASP.NET MVC 3, like the IFilterProvider, are not decorated with this attribute, MEF will not be able to find those. When I find myself in that situation, the MefServiceLocator is simply asking the default service locator for it. Not a beauty, but it works and makes my life easy.

    Wiring things

    To wire this thing, all it takes is adding 3 lines of code to my Global.asax. For clarity, I’m giving you my entire Global.asax Application_Start method:

    protected void Application_Start()
    {
        // Register areas

        AreaRegistration.RegisterAllAreas();

        // Register filters and routes

        RegisterGlobalFilters(GlobalFilters.Filters);
        RegisterRoutes(RouteTable.Routes);

        // Register MEF catalogs

        var catalog = new DirectoryCatalog(
            Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin"));
        MvcServiceLocator.SetCurrent(new MefServiceLocator(catalog, MvcServiceLocator.Default));
    }

    Can you spot the 3 lines of code? This is really all it takes to make the complete application use MEF where appropriate. (Ok, that is a bit of a lie since you would still have to implement a very small IFilterProvider if you want MEF in your action filters, but still.)

    Hooks

    The cool thing is: a lot of things are now requested in the service locator we just created. When browsing to my site index, here’s all the things that are requested:

    • Resolve called for serviceType: System.Web.Mvc.IControllerFactory
    • Resolve called for serviceType: Mvc3WithMEF.Controllers.HomeController
    • Resolve called for serviceType: System.Web.Mvc.IFilterProvider
    • Resolve called for serviceType: System.Web.Mvc.IFilterProvider
    • Resolve called for serviceType: System.Web.Mvc.IFilterProvider
    • Resolve called for serviceType: System.Web.Mvc.IFilterProvider
    • Resolve called for serviceType: System.Web.Mvc.IViewEngine
    • Resolve called for serviceType: System.Web.Mvc.IViewEngine
    • Resolve called for serviceType: ASP.Index_cshtml
    • Resolve called for serviceType: System.Web.Mvc.IViewEngine
    • Resolve called for serviceType: System.Web.Mvc.IViewEngine
    • Resolve called for serviceType: ASP._LogOnPartial_cshtml

    Which means that you can now even inject stuff into views or compose their parts dynamically.

    Conclusion

    I have a strong sense of a power in here… ASP.NET MVC 3 will support DI natively if you want to use it, and I’ll be one of the users happily making use of it. There’s use cases for injecting/composing something in all of the above components, and ASP.NET MVC 3 made this just simpler and more straightforward.

    Here’s my sample code with some more examples in it: Mvc3WithMEF.zip (256.21 kb)


    ASP.NET MVC 3 preview 1 is out! Quick review...

    I just noticed a very interesting download: ASP.NET MVC 3 preview 1. Yes, you are reading this correctly, the first bits for v3.0 are there! Let’s have a quick look around and see what’s new...

    kick it on DotNetKicks.com

    Razor Syntax View Engine

    ScottGu blogged about Razor before. ASP.NET MVC has always supported the concept of “view engines”, pluggable modules that allow you to have your views rendered by different engines like for example the WebForms engine, Spark, NHAML, …

    Razor is a new view engine, focused on less code clutter and shorter code-expressions for generating HTML dynamically. As an example, have a look at the following view:

    <ul>
      <% foreach (var c in Model.Customers) { %>
        <li><%:c.DisplayName%></li>
      <% } %>
    </ul>

    In Razor syntax, this becomes:

    <ul>
      @foreach (var c in Model.Customers) {
        <li>@c.DisplayName</li>
      }
    </ul>

    Perhaps not the best example to show the strengths of this new engine, but do bear in mind that Razor simply puts code literally in your HTML, making it develop faster (did I mention perfect IntelliSense support in the Razor view editor?).

    Also, there’s a nice addition to the “Add View” dialog in Visual Studio: you can now choose for which view engine you want to generate a view.

    Razor view engine - Add view dialog

    ViewData dictionary “dynamic” support

    .NET 4 introduced the “dynamic” keyword, which is abstracting away a lot of reflection code you’d normally have to write yourself. The fun thing is, that the MVC guys abused this thing in a very nice way.

    Controller action method, ASP.NET MVC 2:

    public ActionResult Index()
    {
        ViewModel["Message"] = "Welcome to ASP.NET MVC!";

        return View();
    }

    Controller action method, ASP.NET MVC 3:

    public ActionResult Index()
    {
        ViewModel.Message = "Welcome to ASP.NET MVC!";

        return View();
    }

    “Isn’t that the same?” – Yes, in essence it is exactly the same concept at work. However, by using the dynamic keyword, there’s less “string pollution” in my code. Do note that in most situations, you would create a custom “View Model” and pass that to the view instead of using this ugly dictionary or dynamic object. Nevertheless: I do prefer reading code that uses less dictionaries.

    So far for the controller side, there’s also the view side. Have a look at this:

    @inherits System.Web.Mvc.WebViewPage

    @{
        View.Title = "Home Page";
        LayoutPage = "~/Views/Shared/_Layout.cshtml";
    }

    <h2>@View.Message</h2>
    <p>
        To learn more about ASP.NET MVC visit <a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.
    </p>

    A lot of new stuff there, right? First of all the Razor syntax, but secondly… There’s just something like @View.Message in this view, and this is rendering something from the ViewData dictionary/dynamic object. Again: very readable and understandable.

    It’s a small change on the surface, but I do like it. In my opinion, it’s more readable than using the ViewData dictionary when you are not using a custom view model.

    Global action filters

    Imagine you have a team of developers, all writing controllers. Imagine that they have to add the [HandleError] action filter to every controller, and they sometimes tend to forget… That’s where global action filters come to the rescue! Add this line to Global.asax:

    GlobalFilters.Filters.Add(new HandleErrorAttribute());

    This will automatically register that action filter attribute for every controller and action method.

    Fun fact: I blogged about exactly this feature about a year ago: Application-wide action filters in ASP.NET MVC. Want it in ASP.NET MVC 2? Go and get it :-)

    Cool, eh? Well… I do have mixed feelings about this… I can imagine there are situations where you want to do this more selectively. Here’s my call-out to the ASP.NET MVC team:

    • At least allow to specify action filters on a per-area level, so I can have my “Administration” area have other filters than my default area.
    • In an ideal world, I’d prefer something where I can specify global action filters even more granularly. This can be done using some customizing, but it would be useful to have it out-of-the-box. Here's an example of the ideal world:

    GlobalFilters.Filters.AddTo<HomeController>(new HandleErrorAttribute()) 
                         .AddTo<AccountController>(c => c.ChangePassword(), new AuthorizeAttribute());

    Dependency injection support

    I’m going to be short on this one: there’s 4 new hooks for injecting dependencies:

    • When creating controller factories
    • When creating controllers
    • When creating views (might be interesting!)
    • When using action filters

    More on that in my next post on ASP.NET MVC 3, as I think it deserves a full post rather than jut some smaller paragraphs.

    Update: Here's that next post on ASP.NET MVC 3 and dependency injection / MEF

    New action result types

    Not very ground-skaing news, but there's a new set of ActionResult variants available which will make your life easier:

    • HttpNotFoundResult - Speaks for itself, right :-)
    • HttpStatusCodeResult - How about new HttpStatusCodeResult(418, "I'm a teapot");
    • RedirectPermanent, RedirectToRoutePermanent, RedirectToActionPermanent - Writes a permanent redirect header

    Conclusion

    I only touched the tip of the iceberg. There’s more to ASP.NET MVC 3 preview 1, described in the release notes.

    In short, I’m very positive about the amount of progress being made in this framework! Very pleased with the DI portion of it, on which I’ll do a blog post later.

    Update: Here's that next post on ASP.NET MVC 3 and dependency injection / MEF


    Categories: ASP.NET | C# | General | MVC

    ASP.NET MVC - MvcSiteMapProvider 2.0 is out!

    I’m very proud to announce the release of the ASP.NET MVC MvcSiteMapProvider 2.0! I’m also proud that the name of this product now exceeds the average length of Microsoft product names. In this blog post, I will give you a feel of what you can (and can not) do with this ASP.NET-specific SiteMapProvider.

    As a warning: if you’ve used version 1 of this library, you will notice that I have not thought of backwards compatibility. A lot of principles have also changed. For good reasons though: this release is a rewrite of the original version with improved features, extensibility and stability.

    The example code is all based on the excellent ASP.NET MVC Music Store sample application by Jon Galloway.

    Getting the bits

    As always, the bits are available on CodePlex: MvcSiteMapProvider 2.0.0
    If you prefer to have the full source code, download the example application or check the source code tab on CodePlex.

    Introduction

    MvcSiteMapProvider is, as the name implies, an ASP.NET MVC SiteMapProvider implementation for the ASP.NET MVC framework. Targeted at ASP.NET MVC 2, it provides sitemap XML functionality and interoperability with the classic ASP.NET sitemap controls, like the SiteMapPath control for rendering breadcrumbs and the Menu control.

    Based on areas, controller and action method names rather than hardcoded URL references, sitemap nodes are completely dynamic based on the routing engine used in an application. The dynamic character of ASP.NET MVC is followed in the MvcSiteMapProvider: there are numerous extensibility points that allow you to extend the basic functionality offered.

    Registering the provider

    After downloading the MvcSiteMapProvider, you will have to add a reference to the assembly in your project. Also, you will have to register the provider in your Web.config file. Add the following code somewhere in the <system.web> section:

    <siteMap defaultProvider="MvcSiteMapProvider" enabled="true">
      <providers>
        <clear />
        <add name="MvcSiteMapProvider"
             type="MvcSiteMapProvider.DefaultSiteMapProvider, MvcSiteMapProvider"
             siteMapFile="~/Mvc.Sitemap"
             securityTrimmingEnabled="true"
             enableLocalization="true"
             scanAssembliesForSiteMapNodes="true"
             skipAssemblyScanOn=""
             attributesToIgnore="bling"
             nodeKeyGenerator="MvcSiteMapProvider.DefaultNodeKeyGenerator, MvcSiteMapProvider"
             controllerTypeResolver="MvcSiteMapProvider.DefaultControllerTypeResolver, MvcSiteMapProvider"
             actionMethodParameterResolver="MvcSiteMapProvider.DefaultActionMethodParameterResolver, MvcSiteMapProvider"
             aclModule="MvcSiteMapProvider.DefaultAclModule, MvcSiteMapProvider"
             />
      </providers>
    </siteMap>

    The following configuration directives can be specified:

    Directive Required? Default Description
    siteMapFile No ~/Web.sitemap The sitemap XML file to use.
    securityTrimmingEnabled No false Use security trimming? When enabled, nodes that the user can not access will not be displayed in any sitemap control.
    enableLocalization No false Enables localization of sitemap nodes.
    scanAssembliesForSiteMapNodes No false Scan assemblies for sitemap nodes defined in code?
    skipAssemblyScanOn No (empty) Comma-separated list of assemblies that should be skipped when scanAssembliesForSiteMapNodes is enabled.
    attributesToIgnore No (empty) Comma-separated list of attributes defined on a sitemap node that should be ignored by the MvcSiteMapProvider.
    nodeKeyGenerator No MvcSiteMapProvider.DefaultNodeKeyGenerator, MvcSiteMapProvider Class that will be used to generate sitemap node keys.
    controllerTypeResolver No MvcSiteMapProvider.DefaultControllerTypeResolver, MvcSiteMapProvider Class that will be used to resolve the controller for a specific sitemap node.
    actionMethodParameterResolver No MvcSiteMapProvider.DefaultActionMethodParameterResolver, MvcSiteMapProvider Class that will be used to determine the list of parameters on a sitemap node.
    aclModule No MvcSiteMapProvider.DefaultAclModule, MvcSiteMapProvider Class that will be used to verify security and access rules for sitemap nodes.

     

    Creating a first sitemap

    The following is a simple sitemap XML file that can be used with the MvcSiteMapProvider:

    <?xml version="1.0" encoding="utf-8" ?>
    <mvcSiteMap xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-2.0" enableLocalization="true">
      <mvcSiteMapNode title="Home" controller="Home" action="Index" changeFrequency="Always" updatePriority="Normal">
        <mvcSiteMapNode title="Browse Store" controller="Store" action="Index" />
        <mvcSiteMapNode title="Checkout" controller="Checkout" />
      </mvcSiteMapNode>
    </mvcSiteMap>

    The following attributes can be given on an XML node element:

    Attribute Required? Default Description
    title Yes (empty) The title of the node.
    description No (empty) Description of the node.
    area No (empty) The MVC area for the sitemap node. If not specified, it will be inherited from a node higher in the hierarchy.
    controller Yes (empty) The MVC controller for the sitemap node. If not specified, it will be inherited from a node higher in the hierarchy.
    action Yes (empty) The MVC action method for the sitemap node. If not specified, it will be inherited from a node higher in the hierarchy.
    key No (autogenerated) The unique identifier for the node.
    url No (autogenerated based on routes) The URL represented by the node.
    roles No (empty) Comma-separated list of roles allowed to access the node and its child nodes.
    resourceKey No (empty) Optional resource key.
    clickable No True Is the node clickable or just a grouping node?
    targetFrame No (empty) Optional target frame for the node link.
    imageUrl No (empty) Optional image to be shown by supported HtmlHelpers.
    lastModifiedDate No (empty) Last modified date for the node.
    changeFrequency No Undefined Change frequency for the node.
    updatePriority No Undefined Update priority for the node.
    dynamicNodeProvider No (empty) A class name implementing MvcSiteMapProvider.Extensibility.IDynamicNodeProvider and providing dynamic nodes for the site map.

     

    Defining sitemap nodes in code

    In some cases, defining a sitemap node in code is more convenient than defining it in a sitemap xml file. To do this, decorate an action method with the MvcSiteMapNodeAttribute attribute. For example:

    // GET: /Checkout/Complete
    [MvcSiteMapNodeAttribute(Title = "Checkout complete", ParentKey = "Checkout")]
    public ActionResult Complete(int id)
    {
        // ...
    }

    Note that the ParentKey property should be specified to ensure the MvcSiteMapProvider  can determine the hierarchy for all nodes.

    Dynamic sitemaps

    In many web applications, sitemap nodes are directly related to content in a persistent store like a database.For example, in an e-commerce application, a list of product details pages in the sitemap maps directly to the list of products in the database. Using dynamic sitemaps, a small class can be provided to the MvcSiteMapProvider offering a list of dynamic nodes that should be incldued in the sitemap. This ensures the product pages do not have to be specified by hand in the sitemap XML.

    First of all, a sitemap node should be defined in XML. This node will serve as a template and tell the MvcSiteMapProvider infrastructure to use a custom dynamic node procider:

    <mvcSiteMapNode title="Details" action="Details" dynamicNodeProvider="MvcMusicStore.Code.StoreDetailsDynamicNodeProvider, MvcMusicStore" />

    Next, a class implementing MvcSiteMapProvider.Extensibility.IDynamicNodeProvider or extending MvcSiteMapProvider.Extensibility.DynamicNodeProviderBase should be created in your application code. Here’s an example:

    public class StoreDetailsDynamicNodeProvider
        : DynamicNodeProviderBase
    {
        MusicStoreEntities storeDB = new MusicStoreEntities();

        public override IEnumerable<DynamicNode> GetDynamicNodeCollection()
        {
            // Build value
            var returnValue = new List<DynamicNode>();

            // Create a node for each album
            foreach (var album in storeDB.Albums.Include("Genre"))
            {
                DynamicNode node = new DynamicNode();
                node.Title = album.Title;
                node.ParentKey = "Genre_" + album.Genre.Name;
                node.RouteValues.Add("id", album.AlbumId);

                returnValue.Add(node);
            }

            // Return
            return returnValue;
        }
    }

    Cache dependency

    When providing dynamic sitemap nodes to the MvcSiteMapProvider, chances are that the hierarchy of nodes will become stale, for example when adding products in an e-commerce website. This can be solved by specifying a CacheDescriptor on your MvcSiteMapProvider.Extensibility.IDynamicNodeProvider implementation:

    public class StoreDetailsDynamicNodeProvider
        : DynamicNodeProviderBase
    {
        MusicStoreEntities storeDB = new MusicStoreEntities();

        public override IEnumerable<DynamicNode> GetDynamicNodeCollection()
        {
            // ...
        }

        public override CacheDescription GetCacheDescription()
        {
            return new CacheDescription("StoreDetailsDynamicNodeProvider")
            {
                SlidingExpiration = TimeSpan.FromMinutes(1)
            };
        }
    }

    HtmlHelper functions

    The MvcSiteMapProvider provides different HtmlHelper extension methods which you can use to generate SiteMap-specific HTML code on your ASP.NET MVC views. Here's a list of available HtmlHelper extension methods.

    • Html.MvcSiteMap().Menu() - Can be used to generate a menu
    • Html.MvcSiteMap().SubMenu() - Can be used to generate a submenu
    • Html.MvcSiteMap().SiteMap() - Can be used to generate a list of all pages in your sitemap
    • Html.MvcSiteMap().SiteMapPath() - Can be used to generate a so-called "breadcrumb trail"
    • Html.MvcSiteMap().SiteMapTitle() - Can be used to render the current SiteMap node's title

    Note that these should be registered in Web.config, i.e. under <pages> add the following:

    <pages>
        <controls>
            <! -- ... -->
        </controls>
        <namespaces>
            <! -- ... -->
            <add namespace="MvcSiteMapProvider.Web.Html" />
        </namespaces>
    </pages>

    Action Filter Attributes

    SiteMapTitle

    In some situations, you may want to dynamically change the SiteMap.CurrentNode.Title in an action method. This can be done manually by setting SiteMap.CurrentNode.Title, or by adding the SiteMapTitle action filter attribute.

    Imagine you are building a blog and want to use the Blog's Headline property as the site map node title. You can use the following snippet:

    [SiteMapTitle("Headline")]
    public ViewResult Show(int blogId) {
       var blog = _repository.Find(blogIdId);
       return blog;
    }

    You can also use a non-strong typed ViewData value as the site map node title:

    [SiteMapTitle("SomeKey")]
    public ViewResult Show(int blogId) {
       ViewData["SomeKey"] = "This will be the title";

       var blog = _repository.Find(blogIdId);
       return blog;
    }

    Exporting the sitemap for search engine indexing

    When building a website, chances are that you want to provide an XML sitemap used for search engine indexing. The XmlSiteMapResult class creates an XML sitemap that can be submitted to Google, Yahoo and other search engines to help them crawl your website better. The usage is very straightforward:

    public class HomeController
    {
        public ActionResult SiteMapXml()
        {
            return new XmlSiteMapResult();
        }
    }

    Optionally, a starting node can also be specified in the constructor of theXmlSiteMapResult .

    Conclusion

    Get it while it’s hot! MvcSiteMapProvider 2.0.0 is available on CodePlex.

    kick it on DotNetKicks.com


    Categories: ASP.NET | C# | General | MVC | Projects | Software | XML

    Sharpy - an ASP.NET MVC view engine based on Smarty

    Sharpy - ASP.NET MVC View Engine based on SmartyAre you also one of those ASP.NET MVC developers who prefer a different view engine than the default Webforms view engine available? You tried Spark, NHaml, …? If you are familiar with the PHP world as well, chances are you know Smarty, a great engine for creating views that can easily be read and understood by both developers and designers. And here’s the good news: Sharpy provides the same syntax for ASP.NET MVC!

    If you want more details on Sharpy, visit Jaco Pretorius’ blog:

    kick it on DotNetKicks.com

    A simple example…

    Here’s a simple example:

    {master file='~/Views/Shared/Master.sharpy' title='Hello World sample'}

    <h1>Blog entries</h1>

    {foreach from=$Model item="entry"}
        <tr>
            <td>{$entry.Name|escape}</td>       
            <td align="right">{$entry.Date|date_format:"dd/MMM/yyyy"}</td>       
        </tr>
        <tr>
            <td colspan="2" bgcolor="#dedede">{$entry.Body|escape}</td>
        </tr>
    {foreachelse}
        <tr>
            <td colspan="2">No records</td>
        </tr>
    {/foreach}

    The above example first specifies the master page to use. Next, a foreach-loop is executed for each blog post (aliased “entry”) in the $Model. Printing the entry’s body is done using {$entry.Body|escape}. Note the pipe “|” and the word escape after it. These are variable modifiers that can be used to escape content, format dates, …

    Extensibility

    Sharpy is all about extensibility: every function in a view is actually a plugin of a specific type (there are four types, IInlineFunction, IBlockFunction, IExpressionFunction and IVariableModifier). These plugins are all exposed through MEF. This means that Sharpy will always use any of your custom functions that are exposed through MEF. For example, here’s a custom function named “content”:

    [Export(typeof(IInlineFunction))]
    public class Content : IInlineFunction
    {
        public string Name
        {
            get { return "content"; }
        }

        public string Evaluate(IDictionary<string, object> attributes, IFunctionEvaluator evaluator)
        {
            // Fetch attributes
            var file = attributes.GetRequiredAttribute<string>("file");

            // Write output
            return evaluator.EvaluateUrl(file);
        }
    }

    Here’s how to use it:

    {content file='~/Content/SomeFile.txt'}

    Sharpy uses MEF to allow developers to implement their own functions and modifiers.  All the built-in functions are also built using this exact same framework – the same functionality is available to both internal and external functions.

    Extensibility is one of the strongest features in Sharpy.  This should allow us to leverage any functionality available in a normal ASP.NET view while maintaining simple views and straightforward markup.

    Give it a spin!

    Do give Sharpy a spin, you will learn to love it.


    Categories: ASP.NET | C# | General | MVC

    Translating routes (ASP.NET MVC and Webforms)

    Localized route in ASP.NET MVC - Translated route in ASP.NET MVC For one of the first blog posts of the new year, I thought about doing something cool. And being someone working with ASP.NET MVC, I thought about a cool thing related to that: let’s do something with routes! Since System.Web.Routing is not limited to ASP.NET MVC, this post will also play nice with ASP.NET Webforms. But what’s the cool thing? How about… translating route values?

    Allow me to explain… I’m tired of seeing URLs like http://www.example.com/en/products and http://www.example.com/nl/products. Or something similar, with query parameters like “?culture=en-US”. Or even worse stuff. Wouldn’t it be nice to have http://www.example.com/products mapping to the English version of the site and http://www.exaple.com/producten mapping to the Dutch version? Better to remember when giving away a link to someone, better for SEO as well.

    Of course, we do want both URLs above to map to the ProductsController in our ASP.NET MVC application. We do not want to duplicate logic because of a language change, right? And what’s more: it’s not fun if this would mean having to switch from <%=Html.ActionLink(…)%> to something else because of this.

    Let’s see if we can leverage the routing engine in System.Web.Routing for this…

    Want the sample code? Check LocalizedRouteExample.zip (23.23 kb).

    Mapping a translated route

    First things first: here’s how I see a translated route being mapped in Global.asax.cs:

    routes.MapTranslatedRoute(
        "TranslatedRoute",
        "{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = "" },
        new { controller = translationProvider, action = translationProvider },
        true
    );

    Looks pretty much the same as you would normally map a route, right? There’s only one difference: the new { controller = translationProvider, action = translationProvider } line of code. This line of code basically tells the routing engine to use the object translationProvider as a provider which allows to translate a route value. In this case, the same translation provider will handle translating controller names and action names.

    Translation providers

    The translation provider being used can actually be anything, as long as it conforms to the following contract:

    public interface IRouteValueTranslationProvider
    {
        RouteValueTranslation TranslateToRouteValue(string translatedValue, CultureInfo culture);
        RouteValueTranslation TranslateToTranslatedValue(string routeValue, CultureInfo culture);
    }

    This contract provides 2 method definitions: one for mapping a translated value to a route value (like: mapping the Dutch “Thuis” to “Home”). The other method will do the opposite.

    TranslatedRoute

    The “core” of this solution is the TranslatedRoute class. It’s basically an overridden implementation of the System.Web.Routing.Route class, using the IRouteValueTranslationProvider for translating a route. As a bonus, it also tries to set the current thread culture to the CultureInfo detected based on the route being called. Note that this is just a reasonable guess, not the very truth. It will not detect nl-NL versus nl-BE, for example. Here’s the code:

    public class TranslatedRoute : Route
    {
        // ...

        public RouteValueDictionary RouteValueTranslationProviders { get; private set; }

        // ...

        public override RouteData GetRouteData(HttpContextBase httpContext)
        {
            RouteData routeData = base.GetRouteData(httpContext);

            // Translate route values
            foreach (KeyValuePair<string, object> pair in this.RouteValueTranslationProviders)
            {
                IRouteValueTranslationProvider translationProvider = pair.Value as IRouteValueTranslationProvider;
                if (translationProvider != null
                    && routeData.Values.ContainsKey(pair.Key))
                {
                    RouteValueTranslation translation = translationProvider.TranslateToRouteValue(
                        routeData.Values[pair.Key].ToString(),
                        CultureInfo.CurrentCulture);

                    routeData.Values[pair.Key] = translation.RouteValue;

                    // Store detected culture
                    if (routeData.DataTokens[DetectedCultureKey] == null)
                    {
                        routeData.DataTokens.Add(DetectedCultureKey, translation.Culture);
                    }

                    // Set detected culture
                    if (this.SetDetectedCulture)
                    {
                        System.Threading.Thread.CurrentThread.CurrentCulture = translation.Culture;
                        System.Threading.Thread.CurrentThread.CurrentUICulture = translation.Culture;
                    }
                }
            }

            return routeData;
        }

        public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
        {
            RouteValueDictionary translatedValues = values;

            // Translate route values
            foreach (KeyValuePair<string, object> pair in this.RouteValueTranslationProviders)
            {
                IRouteValueTranslationProvider translationProvider = pair.Value as IRouteValueTranslationProvider;
                if (translationProvider != null
                    && translatedValues.ContainsKey(pair.Key))
                {
                    RouteValueTranslation translation =
                        translationProvider.TranslateToTranslatedValue(
                            translatedValues[pair.Key].ToString(), CultureInfo.CurrentCulture);

                    translatedValues[pair.Key] = translation.TranslatedValue;
                }
            }

            return base.GetVirtualPath(requestContext, translatedValues);
        }
    }

    The GetRouteData finds a corresponding route translation if I entered “/Thuis/Over” in the URL. The GetVirtualPath method does the opposite, and will be used for mapping a call to <%=Html.ActionLink(“About”, “About”, “Home”)%> to a route like “/Thuis/Over” if the current thread culture is nl-NL. This is not rocket science, it simply tries to translate every token in the requested path and update the route data with it so the ASP.NET MVC subsystem will know that “Thuis” maps to HomeController.

    Tying everything together

    We already tied the route definition in Global.asax.cs earlier in this blog post, but let’s do it again with a sample DictionaryRouteValueTranslationProvider that will be used for translating routes. This one goes in Global.asax.cs:

    public static void RegisterRoutes(RouteCollection routes)
    {
        CultureInfo cultureEN = CultureInfo.GetCultureInfo("en-US");
        CultureInfo cultureNL = CultureInfo.GetCultureInfo("nl-NL");
        CultureInfo cultureFR = CultureInfo.GetCultureInfo("fr-FR");

        DictionaryRouteValueTranslationProvider translationProvider = new DictionaryRouteValueTranslationProvider(
            new List<RouteValueTranslation> {
                new RouteValueTranslation(cultureEN, "Home", "Home"),
                new RouteValueTranslation(cultureEN, "About", "About"),
                new RouteValueTranslation(cultureNL, "Home", "Thuis"),
                new RouteValueTranslation(cultureNL, "About", "Over"),
                new RouteValueTranslation(cultureFR, "Home", "Demarrer"),
                new RouteValueTranslation(cultureFR, "About", "Infos")
            }
        );

        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapTranslatedRoute(
            "TranslatedRoute",
            "{controller}/{action}/{id}",
            new { controller = "Home", action = "Index", id = "" },
            new { controller = translationProvider, action = translationProvider },
            true
        );

        routes.MapRoute(
            "Default",      // Route name
            "{controller}/{action}/{id}",   // URL with parameters
            new { controller = "Home", action = "Index", id = ""// Parameter defaults
        );

    }

    This is basically it! What I can now do is set the current thread’s culture to, let’s say fr-FR, and all action links generated by ASP.NET MVC will be using French. Easy? Yes! Cool? Yes!

    Localizing ASP.NET MVC routing

    Want the sample code? Check LocalizedRouteExample.zip (23.23 kb).

    kick it on DotNetKicks.com


    Categories: ASP.NET | C# | General | MVC | Projects

    Ordering fields in ASP.NET MVC 2 templated helpers

    Ever worked with the templated helpers provided by ASP.NET MVC 2? Templated helpers provide a way to automatically build UI based on a data model that is marked with attributes defined in the System.ComponentModel.DataAnnotations namespace. For example, a property in the model can be decorated with the attribute [DisplayFormat(DataFormatString = "{0:c}")], and the templated helpers will always render this field formatted as currency.

    If you have worked with templated helpers, you must agree: they can be useful! There’s one thing which is impossible in the current version: ordering fields.

    kick it on DotNetKicks.com

    Take the following class and the rendered form using templated helpers:

    ASP.NET MVC EditorForModel()

    public class Person
    {
        public string Email { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    Nice, but I would rather have the field “Email” displayed third. It would be nice if the field order could be applied using the same approach as with the System.ComponentModel.DataAnnotations namespace: let’s build us an attribute for it!

    Building the OrderAttribute

    Assuming you have already built an attribute once in your life, let’s go over this quickly:

    [global::System.AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
    public sealed class OrderAttribute : Attribute
    {
        readonly int order;

        public OrderAttribute(int order)
        {
            this.order = order;
        }

        public int Order
        {
            get { return order; }
        }
    }

    The OrderAttribute can be applied to any property of a model, and needs exactly one parameter: order. This order will be used to sort the fields being rendered. Here’s how our Person class may look like after applying the OrderAttribute:

    public class Person
    {
        [Order(3)]
        public string Email { get; set; }

        [Order(1)]
        public string FirstName { get; set; }

        [Order(2)]
        public string LastName { get; set; }
    }

    Speaks for itself, no? Now, before you stop reading: this will not work yet. The reason is that the default ModelMetadataProvider from the ASP.NET MVC framework, which provides the templated helpers all information they need about the model, does not know about this OrderAttribute. Let’s see what we can do about that…

    Building the OrderedDataAnnotationsModelMetadataProvider

    In order for the ASP.NET MVC framework to know and use the OrderAttribute created previously, we’re going to extend the default DataAnnotationsModelMetadataProvider provided with ASP.NET MVC 2. Here’s the code:

    public class OrderedDataAnnotationsModelMetadataProvider : DataAnnotationsModelMetadataProvider
    {
        public override IEnumerable<ModelMetadata> GetMetadataForProperties(
            object container, Type containerType)
        {
            SortedDictionary<int, ModelMetadata> returnValue =
                new SortedDictionary<int, ModelMetadata>();

            int key = 20000; // sort order for "unordered" keys

            IEnumerable<ModelMetadata> metadataForProperties =
                base.GetMetadataForProperties(container, containerType);

            foreach (ModelMetadata metadataForProperty in metadataForProperties)
            {
                PropertyInfo property = metadataForProperty.ContainerType.GetProperty(
                    metadataForProperty.PropertyName);

                object[] propertyAttributes = property.GetCustomAttributes(
                    typeof(OrderAttribute), true);

                if (propertyAttributes.Length > 0)
                {
                    OrderAttribute orderAttribute = propertyAttributes[0] as OrderAttribute;
                    returnValue.Add(orderAttribute.Order, metadataForProperty);
                }
                else
                {
                    returnValue.Add(key++, metadataForProperty);
                }
            }

            return returnValue.Values.AsEnumerable();
        }
    }

    By overriding the GetMetadataForProperties, we’re hooking into the DataAnnotationsModelMetadataProvider’s moment of truth, the method where all properties of the model are returned as ModelMetadata. First of all, we’re using the ModelMetadata the base class provdes. Next, we use a little bit of reflection to get to the OrderAttribute (if specified) and use it to build a SortedDictionary of ModelMetadata. Easy!

    One small caveat: non-decorated properties will always come last in the rendered output.

    One thing left…

    One thing left: registering the OrderedDataAnnotationsModelMetadataProvider with the ModelMetadataProviders infrastructure offered by ASP.NET MVC. Here’s how:

    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        RegisterRoutes(RouteTable.Routes);

        ModelMetadataProviders.Current = new OrderedDataAnnotationsModelMetadataProvider();
    }

    I guess you know this one goes into Global.asax.cs. If all works according to plan, your rendered view should now look like the following, with the e-mail field third:

    image

    kick it on DotNetKicks.com


    Categories: ASP.NET | C# | General | MVC

    Vote to help me speak at the MIX 2010 conference!

    Everybody knows the Microsoft MIX event, right? The one in Las Vegas? The one with all the fancy web-related stuff? Rings a bell? Ok, great. In the beginning of December 2009, Microsoft did an open call for speakers, which I answered with some session proposals. Who doesn’t want to go to Vegas, right?

    The open call proposals have been processed (150+ sessions submitted, wow!) and a voting has started. Yes, you hear me coming: please go ahead and vote for a session I submitted. Voting ends January 15th, 2010.

    Since I could not decide which color of the voting banner matched best with my blog’s theme, I decided to put them all three online:

    image

    Thanks in advance!

    Maarten

    PS: There's also Elijah Manor, Justin Etheredge, K. Scott Allen, and many others who submitted good looking sessions.

    kick it on DotNetKicks.com


    Supporting multiple submit buttons on an ASP.NET MVC view

    Multiple buttons on an ASP.NET MVC view A while ago, I was asked for advice on how to support multiple submit buttons in an ASP.NET MVC application, preferably without using any JavaScript. The idea was that a form could contain more than one submit button issuing a form post to a different controller action.

    The above situation can be solved in many ways, one a bit cleaner than the other. For example, one could post the form back to one action method and determine which method should be called from that action method. Good solution, however: not standardized within a project and just not that maintainable… A better solution in this case was to create an ActionNameSelectorAttribute.

    Whenever you decorate an action method in a controller with the ActionNameSelectorAttribute (or a subclass), ASP.NET MVC will use this attribute to determine which action method to call. For example, one of the ASP.NET MVC ActionNameSelectorAttribute subclasses is the ActionNameAttribute. Guess what the action name for the following code snippet will be for ASP.NET MVC:

    public class HomeController : Controller
    {
        [ActionName("Index")]
        public ActionResult Abcdefghij()
        {
            return View();
        }
    }

    That’s correct: this action method will be called Index instead of Abcdefghij. What happens at runtime is that ASP.NET MVC checks the ActionNameAttribute and asks if it applies for a specific request. Now let’s see if we can use this behavior for our multiple submit button scenario.

    kick it on DotNetKicks.com

    The view

    Since our view should not be aware of the server-side plumbing, we can simply create a view that looks like this.

    <%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<MvcMultiButton.Models.Person>" %>

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "//www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"">http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Create person</title>
        <script src="<%=Url.Content("~/Scripts/MicrosoftAjax.js")%>" type="text/javascript"></script>
        <script src="<%=Url.Content("~/Scripts/MicrosoftMvcAjax.js")%>" type="text/javascript"></script>
    </head>
    <body>

        <% Html.EnableClientValidation(); %>
        <% using (Html.BeginForm()) {%>

            <fieldset>
                <legend>Create person</legend>
                <p>
                    <%= Html.LabelFor(model => model.Name) %>
                    <%= Html.TextBoxFor(model => model.Name) %>
                    <%= Html.ValidationMessageFor(model => model.Name) %>
                </p>
                <p>
                    <%= Html.LabelFor(model => model.Email) %>
                    <%= Html.TextBoxFor(model => model.Email) %>
                    <%= Html.ValidationMessageFor(model => model.Email) %>
                </p>
                <p>
                    <input type="submit" value="Cancel" name="action" />
                    <input type="submit" value="Create" name="action" />
                </p>
            </fieldset>

        <% } %>

        <div>
            <%=Html.ActionLink("Back to List", "Index") %>
        </div>

    </body>
    </html>

    Note the two submit buttons (namely “Cancel” and “Create”), both named “action” but with a different value attribute.

    The controller

    Our controller should also not contain too much logic for determining the correct action method to be called. Here’s what I propose:

    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View(new Person());
        }

        [HttpPost]
        [MultiButton(MatchFormKey="action", MatchFormValue="Cancel")]
        public ActionResult Cancel()
        {
            return Content("Cancel clicked");
        }

        [HttpPost]
        [MultiButton(MatchFormKey = "action", MatchFormValue = "Create")]
        public ActionResult Create(Person person)
        {
            return Content("Create clicked");
        }
    }

    Some things to note:

    • There’s the Index action method which just renders the view described previously.
    • There’s a Cancel action method which will trigger when clicking the Cancel button.
    • There’s a Create action method which will trigger when clicking the Create button.

    Now how do these last two work… You may also have noticed the MultiButtonAttribute being applied. We’ll see the implementation in a minute. In short, this is a subclass for the ActionNameSelectorAttribute, triggering on the parameters MatchFormKey and MatchFormValues. Now let’s see how the MultiButtonAttribute class is built…

    The MultiButtonAttribute class

    Now do be surprised of the amount of code that is coming…

    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class MultiButtonAttribute : ActionNameSelectorAttribute
    {
        public string MatchFormKey { get; set; }
        public string MatchFormValue { get; set; }

        public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
        {
            return controllerContext.HttpContext.Request[MatchFormKey] != null &&
                controllerContext.HttpContext.Request[MatchFormKey] == MatchFormValue;
        }
    }

    When applying the MultiButtonAttribute to an action method, ASP.NET MVC will come and call the IsValidName method. Next, we just check if the MatchFormKey value is one of the request keys, and the MatchFormValue matches the value in the request. Simple, straightforward and re-usable.

    kick it on DotNetKicks.com


    Microsoft PDC09 keynote highlights

    Finally found some time to write a short blog post on the announcements this morning at PDC 2009.Microsoft PDC keynote highlights Ray Ozzie started the keynote this morning, focusing on Microsoft’s “three-screen” vision for the future. There will be three screens connected to the cloud: TV, (handheld) devices and of course good old PC. This vision is driven by some key players: Windows 7, Internet Explorer, Silverlight and Windows Azure. Make sure to have a look at these four if you want to play in this future.

    Some announcements were made as well:

    Had a great day yesterday, driving trough the city of Los Angeles and looking at various places in town. Conference day one was also very interesting, lots of good sessions. Currently missing a session slot though, waiting for a Channel9 interview on the Windows Azure SDK for PHP. Stay tuned!