ASP.NET MVC Linked Dropdown

Here's a solution for linked (or cascading) dropdowns in ASP.NET MVC without server interaction. Showing the correct list in the second dropdown is achieved by hiding the other list items.

The parent dropdown doesn't change; the code below is only used for the child dropdown.

LinkedSelectListItem

This is a SelectListItem with an extra property: LinkValue. If the SelectedValue for the parent dropdown is 2, then all child items with a LinkValue of 2 are shown.


	public class LinkedSelectListItem
	{
		public string Text { get; set; }
		public string Value { get; set; }
		public string LinkValue { get; set; }
		public bool Selected { get; set; }
	}

				

LinkedSelectList

This is mostly copied from the SelectList (sources are downloadable at CodePlex). It is modified to use LinkedSelectListItem and to support the LinkValue (see dataLinkedValueField).


	public class LinkedSelectList : IEnumerable<LinkedSelectListItem>
	{

		public string DataTextField { get; private set; }
		public string DataValueField { get; private set; }
		public string DataLinkedValueField { get; private set; }

		public IEnumerable Items { get; private set; }
		public IEnumerable SelectedValues { get; private set; }

		public LinkedSelectList(IEnumerable items, string dataValueField, string dataTextField, string dataLinkedValueField, IEnumerable selectedValues)
		{
			if (items == null)
			{
				throw new ArgumentNullException("items");
			}

			Items = items;
			DataValueField = dataValueField;
			DataTextField = dataTextField;
			DataLinkedValueField = dataLinkedValueField;
			SelectedValues = selectedValues;
		}

		public virtual IEnumerator<LinkedSelectListItem> GetEnumerator()
		{
			return GetListItems().GetEnumerator();
		}

		internal IList<LinkedSelectListItem> GetListItems()
		{
			return GetListItemsWithValueField();
		}

		private IList<LinkedSelectListItem> GetListItemsWithValueField()
		{
			HashSet<string> selectedValues = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			if (SelectedValues != null)
			{
				selectedValues.UnionWith(from object value in SelectedValues select Convert.ToString(value, CultureInfo.CurrentCulture));
			}

			var listItems = from object item in Items
							let value = Eval(item, DataValueField)
							select new LinkedSelectListItem
							{
								Value = value,
								Text = Eval(item, DataTextField),
								LinkValue = Eval(item, DataLinkedValueField),
								Selected = selectedValues.Contains(value)
							};
			return listItems.ToList();
		}

		private static string Eval(object container, string expression)
		{
			object value = container;
			if (!String.IsNullOrEmpty(expression))
			{
				value = DataBinder.Eval(container, expression);
			}
			return Convert.ToString(value, CultureInfo.CurrentCulture);
		}

		#region IEnumerable Members
		
		IEnumerator IEnumerable.GetEnumerator()
		{
			return GetEnumerator();
		}
		
		#endregion
	}

				

LinkedDropdownHelper

Here's the helper, which uses the above classes and writes out a dropdown list and a bit of (jQuery-dependent) javascript.


	public static class LinkedDropdownHelper
	{

		public static MvcHtmlString LinkedDropdownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string parent, IEnumerable<LinkedSelectListItem> selectList)
		{
			string propertyName = ((MemberExpression)expression.Body).Member.Name;

			TagBuilder select = new TagBuilder("select");
			select.Attributes.Add("id", propertyName);
			select.Attributes.Add("name", propertyName);
			select.Attributes.Add("class", "linked-dropdown");
			select.Attributes.Add("data-parent", parent);

			foreach (var item in selectList)
			{
				TagBuilder option = new TagBuilder("option");
				option.InnerHtml = item.Text;
				option.Attributes.Add("value", item.Value);
				option.Attributes.Add("data-parentvalue", item.LinkValue);

				if (item.Selected)
				{
					option.Attributes.Add("selected", "selected");
				}
				select.InnerHtml += option.ToString(TagRenderMode.Normal);
			}

			string script = @"<script type='text/javascript'>$(document).ready(function () {
								$('#' + $('.linked-dropdown').attr('data-parent')).unbind('change.linked-dropdown');

								$('.linked-dropdown').each(function () {
									var $linked = $(this);

									$('#' + $linked.attr('data-parent')).bind('change.linked-dropdown', function () {
										var parentid = $(this).find('option:selected').val();

										$linked.find('option').hide();
										$linked.find('option[data-parentvalue=' + parentid + ']').show();
										$linked.find('option:visible:first').attr('selected', 'selected');
									});

									$('#' + $linked.attr('data-parent')).change();
								});
							});</script>";

			return MvcHtmlString.Create(script + select.ToString(TagRenderMode.Normal));
		}
	}
					
				

How to use

					
	<h3>@Html.LabelFor(model => model.Project)</h3>
	<p>
		@Html.DropDownListFor(model => model.Project, new SelectList(((List<Models.Project>)ViewData["projects"]), "Id", "Name", null))
		@Html.ValidationMessageFor(model => model.Project)
	</p>

	<h3>@Html.LabelFor(model => model.Component)</h3>
	<p>
		@Html.LinkedDropdownListFor(model => model.Component, "Project", new LinkedSelectList(((List<Models.Component>)ViewData["components"]), "Id", "Name", "ProjectId", null))
		@Html.ValidationMessageFor(model => model.TaskType)
	</p>

				

See the demo in my sandbox.