2010年10月24日日曜日

MVC 3 Betaのステップイン

前回のエントリーでも行ったように、デバッグ時にソースにステップインできると便利ですよね。ソースサーバーに上がってるならそっちのほうが手軽だけど。

とりあえず、ASP.NET MVC3 Betaのソースにステップインしてみましょう!

aspnet - Release: ASP.NET MVC 3 Beta

stepin1

ソースのダウンロードをしたら、まずは新規プロジェクトの作成。

stepin2

stepin3

とにかく作る。何も考えずにクリック・クリック。そしたら↓こうなりますね。

stepin4

ここからです!

参照設定から、

  • System.Web.Mvc
  • System.Web.Helper
  • System.Web.WebPages

の3つを削除。男らしくね。乙女らしくでもいい。

stepin5

つづいて、ソリューションにダウンロードしたMVCのソースからプロジェクトを追加。既存プロジェクトの追加ですね。

結構沢山必要です。

  • mvc3-beta-source\
    • mvc3\
      • src\
        • SystemWebMvc\System.Web.Mvc.csproj
    • webpages\
      • src\
        • System.Web.Helpers\System.Web.Helpers.csproj
        • System.Web.Razor\System.Web.Razor.csproj
        • System.Web.WebPages\System.Web.WebPages.csproj
        • System.Web.WebPages.Razor\System.Web.WebPages.Razor.csproj
        • WebMatrix.Data\WebMatrix.Data.csproj

stepin6

あとは、アプリケーションプロジェクトで削除した参照設定の分をここで追加。

stepin7

ほらね、これでReSharperも黙った。

stepin8

最後の一手が必要なんだけど、この段階でステップインしてみましょう。とりあえず、RazorViewのRenderViewを開いてブレークポイント。F5を押してみる。

stepin9

残念ながら例外ラッシュ。

stepin10

stepin11

最後の一手を打つ時が来ました。

エラー内容の通り、デフォルトのweb.configだとGACのアセンブリを見るようになってるので、ちゃんとローカルのアセンブリを見るよう変更する必要があります。

アプリケーションルートのweb.configのassembliesセクションを以下のように。

<compilation debug="true" targetFramework="4.0">
  <assemblies>
    <add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <!--<add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <add assembly="WebMatrix.Data, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <add assembly="System.Web.Helpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />-->
    <add assembly="System.Web.Mvc" />
    <add assembly="WebMatrix.Data" />
    <add assembly="System.Web.WebPages" />
    <add assembly="System.Web.Helpers" />
  </assemblies>
</compilation>

~/Views/web.configも同じようにGAC見ないように変更。

<configSections>
  <!--<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
    <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
    <section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
  </sectionGroup>-->
  <sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor">
    <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor" requirePermission="false" />
    <section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor" requirePermission="false" />
  </sectionGroup>
</configSections>

<system.web.webPages.razor>
  <!--<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />-->
  <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc" />
  <pages pageBaseType="System.Web.Mvc.WebViewPage">
    <namespaces>
      <add namespace="System.Web.Mvc" />
      <add namespace="System.Web.Mvc.Ajax" />
      <add namespace="System.Web.Mvc.Html" />
      <add namespace="System.Web.Routing" />
    </namespaces>
  </pages>
</system.web.webPages.razor>

いずれも、VersionとCultureとPublicTokenを消すだけね。そうすると...。

stepin12

stepin13

ヒャッホーイ!ステップインするし、ちゃんと動くね!

※ホントはfavicon.icoのnot foundの例外が起きててちょっとウザイけど。

これで何か気になるところがあると何でも調べられて便利この上ないですな。WebPage.InitializePageなんていうvirtualな空メソッドがいて、必ず初期化時に呼び出されてたりするなんてのもみえてきて、ココでも処理の横取りできるな~とか。ワクワクすっぞ。

ちなみに”_ViewStart.cshtml”があるから初期化処理なんかはココに書くんだけど(ギャフン)、RazorViewEngine.ViewStartFileNameにinternal static readonlyでセットされてる。

スクリーンショット祭りだね...。

2010年10月23日土曜日

RazorのLayout

MVC 3 Betaのソースが公開されましたね!凄いリファクタリングが進んでます。WebMatrix namespaceのソース(MVC関係なくWebPageとして)も含まれてるし、マニアにはたまらないですね!

aspnet - Release: ASP.NET MVC 3 Beta

Databaseや各種Helperなんかはドキュメント整理また無くてもすべてまるっとお見通しだ!な状態で使っていけるのは嬉しい限りです。

Scottguのブログで以下のようなエントリがありました。

ASP.NET MVC 3: Layouts with Razor - ScottGu's Blog

WebFormsでいうところの.MasterはWebPagesでLayoutというのですが(基底クラスの違うcshtml/vbhtmlですね)、どうやって使いましょうかという内容です。@RenderBody(他にもSectionとかいいものもたくさんありますよ!)が魔法の言葉です。

まず、簡単にIndex.htmlにLayoutプロパティを指定する方法。これは直感的で分かりやすいですね。対象となるViewのヘッダ部で直書きですから。

で、全部のViewにいちいちLayout書くの面倒だ!ってなった場合、StartPageクラスの隠しViewとして_ViewStart.cshtml(~/Viewsに置いとくとよしなに処理してくれる優れものだけど、規約嫌いの人には気持ち悪いと言われそう)というのがあるので、それを使って一括で指定してしまう方法があります。

ここまでをScottguは紹介してくれてますが、MVCerならだれもが気になるところのViewメソッドのMaster指定オーバーロードの場合どうなるんだべ?というところ。

public ActionResult Index(string name, string country)
{
  ViewModel.Message = "Welcome to ASP.NET MVC!";

  return View();
}

↑これが普通だけど、↓こうするパターンです。

public ActionResult Index(string name, string country)
{
  ViewModel.Message = "Welcome to ASP.NET MVC!";

  return View("Index","_Layout");
}

部分的にLayoutを変更したい時などによく使いますが、StartPageで指定したものと、WebPage内で指定したもの、ViewResultで指定したものの優先順位が気になりますよね!

ね?

試してみましょう。まずは_ViewStart.cshtmlとIndex.cshtmlの両方でLayoutを指定した場合どちらが優先されるのか。比較するために以下のような単純な_SimpleLayout.cshtmlと_PinkLayout.cshtmlを~/Views/Shared直下に置いておきます。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>@View.Title</title>
</head>
<body>
    <h1>Simple Layout</h1>
    <div>
        @RenderBody()
    </div>
</body>
</html>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>@View.Title</title>
</head>
<body style="background-color:pink;">
    <h1>Pink Layout</h1>    
    <div>
        @RenderBody()
    </div>
</body>
</html>

 

この状態で、_ViewStart.cshtmlでは"_Layout.cshtml"を指定し、Index.cshtmlでは"_SimpleLayout.cshtml"を指定してみる。

結果は↓こうです。

layout1

Index.cshtmlに指定したLayoutが優先されますね。

この状態で、ControllerでPinkLayoutを指定してみます。

public ActionResult Index(string name, string country)
{
  ViewModel.Message = "Welcome to ASP.NET MVC!";

  return View("Index", "_PinkLayout");
}

layout2

おぉ~。Controller指定(ViewResult)最強の優先順位。理由は公開されたソースを見ていくとわかるんでしょうね。なんか深くてみるの面倒だな~。見てみるか。えと...。

RazorViewEngineからWebViewPageへ潜って、RazorViewへさかのぼるとLayoutPathをOverridenLayoutPathにセットしてますね。WebPageBaseにLayoutを保持し、WebViewPageにOverridenLayoutPathを保持するという2段構え。RazorViewのコンストラクタでControllerで指定するLayoutパスをRazorViewのLayoutPathに保持して、その値をWebViewPageのOverridenLayoutPathにさらにセットすることで、Razor単体でのLauout設定(_ViewStart.cshtmlとIndex.cshtmlでのLayout)を無視して適用できるようにするってことですね。いつ無視するのかというとWebViewPageのExecutePageHierarchyでしょうか。WebPageBase.Layoutプロパティを!String.IsNullOrEmpty(OverridenLayoutPath)の時に上書いてます。

Razorテンプレートがコンパイルされた時点で、Layoutプロパティの値をセットするというコードが生成されてるけど、それを実行時(Execute実行後)に上書くことでControllerからLayoutを指定できるようになっているという仕組みということですね。

ベースクラスのExecutePageHierarchyを実行後(WebPageBase.ExecutePageHierarchy内部でExecuteを呼び出します)、OverridenLayoutPathを上書きです。

#pragma checksum "…\Views\Home\Index.cshtml" "{406ea660-64cf-4c82-b6f0-42d48172a799}" "AE4546615BCDE09DA34E6447CC0AFD71"
//------------------------------------------------------------------------------
// <auto-generated>
//     このコードはツールによって生成されました。
//     ランタイム バージョン:4.0.30319.1
//
//     このファイルへの変更は、以下の状況下で不正な動作の原因になったり、
//     コードが再生成されるときに損失したりします。
// </auto-generated>
//------------------------------------------------------------------------------

namespace ASP {
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Web;
    using System.Web.Helpers;
    using System.Web.Security;
    using System.Web.UI;
    using System.Web.WebPages;
    using System.Web.Mvc.Ajax;
    using System.Web.Mvc;
    using System.Web.Mvc.Html;
    using System.Web.Routing;
    
    
    public class Index_cshtml : System.Web.Mvc.WebViewPage {
        
        protected ASP.global_asax ApplicationInstance {
            get {
                return ((ASP.global_asax)(Context.ApplicationInstance));
            }
        }
        
        public override void Execute() {

            
            #line 1 "C:\Users\takehara\Documents\My SkyDrive\Develop\MySamples\Mvc3bApplication1\Mvc3bApplication1\Views\Home\Index.cshtml"
  
    Layout = "~/Views/Shared/_SimpleLayout.cshtml";
    View.Title = "Home Page";


            
            #line default
            #line hidden
WriteLiteral("\r\n<h2>");


            
            #line 6 "C:\Users\takehara\Documents\My SkyDrive\Develop\MySamples\Mvc3bApplication1\Mvc3bApplication1\Views\Home\Index.cshtml"
Write(View.Message);

            
            #line default
            #line hidden
WriteLiteral("</h2>\r\n<p>\r\n    To learn more about ASP.NET MVC visit <a href=\"http://asp.net/mvc" +
"\" title=\"ASP.NET MVC Website\">http://asp.net/mvc</a>.\r\n</p>\r\n</script>");


        }
    }
}

 

ん?なんか違う気がしてきた。BuildManagerから出力されたコードだと、Execute内でLayoutセットしてるよね...。おやや~?モヤモヤする。まぁ、いっか。また今度調べてみよう。

追記

で、ちょっと調べたらPageContextっていうのがいた。Push/Popで積んでいく感じで実行するんだけど、これがなにかするくさい。普通に指定されたLayoutを実行したあとにOverridenLayoutPathを実行するのか、実行そのものを置き換えるのか。今度はそこか。Layoutの状態で実行した後、OverridenLayoutPathで実行?そんな無駄な動きするかな~。

しょうがないのでステップ実行。

layout4

RenderView.ExecutePageHierarchyでWebPageBase.ExecutePageHierarchyを実行。ここでStartPageを登録しているので_ViewStart.Executeを実行。続いてChildPage(WebViewPage)として登録されているIndex.cshtmlのExecutePageHierarchyを実行。この時の出力をスタックに積んでいるStringWriterに置き換える(もとはHttpWriter)。で、Index.cshtmlの引数なしWebPageBase.ExecutePageHierarchyを実行(深い)。この中からIndex.cshtmlのExecuteがやっと実行される。このExecuteがRazorテンプレートの出力結果でoverrideされてる実体ですね。なので、ここでページ単位でのLayout指定が実行されます。ちなみにこの段階ではまだ_Layout.cshtmlは実行されてないですね。

ここでやっとOverridenLayoutPathの登場。自身のLayoutをこの値で上書き。WebPageBase.ExecutePageHierarchyに戻ってPopContext()。誰やねん!これが重要で、この中でStringWriterに書き出したRazorのExecute結果を退避。やっとLayoutの実体が見えてきた。

layout3

RenderSurroundingに退避したコンテンツの実体と、Layoutを渡してます。この中で@RenderBodyにコンテンツの実体を埋め込んでレスポンスに流れていくということでした。長い...。

と、言うわけで、Index.cshtmlを実行後、対象となるLayoutをOverridenLayoutPath(Controllerで指定)を優先で取り出し、結果的に_PinkLayout.cshtmlが実行されるというわけですね。スッキリ!

WebPageExecutingBaseに階層がコメントとして残されてます。この辺はソースを見ることの特権ですね。流石のクオリティです。

/*
WebPage class hierarchy

WebPageExecutingBase      The base class for all Plan9 files (_pagestart, _appstart, and regular pages)
    AppStartBase          Used for _appstart.cshtml
    WebPageRenderingBase
        PageStartBase     Used for _pagestart.cshtml
        WebPageBase
            WebPage       Plan9Pages
            ViewWebPage?  MVC Views
*/

2010年10月17日日曜日

カペタ

カペタが面白いとクマさんが言ってたので読んでみたら(全然グルメ漫画とかじゃなく予想外だったけど)、これがかなり面白くてびっくりした。

サルベージすらしてなくてすいません...。

そろそろ真面目にData Services&PowerPivot使ってデータ集計できるようにしておくかと思い、どりゃっとセットアップしてたんですが、100万レコード超えたりとかしてるでかいテーブルに対して、全然レスポンスが帰ってこないくて困った。そんな方、他にもいないですか?あ、ADO.NET Data Servicesのほうです。.NET3.5のほうです。WCFじゃないほうです。

動作的にあからさまにタイムアウトにもかかわらず、レスポンスストリームに何も出力されないっていう気持ち悪い状況なんですよね。コマンドラインでcurl使ってリクエストしてもレスポンスがないぜ!って怒られる。んで、どのレイヤーでタイムアウトしてるのかな~って思うわけじゃないですか。

なんだかんだ言ってもWCFなんでbindingら辺が怪しいのかな~と思っていろいろセットしてみたんだけど、よくわからん。

ADO.NET Data Services, Entity Framework und SQL/HTTP Timeouts | Marco Scheel aka GeekDotNet

↑ここにいろいろ書いてるのを見て試したんですけど、どれもイマイチ。CreateDataSourceのCommandTimeout設定なんてちょっと素敵な感じですよね。いろんなレイヤー気にするのもな~、と思って思い出したんですよね。

ダウンロードの詳細 : Windows 7 および Windows Server 2008 R2 用 .NET Framework 3.5 SP1 の ADO.NET データ サービス更新プログラム

これ。そーか、と。サーバーサイドページングを有効にしておけば、まるっとすべて解決なんじゃんね。データ件数をそもそも絞ってしまえばどのレイヤででもタイムアウトしないじゃんよ!ってことですよ。PowerPivotはさ、賢いから自動でページングして全データ抽出してくれるんですよ。

Server Paging in Data Services - WCF Data Services Team Blog - Site Home - MSDN Blogs

InitializeServiceにIDataServiceConfigurationでそのままconfigを受け取ってると見えないけど、DataServiceConfirugarionクラスとして受け取るか、asで型指定してしまえば見えるようになるじゃないですか。

var v2 = config as DataServiceConfiguration;
if (v2 != null)
{
 v2.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
 v2.SetEntitySetPageSize("*", 100);
}

SetEntittSetPageSizeですべてのテーブルを100件ページング指定してしまえばいいね!