2009年12月20日日曜日

RBKもなかなか

買っちゃった。TOUR大好きなのに乗り換え。

IMG_2542

衝動買いしちゃった。いつもの軟骨が邪魔をするかと思いきや、今のところ特に痛いところもなく。

まぁ、ねじ山が最初からつぶれてたり(スズキ君にネジもらったから問題なし。ありがとう!)、紐止めの部分がいきなり外れたりして、ドキドキしたけど新しい靴はいいもんだね。

モデルチェンジで安くなってたからトップモデルを安く買えたのは良かったけど、ウィールが最悪。雨で濡れた上を滑ってるような気になるくらいのグリップの無さ。マイクロベアリングの良さを生かすにはウィール交換が高くつきすぎるからなんかヤダしな~。とりあえず、ノーマルのベアリングとウィールで乗り切る事にしようと思うところです。

履き心地に慣れる前に戸塚が無くなるのは痛いけど、保土ヶ谷でまったりホッケーでならそうかな。ところで、ずいぶん更新してなかったのね。ブログ。さーせん。

2009年11月29日日曜日

internalのテスト

internalクラスのテストってどう書くのがセオリー?まさかReflectionってことはないよね?今まで知らなくてズルッコしてpublicにしてたんだけど、まさかこんな簡単な方法があったとは。

Easy way to use TDD with internal classes - Sean McAlinden's Blog

常識なんすか!?

namespace Samples
{
  internal class TestInternal
  {
    public string Test()
    {
      return "TestInternal method";
    }
  }

  internal static class TestInternalStatic
  {
    public static string Test()
    {
      return "TestInternalStatic method";
    }
  }
}

普通にこんなclassを作ると、Testクラスで↓こうなるもんね。

test

でも、AssemblyInfo.csに↓この行を追加。

[assembly: InternalsVisibleTo("Samples.Tests")]

※テストプロジェクトのアセンブリ名。今回はSamples.Testsっていうのを作ったのでこの名前。

test2

コンパイルエラーも出ないし、テストも普通に通る。

なんかもう今までのコードを全部書き直したくなってきた...。

Timerでいいんすか

ちょっと試しにCacheDependencyを書いてたんだけど。

using System;
using System.Web.Caching;
using System.Timers;

namespace Sample
{ public class TimerCacheDependency:CacheDependency { private readonly Timer _timer; public TimerCacheDependency(DateTime expireTime) { _timer = new Timer((expireTime - DateTime.Now).TotalMilliseconds); _timer.Elapsed += (sender, e) => NotifyDependencyChanged(this, EventArgs.Empty); _timer.Start(); } protected override void DependencyDispose() { if (_timer == null) return; _timer.Dispose(); } } }

絶対時間でExpireさせるCacheDependency。 Timer使っていいのかな。なんかキャッシュに同時に1万とかデータ入れると1万のTimerだよね...。

Comparing the Timer Classes in the .NET Framework Class Library

特になにかリソースを消費しまくるってわけではない?

Pro ASP.NET 3.5 in C# 2008 - Google ブックス

こっちはSystem.Threading.Timer使ってるな...。ぶふ~。

2009年11月20日金曜日

Html.ActionとHtml.RenderAction

なんともエントリを書かなすぎでした。いろいろ遊んだりしてたんす。シバトラ読みふけったり。

Html.RenderAction and Html.Action

PDC09が盛り上がりまくって、Feedの確認が全然追いつかないなか、ASP.NET MVC 2もベータが公開。早速Philさんが面白い機能の紹介をしてくれてたので少し確認。確認してタンブラのほうで軽くコメント書いとこうと思ってたけど、内容が面白すぎたので、こっちに書いてみるっす。

まずはプロジェクトを作ろうとVS立ち上げたけど、なんかちゃんとできない。なんで~!と思ったらベータインストールしてなかった...。ソースだけダウンロードして見てるだけな楽しみ方もありですよね。無しだな。Preview2をアンインストールしてからベータ入れて見るものの、V1の時と同じように日本語環境にはちゃんと入ってくれなかった。いきなりギャフンですね。プロジェクトテンプレートを1041にコピーしてdevenv /installvatemplates。

準備も出来たところで、サブジェクトの機能について簡単に説明。

Html.Actionはアクションの実行結果をMvcHtmlStringにして返してくれるもので、Html.RenderActionは同じくアクションの実行をしてくれるけど、こっちは文字列としてではなく現在のレスポンスストリームに結果を書き出してくれるものです。

もともとFuturesに入ってた機能ですけど、出世してリリースアセンブリに含まれるようになりました。パッと見、Html.ActionLinkと勘違いしそうなヘルパーだけど、中身は全然違う物です。ソースではChildActionExtensionsにまとまってるので興味のある方はぜひ。

で、中では何をしてるのかというと、Server.Executeです(なのでPageの派生クラス使ってる)。IHttpHandlerとしてラッピングしてProcessRequestを実行してます。この辺の仕組みはFuturesの頃から変わってないです。コードはカッコ良くリファクタリングされてますが。ちなみに今回のリリースには非同期アクション実行も含まれてるので、IHttpAsyncHandlerにももちろん対応してます。ここら辺の実装がHttpHandlerUtil.WrapForServerExecuteですね。500エラー以外のHttpExceptionをServer.Executeが伝播してくれないらしく、がんばった感じが見て取れます。

    public ActionResult Partial(string id)
    {
      return Content("Partial result" + id);
    }

↑こんなアクションメソッドをHomeControllerに定義しておき、Home/Index.aspxで以下のように書いておく。

    <div><% = Html.Action("Partial",new{id=" cool!"}) %></div>
    <div><% Html.RenderAction("Partial", new {id = " so nice!"}); %></div>

そうすると出力されるのは↓こんな感じです。

action

でもって、このPartialアクションメソッドはそのまま"/home/partial"ってやっても呼び出せてしまいますね。Ajaxでの部分更新に使うならいいけど、そうじゃなくHtml.Action/RenderActionでしか使わないならChildActionOnly属性をアクションメソッドに指定しておきましょう。

そしたら↓こう。

action2

ブラウザからは直接アクセス出来なくなります。なんで~!と気になったらソースを確認。ChildActionExtensions.CreateRouteDataでServer.Execute対象のRouteDataを生成してるんですが、その時に現在のViewContextをRoute.DataTokensに入れてます。Areaでnamespaceを渡して違うnamespaceのControllerをRoute登録するのと同じやり方ですね。そのDataTokensが存在してるのかどうかをChildActionOnlyAttributeがチェックして存在してなければ、実行出来ないようにする仕組みです。素敵だね。

で、Viewのコードを見てみるとHtml.Actionは<%= … %>で実行してるのがわかるでしょうか?これはつまり文字列をそのままViewの一部に埋め込んで最後にまとめてレスポンスですよね。これに対してHtml.RenderActionは<% …; %>コード実行の書き方です。なので、レスポンスストリームに書き出すものです。なので、Philさんが書いてる通り、部分的なCacheを有効にしたいときはRenderActionを使う必要があります。Response.WriteSubstitutionを使った出力はFuturesに別途用意されてるね。Html.Substitute。

ね?面白いでしょ?

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のカスタム属性実装とセットになったソースは以下からどうぞ。

眠し!

2009年10月8日木曜日

最初の目標達成!

さっきブログに書いて気がついたんだけど、Feedの購読数がなんかキリ番。

feed

目標はそこじゃなくてですね、最近始めたstackoverflow.comへの書き込みでReputationが最初の目標の100に今朝やっと達しました。次は200?

いやいや500としよう。

rep

何気に英語の勉強もかねて翻訳サイトなんかも駆使しつつ書き込んでいこうと決めて3週間。長かった。でも、超うれしい!

日本語サイトでもASP.NET MVCのフォーラムなんかがあるなら、分かる範囲で書き込みして行きたいところデス。どこかにあるならだけど。

で、話は変わりますが、うちのボスが書いてるのでこっちでも書いておきます。

また社員募集しています

たけはらがYour Fireされたわけじゃなくて、普通に人員増強です。募集要項なんかの細かい話はボスのエントリか募集サイトを見てもらえればいいかと思います。自分新人なんでその辺の話はよくわかんないです。

株式会社クロスワープ/システムエンジニアの転職情報

ただ、これだけは言っておきたい。

たけはらのファンかどうかは採用に関係しないので明記する必要はありません!

こんな感じの職場で一緒に開発してもいいというドMな方はぜひお気軽に応募していただければと思います。

2009年10月7日水曜日

V2のFuturesにViewStateが!?

Exploring the ASP.NET MVC 2 futures assemby

小野さんに振られたので↑こちらのエントリに書かれてるViewStateについての調べてみました。こういうきっかけが無いとソースを追いかけない自分に少し反省。

まさかホントにASP.NET MVCにViewStateを持ち込むのか?と、疑いたくなるようなエントリだけど何となくサンプルとして提示されてるコードが怪しい。そのまま転載させてもらうと↓こうですよ。

<% using (Html.BeginForm()) {%>
    <%Html.Serialize("person", Model); %>
    <fieldset>
        <legend>Edit person</legend>
        <p>
            <%=Html.DisplayFor(p => Model.FirstName)%>
        </p>
        <p>
            <%=Html.DisplayFor(p => Model.LastName)%>
        </p>
        <p>
            <label for="Email">Email:</label>
            <%= Html.TextBox("Email", Model.Email) %>
            <%= Html.ValidationMessage("Email", "*") %>
        </p>
        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
<% } %>

Html.Serialize(“person”,Model)ってなんか怪しいですよね。おまえ、ホントにViewStateを吐いてくれるのか?と。1つのViewに何個も書いたらどうなるんだよ、とか、ポストバックしたControllerでコントロールツリーを構築するのか、とかなんやかんやデス。

で、考えててもラチがあかないので、サンプル書いて試してみました。

  [Serializable]
  public class Drink
  {
    [Required]
    [StringLength(10)]
    public string Name { get; set; }

    [Compare("Name", ErrorMessage = "一致しないよ!")]
    public string CheckName { get; set; }

    [Range(10, 50)]
    public int Size { get; set; }
  }

まずは、前回も使ったDrinkクラスにSerializable属性を追加。これつけないとそもそもシリアライズ出来ないです。どこでシリアライズしてるのかFuturesのソースを追いかけると、MvcSerializerクラスで実装してます。ちなみにシリアライズの方式としてPlaintext、Encrypted、Signed、EncryptedAndSignedの4種類があり、初期値はPlaintext。これはSystem.Web.UI.ObjectStateFormatterを使ってシリアライズしてて。って、おや?マジViewStateなのか?まぁ、いいや。

なのでSerialize属性をつけるわけですが、上記クラスに値を入れてサンプル通りにView書いて実行しても何も出力されない...。

<% Html.Serialize("drink",Model); %>

サンプルだからなんかおかしいのかな~。気になるので更にソースを読み進めると、Html.SerializeはそもそもMvcHtmlStringクラスを返してきます。これは、あれですよね、ASP.NET 4で導入される<%: …%>出力に向けた実装ですよ。IHtmlStringってヤツですよ。でも、ASP.NET 3.5にはそんなもの無いので、Futuresの実装はインターフェースは無しバージョン。ってことは、単純に実行したらレンダリングされるわけじゃ無くて、ToString()かToHtmlString()で取得して、それをレンダリングするようにしないとちゃんと出力されないわけですね。

そうとわかれば、以下のように変更。

<% = Html.Serialize("drink",Model).ToHtmlString() %>

これでちゃんと出力されました。以下のようなModelを渡して結果を見てみます。

    public ActionResult Drinks()
    {
      var model = new Drink {Name = "Cola", CheckName = "Pepsi", Size = 30};
      return View(model);
    }

↑これが↓こうなります。

<input name="drink" type="hidden" value="/wEy7AEAAQAAAP////8BAAAAAAAAAAw
CAAAARk12Y0FwcGxpY2F0aW9uMSwgVmVyc2lvbj0xLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWw
sIFB1YmxpY0tleVRva2VuPW51bGwFAQAAABxNdmNBcHBsaWNhdGlvbjEuTW9kZWxzLkRyaW5
rAwAAABU8TmFtZT5rX19CYWNraW5nRmllbGQaPENoZWNrTmFtZT5rX19CYWNraW5nRmllbGQ
VPFNpemU+a19fQmFja2luZ0ZpZWxkAQEACAIAAAAGAwAAAARDb2xhBgQAAAAFUGVwc2keAAA
ACw==" />

バリバリBase64エンコードされてViewStateっぽいです。でも、これを復元させるコードがどうなるかと言うと、↓こうなります。

    [HttpPost]
    public ActionResult Drinks([Deserialize]Drink drink)
    {
      if(ModelState.IsValid)
      {
        // ... success code
      }
      return View(drink);
    }

このDeserialize属性クラスが何をしてるか、ってことデスよね。またしてもソースを確認すると、そこには...。

        private sealed class DeserializingModelBinder : IModelBinder {

            private readonly SerializationMode _mode;

            public DeserializingModelBinder(SerializationMode mode) {
                _mode = mode;
            }

            public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
                if (bindingContext == null) {
                    throw new ArgumentNullException("bindingContext");
                }

                ValueProviderResult vpResult;
                bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName, out vpResult);
                if (vpResult == null) {
                    // nothing found
                    return null;
                }

                MvcSerializer serializer = new MvcSerializer();
                string serializedValue = (string)vpResult.ConvertTo(typeof(string));
                return serializer.Deserialize(serializedValue, _mode);
            }

        }

単なるModelBinder...。Page.ViewStateなわけじゃないですね。単にシリアライズするためにViewStateと同じものを利用してるだけです。最初の方にも書いたけど、ViewStateと同じようにシリアライズ出来るようにするためにEncryptedやSignedが指定出来るようになってるって事です。ちなみにソースを見てみると、暗号化ViewStateを生成するためにprivate sealed class TokenPersister : PageStatePersisterっていうクラスを定義してて、その中でPageクラスのインスタンスを生成し処理させてます。なんか強引。でも、AntiForgeryDataSerializerでも同じ事してたりしてちょいビックリ。

そんなことはいいとして、これが完全に独自のModelBinderになってしまってるので、これを使う場合にはDataAnnotationsが効かない。ので、hidden書き換えを抑制したいときにはEncryptedやSigned、EncryptedAndSignedを指定しておくようにしないとね!

なぜASP.NET MVCにViewStateを持ち込むんだ~、と怒り心頭な方!ご心配なく。WebFormsでいうところのViewStateでは無かったです。ホッとした。

Futuresのソースみてて気がついたんだけど、AsyncControllerがFuturesに戻されてる。Expression系のユーティリティクラスがたんまり入ってて何やら面白そうな予感がしますが、それはまた今度ってことで。

dotnetConf2015 Japan

https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。