none
入力エラーが起きた時、フォーカスを移動させなくするには RRS feed

  • 質問

  • WPF+Livetの環境でMVVMパターンを使ってデータ入力用のアプリケーションを作成しています。

    金額入力用のTextBoxで数字以外が入力された場合、メッセージボックスを表示し、

    不正な値(数値に変換できない値)が入力されている間は、金額入力用のTextBoxからフォーカスを

    移動したくないのですが、どのように実装すればよいでしょうか。

    VMはIDataErrorInfoとLivetのViewModelの派生クラスになっています。

    よろしくご教授お願いいたします。

    2013年11月5日 11:09

回答

  • PreviewLostKeyboardFocusイベントで、現在のコントロールに検証エラーがある場合にはフォーカス移動をキャンセルしてみる。
    #Livetは使ってないから、Livetの流儀にあってるかはわかりません。

    <Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:app="clr-namespace:WpfApplication1"
            Title="MainWindow" Height="350" Width="525"
            PreviewLostKeyboardFocus="Window_PreviewLostKeyboardFocus" >
        <Window.BindingGroup>
            <BindingGroup Name="check" />
        </Window.BindingGroup>
        <Grid>
            <StackPanel>
                <TextBox Text="バインディングしてないTextBox"/>
                <TextBox >
                    <TextBox.Text>
                        <Binding Path="NumberString"  ValidatesOnDataErrors="true" /> <!-- IDataErrorInfoを使ってVM層で検証 -->
                    </TextBox.Text>
                </TextBox>
                <TextBox>
                    <TextBox.Text>
                        <Binding Path="Data.Number" ValidatesOnDataErrors="true">
                            <Binding.ValidationRules>
                                <app:NumberValidationRule ValidationStep="RawProposedValue"/><!-- 変換する前に検証する -->
                            </Binding.ValidationRules>
                        </Binding>
                    </TextBox.Text>
                </TextBox>
            </StackPanel>
        </Grid>
    </Window>
    namespace WpfApplication1
    {
        public partial class MainWindow : System.Windows.Window
        {
            public MainWindow()
            {
                InitializeComponent();
                this.DataContext = new VM();
            }
    
            private void Window_PreviewLostKeyboardFocus(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e)
            {//キーボードフォーカスが変更される前のイベント
    
                //BindingGroupに属しているBindingを強制的に検証させる。
                this.BindingGroup.CommitEdit();
    
                System.Windows.DependencyObject dp = e.OldFocus as System.Windows.DependencyObject;
                if (dp != null && System.Windows.Controls.Validation.GetHasError(dp))
                {
                    //フォーカスを持っているコントロールが検証エラーの場合にはメッセージを表示。
                    System.Windows.MessageBox.Show(System.Windows.Controls.Validation.GetErrors(dp)[0].ErrorContent.ToString());
                    //フォーカス移動をキャンセルさせる。
                    e.Handled = true;
                }
            }
        }
    
    //ここから下はVMとか作っているだけ
        class VMBase : System.ComponentModel.IDataErrorInfo, System.ComponentModel.INotifyPropertyChanged
        {
            #region IDataErrorInfo メンバー
    
            private System.Collections.Generic.Dictionary<string, string> errors 
                = new System.Collections.Generic.Dictionary<string, string>();
    
            public string Error
            {
                get
                {
                    System.Text.StringBuilder sb = new System.Text.StringBuilder();
                    foreach (string s in errors.Values)
                    {
                        sb.AppendLine(s);
                    }
                    return sb.ToString();
                }
            }
    
            public string this[string columnName]
            {
                get { return errors[columnName]; }
            }
            protected void SetError(string column, string err)
            {
                this.errors[column] = err;
            }
            protected void ClearError(string column)
            {
                this.errors.Remove(column);
            }
    
    
            #endregion
    
            #region INotifyPropertyChanged メンバー
    
            public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
            protected virtual void OnPropertyChanged(string name)
            {
                var pc = PropertyChanged;
                if (pc != null)
                {
                    pc(this, new System.ComponentModel.PropertyChangedEventArgs(name));
                }
            }
            #endregion
        }
    
        class VM : VMBase
        {
            public VM()
            {
                this.Data = new Data();
            }
    
            public Data Data { get; private set; }
    
            public string NumberString
            {
                get
                {
                    return Data.Number.ToString();
                }
                set
                {
                    double d;
                    if (double.TryParse(value, out d))
                    {
                        this.Data.Number = d;
                        base.ClearError("NumberString");
                    }
                    else
                    {
                        base.SetError("NumberString", "IDataErrorInfo 数値を入力してください");
                    }
                    OnPropertyChanged("NumberString");
                }
            }
        }
    
        class Data
        {
            public double Number { get; set; }
        }
    
        class NumberValidationRule : System.Windows.Controls.ValidationRule
        {
            public override System.Windows.Controls.ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
            {
                bool canConvert = false;
                if (value == null)
                {
                    return new System.Windows.Controls.ValidationResult(false, "入力してください");
                }
                System.IConvertible ic = value as System.IConvertible;
                if (ic != null)
                {
                    try
                    {
                        double d = ic.ToDouble(cultureInfo);
                        canConvert = true;
                    }
                    catch
                    {
                    }
                }
                else
                {
                    double d;
                    canConvert = double.TryParse(value.ToString(), out d);
                }
                if (!canConvert)
                {
                    return new System.Windows.Controls.ValidationResult(false, "ValidationRule 数値に変換できません");
                }
                else
                {
                    return new System.Windows.Controls.ValidationResult(true, null);
                }
            }
        }
    }
    VB.NetかC#かわからなかったのでC#で書いてますが、VB.NetでもPreviewLostKeyboardFocusのところを追加すればわかると思います。


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答の候補に設定 星 睦美 2013年11月7日 1:52
    • 回答としてマーク すぴか 2013年11月7日 11:29
    2013年11月5日 15:34

すべての返信

  • PreviewLostKeyboardFocusイベントで、現在のコントロールに検証エラーがある場合にはフォーカス移動をキャンセルしてみる。
    #Livetは使ってないから、Livetの流儀にあってるかはわかりません。

    <Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:app="clr-namespace:WpfApplication1"
            Title="MainWindow" Height="350" Width="525"
            PreviewLostKeyboardFocus="Window_PreviewLostKeyboardFocus" >
        <Window.BindingGroup>
            <BindingGroup Name="check" />
        </Window.BindingGroup>
        <Grid>
            <StackPanel>
                <TextBox Text="バインディングしてないTextBox"/>
                <TextBox >
                    <TextBox.Text>
                        <Binding Path="NumberString"  ValidatesOnDataErrors="true" /> <!-- IDataErrorInfoを使ってVM層で検証 -->
                    </TextBox.Text>
                </TextBox>
                <TextBox>
                    <TextBox.Text>
                        <Binding Path="Data.Number" ValidatesOnDataErrors="true">
                            <Binding.ValidationRules>
                                <app:NumberValidationRule ValidationStep="RawProposedValue"/><!-- 変換する前に検証する -->
                            </Binding.ValidationRules>
                        </Binding>
                    </TextBox.Text>
                </TextBox>
            </StackPanel>
        </Grid>
    </Window>
    namespace WpfApplication1
    {
        public partial class MainWindow : System.Windows.Window
        {
            public MainWindow()
            {
                InitializeComponent();
                this.DataContext = new VM();
            }
    
            private void Window_PreviewLostKeyboardFocus(object sender, System.Windows.Input.KeyboardFocusChangedEventArgs e)
            {//キーボードフォーカスが変更される前のイベント
    
                //BindingGroupに属しているBindingを強制的に検証させる。
                this.BindingGroup.CommitEdit();
    
                System.Windows.DependencyObject dp = e.OldFocus as System.Windows.DependencyObject;
                if (dp != null && System.Windows.Controls.Validation.GetHasError(dp))
                {
                    //フォーカスを持っているコントロールが検証エラーの場合にはメッセージを表示。
                    System.Windows.MessageBox.Show(System.Windows.Controls.Validation.GetErrors(dp)[0].ErrorContent.ToString());
                    //フォーカス移動をキャンセルさせる。
                    e.Handled = true;
                }
            }
        }
    
    //ここから下はVMとか作っているだけ
        class VMBase : System.ComponentModel.IDataErrorInfo, System.ComponentModel.INotifyPropertyChanged
        {
            #region IDataErrorInfo メンバー
    
            private System.Collections.Generic.Dictionary<string, string> errors 
                = new System.Collections.Generic.Dictionary<string, string>();
    
            public string Error
            {
                get
                {
                    System.Text.StringBuilder sb = new System.Text.StringBuilder();
                    foreach (string s in errors.Values)
                    {
                        sb.AppendLine(s);
                    }
                    return sb.ToString();
                }
            }
    
            public string this[string columnName]
            {
                get { return errors[columnName]; }
            }
            protected void SetError(string column, string err)
            {
                this.errors[column] = err;
            }
            protected void ClearError(string column)
            {
                this.errors.Remove(column);
            }
    
    
            #endregion
    
            #region INotifyPropertyChanged メンバー
    
            public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
            protected virtual void OnPropertyChanged(string name)
            {
                var pc = PropertyChanged;
                if (pc != null)
                {
                    pc(this, new System.ComponentModel.PropertyChangedEventArgs(name));
                }
            }
            #endregion
        }
    
        class VM : VMBase
        {
            public VM()
            {
                this.Data = new Data();
            }
    
            public Data Data { get; private set; }
    
            public string NumberString
            {
                get
                {
                    return Data.Number.ToString();
                }
                set
                {
                    double d;
                    if (double.TryParse(value, out d))
                    {
                        this.Data.Number = d;
                        base.ClearError("NumberString");
                    }
                    else
                    {
                        base.SetError("NumberString", "IDataErrorInfo 数値を入力してください");
                    }
                    OnPropertyChanged("NumberString");
                }
            }
        }
    
        class Data
        {
            public double Number { get; set; }
        }
    
        class NumberValidationRule : System.Windows.Controls.ValidationRule
        {
            public override System.Windows.Controls.ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
            {
                bool canConvert = false;
                if (value == null)
                {
                    return new System.Windows.Controls.ValidationResult(false, "入力してください");
                }
                System.IConvertible ic = value as System.IConvertible;
                if (ic != null)
                {
                    try
                    {
                        double d = ic.ToDouble(cultureInfo);
                        canConvert = true;
                    }
                    catch
                    {
                    }
                }
                else
                {
                    double d;
                    canConvert = double.TryParse(value.ToString(), out d);
                }
                if (!canConvert)
                {
                    return new System.Windows.Controls.ValidationResult(false, "ValidationRule 数値に変換できません");
                }
                else
                {
                    return new System.Windows.Controls.ValidationResult(true, null);
                }
            }
        }
    }
    VB.NetかC#かわからなかったのでC#で書いてますが、VB.NetでもPreviewLostKeyboardFocusのところを追加すればわかると思います。


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答の候補に設定 星 睦美 2013年11月7日 1:52
    • 回答としてマーク すぴか 2013年11月7日 11:29
    2013年11月5日 15:34
  • 回答でなくて恐縮なのですが、こういう実装をすると、入力をやめてこのウィンドウを閉じようとしたときにテキストボックスになにか正しい値を入力しないと閉じられなくなってしまう可能性があるのでご注意ください。
    2013年11月5日 17:41
  • 私も回答ではないのですが、このような実装をあえて必要があって行うのであれば良いのですが、どうしてもこのようにする理由が無ければ、他の実装を検討されることをお勧めします。
    もし、テキストボックスに限らず、入力する項目がたくさんある場合、一つずつこのような実装をするのは煩雑ですし、ユーザーにとっても煩わしく感じる場合があるように思います。
    IDataErrorInfoを使っているのであれば、エラーの場合、デフォルトでテキストボックスの周りが赤くなるはずですので、それに加えてToolTipでエラー内容を表示し、エラーがあれば例えば登録ボタンを不活性にする実装などが、代替として考えられます。

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

    2013年11月6日 0:51
    モデレータ
  • OnPreview~のオーバーライドでなんとか実現できそうです。

    ありがとうございました。

    2013年11月6日 11:46
  • 確かにWPF的な実装ではないです。

    FormsかAccessVBAを使った方がいいのかもしれません。

    まだ、開発環境選定の調査期間なので、WPFで実現できないかと思った次第です。

    ちなみに、エンドユーザさんがPC初心者で、データエントリーに使うアプリです。

    データ入力中も、モニタを見ている時間よりキーボード見ている時間の方が長いです。(タッチタイピングできないので)

    なので、入力エラーがあったら即メッセージを表示した方がいいと思ったわけです。

    2013年11月6日 11:58