WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

ASP.NET Core MVC で Bootstrap5 Modal の利用

by WebSurfer 11. November 2023 10:46

Visual Studio 2022 を使って作成する ASP.NET Core MVC アプリで、データベースのテーブル一覧を表示し、その中の項目を選択すると、その詳細を Bootstrap5 の Modal を利用して表示する方法を書きます。

Bootstrap Modal に詳細を表示

この記事ではターゲットフレームワークを .NET 7.0 としています。.NET 5.0 以前をターゲットフレームワークとして作成したプロジェクトでは Bootstrap のバージョンが異なるので注意してください。

この記事のベースとしたアプリは、Microsoft のチュートリアル「CRUD 機能を実装する」です。そのアプリの Student テーブルのレコード一覧を表示する Index ページと各レコードの詳細を表示する Details ページを使います。

チュートリアルでは、Index ページに一覧表示されている各項目の右横のアクションリンク Details をクリックすると、別ページ Details に遷移し、遷移した Details ページ上で選択した項目の詳細を表示するようになっています。チュートリアルの「Details ビューに登録を追加する」セクションの下の方の画像を見てください。

それを、別ページ Details に遷移しないで、この記事の上の画像のように Index ページ上で Bootstrap Modal 内に詳細を表示するようにしてみます。

Bootstrap については、Visual Studio 2022 のテンプレートで、ターゲットフレームワーク .NET 7.0 として作成した ASP.NET Core MVC アプリのプロジェクトに Bootstrap.js, Boorstrap.css v5.1.0 が含まれています。Bootstrap Modal を使うために必要なものもそれらの中に含まれているので何も追加する必要はありません。

レイアウトページ _Layout.cshtml にはプロジェクトの Bootstrap.js, Boorstrap.css を参照する link タグ、script タグが含まれていますので、スキャフォールディングの際 _Layout.cshtml を使用するように設定すれば、それらが参照されるようになります。

Bootstrap5 Modal の詳しい説明は Bootstrap のドキュメント「Modal (モーダル)」にあります。普通の html + css + javascript のページに Bootstrap5 Modal を組み込むなら、そのドキュメントから十分な情報が得られると思います。

ASP.NET Core MVC アプリに、Bootstrap5 Modal を組み込んで、Index ページにのアクションリンクをクリックして詳細データを取得し、Index ページ上で Modal に表示するにはどうしたらいいかというのががこの記事のポイントで、それを以下に述べます。

まずコントローラーのアクションメソッドですが、Index はチュートリアルのものと全く同じ、Details も最後の一行で部分ビューを返すように変更する以外はチュートリアルのものと同じです。

コードは以下の通りとなります。この記事ではアクションメソッドの名前を Index3, Details3 としています。

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MvcNet7App.Data;
using MvcNet7App.Models;

namespace MvcNet7App.Controllers
{
    public class StudentsController : Controller
    {
        private readonly SchoolContext _context;

        public StudentsController(SchoolContext context)
        {
            _context = context;
        }

        // Student のレコード一覧を取得しビューに渡す
        public async Task<IActionResult> Index3()
        {
            return _context.Students != null ?
                   View(await _context.Students.ToListAsync()) :
                   Problem("Entity set 'SchoolContext.Students'  is null.");
        }

        // 詳細を表示する部分ビュー用のアクションメソッド
        public async Task<IActionResult> Details3(int? id)
        {
            if (id == null || _context.Students == null)
            {
                return NotFound();
            }

            //var student = await _context.Students
            //    .FirstOrDefaultAsync(m => m.ID == id);
            var student = await _context.Students
                                        .Include(s => s.Enrollments)
                                            .ThenInclude(e => e.Course)
                                        .AsNoTracking()
                                        .FirstOrDefaultAsync(m => m.ID == id);

            if (student == null)
            {
                return NotFound();
            }

            // 部分ビューを返すように変更
            return PartialView(student);
        }
    }
}

次に、上の Index アクションメソッドに対応するビューですが、スキャフォールディングで自動生成されたコードに (1) Modal 部分を実装、(2) Details アクションリンクを JavaScript のメソッドを呼び出すよう変更、(3) fetch API でサーバーに要求を出し、応答として返される部分ビューを Modal 内の div 要素に書き込んだ後 Modal を表示する JavaScript のメソッドを追加します。

コードは以下の通りです。Modal 部分は Bootstrap 5 のドキュメント「Modal (モーダル)」の「Static backdrop」セクションのサンプルコードをコピペし、若干修正をして利用しています。詳細を書き込む div 要素に id="details" を追加したこと、デフォルトの "medium" サイズでは幅が足らないので CSS に modal-lg を追加したこと、Understood ボタンは削除したことが主な違いです。

@model IEnumerable<MvcNet7App.Models.Student>

@{
    ViewData["Title"] = "Index3";
}

<h1>Index3</h1>

<!-- (1) Modal 部分を実装 -->
<div class="modal fade"
     id="staticBackdrop"
     data-bs-backdrop="static"
     data-bs-keyboard="false" tabindex="-1"
     aria-labelledby="staticBackdropLabel"
     aria-hidden="true">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title"
                    id="staticBackdropLabel">
                    Student Details
                </h5>
                <button type="button"
                        class="btn-close"
                        data-bs-dismiss="modal"
                        aria-label="Close">
                </button>
            </div>
            <div class="modal-body" id="details">
                ...
            </div>
            <div class="modal-footer">
                <button type="button"
                        class="btn btn-primary"
                        data-bs-dismiss="modal">
                    Close
                </button>
            </div>
        </div>
    </div>
</div>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.LastName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.FirstMidName)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @* (2) クリックで JavaScript の showDetails
                    を呼び出す。引数には Student の ID を渡す *@ 
                    <a href="javascript:showDetails(@item.ID)">
                        Details
                    </a>
                </td>
            </tr>
        }
    </tbody>
</table>

@section Scripts {
    <script type="text/javascript">

        // (3) fetch API でサーバーに要求を出し、応答として返される
        // 部分ビューを Modal 内の div 要素に書き込んだ、後 Modal を
        // 表示する

        // Modal を表示するためのヘルパーメソッド
        const showBootstrapModal = () => {
            // staticBackdrop は Modal の div 要素の id
            let divModal = document.getElementById('staticBackdrop');
            let myModal = new bootstrap.Modal(divModal);
            myModal.show();
        }

        const showDetails = async (id) => {
            // 部分ビュー用アクションメソッドの url
            // id は Student テーブルのレコードの ID
            const url = "/Students/Details3/" + id;

            // Student 詳細を表示する Modal 内の div 要素
            const resultDiv = document.getElementById('details');

            const response = await fetch(url);
            if (response.ok) {
                // 部分ビューのテキストを取得
                const text = await response.text();

                // テキストを Modal 内の div 要素に書き込み
                resultDiv.innerHTML = text;

                // Modal を表示
                showBootstrapModal();
            }            
        }

    </script>
}

最後に、Details アクションメソッドに対応する部分ビューですが、以下の通りとなっています。チュートリアルで自動生成されたコードから部分ビューに不要な部���を削除しただけです。

@model MvcNet7App.Models.Student

<div>
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.EnrollmentDate)
        </dd>

        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>

Tags: , , ,

CORE

ASP.NET Core のカスタムクラスから HttpContext にアクセス

by WebSurfer 19. September 2023 14:59

ASP.NET Core Web アプリ内のカスタムクラスから HttpContext にアクセスする方法を備忘録として書いておきます。下の画像は、カスタムクラスから HttpContext にアクセスし、ログイン中のユーザー名を取得して表示したものです。赤枠の部分がそれです。

ログイン中のユーザー名を取得

その方法は Microsoft のドキュメント「ASP.NET Core で HttpContext にアクセスする」の中の「カスタム コンポーネントから HttpContext を使用する」を見ればすぐ分かるのですが、どこに書いてあったか忘れて探し回ることがないように自分のブログに書いておくことにしました。

基本的にどうするかというと、Visual Studio のテンプレートを使って作成する ASP.NET Core アプリには Dependency Injection (DI) 機能が組み込まれていますのでそれを利用します。

まず、DI コンテナに IHttpContextAccessor サービスと、それを利用するカスタムクラスを登録します。具体的には、Program.cs に以下のようにコードを追加します。

using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.EntityFrameworkCore;
using MvcNet7App.Data;
using MvcNet7App.Libraries;

var builder = WebApplication.CreateBuilder(args);

// ・・・中略・・・

// DI コンテナに IHttpContextAccessor サービスを登録
builder.Services.AddHttpContextAccessor();

// それを利用するカスタムクラスを登録
builder.Services.AddTransient<IUserRepository, UserRepository>();

// ・・・後略・・・

カスタムクラス UserRepository の例は以下の通りです。DI 機能によって、コンストラクタ経由で IHttpContextAccessor サービスを inject できるようにしています。LogCurrentUser メソッドは、inject された IHttpContextAccessor サービスから HttpContext にアクセスし、ログイン中のユーザー名を取得して戻り値として返します。

namespace MvcNet7App.Libraries
{
    public class UserRepository : IUserRepository
    {
        // DI でコンストラクタの引数経由 IHttpContextAccessor 
        // サービスを inject できるよう設定
        private readonly IHttpContextAccessor _httpContextAccessor;

        public UserRepository(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        public string? LogCurrentUser()
        {
            // ログイン中のユーザー名を取得
            var username = _httpContextAccessor.HttpContext?
                           .User.Identity?.Name;
            return username;
        }
    }

    public interface IUserRepository
    {
        public string? LogCurrentUser();
    }
}

上のカスタムクラスを利用する場合は、組み込みの DI 機能を使ってカスタムクラスのインスタンスが渡されるようにします。例えば、MVC の Controller の場合、以下のようにコンストラクタの引数を経由してカスタムクラスのインスタンスを渡しています。

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using MvcNet7App.Libraries;
using MvcNet7App.Models;
using System.Diagnostics;

namespace MvcNet7App.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        // 組み込みの DI 機能によりコンストラクタの引数経由で
        // カスタムクラスのインスタンスが渡される
        private readonly Libraries.IUserRepository _user;

        public HomeController(ILogger<HomeController> logger, 
                              IUserRepository user)
        {
            _logger = logger;
            _user = user;
        }

        public IActionResult Index()
        {
            ViewBag.Username = _user.LogCurrentUser();

            return View();
        }

        // ・・・中略・・・
    }
}

ユーザーが上の Home/Index を要求すると、ASP.NET は HomeController のインスタンスを生成して要求を処理しますが、その際 DI コンテナに登録された UserRepository クラスと IHttpContextAccessor サービスのインスタンスは自動的・連鎖的に生成され、コンストラクタの引数経由で inject されます。

なので、Index アクションメソッドで、inject された UserRepository クラスの LogCurrentUser メソッドを使ってログイン中のユーザー名を取得できます。

取得したユーザー名を ViewBag を使って View に渡し、ブラウザ上に表示したのがこの記事の一番上の画像です。赤枠の中の surfer@mail.example.com がそれです。


なお、Controller や View から HttpContext にアクセスする場合は、DI 機能などは使う必要はなく、もっと簡単にできます。詳しくは「ASP.NET Core の HttpContext にアクセスする」を見てください。

Tags: , ,

CORE

WinForms アプリで構成情報の上書き (CORE)

by WebSurfer 19. August 2023 13:26

Windows Forms アプリで接続文字列その他の構成情報がデバッグ版とリリース版で異なるケースがあると思います。ターゲットフレームワークが .NET 6.0 の Windows Forms アプリで、デバッグ版とリリース版での構成情報を別々の json 形式のファイルに格納し、プログラムで構成情報を上書きして使用する方法を書きます。

(注: ターゲットフレームワークが .NET Framework 4.8 の Windows Forms アプリでも Microsoft.Extensions.Configuration.Json を NuGet からインストールして以下の記事と同様なことは可能です)

先の記事「WinForms で構成情報とコンテキストの DI (CORE)」で、Windows Forms アプリでも json 形式の構成ファイルに構成情報を格納し、それから情報を取得してアプリで使用できることを書きました。

(Microsoft のドキュメント「.NET Framework から .NET にアップグレードした後に最新化する」でも、.NET アプリでは App.config ではなくて json 形式のファイルを構成ファイルとして利用することが推奨されているようです)

ASP.NET Core アプリでは、appsettings.json に加えて appsettings.Production.json を追加しておけば、プロジェクトに組み込みの機能が運用環境での環境変数を読み取って、それが Production であれば自動的に appsettings.Production.json から構成情報を取得して上書き・追加を行うようになっています。詳しくは先の記事「環境別の appsettings.json (CORE)」を見てください。

Windows Forms アプリには ASP.NET Core のような組み込みの機能はありませんが、アプリのプログラムで動的に appsettings.json の情報を appsettings.Production.json の情報で上書きすることができます。(注: ASP.NET Core アプリと違って json ファイルの名前は任意です。この記事では ASP.NET Core アプリに合わせていますが)

接続文字列がデバッグ版とリリース版で異なるような場合は、デバッグ版の接続文字列は appsettings.json に設定し、リリース版の接続文字列は appsettings.Production.json に設定しておいて、アプリのプログラムで動的に appsettings.Production.json に設定した接続文字列を使用するということも可能です。

具体例を書きます。まず、以下のような json 形式の構成ファイルを用意します。ファイル名は ASP.NET Core の例に合わせて appsettings.json, appsettings.Production.json としましたが、ASP.NET Core の機能を使うわけではないので、任意です。

appsettings.json

{
  "ConnectionStrings": {
    "NorthwindConnection": "Data Source=(localdb)\\MSSQLLocalDB;Database=NORTHWIND;Integrated Security=True"
  },
  "FilePath": "C:\\Users\\surfe\\Documents",
  "FileName": "development.txt"
}

appsettings.Production.json

{
  "ConnectionStrings": {
    "NorthwindConnection": "Server=MyServer;Database=NORTHWIND;Integrated Security=True"
  },
  "FileName": "production.txt",
  "AdditionalInfo": "project dependent info"
}

上の例で、接続文字列 NorthwindConnection とファイル名 FileName はデバッグ版とリリース版で異なる、パス名 FilePath は共通、AdditionalInfo はリリース版でのみ必要な追加の情報という想定です。

appsettings.json の情報を appsettings.Production.json の情報で上書きする具体例は以下の通りです。説明はコメントに書きましたのでそれを読んでください。(ASP.NET Core アプリのように、環境変数を読み込んでそれにより json ファイルを切り替えるところまでは下のコードには含んでいません。無条件で上書きされるので注意してください)

using Microsoft.Extensions.Configuration;

namespace WinFormsApp5
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            // appsettings.json にはデバッグ版の接続文字列その他の
            // 構成情報を含む

            // appsettings.Production.json にはリリース版の接続��字
            // 列、その他リリース版では異なる情報、追加でリリース版
            // に必要な情報を含む

            // IConfiguration (アプリケーションで使用するキー/値ベー
            // スの構成情報) をビルドするための ConfigurationBuilder
            // を生成
            var builder = new ConfigurationBuilder()
                // ファイルベースのプロバイダ (appsettings.json) が
                // 存在するディレクトリを指定。下のコードで指定して
                // いるのはアプリケーションの現在の作業ディレクトリ
                .SetBasePath(Directory.GetCurrentDirectory())
                // 指定されたディレクトリの appsettings.json (JSON
                // 構成プロバイダ) を ConfigurationBuilder に追加
                .AddJsonFile("appsettings.json", optional: false);

            // 加えて、appsettings.Production.json を追加
            builder.AddJsonFile("appsettings.Production.json",
                                optional: false);

            // 追加されたプロバイダは以下のようにして確認できる
            var sources = builder.Sources;

            // ソースに登録されているプロバイダーのキーと値を使用し
            // て IConfiguration をビルド。その時、appsettings.json
            // と同じ名前のキーが appsettings.Production.json にあ
            // る場合は後者の情報で上書きされる
            IConfiguration config = builder.Build();

            // IConfiguration から構成情報を取得
            string? connString = 
                config.GetConnectionString("NorthwindConnection");
            string? filePath = config["FilePath"];
            string? fileName = config["FileName"];
            string? addInfo = config["AdditionalInfo"];
        }
    }
}

結果は以下の通りとなります。appsettings.json の情報が appsettings.Production.json の情報で上書きされているのが分かりますでしょうか?

構成情報の上書き

Tags: , , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  November 2023  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

View posts in large calendar