ASP.NETでTeedaのリダイレクトスコープのようなものを実装する

ASP.NETTeedaのリダイレクトスコープのようなものがあると便利かと思って考えてみました。要するに、リダイレクトする直前でセッションに情報を置いて、リダイレクト後のGETでその情報をセッションから取得するという機能です。そんなかんじの機能をつくって公開している人がいそうな気はするのですがみつからないですね。

この機能を利用するPageクラスはこんな感じになります。

Default.aspx.cs


public partial class _Default : Page
{
[RedirectionState(Direction.Out)]
public string Hoge { get; set; }

[RedirectionState(Direction.Out)]
public string Foo { get; set; }

protected void Page_Load(object sender, EventArgs e)
{
}

protected void Button1_OnClick(object sender, EventArgs e)
{
Hoge = TextBox1.Text;
Foo = TextBox2.Text;
Response.Redirect("About.aspx", true);
}
}

About.aspx.cs


public partial class About : Page
{
[RedirectionState(Direction.In)]
public string Hoge { get; set; }

[RedirectionState(Direction.In)]
public string Foo { get; set; }

protected void Page_Load(object sender, EventArgs e)
{
Label1.Text = Hoge;
Label2.Text = Foo;
}

protected void Button1_OnClick(object sender, EventArgs e)
{
}
}

RedirectionStateといった属性を作成して、この属性で値の保存とリストアを自動化できるようにします。Direction.InとかOutとかは、値を保存するかリストアするかどうかを示します。この例では、Default.aspxからAbout.aspxへリダイレクトすると、HogeとFooプロパティが引き継がれます。


この機能を実現するポイントは、IHttpHandlerFactoryの実装です。このクラスでPageのPreInitイベントとUnloadイベントに適当なイベントハンドラーを登録しておきます。

IHttpHandlerFactoryの実装


public class MyPageHandlerFactory : PageHandlerFactory
{
public override IHttpHandler GetHandler(HttpContext context, string requestType, string virtualPath, string path)
{
var handler = base.GetHandler(context, requestType, virtualPath, path);
var page = handler as Page;
if (page == null)
{
return handler;
}
page.PreInit += OnPreInit;
page.Unload += OnUnload;
return page;
}

private void OnPreInit(object sender, EventArgs e)
{
var page = (Page) sender;
if (page.IsPostBack)
{
return;
}
var guid = page.Request.QueryString["rid"];
if (guid == null)
{
return;
}
var redirectionStateCollection = GetRedirectionStateCollection();
var redirectionState = redirectionStateCollection[guid];
if (redirectionState == null)
{
return;
}
if (!string.Equals(page.Request.RawUrl, redirectionState.RedirectLocation, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException();
}
var type = page.GetType();
foreach (var property in type.GetProperties(BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.Public))
{
var attribute = (RedirectionStateAttribute) Attribute.GetCustomAttribute(property, typeof(RedirectionStateAttribute));
if (attribute != null && attribute.Direction == Direction.In)
{
if (property.CanWrite)
{
var value = redirectionState[property.Name];
if (value != null)
{
property.SetValue(page, value, null);
}
}
}
}
}

private void OnUnload(object sender, EventArgs e)
{
var page = (Page) sender;
var context = HttpContext.Current;
var response = context.Response;
if (!response.IsRequestBeingRedirected)
{
return;
}
var type = page.GetType();
var redirectionState = new HttpRedirectionState();
foreach (var property in type.GetProperties(BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public))
{
var redirectable = (RedirectionStateAttribute) Attribute.GetCustomAttribute(property, typeof(RedirectionStateAttribute));
if (redirectable != null && redirectable.Direction == Direction.Out)
{
if (property.CanRead)
{
redirectionState[property.Name] = property.GetValue(page, null);
}
}
}
var guid = Guid.NewGuid().ToString();
var location = response.RedirectLocation;
response.RedirectLocation = location.IndexOf('?') > 0 ? location + "&rid=" + guid : location + "?rid=" + guid;
redirectionState.RedirectLocation = response.RedirectLocation;
var redirectionStateCollection = GetRedirectionStateCollection();
redirectionStateCollection[guid] = redirectionState;
context.Session["redirectionStates"] = redirectionStateCollection;
}

private HttpRedirectionStateCollection GetRedirectionStateCollection()
{
var context = HttpContext.Current;
return (HttpRedirectionStateCollection)context.Session["redirectionStates"]
?? new HttpRedirectionStateCollection();
}
}

ちょっと考えたのは2点。

  • リダイレクトのときにクエリストリングにGUIDを振っておいて、同一セッションの別リクエストとまざらないようにしています。また、リダイレクト後のGETのときにリクエストのURLがリダイレクトに指定されたURLと同じかチェックするようにしてみました。
  • セッションに置いたリダイレクトスコープの値ですが、リストアしたあとはすぐに破棄せずにセッションに残しています。リロードしたときにもまだ使えたほうがうれしいような気がして。実装していないですが、MRU(Most Recently Used)で指定した数ぶんだけ残しておくのはどうだろうかと思っています。


MyPageHandlerFactoryはもちろんWeb.configに登録しておきます。


<system.webServer>
<handlers>
<add name="MyPageHandlerFactory" verb="*" path="*.aspx" type="RedirectionStateSamle.MyPageHandlerFactory, RedirectionStateSamle" />
</handlers>
<modules runAllManagedModulesForAllRequests="true" />
</system.webServer>


メインとなるのは上のMyPageHandlerFactoryですが、他に自作したクラスはこんなです。


[Flags]
public enum Direction
{
In, Out
}


[Serializable]
public class HttpRedirectionState : DictionaryBase
{
public string RedirectLocation { get; set; }

public object this[string name]
{
get
{
return Dictionary[name];
}
set
{
this.Dictionary[name] = value;
}
}

[Serializable]
public class HttpRedirectionStateCollection : DictionaryBase
{
public HttpRedirectionState this[string name]
{
get
{
return (HttpRedirectionState)Dictionary[name];
}
set
{
this.Dictionary[name] = value;
}
}
}

[AttributeUsage(AttributeTargets.Property, Inherited = true)]
public class RedirectionStateAttribute : Attribute
{
public Direction Direction { get; private set; }

public RedirectionStateAttribute(Direction direction)
{
Direction = direction;
}
}