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使いましょう。

2009年2月17日火曜日

残念なことが続いてさ

こないだノートパソコンのメモリを2GBから4GBに変えたのよ。Vista32bitだから全部は使えないけど、そこはそれ、みんながやってるようにRAM DISK(Gavotte Ramdisk)にしてさ。
1GBくらいできるじゃないですか。

IMG_0445

んで、思ったわけですよ。
このディスクの使い道をIEとFirefoxのキャッシュじゃなくて、USBメモリ扱い(Removable Media)のドライブにしてReady boostにすると、結局メモリとしてすべて使えて早くなるんじゃないかって。

しばらくそれで動かしてたんだけど、あれだね、関係ないね。Ready Boostとして使える領域が減る分HDDが回転しちゃうからか、バッテリの持ちも悪くなったし、パフォーマンスも上がらないし。

結局Fixed Mediaに設定を戻してReady BoostはSDカードに戻したよ。いいアイデアだと思ったんだけどな~。

これもかなり残念なことだったんだけど、それ以上に残念だったのがこれ。

IMG_0446

最近ホッケーよりも水泳の方が楽しくてね、週3で泳いでるんだけどね、いつも行ってるプールがね、休みだったの...。泣ける。メモリ上手く使えなかったことなんかよりよっぽど泣ける。ちゃんと定休日調べてなかったのが悪いんだけどさ。この休館が実は10日間も続くなんて思ってもなくてさ。だけど、泳ぎたいじゃない?だから違うプールに行ったわけですよ。そしたらね、完全アウェーな空気に心が折れそうになっちゃってさ。プールってコースの泳ぎ方とか場所によってローカルルールがあるわけじゃないですか。でも、なんか、いつもと違うってだけで、心折れる。ちゃんと泳ぎはしたけど、なんかね。スッキリしないっていうか。

今はもうホームプールが復活してるから、我が物顔で泳ぎまくってるんだけど、アウェー怖しだね。

URLは自動生成になっちゃうの?

今気がついたというか、確認してみたら"blog-post_n"みたいなURL(Slug)が生成されてるけど、コレを変えるにはどうするのかな~?

WLWだと出来なかったりするの?

slug

あ、スラッグが"いいえ"になってる...。

自分で指定は出来ないけど、アルファベットで始まるタイトルなら、それを利用するようには出来てるんだね。このエントリのアドレス中途半端に"url"とかってなってるし。

まぁ、いいか。特に使いたいシステムが他にあるわけでもないし、自分でサーバー用意するのも面倒だから、これからは徐々にこっちに移行してしまおう。

最初からFriend Connectでしかコメントを受け付けないようにしとけば、へんなスパムコメントもないだろうし(昨日の今日ですでにスパムコメントがあったのに驚いたけど即削除)。

今度は

Windows Live Writerで書いてみる。 フォントとかどうなっちゃうのかちょっと不安なんだよね。

ちゃんと見れるかな?

dotnetConf2015 Japan

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