コントロールにおけるデータ検証

ユーザー入力を受け付けるアプリケーションは、通常、入力値の有効性を検証する必要があります。Curl® GUI Toolkit の検証パッケージ CURL.GUI.CONTROL-VALIDATION は、データ入力の検証メカニズムを提供します。このパッケージは Curl API バージョン 5.0 から追加されました。
さまざまなレベルの検証を実行できます。たとえば、ユーザーのキーストロークに即座に反応して値を検証したり、入力が完了したフォームまたはダイアログのすべてのコントロールの検証を実行したりできます。問題が見つかった場合は、入力中のフォーム内やポップアップ ダイアログにその内容を表示できます。
検証メカニズムでは、Validate および ValidationComplete という 2 つのイベントを使用します。最初のイベントは、コントロールでそれまでの入力が受け入れられるかどうかを確認する検証を要求します。2 つ目のイベントでは、検証結果の表示を要求します。検証結果は、ActiveTraversor.validation-result プロパティとしてコントロールにアタッチされます。
多くの場合、アプリケーションで検証イベントを直接処理する必要はありません。通常は、次のオブジェクトを組み合わせて使用します。

検証クラス

検証パッケージには、次に示す各種検証クラスが含まれています。

検証サイクル

コントロールは個別に検証できますが、多くの場合、Dialog (RecordFormHttpForm などのサブクラスを含む) でグループ化されたコントロールに対して検証が実行されます。検証サイクルは、入力が変更されると開始し、すべての必要なフィードバックを表示して終了します。
validate-with プロシージャは、ActiveTraversor (コントロール) の検証を要求します。ValidationController を作成し、ActiveTraversor にイベント ハンドラとしてアタッチします。検証サイクルは ValidationController により実装されます。現在フォーカスがあるコントロールで ValueChanged または ValueFinished イベントが発生すると、検証サイクルが開始します。
現在のコントロールが Dialog に含まれていない場合、検証イベントは現在のコントロールでのみ発生します。コントロールが Dialog に含まれている場合は、検証サイクルによってダイアログ内の他のコントロールとそのダイアログ自体の有効性が検証されます。検証サイクルの間、コントロールはタブ トラバーサルの順序で検証されます。
指定した Dialog 内では、検証サイクルの開始時にすべてのメッセージと ActiveTraversor.validation-result プロパティがクリアされます。次に、ValidationControllerValidate イベントと ValidationComplete イベントを現在のコントロールで発生させます。ダイアログが有効であるには、エラーが見つかるかどうかによって次の動作が決まるようにするため、ダイアログ内のすべてのコントロールが有効でなければなりません。
現在の入力が無効な場合は、ダイアログ内の他のコントロールを検証する必要はありませんが、それらのコントロールにエラーを通知する必要があります。コントローラは ValidationComplete イベントを他のコントロールおよびそのコントロールを含む Dialog で発生させるため、適切な方法 (無効状態に変更するなど) でエラーに対処できます。
現在のコントロールにエラーがない場合は、コントローラは Validate イベントと ValidationComplete イベントをダイアログ内の他のコントロールで発生させます。すぐに表示されないエラーは、収集されてサイクル完了時に表示されます。

validate-with プロシージャ

validate-with プロシージャは Curl の検証機能の中心です。EventHandler を返し、指定された Validator により、ActiveTraversor の検証を実行する ValidationController を作成します。
Curl 検証 API には、多数の各種データ入力用の検証クラスが含まれています。「検証クラス」を参照してください。
検証システムは、エラーを MessageDisplay に表示します。既定のメッセージ表示は MessageDialog です。これは MessageDisplay のサブクラスで、収集した検証メッセージをポップアップ ダイアログに表示します。ポップアップ ダイアログはダイアログやフォームの領域を使用しませんが、フォームにメッセージ用の余裕がある場合は、MessageDisplay を使用した方がメッセージをよりわかりやすく表示できます。1 つの MessageDisplay を多くのコントロールで共有できます。
次の例では、1 つのコントロールに対する単純な検証を示しています。NumericValidator (Validator のサブクラス) を使用して TextField を検証します。これは、入力値が数字であるかどうかを確認します。また、この例では、既定のメッセージ表示である MessageDialog を使用しています。ユーザーが Tab キーや Enter キーを押すなどの操作を行うことにより、ValueFinished イベントが発生して検証が開始されます。

例: 数値入力の検証
{let numeric-val:TextField = 
    {TextField
        width = 3cm,
        {validate-with {NumericValidator}}
    }
}
{HBox "Number:", numeric-val}
次の例では、MessageDisplay オブジェクトを使用して検証エラーをレポートします。この場合、ValueChanged イベントが発生すると検証が実行されるため、入力値が無効と判断されるとすぐにエラーが表示されます。
エラー メッセージを表示するには、UI に表示オブジェクトを追加する必要があります。

例: MessageDisplay でのエラー表示
{let md:MessageDisplay = {MessageDisplay}}
{let numeric-val:TextField = 
    {TextField
        width = 3cm,
        message-display = md,
        {validate-with {NumericValidator}, required? = true}
    }
}
{HBox spacing = 3pt, "Number:", numeric-val, md}
MessageDisplayMessageDialog には、検証メッセージの表示や動作を変更できる次のプロパティがあります。
次の例では、いくつかの MessageDisplay のプロパティに新しい値を指定しています。

例: MessageDisplay プロパティの変更
{let md:MessageDisplay = 
    {MessageDisplay
        invalid-entry-background = "magenta",
        required-entry-background = "lime",
        message-color = "magenta"
    }
}
{let numeric-val:TextField = 
    {TextField
        width = 3cm,
        message-display = md,
        {validate-with {NumericValidator}, required? = true}
    }
}
{HBox spacing = 3pt, "Number:", numeric-val, md}

ダイアログの検証

これまでに示した例では、コントロールを個別に検証しました。通常のケースでは、ダイアログ内の複数のコントロールに対して検証を行います。たとえば、ダイアログ内のすべてのコントロールに有効なデータが入力されていることを確認するユーザー インターフェイスを設計できます。
次の例は、ダイアログ内のコントロールをグループ化し、そのダイアログに対して検証を実行します。この例のポイントを次に示します。

例: CommandButton を使用した Dialog の検証
{let md:MessageDialog = {MessageDialog title = "Input error"}}
{let numeric-val:TextField = 
    {TextField
        {validate-with
            {NumericValidator}, 
            dialog-on-finished? = false,
            required? = true
        }
    }
}
{let string-val:TextField = 
    {TextField
        {validate-with
            {StringValidator min-chars = 5, max-chars = 10},
            dialog-on-finished? = false,
            required? = false
        }
    }
}
{let validate-button:CommandButton = 
    {CommandButton
        label = "Validate",
        {on Action at cb:CommandButton do
            {if {validate-dialog {non-null cb.dialog}} then
                {popup-message "Submitted"}
            }
        }
    }
}
{Dialog
    margin = 6pt, 
    message-display = md,
    {VBox
        width = 3cm,
        "Number:",
        numeric-val,
        "String:",
        string-val,
        validate-button
    }
}
ダイアログレベルの検証では、ダイアログ内の該当するデータ入力がすべて有効な場合にコマンド ボタンを有効にすることができます。次の例では、DialogValidator を使用してダイアログを検証します。3 つのテキスト フィールドすべての入力が有効な場合は、ダイアログの検証が完了すると、ValidationComplete イベントのイベント ハンドラによってコマンド ボタンが有効になります。
[Email] フィールドが必須でないため、[String] フィールドと [Number] フィールドに有効な値が入力されるとすぐにダイアログは有効となります。ただし、[Email] フィールドに無効な値が入力された場合は、ダイアログは有効とは見なされなくなります。

例: 有効な Dialog を使用した CommandButton の有効化
{let md:MessageDisplay = {MessageDisplay}}
{let string:TextField = 
    {TextField
        width = 3cm,
        message-display = md,
        name = "string",
        {validate-with
            {StringValidator min-chars = 2, max-chars = 10},
            required? = true
        }
    }
}
{let number:TextField = 
    {TextField
        width = 3cm,
        message-display = md,
        name = "number",
        {validate-with {NumericValidator}, required? = true}
    }
}
{let email:TextField = 
    {TextField
        width = 3cm,
        message-display = md,
        name = "email",
        {validate-with ValidationPattern.email-address}
    }
}
{let cb-val:CommandButton = 
    {CommandButton
        label = "Press",
        name = "button",
        {on Action do
            {popup-message
                {VBox
                    "You entered:",
                    string.value,
                    number.value,
                    email.value
                }
            }
        }
    }
}
{Dialog
    {Table
        columns = 2,
        "String:", string,
        "Number:",number,
        "Email:", email,
        cb-val, md
    },
    {validate-with {DialogValidator}},
    {on vc:ValidationComplete at d:Dialog do
        set {d.get-by-name "button"}.enabled? = d.valid?
    }
}

EnablingValidator

コマンド ボタンの検証に EnablingValidator を使用することもできます。EnablingValidator に渡されたリスト内のすべてのコントロールが有効な場合に、コマンド ボタンが有効となります。コマンド ボタンは、コントロールが無効な場合は無効となり、有効な場合は有効となります。
次の例では、numeric-3 ではなく numeric-1 フィールドと numeric-2 フィールドを使用してコマンド ボタンを検証します。必須の数字をすべて入力すると、ボタンが有効になります。

例: CommandButton の検証
{let numeric-1:TextField = 
    {TextField
        name = "numeric-1",
        {validate-with {NumericValidator}, required? = true}
    }
}
{let numeric-2:TextField = 
    {TextField
        name = "numeric-2",
        {validate-with {NumericValidator}, required? = true}
    }
}
{let numeric-3:TextField = 
    {TextField
        name = "numeric-3",
        {validate-with {NumericValidator}, required? = true}
    }
}
{let enter-data:CommandButton = 
    {CommandButton
        name = "button",
        label = "Enter Data",
        {validate-with
            {EnablingValidator "numeric-1", "numeric-2"}
        },
        {on Action do
            {popup-message
                "You entered:"
                & " " & {value numeric-1.value}
                & " " & {value numeric-2.value}
            }
        }
    }
}
{Dialog
    {VBox
        "Enter three numbers:",
        numeric-1,
        numeric-2,
        numeric-3,
        enter-data
    },
    {validate-with {DialogValidator}}
}

タブ順序の重要性

ValidationController は、ダイアログ内のコントロールに検証イベントを送信す際、ダイアログ内のオブジェクトのタブ順序に従います。これは検証によってコマンド ボタンを有効にするような場合に重要なポイントです。コントロールが正しい順序で検証されなければ、システムは不完全な情報に基づいて動作する場合があるためです。
この問題を次の例に示します。前の例と同様に、コマンド ボタンはリストされたコントロールが有効な場合に有効になります。ただし、この例では、コマンド ボタンはダイアログ内の最初のオブジェクト、つまりタブ順序として最初に設定されているオブジェクトです。いずれかのテキスト フィールドに数字を入力してください。入力が完了するとすぐにコマンド ボタンが有効になります。
値の入力によって検証サイクルが開始され、現在のコントロールの次に、タブ順序の最初にあるコマンド ボタンが検証されます。他の 2 つのテキスト フィールドはまだ検証されていないため、これらのフィールドの検証結果が得られません。そのため、コマンド ボタンは、変更した 1 つのコントロールにのみ基づいて検証されます。
この問題を修正するには、ActiveTraversor.tab-index を使用してタブ順序を制御します。各コントロールの tab-index を設定する行のコメントを解除して、例を実行してみてください。これで、検証は正しく動作します。

例: タブ順序の制御
{let numeric-1:TextField = 
    {TextField
||--            tab-index = 3,
        name = "numeric-1",
        {validate-with {NumericValidator}, required? = true}
    }
}
{let numeric-2:TextField = 
    {TextField
||--            tab-index = 2,
        name = "numeric-2",
        {validate-with {NumericValidator}, required? = true}
    }
}
{let numeric-3:TextField = 
    {TextField
||--            tab-index = 1,
        name = "numeric-3",
        {validate-with {NumericValidator}, required? = true}
    }
}
{let enter-data:CommandButton = 
    {CommandButton
||--            tab-index = 4,
        name = "button",
        label = "Enter Data",
        {validate-with
            {EnablingValidator "numeric-1", "numeric-2", "numeric-3"}
        },
        {on Action do
            {popup-message
                "You entered:"
                & " " & {value numeric-1.value}
                & " " & {value numeric-2.value}
                & " " & {value numeric-3.value}
            }
        }
    }
}
{Dialog
    {VBox
        enter-data,
        "Enter numbers:",
        numeric-1,
        numeric-2,
        numeric-3
    },
    {validate-with {DialogValidator}}
}

MessageDisplay のサブクラス化

このセクションでは、MessageDisplay をサブクラス化して、コントロール、ラベル、およびメッセージをグループとして保持するオブジェクトの作成方法を説明します。各要素は GUI コンテナ (Box のサブクラス) に配置されています。通常、このコンテナは Table です。
コンストラクタは、複合メッセージ画面を構成するさまざまな UI 要素を作成します。これらの要素は、コンストラクタに渡される Box に追加されます。オーバーライドする必要があるメソッドは MessageDisplay.show のみです。

例: MessageDisplay のサブクラスの作成
{define-class public open CompoundMessageDisplay {inherits MessageDisplay}
  let public constant columns:int = 3

  field protected prefix:Frame
  field protected target:Graphic
  field protected suffix:Frame = {Frame}
  field protected message:Frame
  
  {constructor public {default
                          box:Box,
                          label:Visual, 
                          target:Graphic,
                          message-width:Distance = 2in,
                          ...
                      }
    {construct-super ...}
    set self.prefix = {Frame}
    set self.target = target
    set self.message = {Frame}

    {self.prefix.add label}
    set self.message-display = self

    {box.add self.prefix}
    {box.add {HBox self.target, {Fill width = 4px}, self.suffix}}
    {box.add self.message}
    set self.target.message-display = self
  }
  
  || Visual.clear をオーバーライドし、 
  || もし表示されているメッセージがあれば、クリアします。
  {method public {clear}:void
    {self.message.clear}
  }
  
  || イベントに添付されたエラーを表示します。
  {method public open {show 
                          vc:ValidationComplete, 
                          target:ActiveTraversor
                      }:void
    || どのような場合も、変数suffixと適切な色を表示します。
    {self.show-colors target}
    {self.suffix.clear}
    {if vc.controller.required? then
        {self.suffix.add {HBox "*", {Fill}}}
    }
    let display?:bool = not vc.consumed? and vc.display?
    {vc.consume}

    {if-non-null tvr = target.validation-result then
        {if tvr.invalid? and display? then
            set self.message.color = self.message-color
            {self.message.add replace? = true, tvr.message}
            set target.control-content-background = 
                self.invalid-entry-background
            {return}
        }
    }
    {unset target.control-content-background}
  }
}

{let tbl:Table = {Table columns = CompoundMessageDisplay.columns}}
{do
    {CompoundMessageDisplay tbl, "Name:",
        {TextField
            width = 1.5in,
            name = "name",
            {validate-with {StringValidator}, required? = true}
        }
    }

    {CompoundMessageDisplay tbl, "Password:",
        {TextField
            width = 1.5in,
            name = "password",
            {validate-with
                {StringValidator min-chars = 6}, required? = true
            }
        }
    }

    {CompoundMessageDisplay tbl, "Confirm:",
        {TextField width = 1.5in,
            name = "confirm",
            {validate-with {StringValidator}, required? = true},
            {on ve:Validate at tf:TextField do
                let pf:TextField = 
                    {tf.dialog.get-by-name "password"} asa TextField
                {if ve.current? and ve.partial? then
                    {if not {pf.value.prefix? tf.value} then
                        {tf.mark-invalid
                            message =
                                {hlmessage
                                    Confirmation does not match password.
                                }
                        }
                     elseif pf.value != tf.value then
                        {tf.mark-invalid} || incomplete
                    }
                 elseif pf.value != tf.value then
                    {tf.mark-invalid
                        message =
                            {hlmessage
                                Confirmation does not match password.
                            }
                    }
                }
            }
        }
    } 
    {CompoundMessageDisplay tbl, "e-mail address:", 
        {TextField width = 1.5in,
            name = "e-mail",
            {validate-with ValidationPattern.email-address}
        }
    }
    {tbl.add
        {row
            {skip}
            {cell 
                {HBox
                    {ok-button label = "&Login", name = "login",
                        width = 1.5in,
                        {validate-with
                            {EnablingValidator 
                                "password", "name", "confirm", "e-mail"
                            }
                        },
                        {on Action at cb:CommandButton do
                            {type-switch cb.dialog
                             case form:HttpForm do
                                {form.submit}
                             else
                                {popup-message "Logging in..."}
                            }
                        }        
                    }
                }
            }
            {cell * Entry required}
        }
    }

}
{Dialog
    background = "white",
    margin = 6pt,
    tbl,
    {validate-with {DialogValidator}}
}
この例では、Validate のイベント ハンドラを使用して特殊な検証を実行する方法も示します。この例の目的は、パスワードの確認入力がパスワード入力と一致しない場合に、ユーザーに警告することです。確認入力が途中で (partial? フラグで示されます)、パスワードの先頭部分に一致している限り、メッセージは表示されません。フィールドが無効とマークされると、ログイン ボタンは無効なままになります。ユーザーはフィールドの色でエラーを識別できます。パスワードに一致しない文字を入力した場合は、すぐにメッセージが表示されます。フィールドをそのままにしようとすると、partial? フラグが false になり、確認入力がパスワードに一致しなければメッセージが表示されます。
また、電子メールの入力はオプションですが、入力した場合は、有効な値でなければログイン ボタンは有効にはなりません。

検証クラスの階層

BaseFrame
MessageDisplay
MessageDialog

Event
GuiEvent
DialogEvent
BaseValidate
Validate
ValidationComplete

Validator
DialogValidator
DomainValidator
EnablingValidator
NumericValidator
RegExpValidator
StringValidator
ValidationController
ValidationResult
プロシージャ
validate-with
validate-dialog
非ローカル オプション
message-display
その他のプロパティ
ActiveTraversor.validation-result
ActiveTraversor.valid?