2008年4月1日火曜日

TempDataとRailsのflash

ASP.NET MVCをちょこちょこ調べるシリーズの最後に何か作ってみようと思って、ヘコヘコやってるところなんだけど、TempDataにやられた...。

もちろんベータ版だし、最終的には解決されるんだろうとは思うけど、少なくとも今のバージョンでは、SQLServerをセッションストアにしたときに、TempDataが機能しませぬ。 TempDataはそもそもSessionを使って実装してるって言うのは、まぁそうだろうと(どっかに書いてたし)。RedirectToActionとかした場合に、コンテキストが切り替わって情報を持っていけないんだから納得です。 でも、そのTempDataの実装、TempDataCollectionが内部で使ってるのが Pair<Dictionary<string, object>, HashSet<string>>。これがシリアライズできない...。だからInProcならいいけど、SQLだとSessionに入らないです。もちろんStateサーバーもシリアライズできないからダメ。

どうしたものか。SessionをInProcで使うのってなんか、気持ち悪いじゃないですか(そんなこと無い?)。 なので、もっと簡単に同じ仕組みを作ってしまえばいいんじゃないかと思って、FlashData(railsの同じ仕組みはflashという)のを作ってみました。無駄な作業とか言わないで!

public class FlashDataCollection { const string SESSIONKEY = "RevasFlashDataSessionKey"; Dictionary _flashData; HttpContextBase _httpContext; public FlashDataCollection(HttpContextBase httpContext) { if (httpContext != null) { this._httpContext = httpContext; this._flashData = httpContext.Session[SESSIONKEY] as Dictionary; this._httpContext.Session.Remove(SESSIONKEY); } if (this._flashData == null) this._flashData = new Dictionary(); } public object this[string key] { get { if (this._flashData.ContainsKey(key)) return this._flashData[key]; else return null; } set { this._flashData[key] = value; this._httpContext.Session[SESSIONKEY] = this._flashData; } } }

こんなクラスで。 やってることは↓これだけ。

  • ControllerのExecuteでFlashDataCollectionをnew。
  • FlashDataCollectionのコンストラクタで、Sessionに入ってるならそっちを復元してSessionからRemove。
  • 無ければDictionary<string,object>をnew。
  • 後は、インデクサで値をセットするたびにSessionに入れなおす。
んで、ControllerはExecuteをoverrideしたCusomControllerを作っておいて、こっちを派生させる。

public FlashDataCollection FlashData = null; protected override void Execute(ControllerContext controllerContext) { this.FlashData = new FlashDataCollection(controllerContext.HttpContext); base.Execute(controllerContext); }

こんな感じでいいんじゃないかと。 Controller ではTempDataの代わりにFlashDataに値を入れて、RedirectToActionしたときとかにはこっちを参照。ただし、これだけだと、Viewで参照できない(Controllerに入れちゃってるんで)ので、今度はRenderViewをoverrideするかとも思ったけど、そこまでしなくても次のバージョンとかではTempDataもシリアライズできるようにしてくると(期待して)思うので、今回はあえて CustomViewDataを作って、そのViewDataクラスに値を入れ込んでいくって言う方法にしときました(分かりにくい説明だ...)。 たとえば、Home/Inputっていうアクションで入力した内容をHome/Saveって言うアクションにPOSTしたとき、入力値の検証なんかは ModelでやるからSaveで値を取得した後にエラーだと判明するわけだったりするじゃないですか。そしたらエラーメッセージと入力値を TempDataに入れて、Home/InputにRedirectToActionで飛ばすでしょ。そんな使い方のときにうまいことやりたいっす。 ViewDataはSerializable属性付けて自分で作ったクラスを使うようにしておけば、うまくいくっちゃ! そんなこんなでなかなか先に進まないっちゃ! ところで、RouteValueDictionaryのコンストラクタってちょっと不思議だな~、と思ってどうやってんのかと思ったら、普通にリフレクションしてるだけみたい。

routes.Add(new Route("{controller}/{action}/{id}", new MvcRouteHandler()) { Defaults = new RouteValueDictionary(new { action = "Index", id = "" }), });

↑この無名クラスをnewで初期化してるとこ。:action=>"Index",:id=""みたいな書き方で、Dictionaryになるんだよ?

public static System.Collections.IDictionary MakeDictionary(object withProperties) { System.Collections.IDictionary dic = new Dictionary(); foreach (var property in withProperties.GetType().GetProperties(BindingFlags.Public|BindingFlags.Instance)) { dic.Add(property.Name, property.GetValue(withProperties,null)); } return dic; }

↑こんな感じの実装でちゃんとDictionaryになるのね。シンプルでよろしだね。

Turn Anonymous Types into IDictionary of values - ISerializable - Roy Osherove's Blog