【ASP.NET(C#)5】データ一覧画面と作成画面を一緒にしたい。複数のmodel(モデル)参照方法

今回も前回記事や4の記事でCRUDを作成した所からの解説です。CRUDまでを参照したい方は以下の記事を参照してください。は通常実装向け/はスキャフォールディング向けです。

作成画面と一覧(index)画面を一緒にするには?

WordPressの管理画面もそうですが最初の画面には通常ダッシュボード/一覧と呼ばれるインデックスが表示されているだけで作成画面は一緒にしないのが通常だと思います。

ただし、簡易掲示板や昔は主流だったチャットのようなやりとりが味わえた伝言板のようなWebアプリケーションを作りたくなった時とかは、一覧画面と一緒に書き込みできるフォームがあった方が楽ではありませんか?

今回はそんなケースにお答えした記事になります。

基本はCreate.cshtmlのフォームの部分をIndex.cshtmlにコピーすればよいですが・・以下の二つのエラーが出てしまいます。

RZ2001 The ‘model’ directive may only occur once per document.

このエラーは検索してもあまり検索してもひっかからないのですが翻訳すると以下のような意味です。

モデルディレクティブは、ドキュメントごとに1回だけ発生する可能性があります

これはGoogle翻訳した結果ですが、意訳するとモデルの定義は一つのソース一つだけということだと思います。

今回はCreateに使う単独のModels.ArticleEntity 複数が前提のIEnumerable< Models.ArticleEntity>どちらも使いたい。

この件に限らず、もっと複雑なWeb APIになると一つの画面で複数のmodelを参照したい事もあるでしょう。その時はどうすればよいのか?

今のままでは 作成側のArticleEntityは参照できないので配置することができません。

ViewModelを使う

この時はViewModelというModelを新たに定義して使います。要はModelを一個作ってその中に個々のモデルを入れちゃうという感じですね。

ちなみにWPFの回でMVVMパターンで言うViewModelはViewとModelをつないでいるもの。みたいな説明をしました。WPFの場合は基本このMVVMパターンに乗っ取って制作するお作法のようなものですが、今回のASP.NETで用いているViewModelはModelを複数使いたい時だけ使用するものMVVMパターンを使ったお作法ではありませんのでそこは混同しないように注意しましょう。とはいえ、Viewで使うものでModelの定義を複数つなぐために使用するものなので役割的には一緒と思ってもらって良いと思います。

WPFでのMVVMパターンについては下記記事を参照。

.NET 6 バージョンでMVCを作成すると最初からErrorViewModel.csというものができていますがこれも同じ役目なのかと思います。

Modelsフォルダと分けて作った方がわかりやすい

今回はViewModelsフォルダを作成して、その下にArticleViewModel.csを作成しました。Viewで使っていた型に合わせて以下のように修正します。

using WebBBS.Models;

namespace WebBBS_2.ViewModels
{
    public class ArticleViewModel
    {
        public ArticleEntity articleEntity { get; set; }

        public IEnumerable<ArticleEntity> articleEntities { get; set; }
    }
}

次にArticleEntitesControllerのIndex部分にはViewModelを通知するように変更しなければいけません。

こちらはスキャフォールディングされた修正前のソースですがこれを以下のように修正します。

// GET: ArticleEntities
public async Task<IActionResult> Index()
{
    ArticleViewModel articleViewModel = new ArticleViewModel
    {
        articleEntity = new ArticleEntity(),
        articleEntities = await _context.ArticleEntity.ToListAsync()
    };

    return View(articleViewModel);
}

これで二つのモデルを統合したViewModelを渡せましたのでViewのindex.cshtmlを以下のように修正します。

@using WebBBS_2.ViewModels
@model ArticleViewModel

@{
    ViewData["Title"] = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Index</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="articleEntity.Title" class="control-label"></label>
                <input asp-for="articleEntity.Title" class="form-control" />
                <span asp-validation-for="articleEntity.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="articleEntity.Name" class="control-label"></label>
                <input asp-for="articleEntity.Name" class="form-control" />
                <span asp-validation-for="articleEntity.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="articleEntity.Description" class="control-label"></label>
                <input asp-for="articleEntity.Description" class="form-control" />
                <span asp-validation-for="articleEntity.Description" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>


<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.articleEntity.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.articleEntity.Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.articleEntity.Description)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.articleEntity.Created)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.articleEntity.Updated)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.articleEntities) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Description)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Created)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Updated)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

修正後

修正後は以下のような画面になりました。

この状態でCreateを押すと・・・

ご覧ください。画面は特に変わらずに入力した値が書き込まれました。これで画面の動きは昔なつかしの掲示板やチャットのような動きになったのではないでしょうか。

注意点:ViewModelのArticleEntityのフィールド名はCreateでの ArticleEntity の引数名と同じにしよう

画像のようにViewModelで定義したArticleEntityのフィールド名とPostで受け取る引数名を一緒にしましょう。

例えば上記のように違う名前で定義してしまって動かしてみます。するとちゃんと起動して値を入力してCreateを押しても

Create処理が行われても入力した値が渡されていないことがわかります。これも初心者がつまづきやすいポイントなので気を付けましょう。

今回の参考記事

ASP.NETでビュー(View)に複数のモデル(Model)を連携する方法

MVC における ViewModel とは?

今回は主に上の記事を参照させて頂きました。ほぼまんまですが.NETバージョンでやり方が違うこともありますので念のため。当技術ブログは現在最新の .NET 6で実装してます。

まとめ

前回の記事と合わせて、これで簡易掲示板に関する動作がそろいましたね。動作は一旦良いと思うので次は見栄えを掲示板っぽく変えていきます。

次のASP.NETの記事はいよいよJavaScript/Vue.js/BootStrapを組み合わせる画面側に入っていきます。私も画面の知識は疎いですががんばりますね