ViewMakerにみるMVVMの実装(3)

今回はView側で行う入力検証処理です。WPFSilverlightも同時的な入力検証処理を持っていますが、これらは通常画面上で入力されたタイミングでチェック処理が発生します。しかし業務アプリではボタン押下時などに強制的に全入力項目をチェックしたり、一部分の領域をチェックする必要があります。

強制的な入力検証の実行

入力検証を強制的に実行するためには、WPFSilverlightが入力時に行う「ソース(ViewModel)更新処理(UpdateSource)」をプログラムから実行すれば可能です。VisualTreeを探索しながらバインドしているコントロールを探して再帰的に実行していけば特定の領域の入力検証を強制できます。

    /// Silverlightのコントロールの機能を拡張
    internal static class SlFrameworkExtension
    {
        /// 強制的な入力検証を行う対象のコントロールのプロパティ
        internal static Dictionary<Type, DependencyProperty[]> targetProperty;
        static SlFrameworkExtension()
        {
            targetProperty = new Dictionary<Type, DependencyProperty[]>();
            targetProperty.Add(typeof(TextBox), new DependencyProperty[] { TextBox.TextProperty });
            targetProperty.Add(typeof(Selector), new DependencyProperty[] { Selector.SelectedItemProperty, Selector.SelectedValueProperty });
            targetProperty.Add(typeof(ToggleButton), new DependencyProperty[] { ToggleButton.IsCheckedProperty });
            targetProperty.Add(typeof(DatePicker), new DependencyProperty[] { DatePicker.TextProperty, DatePicker.SelectedDateProperty });
        }

        /// 指定したコントロールの領域を強制的にUpdateSourceする。これによって検証処理が実行される。
        public static void UpdateBindingSource(this FrameworkElement element, string name)
        {
            var ctrl = element as Control;
            if (ctrl != null && !ctrl.IsEnabled) return;

            if (name == null || element.Name == name)
            {
                foreach (var dict in targetProperty)
                {
                    if (dict.Key.IsAssignableFrom(element.GetType()))
                    {
                        foreach (DependencyProperty dp in dict.Value)
                        {
                            var expression = element.GetBindingExpression(dp);
                            if (expression == null) continue;
                            var bind = expression.ParentBinding;
                            if (bind.Mode == BindingMode.OneTime || bind.Mode == BindingMode.OneWay) continue;
                            expression.UpdateSource();
                        }
                    }
                }
                name = null;
            }
            foreach (var child in element.GetVisualChildren())
            {
                var fe = child as FrameworkElement;
                if (fe != null) UpdateBindingSource(fe, name);
            }
        }

領域を論理化する

MVVMではViewModelがViewの特定の実装に依存することは避ける必要があります。したがってViewにおけるコントロールの具体的な構造を前提にすることは避ける必要があります。ただこの考えをViewModelで想定した構造にViewは従う必要があるという逆の依存関係で考えると、ViewModelが期待したコントロール構造をViewで実装するという考え方にもなります。要は、Viewのコントロール名≒論理的な領域を前提にすることはMVVM上でも可能であると判断できます。上記のUpdateBindingSourceの実装はその前提のもとに作成されています。
もちろんこれを綺麗でないと思う方は、領域を表す添付プロパティを利用するアイデアもあります。ただ追加の仕組みを作成する必要があるので今回は避けました。

IDataErrorInfoではだめなのか

WPFSilverlightでは入力エラーを通知するための仕組みとしてViewModel上で細かな制御ができるIDataErrorInfoを用意しています。上記のような方式をとらなくてもIDataErrorInfoで対応してしまうと簡単にViewModel上で入力検証の制御ができます。ただIDataErrorInfoの欠点として、ViewModelのプロパティに代入可能な値の場合だけ制御できるのであってそ代入不可の場合には制御できません。数値型の項目に英字を入力してもIDataErrorInfoではエラーメッセージを通知できません。またViewModel側でViewにエラーがあることさえも検知できません。したがって、どうしても前述のようなView側での入力検証の仕組みとの組み合わせになります。なお、IDataErrorInfoについてもViewMakerでサポートしています。

入力検証のまとめ

・入力検証の強制的な実行は、View側でUpdateSourceを利用することで実現可能。
・領域指定はVisual Treeの構造を使いコントロール名を使うのが簡単
・IDataErrorInfoと上手く組み合わせ利用する