ASP.NET MVC working smart with cookies – Custom ValueProvider

ValueProviders are the mechanism within the MVC framework that tries to take information from the HTTP Request and then resolve parameter values on action methods. Complex as well as simple types will be resolved using one of the four inbuilt value providers listed.

  • FormValueProvider: Provides values from posted form parameters.
  • QueryStringValueProvider: Provides values from the query string collection.
  • HttpFileCollectionValueProvider: Provides values from posted files.
  • RouteDataValueProvider: Provides values from route parameters.

Below I have created a controller which has one action. When the Index action is called using GET a cookie is added to the browser called isRegistered with the value “true” assigned to it. When the form is posted the index action is again called and the cookie value is automatically assigned to the isRegistered parameter on the POST index method. But how?  By using a custom Value Provider!

    public class PersonController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            AddCookie();
            ViewData["CookieData"] = false;
            return View(new Employee { CreateDate = new DateTime(2010, 10, 10) });
        }
        [HttpPost]
        public ActionResult Index(Employee employee, bool isRegistered)
        {
            ViewData["CookieData"] = isRegistered;
if (!ModelState.IsValid) return View(employee); return View(employee); } private void AddCookie() { HttpContext.Response.Cookies.Add( new HttpCookie("isRegistered", "true") ); } }

Below I have the mark-up for a checkbox that looks at the value assigned to the ViewData key “CookieValue”.

            <div>
                <label>Registered</label>
                <span><%=Html.CheckBox("Registered", (bool)ViewData["CookieData"])%></span><br />
            </div>

When the form is requested the registered checkbox is unchecked. After the form is posted to the server the registered checkbox is set to checked as the CookieValueProvider resolves the value isRegistered by looking at all the cookies stored for the site.  (Note: the registered checkbox does not have an ID or NAME so it is not resolved by the FormValueProvider)

image 
Below is an implementation of the CookieValueProviderFactory and also CookieValueProvider. By convention when the CookieValueProvider is invoked by the framework it will try to match the name of variable passed to it from the action method signature to a cookie with the same name. If a match is found the variables value will be set.

    public class CookieValueProviderFactory : ValueProviderFactory
    {
        public override IValueProvider GetValueProvider(ControllerContext controllerContext)
        {
                return new CookieValueProvider(controllerContext.HttpContext.Request.Cookies);
        }
        private class CookieValueProvider : IValueProvider
        {
            private readonly HttpCookieCollection _cookieCollection;

            public CookieValueProvider(HttpCookieCollection cookieCollection)
            {
                _cookieCollection = cookieCollection;
            }

            public bool ContainsPrefix(string prefix)
            {
                return _cookieCollection[prefix] != null;
            }

            public ValueProviderResult GetValue(string key)
            {
                HttpCookie cookie = _cookieCollection[key];
                return cookie != null ? 
                                      new ValueProviderResult(cookie.Value, 
                                                              cookie.Value, 
                                                              CultureInfo.CurrentUICulture) 
                                      : null;
            }
        }
    }

All that needs to be done now is for the CookieValueProviderFactory to be added to the ValueProviderFactories collection via the Global.asax.  The framework will now search through five all five value providers in order to set parameters on action methods. Please be aware that this is done in order so if the value of isRegistered is set by the QueryStringValueProvider then the CookieValueProvider would not be used to set the value of isRegistered on the action method.

    public class MvcApplication : System.Web.HttpApplication
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Person", action = "Index", id = UrlParameter.Optional } 
            );
        }

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RegisterRoutes(RouteTable.Routes);
            ValueProviderFactories.Factories.Add(new CookieValueProviderFactory());
        }
    }

The implementation above will work with MVC 2.0 but not 1.0 as the ASP.NET MVC team changed how model binding is implemented between the framework versions.

Sample Code: Download

Twitter Twitter Twitter Twitter Twitter
March 7, 2010 01:52 by DanWatson

Property Matching With Data Annotations

I found myself needing to compare two properties on a view model when developing a new ASP.NET MVC 2.0 site today. My initial thoughts were I would check the two properties for equality by making a call to my business logic assembly.

I wondered if it would be possible to compare two properties using data annotations. Usually you place the data annotations attribute on a property to validate the data in some way. I quickly knocked up a custom ValdationAttribute that could be placed on a class that checks for equality between two properties of the class. It checks both type and value equality.

using System;
using System.ComponentModel.DataAnnotations;
using System.Reflection;

namespace Model.CustomAnnotations
{
    [AttributeUsage(AttributeTargets.Class)]
    public class MatchAttribute : ValidationAttribute
    {
        public String SourceProperty { get; set; }
        public String MatchProperty { get; set; }

        public MatchAttribute(string source, string match)
        {
            SourceProperty = source;
            MatchProperty = match;
        }

        public override Boolean IsValid(Object value)
        {
            Type objectType = value.GetType();

            PropertyInfo[] properties = objectType.GetProperties();

            object sourceValue = new object();
            object matchValue = new object();

            Type sourceType = null;
            Type matchType = null;

            int counter = 0;

            foreach (PropertyInfo propertyInfo in properties)
            {
                if (propertyInfo.Name == SourceProperty || propertyInfo.Name == MatchProperty)
                {
                    if (counter == 0)
                    {
                        sourceValue = propertyInfo.GetValue(value, null);
                        sourceType = propertyInfo.GetValue(value, null).GetType();
                    }
                    if (counter == 1)
                    {
                        matchValue = propertyInfo.GetValue(value, null);
                        matchType = propertyInfo.GetValue(value, null).GetType();
                    }
                    counter++;
                    if (counter == 2)
                    {
                        break;
                    }
                }
            }

            if (sourceType != null && matchType != null)
            {
                if (Convert.ChangeType(sourceValue, sourceType) == Convert.ChangeType(matchValue, matchType))
                    return true;
            }
            return false;
        }
    }
}

Here is how the attribute is placed on a class. The attribute takes two values which are the names of the properties you need to match.

    [Match("Email", "EmailConfirmation", ErrorMessage = "Emails must match")]
    public class PersonViewModel
    {
        public int PersonId { get; set; }
        [Required]
        public string Firstname { get; set; }
        [Required]
        public string Lastname { get; set; }
        [Required]
        public string Email { get; set; }
        [Required]
        public string EmailConfirmation { get; set; }
    }

This then bubbles up to the UI. If you are using MVC 2.0 this will work out of the box as the default model binder supports data annotations.

Pretty straight forward but very useful! You can download the code here to see it working end to end. You will need MVC 2.0 RC installed as well as Visual Studio Standard Edition or above.

Twitter Twitter Twitter Twitter Twitter
January 9, 2010 19:04 by DanWatson