@Html.Actionの代替としてViewComponentを使用する(ASP.NET CORE6 MVC)

概要

.NET6では.NET Framework4 MVC5で使っていた@Html.Actionが使用できなくなったため、@Html.Actionにてページ内にコントローラの出力結果(Model)を部分Viewとして代入することができなくなった。
かわりにビューコンポーネント(ViewComponent)を使用する。

ViewComponentの特徴

ViewComponentはコントローラー(Controller)に似ているが、View出力のための1つのメソッドしかもたない。
あとは、ほぼコントローラーと同じで、DIできるし、HttpContextやModelStateにアクセスできる。
どちらもViewと、Viewで使用するModelを返却することができる。

ViewComponentの構成

ViewComponentを使うために作らなければならないものは以下のとおり。
・ViewComponent本体(コントローラーのようなもの)
・ViewComponentが出力するView(cshtmlファイルで部分ビューのようなもの)
・Viewで使用するモデル
・ViewComponentを埋め込む親ページ

Viewについてはデフォルトの配置場所(ViewComponentでreturn Viewする際にViewを検索しに行くパス)に決まりがある。
ViewComponentの親ページのView(cshtml)と同階層にComponentsフォルダを作成し、さらにその下にViewComponent名(ViewComponentがInformationWindowViewComponentだったら「InformationWindow」が名前)でフォルダを作成し、その下にViewComponentのViewを格納する(下図参照)。
ViewComponentが特定の画面(View)に属さず複数の画面に属する場合は、Views/Shared/Components/配下にViewComponent名のフォルダを作成し、その下にViewを格納する。

今回のサンプルは、以下のフォルダ構成で作成した。
(ViewComponent名は、InformationWindow。LoginControllerのViewから呼び出される)

サンプルの説明

サンプルなので処理に大した意味はありません。サンプルのために作られたViewComponentです。
(このセクションだけ、ですます調になっていますが、出来の悪いサンプルに対して後ろめたさがあったためと思われます)

上図の赤枠で囲まれた箇所がViewComponentになります。
親画面は/Views/Login/Login.cshtmlになります。
初期表示はお知らせのメッセージが「:みなさんさん、こんにちは。」になっています。

ユーザーID、パスワードを入力して、ログインボタンを押下すると、下図のようにお知らせのメッセージが変わります。
(すみません、サンプルはログイン画面を流用したのでログインボタンを押下する形になっていますが、サンプルでやっていることはログイン処理ではありません)

Viewで使用するモデルを作成する

お知らせ(string)を保持するためのモデルクラス。
InformationViewModel.cs

using System.ComponentModel;

namespace TestWebCore3.Models.Login.ViewModel
{
    /// <summary>
    /// お知らせViewモデル
    /// </summary>
    public class InformationViewModel
    {
        /// <summary>
        /// お知らせ
        /// </summary>
        [DisplayName("お知らせ")]
        public string? Infomation { get; set; }

    }
}

ViewComponent(コントローラのようなもの)

以下にサンプルを示す。説明はコメントを参照のこと。
InformationWindowViewComponent.cs

using Microsoft.AspNetCore.Mvc;
using TestWebCore3.Models.Login.Services;
using TestWebCore3.Models.Login.ViewModel;

namespace TestWebCore3.ViewComponents
{
    /// <summary>
    /// ViewComponentサンプルクラス
    /// パラメータで受け取ったユーザIDを使ってお知らせメッセージを表示する
    /// 
    /// ViewComponentを継承すれば、以下の値も取得可能
    /// (ほぼControllerと同じ使い勝手)
    /// this.HttpContext.Request
    /// this.ModelState
    /// </summary>
    public class InformationWindowViewComponent :ViewComponent
    {
        /// <summary>
        /// ログインサービス(DIのサンプル)
        /// </summary>
        LoginService _LoginService;

        /// <summary>
        /// お知らせViewComponent
        /// コントローラ同様にクラスをインジェクションできる
        /// </summary>
        /// <param name="loginService">お知らせ取得用のサービスクラス</param>
        /// <param name="context">コンテキスト</param>
        public InformationWindowViewComponent(LoginService loginService)
        {
            // DIのサンプル
            _LoginService = loginService;
        }

        /// <summary>
        /// 部分View出力処理
        ///
        /// InvokeとInvokeAsyncが使える。
        /// 表示時に時間がかからないように重い処理は非同期で動かすことが推奨されているため
        /// 重い処理がある場合はInvokeAsyncを使用する。
        /// 特に非同期にする処理がない場合はInvokeを使用する。
        /// </summary>
        /// <param name="userId">ユーザID  タグの属性(user-id)で指定する</param>
        /// <param name="userName">ユーザ名 タグの属性(user-name)で指定する</param>
        /// <returns>部分View</returns>
        public async Task<IViewComponentResult> InvokeAsync(string userId, string userName)
        {
            // Viewで使用するModelを初期化
            InformationViewModel model = new InformationViewModel();


            // お知らせを取得する。
            // LoginServiceはuserIdとuserNameをもとにお知らせメッセージを作成するクラス
            // (DIのサンプルのために用意したクラス)
            model.Infomation = await _LoginService.GetInformation(userId, userName);
            // 「Information」部分Viewを、指定したmodelの値で出力し、Viewに埋め込む
            // 戻り値の型は Microsoft.AspNetCore.Mvc.ViewComponents.ViewViewComponentResult
            // コントローラが返却するViewの型 Microsoft.AspNetCore.Mvc.ViewResultとは異なる
            return View("Information", model);
        }
    }
}

LoginService.cs (DIのサンプル)

namespace TestWebCore3.Models.Login.Services
{
    /// <summary>
    /// ダミーサービス
    /// </summary>
    public class LoginService
    {
        /// <summary>
        /// 適当なメソッド。asyncである必要はないが一応時間のかかる処理を想定
        /// </summary>
        /// <param name="userId">ユーザID</param>
        /// <param name="userName">ユーザ名</param>
        /// <returns></returns>
        public async Task<string> GetInformation(string userId, string userName)
        {
            // waitする必要ないが時間かかる想定の処理
            await Task.Delay(10);

            string infomation = $"{userId}:{userName}さん、こんにちは。";
            return infomation;
        }
    }
}

ViewComponentが出力するView(部分ビューのようなもの)

InformationViewModelのInformation項目を出力する。
/Views/Login/Components/InformationWindow/Information.cshtml

@model TestWebCore3.Models.Login.ViewModel.InformationViewModel

<div class="form-group">
    @Html.LabelFor(model => model.Infomation, htmlAttributes: new { @class = "control-label col-xs-3" })
    <div class="col-xs-8">
        @Html.DisplayFor(model => model.Infomation
            , new { htmlAttributes = new { @class = "form-control" } })
    </div>
</div>

ViewComponentを埋め込む親ページ

作成したViewComponentを埋め込むための親ページを作成する。
ここではViewComponentが「InformationWindowViewComponent」で、親ページが「Login.cshtml」になる。
Login.cshtml

@model TestWebCore3.Models.Login.ViewModel.LoginViewModel
@using TestWebCore3.ViewComponents

@* ViewComponentを使用する場合は @addTagHelperで
   ViewComponentが定義されているアセンブリ名を指定する
   アセンブリ名はnamespaceではなくプロジェクト名になる *@
@addTagHelper *,TestWebCore3
@{
    ViewBag.Title = "ログイン画面";
}

<div class="form-horizontal">
    @* ViewComponentは、vcタグで記載する。
       クラス名や変数名はスネークで書く。
       information-window → InformationWindowにマッピングされる
       user-id、user-name → userId、userNameにマッピングされる
       user-id、user-nameの値がInformationWindowViewComponentの
       InvokeAsyncのuserId, userName引数に渡される   *@
    <vc:information-window user-id="@Model.UserId" 
        user-name="@Model.UserName"></vc:information-window>

    @using (Html.BeginForm("Login", "Login"))
    {
        @Html.AntiForgeryToken()

        <div class="form-group">
            @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.UserId
                    , htmlAttributes: new { @class = "control-label col-xs-3" })
            <div class="col-xs-8">
                @Html.EditorFor(model => model.UserId
                        , new { htmlAttributes = new { @class = "form-control" } })
            </div>
        </div>
        <div class="form-group">
            <div class="col-xs-offset-3 col-xs-8">
                @Html.ValidationMessageFor(model => model.UserId, ""
                        , new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Password
                    , htmlAttributes: new { @class = "control-label col-xs-3" })
            <div class="col-xs-8">
                @Html.PasswordFor(model => model.Password
                        , new { htmlAttributes = new { @class = "form-control" } })
            </div>
        </div>
        <div class="form-group">
            <div class="col-xs-offset-3 col-xs-8">
                @Html.ValidationMessageFor(model => model.Password
                        , "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="form-group">
            <div class="col-xs-offset-3 col-xs-8">
                <input type="submit" value="ログイン" class="btn btn-default" />
            </div>
        </div>

    }
</div>

ソースのコメントにも書いたがポイントは3点。
・addTagHelperの記載が必要。
  addTagHelperに記載するのはViewComponentが含まれるアセンブリ名(プロジェクト名)
・ViewComponentは<vc>タグで記載する。
・<vc>タグに記載するViewComponentの名前、InvokeAsyncの引数はスネークで記載する。

LoginController.cs (サンプルとして)

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TestWebCore3.Models.Login.InputModel;
using TestWebCore3.Models.Login.ViewModel;

namespace TestWebCore3.Controllers
{
    /// <summary>
    /// ログイン画面コントローラー
    /// </summary>
    public class LoginController : Controller
    {
        /// <summary>
        /// コンストラクター(DI)
        /// </summary>
        public LoginController()
        {
        }

        /// <summary>
        /// 画面初期表示
        /// </summary>
        /// <returns>View</returns>
        [AllowAnonymous]
        [HttpGet]
        public ActionResult Index()
        {
            // ViewModelを生成する

            LoginViewModel model = new LoginViewModel();
            model.UserName = "みなさん";

            // ViewModelを使ってLoginビューを表示する
            return View("Login", model);
        }

        /// <summary>
        /// ログイン処理
        /// </summary>
        /// <param name="inputModel">入力モデル</param>
        /// <returns>リダイレクト</returns>
        [AllowAnonymous]
        [HttpPost]
        public ActionResult Login(LoginInputModel inputModel)
        {
            LoginViewModel model = new LoginViewModel();
            // 入力エラーがない場合
            if (this.ModelState.IsValid)
            {
                // ログイン
                model.UserId = inputModel.UserId;
                if (string.IsNullOrEmpty(inputModel.UserId))
                {
                    model.UserName = "みなさん";
                }
                else
                {
                    model.UserName = "いちにの";
                }
            }
            // サンプルなのでそのままログイン画面表示
            return View("Login", model);

        }
    }
}

LoginViewModel.cs (サンプルとして)

#nullable enable

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace TestWebCore3.Models.Login.ViewModel
{
    /// <summary>
    /// ログインビューモデル
    /// </summary>
    public class LoginViewModel
    {
        /// <summary>
        /// ユーザーID
        /// </summary>
        [DisplayName("ユーザーID")]
        public string? UserId { get; set; }

        /// <summary>
        /// パスワード
        /// </summary>
        [DisplayName("パスワード")]
        [DataType(DataType.Password)]
        public string? Password { get; set; }

        /// <summary>
        /// ユーザー名
        /// </summary>
        [DisplayName("ユーザー名")]
        public string? UserName { get; set; }


    }
}