- PR -

ブラウザの「戻る」「更新」を検知するサーバーコントロール

1
投稿者投稿内容
きくちゃん
ぬし
会議室デビュー日: 2003/08/01
投稿数: 854
お住まい・勤務地: 都内某所
投稿日時: 2005-09-02 22:52
時々引き合いに出される「SessionとViewStateの両方にカウンタを保持して」云々、という投稿をした張本人です。
実際に、この方式でページのリロードや「戻る」操作後の再ポストバック、あるいはダブルクリックをサーバーサイドで検知する、Webサーバーコントロールを作成して使用していますが、もしかしたら参考になるかも知れないと思いまして、ソースを掲載することにしました。
ツッコミ、機能強化(クライアントサイドでの検証とか)等のフィードバックを期待します。

ちなみに、当初は単純にカウンタをインクリメントしていましたが、リセットしたり戻ったりしてると、偶に値が一致してしまう事があったりして、それはやめました。…やめたんですが、変数名は「カウンタ」のままだったりします。

コード:
'System.Design を参照設定に追加

Imports System.ComponentModel
Imports System.Web.UI

'Namespace MyWebControls

#Region " PostBackValidatorコントロール "

''' <summary>
''' ラウンドトリップの妥当性を検証する Web サーバーコントロール
''' </summary>
<DefaultProperty("ErrorMessage"), _
 Designer(GetType(MyWebControls.PostBackValidatorDesigner)), _
 ToolboxData("<{0}:PostBackValidator runat=server></{0}:PostBackValidator>")> _
Public Class PostBackValidator
    Inherits System.Web.UI.WebControls.Label
    Implements System.Web.UI.IValidator

#Region " メンバ変数宣言 "

    Friend _IsValid As Boolean = True
    Private _Display As System.Web.UI.WebControls.ValidatorDisplay = _
        System.Web.UI.WebControls.ValidatorDisplay.Static
    Private _ErrorMessage As String = String.Empty

    Private CounterKey As String

#End Region

#Region " コンストラクタ "

    ''' <summary>PostBackValidator クラスの新しいインスタンスを初期化</summary>
    Public Sub New()
        MyBase.New()
        Me.ForeColor = Drawing.Color.Red
        MyBase.EnableViewState = True
    End Sub

#End Region

#Region " プロパティ "

    ''' <summary>ラウンドトリップの整合性の検証が成功したかどうかを示す値を取得、または設定</summary>
    <Browsable(False)> _
    Public Property IsValid() As Boolean Implements System.Web.UI.IValidator.IsValid
        Get
            Return _IsValid
        End Get
        Set(ByVal Value As Boolean)
            _IsValid = Value

            If Value Then
                Me.Reset()
            End If
        End Set
    End Property

    ''' <summary>
    ''' 検証コントロールにエラーメッセージを表示する際の動作を表す
    ''' System.Web.UI.WebControls.ValidatorDisplay の値を取得、または設定
    ''' </summary>
    <Description("検証コントロールにエラーメッセージを表示する際の動作"), _
    Category("Appearance"), _
    DefaultValue(GetType(System.Web.UI.WebControls.ValidatorDisplay), "Static")> _
    Public Property Display() As System.Web.UI.WebControls.ValidatorDisplay
        Get
            Return _Display
        End Get
        Set(ByVal Value As System.Web.UI.WebControls.ValidatorDisplay)
            _Display = Value
        End Set
    End Property

    ''' <summary>前景色を表す System.Drawing.Color の値を取得または設定</summary>
    <Description("前景色"), _
    Category("Appearance"), _
    DefaultValue(GetType(System.Drawing.Color), "Red")> _
    Public Overrides Property ForeColor() As System.Drawing.Color
        Get
            Return MyBase.ForeColor
        End Get
        Set(ByVal Value As System.Drawing.Color)
            MyBase.ForeColor = Value
        End Set
    End Property

    ''' <summary>エラーメッセージのテキストを取得、または設定</summary>
    <Description("エラーメッセージのテキスト"), _
    Category("Appearance")> _
    Public Property ErrorMessage() As String Implements System.Web.UI.IValidator.ErrorMessage
        Get
            Return _ErrorMessage
        End Get
        Set(ByVal Value As String)
            _ErrorMessage = Value
        End Set
    End Property

    ' 隠蔽(動作上必要だが弄られたくない)
    <Browsable(False), _
    EditorBrowsable(EditorBrowsableState.Never), _
    DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)> _
    Public Overrides Property Text() As String
        Get
            If (Not _IsValid) And (Not _Display.Equals(System.Web.UI.WebControls.ValidatorDisplay.None)) Then
                Return Me.ErrorMessage
            Else
                Return MyBase.Text
            End If
        End Get
        Set(ByVal Value As String)
            MyBase.Text = Value
        End Set
    End Property

    ' 隠蔽(動作上必要だが弄られたくない)
    <Browsable(False), _
    EditorBrowsable(EditorBrowsableState.Never), _
    DesignOnly(True), _
    DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)> _
    Public Shadows ReadOnly Property EnableViewState() As Boolean
        Get
            Return True
        End Get
    End Property

#End Region

#Region " メソッド "

    '// PostBackValidator.Init イベントハンドラ
    Private Sub PostBackValidator_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
        CounterKey &= Me.ID
        CounterKey &= "_PostbackCounter"
    End Sub

    ''' <summary>
    ''' 検証を実行し、IsValid プロパティを更新
    ''' </summary>
    Public Sub Validate() Implements System.Web.UI.IValidator.Validate
        If Me.Enabled Then
            Dim ClientCounter, ServerCounter As Double

            If Page.IsPostBack Then
                '// カウンタ値取得
                ServerCounter = CType(Page.Session(CounterKey), Double)
                ClientCounter = CType(ViewState(CounterKey), Double)
            Else
                If CType(Page.Session(CounterKey), Double).Equals(0.0#) Then
                    '// セッションに値が保持されていない場合、本当の最初の要
                    '// 求と見なしてカウンタ値を初期化
                    ServerCounter = Microsoft.VisualBasic.DateAndTime.Timer
                    ClientCounter = ServerCounter
                Else
                    '// ブラウザの戻るボタンで GET 要求が走る場合があるので
                    '// クライアント側のカウンタだけを更新
                    ClientCounter = Microsoft.VisualBasic.DateAndTime.Timer
                End If
            End If

            If ServerCounter.Equals(ClientCounter) Then
                '// カウンタ値が一致した場合、双方のカウンタを更新
                ServerCounter = Microsoft.VisualBasic.DateAndTime.Timer
                ClientCounter = ServerCounter
                Page.Session.Add(CounterKey, ServerCounter)
                ViewState.Add(CounterKey, ClientCounter)
            Else
	            '// 一致しない場合、不整合と見なす
                _IsValid = False
            End If
        End If
    End Sub

    ''' <summary>監視状態をリセット</summary>
    Public Overridable Sub Reset()
        _IsValid = True
        ViewState.Remove(CounterKey)
        Page.Session.Remove(CounterKey)
    End Sub

    ''' <summary>PostBackValidator.Init イベント</summary>
    ''' <param name="e">イベント データを格納している System.EventArgs オブジェクト</param>
    Protected Overrides Sub OnInit(ByVal e As System.EventArgs)
        MyBase.OnInit(e)
        Page.Validators.Add(Me)
    End Sub

    ''' <summary>PostBackValidator.Load イベント</summary>
    ''' <param name="e">イベント データを格納している System.EventArgs オブジェクト</param>
    Protected Overrides Sub OnUnload(ByVal e As System.EventArgs)
        If Not (Page Is Nothing) Then
            Page.Validators.Remove(Me)
        End If

        MyBase.OnUnload(e)
    End Sub

    ''' <summary>保存された前回のページ要求からビューステート情報を復元</summary>
    ''' <param name="savedState">復元するデータを保持する System.Object</param>
    Protected Overrides Sub LoadViewState(ByVal savedState As Object)
        If Not savedState Is Nothing Then
            Dim CustomState As Object() = CType(savedState, Object())

            If (Not CustomState(0) Is Nothing) Then
                MyBase.LoadViewState(CustomState(0))
            End If

            If (Not CustomState(1) Is Nothing) Then
                _ErrorMessage = CustomState(1)
            End If

            If (Not CustomState(2) Is Nothing) Then
                _Display = CustomState(2)
            End If

        End If
    End Sub

    ''' <summary>サーバー コントロールのビューステートの変更を保存</summary>
    ''' <returns>サーバー コントロールの現在のビューステート</returns>
    Protected Overrides Function SaveViewState() As Object
        Dim CustomState As System.Collections.ArrayList = New System.Collections.ArrayList

        CustomState.Add(MyBase.SaveViewState)
        CustomState.Add(_ErrorMessage)
        CustomState.Add(_Display)

        Return CustomState.ToArray()
    End Function

#End Region

End Class

#End Region

#Region " PostBackValidatorコントロールデザイナ "

''' <summary>MyWebControls.PostBackValidator Web サーバー コントロールのデザイン時の動作を拡張</summary>
Public Class PostBackValidatorDesigner
    Inherits System.Web.UI.Design.TextControlDesigner


    ''' <summary>デザイン時に関連付けられたコントロールを表示するために使用する HTML を取得</summary>
    ''' <returns>デザイン時にコントロールを表示するための HTML</returns>
    Public Overrides Function GetDesignTimeHtml() As String
        Dim Ctrl As MyWebControls.PostBackValidator = CType(Me.Component, MyWebControls.PostBackValidator)
        Dim Visible As Boolean = Ctrl.Visible

        If Ctrl.Display.Equals(System.Web.UI.WebControls.ValidatorDisplay.None) _
        OrElse Ctrl.ErrorMessage.Length.Equals(0) Then
            Return MyBase.GetDesignTimeHtml()
        Else
            Dim Sw As System.IO.StringWriter = New System.IO.StringWriter
            Dim Htw As HtmlTextWriter = New HtmlTextWriter(Sw)
            Ctrl._IsValid = False
            Ctrl.Visible = True
            Ctrl.RenderControl(Htw)
            Ctrl._IsValid = True
            Ctrl.Visible = Visible
            Return Sw.ToString()
        End If

    End Function

End Class

#End Region

'End Namespace

Jitta
ぬし
会議室デビュー日: 2002/07/05
投稿数: 6267
お住まい・勤務地: 兵庫県・海手
投稿日時: 2005-09-04 21:48
サーバーコントロールですか。そこまで考えなかった。


 Validator ということなので、おそらく、イベントハンドラの入り口で IsValid を検査して、実行するかどうかを判別する必要があると思うのですが、どうでしょう?
 私も、私のページで CustomValidator を使って検証するコードを紹介していますが、それを検証していて、IsValid は False なのに、Button.Click は走ってしまったので焦りました。。。
_________________
きくちゃん
ぬし
会議室デビュー日: 2003/08/01
投稿数: 854
お住まい・勤務地: 都内某所
投稿日時: 2005-09-05 12:40
Jittaさん、こんにちは。

引用:

 Validator ということなので、おそらく、イベントハンドラの入り口で IsValid を検査して、実行するかどうかを判別する必要があると思うのですが、どうでしょう?


そうですね。
私の場合、Load イベントハンドラで Page.Validate() → Page.IsValid 確認 → false ならイベントハンドラの関連づけを無効にして(VB でいうところの RemoveHandler ステートメントの呼出)メッセージの表示、という感じで使っています。
#イベントハンドラを列挙して片っ端から無効にしたりとか出来るんでしたっけ? その辺、追究せずに、必要に応じて一個一個、ベタで記述してますけど…。
Jitta
ぬし
会議室デビュー日: 2002/07/05
投稿数: 6267
お住まい・勤務地: 兵庫県・海手
投稿日時: 2005-09-05 19:42
> #イベントハンドラを列挙して片っ端から無効にしたりとか出来るんでしたっけ?
 片っ端というのは、「とにかく全部」ですか?そうであるなら、できるとしても、しない方がいいでしょう。
というのも、「とにかく全部」やっちゃうと、「×」で終了するしかなくなるかもしれない、という罠があると思うのです。
というか、検証中にそういう状況に陥ったわけです。。。

 連続ポストの場合、ブラウザが見せられるのは最後のポストですよね。最後のポストは IsValid が false になっているはずなので、特にそういう状況はまずいと思います。

_________________
きくちゃん
ぬし
会議室デビュー日: 2003/08/01
投稿数: 854
お住まい・勤務地: 都内某所
投稿日時: 2005-09-06 12:23
Jittaさん、こんにちは。

引用:

というか、検証中にそういう状況に陥ったわけです。。。


......φ(..)

引用:

 連続ポストの場合、ブラウザが見せられるのは最後のポストですよね。最後のポストは IsValid が false になっているはずなので、特にそういう状況はまずいと思います。


この辺りは時間が出来たら、どういった挙動を示すのか試してみたいと思います。

で、使用方法について、ひとつ書き忘れた事がありました。
IsValid が false の場合、復旧させるためには true をセットしてやるか、Reset() メソッドを呼び出す必要があります。
ということで、以下に使用例を掲載します。

WebForm1.aspx 側
コード:
<%@ Page Language="vb" AutoEventWireup="false" Codebehind="WebForm1.aspx.vb" Inherits="VbWebApp001.WebForm1"%>
<%@ Register TagPrefix="mwc" Namespace="MyWebControls" Assembly="MyWebControls, Version=1.0.0.0" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
  <body>
    <form id="Form1" method="post" runat="server">
      <p>
        <asp:Label id="Label1" runat="server" EnableViewState="False"></asp:Label>
        <asp:Button id="Button1" runat="server" Text="更新"></asp:Button>
      </p>
      <p>
        <mwc:PostBackValidator id="PostBackValidator1" runat="server"
        ErrorMessage="不正ポストバック検知" Display="Dynamic"></mwc:PostBackValidator>
        <asp:Button id="Button2" runat="server" Text="確認" Visible="False"></asp:Button>
      </p>
    </form>
  </body>
</HTML>



WebForm1.aspx.vb 側
コード:
' Page.Load イベントハンドラ
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
    Validate()

    If (Not PostBackValidator1.IsValid) Then
        CancelEvents()
        Button1.Enabled = False
        Button2.Visible = True
    End If
End Sub

' Button1.Click イベントハンドラ
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
    Label1.Text = "正常処理"
End Sub

' Button2.Click イベントハンドラ
Private Sub Button2_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button2.Click
    ' カウンタリセット
    PostBackValidator1.Reset()
    Button1.Enabled = True
    Button2.Visible = False
End Sub

' イベント処理キャンセル
Private Sub CancelEvents()
    RemoveHandler Button1.Click, AddressOf Button1_Click
End Sub


1

スキルアップ/キャリアアップ(JOB@IT)