しかし、script タグの src 属性に関しては、チルダを使った仮想パスや相対パスで記述しても変換が一切行われません。
なので、Control.ResolveClientUrl メソッドや VirtualPathUtility.ToAbsolute メソッドを使用して変換する必要があります。幸い、HTML タグの属性値ならば <%= %> が使用できるので (コードのハイライトやインテリセンスは効きませんが) 簡単に対処できます。
Site.Master
<head runat="server">
<title><asp:Localize runat="server" Text="<%$ Resources:CommonResource, SystemName %>" /></title>
<link href="Common.css" type="text/css" rel="stylesheet" />
<link href="Site.css" type="text/css" rel="stylesheet" />
<script src="<%= this.ResolveClientUrl("~/Common.js") %>" type="text/javascript"></script>
<script src="<%= this.ResolveClientUrl("~/Site.js") %>" type="text/javascript"></script>
<asp:ContentPlaceHolder ID="HeadPlaceHolder" runat="server" />
</head>
なお、スタイルシートに関しては Thema 機能を使用すれば link タグを記述する必要はなくなりますが、全てのスタイルシートが読み込まれてしまうため、(スタイルシートの) クラス名の衝突の回避に一工夫必要だったりと管理が複雑化します。テーマの切り替えが不要ならば使用しない方が良いかと思います。
[関連]
C#と諸々 ルートディレクトリを示す ~ 演算子
int GetIisMajorVersion()
{
const string keyName = @"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W3SVC\Parameters";
const string valueName = "MajorVersion";
int value = (int)Registry.GetValue(keyName, valueName, -1);
if (value == -1)
{
throw new Exception("インターネット インフォメーション サービスのバージョンを取得できませんでした。");
}
return value;
}
ちなみにマイナーバージョンは "MinorVersion" に入ってます。
また、Current という静的プロパティを通じて操作するようにも変更してあります。
ちなみに、最近は MVP パターン使ってるのでリンク先のような使い方はしなくなりました。Presenter をセッションに格納する際に便利です。
SessionStateAdapter<TItem> クラス
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.SessionState;
namespace MoroMoro.Enterprise.Web
{
/// <summary>
/// 型をキーとしてセッションステートを利用するアダプターです。
/// </summary>
/// <typeparam name="TItem">セッションステートに保存するインスタンスの型。</typeparam>
public class SessionStateAdapter<TItem>
where TItem : class
{
#region Static Members
/// <summary>
/// 現在のセッションステートに接続する SessionStateAdapter<TItem> オブジェクトを取得します。
/// </summary>
/// <exception cref="InvalidOperationException">現在の HttpContext を取得できませんでした。</exception>
/// <exception cref="InvalidOperationException">セッションステートが利用できません。</exception>
public static SessionStateAdapter<TItem> Current
{
get
{
HttpContext currentContext = GetCurrentContext();
object key = typeof(SessionStateAdapter<TItem>);
if (currentContext.Items[key] == null)
{
HttpSessionState currentSessionState = GetSessionState(currentContext);
currentContext.Items[key] = new SessionStateAdapter<TItem>(currentSessionState);
}
return (SessionStateAdapter<TItem>)currentContext.Items[key];
}
}
/// <summary>
/// 現在の HttpContext を取得します。
/// </summary>
/// <returns></returns>
/// <exception cref="InvalidOperationException">現在の HttpContext を取得できませんでした。</exception>
private static HttpContext GetCurrentContext()
{
HttpContext currentContext = HttpContext.Current;
if (currentContext == null)
{
throw new InvalidOperationException("現在の HttpContext を取得できませんでした。");
}
return currentContext;
}
/// <summary>
/// 現在のセッションステートを取得します。
/// </summary>
/// <returns>現在のセッションステート。</returns>
/// <exception cref="InvalidOperationException">セッションステートが利用できません。</exception>
private static HttpSessionState GetSessionState(HttpContext currentContext)
{
HttpSessionState currentSession;
try
{
currentSession = currentContext.Session;
}
catch (Exception ex)
{
throw new InvalidOperationException("セッションステートが利用できません。", ex);
}
if (currentSession == null)
{
throw new InvalidOperationException("セッションステートが利用できません。");
}
return currentSession;
}
#endregion
#region Constructors
/// <summary>
/// SessionStateAdapter<TItem> クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="sessionState">接続するセッションステート。</param>
private SessionStateAdapter(HttpSessionState sessionState)
{
this._sessionState = sessionState;
this._key = typeof(TItem).FullName;
}
#endregion
#region Fields
/// <summary>
/// 接続するセッションステートを取得します。
/// </summary>
private readonly HttpSessionState _sessionState;
/// <summary>
/// 項目キーを取得します。
/// </summary>
private readonly string _key;
#endregion
#region Methods
/// <summary>
/// ジェネリックパラメータ TItem のインスタンスを現在のセッションステートから取得します。
/// </summary>
/// <returns>ジェネリックパラメータ TItem のインスタンス</returns>
/// <exception cref="NotFoundSessionItemException">指定した項目がセッションステート内に存在しません。</exception>
public TItem GetItem()
{
if (!ItemIsExists())
{
throw new SessionItemNotFoundException(typeof(TItem).FullName);
}
Dictionary<Type, object> holder = this.GetHolder(false);
return holder[typeof(TItem)] as TItem;
}
/// <summary>
/// ジェネリックパラメータ T のインスタンスを現在のセッションステートに設定します。
/// </summary>
/// <param name="target">ジェネリックパラメータ T のインスタンス。</param>
/// <exception cref="ArgumentNullException">引数 target が null です。</exception>
/// <exception cref="InvalidOperationException">セッションステートが読み取り専用です。</exception>
public void SetItem(TItem target)
{
if (target == null)
{
throw new ArgumentNullException("target");
}
if (this._sessionState.IsReadOnly)
{
throw this.CreateExceptionForSessionStateIsReadOnly();
}
Dictionary<Type, object> holder = this.GetHolder(true);
holder[typeof(TItem)] = target;
}
/// <summary>
/// ジェネリックパラメータ T のインスタンスを現在のセッションステートから削除します。
/// </summary>
/// <exception cref="InvalidOperationException">セッションステートが読み取り専用です。</exception>
public void RemoveItem()
{
if (this._sessionState.IsReadOnly)
{
throw this.CreateExceptionForSessionStateIsReadOnly();
}
Dictionary<Type, object> holder = this.GetHolder(true);
holder.Remove(typeof(TItem));
}
/// <summary>
/// ジェネリックパラメータ T のインスタンスが現在のセッションステートに存在するかどうかを表す値を返します。
/// </summary>
/// <returns>ジェネリックパラメータ T のインスタンスが現在のセッションステートに存在するかどうかを表す値。</returns>
/// <exception cref="InvalidOperationException">セッションステートが利用できません。</exception>
public bool ItemIsExists()
{
Dictionary<Type, object> holder = this.GetHolder(false);
if (holder == null)
{
return false;
}
if (!holder.ContainsKey(typeof(TItem)))
{
return false;
}
return (holder[typeof(TItem)] is TItem);
}
/// <summary>
/// ホルダーを取得します。
/// </summary>
/// <remarks>
/// ASP.NET 状態サービスを使用する場合、シリアル化の際に、セッション状態の各項目との間でオブジェクトグラフが共有されません。
/// これを回避するために、セッション状態にホルダー (Dictionary<TKey, Tvalue> オブジェクト) を一つ用意し、
/// セッション状態に格納する全てのオブジェクトをこのホルダーに格納します。
/// </remarks>
/// <returns>ホルダー。</returns>
private Dictionary<Type, object> GetHolder(bool createIfDoesNotExist)
{
Dictionary<Type, object> holder = this._sessionState[typeof(SessionStateAdapter<>).FullName] as Dictionary<Type, object>;
if ((holder == null) && createIfDoesNotExist)
{
if (this._sessionState.IsReadOnly)
{
throw this.CreateExceptionForSessionStateIsReadOnly();
}
holder = new Dictionary<Type, object>();
this._sessionState[typeof(SessionStateAdapter<>).FullName] = holder;
}
return holder;
}
/// <summary>
/// セッションステートが読み取り専用であることを示す例外を生成します。
/// </summary>
/// <returns>セッションステートが読み取り専用であることを示す例外。</returns>
private Exception CreateExceptionForSessionStateIsReadOnly()
{
return new InvalidOperationException("セッションステートが読み取り専用です。");
}
#endregion
}
}
SessionItemNotFoundException クラス
using System;
using System.Runtime.Serialization;
using System.Security.Permissions;
namespace MoroMoro.Enterprise.Web
{
/// <summary>
/// セッションステート内に特定の項目が見つからないことを表す例外。
/// </summary>
[Serializable]
public class SessionItemNotFoundException : Exception
{
#region Constructors
/// <summary>
/// SessionItemNotFoundException クラスの新しいインスタンスを初期化します。
/// </summary>
public SessionItemNotFoundException()
: this(null)
{
}
/// <summary>
/// この例外の原因である項目のキーを指定して、SessionItemNotFoundException クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="key">例外の原因となった項目キー。</param>
public SessionItemNotFoundException(string key)
: this(key, "指定した項目がセッションステート内に存在しません。")
{
}
/// <summary>
/// エラー メッセージ、およびこの例外の原因である項目のキーを指定して、SessionItemNotFoundException クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="key">例外の原因となった項目キー。</param>
/// <param name="message">エラーを説明するメッセージ。</param>
public SessionItemNotFoundException(string key, string message)
: this(key, message, null)
{
}
/// <summary>
/// エラー メッセージ、項目キー、およびこの例外の原因である内部例外への参照を使用して、SessionItemNotFoundException クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="key">例外の原因となった項目キー。</param>
/// <param name="message">例外の原因を説明するエラー メッセージ。</param>
/// <param name="innerException">現在の例外の原因である例外。内部例外が指定されていない場合は、null 参照 (Visual Basic の場合は Nothing)。</param>
public SessionItemNotFoundException(string key, string message, Exception innerException)
: base(message, innerException)
{
this._key = key;
}
/// <summary>
/// シリアル化したデータを使用して、SessionItemNotFoundException クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="context">転送元または転送先に関するコンテキスト情報を含んでいる System.Runtime.Serialization.StreamingContext。</param>
/// <param name="info">スローされている例外に関するシリアル化済みオブジェクト データを保持している System.Runtime.Serialization.SerializationInfo。</param>
/// <exception cref="System.Runtime.Serialization.SerializationException">クラス名が null であるか、または System.Exception.HResult が 0 です。</exception>
/// <exception cref="System.ArgumentNullException">info パラメータが null です。</exception>
protected SessionItemNotFoundException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
this._key = info.GetString("key");
}
#endregion
#region Fields
/// <summary>
/// 例外の原因となった項目キーを取得します。
/// </summary>
private readonly string _key;
#endregion
#region Properties
/// <summary>
/// 例外の原因となった項目キーを取得します。
/// </summary>
public string Key
{
get
{
return this._key;
}
}
#endregion
#region Methods
/// <summary>
/// パラメータ名と追加の例外情報を使用して System.Runtime.Serialization.SerializationInfo オブジェクトを設定します。
/// </summary>
/// <param name="context">転送元または転送先に関するコンテキスト情報。</param>
/// <param name="info">シリアル化されたオブジェクト データを保持するオブジェクト。</param>
/// <exception cref="System.ArgumentNullException">info オブジェクトが null 参照 (Visual Basic の場合は Nothing) です。</exception>
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info == null)
{
throw new ArgumentNullException("info");
}
base.GetObjectData(info, context);
// 追加の例外情報がある場合、ここでパラメータ名と例外情報を info に追加します。
info.AddValue("key", this._key, typeof(string));
}
#endregion
}
}
テストケース (NUnit 2.4.8)
#pragma warning disable 1591
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.Hosting;
using System.Web.SessionState;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
namespace MoroMoro.Enterprise.Web
{
[TestFixture]
[EditorBrowsable(EditorBrowsableState.Never)]
public class SessionStateAdapterTest
{
#region HttpContext Dummy
private void BuildHttpContext(bool sessionStateIsReadOnly, SimpleSessionStateItemCollection collection)
{
SimpleWorkerRequest workerRequest = new SimpleWorkerRequest("", "", "", "", null);
HttpContext.Current = new HttpContext(workerRequest);
if (collection != null)
{
HttpSessionStateContainer container =
new HttpSessionStateContainer("", collection, null, 1, true, HttpCookieMode.UseCookies, SessionStateMode.Custom, sessionStateIsReadOnly);
SessionStateUtility.AddHttpSessionStateToContext(HttpContext.Current, container);
}
}
private void BuildInvalidHttpContext()
{
SimpleWorkerRequest workerRequest = new SimpleWorkerRequest("", "", "", "", null);
HttpContext.Current = new HttpContext(workerRequest);
HttpContext.Current.Items.Add("AspSession", new object());
}
private sealed class SimpleSessionStateItemCollection : NameObjectCollectionBase, ISessionStateItemCollection
{
#region ISessionStateItemCollection メンバ
public void Clear()
{
base.BaseClear();
}
public bool Dirty
{
get;
set;
}
public NameObjectCollectionBase.KeysCollection Keys
{
get
{
return base.Keys;
}
}
public void Remove(string name)
{
base.BaseRemove(name);
}
public void RemoveAt(int index)
{
base.BaseRemoveAt(index);
}
public object this[int index]
{
get
{
return base.BaseGet(index);
}
set
{
base.BaseSet(index, value);
}
}
public object this[string name]
{
get
{
return base.BaseGet(name);
}
set
{
base.BaseSet(name, value);
}
}
#endregion
#region ICollection メンバ
public void CopyTo(Array array, int index)
{
((ICollection)this).CopyTo(array, index);
}
public int Count
{
get
{
return base.Count;
}
}
public bool IsSynchronized
{
get
{
return ((ICollection)this).IsSynchronized;
}
}
public object SyncRoot
{
get
{
return ((ICollection)this).SyncRoot;
}
}
#endregion
#region IEnumerable メンバ
public IEnumerator GetEnumerator()
{
return base.GetEnumerator();
}
#endregion
}
#endregion
[TearDown]
public void TearDown()
{
HttpContext.Current = null;
}
[Test]
public void 現在のHttpContextのインスタンスが再生成されるとSessionStateAdapterも再生成されること()
{
SimpleSessionStateItemCollection collection = new SimpleSessionStateItemCollection();
BuildHttpContext(false, collection);
SessionStateAdapter<object> adapter1 = SessionStateAdapter<object>.Current;
Assert.That(adapter1, Is.EqualTo(SessionStateAdapter<object>.Current));
BuildHttpContext(false, collection);
SessionStateAdapter<object> adapter2 = SessionStateAdapter<object>.Current;
Assert.That(adapter1, Is.Not.EqualTo(adapter2));
}
[Test]
public void 現在のHttpContextのインスタンスがNullの場合エラーになること()
{
bool wasThrown = false;
try
{
SessionStateAdapter<object> adapter1 = SessionStateAdapter<object>.Current;
}
catch (InvalidOperationException)
{
wasThrown = true;
}
Assert.That(wasThrown, Is.True);
}
[Test]
public void 現在のセッションステートがNullの場合エラーになること()
{
BuildHttpContext(false, null);
bool wasThrown = false;
try
{
SessionStateAdapter<object> adapter1 = SessionStateAdapter<object>.Current;
}
catch (InvalidOperationException)
{
wasThrown = true;
}
Assert.That(wasThrown, Is.True);
}
[Test]
public void 現在のセッションステートが正常に取得できない場合エラーになること()
{
BuildInvalidHttpContext();
bool wasThrown = false;
try
{
SessionStateAdapter<object> adapter1 = SessionStateAdapter<object>.Current;
}
catch (InvalidOperationException)
{
wasThrown = true;
}
Assert.That(wasThrown, Is.True);
}
[Test]
public void 項目を設定及び取得できること()
{
BuildHttpContext(false, new SimpleSessionStateItemCollection());
SessionStateAdapter<object> adapter = SessionStateAdapter<object>.Current;
object obj = new object();
adapter.SetItem(obj);
Assert.That(adapter.GetItem(), Is.EqualTo(obj));
}
[Test]
public void 項目を削除できること()
{
BuildHttpContext(false, new SimpleSessionStateItemCollection());
SessionStateAdapter<object> adapter = SessionStateAdapter<object>.Current;
adapter.SetItem(new object());
adapter.RemoveItem();
Assert.That(adapter.ItemIsExists(), Is.False);
}
[Test]
public void 項目の有無を確認できること()
{
BuildHttpContext(false, new SimpleSessionStateItemCollection());
SessionStateAdapter<object> adapter = SessionStateAdapter<object>.Current;
Assert.That(adapter.ItemIsExists(), Is.False);
adapter.SetItem(new object());
Assert.That(adapter.ItemIsExists(), Is.True);
adapter.RemoveItem();
Assert.That(adapter.ItemIsExists(), Is.False);
}
[Test]
public void 項目が存在しない場合項目の取得時にエラーになること()
{
BuildHttpContext(false, new SimpleSessionStateItemCollection());
SessionStateAdapter<object> adapter = SessionStateAdapter<object>.Current;
bool wasThrown = false;
try
{
adapter.GetItem();
}
catch (SessionItemNotFoundException)
{
wasThrown = true;
}
Assert.That(wasThrown, Is.True);
}
[Test]
public void 項目にNullを設定しようとするとエラーになること()
{
BuildHttpContext(false, new SimpleSessionStateItemCollection());
SessionStateAdapter<object> adapter = SessionStateAdapter<object>.Current;
bool wasThrown = false;
try
{
adapter.SetItem(null);
}
catch (ArgumentNullException)
{
wasThrown = true;
}
Assert.That(wasThrown, Is.True);
}
[Test]
public void セッションステートが読み取り専用の場合項目の設定時にエラーになること()
{
BuildHttpContext(true, new SimpleSessionStateItemCollection());
SessionStateAdapter<object> adapter = SessionStateAdapter<object>.Current;
bool wasThrown = false;
try
{
adapter.SetItem(new object());
}
catch (InvalidOperationException)
{
wasThrown = true;
}
Assert.That(wasThrown, Is.True);
}
[Test]
public void セッションステートが読み取り専用の場合項目の削除時にエラーになること()
{
SimpleSessionStateItemCollection collection = new SimpleSessionStateItemCollection();
BuildHttpContext(false, collection);
SessionStateAdapter<object> adapter1 = SessionStateAdapter<object>.Current;
adapter1.SetItem(new object());
BuildHttpContext(true, collection);
SessionStateAdapter<object> adapter2 = SessionStateAdapter<object>.Current;
bool wasThrown = false;
try
{
adapter2.RemoveItem();
}
catch (InvalidOperationException)
{
wasThrown = true;
}
Assert.That(wasThrown, Is.True);
}
}
}
#pragma warning restore 1591
InProc の場合はそもそもシリアル化が行われていないので大丈夫。
[再現方法]
aspx に次の二つのコントロールを配置。
<asp:Label runat ="server" ID="Label1" />
<asp:Button runat="server" ID="Button1" Text="Button1" onclick="Button1_Click" />
コードビハインドに次のコードを記述。
protected void Page_Load(object sender, EventArgs e)
{
if (!this.IsPostBack)
{
object o = new object();
HttpContext.Current.Session["o1"] = o;
HttpContext.Current.Session["o2"] = o;
}
}
protected void Button1_Click(object sender, EventArgs e)
{
object o1 = HttpContext.Current.Session["o1"];
object o2 = HttpContext.Current.Session["o2"];
this.Label1.Text = object.ReferenceEquals(o1, o2).ToString();
}
Button1 をクリックすると、セッション状態のモードが InProc なら True と表示されるけど、StateServer だと False と表示される。
もちろん、InProc のように動作して欲しい。
リダイレクトをキャンセルするには、レスポンスの出力バッファをクリアして、HTTP ステータスコードに OK ( 200 ) を設定します。
HttpContext context = HttpContext.Current;
context.Response.ClearContent();
context.Response.RedirectLocation = null;
const int HTTP_STATUS_OK = 200;
context.Response.StatusCode = HTTP_STATUS_OK;
RedirectLocation プロパティは別にいじらなくてもいいんですが、どうせだから null にしちゃってます。
[ 参考 ]
HttpResponse.StatusCode プロパティ (System.Web)
HTTP Status Codes (Windows)
例えば "http://hoge:8000/fuga/" に配置された ASP.NET アプリケーション内では、"~/Piyo.aspx" は "/fuga/Piyo.aspx" に変換されます。
以下のコードは、現在のディレクトリ階層に関わらずルートディレクトリの Piyo.aspx へリダイレクトします。
HttpContext.Current.Response.Redirect("~/Piyo.aspx");
ただし、この "~" は、 ASP.NET が解釈して変換しているだけであり、ブラウザが解釈できるわけではありません。ブラウザに "~" から始まるパスが渡されたところでブラウザは単に現在のアドレスを基準とした相対パスとして解釈してしまいます。
だから、ASP.NET がパスを変換してくれないものに対して "~" は使えません。
例えば、ASPX ファイルに記述された [ a ] タグは、基本的にそのままクライアントに送られますので、
<a href="~/Foo.aspx">Foo</a>
なんて書いても、ブラウザにはこのリンクが "http://hoge:8000/fuga/~/Foo.aspx" と解釈されてしまいます。こういう場合は大人しく相対パスで記述しましょう。
余談ですが、ASP.NET アプリケーションのルートディレクトリは HttpRuntime.AppDomainAppVirtualPath プロパティ (System.Web) で取得することができます。最初の例で言えば、このプロパティから "/fuga" という値が取得できます。
※ 注意 ※
モバイルページでセッションステートを Cookie なしで使用する場合は問題が発生する可能性があるそうです。詳しくは [ 参考 ] のリンク先ページを読んでください。
[ 参考 ]
VirtualPathUtility クラス (System.Web)
ASP.NET Web サイトのパス
[ 追記 ]
// 2009/06/29
仮想パスから絶対パスへの変換には VirtualPathUtility.ToAbsolute メソッド (String) (System.Web) が利用できます。
仮想パスから相対パスへの変換には Control.ResolveClientUrl メソッド (System.Web.UI) が利用できます。
CodeZine に「ASP.NETのセッションをタイプセーフに取り扱うクラスの作成」という記事が載ってましたが、この方法は僕的にちょっと馴染めなかったもので。
以下、現時点での考えをメモしときます。
セッションステートに保存するオブジェクトは、セッションステートで管理されることを目的として定義されたクラスだけにします。
だから、セッションステートの項目キーはクラスの完全修飾名でいいと考えています。
文字列とかドメインオブジェクト、データセットなんかを直接保存したりはしません。
まず、クラスの完全修飾名を使ってセッションステートを利用するための補助を行う静的クラスを定義しておきます。
項目が見つからなかったり、セッションステートが使えない時 ( IRequiresSessionState インターフェイスを実装していない HTTP ハンドラからの呼び出し等 ) には例外を発生させるようにもします。
SessionStateAdapter(T) クラス
using System;
using System.Web;
using System.Web.SessionState;
/// <summary>
/// 型の完全修飾名を項目キーとしてセッションステートを利用するアダプターです。
/// </summary>
/// <typeparam name="T">セッションステートに保存するインスタンスの型。この型の完全修飾名がセッションステートの項目キーになります。</typeparam>
public static class SessionStateAdapter<T>
{
/// <summary>
/// ジェネリックパラメータ T のインスタンスを現在のセッションステートから取得します。
/// </summary>
/// <returns>ジェネリックパラメータ T のインスタンス</returns>
/// <exception cref="SessionItemNotFoundException">指定した項目がセッションステート内に存在しません。</exception>
/// <exception cref="System.InvalidOperationException">セッションステートが利用できません。</exception>
public static T GetItem()
{
HttpSessionState currentSession = SessionStateAdapter<T>.GetSessionState();
object result = currentSession[typeof(T).FullName];
if ((result == null) || !(result is T))
{
throw new SessionItemNotFoundException(typeof(T).FullName);
}
return (T)result;
}
/// <summary>
/// ジェネリックパラメータ T のインスタンスを現在のセッションステートに設定します。
/// </summary>
/// <param name="target">ジェネリックパラメータ T のインスタンス。</param>
/// <exception cref="System.InvalidOperationException">セッションステートが利用できません。</exception>
/// <exception cref="System.InvalidOperationException">セッションステートが読み取り専用です。</exception>
/// <exception cref="System.ArgumentNullException">引数 target が null です。</exception>
public static void SetItem(T target)
{
if (target == null)
{
throw new ArgumentNullException("target");
}
HttpSessionState currentSession = SessionStateAdapter<T>.GetSessionState();
if (currentSession.IsReadOnly)
{
throw SessionStateAdapter<T>.CreateExceptionForSessionStateIsReadOnly();
}
currentSession[typeof(T).FullName] = target;
}
/// <summary>
/// ジェネリックパラメータ T のインスタンスを現在のセッションステートから削除します。
/// </summary>
/// <exception cref="System.InvalidOperationException">セッションステートが利用できません。</exception>
/// <exception cref="System.InvalidOperationException">セッションステートが読み取り専用です。</exception>
public static void RemoveItem()
{
HttpSessionState currentSession = SessionStateAdapter<T>.GetSessionState();
if (currentSession.IsReadOnly)
{
throw SessionStateAdapter<T>.CreateExceptionForSessionStateIsReadOnly();
}
currentSession.Remove(typeof(T).FullName);
}
/// <summary>
/// ジェネリックパラメータ T のインスタンスが現在のセッションステートに存在するかどうかを表す値を返します。
/// </summary>
/// <returns>ジェネリックパラメータ T のインスタンスが現在のセッションステートに存在するかどうかを表す値。</returns>
/// <exception cref="System.InvalidOperationException">セッションステートが利用できません。</exception>
public static bool ItemIsExists()
{
HttpSessionState currentSession = SessionStateAdapter<T>.GetSessionState();
return (currentSession[typeof(T).FullName] is T);
}
/// <summary>
/// 現在のセッションステートを取得します。
/// </summary>
/// <returns>現在のセッションステート。</returns>
/// <exception cref="System.InvalidOperationException">セッションステートが利用できません。</exception>
private static HttpSessionState GetSessionState()
{
HttpContext currentContext = HttpContext.Current;
HttpSessionState currentSession;
try
{
currentSession = currentContext.Session;
}
catch (Exception ex)
{
throw new InvalidOperationException("セッションステートが利用できません。", ex);
}
if (currentSession == null)
{
throw new InvalidOperationException("セッションステートが利用できません。");
}
return currentSession;
}
/// <summary>
/// セッションステートが読み取り専用であることを示す例外を生成します。
/// </summary>
/// <returns>セッションステートが読み取り専用であることを示す例外。</returns>
private static Exception CreateExceptionForSessionStateIsReadOnly()
{
return new InvalidOperationException("セッションステートが読み取り専用です。");
}
}
セッションステート内に項目が見つからなかった場合にスローされる SessionItemNotFoundException クラスは以下のコードになります。
追加情報として項目のキーを持つことができるようにしてます。
SessionItemNotFoundException クラス
using System;
using System.Runtime.Serialization;
using System.Security.Permissions;
/// <summary>
/// セッションステート内に特定の項目が見つからないことを表す例外。
/// </summary>
[Serializable]
public class SessionItemNotFoundException : Exception
{
#region Constructors
/// <summary>
/// SessionItemNotFoundException クラスの新しいインスタンスを初期化します。
/// </summary>
public SessionItemNotFoundException()
: this(null)
{
}
/// <summary>
/// この例外の原因である項目のキーを指定して、SessionItemNotFoundException クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="key">例外の原因となった項目キー。</param>
public SessionItemNotFoundException(string key)
: this(key, "指定した項目がセッションステート内に存在しません。")
{
}
/// <summary>
/// エラー メッセージ、およびこの例外の原因である項目のキーを指定して、SessionItemNotFoundException クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="key">例外の原因となった項目キー。</param>
/// <param name="message">エラーを説明するメッセージ。</param>
public SessionItemNotFoundException(string key, string message)
: this(key, message, null)
{
}
/// <summary>
/// エラー メッセージ、項目キー、およびこの例外の原因である内部例外への参照を使用して、SessionItemNotFoundException クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="key">例外の原因となった項目キー。</param>
/// <param name="message">例外の原因を説明するエラー メッセージ。</param>
/// <param name="innerException">現在の例外の原因である例外。内部例外が指定されていない場合は、null 参照 (Visual Basic の場合は Nothing)。</param>
public SessionItemNotFoundException(string key, string message, Exception innerException)
: base(message, innerException)
{
this._key = key;
}
/// <summary>
/// シリアル化したデータを使用して、SessionItemNotFoundException クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="context">転送元または転送先に関するコンテキスト情報を含んでいる System.Runtime.Serialization.StreamingContext。</param>
/// <param name="info">スローされている例外に関するシリアル化済みオブジェクト データを保持している System.Runtime.Serialization.SerializationInfo。</param>
/// <exception cref="System.Runtime.Serialization.SerializationException">クラス名が null であるか、または System.Exception.HResult が 0 です。</exception>
/// <exception cref="System.ArgumentNullException">info パラメータが null です。</exception>
protected SessionItemNotFoundException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
this._key = info.GetString("key");
}
#endregion
#region Fields
/// <summary>
/// 例外の原因となった項目キーを取得します。
/// </summary>
private readonly string _key;
#endregion
#region Properties
/// <summary>
/// 例外の原因となった項目キーを取得します。
/// </summary>
public string Key
{
get
{
return this._key;
}
}
#endregion
#region Methods
/// <summary>
/// パラメータ名と追加の例外情報を使用して System.Runtime.Serialization.SerializationInfo オブジェクトを設定します。
/// </summary>
/// <param name="context">転送元または転送先に関するコンテキスト情報。</param>
/// <param name="info">シリアル化されたオブジェクト データを保持するオブジェクト。</param>
/// <exception cref="System.ArgumentNullException">info オブジェクトが null 参照 (Visual Basic の場合は Nothing) です。</exception>
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info == null)
{
throw new ArgumentNullException("info");
}
base.GetObjectData(info, context);
// 追加の例外情報がある場合、ここでパラメータ名と例外情報を info に追加します。
info.AddValue("key", this._key, typeof(string));
}
#endregion
}
ここからが、肝心の「セッションステートで管理されることを目的として定義されたクラス」になります。
業務アプリケーションは大抵の場合、情報の検索だとか、情報の登録、物品の貸出等の複数の機能を持っています。
ここでは、これら一つ一つを "業務" と呼びます。(こうやって呼ぶのって一般的なのかな?)
僕は、画面にビジネスロジックをできるだけ持ち込まないために、ファサードパターンを適用して各業務毎にクラスを用意します。
例えば、ほげ業務には HogeBusiness クラス、ふが業務には FugaBusiness クラスを用意します。
そして、この業務クラスをセッションに保存しておき、複数画面に渡って業務を行います。
そこで、BusinessManager という名前の「セッションステートで管理されることを目的として定義されたクラス」を一つ用意し、そのクラスのフィールドで全ての業務を管理します。
BusinessManager は、Current という読み取り専用のスタティックプロパティを持ちます。
この Current プロパティは、セッションステートを使用してシングルトンパターンのような動きをします。
BusinessManager クラス
using System;
/// <summary>
/// 業務を管理します。
/// </summary>
[Serializable]
public class BusinessManager
{
#region Static Members
#region public static BusinessManager Current
/// <summary>
/// 現在のセッションにて管理されている BusinessManager オブジェクトを取得します。
/// </summary>
/// <exception cref="System.InvalidOperationException">セッションステートが利用できません。</exception>
/// <exception cref="System.InvalidOperationException">セッションステートが読み取り専用です。</exception>
public static BusinessManager Current
{
get
{
BusinessManager currentBusinessManager;
bool isExists = SessionStateAdapter<BusinessManager>.ItemIsExists();
if (isExists)
{
currentBusinessManager = SessionStateAdapter<BusinessManager>.GetItem();
}
else
{
currentBusinessManager = new BusinessManager();
SessionStateAdapter<BusinessManager>.SetItem(currentBusinessManager);
}
return currentBusinessManager;
}
}
#endregion
#endregion
#region Fields
#region private HogeBusiness _hogeBusiness
/// <summary>
/// ほげ業務を取得または設定します。
/// </summary>
private HogeBusiness _hogeBusiness;
#endregion
#region private FugaBusiness _fugaBusiness
/// <summary>
/// ふが業務を取得または設定します。
/// </summary>
private FugaBusiness _fugaBusiness;
#endregion
#endregion
#region Properties
#region public HogeBusiness HogeBusiness
/// <summary>
/// ほげ業務を取得します。
/// </summary>
/// <exception cref="System.InvalidOperationException">ほげ業務が開始されていません。</exception>
public HogeBusiness HogeBusiness
{
get
{
if (this._hogeBusiness == null)
{
throw this.CreateExceptionForNotStartedBusiness("ほげ");
}
return this._hogeBusiness;
}
}
#endregion
#region public FugaBusiness FugaBusiness
/// <summary>
/// ふが業務を取得します。
/// </summary>
/// <exception cref="System.InvalidOperationException">ふが業務が開始されていません。</exception>
public FugaBusiness FugaBusiness
{
get
{
if (this._fugaBusiness == null)
{
throw this.CreateExceptionForNotStartedBusiness("ふが");
}
return this._fugaBusiness;
}
}
#endregion
#endregion
#region Constructors
#region private BusinessManager()
/// <summary>
/// BusinessManager クラスの新しいインスタンスを初期化します。
/// </summary>
private BusinessManager()
{
this.InitializeFields();
}
#endregion
#endregion
#region Methods
#region public void StartHogeBusiness()
/// <summary>
/// ほげ業務を開始します。
/// </summary>
public void StartHogeBusiness()
{
this._hogeBusiness = new HogeBusiness();
}
#endregion
#region public void EndHogeBusiness()
/// <summary>
/// ほげ業務を終了します。
/// </summary>
/// <returns></returns>
public void EndHogeBusiness()
{
this._hogeBusiness = null;
}
#endregion
#region public void StartFugaBusiness()
/// <summary>
/// ふが業務を開始します。
/// </summary>
public void StartFugaBusiness()
{
this._fugaBusiness = new FugaBusiness();
}
#endregion
#region public void EndFugaBusiness()
/// <summary>
/// ふが業務を終了します。
/// </summary>
/// <returns></returns>
public void EndFugaBusiness()
{
this._fugaBusiness = null;
}
#endregion
#region public void EndAllBusiness()
/// <summary>
/// 全ての業務を終了します。
/// </summary>
public void EndAllBusiness()
{
this.InitializeFields();
}
#endregion
#region private void InitializeFields()
/// <summary>
/// フィールドを初期化します。
/// </summary>
private void InitializeFields()
{
this._hogeBusiness = null;
this._fugaBusiness = null;
}
#endregion
#region private void CreateExceptionForNotStartedBusiness(string businessName)
/// <summary>
/// 業務が開始されていないことを表す例外を生成します。
/// </summary>
/// <param name="businessName">業務名。</param>
private Exception CreateExceptionForNotStartedBusiness(string businessName)
{
string message = string.Format("{0}業務は開始されていません。", businessName);
return new InvalidOperationException(message);
}
#endregion
#endregion
}
セッションには BusinessManager のインスタンスが一つだけ格納され、そのインスタンスの各フィールドで業務クラスが管理されます。
セッションで扱う情報は業務クラス以外にもありますので、それらにはまた別の「セッションステートで管理されることを目的として定義されたクラス」を用意します。
それらもまた、Current という読み取り専用のスタティックプロパティを定義し、セッションステートを使用したシングルトンパターンのように実装します。
とりあえず、今の考えはこんな感じです。
他にも、「セッションステートで管理されることを目的として定義されたクラス」を各業務ごとに用意するという方法も考えていて、それはまた少し異なる仕組みとなります ( シングルトンパターンではなくなります ) 。
// コード修正履歴
2007/09/17
・セッションステートが読み取り専用の時は、SessionStateAdapter<T>.SetItem(T) メソッド、SessionStateAdapter<T>.RemoveItem(T) メソッドが例外をスローするよう修正。
2008/12/14
・NotFoundSessionItemException クラスの名称を SessionItemNotFoundException に変更。
セッションの格納方法別に挙動をまとめてみました。 ( SQL Server モードは未検証ですが。。。 )
[ In Process モード ]
未処理例外発生時の HTTP リクエスト処理より以前にセッションデータが格納されていた場合、セッションはクリアされません。
未処理例外発生時の HTTP リクエスト処理より以前にセッションデータが格納されていなかった場合、未処理例外発生時の HTTP リクエスト処理中に格納されたセッションは、未処理例外発生時の HTTP リクエスト処理が終了した時点でクリアされます。
[ State Server モード ]
未処理例外発生時の HTTP リクエスト処理中に格納されたセッションデータは、未処理例外発生時の HTTP リクエスト処理が終了すると無効化され、未処理例外発生時の HTTP リクエスト処理以前の状態が復元されます。 ( 新しくキーを生成した場合は削除され、既存のキーに対してなんらかの処理を行った場合は、その処理が全て取り消される。 )
[ SQL Server モード ]
検証してません。 ( State Server モードと同じ動作な予感。 )
どのみち、Global.asax の Error イベントハンドラでリダイレクトすればセッションは保持されます。Global.asax で ClearError メソッドを呼び出すことで例外が「処理」されるため、上記動作が作動しないということですね。
# この検証をしている時に ClearError メソッドを呼び忘れて検証していたため、「Global.asax でリダイレクトさせても同様の現象が発生する!」って内容の記事を30分程公開していたのは内緒 ^^; もしその時に記事を読んでしまった方がいらっしゃいましたら、本当にすみませんでした m( _ _;)m
Web.config で上記のようにして defaultRedirect を設定した場合、未処理の例外が発生したら自作エラーページである Error.aspx にリダイレクトされます。
僕の場合は、今まで Global.asax の Error イベントハンドラ ( Application_Error メソッド ) で自作エラーページへのリダイレクト処理を行っていました。なので気づかなかったんですが、Web.config に設定した defaultRedirect によってエラーページに遷移した場合、セッションがクリアされます。
Global.asax の Error イベントハンドラでリダイレクトさせるより defaultRedirect でリダイレクトさせた方がいいんじゃないか?と疑問に思って ( ※ ) 検証していたら、これに気づきました。いや、気づくまでしばらくハマってました。
今後も Global.asax の Error イベントハンドラでリダイレクトしていこうと心に決めましたね。
※ defaultRedirect でリダイレクトさせる場合、mode 属性に RemoteOnly 指定するだけで簡単に開発者向けのエラーページを表示するように切り替えられるからです。
[ 関連記事 ]
未処理例外発生時のセッションの動作
このアカウントはファイル・ディレクトリへのアクセス権などが制限されていて、ログファイルを出力することすらできません。
これを解決するには、対象のファイル・ディレクトリへ適切な権限を付加する必要があります。
インストーラで、この権限付加の処理を行いたい場合、インストーラのカスタム動作でこれを行います。
以下に例を示します。
//using System;
//using System.Configuration.Install;
//using System.IO;
//using System.Security.AccessControl;
[RunInstaller(true)]
public partial class CustomInstaller : Installer
{
public CustomInstaller()
{
InitializeComponent();
}
public override void Install(System.Collections.IDictionary stateSaver)
{
string assemblyPathName = this.Context.Parameters["assemblypath"];
int fileDivisionIndex = assemblyPathName.LastIndexOf('\\');
string installDirectoryPathName = assemblyPathName.Remove(fileDivisionIndex);
DirectoryInfo installDirectoryInfo = new DirectoryInfo(installDirectoryPathName);
// インストール先ディレクトリの ACL ( アクセス制御リスト ) を取得
DirectorySecurity installDirectoryACL = installDirectoryInfo.GetAccessControl();
// Network Service アカウントの書き込み権限を表すオブジェクトを生成
FileSystemAccessRule writingAuthorityOfWorkerProcess = new FileSystemAccessRule("NT AUTHORITY\\NETWORK SERVICE", FileSystemRights.Write, AccessControlType.Allow);
// ACL に、Network Service アカウントの書き込み権限を追加する
installDirectoryACL.AddAccessRule(writingAuthorityOfWorkerProcess);
// インストール先ディレクトリの ACL を設定
installDirectoryInfo.SetAccessControl(installDirectoryACL);
base.Install(stateSaver);
}
}
この例では、インストール先ディレクトリに対して、Network Serviceアカウントが書き込みを行うための権限を付加しています。
これにより、Web アプリがインストール先ディレクトリ内にログファイルを出力することが可能となります。
インストーラ、カスタム動作自体の説明はここでは省略します。これについては以下のチュートリアルを参考にしてください。
Windows インストーラでの配置に関するチュートリアル