

WF をちょっと試してみました。
上の画像は、ソースとなるファイルまたはディレクトリを Zip アーカイブを生成し、バイナリデータとして取得するための処理を、WF で実装した際のデザイナ画面です。
下の画像は、ソースパスの指定、保存先の指定、Zip アーカイブの生成・保存という、アプリケーションの一連の流れを、WF で実装した際のデザイナ画面です。Zip アーカイブの生成処理には、もちろん上のワークフローを使用しています。
作ってみた感想としては・・・ めんどくさい ですね(笑) いや、勉強しながら作ってたから手間取ったってのもあるんですけどね。 ( おかげで、下のワークフローは、適当な作りになってしまった orz )
WF はシステムワークフロー、ヒューマンワークフローの両方に対応できるらしいです。実際、その辺は理解できました。WF は、適用しようと思えばかなり色々な所に適用できるということもわかりました。フローがデザイナによって視覚化されるのは、恐らく最大の利点ですね。
で、使いどころはどこになるんでしょうかね?今回くらいの簡単な処理でワークフローを使っても、あまり意味はないように思いました。もうちょっと大きなものや、永続化を必要とするようなものでは変わってくるのかもしれませんが、やってみないと今一イメージがわかないです。
ネット上で活躍している人達の記事を読むと WF はかなり高く評価されてますが、僕にはまだまだ WF の本質が見えません。
とりあえず、↓ の本とか読めば WF というものがある程度見えてくるのかな。でもちょっと高いな。。。
エッセンシャルWF : Windows Workflow Foundation (Programmer’sSELECTION Microsoft.net)
Visual Basic でなんとなくイベントとデリゲート その3。 - イベントに関連することをちょっとだけ追記。 -
とりこびとさんの記事を読んでいて、そういえば VB のイベント定義って C# と違うんだよなぁと思い出したことがきっかけで、久々に VB のコードを書いてみました。そしたら、ちょっとした問題に気づきました。
とりあえず、C# でのイベント定義から。
C# でイベントを定義する場合、通常は以下のように記述します。
public class Class1
{
private int _field1;
public int Property1
{
get
{
return this._field1;
}
set
{
if ((this._field1 != value) && (this.Event1 != null))
{
this.Event1(this, EventArgs.Empty);
}
this._field1 = value;
}
}
public event EventHandler Event1;
}
この書き方は、実はイベント定義のシンタックスシュガーであり、以下のように add アクセサと remove アクセサを自分で実装することも可能です。
public class Class1
{
private int _field1;
public int Property1
{
get
{
return this._field1;
}
set
{
if ((this._field1 != value) && (this._event1 != null))
{
this._event1(this, EventArgs.Empty);
}
this._field1 = value;
}
}
private EventHandler _event1;
public event Eventhandler Event1
{
add
{
this._event1 += value;
}
remove
{
this._event1 -= value;
}
}
}
Visual Basic の場合、以下のように書くようです。
Public Class Class1
Private _field1 As Integer
Public Property Property1() As Integer
Get
Return Me._field1
End Get
Set(ByVal value As Integer)
If (Not Me._field1 = value) Then
RaiseEvent Event1(Me, EventArgs.Empty)
End If
Me._field1 = value
End Set
End Property
Private _event1 As EventHandler
Public Custom Event Event1 As EventHandler
AddHandler(ByVal value As EventHandler)
Me._event1 = CType(System.Delegate.Combine(Me._event1, value), EventHandler)
End AddHandler
RemoveHandler(ByVal value As EventHandler)
Me._event1 = CType(System.Delegate.Remove(Me._event1, value), EventHandler)
End RemoveHandler
RaiseEvent(ByVal sender As Object, ByVal e As EventArgs)
If (Not Me._event1 Is Nothing) Then
Me._event1(sender, e)
End If
End RaiseEvent
End Event
End Class
C# では add アクセサと remove アクセサの2つを定義すれば良かったのですが、VB の場合、AddHandler アクセサと RemoveHandler アクセサの他に、RaiseEvent アクセサというものも定義しなければなりません。
アクセサを自前で実装せずにイベントを定義した場合は、コンパイルされた MSIL に RaiseEvent アクセサに相当するコード ( MSIL では、RaiseEvent アクセサは fire アクセサとなる模様 ) は出力されません。しかし、自前で実装する場合はなぜか必須です。
なぜ必須とするのか、考えてみました。
VB ではイベントを発生させるのに RaiseEvent ステートメントを使用します。もし、自前で実装したイベントに RaiseEvent アクセサがなかった場合、RaiseEvent ステートメントでのイベント発生ができなくなります。代わりに C# のようにデリゲートの Invoke メソッドを呼び出せばいいだけの話なのですが、それを嫌ったのかなぁと。
で、これ実は結構問題なんじゃないかと思うんです。イベントのアクセサを自前で実装するのって、他のオブジェクトのイベントへ委譲する場合が主だと思うんです。 ( 僕はこれ以外の理由でわざわざ自前実装したことがありません。 )
例えば、ユーザーコントロールを開発するとします。このユーザーコントロールには Enter ボタンを配置します。
ユーザーコントロールの外部からは Enter ボタンにアクセスできないようにしたい、でも Enter ボタンのクリックイベントは外部に公開したい。そんな時、ユーザーコントロールに EnterClick イベントを定義してイベントのアクセサを自前で実装するわけです。
AddHandler アクセサは Enter ボタンのクリックイベントにハンドラを登録します。
RemoveHandler アクセサは Enter ボタンのクリックイベントからハンドラを削除します。
では、RaiseEvent アクセサは?
ボタンのクリックイベントの公開が目的なのだから、ユーザーコントロール内のコードでイベント発生なんてさせませんし、できません。でも RaiseEvent アクセサを定義しないといけません。恐らく、何の処理もしない、あるいは例外を発生させるという実装になるかと思います。そして、ユーザーコントロール内のコードに
RaiseEvent EnterClick(Me, EventArgs.Empty)
って書くことは許されてしまうわけです。これをコンパイル時に検出することはできません。
ちなみに、RaiseEvent アクセサに相当するものは C# にはありません。
個人的には、あってもなくてもどっちでもいいって感じですが、これを必ず定義しなきゃいけないってのはよろしくないよなぁと思いました。
[ 参考 ]
Event ステートメント ( Visual Basic )
リダイレクトをキャンセルするには、レスポンスの出力バッファをクリアして、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)
ZipPackage (.net framework 3.0, System.IO.Package) - いろいろ備忘録日記
.NET Framework 3.0 から、新しく ZipPackage クラス (System.IO.Packaging) が追加されたそうです。このクラスを使用することで、Zip アーカイブを生成することができます。
( ただし、このクラスの目的は Open Packaging Conventions (OPC) によるパッケージ化であるため、ZIP アーカイブ内に "[Content_Types].xml" というファイルが必ず含まれてしまうようです。 )
このクラスを使用して、Zip アーカイブを生成する PowerShell 関数を書いてみました。
[ パラメータ ]
sourcePath
対象ファイルまたは対象ディレクトリのパス。
[ 戻り値 ]
Zip アーカイブのバイナリデータを表す byte 配列。
[ Create-ZipArchive 関数 ]
function Create-ZipArchive
{
param([string]$sourcePath)
[System.Reflection.Assembly]::LoadWithPartialName("WindowsBase") | Out-Null;
$source = Get-Item $sourcePath;
$sourceFiles =
&{
if ($source -is [System.IO.FileInfo])
{
return @($source);
}
elseif ($source -is [System.IO.DirectoryInfo])
{
return $source.GetFiles("*", "AllDirectories");
}
else
{
throw (New-Object "System.IO.FileNotFoundException" @($sourcePath));
}
};
$packageStream = New-Object "System.IO.MemoryStream";
$package = [System.IO.Packaging.Package]::Open($packageStream, "CreateNew");
$baseUri = New-Object "System.Uri" @($sourcePath);
foreach ($sourceFile in $sourceFiles)
{
$sourceFileUri = New-Object "System.Uri" @($sourceFile.FullName);
$sourceFileRelativeUri = $baseUri.MakeRelativeUri($sourceFileUri);
$partUri = [System.IO.Packaging.PackUriHelper]::CreatePartUri($sourceFileRelativeUri);
$part = $package.CreatePart($partUri, "", "Maximum");
$sourceFileData = [System.IO.File]::ReadAllBytes($sourceFile.FullName);
$partStream = $part.GetStream();
$partStream.Write($sourceFileData, 0, $sourceFileData.Length);
}
$package.Close();
$result = $packageStream.ToArray();
$packageStream.Dispose();
return $result;
trap
{
if ($() -ne $package)
{
$package.Close();
}
if ($() -ne $packageStream)
{
$packageStream.Dispose();
}
}
}
[ 使用例 ]
$zipData = Create-ZipArchive "C:\Work\Directory1";
[System.IO.File]::WriteAllBytes("C:\Work\Directory1.zip", $zipData);
[ 補足 ]
ZipPackage クラスは、コンストラクタを公開していません。代わりに、Package クラス (System.IO.Packaging) の Open メソッド を使用して ZipPackage クラスのインスタンスを取得します。
[ 参考 ]
System.IO.Packaging 名前空間
OPC: データのパッケージ化のための新しい標準 -- MSDN Magazine, August 2007
【 ダウンロード 】
自作の PowerShell 関数は、以下の記事からまとめてダウンロードできます。
YokoKen.PowerShell.Scripts
Martin Fowler's Bliki in Japanese - ドメインモデル貧血症
有名なアンチパターンです。僕も昔やってしまったことがあります。
例えば、図書の貸出システムを作るとします。このシステムは利用者が直接操作し、借り入れ手続きや返却手続きを行います。
ドメインモデルを適切に適用した設計では、「書籍」クラスや「利用者」クラスを用意し、「利用者」クラスに「借りる」メソッドや「返すメソッド」を用意するでしょう。
public sealed class 書籍
{
public int 管理番号 { get { ... } }
public string タイトル { get { ... } }
public string 著者 { get { ... } }
}
public sealed class 利用者
{
public int 利用者ID { get { ... } }
public ReadOnlyCollection<書籍> 借りてる書籍 { get { ... } }
public void 借りる(書籍 対象書籍) { ... }
public void 返す(書籍 対象書籍) { ... }
}
しかし、ドメインモデル貧血症に陥っている設計では、「利用者」クラスに「借りる」メソッドや「返す」メソッドがありません。代わりに、「借り入れ操作」クラスといった感じの、ロジックのみのクラスが存在します。
public sealed class 利用者
{
public int 利用者ID { get { ... } }
public ReadOnlyCollection<書籍> 借りてる書籍 { get { ... } }
}
public sealed class 借り入れ操作
{
public void 借りる(利用者 対象者, 書籍 対象書籍) { ... }
public void 返す(利用者 対象者, 書籍 対象書籍) { ... }
}
ドメインモデル貧血症では、データと処理が分離されてしまいます。これは決してオブジェクト指向ではありません。
また、この例では、「利用者」が「借りてる書籍」を、読み取り専用コレクションのプロパティとして公開しています。「借り入れ操作」クラスは、「借りてる書籍」をどうやって変更するのでしょうか。カプセル化を崩すか、プロパティの型を読み取り専用でなくするか、あるいは利用者クラスに補助的なメソッドを用意するか。どうするにしろ、複雑さが増してしまうかと思います。
オブジェクト指向じゃないことが悪いのではありません。オブジェクト指向を適用しているつもりで貧血症を起こしているからダメなのです。
ここでは、Department ( 部署 ) クラス と Employee ( 社員 ) クラスを使った簡単な例を挙げます。
部署には ID, 名前, メンバーといったフィールドがあることにします。
public sealed class Department
{
private readonly int _id;
private readonly string _name;
private readonly ReadOnlyCollection<Employee> _members;
public int Id
{
get
{
return this._id;
}
}
public string Name
{
get
{
return this._name;
}
}
public ReadOnlyCollection<Employee> Members
{
get
{
return this._members;
}
}
public Department(int id, string name, ReadOnlyCollection<Employee> members)
{
this._id = id;
this._name = name;
this._members = members;
}
}
社員には ID, 名前といったフィールドがあることにします。
public sealed class Employee
{
private readonly int _id;
private readonly string _name;
public int Id
{
get
{
return this._id;
}
}
public string Name
{
get
{
return this._name;
}
}
public Employee(int id, string name)
{
this._id = id;
this._name = name;
}
}
部署クラスの _members フィールドこそが、今回レイジーロードを行うことになるフィールドです。見ての通り、仮想プロキシによるレイジーロードでは、レイジーロードの為の特別な仕組みがドメインクラスに一切必要ありません。
DepartmentMapper は、仮想リストと仮想リストローダーを使用してレイジーロードを部署クラスに仕込みます。
IListLoader ジェネリックインターフェイスを実装した MembersLoader クラスはインナークラスとして定義してあります。この仮想リストローダーは EmployeeMapper クラス ( 今回コードは用意していませんが ) から部署メンバーを取得します。
public sealed class DepartmentMapper
{
public Department Find(int id)
{
DepartmentsDataSet.DepartmentsRow row = this.FindDataRow(id);
if (row == null)
{
return null;
}
string name = row.Name;
MembersLoader membersLoader = new MembersLoader(id);
VirtualList<Employee> membersVirtualList = new VirtualList<Employee>(membersLoader);
ReadOnlyCollection<Employee> members = new ReadOnlyCollection<Employee>(membersVirtualList);
return new Department(id, name, members);
}
private DepartmentsDataSet.DepartmentsRow FindDataRow(int id)
{
using (DepartmentsTableAdapter adapter = new DepartmentsTableAdapter())
{
DepartmentsDataSet.DepartmentsDataTable table = adapter.GetDataById(id);
if (table.Count == 0)
{
return null;
}
return table[0];
}
}
private sealed class MembersLoader : IListLoader<Employee>
{
private readonly int _id;
public MembersLoader(int id)
{
this._id = id;
}
public IList<Employee> Load()
{
return EmployeeMapper.Singleton.FindByDepartment(this._id);
}
}
}
これで、部署クラスの Members プロパティは、実際に使用されるまでロードを行いません。
静的に生成したクライアントクラスは、ClientBase ジェネリック クラス (System.ServiceModel) を継承します。
動的に生成したクライアントクラスは、IClientChannel インターフェイス (System.ServiceModel) とサービスコントラクトのインターフェイスを実装します。ClientBase ジェネリック クラスは継承していません。
クライアント アーキテクチャ
WCF のクライアントに対して、using ステートメントによる Dispose 呼び出しを行うことは推奨されていないようです。
Using ステートメントに関する問題の回避
using ステートメントによる Dispose 呼び出しを行う代わりに推奨されるクライアントパターンはこちら。
クライアントを使用したサービスの処理
WCF クライアントは、CommunicationException クラス (System.ServiceModel) またはその派生クラスを例外としてスローします。
例外とエラーの処理
エラーの送受信
WCF サービスはセッションを利用して状態を保持することができます。セッションの開始・終了はクライアントによって制御されます。
セッションの使用
WCF のセキュリティはサービスが指定します。クライアントはサービスで指定されたセキュリティに従います。
クライアントのセキュリティ保護
WCF クライアントは、サービスの非同期呼び出しをサポートしています。
方法 : WCF サービス操作を非同期に呼び出す
WCF では双方向に通信を行うサービスを構築できます。
双方向サービス
クライアントのランタイム動作の指定
ここに掲載したページは、だいたい以下のページから辿っていけます。
WCF クライアントの概要
その他。
状態変更の理解
仮想リストは、実装すべきインターフェイスが多いためコード量が多くなっていますが、重要なのは Items プロパティだけです。他のプロパティやメソッドは全て、Items プロパティから取得したリストに処理を委譲しているだけです。
VirtualList<T> クラス
using System;
using System.Collections;
using System.Collections.Generic;
/// <summary>
/// リストの仮想プロキシです。
/// </summary>
/// <typeparam name="T"></typeparam>
[Serializable]
public sealed class VirtualList<T> : IList<T>
{
#region Constructors
/// <summary>
/// VirtualList<T> クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="loader">ローダー。</param>
/// <exception cref="System.ArgumentNullException">引数 loader が null です。</exception>
public VirtualList(IListLoader<T> loader)
{
if (loader == null)
{
throw new ArgumentNullException("loader");
}
this._items = null;
this._loader = loader;
}
#endregion
#region Fields
/// <summary>
/// ソースリストを取得または設定します。
/// </summary>
private IList<T> _items;
/// <summary>
/// ローダーを取得します。
/// </summary>
private readonly IListLoader<T> _loader;
#endregion
#region Properties
/// <summary>
/// ソースリストを取得します。
/// </summary>
private IList<T> Items
{
get
{
if (this._items == null)
{
this._items = this._loader.Load();
}
return this._items;
}
}
#endregion
#region IList<T> メンバ
/// <summary>
/// 指定したインデックスにある要素を取得または設定します。
/// </summary>
/// <param name="index">取得または設定する要素の、0 から始まるインデックス番号。</param>
/// <returns>指定したインデックスにある要素。</returns>
/// <exception cref="System.NotSupportedException">このプロパティが設定されていますが、VirtualList<T> が読み取り専用です。</exception>
/// <exception cref="System.ArgumentOutOfRangeException">index が VirtualList<T> の有効なインデックスではありません。</exception>
public T this[int index]
{
get
{
return this.Items[index];
}
set
{
this.Items[index] = value;
}
}
/// <summary>
/// VirtualList<T> 内での指定した項目のインデックスを調べます。
/// </summary>
/// <param name="item">IList<T> 内で検索するオブジェクト。</param>
/// <returns>リストに存在する場合は item のインデックス。それ以外の場合は -1。</returns>
public int IndexOf(T item)
{
return this.Items.IndexOf(item);
}
/// <summary>
/// VirtualList<T> の指定したインデックス位置に項目を挿入します。
/// </summary>
/// <param name="index">VirtualList<T> に挿入するオブジェクト。</param>
/// <param name="item">item を挿入する位置の、0 から始まるインデックス番号。</param>
/// <exception cref="System.NotSupportedException">VirtualList<T> は読み取り専用です。</exception>
/// <exception cref="System.ArgumentOutOfRangeException">index が VirtualList<T> の有効なインデックスではありません。</exception>
public void Insert(int index, T item)
{
this.Items.Insert(index, item);
}
/// <summary>
/// 指定したインデックス位置の VirtualList<T> 項目を削除します。
/// </summary>
/// <param name="index">削除する項目の 0 から始まるインデックス。</param>
/// <exception cref="System.NotSupportedException">VirtualList<T> は読み取り専用です。</exception>
/// <exception cref="System.ArgumentOutOfRangeException">index が VirtualList<T> の有効なインデックスではありません。</exception>
public void RemoveAt(int index)
{
this.Items.RemoveAt(index);
}
#endregion
#region ICollection<T> メンバ
/// <summary>
/// VirtualList<T> に格納されている要素の数を取得します。
/// </summary>
public int Count
{
get
{
return this.Items.Count;
}
}
/// <summary>
/// VirtualList<T> が読み取り専用かどうかを示す値を取得します。
/// </summary>
public bool IsReadOnly
{
get
{
return this.Items.IsReadOnly;
}
}
/// <summary>
/// VirtualList<T> に項目を追加します。
/// </summary>
/// <param name="item">VirtualList<T> に追加するオブジェクト。</param>
/// <exception cref="System.NotSupportedException">VirtualList<T> は読み取り専用です。</exception>
public void Add(T item)
{
this.Items.Add(item);
}
/// <summary>
/// VirtualList<T> からすべての項目を削除します。
/// </summary>
/// <exception cref="System.NotSupportedException">VirtualList<T> は読み取り専用です。</exception>
public void Clear()
{
this.Items.Clear();
}
/// <summary>
/// VirtualList<T> に特定の値が格納されているかどうかを判断します。
/// </summary>
/// <param name="item">VirtualList<T> 内で検索するオブジェクト。</param>
/// <returns>item が VirtualList<T> に存在する場合は true。それ以外の場合は false。</returns>
public bool Contains(T item)
{
return this.Items.Contains(item);
}
/// <summary>
/// VirtualList<T> の要素を System.Array にコピーします。System.Array の特定のインデックスからコピーが開始されます。
/// </summary>
/// <param name="array">VirtualList<T> から要素がコピーされる 1 次元の System.Array。System.Array には、0 から始まるインデックス番号が必要です。</param>
/// <param name="arrayIndex">コピーの開始位置となる、array の 0 から始まるインデックス番号。</param>
/// <exception cref="System.ArgumentException">
/// array が多次元です。
/// またはarrayIndex が array の長さ以上です。
/// またはコピー元の VirtualList<T> の要素数が、arrayIndex からコピー先の array の末尾までに格納できる数を超えています。
/// または型 T をコピー先の array の型に自動的にキャストすることはできません。
/// </exception>
/// <exception cref="System.ArgumentNullException">array が null です。</exception>
/// <exception cref="System.ArgumentOutOfRangeException">arrayIndex が 0 未満です。</exception>
public void CopyTo(T[] array, int arrayIndex)
{
this.Items.CopyTo(array, arrayIndex);
}
/// <summary>
/// VirtualList<T> 内で最初に見つかった特定のオブジェクトを削除します。
/// </summary>
/// <param name="item">VirtualList<T> から削除するオブジェクト。</param>
/// <returns>
/// item が VirtualList<T> から正常に削除された場合は true。それ以外の場合は false。
/// このメソッドは、item が元の VirtualList<T> に見つからない場合にも false を返します。
/// </returns>
/// <exception cref="System.NotSupportedException">VirtualList<T> は読み取り専用です。</exception>
public bool Remove(T item)
{
return this.Items.Remove(item);
}
#endregion
#region IEnumerable<T> メンバ
/// <summary>
/// コレクションを反復処理する列挙子を返します。
/// </summary>
/// <returns>コレクションを反復処理するために使用できる System.Collections.Generic.IEnumerator<T>。</returns>
public IEnumerator<T> GetEnumerator()
{
return this.Items.GetEnumerator();
}
#endregion
#region IEnumerable メンバ
/// <summary>
/// コレクションを反復処理する列挙子を返します。
/// </summary>
/// <returns>コレクションを反復処理するために使用できる System.Collections.IEnumerator オブジェクト。</returns>
IEnumerator IEnumerable.GetEnumerator()
{
return ((System.Collections.IEnumerable)this.Items).GetEnumerator();
}
#endregion
}
IListLoader<T> インターフェイス
using System;
using System.Collections.Generic;
/// <summary>
/// リストのローダーです。
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IListLoader<T>
{
/// <summary>
/// リストをロードします。
/// </summary>
/// <returns>リスト。</returns>
IList<T> Load();
}
この仮想リスト及びローダーは、そのまま使用できます。
仮想リストはシールクラスにしてあります。これは、仮想リストを継承したクラスを定義する必要がないはずだからです。null を許可しないとか重複を許可しないといった制約は、仮想リストには実装しません。
この仮想リスト及びローダーを使用したレイジーロードの実装例は次回にでも。
// 追記 (2010/06/06)
Lazy クラスを使用した仮想プロキシを実装してみました。
例えば "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) が利用できます。