mvcConf @:Japan ビデオ公開
mvcConf @:Japan ~ ASP.NET MVC ブートキャンプ ~
mvcConf @:Japan の各セッションおよび会場で流れたビデオ等が公開されました。
なんと公開先はChannel9です。
いいのかなぁ。。。
私のセッションについては以下のページで見ることができます。
まぁ、セッションのビデオは画面の映像だけなのがちょっと安心できるとこ(w
http://channel9.msdn.com/Events/mvcConf/Japan/mvcConf-Japan/Session4
セッションは45分と短い時間だったため、実はBlogに書いた内容のほうが詳しくなっています。
それでもなんなので、ほんとに一部だけBlogに書いていない話もあったりしますが。
よろしかったら見てやってください。
#自分ははずかしいので見れない気がする。。。
mvcConf @:Japan ふりかえり 6: Webサービス(OData)連携
.NET Frameworkを利用している場合、WCF Data Servicesを利用することでODataベースのWebサービスを簡単に提供することができます。
このWCF Data Services と連携するASP.NET MVC 3 のプログラムを作成してみます。
まずはWCF Data Servicesの実装、ということで空のWebアプリケーションプロジェクトを作成します。
WCF Data ServicesでCodeFirstを利用することは、いまのところ予定されている、という状態にとどまっています。
ここではデータベースファーストでWCF Data Servicesを構築することにします。
作成したWebアプリケーションにApp_Dataフォルダを追加し、そこに最初のサンプルで構築されたデータベース(Group.sdfファイル)を追加します。
この時点で一度ビルドを実行しておきます。
Entity Data ModelをGroupModel.edmxという名前で追加します。
ウィザードはデフォルトの状態のまますすめていきます。
テーブルをモデルに含めます。
モデルが作成されます。
この時点でまたビルドを実行します。
WCF Data Serviceのページを追加します。
データサービスのコードのひな形が作成されるので、以下の赤い部分を追加します。
—————————————————————————————–
using System.Data.Services;
using System.Data.Services.Common;
namespace WebApplication5
{
public class WcfDataService1 : DataService< GroupEntities >
{
// このメソッドは、サービス全体のポリシーを初期化するために、1 度だけ呼び出されます。
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}
}
}
—————————————————————————————–
プログラムを実行すると、次のような画面が表示され、すでにWebサービスとしてAtomPabの形式でデータの提供が開始されることを確認できます。
これでWebサービス側のシステムが構築できたことになります。
このサービスは起動したまま、次の作業に入ります。
サービスが起動していないと以降の作業はうまく動作しませんので気をつけてください。
もうひとつVisual Web Developerを起動し、MVCのプロジェクトを作成します。
後で記述するサンプルコードと名前空間を合わせるため、「MyService」という名前でプロジェクトを作成してください。
ソリューションエクスプローラー上で参照設定を右クリックし、サービス参照の追加を選択します。
アドレス欄にWebサービスのURLをコピーし、名前空間を「GroupServiceReference」としてOKボタンをクリックします。
コントローラーとしてMemberController.cs を作成し、以下のコードを記述します。
—————————————————————————————–
using System;
using System.Linq;
using System.Web.Mvc;
using System.Data.Entity.Infrastructure;
using MyService.GroupServiceReference;
namespace MyServiceControllers
{
public class MemberController : Controller
{
private GroupEntities db = new GroupEntities(new Uri("WebサービスのURLをコピー"));
public ViewResult Index()
{
var member = db.Members.Expand("Categories");
return View(member.ToList());
}
public ViewResult Details(int id)
{
Members member = db.Members.Where(m => m.ID == id).First();
return View(member);
}
public ActionResult Create()
{
ViewBag.CategoryID = new SelectList(db.Categories, "CategoryID", "Name");
return View();
}
[HttpPost]
public ActionResult Create(Members member)
{
if (ModelState.IsValid)
{
db.AddToMembers(member);
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.CategoryID = new SelectList(db.Categories, "CategoryID", "Name", member.CategoryID);
return View(member);
}
public ActionResult Edit(int id)
{
Members member = db.Members.Where(m => m.ID == id).First();
ViewBag.CategoryID = new SelectList(db.Categories, "CategoryID", "Name", member.CategoryID);
return View(member);
}
[HttpPost]
public ActionResult Edit(Members member)
{
if (ModelState.IsValid)
{
db.AttachTo("Members", member);
db.UpdateObject(member);
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.CategoryID = new SelectList(db.Categories, "CategoryID", "Name", member.CategoryID);
return View(member);
}
public ActionResult Delete(int id)
{
Members member = db.Members.Where(m => m.ID == id).First();
return View(member);
}
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
Members member = db.Members.Where(m => m.ID == id).First();
db.DeleteObject(member);
db.SaveChanges();
return RedirectToAction("Index");
}
}
}
—————————————————————————————–
このプログラムはCodeFirstのサンプルでスキャッフォールドで生成したコードをもとにしています。
CodeFirstではDbContextを利用していましたが、WCF Data Servicesを参照したこのプログラムではDataServiceContextを利用することになります。
基底となるクラスが用意するメソッドが異なるため、おおきな流れはスキャッフォールドで生成したコードから変えていませんが、利用しているメソッドは異なっている点に注意してください。
次にViewを作成していきます。
まずViewフォルダにMemberフォルダを追加し、その中に下記のコードと追加していきます。
・Create.cshtml
—————————————————————————————–
@model MyService.GroupServiceReference.Members
@{
ViewBag.Title = "Create";
}
<h2>Create</h2>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Member</legend>
<div class="editor-label">
@Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Mail)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Mail)
@Html.ValidationMessageFor(model => model.Mail)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.CategoryID, "Category")
</div>
<div class="editor-field">
@Html.DropDownList("CategoryID", String.Empty)
@Html.ValidationMessageFor(model => model.CategoryID)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.BirthDay)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.BirthDay)
@Html.ValidationMessageFor(model => model.BirthDay)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
—————————————————————————————–
・Delete.cshtml
—————————————————————————————–
@model MyService.GroupServiceReference.Members
@{
ViewBag.Title = "Delete";
}
<h2>Delete</h2>
<h3>Are you sure you want to delete this?</h3>
<fieldset>
<legend>Member</legend>
<div class="display-label">Name</div>
<div class="display-field">
@Html.DisplayFor(model => model.Name)
</div>
<div class="display-label">Mail</div>
<div class="display-field">
@Html.DisplayFor(model => model.Mail)
</div>
<div class="display-label">Category</div>
<div class="display-field">
@Html.DisplayFor(model => model.Categories.Name)
</div>
<div class="display-label">BirthDay</div>
<div class="display-field">
@Html.DisplayFor(model => model.BirthDay)
</div>
</fieldset>
@using (Html.BeginForm()) {
<p>
<input type="submit" value="Delete" /> |
@Html.ActionLink("Back to List", "Index")
</p>
}
—————————————————————————————–
・Details.cshtml
—————————————————————————————–
@model MyService.GroupServiceReference.Members
@{
ViewBag.Title = "Details";
}
<h2>Details</h2>
<fieldset>
<legend>Member</legend>
<div class="display-label">Name</div>
<div class="display-field">
@Html.DisplayFor(model => model.Name)
</div>
<div class="display-label">Mail</div>
<div class="display-field">
@Html.DisplayFor(model => model.Mail)
</div>
<div class="display-label">Category</div>
<div class="display-field">
@Html.DisplayFor(model => model.Category.Name)
</div>
<div class="display-label">BirthDay</div>
<div class="display-field">
@Html.DisplayFor(model => model.BirthDay)
</div>
<div class="display-label">Age</div>
<div class="display-field">
@Html.DisplayFor(model => model.Age)
</div>
</fieldset>
<p>
@Html.ActionLink("Edit", "Edit", new { id=Model.ID }) |
@Html.ActionLink("Back to List", "Index")
</p>
—————————————————————————————–
・Edit.cshtml
—————————————————————————————–
@model MyService.GroupServiceReference.Members
@{
ViewBag.Title = "Edit";
}
<h2>
Edit</h2>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
@using (Html.BeginForm())
{
@Html.ValidationSummary(true)
<fieldset>
<legend>Member</legend>
@Html.HiddenFor(model => model.ID)
<div class="editor-label">
@Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Mail)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Mail)
@Html.ValidationMessageFor(model => model.Mail)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.CategoryID, "Category")
</div>
<div class="editor-field">
@Html.DropDownList("CategoryID", String.Empty)
@Html.ValidationMessageFor(model => model.CategoryID)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.BirthDay)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.BirthDay)
@Html.ValidationMessageFor(model => model.BirthDay)
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
—————————————————————————————–
・Index.cshtml
—————————————————————————————–
@model IEnumerable<MyService.GroupServiceReference.Members>
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table>
<tr>
<th>
Name
</th>
<th>
Mail
</th>
<th>
Category
</th>
<th>
BirthDay
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Mail)
</td>
<td>
@Html.DisplayFor(modelItem => item.Categories.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.BirthDay)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.ID }) |
@Html.ActionLink("Details", "Details", new { id=item.ID }) |
@Html.ActionLink("Delete", "Delete", new { id=item.ID })
</td>
</tr>
}
</table>
—————————————————————————————–
Viewのコードはスキャッフォールドで作成したコードからmodelの名前空間を変更し、また、計算項目として追加していたAgeがデータベース内にないことからその表示部分を削っただけの修正しかしていません。
これでとりあえず動くサンプルが完成しました。
データの追加/削除/変更が自由に行え、それがWebサービスをとおしてデータベース内に反映されることを確認してみましょう。
少しさわっていると、バリデーションが行われないことに気づくと思います。
最初のサンプルと同様のバリデーションを組み込むにはMetaDataを利用するのが簡単です。
Modelsフォルダに以下のコードを追加します。
・MetaClass.cs
—————————————————————————————–
using System.ComponentModel.DataAnnotations;
namespace MyService.GroupServiceReference
{
[MetadataType(typeof(MyMetaData))]
public partial class Members
{
}
public class MyMetaData
{
[Required(ErrorMessage="名前は必須入力です")]
public object Name { get; set; }
[RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}", ErrorMessage = "メールが正しくありません")]
public object Mail { get; set; }
}
}
—————————————————————————————–
これでバリデーションが実行されるようになります。
MetaDataを利用する際に一番注意が必要なことは上記のプログラムでいうとpartialクラスとして定義しているMembersの名前空間を含めた定義になります。
この部分がバリデーションをあてようとしている元のクラスと正しく一致しないと設定が反映されません。
やっかいなことに元のクラスと一致していない場合でもコンパイルエラーにはならないため、なぜ設定が反映されないのか悩んで悩んで、結局このクラス定義にミスがあった、ということがよくあります。
○ふりかえりのまとめ
ASP.NET MVC では、.NET Frameworkが用意しているデータ連携の機能すべてを利用できます。
CodeFirstベースでデータベースを利用することもできますし、このサンプルのようにODataベースのWebサービスを簡単に利用することもできます。
今回は説明していませんが、XMLベースでのデータ配信を取り込んで利用するのも簡単です。
そして、多少利用できるメソッドの違いはありますが、全体的な流れはModel側のデータがなんであっても変わりません。
ASP.NET MVC 3ではスキャッフォールドで生成されるコードがかなり参考にできるコードになったので、ぜひ一度自分で試して、中身を納得できるまでよく解析してみると良いと思います。
なおセッション中では特に言及しませんでしたが、このBlogの内容はすべて無償版であるVisual Web Developerで動作させ、確認を行っています。
WebPIからVisual Web Developerをインストールすると必要な環境はすべて整いますので、ぜひインストールして試してみてください。
ASP.NET MVCはWebアプリケーションの開発において非常に魅力的な技術です。
CodeFirstの登場により、開発者にとってはその魅力がより増した、と感じています。
これらの技術に触れる最初の手がかりとして、このBlog記事が役に立つといいな、と思っています。
WEB+DB PRESS に .NETの記事を書きました(そして続きます)
こちらの雑誌に「いまどきの.NET開発」という題で1年間(隔月刊なので6回)連載することになりました。
Vol.63には第一回 「.NETの現在(いま)」という記事が載っています。
「.NET開発」という言葉の指す範囲がだいぶ広くなってますので、そのあたりの誤解とか勘違いとか最近私が気になってたところをまとめた記事です。
このため、開発系の雑誌なのに、プログラムコードが1行も書いてないという。。。(w
まぁ、次からは少しコードもでてくるかと。
1年間という長丁場なので、先のことはまだ確定していなかったりします。
リクエストがあれば、それにお答えした記事を書くことも可能かもしれませんので、もし何かありましたらご連絡いただけるとうれしいです。
そのまえに、まず読んでいただけるとほんとありがたく。
mvcConf @:Japan ふりかえり 5: 同時実行制御
CodeFirstとASP.NET MVCとの組み合わせでは同時実行制御も簡単に実装することができます。
ふりかえり1で作成したサンプルプログラムを同時実行制御のエラーチェックができるように修正してみましょう。
まず、POCOのクラスに以下のようにTimestamp属性を付加した制御用の項目を追加します
—————————————————————————————————–
public class Member
{
public int ID { get; set; }
[Required(ErrorMessage="名前は必須入力です")]
public string Name { get; set; }
[RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}", ErrorMessage = "メールが正しくありません")]
public string Mail { get; set; }
public int CategoryID { get; set; }
public DateTime? BirthDay { get; set; }
public int? Age
{
get
{
int? age = null;
if (BirthDay != null)
{
age = DateTime.Now.Year – BirthDay.Value.Year;
if (DateTime.Now < BirthDay.Value.AddDays(-1).AddYears(age.Value)) age -= 1;
}
return age;
}
}
[Timestamp]
public Byte[] Timestamp { get; set; }
public virtual Category Category { get; set; }
}
—————————————————————————————————–
これにより、データベースにTimestamp項目が追加されます。
データの型がByte[]型であることに注意しておきましょう。
スキャッフォールドで生成されるコントローラー、ビューはTimestamp項目がないときと同じです。
そのままだと更新時にエラーとなるので、Editビュー(Edit.cshtml)にhiddenとしてTimestamp項目を追加します。
—————————————————————————————————–
@model MyDal.Member
@{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Member</legend>
@Html.HiddenFor(model => model.ID)
<div class="editor-label">
@Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Mail)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Mail)
@Html.ValidationMessageFor(model => model.Mail)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.CategoryID, "Category")
</div>
<div class="editor-field">
@Html.DropDownList("CategoryID", String.Empty)
@Html.ValidationMessageFor(model => model.CategoryID)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.BirthDay)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.BirthDay)
@Html.ValidationMessageFor(model => model.BirthDay)
</div>
@Html.HiddenFor(Model => Model.Timestamp)
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
—————————————————————————————————–
同時実行制御のエラーが発生したときは例外としてDbUpdateConcurrencyExceptionがthrowされるので、このエラーをキャッチします。
実際のエラー時の対処はシステムの仕様によって異なります。
ここではとりあえずエラー時のメッセージを表示するようにだけEditアクションに手を入れます。
—————————————————————————————————–
[HttpPost]
public ActionResult Edit(Member member)
{
try
{
if (ModelState.IsValid)
{
db.Entry(member).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DbUpdateConcurrencyException e)
{
ModelState.AddModelError(string.Empty, e.Message);
}
ViewBag.CategoryID = new SelectList(db.Categories, "CategoryID", "Name", member.CategoryID);
return View(member);
}
—————————————————————————————————–
このようにサンプルプログラムを修正して実行します。
ブラウザを2つ起動して、片方でEdit画面を表示させた状態でもう1つのブラウザから同じデータを更新します。
その後、先にEdit画面を表示していたほうでデータを更新しようとすると以下のようなエラーが表示されます。
このサンプルで基本的な動作は理解していただけると思います。
より実際の動きに近い形の同時実行制御のサンプルがこちらで公開されています。
ぜひ参考にしてみてください。
mvcConf @:Japan ふりかえり 4: バリデーション(入力値チェック)
ASP.NET MVCでは入力値のチェックに属性の設定によるバリデーションが利用できます。
Modelとして作成したクラスのプロパティに属性を設定するだけで以下のようなエラーチェックが可能です。
ここではNameプロパティに
[Required(ErrorMessage="名前は必須入力です")]
Mailプロパティに
[RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}", ErrorMessage = "メールが正しくありません")]
という属性を設定しています。
スキャッフォールディングで作成したビューでは、標準で上記の属性によるバリデーションがJQuery.Validateが利用され、クライアント側でチェックが行われるようになっています。
ビュー内に以下の記述がありますが、この記述をはずすとクライアント側でのチェックが行われなくなり、一度サーバーにリクエストをなげて、そこでエラーチェックが行われるようになります。
—————————————————————————————————–
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
—————————————————————————————————–
属性によるバリデーションは基本的に1つの項目(プロパティ)に対してチェックを行うものです。
複数の項目の値によってチェックを行いたい、という場合には属性ではチェックできません。
複数の項目間の値チェックを行うためにModelクラスに IValidatableObject を実装する、という方法が使えます。
Memberクラスを次のように修正します。
—————————————————————————————————–
public class Member : IValidatableObject
{
public int ID { get; set; }
[Required]
public string Name { get; set; }
[RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}", ErrorMessage = "メールが正しくありません")]
public string Mail { get; set; }
public int CategoryID { get; set; }
public DateTime? BirthDay { get; set; }
public int? Age
{
get
{
int? age = null;
if (BirthDay != null)
{
age = DateTime.Now.Year – BirthDay.Value.Year;
if (DateTime.Now < BirthDay.Value.AddDays(-1).AddYears(age.Value)) age -= 1;
}
return age;
}
}
public virtual Category Category { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Age >= 20 && Mail == null)
yield return new ValidationResult("20歳以上メールアドレス必須");
}
}
—————————————————————————————————–
IValidatableObjectをMemberクラスの基底クラスとして設定し、Validateメソッドを実装しています。
Validateメソッド内ではエラーチェックを行い、エラーがある場合にyieldでValidationResultを返しています。
これにより、年齢が20歳以上になる場合にMailプロパティに値が入力されていないと、以下のようにエラーが表示されます。
バリデーションの実装方法にはほかにもいくつかの方法があります。
たとえばコントロール内でModelStateクラスにたいしてAddModelErrorメソッドを利用して直接エラー状態を設定する、といったことも可能です。
バリデーションの使い分けに関しては適材適所という考え方もありますが、むしろシステム全体として利用方針を決めておくべきでしょう。
バリデーションだけでなく、ASP.NET MVCは自由度が高いために同じことを実現するのに複数の実装方法が選択できる、ということが多いです。
このような場合、システムとしての実装方法の利用方針をあらかじめ決定しておくことで、統一感がありメンテナンスのしやすいシステムが構築できると思います。