miso_soup3 Blog

主に ASP.NET 関連について書いています。

Enum からドロップダウンリストを生成する

前回 検証付きドロップダウンリストを実装する では、ドロップダウンリストの実装方法について書きました。その続きになります。

f:id:miso_soup3:20131205034230p:plain

現在 ASP.NET MVC 5 には、Enum からドロップダウンリストを生成してくれる HTML ヘルパーがありません。
(メタ情報を見て適切な Html ヘルパーを選択してくれる @Html.EditorFor でも対応していません。)
なので、Enum からドロップダウンリストを生成するには、以下の方法が挙げられます。

  • 1. 自分で SelectList を組み立てる
  • 2. 自分で Enum から SelectList を組み立てるヘルパーを作り、それを使う
ASP.NET MVC 5.1 !

が、しばやんさんの記事 ASP.NET MVC 5.1 の新機能を Nightly Build で試してみた によると、ASP.NET MVC 5.1 より Enum からドロップダウンリストを生成する HTML ヘルパーが搭載されるそうです。

→ (追記) ASP.NET MVC 5.1 がリリースされ、@Html.EnumDropDownListFor() でできるようになりました。
ASP.NET MVC 5.1 と ASP.NET Web API 2.1 の内容 - miso_soup3

ASP.NET MVC 5.1 がリリースされるまでは、先ほどの 1 か 2 で実装しなければなりません。
が、せっかくのオープンソースなので、2 の方法は ASP.NET MVC 5.1 に搭載されるという新しい HTML ヘルパーを拝借(コピペ)して実装してみます。

ということで、今回の記事は Enum からドロップダウンリストを生成する方法2つの紹介です。

1. 自分で SelectList を組み立てる

Enum のコード:

public enum Color
{
	Red = 0,
	Yellow = 1,
	Green = 2
}

コントローラーからビューへ渡すモデルクラス:

{
public class CreatePersonModel
{
	[Required(ErrorMessage = "{0} が必要です。")]
	[Display(Name = "名前")]
	public string Name { get; set; }

	[Required(ErrorMessage = "{0} を選択して下さい。")]
	[Display(Name = "好きな色")]
	public Color? FavoriteColor { get; set; }

	/// <summary>
	/// 色選択リスト
	/// </summary>
	public IEnumerable<SelectListItem> ColorSelectList { get; set; }
}

前回のコードとは違い、プロパティの型が IEnumerable<SelectListItem> と Enum の Color に変更になっています。

コントローラーのコード:

public class PersonController : Controller
{
	// GET: /Person/Create
	[HttpGet]
	public ActionResult Create()
	{
		//色のコレクションを取得します
		var items = new List<SelectListItem>()
		{
			new SelectListItem() { Value = Color.Red.ToString(), Text = "あか" },
			new SelectListItem() { Value = Color.Yellow.ToString(), Text = "きいろ" },
			new SelectListItem() { Value = Color.Green.ToString(), Text = "みどり" },
		};

		var model = new CreatePersonModel();
		model.ColorSelectList = CreateColorSelectList();

		return View(model);
	}
		
	//色を選択するためのセレクトリストオブジェクトを生成します
	private IEnumerable<SelectListItem> CreateColorSelectList()
	{
		return new List<SelectListItem>()
		{
			new SelectListItem() { Value = Color.Red.ToString(), Text = "あか" },
			new SelectListItem() { Value = Color.Yellow.ToString(), Text = "きいろ" },
			new SelectListItem() { Value = Color.Green.ToString(), Text = "みどり" },
		};
	}
//...

ビューのドロップダウンリストを表示している部分:

<div class="form-group">
	@Html.LabelFor(model => model.FavoriteColor, new { @class = "control-label col-md-2" })
	<div class="col-md-10">
		@Html.DropDownListFor(model => model.FavoriteColor, Model.ColorSelectList,
			"選択して下さい。", new { @class = "form-control" })
		@Html.ValidationMessageFor(model => model.FavoriteColor)
	</div>
</div>

コードは以上です。
コントローラークラスの CreateColorSelectList() メソッドで、自分で SelectList を組み立てています。項目の表示名もここで指定します。

return new List<SelectListItem>()
{
	new SelectListItem() { Value = Color.Red.ToString(), Text = "あか" },
	new SelectListItem() { Value = Color.Yellow.ToString(), Text = "きいろ" },
	new SelectListItem() { Value = Color.Green.ToString(), Text = "みどり" },
};

この方法はとても簡単ですが、各 Enum 毎に SelectList を組み立てなければいけないので若干面倒ではあります。(でも十分な実装方法だと思います。)

2. 自分で Enum から SelectList を組み立てるヘルパーを作り、それを使う

(※ASP.NET MVC 5.1 がリリースされたら、この方法ではなくリリースされた HTML ヘルパーを使うことを推奨します。)

この方法では、Enum の型情報を引数に IEnumerable<SelectList> を生成するヘルパーを作ります。
そのヘルパーは、冒頭で述べた通り、ASP.NET MVC のソースから拝借します。

Enum のコード:

public enum Color
{
	[Display(Name="あか")]
	Red = 0,
	[Display(Name = "き")]
	Yellow = 1,
	[Display(Name = "みどり")]
	Green = 2
}

Enum では表示名を Display 属性で指定します。(詳細は ASP.NET MVC 5.1 の新機能を Nightly Build で試してみた をご覧ください。)

Enum から IEnumerable<SelectList> を生成するヘルパークラス:
aspnetwebstack / src / System.Web.Mvc / Html / EnumHelper.cs をコピペしました。(一部修正あり)

using System;
using System.Collections.Generic;
using System.Web.Mvc;
using System.Reflection;
using System.ComponentModel.DataAnnotations;

namespace WebApplication5.Models
{
	public static class MyHelper
	{
		public static IList<SelectListItem> GetSelectList(Type type)
		{
			if (type == null)
			{
				throw new ArgumentNullException("type");
			}

			if (!IsValidForEnumHelper(type))
			{
				throw new ArgumentException(String.Format("{0} の型はサポートされていません", type.FullName), "type");
			}

			IList<SelectListItem> selectList = new List<SelectListItem>();

			// According to HTML5: "The first child option element of a select element with a required attribute and
			// without a multiple attribute, and whose size is "1", must have either an empty value attribute, or must
			// have no text content."  SelectExtensions.DropDownList[For]() methods often generate a matching
			// <select/>.  Empty value for Nullable<T>, empty text for round-tripping an unrecognized value, or option
			// label serves in some cases.  But otherwise, ignoring this does not cause problems in either IE or Chrome.
			Type checkedType = Nullable.GetUnderlyingType(type) ?? type;
			if (checkedType != type)
			{
				// Underlying type was non-null so handle Nullable<T>; ensure returned list has a spot for null
				selectList.Add(new SelectListItem { Text = String.Empty, Value = String.Empty, });
			}

			// Populate the list
			const BindingFlags BindingFlags =
				BindingFlags.DeclaredOnly | BindingFlags.GetField | BindingFlags.Public | BindingFlags.Static;
			foreach (FieldInfo field in checkedType.GetFields(BindingFlags))
			{
				// fieldValue will be an numeric type (byte, ...)
				object fieldValue = field.GetRawConstantValue();

				selectList.Add(new SelectListItem { Text = GetDisplayName(field), Value = fieldValue.ToString(), });
			}

			return selectList;
		}

		public static bool IsValidForEnumHelper(Type type)
		{
			bool isValid = false;
			if (type != null)
			{
				// Type.IsEnum is false for Nullable<T> even if T is an enum.  Check underlying type (if any).
				// Do not support Enum type itself -- IsEnum property is false for that class.
				Type checkedType = Nullable.GetUnderlyingType(type) ?? type;
				if (checkedType.IsEnum)
				{
					isValid = !HasFlagsInternal(checkedType);
				}
			}

			return isValid;
		}

		private static bool HasFlagsInternal(Type type)
		{
			if (type == null) throw new ArgumentNullException("type");

			FlagsAttribute attribute = type.GetCustomAttribute<FlagsAttribute>(inherit: false);
			return attribute != null;
		}

		public static bool IsValidForEnumHelper(ModelMetadata metadata)
		{
			return metadata != null && IsValidForEnumHelper(metadata.ModelType);
		}

		private static string GetDisplayName(FieldInfo field)
		{
			DisplayAttribute display = field.GetCustomAttribute<DisplayAttribute>(inherit: false);
			if (display != null)
			{
				string name = display.GetName();
				if (!String.IsNullOrEmpty(name))
				{
					return name;
				}
			}

			return field.Name;
		}
	}
}

コントローラーのコード:

public class PersonController : Controller
{
	// GET: /Person/Create
	[HttpGet]
	public ActionResult Create()
	{
		var model = new CreatePersonModel();
		model.ColorSelectList = MyHelper.GetSelectList(typeof(Color));

		return View(model);
	}
//...

コードは以上です。ビューへ渡すモデルクラスと、ビューのコードは 1 と同じです。
コントローラークラスで、ヘルパーを呼び出して IEnumerable<SelectListItem>をモデルクラスにセットしています。

おしまい

2 の方法は、いろいろな実装方法が存在します。

ASP.NET MVC 5.1 がリリースされたら、是非新しい Enum の HTML ヘルパーを使いたいですね!
バージョン 5 未満を使用していても、オープンソースなので参考にできます。

by 深夜連絡 ASP.NET MVC な Web アプリ Advent Calendar 2013 4 日目