ASP.NET MVCはじめました~データベースで管理する BootStrap 多階層ドロップダウンメニュー
概要
ログインユーザの権限などで表示項目が変わるような動的なメニューが欲しかったので作ってみました。2階層くらい表現できれば十分かとも思ったのですが、多階層ドロップダウンメニューにチャレンジしてみます。
環境
- Visual Studio Community 2015
- ASP.NET MVC5
- BootStrap 3.3.7
- jQuery 3.1.1
多階層ドロップダウンメニューの作成
まずはBootStrapで多階層のドロップダウンメニューを実装してみました。CSSはこちらから拝借。CSSとViewを書くだけで実装できました。この時点ではリンクリストは直書き。
■/Views/Shared/_Layout.cshtml
<div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li>@Html.ActionLink("Home", "Index", "Home")</li> <li>@Html.ActionLink("About", "About", "Home")</li> <li>@Html.ActionLink("Contact", "Contact", "Home")</li> <li class="dropdown"> <a class="dropdown-toggle" data-toggle="dropdown" href="#" role="button"> サンプル<span class="caret"></span> </a> <ul class="dropdown-menu" role="menu"> <li>@Html.ActionLink("Upload", "Upload", "Home")</li> <li>@Html.ActionLink("Autocomplete", "Create", "Home")</li> <li><a href="#">リンクのリスト1</a></li> <li><a href="#">リンクのリスト2</a></li> <li><a href="#">リンクのリスト3</a></li> <li class="dropdown-submenu"> <a tableindex="-1" href="#">More options</a> <ul class="dropdown-menu"> <li><a href="http://google.com">Google</a></li> <li><a href="http://yahoo.com">Yahoo</a></li> </ul> </li> </ul> </li> </ul> </div>
3階層までだとこんな感じに。固定でよければ簡単に実装できますね!
データベースよりメニューデータを取得
動的なメニューを実現するにはメンテナンス的にもデータベースでデータを持ってた方がいいんじゃないのってことで、ParentIdによる階層関係のデータを持ったテーブルを用意してみました。これで何階層でも行ける↑↑↑あとはビューヘルパーのActionLinkメソッドを使いたいのでリンクテキストやらアクションメソッドと並び順で構成。
これらのデータを一旦全部取得して、再帰関数で階層構造データになるように再構成。LINQで一発でできそうな気がすると思ったのですが、無念…。いつかちゃんと調べる。
■/Models/MenuItem.cs
public class MenuItem { [Key] public int Id { get; set; } public int? ParentId { get; set; } [Required] public string LinkText { get; set; } public string ActionName { get; set; } public string ControllerName { get; set; } public int Order { get; set; } } public class NavigationLink : MenuItem { public IEnumerable<NavigationLink> ChildMenu { get; set; } } public class Navigation { public IEnumerable<NavigationLink> Menu { get { var menu = new List<NavigationLink>(); using (var context = new AppDbContext()) { var data = context.MenuItems .OrderBy(n => n.Order) .ToList(); var mainMenu = data.Where(n => n.ParentId == null); foreach (var item in mainMenu) { NavigationLink linkItem = MappingLinkItems(item); menu.Add(linkItem); } SetChildMenu(menu, data); } return menu; } } #region helper method... }
カスタムビューヘルパーで描画
TagBuilderを使ったカスタムビューヘルパーを作ってみたのですが、思ってたより複雑になったのでサブメニュー部分だけ実装したらいまいちな感じになってしまいました。本当はこれもヘルパー一発で描画したかった。
■/Helper/NavigationMenuHelper.cs
public static class NavigationMenuHelper { public static IHtmlString NavigationSubmenuListFor(this HtmlHelper helper, IEnumerable<NavigationLink> submenu) { if (submenu == null) { throw new ArgumentNullException("submenu", "submenu is null."); } var sb = new StringBuilder(); foreach (var item in submenu) { if (item.ChildMenu == null) { var li = new TagBuilder("li"); li.InnerHtml = helper.ActionLink(item.LinkText, item.ActionName, item.ControllerName).ToString(); sb.Append(li.ToString(TagRenderMode.Normal)); } else { sb.Append(CreateSubMenu(helper, item.LinkText, item.ChildMenu)); } } return MvcHtmlString.Create(sb.ToString()); } private static string CreateSubMenu(HtmlHelper helper, string linkText, IEnumerable<NavigationLink> subMenu) { var a = new TagBuilder("a"); var attributes = new Dictionary<string, string> { ["tableindex"] = "-1", ["href"] = "#", }; a.MergeAttributes(attributes); a.InnerHtml = linkText; var top_li = new TagBuilder("li"); top_li.MergeAttribute("class", "dropdown-submenu"); top_li.InnerHtml = a.ToString(); var ul = new TagBuilder("ul"); ul.MergeAttribute("class", "dropdown-menu"); foreach (var item in subMenu) { if (item.ChildMenu == null) { var li = new TagBuilder("li"); li.InnerHtml = helper.ActionLink(item.LinkText, item.ActionName, item.ControllerName).ToString(); ul.InnerHtml += li.ToString(); } else { ul.InnerHtml += CreateSubMenu(helper, item.LinkText, item.ChildMenu); } } top_li.InnerHtml += ul.ToString(); return top_li.ToString(); } }
■/Views/Shared/_Layout.cshtml
<div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> @foreach (var item in Session["menu"] as List<NavigationLink>) { if (item.ChildMenu == null) { <li>@Html.ActionLink(item.LinkText, item.ActionName, item.ControllerName)</li> } else { <li class="dropdown"> <a class="dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-expanded="false"> @item.LinkText <span class="caret"></span> </a> <ul class="dropdown-menu" role="menu"> @Html.NavigationSubmenuListFor(item.ChildMenu) </ul> </li> } } </ul> </div>
ASP.NET MVCはじめました~MongoDBのデータを削除する
概要
一覧、登録、更新、CURD処理最後はMongoDBのデータ削除です。
削除画面はこんな感じになります(デフォルト)。
環境
- Visual Studio Community 2015
- ASP.NET MVC5
- MongoDB 3.2
データの削除
ObjectIdをキーに削除してます。ただしstring型のため、Parseメソッドで変換する必要があります。
■UserRepository.cs
public async Task DeleteAsync(string id) { var collection = GetCollection<User>(Collection); var objId = ObjectId.Parse(id); var filter = Builders<User>.Filter.Eq("_id", objId); await collection.DeleteOneAsync(filter); }
ラムダ式だとわざわざ変換しなくてもいい感じにやってくれるみたいです。これもBsonRepresentation属性のおかげ。
■UserRepository.cs
public async Task DeleteAsync(string id) { var collection = GetCollection<User>(Collection); await collection.DeleteOneAsync(d => d.Id == id); }
MVC
コードを一式載せておきます。
Model
■User.cs
using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using System; using System.ComponentModel.DataAnnotations; namespace aspnet_mvc5_mongodb.Models { public class User { [BsonId] [BsonRepresentation(BsonType.ObjectId)] public string Id { get; set; } [BsonElement("name")] [Required] public string Name { get; set; } [BsonElement("age")] public int Age { get; set; } [BsonElement("email")] [Required] public string Email { get; set; } [BsonElement("address")] public string Address { get; set; } [BsonElement("revision")] public int Revision { get; set; } [BsonElement("last_modified")] public DateTime LastModified { get; set; } } }
View
■Delete.cshtml
@model aspnet_mvc5_mongodb.Models.User @{ ViewBag.Title = "Delete"; } <h2>Delete</h2> <h3>Are you sure you want to delete this?</h3> <div> <h4>User</h4> <hr /> <dl class="dl-horizontal"> <dt> @Html.DisplayNameFor(model => model.Name) </dt> <dd> @Html.DisplayFor(model => model.Name) </dd> <dt> @Html.DisplayNameFor(model => model.Age) </dt> <dd> @Html.DisplayFor(model => model.Age) </dd> <dt> @Html.DisplayNameFor(model => model.Email) </dt> <dd> @Html.DisplayFor(model => model.Email) </dd> <dt> @Html.DisplayNameFor(model => model.Address) </dt> <dd> @Html.DisplayFor(model => model.Address) </dd> </dl> @using (Html.BeginForm()) { @Html.AntiForgeryToken() <div class="form-actions no-color"> <input type="submit" value="Delete" class="btn btn-default" /> | @Html.ActionLink("Back to List", "Index") </div> } </div>
Controller
■UsersController.cs
using aspnet_mvc5_mongodb.Models; using aspnet_mvc5_mongodb.Repositories; using aspnet_mvc5_mongodb.Repositories.Abstractions; using System; using System.Net; using System.Threading.Tasks; using System.Web.Mvc; namespace aspnet_mvc5_mongodb.Controllers { public class UsersController : Controller { private readonly IUserRepository _repository; public UsersController() : this(new UserRepository()) { } public UsersController(IUserRepository repository) { this._repository = repository; } [HttpGet] public async Task<ActionResult> Delete(string id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } var model = await _repository.GetByIdAsync(id); if (model == null) { return HttpNotFound(); } return View(model); } [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public async Task<ActionResult> DeleteConfirmed(string id) { await _repository.DeleteAsync(id); return RedirectToAction("Index"); } ... } }
リポジトリ
■MongoDB.cs
using MongoDB.Driver; using System.Configuration; namespace aspnet_mvc5_mongodb.Repositories { public class MongoDB { protected static IMongoClient _client; protected static IMongoDatabase _database; public MongoDB() { var connectionString = ConfigurationManager.AppSettings["MongoDBConnection"]; var database = ConfigurationManager.AppSettings["Database"]; _client = new MongoClient(connectionString); _database = _client.GetDatabase(database); } public static IMongoCollection<T> GetCollection<T>(string collection) { return _database.GetCollection<T>(collection); } } }
■IUserRepository.cs
using aspnet_mvc5_mongodb.Models; using System.Collections.Generic; using System.Threading.Tasks; namespace aspnet_mvc5_mongodb.Repositories.Abstractions { public interface IUserRepository { Task<User> GetByIdAsync(string id); Task DeleteAsync(string id); ... } }
■UserRepository.cs
using aspnet_mvc5_mongodb.Models; using aspnet_mvc5_mongodb.Repositories.Abstractions; using MongoDB.Driver; using System.Collections.Generic; using System.Threading.Tasks; namespace aspnet_mvc5_mongodb.Repositories { public class UserRepository : MongoDB, IUserRepository { private static readonly string Collection = "users"; public async Task<User> GetByIdAsync(string id) { var collection = GetCollection<User>(Collection); return await collection.Find(d => d.Id == id).FirstOrDefaultAsync(); } public async Task DeleteAsync(string id) { var collection = GetCollection<User>(Collection); await collection.DeleteOneAsync(d => d.Id == id); } ... } }
ASP.NET MVCはじめました~MongoDBのデータを更新する
概要
一覧、登録とやってきて、今回はMongoDBのデータ更新です。
編集画面はこんな感じになります(デフォルト)。
環境
- Visual Studio Community 2015
- ASP.NET MVC5
- MongoDB 3.2
更新データの取得
/Users/Edit/57da234368ff011234b23756
一覧画面からの遷移で編集画面を開くときはパラメータにObjectIdを指定してますが、URLがキタナイのでもっと別のわかりやすい一意の値のほうがいいかもしれない。
■UsersController.cs
[HttpGet] public async Task<ActionResult> Edit(string id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } var model = await _repository.GetByIdAsync(id); if (model == null) { return HttpNotFound(); } return View(model); }
■UserRepository.cs
public async Task<User> GetByIdAsync(string id) { var collection = GetCollection<User>(Collection); return await collection.Find(d => d.Id == id).FirstOrDefaultAsync(); }
あと、型はstringです。最初モデルをObjectId型にしてたんですが、モデルバインディングのときにObjectId型に変換出来なくてエラーになりました。
[BsonId]
public ObjectId Id { get; set; }
BsonRepresentationっていう属性をつけてあげて、string型で定義しておくとうまいことやってくれるようです。
[BsonId] [BsonRepresentation(BsonType.ObjectId)] public string Id { get; set; }
データの更新
データの更新は条件と値をUpdateメソッドに渡してあげます。
■UserRepository.cs
public async Task UpdateAsync(User document) { var collection = GetCollection<User>(Collection); var objId = ObjectId.Parse(document.Id); var filter = Builders<User>.Filter.Eq("_id", objId); var update = Builders<User>.Update .Set("name", document.Name) .Set("age", document.Age) .Set("email", document.Email) .Set("address", document.Address); await collection.UpdateOneAsync(filter, update); }
項目名が文字列なのでちょっと気になりますね。ラムダ式が用意されているのでこっちのほうがいいかも。
■UserRepository.cs
public async Task UpdateAsync(User document)
{
var collection = GetCollection<User>(Collection);
var update = Builders<User>.Update
.Set(d => d.Name, document.Name)
.Set(d => d.Age, document.Age)
.Set(d => d.Email, document.Email)
.Set(d => d.Address, document.Address);
await collection.UpdateOneAsync(d => d.Id == document.Id, update);
}
単純に更新するだけであればこれでOKですが、複数のユーザが同時に更新した場合、先に更新したユーザのデータが後に更新したユーザに上書きされてしまいます。
楽観的ロック
MongoDBにはトランザクションがありません。トランザクションが必要な要件であればトランザクション機能を持つRDBMSなどを使うほうがいいのでしょう。ただ、そこまで厳密ではないシンプルな要件であれば楽観的ロックで対処できるかもしれません。
モデルにRevisionフィールドを追加します。
■User.cs
[BsonElement("revision")] public int Revision { get; set; }
更新条件にRivisionフィールドを参照し、更新時にIncメソッドでインクリメントしてあげます。UpdateOneAsyncメソッドは変更した件数を返してくれるので、こいつが1件の場合は成功、それ以外は失敗扱いにしています。
■UserRepository.cs
public async Task<bool> UpdateAsync(User document) { var collection = GetCollection<User>(Collection); var builder = Builders<User>.Filter; var filter = builder.Eq(d => d.Id, document.Id) & builder.Eq(d => d.Revision, document.Revision); var update = Builders<User>.Update .Set(d => d.Name, document.Name) .Set(d => d.Age, document.Age) .Set(d => d.Email, document.Email) .Set(d => d.Address, document.Address) .CurrentDate(d => d.LastModified) .Inc(d => d.Revision, 1); var result = await collection.UpdateOneAsync(filter, update); if (result.IsModifiedCountAvailable && result.ModifiedCount == 1) { return true; } return false; }
最初はDateTime型のLastModifiedフィールドで対応しようと思ったのですが、UTCだし、手間だったのでやめました。
二つブラウザを立ち上げ、同時に編集画面を開きます。この段階ではどちらもRevisionフィールドは同じ値(初期値:1)です。
まず左側のブラウザで住所をフィラデルフィアへ編集しデータベースを更新します。
更新が確定するとRevisionが一つ上がっているので、左側のブラウザで更新しようとしても更新対象が見つからずエラーメッセージが表示されるって感じです。
MongoDBのrevisionフィールドがインクリメントされていることが確認できます(mLabより)。
MVC
コードを一式載せておきます。
Model
Revisionと、ついでにLastModifiedフィールドを追加しました。
■User.cs
using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using System; using System.ComponentModel.DataAnnotations; namespace aspnet_mvc5_mongodb.Models { public class User { [BsonId] [BsonRepresentation(BsonType.ObjectId)] public string Id { get; set; } [BsonElement("name")] [Required] public string Name { get; set; } [BsonElement("age")] public int Age { get; set; } [BsonElement("email")] [Required] public string Email { get; set; } [BsonElement("address")] public string Address { get; set; } [BsonElement("revision")] public int Revision { get; set; } [BsonElement("last_modified")] public DateTime LastModified { get; set; } } }
View
■Edit.cshtml
@model aspnet_mvc5_mongodb.Models.User @{ ViewBag.Title = "Edit"; } <h2>Edit</h2> @{ if (ViewBag.ErrorMsg != null) { <span class="errorMsg">@ViewBag.ErrorMsg</span> } } @using (Html.BeginForm()) { @Html.AntiForgeryToken() <div class="form-horizontal"> <h4>User</h4> <hr /> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) @Html.HiddenFor(model => model.Id) @Html.HiddenFor(model => model.Revision) @Html.HiddenFor(model => model.LastModified) <div class="form-group"> @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Age, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Age, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Age, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Address, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Address, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Address, "", new { @class = "text-danger" }) </div> </div> <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> } <div> @Html.ActionLink("Back to List", "Index") </div> @section Scripts { @Scripts.Render("~/bundles/jqueryval") }
Controller
■UsersController.cs
using aspnet_mvc5_mongodb.Models; using aspnet_mvc5_mongodb.Repositories; using aspnet_mvc5_mongodb.Repositories.Abstractions; using System; using System.Net; using System.Threading.Tasks; using System.Web.Mvc; namespace aspnet_mvc5_mongodb.Controllers { public class UsersController : Controller { private readonly IUserRepository _repository; public UsersController() : this(new UserRepository()) { } public UsersController(IUserRepository repository) { this._repository = repository; } [HttpGet] public async Task<ActionResult> Edit(string id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } var model = await _repository.GetByIdAsync(id); if (model == null) { return HttpNotFound(); } return View(model); } [HttpPost] [ValidateAntiForgeryToken] public async Task<ActionResult> Edit(User document) { if (document == null) { return HttpNotFound(); } if (ModelState.IsValid) { try { if (await _repository.UpdateAsync(document)) { return RedirectToAction("Index"); } ViewBag.ErrorMsg = "他のユーザにより更新されています。"; return View(document); } catch (Exception) { ViewBag.ErrorMsg = "データ更新に失敗しました。"; return View(document); } } return View(document); } ... } }
リポジトリ
■MongoDB.cs
using MongoDB.Driver; using System.Configuration; namespace aspnet_mvc5_mongodb.Repositories { public class MongoDB { protected static IMongoClient _client; protected static IMongoDatabase _database; public MongoDB() { var connectionString = ConfigurationManager.AppSettings["MongoDBConnection"]; var database = ConfigurationManager.AppSettings["Database"]; _client = new MongoClient(connectionString); _database = _client.GetDatabase(database); } public static IMongoCollection<T> GetCollection<T>(string collection) { return _database.GetCollection<T>(collection); } } }
■IUserRepository.cs
using aspnet_mvc5_mongodb.Models; using System.Collections.Generic; using System.Threading.Tasks; namespace aspnet_mvc5_mongodb.Repositories.Abstractions { public interface IUserRepository { Task<User> GetByIdAsync(string id); Task InsertAsync(User document); Task<bool> UpdateAsync(User document); ... } }
新規登録時にRevisionとLastModifiedフィールドの初期値を設定するコードも追加。
■UserRepository.cs
using aspnet_mvc5_mongodb.Models; using aspnet_mvc5_mongodb.Repositories.Abstractions; using MongoDB.Driver; using System.Collections.Generic; using System.Threading.Tasks; namespace aspnet_mvc5_mongodb.Repositories { public class UserRepository : MongoDB, IUserRepository { private static readonly string Collection = "users"; public async Task<User> GetByIdAsync(string id) { var collection = GetCollection<User>(Collection); return await collection.Find(d => d.Id == id).FirstOrDefaultAsync(); } public async Task InsertAsync(User document) { document.Revision = 1; document.LastModified = DateTime.Now; var collection = GetCollection<User>(Collection); await collection.InsertOneAsync(document); } public async Task<bool> UpdateAsync(User document) { var collection = GetCollection<User>(Collection); var builder = Builders<User>.Filter; var filter = builder.Eq(d => d.Id, document.Id) & builder.Eq(d => d.Revision, document.Revision); var update = Builders<User>.Update .Set(d => d.Name, document.Name) .Set(d => d.Age, document.Age) .Set(d => d.Email, document.Email) .Set(d => d.Address, document.Address) .CurrentDate(d => d.LastModified) .Inc(d => d.Revision, 1); var result = await collection.UpdateOneAsync(filter, update); if (result.IsModifiedCountAvailable && result.ModifiedCount == 1) { return true; } return false; } ... } }
実行
/Users/Edit/[ObjectId]にアクセス。
次回は画面からデータの削除です☆彡
参考
https://docs.mongodb.com/getting-started/csharp/update/
https://docs.mongodb.com/manual/reference/operator/update/inc/
- 作者: Kyle Banker,Sky株式会社玉川竜司
- 出版社/メーカー: オライリージャパン
- 発売日: 2012/12/14
- メディア: 大型本
- 購入: 5人 クリック: 55回
- この商品を含むブログ (8件) を見る