2009年10月17日土曜日

ASP.NET MVC 2 Preview2でのArea

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が自動登録されるようになる仕組みです。

特に特徴的なのは、プロジェクトを分けなくてもエリア機能が使えるようになってるところです。これまでわざわざ別プロジェクトを作成しないとエリア機能を使えなかったんだけど(規模が大きくなっても開発効率が悪くならなくても済むようにプロジェクト分割は常套手段ですよね?)、あえて分割を強制しなくなりました。

と、文章だと全然意味わからないですね。

p2area

↑こんな感じです。

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に対してきちんと指定するようにします。

p2area2

※ちゃんと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の型を変えておきます。なんでこうするかというと、単に以下のエラーが起きるのを確認したかったから。

p2area3

Asp.NET MVC 2 Preview 2: Area's aspx namespace problem - Stack Overflow

この現象を起こしてみたかったんデス。これを回避するには~/Areas/Sub1/Viewsフォルダに~/Viewsフォルダにあるweb.configをコピーしておく事。これを忘れるとジェネリックで他のアセンブリ(じゃ無かったとしても?試してみてね!)に含まれるモデルクラスを指定すると上記エラーが発生。ビックリですね~。pageParserFilterTypeが処理してくれないって事です。

ここまでやって実行したのが↓この画面。標準/Sub1/Sub2それぞれのHome/Indexがちゃんと判別できて実行されてるのが確認出来ます。

p2area4 p2area5 p2area6

ちなみにAreaRegistrationクラスの派生クラスの名前と、~/Areasフォルダが何でもいい理由というのが、AreaRegistration.RegisterAllAreasのソースに書かれてる内容から判断出来ます。何をしてるかというとBuildManagerWrapper.GetReferencedAssembliesでアセンブリの参照出来るすべてのTypeの中からAreaRegistrationの派生クラスを抽出(IsAreaRegistrationTypeをPredicateとして)してるからですね。後はCreateContextAndRegisterでNamespacesを追加した上でoverrideされてるRegisterAreaを呼び出して、ルートを登録するだけ。なので、AreaRegistration派生(ちなみにAreaRegistration自体はabstractなので除外されます)をすべて抽出するので名前は自動でわかるようになってるというオシャレ実装。素敵だね!

DataAnnotationsのカスタム属性実装とセットになったソースは以下からどうぞ。

眠し!