7.14.2010

MVC 2 DropDownListFor - Any troubles? - Part 2

Hello again. Today I want to talk about select list with optgroups. So, I need such a list. MVC has no support for it. What to do?

After a little browsing I decide wrote my own solution. It is based on code from fixed version of MVC DropDownList (you can read about it in Part 1).

Main features of the list:
of course - support of groups of options
you can use it with options and with groups of options in any order, iow first item can be option, then - option group, and at the end, again - option.
Lets go to quick code-review: option and group of options (SelectListItem3Option, SelectListItem3OptionGroup) inherited from base class SelectListItem3. So all collections you can create as collections of this base class and store options and optgroups simultaneously. All extension methods has suffix "3". List of extension methods are the same as in MVC.

In general - thats all. Enjoy yourself! All required code listed below:

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 SelectExtensions3
    {
        /// <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 DropDownList3(this HtmlHelper htmlHelper, string name)
        {
            return htmlHelper.DropDownList3(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.SelectListItem3" /> 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 DropDownList3(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem3> selectList)
        {
            return htmlHelper.DropDownList3(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 DropDownList3(this HtmlHelper htmlHelper, string name, string optionLabel)
        {
            return htmlHelper.DropDownList3(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.SelectListItem3" /> 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 DropDownList3(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem3> selectList, IDictionary<string, object> htmlAttributes)
        {
            return htmlHelper.DropDownList3(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.SelectListItem3" /> 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 DropDownList3(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem3> selectList, object htmlAttributes)
        {
            return htmlHelper.DropDownList3(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.SelectListItem3" /> 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 DropDownList3(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem3> selectList, string optionLabel)
        {
            return htmlHelper.DropDownList3(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.SelectListItem3" /> 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 DropDownList3(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem3> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
        {
            return DropDownList3Helper(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.SelectListItem3" /> 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 DropDownList3(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem3> selectList, string optionLabel, object htmlAttributes)
        {
            return htmlHelper.DropDownList3(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.SelectListItem3" /> 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 DropDownList3For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem3> selectList)
        {
            return htmlHelper.DropDownList3For<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.SelectListItem3" /> 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 DropDownList3For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem3> selectList, IDictionary<string, object> htmlAttributes)
        {
            return htmlHelper.DropDownList3For<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.SelectListItem3" /> 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 DropDownList3For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem3> selectList, object htmlAttributes)
        {
            return htmlHelper.DropDownList3For<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.SelectListItem3" /> 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 DropDownList3For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem3> selectList, string optionLabel)
        {
            return htmlHelper.DropDownList3For<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.SelectListItem3" /> 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 DropDownList3For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem3> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
        {
            if (expression == null)
            {
                throw new ArgumentNullException("expression");
            }
            return DropDownList3Helper(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.SelectListItem3" /> 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 DropDownList3For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem3> selectList, string optionLabel, object htmlAttributes)
        {
            return htmlHelper.DropDownList3For<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 ListBox3(this HtmlHelper htmlHelper, string name)
        {
            return htmlHelper.ListBox3(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.SelectListItem3Option" /> 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 ListBox3(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem3Option> selectList)
        {
            return htmlHelper.ListBox3(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.SelectListItem3Option" /> 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 ListBox3(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem3Option> selectList, IDictionary<string, object> htmlAttributes)
        {
            return ListBox3Helper(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.SelectListItem3Option" /> 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 ListBox3(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem3Option> selectList, object htmlAttributes)
        {
            return htmlHelper.ListBox3(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.SelectListItem3Option" /> 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 ListBox3For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem3Option> selectList)
        {
            return htmlHelper.ListBox3For<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.SelectListItem3Option" /> 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 ListBox3For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem3Option> selectList, IDictionary<string, object> htmlAttributes)
        {
            if (expression == null)
            {
                throw new ArgumentNullException("expression");
            }
            return ListBox3Helper(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.SelectListItem3Option" /> 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 ListBox3For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem3Option> selectList, object htmlAttributes)
        {
            return htmlHelper.ListBox3For<TModel, TProperty>(expression, selectList, ((IDictionary<string, object>)new RouteValueDictionary(htmlAttributes)));
        }

        private static MvcHtmlString ListBox3Helper(HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem3Option> selectList, IDictionary<string, object> htmlAttributes)
        {
            return htmlHelper.SelectInternal3(null, name, selectList.Cast<SelectListItem3>(), true, htmlAttributes);
        }

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

        private static MvcHtmlString SelectInternal3(this HtmlHelper htmlHelper, string optionLabel, string name, IEnumerable<SelectListItem3> selectList, bool allowMultiple, IDictionary<string, object> htmlAttributes)
        {
            string shortName = name;
            string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(shortName);

            if (string.IsNullOrEmpty(fullName))
                throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name");

            bool selectListRestored = false;

            if (selectList == null)
            {
                selectList = htmlHelper.GetSelectData3(shortName);
                selectListRestored = true;
            }

            object selectListModelState = allowMultiple ? htmlHelper.GetModelStateValue(fullName, typeof(string[])) : htmlHelper.GetModelStateValue(fullName, typeof(string));
            if (!selectListRestored && selectListModelState == null)
                selectListModelState = htmlHelper.ViewData.Eval(shortName);

            if (selectListModelState != null)
            {
                IEnumerable source = allowMultiple ? selectListModelState as IEnumerable : (IEnumerable)new object[] { selectListModelState };

                HashSet<string> set = new HashSet<string>(
                    source
                        .Cast<object>()
                        .Select<object, string>(x => Convert.ToString(x, CultureInfo.CurrentCulture)),
                    StringComparer.OrdinalIgnoreCase);

                List<SelectListItem3> list = new List<SelectListItem3>();
                foreach (SelectListItem3 item in selectList)
                {
                    SelectListItem3OptionGroup group = item as SelectListItem3OptionGroup;

                    if (group == null)
                    {
                        SelectListItem3Option option = item as SelectListItem3Option;
                        option.Selected = option.Value != null ? set.Contains(option.Value) : set.Contains(option.Text);
                        list.Add(option);
                    }
                    else
                    {
                        foreach (SelectListItem3 item2 in group.Items)
                        {
                            SelectListItem3Option option = item2 as SelectListItem3Option;
                            option.Selected = option.Value != null ? set.Contains(option.Value) : set.Contains(option.Text);
                        }

                        list.Add(group);
                    }
                }
                selectList = list;
            }
            StringBuilder builder = new StringBuilder();

            if (optionLabel != null)
            {
                SelectListItem3Option defaultItem = new SelectListItem3Option();
                defaultItem.Text = optionLabel;
                defaultItem.Value = string.Empty;
                defaultItem.Selected = false;
                builder.AppendLine(OptionToHtml(defaultItem));
            }

            foreach (SelectListItem3 item in selectList)
            {
                SelectListItem3OptionGroup group = item as SelectListItem3OptionGroup;
                SelectListItem3Option option = item as SelectListItem3Option;

                if (group == null && option != null)
                    builder.AppendLine(OptionToHtml(item as SelectListItem3Option));
                else if (group != null && option == null)
                    builder.AppendLine(OptionGroupToHtml(group));
                else
                    throw new InvalidOperationException("Incorrect data type.");
            }

            TagBuilder builderSelect = new TagBuilder("select");
            builderSelect.InnerHtml = builder.ToString();
            builderSelect.MergeAttributes<string, object>(htmlAttributes);
            builderSelect.MergeAttribute("name", fullName, true);
            builderSelect.GenerateId(fullName);

            if (allowMultiple)
                builderSelect.MergeAttribute("multiple", "multiple");

            ModelState state;
            if (htmlHelper.ViewData.ModelState.TryGetValue(fullName, out state) && (state.Errors.Count > 0))
                builderSelect.AddCssClass(HtmlHelper.ValidationInputCssClassName);

            return builderSelect.ToMvcHtmlString(TagRenderMode.Normal);
        }

        private static IEnumerable<SelectListItem3> GetSelectData3(this HtmlHelper htmlHelper, string name)
        {
            object data = null;
            if (htmlHelper.ViewData != null)
                data = htmlHelper.ViewData.Eval(name);

            if (data == null)
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, MvcResources.HtmlHelper_MissingSelectData, new object[] { name, "IEnumerable<SelectListItem3>" }));

            IEnumerable<SelectListItem3> enumerable = data as IEnumerable<SelectListItem3>;
            if (enumerable == null)
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, MvcResources.HtmlHelper_WrongSelectDataType, new object[] { name, data.GetType().FullName, "IEnumerable<SelectListItem3>" }));

            return enumerable;
        }

        private static string OptionGroupToHtml(SelectListItem3OptionGroup group)
        {
            TagBuilder builder = new TagBuilder("optgroup");
            builder.Attributes["label"] = HttpUtility.HtmlAttributeEncode(group.Text);

            if (group.Items != null && group.Items.Count() > 0)
            {
                StringBuilder items = new StringBuilder();
                foreach (SelectListItem3Option item in group.Items)
                    items.AppendLine(OptionToHtml(item));

                builder.InnerHtml = items.ToString();
            }

            return builder.ToString();
        }

        private static string OptionToHtml(SelectListItem3Option item)
        {
            TagBuilder builder = new TagBuilder("option");
            builder.InnerHtml = HttpUtility.HtmlEncode(item.Text);

            if (item.Value != null)
                builder.Attributes["value"] = item.Value;

            if (item.Selected)
                builder.Attributes["selected"] = "selected";

            return builder.ToString(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
    }

    /// <summary>Represents the base select item.</summary>
    public abstract class SelectListItem3
    {
        /// <summary>Gets or sets the text of the item.</summary>
        /// <returns>The text.</returns>
        public string Text { get; set; }
    }

    /// <summary>Represents the select optgroup.</summary>
    public class SelectListItem3OptionGroup : SelectListItem3
    {
        /// <summary>Gets or sets items of the group.</summary>
        public IEnumerable<SelectListItem3Option> Items { get; set; }
    }

    /// <summary>Represents the select option.</summary>
    public class SelectListItem3Option : SelectListItem3
    {
        /// <summary>Gets or sets a value that indicates whether this instance is selected.</summary>
        /// <returns>true if the item is selected; otherwise, false.</returns>
        public bool Selected { get; set; }

        /// <summary>Gets or sets the value of the selected item.</summary>
        /// <returns>The value.</returns>
        public string Value { get; set; }
    }
}

See you later!


Shout it

kick it on DotNetKicks.com

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