次の方法で共有


TextBoxの入力内容を判断して取消しする機能の実装方法について

質問

2013年5月13日月曜日 9:53

入力可能な内容を制限できるテキストボックス(カスタムコントロール)を作成しようとしていますが、依存関係プロパティの値の強制で実現しようとしたところ期待通りの動作が得られません。

解決方法をお持ちの方、ご教示くださいますよう宜しくお願いいたします。.Net Framework のバージョンは 4.0 です。

尚、数値のみ入力可能なテキストボックスを作成する場合を例として質問させて頂きます。

    public class MyTextBox : TextBox
    {
        static MyTextBox()
        {
            TextBox.TextProperty.OverrideMetadata(
                typeof(MyTextBox), new FrameworkPropertyMetadata(null, null, MyTextBox.CoerceText));
        }

        static object CoerceText(DependencyObject d, object value)
        {
            decimal decValue;
            return (decimal.TryParse((string)value, out decValue))
                ? value : ((MyTextBox)d).Text;
        }
    }

TextBox を継承した MyTextBox クラスにて TextBox.Text 依存関係プロパティのメタデータをオーバーライドして強制コールバックを追加しました。

これによって CoerceText メソッドは正しく呼び出され、数値に変換できればそのままの値を、できなければ現在の Text プロパティの値を返しています。

ところが内部的な Text プロパティの値は期待通り強制されますが、画面(テキストボックス)に表示されている文字列は強制前の間違った文字列のままです。

1. "1" を入力 => Text プロパティ、表示内容共に "1" となる

2. "a" を入力 => Text プロパティは空("a"を入力する前の値)、表示内容は "a"のまま強制されず

最初、画面入力値を内部値に反映させるロジックの最中で、その値を書き換えたとしても画面には通知されないのだと思いました。

しかし、コードを次のように書き換えると、テキストボックスにも "Error" と表示され、期待通りの強制動作が行われます。

        static object CoerceText(DependencyObject d, object value)
        {
            decimal decValue;
            return (decimal.TryParse((string)value, out decValue))
                ? value : "Error";
        }

上記のことから、強制前と異なる値に強制すればプロパティ、画面の両方に反映され、強制前と同じ値であればプロパティにしか反映されないという結果が得られました。

カレントと新しい値の比較周りで残念なバグの香りがします・・・なにかよい解決方法はありませんでしょうか。

そもそも、値の強制で入力の取消し制御を行うのが間違っているのでしょうか。よい方法がございましたら、そちらもご教示いただけると幸いです。

宜しくお願いいたします。

以上

すべての返信 (3)

2013年5月14日火曜日 1:33 ✅回答済み

昔同じようなことをしました。
CoerceValueCallback は同じ理由で諦めたと思います。

で、UIElement.OnPreviewTextInput と TextBoxBase.OnTextChanged を使う方法に変えました。
ただ、OnTextChanged では、実際には入力制限できないので、強制的に Text を書き換え(復元)なければいけません。
また、この場合、カレットの位置や選択状態も復元する必要があります。

本来は、OnPreviewTextInput での制限にとどめ、Validation で入力値をチェックする方がいいと思います。

参考までに、コードを載せておきます。

protected override void OnPreviewTextInput( TextCompositionEventArgs e )
{
  e.Handled = !IsAllowed( e.Text );
  base.OnPreviewTextInput( e );
}

protected override void OnTextChanged( TextChangedEventArgs e )
{
  base.OnTextChanged( e );
  if ( !CheckText( Text ) ) {
    RestoreState();
    return;
  }
  StoreState();
}

IsAllowed で入力しようとしている文字をチェックしています。(1文字しか来ないはずです)
CheckText では、文字列全体をチェックし、StoreState と RestoreState で Text や CaretIndex 等のプロパティを操作しています。
その他、OnSelectionChanged等でもStoreStateする必要があります。


2013年5月14日火曜日 8:57 ✅回答済み

カレントと新しい値の比較周りで残念なバグの香りがします・・・なにかよい解決方法はありませんでしょうか。

気持ちはわかりますが、CoerceCallBackはプロパティ値を決定する前に実行されるため、プロパティに現在の値と同じ値をセットした時に何もしないというのはわからなくもない仕様だと思います。
しかし、そうだとしても強制的に表示されている値を再表示する手段があってもよいとは思います。今回はこの手段がよくわかりませんでしたので、以下のような違うアプローチを取ってみました。

public class MyTextBox : TextBox
{
    public MyTextBox()
    {
        DataObject.AddPastingHandler(this, new DataObjectPastingEventHandler(OnPaste));
    }

    //貼り付けを防ぐ
    private void OnPaste(object sender, DataObjectPastingEventArgs e)
    {
        if (e.DataObject.GetDataPresent(typeof(String)))
        {
            String text = (String)e.DataObject.GetData(typeof(String));

            decimal decValue;

            if (! decimal.TryParse(text, out decValue))
            {
                e.CancelCommand();
            }
        }
        else
        {
            e.CancelCommand();
        }
    }

    protected override void OnPreviewTextInput(TextCompositionEventArgs e)
    {
        decimal decValue;

        if (!decimal.TryParse(e.Text, out decValue))
            e.Handled = true;

        base.OnPreviewTextInput(e);
    }
}

★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/


2013年5月15日水曜日 0:21

返信ありがとうございます。

その後、こちらでも調査を続けておりましたが解決策はみつかりませんでした。

私も trapemiya さんの仰るとおり、CoerceCallBack によって現在値と同値に強制した場合は“値が変更されなかった”として何もしないのが正だと思います。

それを踏まえても、内部値と表示値の相違が発生している状態はコントロールとして不正な状態ですので、きっと再表示する方法があるはずと期待しました。

※画面上は "A" と表示されているのに、Text プロパティにアクセスすると "B" が取れる状態を仕様と言うのは厳しいですよね。

今回はお二人のご意見を参考に PreviewTextInput や TextChanged で制御しようと思います。

貴重なご意見ありがとうございました。