WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

ASP.NET Core Identity 独自実装(その1)

by WebSurfer 4. September 2020 15:30

ASP.NET Core Identity によるユーザー認証に用いるデータソースとプロバイダを独自実装する話を書きます。認証機能の全てを独自実装するわけではありません(そんな実力は自分にはありません)。具体的には、下の画像(Microsoft のドキュメントより借用)にある Data Source, Data Access Layer, Identity Store を自力で実装するだけです。

ASP.NET Identity のアーキテクチャ

この記事のタイトルが「その1」となっているのは、まずはロール機能なしでユーザー認証を実装して「その1」として書き、ロール機能の追加を「その2」で書く予定だからです。(「その2」は途中で挫折するかもしれませんが・・・)

Data Source はユーザー名とパスワードだけを保持する SQL Server とし、Data Access Layer には Entity Framework を用い、Identity Store(プロバイダ)を独自実装して、Register, Login, Logout という基本機能を ASP.NET Core 3.1 MVC アプリで使えるようにしてみました。(アカウントロックアウト、メールを使ったアカウント確認とパスワードリセット、二要素認証などの機能は実装しません)

やってみて思ったのは、既存のデータベースにユーザー情報があってそれをどうしても使わなければならないということでなければ、独自実装を行うのに必要な時間と労力に見合うメリットはなさそうということです。(Visual Studio のテンプレートで「個別のユーザーアカウント」を選んでプロジェクトを生成すれば済むはずですので)

ということで、この記事の情報はあまり役に立たないかもしれませんが、せっかく実装して動くようになったので備忘録として書いておきます。

基本的なことは Mcrosoft のドキュメント Custom storage providers for ASP.NET Core Identity が参考になると思います。それに、"To create a custom storage provider, create the data source, the data access layer, and the store classes that interact with this data access layer (上の画像の緑色と灰色の箱). You don't need to customize the managers or your app code that interacts with them (上の画像の青色の箱)." と書いてあり、それに従いました。

クラスやメソッドのコードなど具体的な実装方法は Using your own database schema and classes with ASP.NET Core Identity and Entity Framework CoreCustomize ASP.NET Core Identity を参考にさせていただきました。

それらの記事をベースに、上の画像の緑色と灰色の箱の部分を自力で実装した手順を以下に書きます。

(1) プロジェクトの作成

プロジェクトの作成

Visual Studio 2019 で Core 3.1 ベースの ASP.NET MVC アプリを認証なしで作成します。プロジェクト名は MvcIdCustom としました。(プロジェクト名は任意です)

(2) NuGet パッケージ

NuGet パッケージ

NuGet パッケージをインストールします。画像の上から 3 つは必須のようです。一番下のパッケージは Visual Studio で Controller / View を生成する場合に必要なもので、ASP.NET Identity とは直接関係なさそうです。

(3) コンテキストクラスの実装

プロジェクトに DAL フォルダを新規に作ってそこにコンテキストクラス DataContext.cs を実装します。フォルダの名前やクラス名は任意ですが、これを使うコードで名前空間の指定などに影響しますので注意してください。

using MvcIdCustom.Models;
using Microsoft.EntityFrameworkCore;

namespace MvcIdCustom.DAL
{
    public class DataContext : DbContext
    {
        public DataContext(DbContextOptions<DataContext> options) : 
            base(options)
        {
        }

        public DbSet<Role> Roles { get; set; }

        public DbSet<User> Users { get; set; }

        public DbSet<UserRole> UserRoles { get; set; }
    }
}

(4) エンティティクラスの実装

既存の Models フォルダにエンティティクラス User, Role, UserRole を実装します。場所は Models フォルダでなくても良いですが、これを使うコードで名前空間の指定などに影響しますので注意してください。

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcIdCustom.Models
{
    [Table("User")]
    public class User
    {
        [Key, Required]
        public int Id { get; set; }

        [Required, MaxLength(128)]
        public string UserName { get; set; }

        [Required, MaxLength(1024)]
        public string PasswordHash { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }

    [Table("Role")]
    public class Role
    {
        [Key, Required]
        public int Id { get; set; }

        [Required]
        public string Name { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }

    [Table("UserRole")]
    public class UserRole
    {
        [Key, Required]
        public int Id { get; set; }

        [Required, ForeignKey(nameof(User))]
        public int UserId { get; set; }

        [Required, ForeignKey(nameof(Role))]
        public int RoleId { get; set; }

        public virtual Role Role { get; set; }
        public virtual User User { get; set; }
    }
}

User クラスは「個別のユーザーアカウント」を選んでプロジェクトを生成した場合に使われる IdentityUser クラスに該当します。

(5) service と接続文字列の追加

上のコンテキストクラスとエンティティクラスをベースに、Entity Framework Code First の機能を利用して SQL Server に DB を生成するのですが、その前に以下の設定をしておきます。

i) Startup.cs に service を追加

以下のコードを Startup.cs 内の ConfigureServices メソッドに追加します。接続文字列名は任意です。この記事では "Default" としています。

services.AddDbContext<DataContext>(options =>
    options.UseSqlServer(
        Configuration.GetConnectionString("Default")));

ii) 接続文字列を appsettings.json に追加

プロジェクトに appsettings.json ファイルがあるはずなので、それを開いて以下のような接続文字列情報を追加します。全体的に有効な JSON 形式となるよう注意。

"ConnectionStrings": {
    "Default": "Server=(local)\\sqlexpress;Database=MvcIdCustom ..."
}

(6) Add-Migration, Update-Database

Visual Studio のパッケージマネージャーコンソールから Add-Migration CustomUserData コマンド(名前 CustomUserData は任意)を実行します。成功するとプロジェクトに Migrations フォルダと、その中に xxx_CustomUserData.cs ファイルが作られるはずです (xxx はコマンドの実行日時)。

その後 Update-Database を実行すれば、接続文字列に指定した名前(上の例では MvcIdCustom)で DB が生成され、その中にエンティティクラスの Table 属性に指定した名前のテーブルが生成されているはずです。

ここまでで、この記事の一番上の画像の Data Source と Data Access Layer の実装が終わったことになります。次はその上の Identity Store レイヤーの中の UserStore クラスの実装です。

(7) UserStore クラスの実装

参考にした記事をベースに、プロジェクトの DAL フォルダに、UserStore.cs として以下のように実装しました。この上のレイヤーにある UserManager, SignInManager がこのクラスに定義したメソッドを使って動くようです。

実装しましたとか簡単に書きましたが、上位レイヤーと調和を取って動くようにするにはどのように実装すべきかが全く分からず、実はこの実装が一番難問でした。パスワードのハッシュなど行われますが、何処でどう動いているのかさっぱり分かっていません。

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using MvcIdCustom.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace MvcIdCustom.DAL
{
    public class UserStore : IUserStore<User>, 
                             IUserPasswordStore<User>, 
                             IQueryableUserStore<User>
    {
        private DataContext db;

        public UserStore(DataContext db)
        {
            this.db = db;
        }

        public void Dispose()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (db != null)
                {
                    db.Dispose();
                    db = null;
                }                
            }
        }

        // IQueryableUserStore<User> のメンバー
        public IQueryable<User> Users
        {
            get { return db.Users.Select(u => u); }
        }

        // IUserStore<TUser>
        public Task<string> GetUserIdAsync(User user, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));

            return Task.FromResult(user.Id.ToString());
        }

        // IUserStore<TUser>
        public Task<string> GetUserNameAsync(User user, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));

            return Task.FromResult(user.UserName);
        }

        // IUserStore<TUser>
        public Task SetUserNameAsync(User user, 
            string userName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));
            if (string.IsNullOrEmpty(userName)) 
                throw new ArgumentException("userName");

            user.UserName = userName;

            return Task.CompletedTask;
        }

        // IUserStore<TUser>
        public Task<string> GetNormalizedUserNameAsync(User user, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));

            return Task.FromResult(user.UserName);
        }

        // IUserStore<TUser>
        public Task SetNormalizedUserNameAsync(User user, 
            string normalizedName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));
            if (string.IsNullOrEmpty(normalizedName)) 
                throw new ArgumentException("normalizedName");

            return Task.CompletedTask;
        }

        // IUserStore<TUser>
        // 同じユーザー名の二重登録の防止機能は上位レイヤーに組み込まれている
        public async Task<IdentityResult> CreateAsync(User user, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));

            db.Add(user);

            int rows = await db.SaveChangesAsync(cancellationToken);

            if (rows > 0)
            { 
                return IdentityResult.Success;
            }

            return IdentityResult.Failed(
                new IdentityError { Description = $"{user.UserName} 登録失敗" });
        }

        // IUserStore<TUser>
        // 同じユーザー名の二重登録の防止機能は上位レイヤーに組み込まれている
        public async Task<IdentityResult> UpdateAsync(User user, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));

            db.Update(user);

            int rows = await db.SaveChangesAsync(cancellationToken);

            if (rows > 0)
            {
                return IdentityResult.Success;
            }

            return IdentityResult.Failed(
                new IdentityError { Description = $"{user.UserName} 更新失敗" });
        }

        // IUserStore<TUser>
        public async Task<IdentityResult> DeleteAsync(User user, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));

            db.Remove(user);

            int rows = await db.SaveChangesAsync(cancellationToken);

            if (rows > 0)
            {
                return IdentityResult.Success;
            }

            return IdentityResult.Failed(
                new IdentityError { Description = $"{user.UserName} 削除失敗" });
        }

        // IUserStore<TUser>
        public async Task<User> FindByIdAsync(string userId, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();

            if (int.TryParse(userId, out int id))
            {
                return await db.Users.SingleOrDefaultAsync(u => u.Id == id, 
                    cancellationToken);
            }
            else
            {
                return await Task.FromResult<User>(null);
            }
        }

        // IUserStore<TUser>
        public async Task<User> FindByNameAsync(string normalizedUserName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (string.IsNullOrEmpty(normalizedUserName))
                throw new ArgumentException("normalizedUserName");

            User user = await db.Users.SingleOrDefaultAsync(
                u => u.UserName.Equals(normalizedUserName.ToLower()), 
                cancellationToken);

            return user;
        }

        // IUserPasswordStore<TUser>
        public Task SetPasswordHashAsync(User user, 
            string passwordHash, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));
            if (string.IsNullOrEmpty(passwordHash))
                throw new ArgumentException("passwordHash");

            user.PasswordHash = passwordHash;

            return Task.CompletedTask;
        }

        // IUserPasswordStore<TUser>
        public Task<string> GetPasswordHashAsync(User user, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));

            return Task.FromResult(user.PasswordHash);
        }

        // IUserPasswordStore<TUser>
        public Task<bool> HasPasswordAsync(User user, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));

            return Task.FromResult(
                !string.IsNullOrWhiteSpace(user.PasswordHash));
        }
    }
}

ロール機能を使えるようにするには IUserRoleStore<User> を継承し、AddToRoleAsunc, GetRolesAsync, IsInRoleAsync, RemoveFromRoleAsync メソッドを実装する必要があるかもしれませんが、それは「その2」で考えることにします。

(8) RoleStore クラスの実装

この記事「その1」ではロール機能は実装しないので必要ないのですが、「その2」で実装する予定ですので骨組みだけ作っておくことにしました。

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using MvcIdCustom.Models;

namespace MvcIdCustom.DAL
{
    public class RoleStore : IRoleStore<Role>, IQueryableRoleStore<Role>
    {
        public IQueryable<Role> Roles => throw new NotImplementedException();

        public Task<IdentityResult> CreateAsync(Role role, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<IdentityResult> DeleteAsync(Role role, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public void Dispose()
        {
            //throw new NotImplementedException();
        }

        public Task<Role> FindByIdAsync(string roleId, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<Role> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<string> GetNormalizedRoleNameAsync(Role role, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<string> GetRoleIdAsync(Role role, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<string> GetRoleNameAsync(Role role, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task SetNormalizedRoleNameAsync(Role role, string normalizedName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task SetRoleNameAsync(Role role, string roleName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<IdentityResult> UpdateAsync(Role role, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
    }
}

上の RoleStore クラスの中身は Visual Studio で自動生成できます。Dispose メソッドの throw new NotImplementedException(); をコメントアウトしておかないとエラーになるので注意してください。

(9) Startup.cs への追加

以下のコードを Startup.cs 内の ConfigureServices メソッドに追加します。

// Tell identity to use custom classes for users and roles
services.AddIdentity<User, Role>()
    .AddDefaultTokenProviders();

// Tell identity to use custom storage provider for users
services.AddTransient<IUserStore<User>, UserStore>();

// Tell identity to use custom storage provider for roles
services.AddTransient<IRoleStore<Role>, RoleStore>();

さらに、認証ミドルウェアを使うために app.UseAuthorization(); を Configure メソッドに追加します。順番があるので注意してください。以下の場所に追加します。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ・・・中略・・・

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseRouting();

    // 以下の一行を追加
    app.UseAuthentication();
            
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

以上でこの記事の一番上の画像の Data Source, Data Access Layer, Identity Store レイヤーの実装は完了です。ここまでの実装で、先の記事「ASP.NET Identity のユーザー管理 (CORE)」と同じ手段でユーザー情報の表示、追加、変更、削除が可能になることは確認しました。

次は Register, Login, Logout 機能の実装ですが、それを続けてここに書くと一つの記事としては長くなりすぎるので、別の記事「Register, Login, Logout 機能の実装」に書きました。


「その2」も書きました。こちらの記事「ASP.NET Core Identity 独自実装(その2)」です。

Tags: , , , ,

CORE

連動ドロップダウンリスト (MVC5 版)

by WebSurfer 22. August 2020 14:53

ASP.NET MVC での連動ドロップダウンリストの実装方法を備忘録として書いておきます。

ASP.NET MVC の連動 DropDownList

ASP.NET Web Forms アプリの場合は先の記事「DetailsView 中の連動 DropDownList」に書きました。それと同様な連動機能を MVC アプリで実装してみます。

記事「DetailsView 中の連動 DropDownList」と同様に、Northwind サンプルデータベースの Categories テーブルと Products テーブルを使って例を書きます。

上の画像に示すように 2 つのドロップダウンリストを配置し、1 つめのドロップダウンリストで分類を選択すると、2 つめのドロップダウンリストにはその分類に属する製品が絞り込まれて表示されるようにします。

データベースへのアクセスやデータの取得は先の記事「スキャフォールディング機能」のステップ 1 ~ 10 で書きました Entity Data Model (EDM) を利用します。Categories テーブルと Products テーブルの EDM ダイアグラムは以下の通りです。

Categories  テーブルと Products テーブル

記事「DetailsView 中の連動 DropDownList」の場合は、分類ドロップダウンリストの選択が変更されるとポストバックがかかり、サーバー側で変更に応じて製品ドロップダウンリストの内容を書き換えてページ全体を再描画するというものでした。いちいちポストバックして再描画というのがちょっとやりすぎの感があります。

Ajax Control Toolkit の中に CascadingDropDown というものがありますが、こちらは Ajax を利用して、上位ドロップダウンリストの変更があると、サーバー側の Web サービスに下位ドロップダウンリストに表示するデータを要求し、Web サービスから戻ってきた JSON 形式のデータで下位ドロップダウンリストの内容を書き換えるというものです。

この記事の MVC 版連動ドロップダウンリストでも、Ajax Control Toolkit の CascadingDropDown のやり方にならって、jQuery ajax を利用して、分類ドロップダウンリストの選択に応じて製品ドロップダウンリストに表示する ProductID と ProductName を JSON 形式で取得し、製品ドロップダウンリストの内容を書き換えるようにしました。

コードは以下の通りです。説明はコード内にコメントとして書きましたのでそちらを見てください。手抜きでスミマセン。

Model

using System.ComponentModel.DataAnnotations;

namespace Mvc5App.Models
{
    public class Sales
    {
        public int Id { get; set; }

        [Display(Name = "分類")]
        [Required(ErrorMessage = "{0} の選択は必須")]
        public int CategoryID { get; set; }

        [Display(Name = "製品")]
        [Required(ErrorMessage = "{0} の選択は必須")]
        public int ProductID { get; set; }

        [Display(Name = "コメント")]
        [Required(ErrorMessage = "{0} は必須")]
        public string Comment { get; set; }
    }
}

Controller / Action Method

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Net;
using System.Web;
using System.Web.Mvc;
using Mvc5App;

namespace Mvc5App.Controllers
{
    public class ProductsController : Controller
    {
        private NORTHWINDEntities db = new NORTHWINDEntities();

        public ActionResult CascadingDropDown()
        {
            ViewBag.CategoryID = 
                new SelectList(db.Categories, "CategoryID", "CategoryName");
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult CascadingDropDown(
            [Bind(Include = "ProductID,CategoryID,Comment")] 
            Mvc5App.Models.Sales sales)
        {
            if (ModelState.IsValid)
            {
                return RedirectToAction("Index");
            }

            // 検証結果 NG で再描画する際は以下のように再度 ViewBag
            // に SelectList を設定しないと分類ドロップダウンの中身
            // が表示されないので注意
            ViewBag.CategoryID = 
                new SelectList(db.Categories, "CategoryID", "CategoryName");
            return View(sales);
        }

        // 製品ドロップダウンに表示する ProductID と ProductName を
        // JSON 形式で取得するアクションメソッド。引数 id が分類ドロ
        // ップダウンで選択された CategoryID
        public async Task<ActionResult> GetProducts(int id)
        {
            var products = from p in db.Products
                           where p.CategoryID == id
                           select new
                           {
                               ProductID = p.ProductID,
                               ProductName = p.ProductName
                           };
            return Json(await products.ToListAsync(), 
                        JsonRequestBehavior.AllowGet);
        }
    }
}

View

@model Mvc5App.Models.Sales

@{
    ViewBag.Title = "CascadingDropDown";
}

<h2>CascadingDropDown</h2>


@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Sales</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.CategoryID, 
                htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.DropDownList("CategoryID", null, 
                    "▼分類を選択してください▼", 
                    htmlAttributes: new { @class = "form-control" })
                @Html.ValidationMessageFor(model => model.CategoryID, 
                    "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.ProductID, 
                htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                <select class="form-control" id="ProductID" name="ProductID"></select>
                @Html.ValidationMessageFor(model => model.ProductID, 
                    "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Comment, 
                htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Comment, 
                    new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Comment, 
                    "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")

    <script type="text/javascript">
        //<![CDATA[

        // 長くなるので JavaScript / jQuery のコードは下に別掲

        //]]>
    </script>
}

JavaScript / jQuery

実際は以下のコードは View の @section Scripts { ... } ブロックの中にインラインで書いています。View のコードが長くなって見難くなるので別掲にしました。

$(function () {
    // 分類ドロップダウンの jQuery オブジェクトを取得。
    // html コードの id は html ヘルパの Id メソッドで取得
    var categoryDDL = $('#@Html.Id("CategoryID")');

    // 製品ドロップダウンの jQuery オブジェクトを取得。
    // こちらは <select id="ProductID" とハードコーディン
    // グされているので以下で OK (Id メソッドは不要)
    var productDDL = $("#ProductID");

    // DropDownList の第 3 引数 "▼分類を選択してください▼"
    // は以下の option 要素になる (必ず value="" になると書
    // いた Microsoft の文書が見つからないのが不安要素):
    // <option value="">▼分類を選択してください▼</option>

    // 以下は初期画面および検証結果 NG での再描画の際必要。
    // 初期画面では categoryDDL.val() は上の option 要素の
    // value="" を取得するので if 文の条件は false となる。
    // 検証結果 NG で再描画される際は分類ドロップダウンの
    // 選択に応じて製品ドロップダウンの内容を書き換える
    if (categoryDDL.val() != "") {
        productDDL.children().remove();
        productDDL.append(
            '<option value="">▼製品を選択してください▼</option>');
        productDDL.removeAttr('disabled');

        // 分類ドロップダウンの選択結果 (CategoryID) を
        // jQuery ajax でサーバー側の GetProducts アクション
        // メソッドに送信。アクションメソッドは Products テ
        // ーブルに SELECT クエリを発行し、CategoryID に属す
        // 製品の ProductID, ProductName を取得して JSON 文
        // 字列として返す。その JSON 文字列から option 要素
        // の文字列を組み立てて製品ドロップダウンに追加する
        $.ajax({
            url: '/Products/GetProducts/' + categoryDDL.val(),
            method: 'get',
        }).done(function (data) {
            $.each(data, function (key, val) {
                productDDL.append('<option value=' +
                    val.ProductID + '>' + val.ProductName +
                    '</option>');
            });
        }).fail(function (jqXHR, textStatus, errorThrown) {
            alert('Error getting products!');
        });
    } else {
        productDDL.children().remove();
        productDDL.attr('disabled', 'disabled');
    }

    // 分類ドロップダウンの選択が変更されると change イベント
    // が発生 するのでそのリスナで製品ドロップダウンの内容を
    // 分類に応じて書き換え。リスナの中身は上と同じコード
    categoryDDL.on("change", function () {
        if (categoryDDL.val() != "") {
            productDDL.children().remove();
            productDDL.append(
                '<option value="">▼製品を選択してください▼</option>');
            productDDL.removeAttr('disabled');

            $.ajax({
                url: '/Products/GetProducts/' + categoryDDL.val(),
                method: 'get',
            }).done(function (data) {
                $.each(data, function (key, val) {
                    productDDL.append('<option value=' +
                        val.ProductID + '>' + val.ProductName +
                        '</option>');
                });
            }).fail(function (jqXHR, textStatus, errorThrown) {
                alert('Error getting products!');
            });
        } else {
            productDDL.children().remove();
            productDDL.attr('disabled', 'disabled');
        }
    });
});

このコードで、この記事の上の画像のとおり表示されます。

分類ドロップダウンリストの選択変更に連動して製品ドロップダウンリストの内容が変わります。送信ボタンクリックの際のドロップダウンリストの検証は、クライアント側ではかかりませんが、サーバー側では検証されてエラーメッセージは期待通り表示されます。

サーバー側での検証結果が NG の場合、同じ画面が再描画されユーザーに選択の修正を促すようにしていますが、その際のドロップダウンリストの連動も OK なことを確認しています。

Tags: , ,

MVC

Entity Framework 6 Power Tools

by WebSurfer 19. August 2020 15:12

Entity Framework Code First でデータベースを生成するときに作成するコンテキストクラスから、各テーブルに属するプロパティとテーブル間のリレーションを現すダイアグラムを表示するツールを紹介します。(注: .NET Framework 用です。Core には使えません)

EDM ダイアグラム

上の画像がそのツールを使って表示したダイアグラムです。(既存のデータベースから Visual Studio の ADO.NET Entity Data Model デザイナを使って EDM を作る場合はここで紹介するツールは不要で、自動生成される .edmx ファイルに表示されます。具体例は、先の記事「スキャフォールディング機能」のステップ 1 ~ 10 参照)

ツールは Microsoft の ASP.NET MVC5 のチュートリアルの「Entity Framework パワーツール」に紹介されているものです。以下のサイトからダウンロードできます。

Entity Framework 6 Power Tools Community Edition

ダウンロードした EFPowerTools.vsix を実行すればツールは Visual Studio に自動的にインストールされます(VS2015, VS2019 で確認。VS2017 は持ってないので不明)。下の画像は VS2019 に拡張機能としてインストールされたところです。

拡張機能にインストール

使い方は次の通りです。

例えば、Entity Framework Code First でデータベースを生成するために以下のようなコンテキストクラスを定義したとします。

コンテキストクラス

Visual Studio のソリューションエクスプローラーからそのファイル(下の画像の SchoolContext.cs)を右クリックし、表示されたメニューから[View Entity Data Model (Read-only)]を選択します。

View Entity Data Model (Read-only)

そうすればこの記事の一番上の画像の通り Entity Data Model のダイアグラムが表示されます。

Tags: , ,

DevelopmentTools

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  September 2020  >>
MoTuWeThFrSaSu
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar