Kobarin's Development Blog

C#やASP.NETなどについての記録です。

ASP.NET MVC のDisplayFormatを表示用と編集用で分ける方法

タイトルが分かりにくいですが、正確には

  • DisplayFormat属性のDataFormatStringを、表示のみする場面(例:Index、Details)と編集する場面(例:Create、Edit)で別々に作る

といった内容です。

結論「別々に作る事はできない」

という事で、当然ですがDataFormatStringは1つしか作れないため、プロパティ自体を表示用と編集用に別々に用意する事で対応しましたが、とりあえず1つ1つ流れを見ていきます。

例えば、以下のようなProductエンティティがあるとします。

public class Product {
  public int ProductID { get; set; }
  …
  public decimal StandardCost { get; set; }
  …
}

まずはDisplayFormat

StandardCostはdecimal型なので、上記のままだと表示時も編集時も「1000.00」のような表記になりますが、このままだと見にくいと感じます。
そこで、期待する表記は「3桁区切りカンマ、小数点以下は必要次第で表示」だとします。

  • 例1:「1000」の場合なら「1,000」
  • 例2:「1234.5」の場合なら「1,234.5」

そこでDisplayFormatを下記のように記述します。

public class Product {
  …
  [DisplayFormat(DataFormatString = "{0:#,##0.#}", ApplyFormatInEditMode = true)]
  public decimal StandardCost { get; set; }
  …
}

ApplyFormatInEditMode は、編集時でも同じフォーマットを適用する場合に使う属性です(既定でfalse)。

編集の時はカンマ不要

これで期待する通りの表示になりますが、困った事に編集用のTextboxでも「1,000」のようにカンマが現れてしまいます。
しかもそのままだと検証に引っかかり「The field StandardCost must be a number(StandardCostは数字でなければなりません)」と修正を求められます。変更もないのにカンマを削除するのは面倒ですよね。


それでは…とApplyFormatInEditModeを取り除いてみます。

public class Product {
  …
  [DisplayFormat(DataFormatString = "{0:#,##0.#}")]
  public decimal StandardCost { get; set; }
  …
}

こうすると表示は変化ありませんが、編集では「1000.00」と小数点以下の余計な部分が現れてしまいます。
このままでも問題はないのですが、何となく鬱陶しいですよね。

DisplayFormatの限界…(面倒だけど)プロパティを2つ作る

このようにDisplayFormatは、表示と編集とで表記を統一する機能はあっても、別々に定義することが出来ないのです。
そこで苦肉の策となりますが、別々に定義するのであれば、以下のようにプロパティを2つに分けるしかありません。

public class Product {

  // 編集用
  [DisplayFormat(DataFormatString = "{0:#0.#}", ApplyFormatInEditMode = true)]
  public decimal StandardCost { get; set; }

  // 表示用
  [DisplayFormat(DataFormatString = "{0:#,##0.#}")]
  public decimal StandardCostDisplay { get { return StandardCost; } }
}

「表示用」でStandardCostDisplayというStandardCostを参照するだけのプロパティを作り、希望通りのフォーマットにします。
一方、「編集用」ではApplyFormatInEditModeを付け、カンマは適用しないフォーマットにします。

Viewに少し修正を加えて終了

最後に、View側を少し修正し、IndexやDetailsといった表示のViewではStandardCostDisplayをバインドし、CreateやEditはStandardCostをバインドさせます。

Index, Details
  <td>
    @Html.DisplayFor(model => model.StandardCostDisplay)
  </td>
Create, Edit
  @Html.EditorFor(model => model.StandardCost, new { htmlAttributes = new { @class = "form-control" } })

希望

本当はEditFormatみたいな属性があれば最も話が早いと思うんですが…まぁ現状ではこれしか思いつきませんでした。

ASP.NET MVCで独自DataAnnotation作成

MVCの検証に使うDataAnnotationはかなり便利で、標準のものだけでも例えば以下のようなものがあります。

  • Required : 必須項目化
  • Range : 値の範囲(例:1~100)
  • Compare : 他プロパティと同一判定(例:メルアドを2回入力した時、等)
  • RegularExpression : 正規表現(文字列の規則ならほぼ何でもあり)

これだけでも結構使えるんですが、WebFormsにあったような「greater than(より大きい)」や「less than(より小さい)」といった、他プロパティを参照するタイプの検証がCompareくらいしか用意されていません。
例えば「開始日」と「終了日」とか、「最安値」と「最高値」といった、あらかじめ上下関係が決まっている要素ってありますよね。
開始日が「2017/07/03」なのに「終了日」が「2016/06/30」ではおかしいので、こうした検証が必要な場面で使う属性がMVCにはありません。

DataAnnotation の自作?

DataAnnotationは完全独自で1からコーディングする事もできますし一番融通は効くと思われますが、やはりちょっと面倒です。興味ある方は以下の最上部の回答が参考になります。
stackoverflow.com

CustomValidation属性は?

RegularExpression等で対応しきれない場合、CustomValidationという手もあります。
エンティティ内に検証用コードを記述できるため自由度は高い反面、自作DataAnnotationのように再利用性は高くない上、やっぱりコーディングが面倒ですね。

ExpressiveAnnotations という選択肢

実際の開発の場ではほとんどは、それほど複雑な検証でなく先述のGreaterThanとかLessThan程度で済むレベルが多いのではないでしょうか。
探したところ、「ExpressiveAnnotations」という拡張機能があり、Nugetからインストールできます。

ExpressiveAnnotations の使い方

Nugetからインストールした後、エンティティ定義をします。
この例では、イベント情報をモデルに進めてみます。

using ExpressiveAnnotations.Attributes;


public class DemoEventModel
{
public int Id { get; set; }


[DisplayName("名称")]
[Required]
public string Name { get; set; }


[DisplayName("最安価格")]
public int? LowPrice { get; set; }


[DisplayName("最高価格")]
[AssertThat("LowPrice < HighPrice", ErrorMessage = "最安価格より金額が大きい必要があります")]
public int? HighPrice { get; set; }


[DisplayName("駐車場可")]
public bool ParkingAvailable { get; set; }


[DisplayName("駐車台数")]
[RequiredIf("ParkingAvailable == true", ErrorMessage = "駐車場可であれば、台数が必要です")]
public int? ParkingSpaces { get; set; }


[DisplayName("開始日")]
[AssertThat("OpenDate < CloseDate", ErrorMessage = "終了日より前である必要があります")]
public DateTime? SmallDate { get; set; }


[DisplayName("終了日")]
public DateTime? CloseDate { get; set; }
}

赤くしてある箇所が、ExpressiveAnnotations による追加部分です。AssertThatとRequireIfがあります。
AssertThatには、自プロパティも含めた式を記述します。複雑な数式も可能らしく、例えば「MaleRatio + FemaleRatio = 100」も可能なようです。
RequireIfは、特定条件下でのみ必須化したい場合に使います。上記では、「駐車場利用可能であれば、駐車可能台数を入力する」というものです。

クライアントサイドは?

上記ではサーバーサイドでの検証のみ有効となります。情報不足であるためクライアントサイドについても出来るか否か現在調べています。
分かり次第追記していきたいと思います。

MvcSiteMapProviderでパンくずリストを作成する

ASP.NET MVCパンくずリストを使う場合、、MvcSiteMapProviderというパッケージを使うケースが多いようです。MvcSiteMapProviderはnugetからインストールでき、WebFormsでいうSiteMapPathと同じような機能が実現できます。

ただ、SiteMapPath同様に静的なサイト…つまり「Home > About」や「Home > Company」のような、サイトマップXML(WebFormsではWeb.sitemap、MVCではMvc.sitemap)で管理できる程度のサイトであれば楽勝です。
しかし実際の運用では「Home > 食品 > オリーブオイル」「HOME > トヨタプリウス」といったように、データベースのレコードからの情報取得を必要とするケースが多いですよね。

WebFormsの頃のSiteMapPathでは私は諦めて自作のかなり面倒なパンくずリストをサイト毎に作ってましたが、正直骨が折れました。
MVCではもっと楽に実現できますが、カスタマイズに関してはあまり情報が多くありません。StackOverFlowあたりの情報を活用しつつ、試行錯誤してようやくそれらしく動いたので、その報告をしたいと思います。
Visual Studioは英語版を基に説明しますがご了承下さい(バージョンは2015)。

まずインストール

VSの[Tools]メニューから[Nuget Package Manager > Manage Nuget Packages for Solution]を実行
[Browse]タブを押し、「Search」とある検索ボックスがあるので、「MvcSiteMapProvider」と入力
私の場合は「MvcSiteMapProvider.Web」と「MvcSiteMapProvider.Mvc5」をインストールしました。後者はMVCのバージョンによって変わるので、適宜選択して下さい。なお、パッケージの説明欄に「This package is obsolete…」等と書かれたパッケージは旧式なのでインストールしないよう注意です。
インストールが終わったら、nugetは閉じましょう

Viewに貼り付け

既にパンくずを使う準備はできているので、Viewに貼り付けてみましょう。
オーソドックスに全ページに適用されるよう「~/Views/Share/_Layout.cshtml」を開き、「@RenderBody()」の上あたりに適当に「@Html.MvcSiteMap().SiteMapPath()」をコピペします(パンくず用のdiv等で括るのが良いかも)。

Mvc.sitemapには最初からAboutページ等が定義されているので、http://localhost:XXXX/Home/Aboutにアクセスしてみると、パンくずが表示されると思います。

サイトマップの作成

サイトマップを定義するために、ルート直下にある「Mvc.sitemap」を開きます。
この編集の仕方はWebで探すと多くの情報が見つかるため割愛します。
ノード毎に、ControllerとAction、パラメーターを記述する事で、それに対応したViewにアクセスした際にパンくずが表示されます。
以上で、静的なコンテンツでのパンくずは対応できます。

LINQ to Entitiesからパンくずリストを生成

さてここからが本題ですが、冒頭の「データベースからの情報取得」に入りたいと思います。実際にはLINQを使っているケースが多いと思われるため、ここではLINQ to Entitiesを基に説明していきます。

ModelおよびRepository作成

まず冒頭で示した「食品 > オリーブオイル」「トヨタプリウス」を基に説明していきますので、それぞれ「商品分類」、「商品」、「自動車メーカー」、「車種」の4モデルを用意したいところですが、面倒なので単純に示したいと思うので、全部同じModelで共用します(スイマセン)。
また、例は2つあり、1つは「食品 > オリーブオイル」のように多段階で一挙に展開するタイプ、2つ目は「自動車メーカー」「車種」を別々に展開するタイプです(通常は前者を使います)。

それと4モデルを一覧で呼び出すための、簡易的なRepositoryも作ります。
適当に~/Models/Repositories.cs あたりに作ります。

    using System;
    using System.Collections.Generic;

    namespace MyProject.Repositories
    {
        public class MyRepository
        {
          public MyRepository() { }

            public List<ItemModel> GetCategories()
            {
                List<ItemModel> result = new List<ItemModel>();
                result.Add(new ItemModel(1, "食品", 0));
                result.Add(new ItemModel(2, "家具", 0));
                result.Add(new ItemModel(3, "家電", 0));
                return result;
            }

            public List<ItemModel> GetProducts(int catId)
            {
                List<ItemModel> result = new List<ItemModel>();
                switch (catId)
                {
                    case 1:
                        result.Add(new ItemModel(1, "ケチャップ", 1));
                        result.Add(new ItemModel(2, "オリーブオイル", 1));
                        result.Add(new ItemModel(3, "牛乳", 1));
                        break;
                    case 2:
                        result.Add(new ItemModel(4, "ベッド", 2));
                        break;
                    case 3:
                        result.Add(new ItemModel(5, "ドライヤー", 3));
                        result.Add(new ItemModel(6, "除湿機", 3));
                        break;
                    default:
                        break;
                }
                return result;
            }

            public List<ItemModel> GetMakers()
            {
                List<ItemModel> result = new List<ItemModel>();
                result.Add(new ItemModel(1, "トヨタ", 0));
                result.Add(new ItemModel(2, "日産", 0));
                result.Add(new ItemModel(3, "ホンダ", 0));
                return result;
            }

            public List<ItemModel> GetCarModels(int makerId)
            {
                List<ItemModel> result = new List<ItemModel>();
                switch (makerId)
                {
                    case 1:
                        result.Add(new ItemModel(1, "プリウス", 1));
                        result.Add(new ItemModel(2, "ヴィッツ", 1));
                        result.Add(new ItemModel(3, "アクア", 1));
                        result.Add(new ItemModel(4, "ハリアー", 1));
                        break;
                    case 2:
                        result.Add(new ItemModel(5, "マーチ", 2));
                        result.Add(new ItemModel(6, "ノート", 2));
                        result.Add(new ItemModel(7, "セレナ", 2));
                        break;
                    case 3:
                        result.Add(new ItemModel(8, "フィット", 3));
                        result.Add(new ItemModel(9, "フリード", 3));
                        break;
                    default:
                        break;
                }
                return result;
            }
        }

        /// <summary>
        /// 分類、商品、自動車メーカー、車種に共通して使用
        /// </summary>
        public class ItemModel
        {
            public ItemModel() { }
            public ItemModel(int id, string name, int parentId) : base()
            {
                Id = id;
                Name = name;
                ParentId = parentId;
            }
            public int Id { get; set; }
            public string Name { get; set; }
            public int ParentId { get; set; }
        }
    }

DynamicNodeProviderを作成

Mvc.sitemapから呼び出すためのDynamicNodeProviderを作ります。Mvc.sitemapから呼び出すことで、Mvc.sitemapのXMLファイル内でなく動的なノードを読み込むことが出来るようです(説明がうまく出来なくてスイマセン)。
適当に~/Models/dynamicnode.cs みたいに置いておきましょう。

namespace MyProject.SiteMaps
{
 /// <summary>
 /// 商品分類 > 商品
 /// </summary>
 public class ProductDynamicNodeProvider : DynamicNodeProviderBase
 {
  public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
  {
   var nodes = new List<DynamicNode>();

   MyRepository rp = new MyRepository();

   foreach(var cat in rp.GetCategories())
   {
    var cnode = new DynamicNode
    {
     Key = $"cat_{cat.Id}",
     Title = cat.Name,
     Controller = "Category",
     Action = "Details",
    };
    cnode.RouteValues.Add("id", cat.Id);
    nodes.Add(cnode);

    foreach(var pro in rp.GetProducts(cat.Id))
    {
     var pnode = new DynamicNode
     {
      Key = $"pro_{pro.Id}",
      ParentKey = cnode.Key,
      Title = pro.Name,
      Controller = "Product",
      Action = "Details",
     };
     pnode.RouteValues.Add("id", pro.Id);
     nodes.Add(pnode);
    }
   }

   return nodes;
  }
 }

 /// <summary>
 /// 自動車メーカー
 /// </summary>
 public class MakerDynamicNodeProvider : DynamicNodeProviderBase
 {
  public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
  {
   var nodes = new List<DynamicNode>();

   MyRepository rp = new MyRepository();

   foreach (var maker in rp.GetMakers())
   {
    var mnode = new DynamicNode
    {
     Key = $"maker_{maker.Id}",
     Title = maker.Name,
     Controller = "Maker",
     Action = "Details",
    };
    mnode.RouteValues.Add("id", maker.Id);
    nodes.Add(mnode);
   }

   return nodes;
  }
 }

 /// <summary>
 /// 車種
 /// </summary>
 public class CarModelDynamicNodeProvider : DynamicNodeProviderBase
 {
  public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
  {
   var nodes = new List<DynamicNode>();

   MyRepository rp = new MyRepository();

   foreach (var maker in rp.GetMakers())
   {
    foreach(var car in rp.GetCarModels(maker.Id)){
     var cnode = new DynamicNode
     {
      Key = $"car_{car.Id}",
      ParentKey = $"maker_{car.ParentId}",
      Title = car.Name,
      Controller = "CarModel",
      Action = "Details",
     };
     cnode.RouteValues.Add("id", car.Id);
     nodes.Add(cnode);
    }
   }

   return nodes;
  }
 }
}

ContollerおよびViewの作成

上記のDynamicNodeProverの中で記したContollerを作成します。つまり、以下の4つです。

  • Category
  • Product
  • Maker
  • CarModel

更に、それぞれのActionとして「Details」を各々作成します。ここでは、中身は「return View();」だけで良いです。
4つのDetailsアクションのViewを作成して下さい。これも作成するだけで中身はデフォのままで良いです。

Mvc.sitemapの編集

最後にMvc.sitemapの変更です。
DynamicNodeProviderを呼び出しているノードで、「MyProject」が2度ずつ記述されていますが、これは決め事のようです。カンマ区切りでclass名とプロジェクト名を書く決まりみたいです。

<?xml version="1.0" encoding="utf-8" ?>
<mvcSiteMap xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0"
            xsi:schemaLocation="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0 MvcSiteMapSchema.xsd">

  <mvcSiteMapNode title="Home" controller="Home" action="Index">
    <mvcSiteMapNode title="商品分類>商品のサイトマップ" controller="Category" action="Index">
      <mvcSiteMapNode title="" controller="Category" action="Details" dynamicNodeProvider="MyProject.SiteMaps.ProductDynamicNodeProvider, MyProject" />
    </mvcSiteMapNode>
    <mvcSiteMapNode title="自動車メーカー>車種のサイトマップ" controller="Maker" action="Index">
      <mvcSiteMapNode title="" controller="Maker" action="Details" dynamicNodeProvider="MyProject.SiteMaps.MakerDynamicNodeProvider, MyProject">
        <mvcSiteMapNode title="" controller="CarModel" action="Index" dynamicNodeProvider="MyProject.SiteMaps.CarModelDynamicNodeProvider, MyProject" />
      </mvcSiteMapNode>
    </mvcSiteMapNode>
  </mvcSiteMapNode>
</mvcSiteMap>

サイトマップページの作成

HomeContollerに、SiteMapアクションを作成し、Viewも作成して下さい。
~/Views/Home/SiteMap.cshtml を開き、ページの何処かに「@Html.MvcSiteMap().Menu()」を貼り付けてましょう。

動作確認

ビルドして、ブラウザでhttp://(hostname)/Home/SiteMap を開いてみて下さい。
定義したとおりの階層と各ページへのリンクが表示されれば成功です。

更に、冒頭で_Layout.cshtmlでパンくずを記載してあるはずなので、各ページへのリンク先に行くと、パンくずも表示されていると思います。

さいごに

いかがででしたでしょうか。
このように、MvcSiteMapProviderを使うことで、ノードを動的に生成する事が出来ます。
各コンテンツのContollerやView側では、何もする事無く階層的なパンくず、更にはサイトマップまでできてしまいます。
今まで「ASP.NET(特にWebForms)は便利だが、パンくずはちょっと…」と思っていた方も、これでもう楽ちんですね。
ただ、コードを見てもらうと分かる通り、レコードの全件呼び出しをしているため、レコード数が10万件以上とかになった時のパフォーマンスが心配です。

jStatMapで「グラフ」を保存する方法

商圏調査等をする上でかなり有用なjStatMapですが、その機能の多さ故に初めて使うと戸惑う事も多いと思います。

jStatMapにはグラフ保存機能がない

その中で1つ、わかりづらいというか注意しなければならないのは、「グラフの保存機能がない」点です。
jStatMapの主な機能は「プロット」「エリア」「グラフ」「レポート」の4つから構成されます。その内「プロット」と「エリア」はクラウド上に保存してくれるのですが、「グラフ」は保存できません。

f:id:fskkoba:20160603133205p:plain

ログアウトするまでは存在しますし名前変更等も可能なので一見保存してくれているのかと思いきや、ログアウトして再度ログインすると、空になってしまいます。

これでは、せっかく集計データを地図上に展開しても、毎回同じ作業を繰り返すことになり、何度もやっているうちに間違いも起きやすいです。

「エクスポート」機能でローカル保存

これを解決する方法として「エクスポート」があります。

f:id:fskkoba:20160603135324p:plain

  1. 統計データを作成後、「グラフ」を選択してレイヤーを選択
  2. 上記の「エクスポート」ボタンを押す
  3. 「ファイル種別選択」ウィンドウが開くので「グラフファイル」を選択
  4. 「エクスポートファイルのダウンロード」ボタンが現れるので、クリック

以上で、.gsfファイルをダウンロードできます。
つまりグラフについては、ローカルで保存するという運用方法のようです。

インポート方法

次に、一度ログアウトしてから、先ほどの.gsfファイルをインポートする方法です。

  1. 先ほどのエクスポートボタンの右にある「インポート」ボタンを押す
  2. 「緯度経度リスト」「シェープファイル」「GMLファイル」「グラフファイル」の4つのボタンが現れるので、「グラフファイル」を選択
  3. 「参照」ボタンで先ほどの.gsfファイルを選択
  4. 「アップロードする」ボタンを押す

以上です。ちなみに、ファイル名がそのままレイヤー名になるので、それを踏まえてファイル名をつけておくと手間がかかりません。

容量制限の回避方法としても

なお、ローカル保存できるのは「グラフ」の他に「エリア」も可能です(プロットは不可)。
jStatMapには5MBという容量制限があるので、このローカル保存を活用すれば、プロットを除けば制限に縛られること無く使用する事ができます。

LINQPadで、ASP.NET MVCのModelを呼び出すの巻

MVCを始めて数ヶ月経ちますが、LINQ to Entitiesのテスト実行ができずに不便に感じていました。ControllerやModelで定義している場合、プロジェクトごとビルドしなくてはならず、効率的とはいえません。
ちなみにSQL Serverには、Common SQL Environmentという伝説的な便利ツールがありましたので、この中でSQLのテスト実行してから本番環境で実行していました。(8か10以降のWindowsでは正常動作しないので黒猫 SQL Studio使わせてもらっています)。

LINQ実行環境(?)、LINQPad

LINQにもテスト環境ツールってないのかね」と思い調べると、ありました。

https://www.linqpad.net/

Standard版は無料でDLできインストールも難なく終わりましたが、問題は

「既存のEntity モデルを手間なく呼び出せるか」

です。テスト実行が目的なので、出来ることなら、楽に、素早く、確実にやってしまいまいたいものです。

ASP.NET MVCサイトの指定

まずLINQPadを開いて行うのが、左ペインの「Add Connetion」です。

f:id:fskkoba:20160520114108p:plain


「Choose data context」ウィンドウが開くと、上下に選択ボックスが分割されていますが、上段は「Build data…」にもある通り新たにコンテクストを作成する選択肢で、下段は「Use a typed data…」のとおり既存のコンテクストを選べる欄です。
当記事では既存のMVCサイトのコンテキストを使う事を想定しているため、下段の選択肢から「Entity Framework (DBContext)」を選び「Next」を押します。

f:id:fskkoba:20160520115029p:plain

すると「Entity Framework DBContext Connection」ウィンドウが開くので、「Path to custom Assembly」欄に、ASP.NET MVCプロジェクトのdllを選択します。
私の場合は、「(Documents Root) \ Visual Studio 2015 \ Projects \ (Project Name) \ (Project Name) \ bin \ (Project Name).dll」でした。

f:id:fskkoba:20160520115332p:plain

次にすかさず「Choose Custom Type」が現れます(私の場合はSQLServerの接続文字列が2つあったので現れましたが、1つしかなければ現れないかもしれません)。対象の方を選択して「OK」を押します。

f:id:fskkoba:20160520115405p:plain

最後に、「Path to application config file…」の横にある「Choose」を選択し、サイト内の「web.confing」を選択します。
私の場合は、「(Documents Root) \ Visual Studio 2015 \ Projects \ (Project Name) \ (Project Name) \ web.config」でした。

f:id:fskkoba:20160520115842p:plain

これで「OK」ボタンを押して完了です。
元の画面の左ペンに接続情報が追加されているはずです。
これでMVCサイト内と(ほぼ)同じようにLINQる事ができますね。

参考

最初なかなかわかりませんでしたが、以下のサイトを参考にさせていただきました。

Kevin LaBranche - Connecting LinqPad to Entity Framework Code First

 

ASP.NET MVC 5.2 プロジェクトへのMvcSiteMapProviderのインストール方法

通常はプロジェクトを開いてNuget Package Managerで「MvcSiteMapProvider」を検索してインストールするだけですが、ASP.NET MVC 5.2の場合依存関係(MVC5.2 and MvcSiteMapProvider 4.6.x)によりインストール出来ません。

以下のサイトを参考に、コンソールからバージョン指定することでインストール出来ました。

Breadcrumb in MVC - CodeProject

Install-package MvcSiteMapProvider.MVC<version>

 とありますが、MVC5.2(5の場合も同じだと思いますが)の場合は以下のとおりです。

Install-package MvcSiteMapProvider.MVC5

てっきりNugetで自動的に選別してくれるものと考えていましたが、そうでもないようです。

ASP.NET MVCからSQL ServerにWindows認証ログインで接続し、イニシャライザーを実行させる

Entity Frameworkではイニシャライザーを使うことで、データモデルに基づき

  • データベースの作成
  • テーブルの作成
  • レコードの初期生成

を自動実行してくれます。

まずはLocalDBでなくSQL Server接続の接続文字列を取得

Visual Studio環境であれば /app_data/~.mdf が生成されますが、これはLocalDBといい、実用上はLocalDBではなくSQL Serverへの通常接続を行います。
そこで、まずはSQL Serverへの接続文字列から生成する事になります。

以下の操作により、接続文字列を取得できます。

  1. VSの「サーバーエクスプローラー」(右上のソリューションエクスプローラーと並列に並んでいます)を選択
  2. 「データ接続」を右クリックし「接続の追加」
  3. 「データソース」の中から「Microsoft SQL Server」を選択(Microsoft SQL Server データファイルではない)
  4. 「サーバーにログオンする」から「Windows認証を使用する」
  5. ご自身のDBを選択し、テスト接続できることを確認してください
  6. 「詳細設定」ボタンを押しと現れるウィンドウの最下部に「Data Source=~」で始まる接続文字列があるので、全て選択してコピーする
  7. コピーした接続文字列を、web.configのconnectionstring内の<add~内に記載

上記までで、Windows認証によりSQL Serverへの接続ができることになりますが、今回はイニシャライザーにて「DB作成、TABLE作成、レコード生成」を行うため、権限不足です。当然、権限設定を事前に適切に行う必要があります。

ちなみに、このまで何も設定せずに「Webを発行」してブラウザからサイトを開いてみる(VSから起動するIIS Expressではない)と、イニシャライザーが実行されようとする段階でSQL Server関連のエラーが発生し、「CREATE権限がありません」といった旨のエラーが発生すると思います。

では「どのユーザー(SQLServerログイン)にどの程度の権限を与えればよいのか?」という事になりますが、この事について解説していきます。

ASP.NET実行ユーザーにCREATE権限を与える

 まず今回、以下のサイトを参考にさせて頂きました。

ASP.NET で MSSQL に Windows 認証で接続 | クリエイティブ Web

記事中には「アプリケーションプール毎にログインユーザーを作成する」とありますが、開発用マシン等の緩い環境の場合には、解説内にもある通り「BUILTIN\Users」グループに対して権限設定すれば良いと思います(アプリケーションプール毎のログインユーザーは、Usersグループに属しているため)。
操作は以下のとおり。

  1. SSMS > セキュリティ > ログイン > BUILTIN\Users
    を選択
  2. 右クリックして「プロパティ」を開く。すると「ログインのプロパティ」が現れる。
  3. 左ペインの「ページの選択」から「サーバーロール」を選択
  4. 「dbcreator」にチェックし、「OK」ボタンで閉じる

以上です。
これにて、IIS上からイニシャライザーが実行できるはずです。
実行してエラーが表示されなければ、SSMSを開いてご確認ください。データーベースが表示され、その中のテーブルがデータモデルと一致していれば成功です。

なお繰り返しになりますが、Visual Studio上から実行するIIS Expressではありません。VS上から実行しても/app_data/~.mdfが作られるだけです。

後処理

BUILTIN\Usersがずっとdbcreatorのままでは危険なので、イニシャライザーの実行後はサーバーロールから外しておきましょう。