6.27.2010

MVC 2 DropDownListFor - Any troubles? - Part 1

When working on one project, I have a trouble with MVC DropDownListFor - selected value doesn't returned to the server. I saw a lot of articles, questions, etc.. about this thing. And I decide to write a little article about my solution. As for me - it is more than simple :)

So... using Reflector I take code of SelectExtensions class, and slightly change it:
all methods renamed with suffix "2", to avoid conflicts with MVC's DropDownList
changed 2 lines of code, and added 1 - all changes highlighted on the listing below
Here is a listing:
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Resources;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace HennadiyKurabko.Web.Mvc.Html
{
    /// <summary>Represents support for making selections in a list.</summary>
    public static class SelectExtensions2
    {
        #region Fixed

        /// <summary>Returns a single-selection select element using the specified HTML helper and the name of the form field.</summary>
        /// <returns>An HTML select element.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field to return.</param>
        /// <exception cref="T:System.ArgumentException">The <paramref name="name" /> parameter is null or empty.</exception>
        public static MvcHtmlString DropDownList2(this HtmlHelper htmlHelper, string name)
        {
            return htmlHelper.DropDownList2(name, null, null, ((IDictionary<string, object>)null));
        }

        /// <summary>Returns a single-selection select element using the specified HTML helper, the name of the form field, and the specified list items.</summary>
        /// <returns>An HTML select element with an option subelement for each item in the list.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field to return.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the drop-down list.</param>
        /// <exception cref="T:System.ArgumentException">The <paramref name="name" /> parameter is null or empty.</exception>
        public static MvcHtmlString DropDownList2(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList)
        {
            return htmlHelper.DropDownList2(name, selectList, null, ((IDictionary<string, object>)null));
        }

        /// <summary>Returns a single-selection select element using the specified HTML helper, the name of the form field, and an option label.</summary>
        /// <returns>An HTML select element with an option subelement for each item in the list.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field to return.</param>
        /// <param name="optionLabel">The text for a default empty item. This parameter can be null.</param>
        /// <exception cref="T:System.ArgumentException">The <paramref name="name" /> parameter is null or empty.</exception>
        public static MvcHtmlString DropDownList2(this HtmlHelper htmlHelper, string name, string optionLabel)
        {
            return htmlHelper.DropDownList2(name, null, optionLabel, ((IDictionary<string, object>)null));
        }

        /// <summary>Returns a single-selection select element using the specified HTML helper, the name of the form field, the specified list items, and the specified HTML attributes.</summary>
        /// <returns>An HTML select element with an option subelement for each item in the list.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field to return.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the drop-down list.</param>
        /// <param name="htmlAttributes">An object that contains the HTML attributes to set for the element.</param>
        /// <exception cref="T:System.ArgumentException">The <paramref name="name" /> parameter is null or empty.</exception>
        public static MvcHtmlString DropDownList2(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
        {
            return htmlHelper.DropDownList2(name, selectList, null, htmlAttributes);
        }

        /// <summary>Returns a single-selection select element using the specified HTML helper, the name of the form field, the specified list items, and the specified HTML attributes.</summary>
        /// <returns>An HTML select element with an option subelement for each item in the list.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field to return.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the drop-down list.</param>
        /// <param name="htmlAttributes">An object that contains the HTML attributes to set for the element.</param>
        /// <exception cref="T:System.ArgumentException">The <paramref name="name" /> parameter is null or empty.</exception>
        public static MvcHtmlString DropDownList2(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, object htmlAttributes)
        {
            return htmlHelper.DropDownList2(name, selectList, null, ((IDictionary<string, object>)new RouteValueDictionary(htmlAttributes)));
        }

        /// <summary>Returns a single-selection select element using the specified HTML helper, the name of the form field, the specified list items, and an option label.</summary>
        /// <returns>An HTML select element with an option subelement for each item in the list.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field to return.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the drop-down list.</param>
        /// <param name="optionLabel">The text for a default empty item. This parameter can be null.</param>
        /// <exception cref="T:System.ArgumentException">The <paramref name="name" /> parameter is null or empty.</exception>
        public static MvcHtmlString DropDownList2(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel)
        {
            return htmlHelper.DropDownList2(name, selectList, optionLabel, ((IDictionary<string, object>)null));
        }

        /// <summary>Returns a single-selection select element using the specified HTML helper, the name of the form field, the specified list items, an option label, and the specified HTML attributes.</summary>
        /// <returns>An HTML select element with an option subelement for each item in the list.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field to return.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the drop-down list.</param>
        /// <param name="optionLabel">The text for a default empty item. This parameter can be null.</param>
        /// <param name="htmlAttributes">An object that contains the HTML attributes to set for the element.</param>
        /// <exception cref="T:System.ArgumentException">The <paramref name="name" /> parameter is null or empty.</exception>
        public static MvcHtmlString DropDownList2(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
        {
            return DropDownList2Helper(htmlHelper, name, selectList, optionLabel, htmlAttributes);
        }

        /// <summary>Returns a single-selection select element using the specified HTML helper, the name of the form field, the specified list items, an option label, and the specified HTML attributes.</summary>
        /// <returns>An HTML select element with an option subelement for each item in the list.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field to return.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the drop-down list.</param>
        /// <param name="optionLabel">The text for a default empty item. This parameter can be null.</param>
        /// <param name="htmlAttributes">An object that contains the HTML attributes to set for the element.</param>
        /// <exception cref="T:System.ArgumentException">The <paramref name="name" /> parameter is null or empty.</exception>
        public static MvcHtmlString DropDownList2(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel, object htmlAttributes)
        {
            return htmlHelper.DropDownList2(name, selectList, optionLabel, ((IDictionary<string, object>)new RouteValueDictionary(htmlAttributes)));
        }

        /// <summary>Returns an HTML select element for each property in the object that is represented by the specified expression using the specified list items.</summary>
        /// <returns>An HTML select element for each property in the object that is represented by the expression.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="expression">An expression that identifies the object that contains the properties to render.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the drop-down list.</param>
        /// <typeparam name="TModel">The type of the model.</typeparam>
        /// <typeparam name="TProperty">The type of the value.</typeparam>
        /// <exception cref="T:System.ArgumentNullException">The <paramref name="expression" /> parameter is null.</exception>
        public static MvcHtmlString DropDownList2For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList)
        {
            return htmlHelper.DropDownList2For<TModel, TProperty>(expression, selectList, null, ((IDictionary<string, object>)null));
        }

        /// <summary>Returns an HTML select element for each property in the object that is represented by the specified expression using the specified list items and HTML attributes.</summary>
        /// <returns>An HTML select element for each property in the object that is represented by the expression.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="expression">An expression that identifies the object that contains the properties to render.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the drop-down list.</param>
        /// <param name="htmlAttributes">A dictionary that contains the HTML attributes to set for the element.</param>
        /// <typeparam name="TModel">The type of the model.</typeparam>
        /// <typeparam name="TProperty">The type of the value.</typeparam>
        /// <exception cref="T:System.ArgumentNullException">The <paramref name="expression" /> parameter is null.</exception>
        public static MvcHtmlString DropDownList2For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
        {
            return htmlHelper.DropDownList2For<TModel, TProperty>(expression, selectList, null, htmlAttributes);
        }

        /// <summary>Returns an HTML select element for each property in the object that is represented by the specified expression using the specified list items and HTML attributes.</summary>
        /// <returns>An HTML select element for each property in the object that is represented by the expression.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="expression">An expression that identifies the object that contains the properties to render.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the drop-down list.</param>
        /// <param name="htmlAttributes">An object that contains the HTML attributes to set for the element.</param>
        /// <typeparam name="TModel">The type of the model.</typeparam>
        /// <typeparam name="TProperty">The type of the value.</typeparam>
        /// <exception cref="T:System.ArgumentNullException">The <paramref name="expression" /> parameter is null.</exception>
        public static MvcHtmlString DropDownList2For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, object htmlAttributes)
        {
            return htmlHelper.DropDownList2For<TModel, TProperty>(expression, selectList, null, ((IDictionary<string, object>)new RouteValueDictionary(htmlAttributes)));
        }

        /// <summary>Returns an HTML select element for each property in the object that is represented by the specified expression using the specified list items and option label.</summary>
        /// <returns>An HTML select element for each property in the object that is represented by the expression.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="expression">An expression that identifies the object that contains the properties to render.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the drop-down list.</param>
        /// <param name="optionLabel">The text for a default empty item. This parameter can be null.</param>
        /// <typeparam name="TModel">The type of the model.</typeparam>
        /// <typeparam name="TProperty">The type of the value.</typeparam>
        /// <exception cref="T:System.ArgumentNullException">The <paramref name="expression" /> parameter is null.</exception>
        public static MvcHtmlString DropDownList2For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel)
        {
            return htmlHelper.DropDownList2For<TModel, TProperty>(expression, selectList, optionLabel, ((IDictionary<string, object>)null));
        }

        /// <summary>Returns an HTML select element for each property in the object that is represented by the specified expression using the specified list items, option label, and HTML attributes.</summary>
        /// <returns>An HTML select element for each property in the object that is represented by the expression.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="expression">An expression that identifies the object that contains the properties to render.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the drop-down list.</param>
        /// <param name="optionLabel">The text for a default empty item. This parameter can be null.</param>
        /// <param name="htmlAttributes">A dictionary that contains the HTML attributes to set for the element.</param>
        /// <typeparam name="TModel">The type of the model.</typeparam>
        /// <typeparam name="TProperty">The type of the value.</typeparam>
        /// <exception cref="T:System.ArgumentNullException">The <paramref name="expression" /> parameter is null.</exception>
        public static MvcHtmlString DropDownList2For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
        {
            if (expression == null)
            {
                throw new ArgumentNullException("expression");
            }
            return DropDownList2Helper(htmlHelper, ExpressionHelper.GetExpressionText(expression), selectList, optionLabel, htmlAttributes);
        }

        /// <summary>Returns an HTML select element for each property in the object that is represented by the specified expression using the specified list items, option label, and HTML attributes.</summary>
        /// <returns>An HTML select element for each property in the object that is represented by the expression.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="expression">An expression that identifies the object that contains the properties to render.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the drop-down list.</param>
        /// <param name="optionLabel">The text for a default empty item. This parameter can be null.</param>
        /// <param name="htmlAttributes">An object that contains the HTML attributes to set for the element.</param>
        /// <typeparam name="TModel">The type of the model.</typeparam>
        /// <typeparam name="TProperty">The type of the value.</typeparam>
        public static MvcHtmlString DropDownList2For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel, object htmlAttributes)
        {
            return htmlHelper.DropDownList2For<TModel, TProperty>(expression, selectList, optionLabel, ((IDictionary<string, object>)new RouteValueDictionary(htmlAttributes)));
        }

        /// <summary>Returns a multi-select select element using the specified HTML helper and the name of the form field.</summary>
        /// <returns>An HTML select element.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field to return.</param>
        /// <exception cref="T:System.ArgumentException">The <paramref name="name" /> parameter is null or empty.</exception>
        public static MvcHtmlString ListBox2(this HtmlHelper htmlHelper, string name)
        {
            return htmlHelper.ListBox2(name, null, ((IDictionary<string, object>)null));
        }

        /// <summary>Returns a multi-select select element using the specified HTML helper, the name of the form field, and the specified list items.</summary>
        /// <returns>An HTML select element with an option subelement for each item in the list.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field to return.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the drop-down list.</param>
        /// <exception cref="T:System.ArgumentException">The <paramref name="name" /> parameter is null or empty.</exception>
        public static MvcHtmlString ListBox2(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList)
        {
            return htmlHelper.ListBox2(name, selectList, ((IDictionary<string, object>)null));
        }

        /// <summary>Returns a multi-select select element using the specified HTML helper, the name of the form field, the specified list items, and the specified HMTL attributes.</summary>
        /// <returns>An HTML select element with an option subelement for each item in the list..</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field to return.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the drop-down list.</param>
        /// <param name="htmlAttributes">An object that contains the HTML attributes to set for the element.</param>
        /// <exception cref="T:System.ArgumentException">The <paramref name="name" /> parameter is null or empty.</exception>
        public static MvcHtmlString ListBox2(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
        {
            return ListBox2Helper(htmlHelper, name, selectList, htmlAttributes);
        }

        /// <summary>Returns a multi-select select element using the specified HTML helper, the name of the form field, and the specified list items.</summary>
        /// <returns>An HTML select element with an option subelement for each item in the list..</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field to return.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the drop-down list.</param>
        /// <param name="htmlAttributes">An object that contains the HTML attributes to set for the element.</param>
        /// <exception cref="T:System.ArgumentException">The <paramref name="name" /> parameter is null or empty.</exception>
        public static MvcHtmlString ListBox2(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, object htmlAttributes)
        {
            return htmlHelper.ListBox2(name, selectList, ((IDictionary<string, object>)new RouteValueDictionary(htmlAttributes)));
        }

        /// <summary>Returns an HTML select element for each property in the object that is represented by the specified expression and using the specified list items.</summary>
        /// <returns>An HTML select element for each property in the object that is represented by the expression.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="expression">An expression that identifies the object that contains the properties to render.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the list.</param>
        /// <typeparam name="TModel">The type of the model.</typeparam>
        /// <typeparam name="TProperty">The type of the value.</typeparam>
        /// <exception cref="T:System.ArgumentNullException">The <paramref name="expression" /> parameter is null.</exception>
        public static MvcHtmlString ListBox2For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList)
        {
            return htmlHelper.ListBox2For<TModel, TProperty>(expression, selectList, ((IDictionary<string, object>)null));
        }

        /// <summary>Returns an HTML select element for each property in the object that is represented by the specified expression using the specified list items and HTML attributes.</summary>
        /// <returns>An HTML select element for each property in the object that is represented by the expression.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="expression">An expression that identifies the object that contains the properties to render.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the list.</param>
        /// <param name="htmlAttributes">A dictionary that contains the HTML attributes to set for the element.</param>
        /// <typeparam name="TModel">The type of the model.</typeparam>
        /// <typeparam name="TProperty">The type of the value.</typeparam>
        /// <exception cref="T:System.ArgumentNullException">The <paramref name="expression" /> parameter is null.</exception>
        public static MvcHtmlString ListBox2For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
        {
            if (expression == null)
            {
                throw new ArgumentNullException("expression");
            }
            return ListBox2Helper(htmlHelper, ExpressionHelper.GetExpressionText(expression), selectList, htmlAttributes);
        }

        /// <summary>Returns an HTML select element for each property in the object that is represented by the specified expression using the specified list items and HTML attributes.</summary>
        /// <returns>An HTML select element for each property in the object that is represented by the expression.</returns>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="expression">An expression that identifies the object that contains the properties to render.</param>
        /// <param name="selectList">A collection of <see cref="T:System.Web.Mvc.SelectListItem" /> objects that are used to populate the list.</param>
        /// <param name="htmlAttributes">An object that contains the HTML attributes to set for the element.</param>
        /// <typeparam name="TModel">The type of the model.</typeparam>
        /// <typeparam name="TProperty">The type of the value.</typeparam>
        /// <exception cref="T:System.ArgumentNullException">The <paramref name="expression" /> parameter is null.</exception>
        public static MvcHtmlString ListBox2For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, object htmlAttributes)
        {
            return htmlHelper.ListBox2For<TModel, TProperty>(expression, selectList, ((IDictionary<string, object>)new RouteValueDictionary(htmlAttributes)));
        }

        internal static string ListItemToOption(SelectListItem item)
        {
            TagBuilder builder2 = new TagBuilder("option");
            builder2.InnerHtml = HttpUtility.HtmlEncode(item.Text);
            TagBuilder builder = builder2;
            if (item.Value != null)
            {
                builder.Attributes["value"] = item.Value;
            }
            if (item.Selected)
            {
                builder.Attributes["selected"] = "selected";
            }
            return builder.ToString(TagRenderMode.Normal);
        }

        private static MvcHtmlString ListBox2Helper(HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
        {
            return htmlHelper.SelectInternal(null, name, selectList, true, htmlAttributes);
        }

        private static MvcHtmlString DropDownList2Helper(HtmlHelper htmlHelper, string expression, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
        {
            return htmlHelper.SelectInternal(optionLabel, expression, selectList, false, htmlAttributes);
        }

        private static IEnumerable<SelectListItem> GetSelectData(this HtmlHelper htmlHelper, string name)
        {
            object obj2 = null;
            if (htmlHelper.ViewData != null)
            {
                obj2 = htmlHelper.ViewData.Eval(name);
            }
            if (obj2 == null)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, MvcResources.HtmlHelper_MissingSelectData, new object[] { name, "IEnumerable<SelectListItem>" }));
            }
            IEnumerable<SelectListItem> enumerable = obj2 as IEnumerable<SelectListItem>;
            if (enumerable == null)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, MvcResources.HtmlHelper_WrongSelectDataType, new object[] { name, obj2.GetType().FullName, "IEnumerable<SelectListItem>" }));
            }
            return enumerable;
        }

        private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, string optionLabel, string name, IEnumerable<SelectListItem> selectList, bool allowMultiple, IDictionary<string, object> htmlAttributes)
        {
            ModelState state;
            string shortName = name; // added
            name = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name");
            }
            bool flag = false;
            if (selectList == null)
            {
                // changed
                // selectList = htmlHelper.GetSelectData(name);
                selectList = htmlHelper.GetSelectData(shortName);
                flag = true;
            }
            object obj2 = allowMultiple ? htmlHelper.GetModelStateValue(name, typeof(string[])) : htmlHelper.GetModelStateValue(name, typeof(string));
            if (!flag && (obj2 == null))
            {
                // changed
                // obj2 = htmlHelper.ViewData.Eval(name);
                obj2 = htmlHelper.ViewData.Eval(shortName);
            }
            if (obj2 != null)
            {
                IEnumerable source = allowMultiple ? (obj2 as IEnumerable) : ((IEnumerable)new object[] { obj2 });
                HashSet<string> set = new HashSet<string>(source.Cast<object>().Select<object, string>(delegate(object value)
                {
                    return Convert.ToString(value, CultureInfo.CurrentCulture);
                }), StringComparer.OrdinalIgnoreCase);
                List<SelectListItem> list = new List<SelectListItem>();
                foreach (SelectListItem item in selectList)
                {
                    item.Selected = (item.Value != null) ? set.Contains(item.Value) : set.Contains(item.Text);
                    list.Add(item);
                }
                selectList = list;
            }
            StringBuilder builder = new StringBuilder();
            if (optionLabel != null)
            {
                SelectListItem item2 = new SelectListItem();
                item2.Text = optionLabel;
                item2.Value = string.Empty;
                item2.Selected = false;
                builder.AppendLine(ListItemToOption(item2));
            }
            foreach (SelectListItem item3 in selectList)
            {
                builder.AppendLine(ListItemToOption(item3));
            }
            TagBuilder builder3 = new TagBuilder("select");
            builder3.InnerHtml = builder.ToString();
            TagBuilder builder2 = builder3;
            builder2.MergeAttributes<string, object>(htmlAttributes);
            builder2.MergeAttribute("name", name, true);
            builder2.GenerateId(name);
            if (allowMultiple)
            {
                builder2.MergeAttribute("multiple", "multiple");
            }
            if (htmlHelper.ViewData.ModelState.TryGetValue(name, out state) && (state.Errors.Count > 0))
            {
                builder2.AddCssClass(HtmlHelper.ValidationInputCssClassName);
            }
            return builder2.ToMvcHtmlString(TagRenderMode.Normal);
        }

        #region Common

        private static object GetModelStateValue(this HtmlHelper self, string key, Type destinationType)
        {
            ModelState state;
            if (self.ViewData.ModelState.TryGetValue(key, out state) && (state.Value != null))
            {
                return state.Value.ConvertTo(destinationType, null);
            }
            return null;
        }

        private static MvcHtmlString ToMvcHtmlString(this TagBuilder self, TagRenderMode renderMode)
        {
            return MvcHtmlString.Create(self.ToString(renderMode));
        }

        private static class MvcResources
        {
            private static CultureInfo resourceCulture;
            private static ResourceManager resourceMan;

            [EditorBrowsable(EditorBrowsableState.Advanced)]
            internal static CultureInfo Culture
            {
                get
                {
                    return resourceCulture;
                }
                set
                {
                    resourceCulture = value;
                }
            }

            [EditorBrowsable(EditorBrowsableState.Advanced)]
            internal static ResourceManager ResourceManager
            {
                get
                {
                    if (object.ReferenceEquals(resourceMan, null))
                    {
                        Assembly mvcAssembly = Assembly.Load("System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
                        ResourceManager manager = new ResourceManager("System.Web.Mvc.Resources.MvcResources", mvcAssembly);
                        resourceMan = manager;
                    }
                    return resourceMan;
                }
            }

            internal static string Common_NullOrEmpty
            {
                get
                {
                    return ResourceManager.GetString("Common_NullOrEmpty", resourceCulture);
                }
            }

            internal static string HtmlHelper_MissingSelectData
            {
                get
                {
                    return ResourceManager.GetString("HtmlHelper_MissingSelectData", resourceCulture);
                }
            }

            internal static string HtmlHelper_WrongSelectDataType
            {
                get
                {
                    return ResourceManager.GetString("HtmlHelper_WrongSelectDataType", resourceCulture);
                }
            }
        }

        #endregion
    }
}
Thats it... When, in MVC 3, will be DropDownList that will work fine you can simply remove suffix "2".

Also.. use it on your own risk ;)


Shout it

kick it on DotNetKicks.com

ASP.NET MVC 2 - Building extensible view engine.

Contents


Introduction
Understanding an Idea
Extensible version of view engine
Sample application

Introduction


In time of MVC 1, inspired by Phil Haack's blog post about Grouping Controllers with ASP.NET MVC one my colleague added a support of areas and skins to our project. Skins was fine, but about areas... my intuition asked me that things can be more simple, and there might be a better, more testable solution than proposed by Phil.
So I wrote a WebFormExtensibleViewEngine that can be easily extended to do whatever you want. After that I extend it and wrote a WebFormSkinedAreaViewEngine that supports areas (no matter nested or not) and skinning.

Then I wrote this article, but there was MVC 2.0 Beta 2 and I found there exactly similar solution. So I decided do not publish this article. But after moving to MVC 2.0 RTM, I found that this theme is still actual, so I decide to publish this article but without description how to implement areas.
Go to contents >

Understanding an Idea


First of all I want to describe an idea about how all this works and we will develop the simple version of a view engine. So, in ASP.NET MVC we have a default WebFormViewEngine. This class inherits from an abstract VirtualPathProviderViewEngine; realizes 2 abstract methods: CreatePartialView, CreateView; overrides base method FileExists and... thats all. Oh, also this class initializes 6 *LocationFormats properties of the base class in his constructor: MasterLocationFormats, ViewLocationFormats, PartialViewLocationFormats, AreaMasterLocationFormats, AreaViewLocationFormats, AreaPartialViewLocationFormats. You can see all this in source code of MVC 2.0 RTM, for readability reasons I place an implementation of WebFormViewEngine here:
public class WebFormViewEngine : VirtualPathProviderViewEngine
{
    private IBuildManager _buildManager;

    public WebFormViewEngine()
    {
        base.MasterLocationFormats = new string[] { "~/Views/{1}/{0}.master", "~/Views/Shared/{0}.master" };
        base.AreaMasterLocationFormats = new string[] { "~/Areas/{2}/Views/{1}/{0}.master", "~/Areas/{2}/Views/Shared/{0}.master" };
        base.ViewLocationFormats = new string[] { "~/Views/{1}/{0}.aspx", "~/Views/{1}/{0}.ascx", "~/Views/Shared/{0}.aspx", "~/Views/Shared/{0}.ascx" };
        base.AreaViewLocationFormats = new string[] { "~/Areas/{2}/Views/{1}/{0}.aspx", "~/Areas/{2}/Views/{1}/{0}.ascx", "~/Areas/{2}/Views/Shared/{0}.aspx", "~/Areas/{2}/Views/Shared/{0}.ascx" };
        base.PartialViewLocationFormats = base.ViewLocationFormats;
        base.AreaPartialViewLocationFormats = base.AreaViewLocationFormats;
    }

    protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
    {
        return new WebFormView(partialPath, null);
    }

    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {
        return new WebFormView(viewPath, masterPath);
    }

    protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
    {
        try
        {
            return (this.BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(object)) != null);
        }
        catch (HttpException exception)
        {
            if (exception is HttpParseException) throw;
            if ((exception.GetHttpCode() != 0x194) || base.FileExists(controllerContext, virtualPath)) throw;
            return false;
        }
    }

    internal IBuildManager BuildManager
    {
        get
        {
            if (this._buildManager == null)
                this._buildManager = new BuildManagerWrapper();
            return this._buildManager;
        }
        set { this._buildManager = value; }
    }
}
Methods CreateView, CreatePartialView used by base class (VirtualPathProviderViewEngine) to create results in FindView, FindPartialView methods respectively. Method FileExists used by base class to check if requested view really exists in the 'location' or not.

The most interesting are 6 properties initialized in the constructor. They provide 'location formats' (as you can see from their names) that used to form possible 'location' string when engine will search the requested View or PartialView or even MasterPage. As you can see View and PartialView locations are the same also there are two versions of format strings one with aspx and another with ascx file extension. This gives you an ability to use both of this formats as views/partial views as you wish. Format strings has 3 placeholders with next purpose:
{0} - name of the View\PartialView\MasterPage
{1} - name of the controller
{2} - name of the area
My idea is simple - extend list of search location formats with own custom format strings that include support for skinning, for example:
"~/Content/{3}/Views/{1}/{0}.master"
"~/Content/{3}/Views/Shared/{0}.master"
"~/Content/{3}/Areas/{2}/Views/{1}/{0}.ascx"
"~/Content/{3}/Areas/{2}/Views/Shared/{0}.ascx"
As you can see, I've added new placeholder {3}, it is used for skin name.

Since standard version of FindView and FindPartialView methods realized in VirtualPathProviderViewEngine class have no support for placeholder {3}, we must override this two methods. Overridden versions will do thing that I call 'preformatting' - replace {3} by real value and leave {0}, {1}, {2} unchanged. 'Preformatted' location can be used by base FindView, FindPartialView methods. Here is a simple implementation of view engine that supports skinning:
using System.Collections.Generic;
using System.Web.Mvc;

namespace HennadiyKurabko.SimpleViewEngine
{
    public class WebFormSimpleViewEngine : WebFormViewEngine
    {
        public WebFormSimpleViewEngine()
        {
            MasterLocationFormats = new[] 
            {
                "~/Content/{3}/Views/{1}/{0}.master",
                "~/Content/{3}/Views/Shared/{0}.master",
                "~/Views/{1}/{0}.master",
                "~/Views/Shared/{0}.master",
            };

            AreaMasterLocationFormats = new[]
            {
                "~/Content/{3}/Areas/{2}/Views/{1}/{0}.master",
                "~/Content/{3}/Areas/{2}/Views/Shared/{0}.master",
                "~/Areas/{2}/Views/{1}/{0}.master",
                "~/Areas/{2}/Views/Shared/{0}.master",
            };

            ViewLocationFormats = new[] 
            { 
                // asPx
                "~/Content/{3}/Views/{1}/{0}.aspx",
                "~/Content/{3}/Views/Shared/{0}.aspx",
                "~/Views/{1}/{0}.aspx",
                "~/Views/Shared/{0}.aspx",

                // asCx
                "~/Content/{3}/Views/{1}/{0}.ascx",
                "~/Content/{3}/Views/Shared/{0}.ascx",
                "~/Views/{1}/{0}.ascx",
                "~/Views/Shared/{0}.ascx",
            };

            AreaViewLocationFormats = new[] 
            { 
                // asPx
                "~/Content/{3}/Areas/{2}/Views/{1}/{0}.aspx",
                "~/Content/{3}/Areas/{2}/Views/Shared/{0}.aspx",
                "~/Areas/{2}/Views/{1}/{0}.aspx",
                "~/Areas/{2}/Views/Shared/{0}.aspx",

                // asCx
                "~/Content/{3}/Areas/{2}/Views/{1}/{0}.ascx",
                "~/Content/{3}/Areas/{2}/Views/Shared/{0}.ascx",
                "~/Areas/{2}/Views/{1}/{0}.ascx",
                "~/Areas/{2}/Views/Shared/{0}.ascx",
            };

            PartialViewLocationFormats = ViewLocationFormats;
            AreaPartialViewLocationFormats = PartialViewLocationFormats;
        }

        public new string[] AreaMasterLocationFormats { get; set; }
        public new string[] AreaPartialViewLocationFormats { get; set; }
        public new string[] AreaViewLocationFormats { get; set; }
        public new string[] ViewLocationFormats { get; set; }
        public new string[] MasterLocationFormats { get; set; }
        public new string[] PartialViewLocationFormats { get; set; }

        public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
        {
            base.AreaPartialViewLocationFormats = PrepareLocationFormats(controllerContext, this.AreaPartialViewLocationFormats);
            base.PartialViewLocationFormats = PrepareLocationFormats(controllerContext, this.PartialViewLocationFormats);

            return base.FindPartialView(controllerContext, partialViewName, useCache);
        }

        public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
        {
            base.AreaViewLocationFormats = PrepareLocationFormats(controllerContext, this.AreaViewLocationFormats);
            base.AreaMasterLocationFormats = PrepareLocationFormats(controllerContext, this.AreaMasterLocationFormats);
            base.ViewLocationFormats = PrepareLocationFormats(controllerContext, this.ViewLocationFormats);
            base.MasterLocationFormats = PrepareLocationFormats(controllerContext, this.MasterLocationFormats);

            if (string.IsNullOrEmpty(masterName))
                masterName = "Site";

            return base.FindView(controllerContext, viewName, masterName, useCache);
        }

        protected virtual string[] PrepareLocationFormats(ControllerContext controllerContext, string[] locationFormats)
        {
            if (locationFormats == null || locationFormats.Length == 0)
                return locationFormats;

            // get skin - this can be extracted to method
            string skin = (string)controllerContext.HttpContext.Session["SelectedSkin"] ?? "Default";

            List<string> locationFormatsPrepared = new List<string>();
            foreach (string locationFormat in locationFormats)
                locationFormatsPrepared.Add(string.Format(locationFormat, "{0}", "{1}", "{2}", skin));

            return locationFormatsPrepared.ToArray();
        }
    }
}
As you can see '*LocationFormats' properties are redeclared with new keyword, so we can easily switch between this. and base. implementations.
In constructor we initialize this. location formats. Unlike in MVC implementation, we have 4 placeholders. 0, 1, 2 is unchanged, 3 - for skin name. This was discussed above.
Also, in FindView method I check value of masterPage, and if it is null or empty string, specify it. It is done because when masterPage not specified ASP.NET will use default value from processed *.aspx file and skinning will not work.

In MasterLocationFormats list we can see '~/Content/{3}/Views/Shared/{0}.master' location. It is used for skinning, so we can have next folder tree:
~/Content
    /Default/Views/Shared/Site.master
    /RedTheme/Views/Shared/Site.master
    /BlueTheme/Views/Shared/Site.master
    /GreenTheme/Views/Shared/Site.master
Yes, I know, it is not an ASP.NET-way of skinning. There is no classic skin files. But this method have its own benefits - you can totally redesign master page. Similar folder tree used for views and partial views - so in each skin we can have duplicated tree of folders as in main site, but with skin-specific views.

In overriden FindPartialView and FindView methods we initialize the base.*LocationFormats by preformatted versions of this.*LocationFormats, and then call to the base methods. Now base methods can work fine, replacing only well-known '{0}', '{1}' and '{2}' placeholders. This trick is used to deceive our abstract friend - a VirtualPathProviderViewEngine class.

Method PrepareLocationFormats used to preformat our location formats by skin name. First of all we check if array is null or contains nothing - we just return it:
if (locationFormats == null || locationFormats.Length == 0)
return locationFormats;
Then we must prepare skin name.
string skin = (string)controllerContext.HttpContext.Session["skin"] ?? "Default";
In this simple example I decided not to use sophisticated logic for skinning. So I store skin name in the session.
You can extend this example by extracting skin-related logic to a method and implement more usable storage for skin selected by user.

The next thing I've done - create storage for our preformatted locations, and start iterating through each location.
List<string> locationFormatsPrepared = new List<string>();
foreach (string locationFormat in locationFormats)
    locationFormatsPrepared.Add(string.Format(locationFormat, "{0}", "{1}", "{2}", skin));
At the end we can convert list to an array and return it:
return locationFormatsPrepared.ToArray();
That's all about view engine - clear and simple (I guess so). In the next section I describe more extensible (in my opinion) way, how to add skin support.
Go to contents >

Extensible version of view engine


To make our view engine extensible we must extract preformatting logic, overridden FindView and FindPartialView methods and our implementation of *LocationFormats properties to the base class. Also we must add an ability to easily extend placeholders count, and link 'value-calculation' logic to related placeholder number. Here is a prototype of proposed class:

public delegate object PlaceholderValueFunc(ControllerContext controllerContext, string locationFormat, ref bool skipLocation);

public class PlaceholdersDictionary : Dictionary<int ,PlaceholderValueFunc>
{
}

public class WebFormExtensibleViewEngine : WebFormViewEngine
{
    public WebFormExtensibleViewEngine() : this(new PlaceholdersDictionary());
    public WebFormExtensibleViewEngine(PlaceholdersDictionary config) : base();

    public new string[] AreaMasterLocationFormats { get; set; }
    public new string[] AreaPartialViewLocationFormats { get; set; }
    public new string[] AreaViewLocationFormats { get; set; }
    public new string[] ViewLocationFormats { get; set; }
    public new string[] MasterLocationFormats { get; set; }
    public new string[] PartialViewLocationFormats { get; set; }
    protected PlaceholdersDictionary Config { get; set; }
    public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache);
    public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache);

    protected virtual PlaceholdersDictionary ValidateAndPrepareConfig(PlaceholdersDictionary config);
    protected virtual string[] PrepareLocationFormats(ControllerContext controllerContext, string[] locationFormats);
}

I've created a PlaceholderValueFunc delegate that represents a signature of method used to calculate value of the placeholder. This method returns an object and sends a boolean skipLocation argument by reference. skipLocation used to specify if we must skip processed location and does not perform any searching in it. Besides described delegate takes a controller context and location format string as an arguments.

To link a placeholder number with calculation method I decided to use specific dictionary - PlaceholdersDictionary. Integer key represents a placeholder number, and delegate as a value represents calculation logic.

public WebFormExtensibleViewEngine()
    : this(new PlaceholdersDictionary())
{
}

public WebFormExtensibleViewEngine(PlaceholdersDictionary config)
    : base()
{
    Config = config;
    ValidateAndPrepareConfig();
}

protected virtual void ValidateAndPrepareConfig()
{
    // Validate
    if (Config.ContainsKey(0) || Config.ContainsKey(1) || Config.ContainsKey(2))
        throw new InvalidOperationException("Placeholder index must be greater than 2. Because {0} - view name, {1} - controller name, {2} - area name.");

    // Prepare
    Config[0] = (ControllerContext controllerContext, string location, ref bool skipLocation) => "{0}";
    Config[1] = (ControllerContext controllerContext, string location, ref bool skipLocation) => "{1}";
    Config[2] = (ControllerContext controllerContext, string location, ref bool skipLocation) => "{2}";
}

Constructor takes an instance of the PlaceholdersDictionary class as an argument and calls to ValidateAndPrepareConfig method. It checks if {0}, {1}, {2} placeholders are used in dictionary, that is denied. And adds this three placeholders with anonymous delegates that simply return "{0}" for 0 placeholder, {1} for 1 placeholder, etc...

What remains unchanged from our SimpleWebFormViewEngine is *LocationFormats properties and overridden FindView, FindPartialView methods:

public new string[] AreaMasterLocationFormats { get; set; }
public new string[] AreaPartialViewLocationFormats { get; set; }
public new string[] AreaViewLocationFormats { get; set; }
public new string[] ViewLocationFormats { get; set; }
public new string[] MasterLocationFormats { get; set; }
public new string[] PartialViewLocationFormats { get; set; }

public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
    base.AreaPartialViewLocationFormats = PrepareLocationFormats(controllerContext, this.AreaPartialViewLocationFormats);
    base.PartialViewLocationFormats = PrepareLocationFormats(controllerContext, this.PartialViewLocationFormats);

    return base.FindPartialView(controllerContext, partialViewName, useCache);
}

public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
    base.AreaViewLocationFormats = PrepareLocationFormats(controllerContext, this.AreaViewLocationFormats);
    base.AreaMasterLocationFormats = PrepareLocationFormats(controllerContext, this.AreaMasterLocationFormats);
    base.ViewLocationFormats = PrepareLocationFormats(controllerContext, this.ViewLocationFormats);
    base.MasterLocationFormats = PrepareLocationFormats(controllerContext, this.MasterLocationFormats);

    if (string.IsNullOrEmpty(masterName))
        masterName = "Site";

    return base.FindView(controllerContext, viewName, masterName, useCache);
}

And the core of our functionality is a PrepareLocationFormats method, it was rewritten to use PlaceholdersDictionary:

protected virtual string[] PrepareLocationFormats(ControllerContext controllerContext, string[] locationFormats)
{
    if (locationFormats == null || locationFormats.Length == 0)
        return locationFormats;

    List<string> locationFormatsPrepared = new List<string>();

    foreach (string locationFormat in locationFormats)
    {
        object[] formatValues = new object[Config.Count];

        bool skipLocation = false;
        for (int i = 0; i < Config.Count; i++)
        {
            object formatValue = Config[i](controllerContext, locationFormat, ref skipLocation);

            if (skipLocation) break;

            formatValues[i] = formatValue;
        }
        if (skipLocation) continue;

        locationFormatsPrepared.Add(string.Format(locationFormat, formatValues));
    }

    return locationFormatsPrepared.ToArray();
}
First it checks if locationFormats array is null or contains no items and simply returns this array as result if so. In other case, it initializes locationFormatsPrepared local variable - a list that will be used to store locations that was prepared and ready to be used by standard MVC mechanism.

Then in foreach loop, for every location format it creates an array of values that will be used to replace placeholders. Each value calculated by invoking appropriate delegate. If delegate sets skipLocation flag to true, processing of location will be stopped and such location will be skipped.

When all values were calculated, it calls 'string.Format' method with location format and array of values as arguments.

At last, preformatted location added to the locationFormatsPrepared list. This list will be converted to an array and returned as a result when all of the locations will be processed.

Thats all, now our view engine is ready to be extended with needed functionality. And the next thing we must to do is to implement concrete class that support skinning:
public class WebFormSkinnedViewEngine : WebFormExtensibleViewEngine
{
    public WebFormSkinnedViewEngine()
    {
        // ...
    }

    protected virtual object GetSkinName(ControllerContext controllerContext, string locationFormat, ref bool skipLocation)
    {
        return (string)controllerContext.HttpContext.Session["SelectedSkin"] ?? "Default";
    }
}
This class has only one method: GetSkinName. It simply retrieves name of the skin from user's session, or returns 'Default' if session is empty:

The constructor of this class initialize placeholders dictionary by linking 3rd placeholder with GetSkinName method. Then it calls ValidateAndPrepareConfig method of the base class, this method was described above. And at last it initialize *LocationFormats with appropriate values.
public WebFormSkinnedViewEngine() : base()
{
    Config = new PlaceholdersDictionary()
    {
        { 3, GetSkinName }
    };
    ValidateAndPrepareConfig();

    // Our format
    // {0} - View name
    // {1} - Controller name
    // {2} - Area name
    // {3} - Skin name

    // MVC format
    // {0} - View name
    // {1} - Controller name
    // {2} - Area name
    MasterLocationFormats = new[] 
    {
        "~/Content/{3}/Views/{1}/{0}.master",
        "~/Content/{3}/Views/Shared/{0}.master",
        "~/Views/{1}/{0}.master",
        "~/Views/Shared/{0}.master",
    };

    AreaMasterLocationFormats = new[]
    {
        "~/Content/{3}/Areas/{2}/Views/{1}/{0}.master",
        "~/Content/{3}/Areas/{2}/Views/Shared/{0}.master",
        "~/Areas/{2}/Views/{1}/{0}.master",
        "~/Areas/{2}/Views/Shared/{0}.master",
    };

    ViewLocationFormats = new[] 
    { 
        // asPx
        "~/Content/{3}/Views/{1}/{0}.aspx",
        "~/Content/{3}/Views/Shared/{0}.aspx",
        "~/Views/{1}/{0}.aspx",
        "~/Views/Shared/{0}.aspx",

        // asCx
        "~/Content/{3}/Views/{1}/{0}.ascx",
        "~/Content/{3}/Views/Shared/{0}.ascx",
        "~/Views/{1}/{0}.ascx",
        "~/Views/Shared/{0}.ascx",
    };

    AreaViewLocationFormats = new[] 
    { 
        // asPx
        "~/Content/{3}/Areas/{2}/Views/{1}/{0}.aspx",
        "~/Content/{3}/Areas/{2}/Views/Shared/{0}.aspx",
        "~/Areas/{2}/Views/{1}/{0}.aspx",
        "~/Areas/{2}/Views/Shared/{0}.aspx",

        // asCx
        "~/Content/{3}/Areas/{2}/Views/{1}/{0}.ascx",
        "~/Content/{3}/Areas/{2}/Views/Shared/{0}.ascx",
        "~/Areas/{2}/Views/{1}/{0}.ascx",
        "~/Areas/{2}/Views/Shared/{0}.ascx",
    };

    PartialViewLocationFormats = ViewLocationFormats;
    AreaPartialViewLocationFormats = PartialViewLocationFormats;
}
One important thing is that our extended formats going before standard MVC formats. So, searching will be done in Content folder first.

Thats all, functionality are implemented. You can register our view engine in Global.asax.cs using next code:
WebFormSkinnedViewEngine viewEngine = new WebFormSkinnedViewEngine();
ViewEngines.Engines.Insert(0, viewEngine);
Sample application for this post you can download here: ViewEnginesExtended.zip
Go to contents >

Good luck and happy coding!


Shout it

kick it on DotNetKicks.com

3.13.2010

Requirements specification and RFC

Many of the people when write requirements specification widely use words such as "SHALL", "SHOULD", "MAY" etc... However usage of this words is chaotic, not well-defined, and such technical writer can't describe why he\she use this word and not another.
It is decreases documentation readability because can be treated incorrectly by one stakeholder, when another can understand it correctly.

So it is critical to have some standard for usage of such words, and there is such a standard in RFC documents. It is RFC 2119 - Key words for use in RFCs to Indicate Requirement Levels.

Hope this info helps you to write more strict requirement documents!



Shout it

kick it on DotNetKicks.com

C# renderer for jQuery DataTables

DataTables is an excellent plugin for the jQuery javascript library to extend HTML table with advanced functionality. I like to use it in projects, but (may be it is strange for someone) I don't like lot's of raw JavaScript code in aspx\ascx pages.
Some code I extract from page and write it using Script# - another excellent tool. But some code needs to be on a page for some reasons. In such cases I prefer to use a "renderer" - set of strong typed C# classes that renders needed JavaScript to the page.
One of such renderer I wrote a time ago for DataTables. It is compatible with DataTables v1.5.6-1.6.2 and realizes all possible DataTables' properties, callbacks, options, features, localization, etc... Here is a simple usage example:
<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
    <%
    string tableId = "Demo1_Table";
    string tableVariable = "Demo1_DataTable";
   
    Html.DataTable()
        .Config(new DataTableConfig()
        {
            TableId = tableId,
            TableCss = "display",
            DataTableVariableName = tableVariable,
            CreateNew = false,
            RenderScriptTags = true,
            RenderJQueryReady = true
        })
        .Options(new DataTableOptions()
        {
            PaginationType = PaginationType.full_numbers
        })
       .Render();
    %>
           
    <table id="Demo1_Table" cellpadding="0" cellspacing="0" border="0" class="display">
     <thead> 
      <tr> 
       <th>Rendering engine</th> 
       <th>Browser</th> 
       <th>Platform(s)</th> 
       <th>Engine version</th> 
       <th>CSS grade</th> 
      </tr> 
     </thead> 
     <tbody> 
      <tr class="gradeX"> 
       <td>Trident</td> 
       <td>Internet
         Explorer 4.0</td> 
       <td>Win 95+</td> 
       <td class="center">4</td> 
       <td class="center">X</td> 
      </tr> 
      <tr class="gradeC"> 
       <td>Trident</td> 
       <td>Internet
         Explorer 5.0</td> 
       <td>Win 95+</td> 
       <td class="center">5</td> 
       <td class="center">C</td> 
      </tr>
     </tbody> 
     <tfoot> 
      <tr> 
       <th>Rendering engine</th> 
       <th>Browser</th> 
       <th>Platform(s)</th> 
       <th>Engine version</th> 
       <th>CSS grade</th> 
      </tr> 
     </tfoot>
    </table>
</asp:Content>
As you can see, there are no JavaScript, only strong-typed C# code.

As a bonus, I've implemented simplified mechanism for localizing DataTables, an example how to use it you can found in example project (see download link at the end of the article). Generally speaking, all you need is a ResourceManager instance, ResourceKeyFormat specified with {0} placeholder, and list of resources named correctly. For example if ResourceKeyFormat = "DataTable_{0}", resource for DataTableLocalization.Search that represents actual resource "oLanguage.sSearch" should be named as "DataTable_Search". That's all! Automatic localizing will do all the work itself.

You can download sample project with DataTables renderer here: DataTablesRenderer.zip.


Shout it

kick it on DotNetKicks.com

2.14.2010

Tips on using Virtual Directories in ASP.NET, MVC, HttpSimulator, SiteMap

Imagine you develop a web-site. You register IIS site to test it, seems like all is ok. You deploy it to customer, and... oh no! where is my images, styles, scripts? and why site map causes an exception?!
It is common situation when site developed and tested on IIS web-site root, and deployed into Virtual Directory. Here I want to describe some tips to avoid such situations.

1. Do not use relative paths.
It is a first and biggest pain. Convert relative paths to absolute using:
VirtualPathUtility.ToAbsolute(virtualPath);
2. Add trailing slash to default path.
When you use MVC, you can have an exception in your site map (I use MvcSiteMap provider) when you request your site without trailing slash. For example, this request may works fine:
http://siteroot/testvd/
but this one cause an exception when you try to access SiteMap.CurrentNode:
http://siteroot/testvd
Why? Hmm... it is an interesting question, I saw such situations on IIS 5,6 configured to work with MVC. If not to go very deep in ASP.NET - because registered path in site map contains trailing slash, and there is no path without slash. It is standard behavior of ASP.NET. In WebForms (not MVC) site, when you try to access such path, IIS redirects you to path with trailing slash automatically. I think, since here IIS send all requests directly to ASP.NET (because of registered wildcard or .* extension used for MVC works fine), it can't redirect user to page with trailing slash.
To fix this you can add next code in your Global.asax.cs:
/// <summary>
/// Starts each request
/// </summary>
protected void Application_BeginRequest()
{
        if (Request.ApplicationPath == Request.Path)
                Context.RewritePath(VirtualPathUtility.AppendTrailingSlash(Request.ApplicationPath), true);
}
3. Testing site in VD using HttpSimulator, developed by Phil Haack, can cause unexpected result.
Here is pseudo-code that can cause incorrect results:
string path = string.Empty;
using (new HttpSimulator("/TestVD").SimulateRequest())
{
        path = VirtualPathUtility.ToAbsolute("/images/Test.png");
}

// expected result: path = "/TestVD/images/Test.png";
// actual result: path = "/TestVDimages/Test.png";
To correct this you must change code of method SetHttpRuntimeInternals in HttpSimulator.cs (changed lines are highlighted):
void SetHttpRuntimeInternals()
{
        // ...
        // set app virtual path property value
        string vpathTypeName = "System.Web.VirtualPath, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a";
        string appPath = VirtualPathUtility.AppendTrailingSlash(ApplicationPath);
        object virtualPath = ReflectionHelper.Instantiate(vpathTypeName, new Type[] { typeof(string) }, new object[] { appPath });
        ReflectionHelper.SetPrivateInstanceFieldValue("_appDomainAppVPath", runtime, virtualPath);
        // ...
}

Shout it

kick it on DotNetKicks.com

1.01.2010

String templates revisited

Sometimes I encounter a task - create some little template, that user can easily configure. Yeah, there are many powerful template engines such as
  1. NVelocity(Caslte project)
  2. Brail(Caslte project)
  3. NHaml
  4. Spark
  5. StringTemplate .NET
  6. etc...
But, what if you don't want reference additional assemblies, and you need only simple basic functionality? For this time I saw 2 solutions:
  1. Use string format placeholders {0}, {1}, etc - it is good solution if you have only 2-3 placeholders, do not need descriptiveness of them, and (may be) need to use specific formatters such as {0:d}, {0:X} etc:
    string result = string.Format("{0} + {1} = {2}", 4, 5, 6);
    
  2. Use replacement, handwritten placeholders: #ID#, #NAME#, etc - they are more descriptive, but without formatters:
    string result = "#Arg1# + #Arg2# = #Res#"
                    .Replace("#Arg1#", 4)
                    .Replace("#Arg2#", 5)
                    .Replace("#Res#", 9);
    
But what if we will use templates similar to ASP.NET Ajax 4.0 - {Id}, {UserName}? They are more descriptive, reflects names of properties of the some object. Using reflection we can easily use names of the properties as placeholders. I wrote extension methods for StringBuilder and String for this purpose. Here is one for a StringBuilder:

public static StringBuilder AppendFormatEx(this StringBuilder self, string format, object args, MemberKind memberKind)
{
    MemberInfo[] members;

    switch (memberKind)
    {
        case MemberKind.Property:
            members = args.GetType().GetProperties();
            break;
        case MemberKind.Field:
            members = args.GetType().GetFields();
            break;
        default:
            throw new ArgumentOutOfRangeException("memberKind");
    }

    foreach (MemberInfo member in members)
    {
        string placeholder = string.Format("{{{0}}}", member.Name);

        object value;
        if (member is FieldInfo)
            value = (member as FieldInfo).GetValue(args);
        else if (member is PropertyInfo)
            value = (member as PropertyInfo).GetValue(args, null);
        else
            throw new InvalidOperationException("Only fields and properties are supported. But you use " + member.GetType().Name);

        format = format.Replace(placeholder, Convert.ToString(value));
    }

    self.Append(format);

    return self;
}

It is a simplified for readability main method that contains all the functionality. First of all, we take an array of public members from args, kind of members (property or field) you can specify by argument memberKind. Then for each extracted member we create name of the placeholder, get the value of the member and replace placeholder by value. Thats all.. as simple as possible.

An overloaded version of this method working only with public properties:
public static StringBuilder AppendFormatEx(this StringBuilder self, string format, object args)
{
    return AppendFormatEx(self, format, args, MemberKind.Property);
}

For String I have an overloaded extension method too:
public static string Format(this string format, object args, MemberKind memberKind)
{
    StringBuilder builder = new StringBuilder();
    builder.AppendFormatEx(format, args, memberKind);
    return builder.ToString();
}
It is uses a StringBuilder's method to format a string. An instance of the string represents a format. And overloaded version working only with public properties:
public static string Format(this string format, object args)
{
    return Format(format, args, MemberKind.Property);
}
Now I show you simple test console application, that gives you an idea how this works in code:
using System;
using System.Text;

namespace HennadiyKurabko.StringFormattersEx
{
    class Program
    {
        static void Main(string[] args)
        {
            // Arrange
            string format = "{Name}:\t\t{Age} year(s)\t\t{Login}\r\n";

            object[] users = new object[] {
                new { Name = "Jane Siemens", Age = 25, Login = "janes" },
                new { Name = "Tom Clancy", Age = 25, Login = "tomc" },
                new { Name = "Sofi Payne", Age = 25, Login = "sofip" }
            };

            // Act [String]
            Console.WriteLine("String extensions test");
            Console.WriteLine();

            foreach (var user in users)
            {
                Console.Write(format.Format(user));
            }

            Console.WriteLine("-----------------------------");

            // Act [StringBuilder]
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.AppendLine("StringBuilder extensions test");
            stringBuilder.AppendLine();

            foreach (var user in users)
            {
                stringBuilder.AppendFormatEx(format, user);
            }

            stringBuilder.AppendLine("-----------------------------");

            Console.WriteLine(stringBuilder.ToString());
        }
    }
}

You see, that format string contains placeholders with property names: {Name}, {Age}, {Login}. It is far more descriptive as {0}, {1}. But in that case you cannot use specific formatters. So, it is your choice what of the methods to use. Hope this helps you.

An example for this article you can download here: StringFormattersEx.zip

Good luck and happy coding!


Shout it

kick it on DotNetKicks.com

12.20.2009

MVC 2.0 Client validation exposed

There are many good articles about new validation model in MVC 2.0, for example an excellent article ASP.NET MVC 2 Custom Validation on Phil Haack's blog.
Yeah, data annotation and rules in model is cool. But I have found no info about how to add client validation rules from code, it can be useful in many cases. So, I go to Reflector and try to find some way to do this.
After a hour of browsing, I found that there is a class FormContext. Here is a prototype of this class:

    public class FormContext
    {
        public FormContext();

        public bool ClientValidationEnabled { get; set; }
        public string ClientValidationFunction { get; set; }
        public object ClientValidationState { get; set; }
        public IDictionary<string, FieldValidationMetadata> FieldValidators { get; }
        public string FormId { get; set; }
        public string ValidationSummaryId { get; set; }

        public string GetJsonValidationMetadata();
        public FieldValidationMetadata GetValidationMetadataForField(string fieldName);
        public FieldValidationMetadata GetValidationMetadataForField(string fieldName, bool createIfNotFound);
    }

This class available only between Html.BeginForm() and Html.EndForm() methods, in other words - only inside the form. You can access it from view, using inline code, as listed below:

<% ViewContext.FormContext %>

In the class signature there is an overloaded method GetValidationMetadataForField, it returns instance of the FieldValidationMetadata class for the specified field name of the form. FieldValidationMetadata contains ValidationRules property that we can change - add new rules. Cool! Here is a prototype of this class:

    public class FieldValidationMetadata
    {
        public FieldValidationMetadata();

        public string FieldName { get; set; }
        public bool ReplaceValidationMessageContents { get; set; }
        public string ValidationMessageId { get; set; }
        public ICollection<ModelClientValidationRule> ValidationRules { get; }
    }

As you can see from signature, property ValidationRules is a collection of ModelClientValidationRule class. It is a base class for all validation rules.
So, to say programmatically that field "price" is a required field, we must do the next:

<% using (Html.BeginForm()) { %>
// ............

<%
       FieldValidationMetadata metadata = ViewContext.FormContext.GetValidationMetadataForField("price", true);
       metadata.ValidationRules.Add(new ModelClientValidationRequiredRule("Please, specify the price."));
%>

// ............
<% } %>

Good, but not excellent. To do this with fluent syntax, and to emphasize that it is can be done only inside a form I had created extension method for MvcForm:

using System;
using System.Collections.ObjectModel;
using System.Web.Mvc;
using System.Web.Mvc.Html;

namespace HennadiyKurabko.Web.Mvc.Html.Extensions
{
    /// <summary>
    /// Represents support for adding client validation rules.
    /// </summary>
    public static class MvcFormExtensions
    {
        /// <summary>
        /// Adds the client validation rules to specified fields.
        /// </summary>
        /// <param name="form">The MVC form.</param>
        /// <param name="formContext">Context of the form.</param>
        /// <param name="validationRuleDescriptors">Descriptors of fields and related validation rules.</param>
        /// <returns>An <see cref="MvcForm"/> (for fluent syntax).</returns>
        /// <exception cref="System.ArgumentNullException"><paramref name="form"/>, <paramref name="formContext"/> or <paramref name="validationRuleDescriptors"/> is null.</exception>
        public static MvcForm AddClientValidationRules(this MvcForm form, FormContext formContext, ValidationRuleDescriptorCollection validationRuleDescriptors)
        {
            foreach (ValidationRuleDescriptor descriptor in validationRuleDescriptors)
            {
                if (descriptor.Rules == null || descriptor.Rules.Count == 0) continue;

                FieldValidationMetadata metadata = formContext.GetValidationMetadataForField(descriptor.FieldName, descriptor.CreateIfNotFound);

                foreach (ModelClientValidationRule rule in descriptor.Rules)
                    metadata.ValidationRules.Add(rule);
            }

            return form;
        }
    }

    /// <summary>
    /// Encapsulates the name of the field and related client validation rules.
    /// </summary>
    public class ValidationRuleDescriptor
    {
        #region Fields

        private string _fieldName;
        private ModelClientValidationRuleCollection _rules;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="ValidationRuleDescriptor"/> class.
        /// </summary>
        public ValidationRuleDescriptor()
        {
            CreateIfNotFound = true;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ValidationRuleDescriptor"/> class.
        /// </summary>
        /// <param name="fieldName">Name of the associated field.</param>
        public ValidationRuleDescriptor(string fieldName)
            : this()
        {
            FieldName = fieldName;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ValidationRuleDescriptor"/> class.
        /// </summary>
        /// <param name="fieldName">Name of the associated field.</param>
        /// <param name="createIfNotFound">true to create a validation value if one is not found; otherwise, false.</param>
        public ValidationRuleDescriptor(string fieldName, bool createIfNotFound)
            : this(fieldName)
        {
            CreateIfNotFound = createIfNotFound;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ValidationRuleDescriptor"/> class.
        /// </summary>
        /// <param name="fieldName">Name of the associated field.</param>
        /// <param name="rules">Client validation rules.</param>
        public ValidationRuleDescriptor(string fieldName, ModelClientValidationRuleCollection rules)
            : this(fieldName)
        {
            Rules = rules;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ValidationRuleDescriptor"/> class.
        /// </summary>
        /// <param name="fieldName">Name of the associated field.</param>
        /// <param name="createIfNotFound">true to create a validation value if one is not found; otherwise, false.</param>
        /// <param name="rules">Client validation rules.</param>
        public ValidationRuleDescriptor(string fieldName, bool createIfNotFound, ModelClientValidationRuleCollection rules)
            : this(fieldName, createIfNotFound)
        {
            Rules = rules;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets a name of the field.
        /// </summary>
        public string FieldName
        {
            get { return _fieldName; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("FieldName");
                if (string.IsNullOrEmpty(value))
                    throw new ArgumentException("Cannot be an empty string.", "FieldName");

                _fieldName = value;
            }
        }

        /// <summary>
        /// Gets or sets a value that indicates what to do if the validation value is not found.
        /// </summary>
        public bool CreateIfNotFound { get; set; }

        /// <summary>
        /// Gets or sets a collection of client validation rules.
        /// </summary>
        public ModelClientValidationRuleCollection Rules
        {
            get { return _rules; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("Rules");

                _rules = value;
            }
        }

        #endregion
    }

    /// <summary>
    /// A collection of <see cref="ValidationRuleDescriptor"/> instances representing fields and related validation rules.
    /// </summary>
    [Serializable]
    public class ValidationRuleDescriptorCollection : Collection<ValidationRuleDescriptor>
    {
    }

    /// <summary>
    /// A collection of <see cref="ModelClientValidationRule"/> instances representing set of client validation rules.
    /// </summary>
    [Serializable]
    public class ModelClientValidationRuleCollection : Collection<ModelClientValidationRule>
    {
    }
}

So, to add validation rule you can write next code:

<% using (Html.BeginForm().AddClientValidationRules(ViewContext.FormContext, new ValidationRuleDescriptorCollection()
   {
       new ValidationRuleDescriptor("price", true, new ModelClientValidationRuleCollection() {
           new ModelClientValidationRequiredRule("It is a custom rule."),
           new ModelClientValidationRangeRule("Value must be greater than zero, and less than or equal to 10.", 0, 10)
       })
   }))
   { %>

// ...............
<% } %>

An example you can download here: ManualClientValidation.zip

Good luck, and happy coding!

Shout it

kick it on DotNetKicks.com