2009年2月24日火曜日

心機一転

ブログをBloggerに移行します。 FeedBurner経由でFeedを購読してる場合は、そのままのでOKです。 特に、現在がんばってるASP.NET MVCに関するエントリーは移行しました。 が、その他のエントリーは、移行せずそのままにしておきます。 今後、こちらのブログでの更新がメインになります。 あと、Tumblrはそのまま継続使用デス。

BloggerのRate Limit

まさか、自分がこの制限に引っかかるとは...。 調子に乗って、前ブログからエントリーを移行させるという行為が仇になった。 24時間で解除されるみたいだけど、それまでWindows Live Writer経由でのエントリーが出来ませぬ。 Windows Live Writerで書き込もうとすると...。
400 Bad Request Blog has exceeded rate limit or otherwise requires word verification for new posts
と、悲しいメッセージが出てくる。 50件あたりで出てきた気がする...。切ないね~。 ブラウザ経由でも、API経由でも出るみたいだから、一気にやるならImport機能のフォーマット似合わせたXMLファイルを作成して、それを取り込むのがいいんだろうね。 面倒くさいな~。特に、画像をPicasaに入れてimgタグのsrc書き換えとかゲンナリするし。 まぁ、ASP.NET MVC関係のエントリだけを移行させようと思ってるから手作業でやるさ。

ASP.NET MVCとGearsの連携

Maarten Balliauw {blog} - Creating an ASP.NET MVC application with Google Gears

面白いエントリー発見。 Gearsは最初のリリースの時に少し触ってみた程度で、名前からGoogleが消えたあたりから全く状況を追いかけてないんだけど、このサンプルがかなり面白い。

エントリーには2つソリューションがあって、1つはMVCオンリーでもう一つがGears対応版。両方見比べると分かりやすいんだけど、なにせDBファイル込みになってるから少しサイズが大きいのが難点。今時気にしないか。

どっちにも共通して存在してる(デフォルトで作成されるファイルを除く)のが、NoteControllerとそのView達(List/Detail/Edit/Create)。

で、Gears対応版では、Routeにmanifest.jsonへのリクエストに応えるためのルート登録をして(GearsController)、関連jsファイルが含まれててそれをインクルードするためにSite.Masterが少し違うだけ。

MVC部に注目してみると、GearsControllerのIndexアクションが~/Contentと~/Scriptsフォルダの全ファイルのURL、それとMVCがViewを返すURL(/Homeや/NoteList)、データ詳細のURLをJsonで返すだけに見える。

一体全体どこでデータとViewのHTMLとを分離するコードがあるんだろう。テンプレート的な物が定義されてるんだとばかり思ってたけど、そんなコードが見あたらない。

demo_offline.jsはGearsを有効にしたり、無効にしたときのローカルストアの操作と、完全オフラインになったときに編集用リンクを無効にしたり、オフラインかどうかを監視するコードだけっぽい。 gears_init.jsはいじらないコードだよね。

formやinputのname属性とかを見て自動で判断してくれてるのかな。それともシンプルに各URLに対するアクションをすべて記録してて、オンラインになったときに再生? ちゃんとGearsの仕様を確認すれば分かることなんだろうけど、まぁ、ねぇ。今はそんなに興味ないって言うか。

それにしても、Gearsはずいぶんスゴイ事になってる気がするし、MVCとの連携がこれほどシンプルに出来るのもViewStateとPostBackがないおかげだよね。

2009年2月23日月曜日

DataAnnotationsだけでの入力検証の盲点

以前の投稿(ASP.NET MVC RCの入力検証)で、如何にASP.NET MVCのDefaultModelBinder(IModelBinder)が汎用的になったかを取り上げましたね。

で、以前の投稿で見事に見逃してたのが「必須入力ではないけど、入力形式の不正メッセージを表示したいな」というところ。分かりにくいですね。例えば日付フィールドがあってモデルのプロパティはDateTimeなら当然日付形式の文字列じゃないとキャスト出来ないから、入力エラー。数値型ならintで当然アルファベットとか勘弁してくれよ、と。

必須フィールドならRequire属性でいいですよね。キャストに失敗した場合、何もセットされず型初期値(default(T))が入ったままだから、エラーメッセージに「ちゃんと入力してね(ハート)」って表示すれば。それでも、初期値が数値で0だと困る!って時にはNullable<Int32>とかでnullにしとけば、初期値のまま処理が進んじゃうって事も防げますから。

だけど、必須じゃない場合に「キャスト出来ませんでした」なんていうシステム固定のメッセージを出すのは、どうなのよ、なんて時があるもん。社内システムとかならそういうモンだから、で済むかもしれないけど、ネットに公開するならそういうメッセージはダサイ。いや、社内システムでもダサイけど、対象ユーザー層を考えれば、それでもまぁいいじゃん、っていうかね。

ちなみに、DataAnnotationsを使って、入力検証を実装した以前の実装だと、キャストエラー表示してくれないもんね。ModelStateDictionaryには(モデルを復元したタイミングで)ちゃんとエラーとして入ってるんだけど、ModelErrorクラスのErrorMessageにはメッセージが入って無くて、Exceptionプロパティに例外情報として入ってるから展開されないんです。

以前のテストプロジェクトにここで登場してもらいましょう。で、1箇所変更点として、PersonViewModelクラスのAgeプロパティについてるRequire属性を削除して、Range属性だけにしてみます。クラス定義は以下の通り。

public class PersonViewModel : BaseViewModel
{
 public int Id { get; set; }

 [Required(ErrorMessage="名前は?")]
 public string FirstName { get; set; }

 [Required(ErrorMessage="名字は?")]
 public string LastName { get; set; }

    [Range(0,150,ErrorMessage="0歳から150歳で")]
    public int? Age { get; set; }

 public List Weapons { get; set; }

 public PersonViewModel()
 {
   Weapons = new List();
 }

 public override string Error
 {
   get
   {
     if (Weapons == null || Weapons.Count == 0)
       return "武器、っていうか必殺技は?";

     return null;
   }
 }
}

太字のところですね。 これで、入力値に整数以外を入れて、ポストしたときのスクリーンショットが↓これです。

modelbind

ViewがRenderされるときに、ModelStateDictionaryがどうなってるかをブレークポイントをセットして確認してみましょう。

modelbind2

クリックすると大きく見れます。 Render時には、ModelState内のModelErrorは存在してるけど、ErrorMessageは""空文字で、ExceptionにInvalidOperationExceptionが入ってるのが分かります。

で、このExceptionをエラーメッセージとして表示するなら、そのまま取り出して、ErrorMessageに入れてしまうようなコードを書いてしまえばいいですね。ただ、エラーメッセージが今回の場合だと「17a は Int32 の有効な値ではありません。」と、出ちゃうんですよね。Int32って...。そんなこと表示されても普通の人は理解できないし。そもそもどのタイミングでメッセージの取り出し処理をすればいいでしょうね、って展開になります。

そこで、登場するのがIModelBinderのオーバーライド出来るメソッド群。これまたオレルールの方のエントリーの一番最後に書いてるIModelBinderのイベント発生順がキーになります。

結論から言うと、OnModelUpdatedのタイミング(モデルの復元完了時)に、ModelStateDictionary内のModelErrorに上記例外が含まれてるかチェックしてしまえばいい、という事になります。 ドンドン意味の分かりにくいエントリーになってきてますね~。

で、コードとしてはシンプルに↓こんな感じで動きます。LINQ部がかなり適当...。

public class ValidateModelBinder : DefaultModelBinder
{
 protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
 {
   base.OnModelUpdated(controllerContext, bindingContext);

   var modelStates = bindingContext.ModelState;


   // ここでModelStateDirctionaryにInvalidCastExceptionを含んだエラーが
   // ないかチェックして、あれば入力エラーメッセージをここでいれる。
   // ※表示するときに空メッセージ(InvalidCastException)は除外するから、
   //   コレクションには入れたままにしておく。
   // ModelErrorを消さずにそのまま残しておくことで、ViewPageでCSSクラス名
   // が追加されてどのフォームエレメントがエラーなのかは視覚的に
   // 判断できる。
   var key = "__InvalidOperationException__";
   if (!modelStates.Keys.Contains(key))
   {
     var isInvalidCast = (
                           from ms in modelStates
                           from err in ms.Value.Errors
                           where err.Exception is InvalidOperationException
                           select err
                         ).Count() > 0;
     if (isInvalidCast)
       modelStates.AddModelError(key, "入力形式の間違った項目があります。");
   }
 }
} 

こんな感じで、DefaultModelBinderを派生させたクラスを作成し、このModelBinderをDefaultBinderにしちゃいます。もう、これだけでいいっす。 なので、Global.asaxのApprication_Startの所に以下のコードを追加。

protected void Application_Start() { RegisterRoutes(RouteTable.Routes); ModelBinders.Binders.DefaultBinder = new ValidateModelBinder(); }

特定のViewModelでしか使わないよ!っていうならViewModelクラスの宣言時に

[ModelBinder(typeof(ValidateModelBinder))] public class モデルクラス {…}

って、書きましょう。

この状態で、もう一度動かしてみます。

modelbind3

今度はちゃんと、ValidationSummary()で表示されるようになりました。 今回のコードは無理矢理エラー項目のキーに"__InvalidOperationException__"と入れて、複数のエラーメッセージが出ないようにしてますが、もちろん項目毎に表示してもいいですよね。

若干気になるのは、なんでInvalidOperationExceptionを発生させるようにしてるのかな~、というところ。なんとなくだけど、InvalidCastExceptionのほうがシックリ来ないですかね~。そういうもんなんですか? ちなみにどこでInvalidOperationExceptionを発生させてるかというと、ASP.NET MVCに含まれるValueProviderResultクラスのConvertSimpleTypeメソッド。この中でTypeDescriptor.GetConverter()で取得したコンバーターのConvertToを呼び出すところらへん。

ココまで書いて思ったんだけど、ASP.NET MVCに関するエントリーだけでも全部こっちに持ってくればいいのかな。今はそれ以外の事を書く事もほとんどないし。そうしちゃおっかな。

ASP.NET MVCのエントリは全部移行しました。手作業で...。

なにを~!タイムリー過ぎるぜ!

You Still Can’t Create a jQuery Plugin? – NETTUTS

まだ、作ったことないっていうかjQuery自体まだ使ったことないってばよ。fnに入れればいいんでしょ?

そのくらいしか、知識がないので、ビデオでお勉強。
シンプルなプラグインだけど、まずはシンプルな物を理解しないとね!

(function($){

})(jQuery);

っていうのは基本みたい。jQuery本体を引数に渡して$でショートカット。

ちょっと、不思議に思ったのがthisの扱い。
Enumerable(っていうのかな)なリストに対して、プラグインを実行すると、プラグイン内でのthisがこのリストを指す。プラグイン内でそのリストをeachで回したときのthisはDOMエレメント(だよね?)。だからeach内では$(this)でjQueryでかぶせて色々便利になるようにしないといけない。

hoverに渡すfunctionがmouseenterとmouseoutの両方なのが素敵な感じがする。
ビデオ中ではmouseoutの時に$(‘tooltip’).hide()って書いてるけど、これじゃ、無限に同じIDのtooltipが出来ちゃうからダメじゃん!と思ってたけど、デモで使ってるコード(ブログエントリの最後のコードも)ではちゃんとremove()になってる。前もそうだったけど、ここで取り上げるビデオな何かしら引っかけがあるよね。

最後に、チェインするためにreturn thisでエレメントのリストをそのまま返すあたりも、jQuery流なのかな。これしないと、"."で区切って続けていろいろ出来ないもんね。

この例だと、fnに単一のfunction(内部では無名関数使ってるけど)を入れてるだけの、シンプルな物だけどもっと大きなプラグインを作る時にはどうするか気になる。けど、そこはすでに公開されてる他のプラグインをいくらでも参照すればいいから、さぁ、作ってみよう!って言うときにはそれほど問題にならないよね、きっと。

2009年2月19日木曜日

AreasサンプルがRCに対応

Philさんすげーよ。あんたやっぱり最高だよ。
RCに対応させたAreasのコードがアップロードされてる。

Grouping Controllers with ASP.NET MVC

RouteCollectionの簡単登録ヘルパーと、Viewの位置を解決させるためだけのカスタムViewEngine。

何でこれだけで出来るのか不思議だ。コントローラのパスだってデフォルトの名前空間だって違うのに、ControllerFactoryを作ることなく出来るんだよ。

で、いつだったか忘れた(ベータだっけな~?もっと前?)けど、ルーティングのDataTokensにNamespacesを入れとけば、コントローラのNamespaceに自動で入れてくれるっていう仕様になったような。

気になったらとめられない。とりあえずRCのソースをチェック。
たぶんControllerFactoryあたりだろう。

   

protected internal virtual Type GetControllerType(string controllerName) {
    if (String.IsNullOrEmpty(controllerName)) {
        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName");
    }

    // first search in the current route's namespace collection
    object routeNamespacesObj;
    Type match;
    if (RequestContext != null && RequestContext.RouteData.DataTokens.TryGetValue("Namespaces", out routeNamespacesObj)) {
        IEnumerable<string> routeNamespaces = routeNamespacesObj as IEnumerable<string>;
        if (routeNamespaces != null) {
            HashSet<string> nsHash = new HashSet<string>(routeNamespaces, StringComparer.OrdinalIgnoreCase);
            match = GetControllerTypeWithinNamespaces(controllerName, nsHash);
            if (match != null) {
                return match;
            }
        }
    }

正解だね。
太字のところ。コレでRouteData.DataTokensからNamespacesを取得して、Controllerの型を判定してる。

   

public static class AreaRouteHelper {
    public static void MapAreas(this RouteCollection routes, string url, string rootNamespace, string[] areas) {
        Array.ForEach(areas, area => {
            Route route = new Route("{area}/" + url, new MvcRouteHandler());
            route.Constraints = new RouteValueDictionary(new { area });
            string areaNamespace = rootNamespace + ".Areas." + area + ".Controllers";
            route.DataTokens = new RouteValueDictionary(new { namespaces = new string[] { areaNamespace } });
            route.Defaults = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "" });
            routes.Add(route);
        });
    }

    public static void MapRootArea(this RouteCollection routes, string url, string rootNamespace, object defaults) {
        Route route = new Route(url, new MvcRouteHandler());
        route.DataTokens = new RouteValueDictionary(new { namespaces = new string[] { rootNamespace + ".Controllers" } });
        route.Defaults = new RouteValueDictionary(new { area="root", action = "Index", controller = "Home", id = "" });
        routes.Add(route);
    }
}

なので、上記ヘルパー(Philさんのコードです)の太字の部分さえ書いておけば、ちゃんとサブフォルダーのControllerとしてインスタンスを作ってくれる。凄いな~。
Namespacesがうんたらかんたらってアナウンスしてたときは、はぁ、そうですか、それ何に使うんですか?ってなもんだったけど、こうやって実装を見せられると納得。

ところで、Windows Live Writerのアドインでコードを色つきに出来るみたいだけど、BloggerにポストするとCSSがエンコードされちゃってちゃんと出来ない。使い方としてはCSSを外部に出してlinkタグを書くのが正んでしょうか?その場合、外部CSSはどこに置いておくのが正しいんでしょうか?いまいち使い方が分からずだけど、徐々に慣れていこうと思うところです。

2009年2月18日水曜日

DataAnnotationsのValidationAttributeを作る

何となくね、n以上の値のみ許可、っていうValidationAttributeが欲しいな、と思ったわけですよ。 でも、実際はRangeで最小値とdouble.MaxValue(intでもいいけど)とかで、代用出来るじゃないですか。 悩ましいけど、コードを見てコレってようは最小値を制限したいってことなんでしょ?っていうのをすぐ分かるようにしとくなら専用のValidationAttributeかなって思ってね。

で、思ったわけ。そもそもなんて言う名前のクラスにすれば適切なんですか、って。 辞書で調べたらor moreとか、aboveとか出てくるじゃない。でもなんかね~、クラス名にするにはしっくり来ない。

みんなどうしてるんだろ。気になる。結局はさ古典(昔、なんかで勉強したときに確かGTEとかって書いた気がするし)に則って、GreaterThanOrEqualAttributeって名前にしたんだけどさ。長いよね。ちなみにLINQのExpression Treeではどうしてるのかな、とMSDN見てみたら(ExpressionType 列挙体 (System.Linq.Expressions))、ExpressionType.GreaterThanOrEqualだね。じゃ、そういうことで。

   

public class GreaterThanOrEqualAttribute : ValidationAttribute { private Type targetType { get; set; } private object minValue { get; set; }

public GreaterThanOrEqualAttribute(Type type, object min) { targetType = type; minValue = min; }

public override bool IsValid(object value) { if (value == null) return false;

var val = Convert.ChangeType(value, targetType) as IComparable; var min = Convert.ChangeType(minValue, targetType) as IComparable; // 同値以上ならOK return val.CompareTo(min) > 0; } }

って、書いて動くんだけど、まぁ、ちょっと、ね。

最初から用意されてないってことはRange使えよっていう意図があるような気がするから、Range使いましょう。

dotnetConf2015 Japan

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