しばやん雑記

ASP.NET Core とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET Core MVC を使ってドロップダウンリストをいい感じに実装する

大昔に書いた ASP.NET MVC 3 向けの内容を Core MVC 向けにします。と言ってもほぼ別物になってますが、基本的な考え方は SelectListItem 側に寄せる、なのでいい感じに扱えるようにしてみます。

Core MVC では Razor が超進化しているので、DI や Tag Helpers を組み合わせて MVC 3 時代よりスマートに書けるようになったと思います。

例えば select で表示したいデータは以下のように適当にクラスを作っておきます。

public class AppMaster
{
    public IEnumerable<SelectListItem> Devices { get; } = new List<SelectListItem>
    {
        new SelectListItem { Text = "Android", Value = "android" },
        new SelectListItem { Text = "iPhone", Value = "ios" },
        new SelectListItem { Text = "Windows 10 Mobile", Value = "windows" }
    };
}

昔なら static クラスにしたかもしれないですが、このクラスは DI を使ってシングルトンとして扱います。

なので Startup.cs の ConfigureServices にて AddSingleton を呼び出して追加します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<AppMaster>();

    services.AddMvc();
}

これで DI を使って、どのタイミングでも AppMaster クラスを利用出来るようになりました。

表示するデータは用意したので、Razor を使ってビューを書いていきます。Tag Helpers のおかげで HTML の中にシームレスに書くことが出来て素晴らしいですね。これは何回でも言いたい部分です。

@model WebApplication17.Models.FormModel

@inject WebApplication17.AppMaster AppMaster

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<form asp-action="Index">
    <div class="form-group">
        <label asp-for="Name"></label>
        <input type="text" asp-for="Name" class="form-control" />
        <span asp-validation-for="Name"></span>
    </div>

    <div class="form-group">
        <label asp-for="Device"></label>
        <select asp-for="Device" asp-items="AppMaster.Devices" class="form-control">
            <option value="">選択してください</option>
        </select>
        <span asp-validation-for="Device"></span>
    </div>
    
    <button type="submit" class="btn btn-success">Confirm</button>
</form>

@inject を使って AppMaster を Razor ビューに追加しています。そして select#asp-items にて AppMaster で用意したプロパティを与えることでドロップダウンリストを生成することが出来ます。

実際に実行してみると、ちゃんと AppMaster の値が表示されていることが分かります。

f:id:shiba-yan:20171023162008p:plain

ここまでは簡単なのですが、問題は確認画面が必要になった場合です。この辺りは MVC 3 から変わってない部分で、特に組み込みの Tag Helper などは用意されていないです。

普通に以下のように Razor ビューを書くとドロップダウンリストで選んだ部分が残念なことになります。

@model WebApplication17.Models.FormModel

@{
    ViewBag.Title = "Confirm";
}

<h2>Confirm</h2>

<form asp-action="Confirm">
    <div class="form-group">
        <label asp-for="Name"></label>
        @Model.Name
    </div>

    <div class="form-group">
        <label asp-for="Device"></label>
        @Model.Device
    </div>
    
    <button type="submit" class="btn btn-success">Submit</button>
</form>

実際に表示してみると、value の値がそのまま表示されるだけです。まあ、そういうコードなので当たり前といえば当たり前なのですが、SelectListItem の Text に指定した値が表示されて欲しいです。

f:id:shiba-yan:20171023162221p:plain

なので MVC 3 の頃に作ったような Html Helper の Tag Helpers 版を用意しました。

[HtmlTargetElement("display")]
public class DisplayTagHelper : TagHelper
{
    public DisplayTagHelper(IHtmlGenerator generator)
    {
        Generator = generator;
    }

    protected IHtmlGenerator Generator { get; }

    [HtmlAttributeName("asp-for")]
    public ModelExpression For { get; set; }

    [HtmlAttributeName("asp-items")]
    public IEnumerable<SelectListItem> Items { get; set; }
        
    [HtmlAttributeNotBound]
    [ViewContext]
    public ViewContext ViewContext { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        var selectedValue = Generator.GetCurrentValues(ViewContext, For.ModelExplorer, For.Name, false).FirstOrDefault();

        var selectListItem = Items.FirstOrDefault(x => selectedValue == x.Value);

        if (selectListItem == null)
        {
            output.SuppressOutput();
        }
        else
        {
            output.TagName = null;
            output.Content.SetContent(selectListItem.Text);
        }
    }
}

IHtmlGenerator を使うとモデルの string な値が簡単に取れるので便利になりました。

この Tag Helpers を使って確認画面を書くと以下のようになります。組み込みの Tag Helpers に合わせる形で属性を用意しているので、簡単に扱えるような気がしています。

@model WebApplication17.Models.FormModel

@inject WebApplication17.AppMaster AppMaster

@{
    ViewBag.Title = "Confirm";
}

<h2>Confirm</h2>

<form asp-action="Confirm">
    <div class="form-group">
        <label asp-for="Name"></label>
        @Model.Name
    </div>

    <div class="form-group">
        <label asp-for="Device"></label>
        <display asp-for="Device" asp-items="AppMaster.Devices"></display>
    </div>
    
    <button type="submit" class="btn btn-success">Submit</button>
</form>

これで実際に表示してみると、意図したとおりに SelectListItem の Text に指定した値が表示されます。

f:id:shiba-yan:20171023163626p:plain

たったこれだけのことに時間がかかりすぎだし、組み込みで用意して欲しい気もしますが Tag Helpers と DI のおかげで MVC 3 よりも簡単かつシンプルに書けるようになりました。

ASP.NET Core MVC の新しい Razor ではビュー内で await も書けるので、データベースなどから値を引っ張ってくる場合も同じように書けるので便利です。