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系のユーティリティクラスがたんまり入ってて何やら面白そうな予感がしますが、それはまた今度ってことで。

2009年10月4日日曜日

ValidationAttributeがちょっと違う

ASP.NET MVC V2も既にPreview 2が出ましたね。入力検証がDataAnnotationsを前提にした設計(IDataErrorInfoじゃ物足りないしね)になってるのと、Templateベースのモデル描画がV2での重要ポイントですよね。

入力検証にxValと同じようにModelValidatorProviderとクライアントサイドバリデーションにjQuery.validateが導入されてのが興味深い。

そもそもV1にも追加コードとして公開されてるDataAnnotationsModelBinderがあって、System.ComponentModel.DataAnnotationsのValidationAttrbiuteを使えるようになてます。が、残念ながら標準アセンブリじゃなく別途配布されてるDLLを参照設定して使うようにしないと、全く機能しないというのがあります。何でかというとValidationAttributeクラスの実装が違うのと、その検証属性の実行方法が違うから。

.NET 3.5 SP1に含まれるVaridationAttributeクラスってIsValid(object value)なのに対し、配布されてるアセンブリではIsValid(object value)に加え、IsValid(object value, ValidationContext validationContext, out ValidationResult validationResult)があります。入力値を取得して検証するだけなら対象となる、プロパティ値があればそれで事足りるかも知れないけど、たとえばCompareValidatorは実装できないですよね。同じモデルインスタンス内の他のプロパティを見れないと比較できないじゃないですか。V1の場合は、特殊なアセンブリだからいいかもしれないけど、V2は標準アセンブリだからVaridationAttribute実装にValidationContextなんて無い(実はある?)。先日1.0がリリースされたxValが使用するアセンブリも標準アセンブリ参照だから当然インスタンス参照なんてない。

と、言うわけで、V1でxValを使うときの入力検証でモデルの他プロパティを参照する際のCompareAttributeクラスを書いて遊んでみました。何に使うのかは後で考える。

まずは、モデルインスタンスを渡せるIsValidを実装するためのインターフェイス定義。ValidationAttributeとそのインターフェースを実装するCompareAttributeクラスを定義。

  public interface IInstanceValidationAttribute
  {
    bool IsValid(object instance, object value);
  }

  public class CompareAttribute : ValidationAttribute, IInstanceValidationAttribute
  {
    public string PropertyName { get; set; }
    
    public CompareAttribute(string propertyName)
    {
      PropertyName = propertyName;
    }

    public override bool IsValid(object value)
    {
      throw new NotImplementedException();
    }

    public bool IsValid(object instance, object value)
    {
      var property = instance.GetType().GetProperty(PropertyName);
      if(property==null)
        throw new ArgumentException("パラメータ間違えてるよ");

      var targetValue = property.GetValue(instance, null);
      if (targetValue != null && targetValue.Equals(value))
        return true;

      return false;
    }
  }

CompareAttributeを使うモデルクラスの定義。他にも必須チェックやら付けてみる。

  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; }
  }

続いて、xValのサンプルを参考に、入力検証を実行するためのDataAnnotationsValidationRunnerクラスを実装。

  internal static class DataAnnotationsValidationRunner
  {
    public static IEnumerable<ErrorInfo> GetErrors(object instance)
    {
      var attrs = from prop in TypeDescriptor.GetProperties(instance)
.Cast<PropertyDescriptor>() from attribute in prop.Attributes.OfType<ValidationAttribute>() select new { Property = prop, Validator = attribute, IsInstanceValidator = attribute is IInstanceValidationAttribute }; foreach(var attr in attrs) { bool isvalid; if (!attr.IsInstanceValidator) isvalid = attr.Validator.IsValid(attr.Property.GetValue(instance)); else isvalid = (attr.Validator as IInstanceValidationAttribute)
.IsValid(instance, attr.Property.GetValue(instance)); if (!isvalid) yield return new ErrorInfo(attr.Property.Name,
attr.Validator.FormatErrorMessage(string.Empty),
instance); } } }

なんかちょっとダサいけど、まぁ、その辺はセンスが無いってことで勘弁してください。 最後にControllerにアクションを追加してコードは完成。

    public ActionResult Drinks()
    {
      var model = new Drink();
      return View(model);
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Drinks(Drink model)
    {
      var errors = DataAnnotationsValidationRunner.GetErrors(model);
      if (errors.Any())
        new RulesException(errors).AddModelStateErrors(ModelState,"");
      
      return View(model);
    }

実行したのが↓これ。

xVal xVal2 

ここまでが、V1の話で、ここからV2 P2で同じことをしてみようと思います。こんなに違うのかと思えるほどDataAnnotationsの組み込みと、ModelValidatorProviderに感動です。CompareAttributeクラスとモデルクラス(Drinkクラス)は一切いじりません。追加で作成の必要なクラスは以下の1つのみ。

  public class CompaireValidator : DataAnnotationsModelValidator<CompareAttribute>
  {
    public CompaireValidator(ModelMetadata metadata, ControllerContext context, CompareAttribute attribute) : base(metadata, context, attribute) { }
    internal static ModelValidator Create(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute)
    {
      return new CompaireValidator(metadata, context, (CompareAttribute)attribute);
    }

    public override IEnumerable<ModelValidationResult> Validate(object container)
    {
      if (!(Attribute as IInstanceValidationAttribute).IsValid(container, Metadata.Model))
        yield return (new ModelValidationResult
                        {
                          MemberName = Metadata.PropertyName, 
                  Message = Attribute.FormatErrorMessage(Metadata.GetDisplayName())
                        });
    }
  }

すごいっすね~。たったこれだけ。で、このValidatorクラスを登録するためにGlobal.asax.csのApplication_Startに1行追加。

    protected void Application_Start()
    {
      RegisterRoutes(RouteTable.Routes);
      DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(CompareAttribute), typeof(CompaireValidator));
    }

プロパティーの取出しからAttributeの取り出しまで一切合財がMVCの処理の範疇になってます。DataAnnotations関連のProvider/Validatorを使って作ってるけど、この辺も自分でベースクラスから派生させたり、AssociatedValidatorProviderからの派生で作ることも可能。もちろん簡単なのはDataAnnotations関連のクラスを派生させて作ること。

ちなみにControllerでの使用は何も考える必要も無く以下のようにしとくだけ。

    public ActionResult Drinks()
    {
      return View(new Drink());
    }

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

ValidationAttributeクラスは標準クラスのままなので、モデルクラスのインスタンス参照を持ってないのは今後もそうだと思うので、今回のような方法でうまいことインスタンスを渡せるオーバーロードを定義することで、対応していくのがいいんじゃないかと思う次第です。

ASP.NET MVC Reference
Brad Wilson: Enterprise Library Validation example for ASP.NET MVC 2
Shaan's Official Blog : New features in ASP.NET MVC 2 Preview

2009年9月26日土曜日

アイスホッケーのルール改定

最近アイスは試合に出てないので知らなかった(レフェリークリニックにも出てないし)んだけど、ルール改定があると語りべさんとこで知りました。戦国時代到来!<1> | 加藤じろう直営!「語りべ」通信

レフェリー2人制とアイシング後のチェンジ禁止。NHLでは既に実施されてるルールだし、これはとてもいいルール改定だと思うな~。アイスホッケーの最大の問題点は「レフェリーに見られなければ反則も反則にならない」部分。どんな競技でもそうだけど。ホッケーって使ってる道具が凶器にもなる危険なものだから悪意をもってそんなことするとすぐケガするし、感情的になって報復行為に行ったりする人をかなり見かける。

上手なプレーヤーもそんな人がいたりして、スポーツマンシップはどこへやら。チェックに行くのとは意味が違う部分の話ね。そこに来てレフェリー2人制は希望を感じる。それを県連レベルでもちゃんとやれればの話だけど。県連レベルで実施することの最大の懸念は「判定基準の同等な2人のレフェリーを常に立たせることが出来るのか」につきる。とりあえず、県連ローカルルールでやるような社会人リーグではどこも適用しなさそうな雰囲気。そんなに沢山(ラインズマンいれて4人必要になる)レフェリー用意できないっていうのは致し方ないのかもね。

アイシングでのチェンジ禁止はクリアしかしないようなディフェンスのいるチームではかなり致命的。でも、ホッケーを楽しむ(試合の勝ち負けだけが楽しみだとあれだけど)にはいいルール。で、こちらについてはルールブックの条文にきちんと記載があって「412条の2 ラインズマンがアイシングコールをするためにハンドアップをした後...」ってなってる。ラインズマンのハンドアップが遅かったり、フロントがアイシングを取ったけど、バックがハンドアップしてないとかになると大荒れの予感。とりあえず今シーズンはAプールのみの適用で様子見みたいだね、神奈川は。

話変わって今日は先週に続けてアメージングで試合。3戦目にしてやっと勝てた。オオニョさんがいなくてマジ助かった。スガマタさんに続いて今期から参戦してくれた新ゴーリーのアオキさんの活躍がすばらしかったデス。なんか最近のS40蒲公英はゴーリーに恵まれてきてる気がする。しかも帰りに寄ったスタバで初めて黒エプロンのバリスタを見た。ちょっと感動した。

2009年9月23日水曜日

SHURE

Amazon.co.jp: Ultimate Ears MetroFi 220 MF220: 家電・カメラ

これをさ~、気に入って使ってたんだけどさ~。

断線。半年で。泣ける。

代わりになる物を探しに電器屋巡りしたけど、置いてないんだね。みんなiPhoneで音楽聞くのと通話とするときどうしてるんですかね。Bluetoothとか充電面倒じゃないっすか?面倒でくじけたんだけど...。

結局、今回もAmazonのお世話になることにしようと思うんだけど、少し学習してイヤホンとマイク部は別々に出来るようにまずは↓これ。

Amazon.co.jp: SHURE iPhoneでの高品位なハンズフリー通話を可能にするマイク付き接続コード MPA-3C: 家電・カメラ

んで、なるべく素の音がいいから、イヤホンは↓これ。

Amazon.co.jp: SHURE 高遮音性イヤホン・ブラック SE102-K-J: 家電・カメラ

写真 

連休が空ける前に届いてくれて嬉しす。明日からの通勤が楽しみだぜ~。遮音って怖い気もするけど、気配を感じ取れるように修行します。

2009年9月21日月曜日

Routeでデフォルト省略させたくない

GenerateLink does not return the control name and the view name only for the INDEX view - Stack Overflow

これってデフォルト値と同じパラメータを省略(後方に有効値パラメータ無しの場合)させずに、URLを取得出来るようになればいい、っていう問題だったり?

  routes.MapRoute(
    "Default",                                              // Route name
    "{controller}/{action}/{id}",                           // URL with parameters
    new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
  );

↑こういう登録をしてる場合に、

  var url1 = Url.RouteUrl("default");
  var url2 = Url.RouteUrl("default", new { controller="Home" });
  var url3 = Url.RouteUrl("default", new { controller = "Home", action="Index" });

↑これは全部"/"と出力されるけど、これが"/Home/Index"になって欲しい、と。違うなら意味なくなっちゃうからそういう質問だと仮定。

やっぱりRouteクラスを派生させてGetVirtualPathのoverrideでしょ!

  public class FullRoute : Route
  {
    public FullRoute(string url, IRouteHandler routeHandler): 
base(url, routeHandler) { } public FullRoute(string url, RouteValueDictionary defaults,
IRouteHandler routeHandler):
base(url, defaults, routeHandler) { } public FullRoute(string url, RouteValueDictionary defaults,
  RouteValueDictionary constraints, IRouteHandler routeHandler):
base(url, defaults, constraints, routeHandler) { } public FullRoute(string url, RouteValueDictionary defaults,
RouteValueDictionary constraints, RouteValueDictionary dataTokens,
IRouteHandler routeHandler):
base(url, defaults, constraints, dataTokens, routeHandler) { } public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) { var dict = new RouteValueDictionary(Defaults); values.ToList().ForEach(kv=>dict[kv.Key] = kv.Value); var url = Url; dict.ToList().ForEach(kv => url = url.Replace(string.Format("{{{0}}}", kv.Key), (string)kv.Value)); url = Regex.Replace(url, @"{([\w].+)?}", ""); url = Regex.Replace(url, @"(\/){2,}", ""); var path = base.GetVirtualPath(requestContext, values); path.VirtualPath = url; return path; } }

※途中のパラメータが空白の時は考慮してないのであしからず。なんかthrowすべきだよね~。

これを使うようにRouteTableに追加。

  routes.Add("FullDefault",
    new FullRoute(
      "{controller}/{action}/{id}/{foo}/{bar}",

      new RouteValueDictionary(new { controller = "Home", action = "Index", id = "", foo = "", bar = "" }),
      new RouteValueDictionary(),
      new MvcRouteHandler()
  ));

※idより後ろのパラメータは特に意味なしデス。

そうすると

  var full1 = Url.RouteUrl("fulldefault");
  var full2 = Url.RouteUrl("fulldefault", new { controller = "Home" });
  var full3 = Url.RouteUrl("fulldefault", new { controller = "Home", action = "Index" });

これが全部"/Home/Index"を返すようになります。

こういう事でいいんですかね?って英語でどうやって答えればいいのか分からない...。

2009年9月20日日曜日

戸塚ホッケー事情

MHLのシーズンも始まって、この土曜で既に2試合消化したけど、どっちも負けたから特に書く事もない...。切ない。とりあえず、あれだ。初戦はゴーリーいなかったからいいとして、2戦目はひどい。あれじゃ、100戦しても100回負ける。もう少し賢く守りましょう。新規加入のゴーリー、スガマタさんはとてもすばらしいゴールテンディングを見せてくれてたよね?後はディフェンスの問題だけなんだから。

で、試合の話はいいとして、今日戸塚でみんなでおしゃべりしてる時に、戸塚に誰がいるのか誰か来るのかを簡単にみんなで共有できるようにするのに、ミチアキさんが「Twitterどう?」って話になったよね。その場にいた数人には説明したけど、なかなか言葉だけだと分かりにくいと思うので、みんなでガンガン使ってみよう!

http://twitter.jp

ネット関連に疎い人も多いと思うけど、スズキ君にはサヨちゃんが上手いこと説明して登録するようにしといてね!S40組にはオレが説明するとして、クラッシャーズ組にはヤマケン選手よろしく!後は、ちょっとづつ広めていきましょう。クローバー組はタカオ?

Twitterって何?って言う人も多いと思うけど、検索すればヤマのように説明出てくるから何とかなるよ。ケータイからのアクセスは...ケータイ持ってないからよく分かんないよ!モバツイッターとか?

そのままTwitterにつぶやいても、メッセージが流れてよく分かんなくなると困るので、戸塚スケートファクトリー用のハッシュタグとして Totuka Skate Factory Hockey の頭文字を取って#tsfhとしてみる感じでどうっすか。つぶやきの最後に#tsfhって書いてつぶやくだけ。そしたらTwitter / Search - #tsfhここにアクセスすればいい、戸塚の状況がすべて見られるようになるっていう寸法。

twitter

戸塚に行く前・着いてから・誰かを誘う時、いろいろつぶやいてみたら便利なんじゃないかね~。

追記

大事なことを書き忘れてた。Twitterでたけはらをフォローするときのアカウント名はtakeparaデス。

2009年9月15日火曜日

こういうのはいいのかどうか

ASP.NET MVC Transaction Attribute (using NHibernate) - Scott's Blog

NHibernateじゃなくても、以下のようなTransactionAttributeで。

using System;
using System.Transactions;
using System.Web.Mvc;

namespace MainSite.Controllers
{
  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
  public class TransactionAttribute : ActionFilterAttribute
  {
    private TransactionScope _transactionScope;

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      _transactionScope = new TransactionScope();

      base.OnActionExecuting(filterContext);
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
      base.OnActionExecuted(filterContext);

      if (filterContext.Exception == null)
        _transactionScope.Complete();

      _transactionScope.Dispose();
    }
  }
}

適当なDataContextを用意してこれまた適当なAction(適当すぎてすいません)で。

    public ActionResult Index()
    {
      ViewData["Message"] = "Welcome to ASP.NET MVC!";

      return View();
    }

    [HttpPost,ActionName("Index")]
    [Transaction]
    public ActionResult IndexPost(string name)
    {
      var db = new DataClasses1DataContext();
      db.Table1s.InsertOnSubmit(new Table1 {name = name});
      db.SubmitChanges();

      //throw new HttpException(500,"Error!");
      return RedirectToAction("Index");
    }

Unit of Workって言うんですかね~。Exception起こせばロールバックするんだけど...。トランザクションってこのレイヤで意識するものなのかな~。う~ん。好みや規模に合わせて好きなやり方でどうぞ、ってことで。

2009年9月12日土曜日

runAllManagedModulesForAllRequests

VS2008での開発中のWebサーバーはWebDev.WebServerで、これってsystem.webServerセクションを処理しないで、IIS6と同じ動きになるじゃないっすか。

ドはまった。いきなり全然動かない状況が発生してテンヤワンヤ。そもそもIIS6にデプロイするものを作ってて、でもちょっとテストしたくなってIIS7にデプロイしたらイエロースクリーン。サッパリ原因が分からず泣きそうになった。出てきたエラーは↓これ。

セッション状態は、構成ファイルまたは Page ディレクティブで enableSessionState が true に設定されているときのみ使用できます。System.Web.SessionStateModule またはカスタムセッション状態モジュールがアプリケーション構成の <configuration>\<system.web>\<httpModules> セクションに含まれていることも確認してください。
説明: 現在の Web 要求を実行中に、ハンドルされていない例外が発生しました。エラーに関する詳細および例外の発生場所については、スタック トレースを参照してください。

例外の詳細: System.Web.HttpException: セッション状態は、構成ファイルまたは Page ディレクティブで enableSessionState が true に設定されているときのみ使用できます。System.Web.SessionStateModule またはカスタムセッション状態モジュールがアプリケーション構成の <configuration>\<system.web>\<httpModules> セクションに含まれていることも確認してください。

全然、解決出来そうなtipsが見つからなくて、将来の自分のためにここにコピペしとく。なんでセッションがいきなり動作しないんじゃ!と、お悩みなら、とにかくrunAllManagedModulesForAllRequests属性を確認。IIS7って統合パイプラインだったりするのが魅力なんだけど、こんなことに時間をつぶされるとは...。

    <system.webServer>
      <validation validateIntegratedModeConfiguration="false"/>
      <modules runAllManagedModulesForAllRequests="true">
        <remove name="ScriptModule" />
        <remove name="UrlRoutingModule" />

SessionStateModuleがマネージモジュールなおかげでこの属性を外すと、EnableSessionStateを設定しろと全然違うメッセージになって泣ける。

今後主流になるIIS7.5(Windows 7だけど)の場合は、英語っぽいメッセージになったけど、これまた検索で見つかるようにコピペしとこう。

The SessionStateTempDataProvider requires SessionState to be enabled.
説明: 現在の Web 要求を実行中に、ハンドルされていない例外が発生しました。エラーに関する詳細および例外の発生場所については、スタック トレースを参照してください。

例外の詳細: System.InvalidOperationException: The SessionStateTempDataProvider requires SessionState to be enabled.

そもそも、この属性をいついじってしまったのかさえ覚えてない...。普通にプロジェクト作成したときには指定されてるから自分で消したのは間違いないんだけど、IIS7にデプロイするまで問題が発覚しないから、要注意。

全然関係無いけどBubbleShareにアップしてるアルバムをダウンロードしようとしても0バイトのZIPファイルになってて全然ダウンロード出来ない。早くダウンロードしないとそろそろ閉鎖しちゃう。あわあわ。

2009年9月6日日曜日

HandleErrorの使い方リベンジ

前回の投稿の時には全然気にしてなかった問題を最近突っ込まれたので、改めて試して見ました。

そもそもASP.NET MVCで例外発生時のハンドリングをどうするのか、というのは前回の投稿に書いてるので読んでもらえれば分かると思います。と、言うと読んでもらえない気もするので簡単に説明すると、web.configでsystem.web/customErrorsをOnにしておき、ControllerやActionにHandleError属性を指定する。そうするとException毎に出力する内容を自分で制御出来るのさ!、というものです。

ここでポイントになるのはsystem.web/customErrorsでエラーハンドリングをする部分です。と、いうのも発生したエラーを捕捉し、表示したい内容を切り替える際に、この方法だとエラーページから302 Redirectで表示ページに遷移します。でも、人ではない何か(クローラやらAjaxなんかでのRESTクライアント)に正しくエラーを伝えるにはHTTP StatusCodeに正しい値をセットしたレスポンスを返さないとダメじゃないですか。ダメだとしましょう。

前回の実装では、302 Redirectの後に返されるレスポンスで正しいHTTP StatusCodeを返してる。けど、ちょっと待てと。そもそもRedirectいらないだろと。おっしゃる通りです。まったくその通りで、リクエストに対してRedirectなんかせず、スパッとステータスを返すのが正しい実装ですよね。

どうすればそういう実装が簡単にできましょうか、が今回の主題です。で、この主題を実装して正しく動くのはIIS7以降に限定なのでそうじゃない環境の人は「ふ~ん、そっすか」くらいにしか得るものがないかも。ガンバです!

まずsystem.web/customErrorsだけだとリダイレクトされてしまうので、これを何とかしたい。IHttpModuleとかで実装するのも手としてはありかもしれないですが、そんなことしなくてもweb.configにsystem.webServer/httpErrorsというのがありまして。このセクションにエラー発生時の挙動を書きます。それだけだとcutomErrorsと何が違うんだと思われても致し方なし。決定的に違うものがありまして、それがresponseMode="ExecuteURL"という属性。これを指定するとただのHTMLレスポンスでもなく、リダイレクトでもなく、指定したURLの実行結果を返してくれるんです。まばゆいくらい素敵です。

httpErrors Element [IIS 7 Settings Schema]

詳しくはリファレンスをチェケラッなんだけど、残念なことに日本語になって無くてですね...。ここは一つ「早く日本語にしてください!」とこのエントリを読んだ人はチャックに言いましょう!IISというかインフラは担当が違うと弱音を吐くようなら「担当に伝えてください!」とチャックに言いましょう!だって担当知らないし。

MSDNのドキュメントの苦情もすべてチャックに!そんなチャックはTHE TRUTH IS OUT THEREこちら(ゴメンねチャック)。

話を進める前に前回の状況のスクリーンショットを確認しておきます。

error1 error2

左が立ち上げた時の状態。右がBad Requestのリンクをクリックした状態。クリックすると拡大するのでステータスを確認してみてください。302 Redirectの後に400 Bad Requestになってますね。ちなみに開発環境のwebDev.webServerはwebServerセクションを見てくれないので、ローカルのIISにデプロイして確認する必要があります。今回はWindows 7上のIIS7.5にデプロイしてます。

まずはsystem.webServer.httpErrorsを設定します。

    <httpErrors errorMode="Custom">
      <clear />
      <error statusCode="400" path="/ErrorHandle/Errors/400" responseMode="ExecuteURL" />
      <error statusCode="403" path="/ErrorHandle/Errors/403" responseMode="ExecuteURL" />
      <error statusCode="404" path="/ErrorHandle/Errors/404" responseMode="ExecuteURL" />
    </httpErrors>

適当な感じで申し訳ないっす。 で、web.server/customErrorsを変更します。

    <customErrors mode="On" defaultRedirect="Errors" />

前回の設定内容をガッツリ消して、上記のみにしてしまいましょう。これすら消すとHandleError属性が効かなくなってエラーが捕捉出来なくなるので残しておきます。

続いて、HandleErrorAttributeクラスを派生させたHttpHandleErrorAttributeクラスを作成します。この派生クラスでOnExceptionをoverrideして処理してしまいましょう!

using System.IO;
using System.Web;
using System.Web.Mvc;

namespace ErrorHandling.Controllers
{
  public class HttpHandleErrorAttribute : HandleErrorAttribute
  {
    public override void OnException(ExceptionContext filterContext)
    {
      var exception = filterContext.Exception as HttpException;
      if (exception == null)
      {
        base.OnException(filterContext);
        return;
      }

      var statusCode = exception.GetHttpCode();
      var errorViewName = string.Format("~/Views/Errors/Error{0}.aspx", statusCode);
      if (!File.Exists( filterContext.HttpContext.Server.MapPath(errorViewName)))
        errorViewName = "~/Views/Shared/Error.aspx";

      filterContext.Result = new StatusViewResult(new ViewResult { ViewName = errorViewName }) { StatusCode = statusCode };
      filterContext.ExceptionHandled = true;
    }
  }
}

普通のExceptionは無視して、HttpExceptionだけ捕捉します。後は、ControllerにHttpHanderErrorを追加するだけ。

using System;
using System.Web;
using System.Web.Mvc;

namespace ErrorHandling.Controllers
{
  [HandleError(Order = 1)]
  [HttpHandleError]
  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      ViewData["Message"] = "Welcome to ASP.NET MVC!";

      return View();
    }
// 以下省略

太字のところです。 ちなみに前回のサンプルソースに付け足してます。

実行すると↓こうなります。

error3

ちゃんと1度目のレスポンスで400がが返ってますね!

error4 error5

上の2つは左がThrow Exceptionリンクをクリックしたもので、これはcustomErrorsが効いてる結果です。右がMethod not arrowedをクリックした結果で、HttpHandleErrorでViewの定義が無かった(405を用意してないです)時のものです。

エラー処理もこれでスッキリ!

2009年9月4日金曜日

あ、メタルキングじゃないことがばれた

先日のBoFの様子が早くもCodeZineで記事になってます!

「ASP.NET Web Form」か「ASP.NET MVC」か? .NETによるWebアプリ開発の今を徹底討論(1/4):CodeZine

最初のページのトップにある写真でプロジェクターの映像を見上げてるのが自分です。これだけ小さい写真だと顔も分からないから恥ずかしくないね!同じ写真内で真ん中あたりにノートパソコンのモニターがちらっと写り混んでるね~。ボスなんだよね~。このノートパソコンの画面気になるでしょ~?メモとか取ってるとか思ってるでしょ~?違うんですよ!パワポ(?)で「ここでボケて」とか「もっと詳しく」とか「なんでスーツ着てないの?」とかが用意されてて、しゃべってる途中でチョイチョイ画面をこちらに向けて楽しんでたんだよ!上手いこと写真には写ってないけど一番後ろの席で肉好きの熊さんも同じくカンペを用意してたんだよ!怖え~。BoF怖え~。罠ばっかりだ。

写真小さいとか思ったそこのあなた。2ページ目ではちょっとデカイ写真が。メタルキングじゃないことがばれた。東京生まれヒップホップ育ちのメタルキングで通してたのに普通の人なのがばれた。ちなみにカメラ目線ではありません。たまたまです。写真撮ってることなんて気がついてませんから。ド緊張してたんで。

記事ではスゴイまじめに技術の話をしてる風ですが、そこは記事を書いてくれたナオキさんの腕っす。自分へたれっす。

これからもご贔屓に~。

2009年8月28日金曜日

ボフったー

本日BoFに参加してくださったみなさま!ありがとうございました!

超、緊張しました。結構あれこれとしゃべること用意してたんですが、全然しゃべれませんでした。自己紹介を途中棄権ってなんだよ!みたいな。

内容的にはちゃんと「適材適所で使い分け」というのを入れたかったんですが、なにもかもが後の祭り。だって、管理画面なんか小野さんのサンプルみたいにゼロコーディングで出来たら最高ですよね。ViewState/PostBackなPage Controllerスタイルで、コントロールに処理を完全に委譲してしまうWebFormは最強です。それらの機能を少し横取りして、Pageからすべての機能を切り離し(この時点でもうアーキテクチャスタイルが違うので比較することじたいに無理があるんですけど)Front ControllerスタイルにしたASP.NET MVCによるスクラッチ開発は面白さ爆発です!

とにかくまず最初に驚いたのがASP.NET MVCで何か作ったことのある開発者の方が思ってた以上に多かったこと。ビックリしました。可能性たっぷりですね!フレームワークと名乗るにはまだまだ万人向けなものにはなってないMVCですが、何となくこの路線を突っ走って欲しいという個人的には思ってます。だってチャップリンも言ってるじゃないですか。

人生は恐れなければ、とても素晴らしいものなんだよ。
人生に必要なもの。それは勇気と想像力、そして少しのお金だ。

うん、全然関係無い言葉だね!

えと、ちなみにデスね、今日話したかったメモを以下に残しておきます。

  • 共通のnamespaceはImportでaspx/ascx毎にしていするんじゃなくて、web.configで指定しておこう。
  • HTML入力を受け付ける時にはValidateInput属性で指定しよう。Pageディレクティブやweb.configでのValidateRequestはController関係無いよ!
  • csprojのMvcBuildViewsをtrueにしてViewのコンパイルを強制するのが便利なときもある。
  • JavaScript無しで機能を実装したあとに、JavaScriptでのUX向上を目指すのが王道。でも、こだわらなくてもいいかもね。
  • URLはリソース指向に考えてルーティングに定義しよう。
  • RepositoryとServiceを作成しといて、アクションでは単なる呼び出しがいい。
  • DB Modelと View Modelを分ける(相互にマッピングする)。
  • テストしやすくするために、HttpContextへの依存をControllerからなくす(UserNameとIsXhr)。依存性が無いならテストは結構楽ちん!
  • 全体共通なBaseControllerを定義しておくと何かと楽ちん(abstractでね!)。
  • Viewでの作業を楽にするためにヘルパーを定義。
  • Controller名やAction名のマジックストリングをなくすために、T4MVCを活用。
  • 入力検証を楽にするためにDataAnnotationsModelBinderを活用。
  • もっと複雑になるならDIでRepository/Serviceの依存性注入。
  • マッピングも大変ならAutoMapperやら(自分で書いてもいいけど)。
  • テストが複雑になりそうなら(依存性を持ったもののテストをしたいとか)Mockを使おう。MoqやらRhino.Mocksやら。
  • カッコ良くRESTfulな実装にしたいならREST for ASP.NET MVC!

参考にするといいかもなURL

お勧め書籍

サンプルに実装してる項目もあれば、無い項目もあります(DIやMockや)が、こういうのを取り入れると楽しく開発出来るっていうのを話したかったデス。

サンプル一式は↓こちらからどうぞ。

動かすにはSQLServer2008Express以上が必要になります。Databasesフォルダ内の2つのMDFをそれぞれAspNetDBとMyTimeDataという名前でアタッチ(MS SQL Server 2008 Attach Database .MDF File)するとそのままで動くんじゃないかと思います。

※App_Dataに入れてAttachDbFilenameをConnectionStringに指定する方が簡単なんですが、それだとウェブのプロジェクトとテストのプロジェクトで同じインスタンス名でDB作成しようとして動かないんで気をつけてください。

機能的な物はBoFで紹介したとおり、Membershipによるユーザー管理とログイン、作業時間の追加・更新・削除と合計表示のみです。これにチャートコントロールでの表示やら、月内デイリー合計なんかの機能をつければ、それなりに使える物になるんじゃないかと思いますがどうでしょうかね。プロジェクトの名前を「ボフったー」にしなかったのが「エドったー」に完全に負けてるところデス!

細かいサンプルの説明は...、必要ならしますが、どうなんでしょう。週末だし書くかもしれないですが、ホッケーもしたいので微妙です。たけはらが書かなくてもチャックが説明してくれると思います!無茶ぶり返し!

最後に。

こういう機会を与えてくれた、小野さんとナオキさんには感謝しきれません。ボスにはいろんな人を紹介してもらってマジ感謝です。一人だと誰とも知り合えずに過ごす事になったこと間違い無しです。MSの方達も、こんなどこの馬の骨なのか分からないチンピラ開発者の話に耳を傾けてくれてホント嬉しかったです。

BoFでたけはらの話に耳を傾けてくれた人たちの暖かさには、感謝しても仕切れません。マジ泣ける。本当にありがとうございました。

コンカレント

Tech ED2日目。今日はボスと別行動もあって、ちょっぴり大人の仲間入り?

まぁ、最初のセッションは同じのだったけど、別々にセッションルームに入ったので別行動ってことでいいですかね?今日も熊澤さんの話を聞きに行ったんですが、ちょいちょい熊澤さんが明後日の方向を向いて話をするわけですよ。ナゼ正面向いて話をしないんだろうかと思って目線の先を見るとボスが立って見てた...。そこ、マンツーなんですか。何でですか。見に来てる人100人くらいいる感じなんですが、マンツーで説明なんですか。そーですか。NUMAの話で盛り上がり、アンドキュメンテッドな動的管理ビューを紹介してくれたりと、初日に続いて濃厚な内容でした。

とりあえずBoFって何?みたいな状態を打破するために他の方のBoFを見てみることにしました。ボスとべーさんの言うとおりみんなスーツでした。な、わけねーよ。誰もスーツじゃないじゃないか!でも、不安だったからMSの人に「スーツじゃないとダメなんですか?」って聞いたら「そんなことないです」って教えてくれました。隣で渋木さんが「おまえ、ばらしてんじゃねーよ」という目でMSの人のこと見てた...。その目はまるで熊。ハンマー持った熊。

なんやかんやで、セッション全終了の後、休憩ルームでおしゃべりしてたら、ナオキさんが「ガンダムの人がいます!」と呼んできてくれて、少しお話しすることが出来ました。もうね、とにかくね、ガンダムは分からないから違う例えにしてください、と。攻殻機動隊か電脳コイルなら分かるので、そっちでお願いしますと。Azureの話をもっとしたかったけど途中で渋木さんと赤間さんも参加して、猫バス事件の話になって...。面白かったからいっか。でも、あれだ、みんな日常会話で使う単語が変。誰もなんの突っ込みもなく普通にそういう単語なんだもんよ。技術の話をしてるわけじゃなく、風呂とトイレがコンカレントだとか、会話がかみ合うのをプロトコルが一致するとか、食事に合流するのをジョインするとか、日常会話でそれはおかしいと思うんだ。オレおかしいと思うんだ。オレがおかしいのか??

夕食の時に、今度は荒井さんが同席。The Root of .NET Frameworkでハートを鷲づかみにされた荒井さん。なんかね、今までどんな人が本を書いてるとか、ブログを書いてるとか全然意識したこと無かったんですよ。自分とは関係無い世界の人たちっていうか。実際会って話をするなんて考えたこと無かったし。でも、なんかみんな素敵な人ばっかりでさ~。食事の後、途中まで電車が一緒でマンツーマンで荒井さんとおしゃべり出来て得した気分。名もない開発者な自分の話をずっと聞いてくれて、答えを返してくれて。あ、そうそう、自分もだけど、荒井さんも気がついてないだろう接点を発見した。荒井省三のBlog : The Root of .NET Framework の書評で荒井さんがたけはらタンブラのThe root ofの感想へリンクしてくれてた。う~ん、今日一緒に話をするまえに知ってたら、もっと楽しかったかも!でも、明日もあるし、会ったらその話をふってみようかな。

んでもって明日は、とうとうBoFに登壇(最後の枠のBoF-14 ASP.NET Web Form vs. ASP.NET MVC in Japanデス)。小野さんは「なんとかなるよ」と言ってくれてるけど、全く打ち合わせもなく、かなりドキドキなんですけど...。チャックが初日のセッションで無茶ぶりするもんだから胃が痛くてしょうがねーす。たぶん小野さんとナオキさんの陰に隠れて小さくなってると思います。

2009年8月27日木曜日

ボスと一緒に

2009だけど初めての参加。なんか15周年ってキーノートで言ってたから、ずいぶんと周回遅れだね!9/5からクイーンズスクエア内にクリスピークリームの店がオープンするっていうデカイポスターもあって、そっちのほうが気になるとは言いづらい。

IMG_2524

まぁ、ドーナツパーリーはまだ出来ないのでからいいとして、今回のTech EDは小野さんナオキさんに誘われ、BoFで前にでるっていう状況での参加。しかもそんな感じの参加なもんで参加証も持ってない。入り口でいきなり「帰れ!」とか言われたら泣きながら帰ることになるだろーな、と不安になりながら会場に向かってたら、パシフィコで海のエジプト展やってるって書いてあったからいざとなったらエジプト展でも見て帰ろう。

IMG_2526

前日にボスに不安な胸の内を打ち明けてみると「行けば何とかなる」と猪木ばりに男前なことを言われてしまい涙目。超不安なまま受付に行ったら、こっちじゃなくてあっちいけ、いやこっちじゃなくて君はあっち、とたらい回しのあげく、奇跡的にも小野さんと同時に受け付けするというナイスな展開。そのままくっついてキーノート聞き終わると小野さんは「じゃ、MVPのランチがあるから」と去っていき、会場出口でナオキさんにメールしたら、もちろん「MVPのランチです」と返事が返ってきて、完全に親とはぐれたアザラシの子供状態。おかしい。横浜在住なのに完全なるアウェー感。横浜大会で散々行きまくったはずのパシフィコで超アウェー。こんな時にはボスに頼ろう。急いでボスに電話をしたら何とか落ち合うことが出来ました。Tech EDこえ~。

なんと言っても、勝手知らないTech ED。で、ここからどうすればいいんでしょう。ご飯食べたはいいけど...。とりあえず「SQL Server チューニング」のセッションは絶対聞いた方がいい、熊澤さんのセッションはパネー、と言われたのでそれを聞くことにしました。ボスと一緒に。これがまた恐ろしく濃い内容で、話す内容とスライドとデモとがマジパネースピードで「今回使ったクエリは後でどこかからダウンロード出来るようなる思います」と熊澤さんが言うんだけど、それがどこになるのかが分からないという罠。ちなみにスピーカーテーブルの真ん前の特等席でボスと2人で「はえ~」やら「すげ~」やらはしゃぎながらもとてもすばらしい内容で感動しました。Tech EDぱねー。

その後、続けてASP.NET MVCのセッションが同じ場所で始まり、もちろんそこにも参加。

IMG_2527

チャック

で、デモが今回作ったサンプルとほとんどかぶってる感じで切なくなりました。エドったーめ!そういえば今までTwitterを登録はしてても使ってなかったのをボスに突っ込まれ、チャックのセッションでも話しが出てきたってので、勢いでボスとチャックをフォローしてみた。

その後はライトニングトークっていうのをボスと一緒に聞きに行って、更にボスの宿泊先で渋木さんとおしゃべり。挙げ句の果てにボスと渋木さんと3人で食事に行って、オモロ話で盛り上がる。渋木さんは肉が似合いすぎる。

結局、一人では何も出来ず、いつも以上にボスにべったりなTech ED初日でした。Tech EDおもしれ~。

2009年8月22日土曜日

BoF

ナオキにASP.NET(仮) : Tech Ed Japan 2009 最終日の BoF 「ASP.NET Web Form vs. ASP.NET MVC in Japan」に登壇します

と、言うわけで当日はお手柔らかに(特にボス)。

こんなサンプル見てみたいという要望があれば持参もあるかもよ~。ないかもよ~。

ナカジその3、3on3

ホッケーを愛する日本全国の人々へ:COVERAGE(カバリッジ)~ステップ3「3 on 3」 - livedoor Blog(ブログ)

1on1,2on2とくれば次は3on3。

というわけで、ナカジのすばらしい守り方の第3弾。今回も延長線上での考え方なので、2on2までが理解出来てれば難しい話ではないけど「ゴール前にいろ」が通用しないというのが大事なポイント。相手との距離を測らずに(スティック1本)立ってても役にたたない。

DFの1~3はそれぞれのポジション毎に役割が変わるので、自分の役割は1つだけだと決めつけるようなプレー(この人をマーク!とかココに立ってればいい!っていうのはチームプレーにならない)をすると全然機能しなくなるので要注意。

2009年8月18日火曜日

はしご

先週末の土曜(8/15)、サンピア日立に行ってきた。ホッケーしに。前回行ったのが2年前のロー連の大会で、今回で2度目。しかもお盆休みまっただ中で、行きはまだしも帰りは超渋滞が予想される日帰り強行日程。合宿のお手伝いってことでホントは土日2日間なんだけど、オグさんがどーしてもっていうから日帰りで。

いつもは単独行動で一人ドライブなんだけど、今回は日帰りだからオグさんと大島さんとイマイちゃんと4人でゴー!みんなでおしゃべりしながらのドライブを楽しみつつ、途中のサービスエリアにあるスタバで朝食を取ったのが運の尽き。オグさんと二人で食事直後にゲリラ豪雨。しかもトイレに立ち寄ったサービスエリアではビッグルーム激混み。もう無理。もう限界と諦めかけたところで、ギリギリ入れた。マジ漏らすかと思ったよ。先行き不安。

サヨちゃん整腸剤ありがとう。

IMG_2516 IMG_2515

10:00練習開始で16:00まで練習して、帰りの渋滞で便意をもよおしたらパネーことになるんで、再度薬を飲んでおく。泊まり組はこの後ボインボインクイーンBBQだけど日帰り組はすぐに帰らないと今日中にたどり着かないんで、そそくさと撤収。途中、魚のおいしいお店があるってことで、食事をしてたら、BBQ担当ミチアキさんの買い出しにばったり遭遇。「今帰ったって渋滞なんだからBBQしようよ」と明らかに一人残されるのが寂しいという目で誘いかける。「いや、帰る」と無下に断り、子犬のような目をしてるミチアキさんを残し出発。

渋滞40kmとか表示出てたけど、あれよあれよと3時間で到着。思いのほか早く着いたんで荷物を詰め替えて22:00から新横でアイス。ホッケーのはしごしてやった。てへ。

更に、翌日「戸塚集合」と呼び出されて、ハタ君とミチと3人で(少なすぎるだろ)ひたすら1対1。超疲れたけど、やりきった感のある週末でした。

2009年8月9日日曜日

なかじスゲ~!

ホッケーを愛する日本全国の人々へ:COVERAGE(カバリッジ)~ステップ1「1 on 1」 - livedoor Blog(ブログ)

まずは1オン1の守りの説明。この時点でかなりグッとくる。

ホッケーを愛する日本全国の人々へ:COVERAGE(カバリッジ)~ステップ2「2 on 2」 - livedoor Blog(ブログ)

続いて2オン2の説明。感動的。

マンツーマークは10年前の発想だと言い切るところがすばらしい。利にかなってないもん。そこで「マンツーなんだから、おまえのせいだ!」なんて話はうんざりっすよ。

ASP.NET MVC 2 Preview1でのArea

ふぅ~。出遅れ。かなり出遅れでしょんぼり。

ASP.NET MVC V2 Preview 1 Released - ScottGu's Blog
日本語はもちろんこちら→ASP.NET MVC V2プレビュー1がリリース - @IT

少しずつ順番に見ていこうと思います。順番なのでまずはArea機能。もともとPhilさんがこの機能をV1の時から紹介(Grouping Controllers with ASP.NET MVC)してて、V1のPreviewのいつだか忘れたんですがRouting登録時にControllerのnamespaceを指定出来るようになってるんです。それを使って違うnamespaceのControllerを指定することを"Area"と呼んでるという感じです。RouteControllerへのArea登録用拡張メソッドの作成でV1は実現して他のをV2では最初から用意しといたら便利かもね~、な流れ(特に開発規模が大きくなって分業始める事を考えると)でしょうか。

とりあえず、使ってみましょう。と、いっても上記記事を見てもやり方は書いてないのでサッパリ分からないですよね。でも今回からはMSDNにすでにドキュメントが用意されてます。

Walkthrough: Organizing an ASP.NET MVC Application by Logical Areas

MSDNマガジンを機械翻訳にするなら、その分のリソースをこっちにさいて日本語化しといてくれよ、なんてぼやきは無しでいきましょう。ぶつぶつ。

まず、通常通りにMVC 2のプロジェクトを作成。”MainSite”という名前で作ってみました。

area1

そうそう、V1とV2はそれぞれ参照するアセンブリが違うので、一緒にインストールしててもなんの問題もないので、ガンガン入れて試すといいと思います。

area2

プロジェクトテンプレートももちろん最初から日本語版のVSで動きますね。こなれてきてます。

ドキュメントに書かれてる手順でサブプロジェクトを作っていきましょう。ソリューションに新しく「ソリューションフォルダ」を追加して、その中に"Sub1"と"Sub2"という名前でMVC2のプロジェクトを作成。でもって、不要なファイル達をゴッソリ削除。とにかくControllersフォルダとルートのweb.configだけが残ってればよろしデス。

area3

↑こんなようになってればOK。

で、ルーティング登録用のファイルを各サブプロジェクトに作っておけと、ドキュメントに書かれてるのでそれに従います。ん?それだと面白味に欠ける?分かりました。んじゃ、MainSiteのルーティングにまとめて書くやり方でやってみましょう!(本来はきちんと分けた方が機能分離出来て分担しやすいはずです)

MainSiteのGlobal.asaxのルート登録を以下のように書き換えます。

    public static void RegisterRoutes(RouteCollection routes)
    {
      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

      routes.MapAreaRoute(
        "AreasSub1",
        "Sub1Default",
        "s1/{controller}/{action}/{id}",
        new {controller = "Home", action = "Index", id = ""},
        new[] {"Sub1.Controllers"}
      );

      routes.MapAreaRoute(
        "AreasSub2",
        "Sub2Default",
        "s2/{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = "" },
        new[] { "Sub2.Controllers" }
      );

      routes.MapAreaRoute(
        "MainSite",
        "Default",
        "{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = "" },
        new[] { "MainSite.Controllers" }
      );

      //routes.MapRoute(
      //    "Default",                                              // Route name
      //    "{controller}/{action}/{id}",                           // URL with parameters
      //    new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
      //);

    }

デフォルトで登録されてるMapRouteがコメントアウトになって、MapAreaRouteで登録し直してるのには理由がありまして、Areas内のHome/Indexを表示したりしてるときに、MainSiteへのリンクをActionLinkで出力する際にエリア名を指定しておく必要があるんですね。その時、デフォルトの登録だとエリア名がないので、ちゃんとリンクを出力できなくなります。気をつけて!

MainSiteのHome/IndexにSub1とSub2へのリンクを書きます。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="indexTitle" ContentPlaceHolderID="TitleContent" runat="server">
    Home Page
</asp:Content>

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">

  <% = Html.ActionLink("MainSite Home","Index","Home") %><br />
  <% = Html.ActionLink("Sub1 Home", "Index", "Home", new { area = "AreasSub1" }, null)%><br />
  <% = Html.ActionLink("Sub2 Home", "Index", "Home", new { area = "AreasSub2" }, null)%><br />

</asp:Content>

これで一度動かしてみます。

area4

ぬほ!ちゃんと動く。リンクをクリックしてもエラーでない。でも、まだ、Sub1/Sub2用のIndex Viewを作成してないのですべてのリンクで全く同じViewが出てきます。とりあえず、この状態でレンダリングされるHTMLは↓こんな感じで予想通り。

  <a href="/">MainSite Home</a><br />
  <a href="/s1">Sub1 Home</a><br />
  <a href="/s2">Sub2 Home</a><br />

そりゃそうっすね。特になんの説明もなくここまで進みましたが、少し捕捉。MapAreaRouteの台1引数のエリア名とActionLinkなんかで指定するRouteValueDictionaryのarea指定が一致しないとちゃんと解決出来ないので気をつけましょう。そこだけです。上記の例で言えばAreasSub1/AreasSub2がそれぞれエリア名に当たるんで、同じものをActionLinkのareaに指定しなきゃダメです。あと、まだPreviewだからだと思いますが、今のところHtmlAttributeの指定がなくてもActionLinkの引数にはnull指定で全部していするようにしないと、これまたリンクが出力されません。overload実装が部分的。

続いて、Sub1/Sub2用のViewを定義します。これがデスね、勘でやってると気がつかない(Sub1/Sub2のプロジェクトにおきたくなるよね?)んですが、MainSiteに置いておく必要があります。いや、必須じゃないけどWebFormViewEngineを書き換えれば他のプロジェクトに置いておいてもいいけど、デフォルトはメインのプロジェクトのViewsフォルダを探すようになってるので、その辺気をつけましょう。

なので、MainSite/Views/Areas/AreasSub1/Home、MainSite/Views/Areas/AreasSub2/HomeにそれぞれのIndex.aspxをおきます。ここでもAreasSub1/AreasSub2とルーティングで指定したエリア名を使う事になってます。

area5

↑こんな感じで。それぞれのIndexが区別出来るように、AreasSub1の内容は

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>AreasSub1のHome/Index</h2>

</asp:Content>

AreasSub2の内容は

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>AreasSub2のHome/Index</h2>

</asp:Content>

と、しておきます。ドキュメントには以下のようなサンプルが載ってるので、これで試して見るとルーティングのデータが見れるので分かりやすいかも。

<div id="main">
    <asp:ContentPlaceHolder ID="MainContent" runat="server" />
        <p>
            Controller: <%= ViewContext.RouteData.Values["controller"] %><br />
            Action: <%= ViewContext.RouteData.Values["action"] %><br />
            Area: <%= ViewContext.RouteData.DataTokens["area"] %>
        </p>
    <div id="footer">
    </div>
</div>

areaの情報(どこのnamespaceを参照するのか)が、DataTokensに入ってControllerの解決時に参照されるのが分かりますね。ソースをテケテケ追いかけてみるとですね、MapAreaRouteがMapRouteを呼び出した後に、Defaults/Constraints/DataTokensそれぞれにareaを設定してる。内部ではこれまで通り"Namespaces"で参照するようになってるので、DefaultControllerFactoryでControllerTypeを取得する部分のコードには変化無しです。

area7 area6

実際には、これだけだと、Home/Aboutそれぞれの参照先がs1/s2のArea内に入ってしまって、MainSiteを参照しなくなってしまうので、そこら辺のActionLinkにもすべてareaを指定するように変更する必要があります(デフォルトのMapRouteを消した理由はココに絡んできます)。

ちなみに、Philさんのエントリで「Single Project Areas With ASP.NET MVC 2 Preview 1」というのがありまして、同じプロジェクト内でのAreaの作り方を説明してくれてます。こっちのほうがViewsも分離(WebFormViewEngineのパスを変更)出来るし、最初からこの方がいいんじゃないかと思わずにはいられないところです。

とにかくMVCはV2になっても目が離せないですね。

最後に気になる問題がASP.NET Forumsにあがってたので紹介。

MVC2 InvalidOperationException - ASP.NET Forums

ViewへViewDataを型付きで指定して渡すとき、インターフェースを指定して渡す事も出来るんですが、その時に渡されたインスタンスを使ってEditorForで出力しようとすると型が分からないといってInvalidOperationExceptionが発生するという話。解決策としてはダサイけどEditorForに"Object"と固定でテンプレート名を渡すことで、デフォルトテンプレートを参照してくれるようになるんだそうです。リリースノートのKnown Issuesにちゃんと書かれてるけど、解決方法は書かれてないよね。

追記です。今回みたいに複数のMVCプロジェクトをソリューションに追加して実行した場合、アプリケーションのインスタンスがプロジェクト毎に起動して(今回なら3つ)無駄使い感が漂います。なので、以下のようにデバッグ時に起動しないようするとエコなんじゃないかなと思うところです。

area8

2009年8月4日火曜日

LIFE

歌ネットはいつからログインしなくても良くなったんだろう。

キマグレン LIFE [無料]歌詞検索の歌ネット

今朝、この歌の歌詞を口ずさまずにはいられなかったのは、昨日のめざましライブがキマグレンだったからなわけじゃない。

IMG_2501

泣きたくて、笑いたくて...。

いや、まじ、もうメガネかけたまま寝るのはやめようよ。学習しようよ。

話変わって、土曜日、恒例の港南区子供会主催(港南区関係無いのに呼ばれるのはナゼなんだ...)のアイススケートイベントのお手伝い。朝8時から3コマ使って15:30まで氷に乗りっぱなしでした。子供達相手に一緒にスケート滑ったり教えたりするんだけど、ずっと中腰で疲れたっす。でも、ちょこっとホッケーして見せたりして楽しかったよ!

IMG_2495

朝一はすっからかん。

IMG_2497

ちょこっとホッケー体験コーナーもあり。

IMG_2498

全3枠で756人だって。多いよ...。次回は来年2月6日だそうで。もうリンクの予約は入れてるからスケジュール空けといてだってさ。がんばる...。

2009年8月2日日曜日

MVC V2のチラ見

やっと、ため込んだFeedの処理に追いついて、ビデオの確認ができた。

Hanselminutes on 9 - ASP.NET MVC 2 Preview 1 with Phil Haack and Virtual Scott | Scott Hanselman | Channel 9

内容も15分と短いので、是非。

ってことで、このビデオ見つつ少しだけv2のソースも少しだけ確認。Editor/EditorFor(EditorTemplateフォルダ)、Display/DisplayFor(DisplayTemplateフォルダ)がそれぞれあって、なおかつLabelForがFuturesから標準に入ってきてる。ここでTextBoxForを作ろうとして、これは拡張性に問題がありそうだと気がついて、Dynamic Dataで使われてるTemplateベースのレンダリングを参考に拡張方法を変えたのかな。前のFuturesには入ってたモンね、 TextBoxFor。

なので、TemplateHelperがかなり重要なポジションをしめる気がする。さらにModelBinderの時と同じようにDefaultDisplayTemplate/DefaultEditorTemplateを用意して、ある程度は自動テンプレート出力をしてくれる模様。すばらしい。

v2 のリリースノートをチラ見した時には「えぇ~、そっちかよ~!」と思ったけど、中身を見ると素敵かも。ITemplateでのテンプレート処理じゃなくてあくまで、HTMLをベースにascxでのテンプレート処理にこだわることで、ViewStateを取り込まないし、クリーンHTMLをはき出せるしで、 MVCらしさをそのまま引き継いだ上での拡張っす!System.Web.UI.WebControls.DataBoundControlModeがInitializeDataCellDataControlRowStateを使わなくて良くしてくれてるんでしょう。

Scott Hanselman's Computer Zen - Hanselminutes on 9 - ASP.NET MVC 2 Preview 1 Released

↑ここにも書かれてる通り、Preview1ではそれほど大量の機能追加はなくて、Preview2が本気リリースっぽい。なので、V2に関しても順を追って追いかけて行くことで、一度に沢山の機能を調べなくて済む感じです。

DataAnnotationsの取り込みがどの程度のものなのか。その片鱗をこのビデオで垣間見れた気がします。Dynamic Dataと同じくらい取り込もうとしてますね。DataType、DisypayNameにUIHint、それらもしっかりとUIの出力(TemplateHelperとDefaultEditorTemplate)に反映されてる。前まではDataAnnotaionModelBinderを使って、システムに対して入力方向での連携で入力値検証でしか利用してなかったのが、V2からは出力方向にもしっかり活用。なので、Templateベースの出力になってるのもうなずける。

大枠はいつものごとく、ガスリーさんところで書かれてるから、そこで確認。

ASP.NET MVC V2 Preview 1 Released - ScottGu's Blog

まだちゃんと見てないけどね!

すでにフィルさんところでAreaに関する投稿(Single Project Areas With ASP.NET MVC 2 Preview 1)もあるし、Maartenさんとこ(ASP.NET MVC 2 Preview 1 released!)にも簡単な説明が出てるので、これから少しずつ見ていこうと思います。

つか、この週末でちゃんと調べる予定だったのに、全然観れなくてちょっと残念...。

T4MVC再び

世界中でMVC v2祭りな雰囲気の中、少し前にリリースのあったT4MVC 2.4.0.1について。早くMVC祭りに参加したいところだけど、そこは焦らずに少しずつ...。

Angle Bracket Percent : T4MVC 2.4 updates: settings file, sub view folders, ActionName support and more

前回のT4MVCの投稿でActionNameをサポートするようttファイルの書き換えを行ったけど、新しいバージョンではしっかりとその辺も含めた改良が施されてます。なので、もうActionNameで名前を変えたものを指定したいからと、たけはらエディションを使う必要は全く無くなりました。

どうやってActionName属性を取り出すのかなと確認したところ、メソッドのCodeAttribute2を取り出してActionNameAttributeが付いてるかを見てました。ActionMethodInfoが変更かかってるのも合わせて、やり方はだいたい同じですね。間違ってなくてホッとした。変更前のAction名が選択肢に出てくるあたりの仕様も同じだし。

HomeControllerに以下のIndexPostを追加してみる。

    public virtual ActionResult Index()
    {
      ViewData["Message"] = "Welcome to ASP.NET MVC!";

      return View();
    }

    [AcceptVerbs(HttpVerbs.Post)]
    [ActionName("Index")]
    public virtual ActionResult IndexPost(string value)
    {
      ViewData["Message"] = "Welcome to ASP.NET MVC!";

      return View();
    }

このIndexPostを参照するようにIndex.aspxにFORMを書いてみる。

    <% using (Html.BeginForm(MVC.Home.Actions.IndexPost, MVC.Home.Name))
       { %>
       
       <input type="submit" value="送信" />
       
    <% } %>

結果、出力されるHTMLは↓。

    <form action="/" method="post">
       
       <input type="submit" value="送信" />
       
    </form>

それから、既定ではMVCがクラス名だったり、ViewsフォルダやControllersが固定だったけど、今回からT4MVC.settings.t4というファイルで変更出来るようになってます。なので、MVC.Home.~じゃ無くてMy.Home.~とかに変更も可能。サブディレクトリを再帰(なんちゃらRecursive関数達)で潜っていくようになってるので、より実用的に。

Actionの引数をなくしたoverloadを定義してるみたで、上記BeginFormの書き方を以下のように変えてもOKらしい。

    <% using (Html.BeginForm(MVC.Home.IndexPost()))
       { %>
       
       <input type="submit" value="送信" />
       
    <% } %>

だけど、これで出力されるURLがなんかキモイ。

    <form action="/?RouteValues=System.Web.Routing.RouteValueDictionary" method="post">
       
       <input type="submit" value="送信" />
       
    </form>

変なの。と、思って出力されたコードを確認してみたけど、今回のような使い方の場合、上記のようにBegineFormを使うと、標準のHtmlHelper.BeginFormが呼ばれるから変なURLになる。正しくは↓こう。

    <% using (Html.BeginForm(MVC.Home.IndexPost(), FormMethod.Post))
       { %>
       
       <input type="submit" value="送信" />
       
    <% } %>

ちゃんと、FormMethodを指定しましょう。じゃないと、T4MVCで定義してるBeginFormのoverloadに入ってこないもん。こうすれば、ちゃんとURLが出てくるようになりました。にゃろめ。ちなみにルーティング登録時(MapRoute)でもマジックストリングを使わずに登録できるようになってるT4MVC。

最高です。

2009年7月26日日曜日

楽しみ

昨日、今期MHLのプレーオフ初戦でした。見事初戦敗退...。なんか、切ないけど、まぁ、しょうがないか。練習も出来てないし。個人的にそうは思ってても、チームメイトはどう思ってるのかな~。

そんなこんなで、アッサリと夏も終わり(球児じゃないけど)、後はのんびりとホッケーして次期シーズンに備えるだけなわけですが、なにやら次期はエントリーチーム数が多すぎるみたいで抽選らしい。抽選に落ちたら全く試合に出れなくなっちゃうね~。どうなることやら。今更他のチームで出たいとも思わないしな~。抽選落ちしないことを祈るのみ。な~む~。

試合が午前中で終了したから、午後戸塚に一汗かきに移動。スズキ君も来て久しぶりにシャキッとしたホッケーもできて面白かったよ。でも、あれだよね、マッチョな感じが楽しい時間をダメにするっていうのもあるよね。ピックアップっていうと、その時にいる人それぞれ全然違うポイントを楽しいと感じるわけで。上達することだけを目的にするのもいいし、単純にそこにいるメンツでホッケーするのが楽しいからっていう理由だけでプレーする自分みたいな人もいるし。サヨちゃんとの対戦だけを楽しんだりするのも楽しみ方だと思うわけですよ。マッチョもいいと思うけど、それをみんなの共通の目標とか楽しみだっていう価値観はとても狭い世界でしか物を見れなくなりかねないぜ?

なんてね。

2009年7月18日土曜日

あちゃ~!おちゃ~!

いや~、2週間沈黙だったっす。なんか先週もちゃんとエントリ書いたんだけど、書いた後に恐ろしい勘違いエントリだと気がついて即削除。読まれたらちょっと恥ずかしい内容だったんで、修正とかじゃなく削除しちゃいました。ルーティングとURL引数(QueryString)を超勘違いしてた。とりあえずURL引数に禁止文字を使いたい場合は[FIX] .NET Framework 1.1 で "HTTP 400 - 正しくない要求" エラー メッセージが表示されるで無理矢理できるってことで(Routingには関係無い話でした)。

こないだ大事件が起きて。やっぱりキーボード(パソコンの)はこだわりの逸品を使いたいじゃないですか。普段からノートPC使ってるとデスクトップのキーの深さに疲れちゃうから、お気に入りは薄いキーボードなんですよね。押すのも楽だし。ぬたーん、とした感じがいいじゃないですか。

なもんで、使ってるのは電源メーカーで有名なENERMAXのアルミ削りだしキーボード(KB007U-B/KB007U-S)なのね。ひんやり感がたまらない。ThinkPadのパームレストが熱いのとは雲泥の差。

kb007ub-b06

「今週会社がつぶれます」のエントリは書いてないけど、そんな感じで新しい会社に移ったて、そこでももちろんこのお気に入りキーボードを使うデスよ。マウスもお気に入り使うデスよ。べーさんなんかごつい体型なのにキーボードは弁当箱か!ってくらい小さいデスよ。

いつものように解決方法が思いつかない~と、もんどり打ってたらコップを倒した...。これがまた奇跡のような倒れしてキーボードの上にこてーんと。なぜ、キーボードに向かって倒れたんだ...。もちろんコップの中身が全部キーボードに注ぎ込まれるじゃないですか。泣ける。もう帰ろうかと思ったけど、まだ14時...、みたいな。こぼしたのはお茶だったから慌てて拭いてキーボードを外に干した。とりあえず、乾いたところでつなげてみたらなんとなく普通に使えてホッとしたけど。あれだよね、コップは気をつけないとね!そういえば、前にもコーヒーをこぼした気がする...。コーヒーとお茶にまみれてもがんばって動くキーボードにはこれからも耐え続けてもらいたい。

ちなみにドラクエも全然進まない。この連休は旅にでるぞ~!

2009年7月4日土曜日

男らしさってなんですか?

前回のエントリーに引き続きT4MVCです。

前回判明したのが、ActionName属性を指定したアクションはオリジナルのアクション名でしかMVCクラスに展開されないというもの。で、改善リリースを待つか、自分でttファイルをいじるのか、どっちが男らしいかというところで、とりあえず逃げの一手を打ったんでしたね。

このままじゃべーさんにどやされる。週明けに「この腰抜けが!」なんて言われた日には、枕を涙でぬらす日々。涙じゃない、悲しみのシミだよ、なんて言ったところで「誠意ってなにかね? by 文太」とたたみかけられる。

まぁ、いいや。

早速、面白そうなものを見つけたんだから、T4追いかけてみるかと、VSでソースを開くと真っ白背景に白の文字。あ、コードハイライト効かないんだった。

そんなときには"Clarius Visual T4"。Professionalエディションを$99.99払ってまでは使い込まないだろうから、Communityエディションで。無料だし。Code Generatorエディション出たら買ってもいいかもね。いや、買わないか。で、インストールして早速T4MVC.ttファイルを開いてみると...。

t4mvc1

ぽかーんデス。なんじゃこれ。テーマ設定してるとグチャグチャっすね。

Clarius Forums • View topic - t4Editor and custom fonts and colors

フォーラム見てみたら、白バックのデフォルトテーマに戻せと書いてるけど、日本語版のVSだからか、「ツール>オプション>環境>フォントおよび色>テキストエディタ・表示項目」にそんな選択肢がそもそも出てこなかったり。ガッカリだな。無料だし、それもやむなし。そういえば、CommunityエディションだとIntelliSenceも使えないんだった。インストールする意味がまるでなかった。

で、結局↓こんな素っ気ない画面で。

t4mvc2

そろそろ、中身を確認せねば。T4の使い方は前にどこかで読んだことがあったから、細かい仕様的なのは無視。とにかく、<# #>で囲まれてる部分がテンプレート処理部。で、テンプレート内で参照出来る変数を下の方で宣言してて、その辺にいろいろコードがあるはずなので、そこら辺中心にチェックで。

一応ASP.NET MVCに関する投稿デス。

たぶんアセンブリ内をリフレクションでグルグル処理してるんだろーなと、たかをくくってたらこれが全然違うのね。ビックリした。CodeFunction2って誰ちゃん?CodeElementインターフェースってどこからはえてきたの?みたいな。どうやらVisual Studioオートメーションってことらしい。クラスの定義をpartialにしたり、関数定義をvirtualにしたり、ソースをいじるのにそうしとかないと面倒なことになるからなんだろうね。そんな物の存在を全然知らなくてたまげたけど、使う部分はちょびっとだけなんだから気にせず読み進める。

Controllers変数の中にControllerInfoのコレクションが入ってて、ControllerInfo.ActionMethodsの中にActionMethodInfoのコレクションが入ってるんだって。それらコレクション達を見ながらコード部を生成させる仕組み。今回はActionNameAttributeが宣言されてれば、アクション名をそっちに切り替えるってことをしたいので、ActionMethodInfoあたりを中心にチェック。ふと思ったけど、オリジナルのアクション名が消えるとちょっと分かりにくいかも?オリジナル+ActionName指定の両方を展開しといたほうが、優しさチラリな気がしたので、そういう事にします。

CodeFunctionのNameを書き換えるのは無理なんだろうから、ActionNameが指定されてるならそっちの名前を返すように内部のクラスを書き換える。

// Data structure to collect data about a method
class FunctionInfo: BaseFunctionInfo {
public string _actionName = null; public FunctionInfo(CodeFunction2 method) : base(method) { } public FunctionInfo(CodeFunction2 method, string actionName) : base(method) { _actionName = actionName; } public string Name { get { return _actionName ?? _method.Name; } } public string ReturnType { get { return _method.Type.CodeType.Name; } } public bool IsPublic { get { return _method.Access == vsCMAccess.vsCMAccessPublic; } } }

太字の部分が追加したコードです。さらに、このクラスを派生させてる部分も同じように変える。

// Data structure to collect data about an action method
class ActionMethodInfo: FunctionInfo {
    private bool _isOverride = true;
	
    public ActionMethodInfo(CodeFunction2 method, ControllerInfo controller): base(method) {
        Controller = controller;
    }

    public ActionMethodInfo(CodeFunction2 method, ControllerInfo controller, string actionName): base(method,actionName) {
        Controller = controller;
        _isOverride = false;
    }
    
    public bool IsOverride {get {return _isOverride;} }
    public ControllerInfo Controller { get; private set; }

    public string GeneratedName {
        get {
            // If the action name would cause a class/method conflict, append an underscore to it
            if (Controller.Name == Name)
                return Name + "_";
            return Name;
        }
    }
}

これも太字。IsOverrideって言うのが追加されてるのは、Controllerの書き換えをされる時に、アクション関数はすべてvirtualになって、そのControllerを内部で派生させて、すべてのアクションをoverrideしてるから。でも、今回追加するActionNameで指定したアクション関数はそもそもそんな名前でControllerには存在しないから、派生クラスでしか不要だし、overrideするものじゃ無いじゃないですか。なので、ここでそのフラグを保持しておいて、テンプレート処理部でこれを見て、overrideを付加するかどうか判定させるって感じです。

続いて、ActionMethodsコレクションを構築しているProcessControllerActionMethodsを書き換える。

void ProcessControllerActionMethods(ControllerInfo controllerInfo, CodeClass2 type) {
    foreach (CodeFunction2 method in GetMethods(type)) {
        ~ 長いのでココはカット ~
        // Collect misc info about the action method and add it to the collection
        controllerInfo.ActionMethods.Add(new ActionMethodInfo(method, controllerInfo));

        // ActionNameAttributeを見てそっちも追加する。
        // オリジナルが不要なら↑のAddを削除。
        foreach(CodeAttribute2 attr in method.Attributes)
        {
          if(attr.Name == "ActionName")
          {
            foreach(CodeAttributeArgument arg in attr.Arguments)
            {
              var actionName = arg.Value.Replace("\"","");
              controllerInfo.ActionMethods.Add(new ActionMethodInfo(method, controllerInfo, actionName));
            }
          }
        }
    }
}

太字部です。なんか、リフレクションじゃないから属性とか属性パラメータの取り方がちょっと特殊。MSDN最高。基本すべてソースコードを解析した結果構築されるオブジェクト達だから中身は単なる文字列。arg.Valueで取り出したActionNameに指定してるName値がソース上で定義されてるのと同じようにダブルクォーテーション付きになってるので、そこを削除。

最後にテンプレート部で処理してる部分を書き換え。

public static class MVC {
<#  foreach (var controller in Controllers) { #>
    public static <#= controller.DerivedClassName #> <#= controller.Name #> = new <#= controller.DerivedClassName #>();
<#  } #>
}

まずはMVCクラスの部分でオリジナルControllerクラスを入れ物にしてたから、テンプレートで生成した派生クラスになるように変更。こうしないとActionName名のアクション関数が無いから困るんです。

namespace <#= T4MVCNamespace #> {
<#  foreach (var controller in Controllers.Where(c => !c.SharedViewFolder)) { #>
    [CompilerGenerated]
    public class <#= controller.DerivedClassName #>: <#= controller.FullClassName #> {
        public <#= controller.DerivedClassName #>() : base(_Dummy.Instance) { }

<#      foreach (var method in controller.ActionMethods) { 
        var overrideKeyword = method.IsOverride ? "override " : "";
#>
        public <#= overrideKeyword #><#= method.ReturnType #> <#= method.Name #>(<# method.WriteFormalParameters(true); #>) {
            var callInfo = new T4MVC_<#= method.ReturnType #>("<#= controller.Name #>", "<#= method.Name #>");
<#          if (method.Parameters.Count > 0) { #>
<#              foreach (var p in method.Parameters) { #>
            callInfo.RouteValues.Add("<#= p.Name #>", <#= p.Name #>);
<#              } #>
<#          }#>
            return callInfo;
        }

<#      } #>
    }
<#  } #>

    [CompilerGenerated]
    public class _Dummy {
        private _Dummy() { }
        public static _Dummy Instance = new _Dummy();
    }
}

上記太字の部分で最後です。無駄にソースを引用してるんで、長いですけど、実質10行ほどじゃないっすかね。思ったより、変更箇所も少なくて助かりました。Visual Studioオートメーション&T4の組み合わせ恐るべし。

そうすると、前回の投稿ではIntelliSenceに出てこなかったActionName指定の部分も出てくるようになりました。

t4mvc4

やったね!

これまた興味ある方はダウンロードどうぞ(T4MVC-T.ttに名前変えてます)。

T4MVC

べーさんも気になってるように、ASP.NET MVCでは文字列(マジックストリング)を指定して、リンクやURLを生成するのが普通ですね。

    <% using(Html.BeginForm("Index","Home")) { %>
      <% = Html.TextBox("Name") %>
      <input type="submit" value="ポスト" />
    <% } %>

例えば、↑こんな感じで書くと↓こんな感じの出力に。

    <form action="/" method="post">
<input id="Name" name="Name" type="text" value="" /> <input type="submit" value="ポスト" /> </form>

リンクの場合もそう。

    <% = Html.ActionLink("ホーム", "Index") %>

↑こんな感じでしょう。URL生成はコントローラとアクションを指定したり、ルーティング名を指定したりするのが、今までのオーソドックスな書き方。

そんな状況は誰も嬉しくないよ!だってアクション名変えたら、指定してる箇所全部検索して変更しなきゃ行けないし、1文字でも間違ってたらちゃんと出力してくれない。あぁ、誰か助けて。

そもそもの始まりは↓ここからでした。

Angle Bracket Percent : A BuildProvider to simplify your ASP.NET MVC Action Links

BuildProviderを使ってASP.NET実行時に、CodeDom使ったコンパイラを走らせてがんばる方法(自アセンブリをリフレクションで探索)。なるほど~、と。ただ、やっぱり応用しにくいっていうか、コードが書きにくい感じで、その時はそれほど便利でもないかな~、なんて油断してました。それでも、この時点でHtmlHelperを拡張して、文字列指定じゃなくて関数指定でリンク生成出来てました。この辺でPhilさんもエントリ上げてて、ActionNameで別名使ったときにこれだと対応出来ないから、その辺上手いこと処理出来るのを次のバージョンに向けて考えてます的なことを言ってた気がする。

その後、いろいろ試行錯誤があって、出てきたのが↓これですよ。

Angle Bracket Percent : A new and improved ASP.NET MVC T4 template

CodeDomじゃなくてT4でいいんじゃね?と気がついたんでしょうか。コード生成するならこういうテンプレートエンジン使った方が断然生産性が高いしね。このときはまだMvc-CodeGenっていう名前だったんですが、これを更にブラッシュアップして出てきたのが↓。

Angle Bracket Percent : T4MVC 2.2 update: Routing, Forms, DI container, fixes

ちょっとバージョンアップしたんだけど、T4MVCに名前変わってからはMVCのソースが公開されてるCodePlexに組み込まれました(アドインだからこういう言い方はおかしいかも?)。

ASP.NET - Release: ASP.NET MVC v1.0 Source

ドキュメントなんてかけらもないので、どういう物かはソースを見て判断しましょう。まぁ、TTファイルの最初に書かれてる内容がそのままなんですけどね。

とにかくまず、自分のMVCプロジェクトのルートにT4MVC.ttをコピー。準備はこれだけ。後はT4なんで勝手にコード生成してコンパイルしてくれるので、すぐに使い始められます(最初にApp_Codeに放り込んでみたら全然動かなくてビックリした)。

t4mvc1

大枠でMVCという静的クラスが生成されて、そこからたどっていく感じになります。ASP.NETでも自動生成されるグローバルクラスにASPっていうのがあるけどそれと同じような感覚ですね。とりあえず、最初に書いたサンプルをT4MVCで書くとどうなるか、ですが、↓こうです。

    <% using(Html.BeginForm(MVC.Home.Actions.Index, MVC.Home.Name)) { %>
      <% = Html.TextBox("Name") %>
      <input type="submit" value="ポスト" />
    <% } %>

    <% = Html.ActionLink("ホーム", MVC.Home.Index()) %>

リンクに表示する部分以外のマジックストリングが無くなりました。しかもこのMVCクラスは動的なので、アクションやコントローラの名前を変更したら、コンパイルエラーが起きるので、変更し忘れともおさらばですよ!素敵です。

これだけじゃなくてデスね、Linksというクラスも同時に生成されるんですが、そこにはContentフォルダとScriptsフォルダに含まれてるファイル達のリンクが生成されます。

    <% = Links.Content.Site_css %>
    <% = Links.Scripts.jquery_1_3_2_js %>

↑こんな感じで。

さて、ここでActionNameAttributeをつけたアクションはちゃんと指定出来るのか気になるところなので、簡単に実験してみましょう。

    [ActionName("Index2")]
    public ActionResult KoukaiShitakunaiNamaeNoAction()
    {
      return View("Index");
    }

こんなのを作成してみる。保存したりすると、勝手にT4MVCが動く。で、ViewでIntelliSenceをきかせてみると~?

t4mvc2

残念!ActionNameまでは見てくれませんでした。Index2が出てくるのを期待したんだけど。実行結果も残念ながら。元の名前のままでした。がんばって自分でtt書き換えてActionName属性を見るようにするか、おとなしく新しいバージョンを待つか。どっちが男らしい?

ところで、このT4MVCを実行するとすべてのコントローラはpartialクラスになって、すべてのアクションはvirtualが自動でくっつきます。どうしてかというと、ActionResultを引数に受け取るHtmlHelper拡張に渡して、URLを生成しやすくするためですよね。内部で独自クラスに派生させたコントローラを生成し、アクションをオーバーライドしてるんですね。なんとも強引な方法。コントローラ書き換えられるのが気持ち悪い!って人にはお勧めしないですけど...。

お試しあれ!

dotnetConf2015 Japan

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