Visual Web Developer Team Blog : Single Project Add View in ASP.Net MVC 2 Preview 2
まんまな感じのエントリで申し訳ない気持ちもするんですが、Preview1から全然使い方の変わってる部分でもあるんで、エントリしとこうと思った次第です(もう役にたたない情報ですがPreview1の時のエントリはこちら)。言い訳から書き出すのもすっかり定着してきた感が否めない...。
MVC 2に関してはMSDNにドキュメント整備も進んでるんで、そちらも参照すると更に理解が進んでいいですね。
Walkthrough: Creating an ASP.NET MVC Areas Application Using a Single Project
今回からRouteCollection.MapAreaRouteは廃止され、代わりにAreaRegistrationクラスが導入されてます。同じ目的でAreaRegistrationContextも追加されてます。メインはAreaRegistrationですが、このクラスを派生させたクラス(サンプルはRoutesになってるけど名前は何でもいいです)各エリアフォルダのルートに作成しておき、AreaName/RegisterAreaをそれぞれoverrideして、Route登録時にNamespacesが自動登録されるようになる仕組みです。
特に特徴的なのは、プロジェクトを分けなくてもエリア機能が使えるようになってるところです。これまでわざわざ別プロジェクトを作成しないとエリア機能を使えなかったんだけど(規模が大きくなっても開発効率が悪くならなくても済むようにプロジェクト分割は常套手段ですよね?)、あえて分割を強制しなくなりました。
と、文章だと全然意味わからないですね。
↑こんな感じです。
MvcApplication1という名前のプロジェクトの中にAreasフォルダ(これも名前は何でもいんですが、慣例としてあえてAreas)を作成。その中にエリア分割したいフォルダを更に作成。今回ならSub1とSub2。更にその中にControllersとViewsをそれぞれ作成。仕組みをわかりやすくするためにController名はHome、Action/Viewの名前はIndexとして作成しておきます。
これで、3つのHome/Indexの組が出来たことになります。各Indexアクションは以下の通り。
標準のHome/Index
public ActionResult Index()
{
ViewData["Message"] = "RootのHome/Index";
return View(new{});
}
Sub1のHome/Index
public ActionResult Index()
{
ViewData["Message"] = "Sub1エリアのHome/Index";
return View(new ModelsLibrary.Class1());
}
Sub2のHome/Index
public ActionResult Index()
{
ViewData["Message"] = "Sub2エリアのHome/Index";
return View();
}
ちょいちょいViewに渡すモデルを変えてるのは実験のためです。Sub1とSub2にそれぞれAreaRegistrationクラスを作成しておいときます。
Sub1のAreaRegistration
public class Routes : AreaRegistration
{
public override string AreaName
{
get { return "Sub1"; }
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"sub1_Default",
"sub1/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
}
}
Sub2のAreaRegistration
public class Routes : AreaRegistration
{
public override string AreaName
{
get { return "Sub2"; }
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"sub2_Default",
"sub2/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
}
}
最後にGlobal.asaxのルート登録部分に以下のコードを追加。
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
AreaRegistration.RegisterAllAreas();
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" }, // Parameter defaults
null,
new[] { "MvcApplication1.Controllers" }
);
}
前回のエントリにも書いたように、namespacesを指定しないと各エリアのHome/Indexと区別出来なくてエラーになってしまうので、標準のHome/Indexに対してきちんと指定するようにします。
※ちゃんとnamespacesを指定しないと↑こんな感じでエラーになるっす。
えと、眠くなってきた。あと少し!
すべてのIndex.aspxは以下で統一。
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2><%= Html.Encode(ViewData["Message"]) %></h2>
<h3>Area: <%= ViewContext.RouteData.DataTokens["area"] %></h3>
<h3>Model Type:<%= Model != null ? Model.GetType().ToString() : "(null)"%></h3>
<% = Html.ActionLink("Root Home", "Index", "Home", new { area = "" }, null)%><br />
<% = Html.ActionLink("Sub1 Home", "Index", "Home", new { area = "sub1" }, null)%><br />
<% = Html.ActionLink("Sub2 Home", "Index", "Home", new { area = "sub2" }, null)%>
</asp:Content>
で、Sub1の時だけViewPage<ModelsLibrary.Class1>を指定してModelの型を変えておきます。なんでこうするかというと、単に以下のエラーが起きるのを確認したかったから。
Asp.NET MVC 2 Preview 2: Area's aspx namespace problem - Stack Overflow
この現象を起こしてみたかったんデス。これを回避するには~/Areas/Sub1/Viewsフォルダに~/Viewsフォルダにあるweb.configをコピーしておく事。これを忘れるとジェネリックで他のアセンブリ(じゃ無かったとしても?試してみてね!)に含まれるモデルクラスを指定すると上記エラーが発生。ビックリですね~。pageParserFilterTypeが処理してくれないって事です。
ここまでやって実行したのが↓この画面。標準/Sub1/Sub2それぞれのHome/Indexがちゃんと判別できて実行されてるのが確認出来ます。
ちなみにAreaRegistrationクラスの派生クラスの名前と、~/Areasフォルダが何でもいい理由というのが、AreaRegistration.RegisterAllAreasのソースに書かれてる内容から判断出来ます。何をしてるかというとBuildManagerWrapper.GetReferencedAssembliesでアセンブリの参照出来るすべてのTypeの中からAreaRegistrationの派生クラスを抽出(IsAreaRegistrationTypeをPredicateとして)してるからですね。後はCreateContextAndRegisterでNamespacesを追加した上でoverrideされてるRegisterAreaを呼び出して、ルートを登録するだけ。なので、AreaRegistration派生(ちなみにAreaRegistration自体はabstractなので除外されます)をすべて抽出するので名前は自動でわかるようになってるというオシャレ実装。素敵だね!
DataAnnotationsのカスタム属性実装とセットになったソースは以下からどうぞ。
眠し!