WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

異なるデータソースの結合と表示

by WebSurfer 2017年11月26日 13:34

異なるデータソース(例えば SQL Server と CSV ファイル)のレコードを内部結合または左外部結合して GridView などに一覧表示する例を書きます。

GridView に表示

上の画像はこの記事で紹介するサンプルの実行結果で、SQL Server と CSV ファイルをデータソースに使い、左側が内部結合、右側が左外部結合した結果一覧を ASP.NET Web Forms アプリの GridView に表示したものです。

この記事で使用したデータソースは、Microsoft が提供しているサンプルデータベース Northwind の Orders テーブルと、Customers テーブルから一部のフィールド / レコードを抜き出して作った以下の画像の CSV ファイルです。

CSV ファイル内容

データソースが両方とも SQL Server のサンプルデータベース Northwind にあれば、SELECT クエリで JOIN 句を使って結合し、その結果を DataTable などに取得するのが簡単ですが、一方が CSV ファイルではそうはいきません。

ではどうするかと言うと、SQL Server のテーブルと CSV ファイルそれぞれから List<T> 型のオブジェクトを作り、それを Linq で結合した結果を GridView のデータソースとしてバインドしてやるのがよさそうです。

Linq を使って結合する例は Microsoft の文書「join 句 (C# リファレンス)」やそれからリンクが張ってある記事が参考になりました。

上の画像を表示したサンプルコードは以下の通りです。説明はコメントとして書きましたので、それを見てください。(手抜きでスミマセン)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data;
using System.Data.SqlClient;
using System.Web.Configuration;

// SQL Server の Northwind サンプルデータベース Orders
// テーブルのレコードを格納するクラス定義
public class Order
{
    public int OrderID { get; set; }
    public string CustomerID { get; set; }
    public int& EmployeeID { get; set; }
    public DateTime& OrderDate { get; set; }
    public DateTime& RequiredDate { get; set; }
    public DateTime& ShippedDate { get; set; }
    public int& ShipVia { get; set; }
    public decimal& Freight { get; set; }
    public string ShipName { get; set; }
    public string ShipAddress { get; set; }
    public string ShipCity { get; set; }
    public string ShipRegion { get; set; }
    public string ShipPostalCode { get; set; }
    public string ShipCountry { get; set; }
}

// CSV ファイルのレコードを格納するためのクラス定義
public class Customer
{
    public string CustomerID { get; set; }
    public string CompanyName { get; set; }
    public string ContactName { get; set; }
    public string ContactTitle { get; set; }
}

// 結合後の結果を格納するためのクラス定義
public class Result
{
    public int OrderID { get; set; }
    public string CompanyName { get; set; }
    public DateTime& OrderDate { get; set; }
    public decimal& Freight { get; set; }
}

public partial class _0019_GridViewJoinedList : 
    System.Web.UI.Page
{
  // SQL Server のサンプルデータベース Northwind の
  // Orders テーブルからデータを取得して List<Order>
  // オブジェクトを生成。Entity Framework を使う方が簡単
  // だが、ここではプリミティブに ADO.NET の SqlDataReader 
  // を使用した。
  protected List<Order> CreateOrderList()
  {
    List<Order> orders = new List<Order>();

    string connString = WebConfigurationManager.
            ConnectionStrings["NORTHWINDConnectionString"].
            ConnectionString;

    string query = "SELECT [OrderID], [CustomerID]," +
            "[EmployeeID], [OrderDate], [RequiredDate]," +
            "[ShippedDate], [ShipVia], [Freight]," +
            "[ShipName], [ShipAddress], [ShipCity]," +
            "[ShipRegion], [ShipPostalCode], [shipCountry]" +
            "FROM [Orders]";

    using (SqlConnection conn = new SqlConnection(connString))
    {
      conn.Open();
      using (SqlCommand cmd = new SqlCommand(query, conn))
      {
        using (SqlDataReader reader = cmd.ExecuteReader())
        {
          if (reader != null)
          {
            while (reader.Read())
            {
              Order record = new Order();

              record.OrderID = reader.GetInt32(0);
              record.CustomerID = reader.IsDBNull(1) ?
                      null : reader.GetString(1);
              record.EmployeeID = reader.IsDBNull(2) ?
                      null : (int&)reader.GetInt32(2);
              record.OrderDate = reader.IsDBNull(3) ?
                      null : (DateTime&)reader.GetDateTime(3);
              record.RequiredDate = reader.IsDBNull(4) ?
                      null : (DateTime&)reader.GetDateTime(4);
              record.ShippedDate = reader.IsDBNull(5) ?
                      null : (DateTime&)reader.GetDateTime(5);
              record.ShipVia = reader.IsDBNull(6) ?
                      null : (int&)reader.GetInt32(6);
              record.Freight = reader.IsDBNull(7) ?
                      null : (decimal&)reader.GetDecimal(7);
              record.ShipName = reader.IsDBNull(8) ?
                      null : reader.GetString(8);
              record.ShipAddress = reader.IsDBNull(9) ?
                      null : reader.GetString(9);
              record.ShipCity = reader.IsDBNull(10) ?
                      null : reader.GetString(10);
              record.ShipRegion = reader.IsDBNull(11) ?
                      null : reader.GetString(11);
              record.ShipPostalCode = reader.IsDBNull(12) ?
                      null : reader.GetString(12);
              record.ShipCountry = reader.IsDBNull(13) ?
                      null : reader.GetString(13);

              orders.Add(record);
            }
          }
        }
      }
    }
    return orders;
  }

  // CSV ファイルからデータを取得して List<Customer> オブ
  // ジェクトを生成。
  protected List<Customer> CreateCustomerList()
  {
    List<Customer> customers = new List<Customer>();

    string csvFile = Server.MapPath("~/App_Data/TextFile.csv");

    using (Microsoft.VisualBasic.FileIO.TextFieldParser tfp =
      new Microsoft.VisualBasic.FileIO.TextFieldParser(
        csvFile,
        System.Text.Encoding.GetEncoding("Shift_JIS")))
    {
      //フィールドがデリミタで区切られている
      tfp.TextFieldType =
        Microsoft.VisualBasic.FileIO.FieldType.Delimited;
      // デリミタを , とする
      tfp.Delimiters = new string[] { "," };
      // フィールドを " で囲み、改行文字、デリミタを
      // 含めることができるか
      tfp.HasFieldsEnclosedInQuotes = true;
      // フィールドの前後からスペースを削除
      tfp.TrimWhiteSpace = true;

      while (!tfp.EndOfData)
      {
        string[] fields = tfp.ReadFields();

        Customer customer = new Customer()
        {
          CustomerID = fields[0],
          CompanyName = fields[1],
          ContactName = fields[2],
          ContactTitle = fields[3]
        };
        customers.Add(customer);
      }
    }
    return customers;
  }

  protected void Page_Load(object sender, EventArgs e)
  {
    if (!IsPostBack)
    {
      List<Order> orders = CreateOrderList();
      List<Customer> customers = CreateCustomerList();

      // 内部結合
      var innerJoin = from o in orders
                      join c in customers
                      on o.CustomerID equals c.CustomerID
                      select new Result
                      {
                        OrderID = o.OrderID,
                        CompanyName = c.CompanyName,
                        OrderDate = o.OrderDate,
                        Freight = o.Freight
                      };

      // シーケンスが空の場合に返すデフォルト値
      // 下の DefaultIfEmpty メソッドの引数に設定する
      Customer defaultValue = new Customer() {
                CustomerID = string.Empty,
                CompanyName = string.Empty,
                ContactName = string.Empty,
                ContactTitle = string.Empty };

      // 左外部結合
      var leftOuterJoin = 
          from o in orders
          join c in customers
          on o.CustomerID equals c.CustomerID into cGroup
          from item in cGroup.DefaultIfEmpty(defaultValue)
          select new Result
          {
              OrderID = o.OrderID,
              CompanyName = item.CompanyName,
              OrderDate = o.OrderDate,
              Freight = o.Freight
          };

      // 上の画像の左側の GridView(内部結合)
      GridView1.DataSource = innerJoin;
      GridView1.DataBind();

      // 上の画像の右側の GridView(左外部結合)
      GridView2.DataSource = leftOuterJoin;
      GridView2.DataBind();
    }
  }
}

Tags: , ,

ASP.NET

MonthCalendar の Size

by WebSurfer 2017年11月4日 14:32

MonthCalendar

Windows Forms アプリケーション用に、カレンダーを表示してユーザーが日付を選択できる MonthCalendar コントロールがあります。

その Size プロパティから MonthCalendar のサイズを取得する際、タイミングによっては正しいサイズが取得できない、その場合でもデバッガで MonthCalendar を開いてその中身を見ると正しいサイズになるという不可解なことがありました。

具体的には、下のコードの Button_Click メソッドをデバッガでステップ実行させ、コメント (3) の行で止めて Size プロパティを見ると 178 x 155 となっているが、calendar の中身をデバッガで開いて見た後で Size プロパティを見ると 199 x 162 と正しい値になるというものです。

その理由を調べたので備忘録として書いておきます。なお、元の話は Teratail のスレッド「VisualStudioでデバッグ中にプロパティの値が変化する」です。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

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

      Button button = new Button();
      button.Click += Button_Click;
      this.Controls.Add(button);
    }

    private void Button_Click(object sender, EventArgs e)
    {
      Form f = new Form();
      Size size1 = f.Size;    // (1) W: 300, H: 300
      f.Size = new Size(200, 200);
      size1 = f.Size;         // (2) W: 200, H: 200

      MonthCalendar calendar = new MonthCalendar();
      Size size2 = calendar.Size;   // (3) W: 178, H: 155
      calendar.Size = new Size(200, 200);
      size2 = calendar.Size;        // (4) W: 178, H: 155
      f.Controls.Add(calendar);
      size2 = calendar.Size;        // (5) W: 178, H: 155
      f.Show();
      size2 = calendar.Size;        // (6) W: 199, H: 162

      f.ClientSize = calendar.Size;
    }
  }
}

上に書いた「不可解なこと」の理由は、多少想像が入っていますが、以下のようなことだと思われます。

  1. MonthCalendar のサイズを決定するのは使用されるフォントのみ。MonthCalendar.Size プロパティの設定では変えられない・・・コメント (4) 参照。
  2. new MonthCalendar() の時点ではフォントが不明なので MonthCalendar のサイズは未定。
  3. コメント (3) の時点で Size プロパティを見ると 178 x 155 となっているが、それはデフォルト値でフォントを反映した正しいサイズではない。
  4. コメント (3) の時点で calendar.Size の calendar にマウスカーソルを当てて開くと、その時点で calendar が初期化され、使用されるフォントに応じて正しいサイズが Size プロパティに設定される。
  5. その後で calendar.Size の Size にマウスカーソルを当てると正しいサイズ 199 x 162 が取得できる。
  6. デバッガで calendar を開いて見るということをしなければ、使用されるフォントに応じて正しいサイズが Size プロパティに設定されるのは、上記のコードでは f.Show(); の時点。
  7. その後であれば、calendar.Size で正しいサイズを取得でき、それを Form の ClientSize に設定してやれば上の画像の通り calendar がフォーム内にぴったり収まる。

上のことを書いた Microsoft の公式文書などは見つからないのですが、コードで検証した結果が上記の想像は正しいことを裏付けていると思います。

Tags: ,

.NET Framework

ASP.NET Identity のロール管理 (MVC)

by WebSurfer 2017年11月2日 16:40

ASP.NET Identity ベースのフォーム認証を実装した ASP.NET MVC5 アプリケーションで、管理者がロール管理を行う機能を実装する例を書きます。先の記事「ASP.NET Identity のユーザー管理 (MVC)」の続きです。

ロール管理画面

CodeZine の記事「ASP.NET Identityでユーザーに役割(ロール)を持たせる 」に、ASP.NET Web Forms アプリ用のユーザー管理画面を作る手順が載っていますが、その MVC 版です。

ベースとなるのは、先の記事「ASP.NET Identity のユーザー管理 (MVC)」と同様、Visual Studio 2015 Community で MVC のテンプレートを利用し、認証に「個別のユーザーアカウント」を指定して自動生成させたプロジェクトです。

それにロール情報の追加、変更、削除および登録済みユーザーのロールのアサインを行う機能を実装するには以下のようにします:

(1) コードの追加

ロール情報の追加、変更、削除を行うのに用いるクラスは、IdentityRole クラスRoleManager<TRole, TKey> クラスです。

テンプレートで自動生成されたコードにはそれらのコードは含まれていませんので追記する必要があります。

具体的には、Models\IdentityModels.cs, App_Start\IdentityConfig.cs, App_Start\Startup.Auth.cs の 3 つの既存のファイルに、CodeZine の記事「ASP.NET Identityでユーザーに役割(ロール)を持たせる」を参考にコードを追加します。(記事のコードをそのままコピペすれば OK)

何を追加するかを簡単に書きますと、IdentityModels.cs, IdentityConfig.cs, Startup.Auth.cs の順に、以下の通りです。

  1. IdentityRole クラスを継承した ApplicationRole クラスを IdentityModels.cs に追加。
  2. RoleManager<TRole, TKey> クラスを継承した ApplicationRoleManager クラスを IdentityConfig.cs に追加。定義には ApplicationRoleManager クラスのインスタンスを生成して OwinContext に登録するための Create メソッドを含める。Create メソッドにはロールストアに何も定義されてない場合 Administrator という名前のロールを作成するコードも含まれています。
  3. ApplicationRoleManager のインスタンスを生成して OwinContext に登録するコードを Startup.Auth.cs の ConfigureAuth メソッドに追加。これにより、Controller で HttpContext.GetOwinContext() メソッドにより OwinContext を取得し、それから Get<ApplicationRoleManager>() メソッドで ApplicationRolerManager オブジェクトを取得できるようになります。

(2) Migrations を実施

上記 (1) の作業後アプリを実行すると、先にユーザー情報を登録して EF Code First の機能で DB を生成済みの場合は以下のようなエラーが出ると思います。ちなみに、下の画像でエラーとなっている行は、上記 (1) - 2 で追加した Create メソッドでロールストアにあるロール情報を取得するものです。

エラー画面

その場合は Migrations 実施してから再度実行してください。

具体的には、Visual Studio のパッケージマネージャーコンソールで、Enable-Migrations コマンドを実行して Code First Migrations を有効にし、次に Update-Database –Verbose コマンドを実行して保留中の移行をデータベースに適用します。

(3) AspNetRoles テーブルの確認

上記 (2) でエラーなく無事実行できれば、以下の画像のようにロール情報のストアとなる AspNetRoles テーブルに Discriminator フィールドが追加され、「結果」ウィンドウに示すように Name が Administrator というレコードができているはずです。

AspNetRoles テーブル

(4) ロール管理用 Controller / View の実装

ロール情報の追加、変更、削除および登録済みユーザーのロールのアサインを行うための Controller / View を実装します。Controller の実装例を以下に示します。説明はコード内のコメントに書きましたので、それを見てください。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

// 上は自動生成されたもの。それに以下の名前空間を追加
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using System.Net;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Mcv5App2.Models;

namespace Mcv5App2.Controllers
{
  // 以下の RoleModel, UserWithRoleInfo, RoleInfo の 3 クラス
  // はここで利用する Model。Controller 内に定義したのは単に
  // 分けるのが面倒だから

  // アクションメソッド Create, Edit 用の Model
  public class RoleModel
  {
    [Required]
    [StringLength(
      100, 
      ErrorMessage = "{0} は {2} 文字以上",
      MinimumLength = 3)]
    [Display(Name = "Role")]
    public string Role { get; set; }
  }

  // アクションメソッド UserWithRoles, EditRoleAssignment
  //  (各ユーザーのロール一覧表示、ロールのアサイン・削除) 
  // 用の Model
  public class UserWithRoleInfo
  {
    public UserWithRoleInfo()
    {
      UserRoles = new List<RoleInfo>();
    }

    public string UserId { set; get; }
    public string UserName { set; get; }
    public string UserEmail { set; get; }

    public IList<RoleInfo> UserRoles { set; get; }
  }

  public class RoleInfo
  {
    public string RoleName { set; get; }
    public bool IsInThisRole { set; get; }
  }


  // ここから Controller のコード
  public class RolesController : Controller
  {
    private ApplicationUserManager _userManager;
    private ApplicationRoleManager _roleManager;

    public ApplicationUserManager UserManager
    {
      get
      {
        return _userManager ?? 
            HttpContext.GetOwinContext().
            GetUserManager<ApplicationUserManager>();
      }
      private set
      {
        _userManager = value;
      }
    }

    public ApplicationRoleManager RoleManager
    {
      get
      {
        return _roleManager ?? 
            HttpContext.GetOwinContext().
            Get<ApplicationRoleManager>();
      }
      private set
      {
        _roleManager = value;
      }
    }

    // GET: Roles(登録済みロールの一覧)
    // Model は ApplicationRole
    public ActionResult Index()
    {
      var roles = 
        RoleManager.Roles.OrderBy(role => role.Name);
      return View(roles);
    }

    // GET: Roles/Details/Id(指定 Id のロール詳細)
    // 上の一覧以上の情報は含まれないので不要かも・・・
    // Model は ApplicationRole
    public async Task<ActionResult> Details(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                        HttpStatusCode.BadRequest);
      }

      var target = await RoleManager.FindByIdAsync(id);
      if (target == null)
      {
        return HttpNotFound();
      }
      return View(target);
    }

    // GET: Roles/Create(新規ロール作成・登録)
    // Model は上に定義した RoleModel クラス
    public ActionResult Create()
    {
      return View();
    }

    // POST: Roles/Create(新規ロール作成・登録)
    // Model は上に定義した RoleModel クラス
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Create(RoleModel model)
    {
      if (ModelState.IsValid)
      {
        // ユーザーが入力したロール名を model.Role から取得し
        // て ApplicationRole を生成
        var role = new ApplicationRole { Name = model.Role };

        // 上の ApplicationRole から新規ロールを作成・登録
        var result = await RoleManager.CreateAsync(role);

        if (result.Succeeded)
        {
          // 登録に成功したら Roles/Index にリダイレクト
          return RedirectToAction("Index", "Roles");
        }

        // result.Succeeded が false の場合 ModelSate にエ
        // ラー情報を追加しないとエラーメッセージが出ない。
        // AccountController と同様に AddErrors メソッドを
        // 定義して利用(一番下に定義あり)
        AddErrors(result);
      }

      // ロールの登録に失敗した場合、登録画面を再描画
      return View(model);
    }

    // GET: Roles/Edit/Ed(指定 Id のロール情報の更新)
    // ここで更新できるのはロール名のみ
    // Model は上に定義した RoleModel クラス
    public async Task<ActionResult> Edit(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      var target = await RoleManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      RoleModel model = new RoleModel() { Role = target.Name };

      return View(model);
    }

    // POST: Roles/Edit/Id(指定 Id のロール情報の更新)
    // ここで更新できるのはロール名のみ
    // Model は上に定義した RoleModel クラス
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> 
                        Edit(string id, RoleModel model)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      if (ModelState.IsValid)
      {
        var target = await RoleManager.FindByIdAsync(id);

        // ユーザーが入力したロール名を model.Role から取得し
        // て ApplicationRole の Name を書き換え
        target.Name = model.Role;

        // Name を書き換えた ApplicationRole で更新をかける
        var result = await RoleManager.UpdateAsync(target);

        if (result.Succeeded)
        {
          // 更新に成功したら Roles/Index にリダイレクト
          return RedirectToAction("Index", "Roles");
        }

        // result.Succeeded が false の場合 ModelSate にエ
        // ラー情報を追加しないとエラーメッセージが出ない。
        AddErrors(result);
      }

      // 更新に失敗した場合、編集画面を再描画
      return View(model);
    }

    // GET: Roles/Delete/Id(指定 Id のロールを削除)
    // 階層更新が行われているようで、ユーザーがアサインされて
    // いるロールも削除可能。
    // Model は ApplicationRole
    public async Task<ActionResult> Delete(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      var target = await RoleManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      return View(target);
    }

    // POST: Roles/Delete/Id(指定 Id のロールを削除)
    // Model は ApplicationRole
    // 上の Delete(string id) と同シグネチャのメソッド
    // は定義できないので、メソッド名を変えて、下のよう
    // に ActionName("Delete") を設定する
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> 
                        DeleteConfirmed(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      var target = await RoleManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      // ユーザーがアサインされているロールも以下の一行で
      // 削除可能。内部で階層更新が行われているらしい。
      var result = await RoleManager.DeleteAsync(target);

      if (result.Succeeded)
      {
        // 削除に成功したら Roles/Index にリダイレクト
        return RedirectToAction("Index", "Roles");
      }

      // result.Succeeded が false の場合 ModelSate にエ
      // ラー情報を追加しないとエラーメッセージが出ない。
      AddErrors(result);

      // 削除に失敗した場合、削除画面を再描画
      return View(target);
    }


    // 以下は:
    // (1) UserWithRoles で、上の画像のように、登録済みユーザー
    //     の一覧と各ユーザーへのロールのアサイン状況を表示し、
    // (2) Edit ボタンクリックで EditRoleAssignment に遷移し、
    //     当該ユーザーへのロールのアサインを編集して保存する
    // ・・・ためのもの。

    // GET: Roles/UserWithRoles
    // ユーザー一覧と各ユーザーにアサインされているロールを表示
    // Model は上に定義した UserWithRoleInfo クラス
    public async Task<ActionResult> UserWithRoles()
    {
      var model = new List<UserWithRoleInfo>();

      // ToList() を付与してここで DB からデータを取得して
      // DataReader を閉じておかないと、下の IsInRole メソッド
      // でエラーになるので注意
      var users = UserManager.Users.
                  OrderBy(user => user.UserName).ToList();
      var roles = RoleManager.Roles.
                  OrderBy(role => role.Name).ToList();
            
      foreach (ApplicationUser user in users)
      {
        UserWithRoleInfo info = new UserWithRoleInfo();
        info.UserId = user.Id;
        info.UserName = user.UserName;
        info.UserEmail = user.Email;

        foreach (ApplicationRole role in roles)
        {
          RoleInfo roleInfo = new RoleInfo();
          roleInfo.RoleName = role.Name;
          roleInfo.IsInThisRole = await
              UserManager.IsInRoleAsync(user.Id, role.Name);
          info.UserRoles.Add(roleInfo);
        }
        model.Add(info);
      }

      return View(model);
    }

    // GET: Roles/EditRoleAssignment/Id
    // 指定 Id のユーザーのロールへのアサインの編集
    // Model は上に定義した UserWithRoleInfo クラス
    public async Task<ActionResult> 
                        EditRoleAssignment(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      var user = await UserManager.FindByIdAsync(id);

      if (user == null)
      {
        return HttpNotFound();
      }

      UserWithRoleInfo model = new UserWithRoleInfo();
      model.UserId = user.Id;
      model.UserName = user.UserName;
      model.UserEmail = user.Email;

      // ToList() を付与しておかないと下の IsInRole メソッド
      // で DataReader が閉じてないというエラーになる
      var roles = RoleManager.Roles.
                  OrderBy(role => role.Name).ToList();

      foreach (ApplicationRole role in roles)
      {
        RoleInfo roleInfo = new RoleInfo();
        roleInfo.RoleName = role.Name;
        roleInfo.IsInThisRole = await
            UserManager.IsInRoleAsync(user.Id, role.Name);
        model.UserRoles.Add(roleInfo);
      }

      return View(model);
    }

    // GET: Roles/EditRoleAssignment/Id
    // 指定 Id のユーザーのロールへのアサインの編集
    // Model は上に定義した UserWithRoleInfo クラス
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> 
      EditRoleAssignment(string id, UserWithRoleInfo model)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      if (ModelState.IsValid)
      {
        IdentityResult result;

        foreach (RoleInfo roleInfo in model.UserRoles)
        {
          // id のユーザーが roleInfo.RoleName のロールに属して
          // いるか否か。以下でその情報が必要。
          bool isInRole = await 
            UserManager.IsInRoleAsync(id, roleInfo.RoleName);

          // roleInfo.IsInThisRole には編集画面でロールのチェッ
          // クボックスのチェック結果が格納されている
          if (roleInfo.IsInThisRole)
          {
            // チェックが入っていた場合

            // 既にロールにアサイン済みのユーザーを AddToRole
            // するとエラーになるので以下の判定が必要
            if (isInRole == false)
            {
              result = await UserManager.
                       AddToRoleAsync(id, roleInfo.RoleName);
              if (!result.Succeeded)
              {
                AddErrors(result);
                return View(model);
              }
            }
          }
          else
          {
            // チェックが入っていなかった場合

            // ロールにアサインされてないユーザーを
            // RemoveFromRole するとエラーになるので以下の
            // 判定が必要
            if (isInRole == true)
            {
              result = await UserManager.
                       RemoveFromRoleAsync(id, roleInfo.RoleName);
              if (!result.Succeeded)
              {
                AddErrors(result);
                return View(model);
              }
            }
          }                    
        }

        // 編集に成功したら Roles/UserWithRoles にリダイレクト
        return RedirectToAction("UserWithRoles", "Roles");
      }

      // 編集に失敗した場合、編集画面を再描画
      return View(model);
    }

    protected override void Dispose(bool disposing)
    {
      if (disposing)
      {
        if (_userManager != null)
        {
          _userManager.Dispose();
          _userManager = null;
        }

        if (_roleManager != null)
        {
          _roleManager.Dispose();
          _roleManager = null;
        }
      }

      base.Dispose(disposing);
    }

    // ModelSate にエラー情報を追加するためのヘルパメソッド
    private void AddErrors(IdentityResult result)
    {
      foreach (var error in result.Errors)
      {
        ModelState.AddModelError("", error);
      }
    }
  }
}

MSDN ライブラリの UserManager<TUser> クラスRoleManager<TRole, TKey> クラスの説明には非同期版メソッドしか書いてないのでそれらを使っていますが、同期版も拡張メソッドとして定義されているようです。ケースバイケースでどちらが適切かを考えて使い分けた方が良いかもしれません。

View はコントローラをベースにスキャフォールディング機能を利用して自動生成できます。Visual Studio でアクションメソッドのコードを右クリックして[ビューを追加(D)...]を選ぶと、そのためのダイアログが表示されます。

アクションメソッド EditRoleAssignment に対応する View は自動生成されるコードだけでは不十分でいろいろ追加・修正が必要です。以下にその完成版のコードを載せておきます。

@model Mcv5App2.Controllers.UserWithRoleInfo

@{
  ViewBag.Title = "EditRoleAssignment";
  Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>EditRoleAssignment</h2>


@using (Html.BeginForm())
{
  @Html.AntiForgeryToken()
    
  <h4>UserWithRoleInfo</h4>
  <hr />
  @Html.ValidationSummary("",new { @class="text-danger" })
  <table class="table">
    <tr>
      <th>
        @Html.DisplayNameFor(model => model.UserName)
      </th>
    @foreach (var role in Model.UserRoles)
    {
      <th>
        @Html.DisplayFor(modelItem => role.RoleName)
      </th>
    }
    </tr>
            
    <tr>
      <td>
        @Html.DisplayFor(model => model.UserName)
        @Html.HiddenFor(model => model.UserName)
        @Html.HiddenFor(model => model.UserEmail)
        @Html.HiddenFor(model => model.UserId)
      </td>
    @for (int i = 0; i < Model.UserRoles.Count; i++)
    {
      <td>
        @Html.EditorFor(model =>
                     model.UserRoles[i].IsInThisRole)
        @Html.HiddenFor(model => 
                     model.UserRoles[i].RoleName)
      </td>
    }
                    
    </tr>
  </table>
  <div class="form-group">
    <div class="col-md-offset-2 col-md-10">
      <input type="submit" value="Save" 
             class="btn btn-default" />
    </div>
  </div>
}

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

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

上の View のコードで注意すべき点は、コレクション(上の例では Model.UserRoles で取得できる RoleInfo クラスのコレクション) についてはレンダリングされる html 要素の name 属性が連番のインデックスを含むようにすることです。具体的には、name="prefix[index].Property" というパターンにします。

そうしないとモデルバインディングがうまくいきません。その理由など詳しくは先の記事「コレクションのデータアノテーション検証」を見てください。

Tags: ,

MVC

About this blog

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

Calendar

<<  2017年12月  >>
262728293012
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar