[ASP.NET MVC][C#]Controllerで複数のcheckboxの状態を個々のチェック状態のBooleanではなく、チェックされているものだけの配列を引数で受け取りたい

この記事は One ASP.NET Advent Calendar 2013の20日目の記事です。

複数のinput type=”checkbox”の状態をControllerで受け取るときに、個々のcheckboxの状態をBooleanではなく、チェックされているものだけを配列として受け取りたい。

というテーマです。

シナリオは以下の流れ
1.寿司ネタがチェックボックス付きでリストアップ
2.その中から好きな寿司ネタにチェックを入れる
3.「報告」ボタンを押すと、好きな寿司ネタを報告した結果が表示される(実際には表示するだけで何もしない)

というものです。

データは以下のようになっています。

private Dictionary<string, string> SampleData = new Dictionary<string, string>
{
{"sushi1","まぐろ"},
{"sushi2","はまち"},
{"sushi3","金目鯛"},
{"sushi4","中トロ"},
{"sushi5","赤貝"},
{"sushi6","こはだ"},
{"sushi7","うに"},
{"sushi8","いくら"},
{"sushi9","たまご"},
{"sushi10","ネギトロ巻"}
};

実際に動作させると以下のようになることを想定しています。

/Home/Index
選択肢

報告ボタンを置くと以下のページに遷移
/Home/Result
実行結果

Case1:標準helperを利用した場合

ASP.NET MVCに用意されている標準helperを利用した場合、viewは以下のようになります。

index.cshtml

@model Dictionary&lt;string,string&gt;

@using (Html.BeginForm("Result"))
{
<ul>
<ul>@foreach (var item in Model)</ul>
</ul>
&nbsp;
<ul>
<ul>{
	<li>@Html.CheckBox(item.Key)@item.Value</li>
</ul>
</ul>
}

<input type="submit" value="報告" />
}

result.cshtml

@model Dictionary&lt;string,string&gt;

@using (Html.BeginForm("Result"))
{
<ul>
<ul>@foreach (var item in Model)</ul>
</ul>
&nbsp;
<ul>
<ul>{
	<li>@Html.CheckBox(item.Key)@item.Value</li>
</ul>
</ul>
}

<input type="submit" value="報告" />
}

Controller側は以下のようになります。
本来、checkboxのnameで名前解決して以下のようにできますが、

        public ActionResult Result(bool sushi1,bool sushi2,bool sushi3,bool sushi4,bool sushi5,bool sushi6,bool sushi7,bool sushi8,bool sushi9,bool sushi10)

一連のデータを上のように名前解決するのはありえないので以下のようなコードになってしまいます。

        public ActionResult Result()
        {
                    
            var values = Request.Form.AllKeys
                .Where(x => x.StartsWith("sushi"))
                .Select(x => new {Key = x,Value = Boolean.Parse(Request.Form[x].Split(',').First())})
                .ToDictionary(x => x.Key,x => x.Value);
      
            var result = SampleData
                .Where(x => values[x.Key])
                .Select(x => x)
                .ToDictionary(x => x.Key,x => x.Value);

            return View(result);
        }

名前解決されていないので、いちいちFormから持ってくる必要があったり、自分でBooleanにパースする必要があったりします。
また、標準HelperのHtml.CheckBoxはチェックされていない時も、サーバにfalseを返すために
(本来、input type=”checkbox”はチェックされているものだけを返すのが仕様)
input type=”checkbox”とセットでinput type=”hidden” value=”false”を出力するようになっており、チェックが入っているときは、一つ当たり”true,false”という文字列が値として渡されてきますので、”,”で区切って、最初の値を有効とするようにしなくてはいけない。

などとめんどくさい上にイレギュラーに強くなさそうな状態になります。
Html.CheckBoxは、何かのオンオフのチェックなどの時は、便利ですが、こういうチェックボックスをリストにして、どれを選択しているかを判定するのには向いてません。

では、どうするか

Case2:標準helperを利用しない

身も蓋もないのですが、標準Helperを利用しないで、直に書きます。(Helperを新しく作っても良いと思いますが・・・)

この場合、すべてのcheckboxのnameをsushiとしvalueにkeyをあてます。
以下のようになります。

    @model Dictionary<string,string>

    @using (Html.BeginForm("Result","Home"))
    {
        <ul>
            @foreach (var item in Model)
            {
            <li>
                <input type="checkbox" name="sushi" value="@item.Key" /> @item.Value
            </li>
            }
        </ul>
        <input type="submit" value="報告" />
    }

この場合、submitで返されるのはチェックが入っているものだけになります。

そして、Controller側は以下のようにします。
先ほどのview側で指定したsushiというnameで名前解決され、valueが配列で渡されてきます。

        public ActionResult Result(string[] sushi)
        {
            sushi = sushi ?? new string[]{};                               
            
            var result = SampleData
                .Where(x => sushi.Contains(x.Key))
                .Select(x => x)
                .ToDictionary(x => x.Key,x => x.Value);

            return View(result);
        }

標準ヘルパを使っていた時のような、自前で値をパースして、どうこうなどなどの、面倒なことをすることなく、処理できるようになりました。
チェックが入っていない時用のhiddenが作られることもないので、チェックされているものだけが返ってくることが保証されています。

まとめ

・なんらかの判断をユーザに要求するような(○○はスキップするなど・・・)時は標準ヘルパを使ってbool isSkipなどでcontroller側で受け取るのが便利、フォームの複数回答可などの選択肢に使うと、むしろ煩雑
・フォームの複数回答可などのcheckboxはnameを同じにして、Controller側でstring[] sushiなど配列として名前解決された状態で受け取るのが便利

という感じでしょうか。
小ネタも小ネタですいませんが、以上でOne ASP.NET Advent Calender 2013への投稿の閉めとしたいと思います。

Share