使用MVVM将错误的WPF TextBox滚动到视图中

时间:2013-10-18 13:58:08

标签: c# wpf mvvm

我有一个控件,在最基本的级别是一个带有StackPanel(Orientation = Vertical)的ScrollViewer,里面有很多TextBox。

<ScrollViewer>
    <StackPanel x:Name="MyStackPanel"
                Orientation="Vertical">
        <TextBox Text="{Binding PropertyA, ValidatesOnDataErrors=True}" />
        <TextBox Text="{Binding PropertyB, ValidatesOnDataErrors=True}" />
        <TextBox Text="{Binding PropertyC, ValidatesOnDataErrors=True}" />
        <!-- ... -->
        <TextBox Text="{Binding PropertyX, ValidatesOnDataErrors=True}" />
        <TextBox Text="{Binding PropertyY, ValidatesOnDataErrors=True}" />
        <TextBox Text="{Binding PropertyZ, ValidatesOnDataErrors=True}" />
    </StackPanel>
</ScrollViewer>

我想在发生错误时将带有错误的任何控件滚动到视图中。因此,例如,如果用户位于列表顶部且绑定到PropertyX的TextBox出错,那么我希望ScrollViewer滚动到它。

目前我从ScrollViewer继承并添加了以下方法。

    public void ScrollErrorTextBoxIntoView()
    {
        var controlInError = GetFirstChildControlWithError(this);

        if (controlInError == null)
        {
            return;
        }            
            controlInError.BringIntoView();
        }
    }

    public Control GetFirstChildControlWithError(DependencyObject parent)
    {
        if (parent == null)
        {
            return null;
        }

        Control findChildInError = null;

        var children = LogicalTreeHelper.GetChildren(parent).OfType<DependencyObject>();

        foreach (var child in children)
        {
            var childType = child as Control;
            if (childType == null)
            {
                findChildInError = GetFirstChildControlWithError(child);

                if (findChildInError != null)
                {
                    break;
                }
            }
            else
            {
                var frameworkElement = child as FrameworkElement;

                // If the child is in error
                if (Validation.GetHasError(frameworkElement))
                {
                    findChildInError = (Control)child;
                    break;
                }
            }
        }

        return findChildInError;
    }

我很难让它正常工作。我看到它的方式,我有两个选择。

  1. 尝试让ViewModel执行ScrollErrorTextBoxIntoView方法。我不确定这样做的最佳方法是什么。我试图设置一个属性并从那起行动,但它看起来并不正确(并且它无论如何也没有用)

  2. 让控件以独立的方式执行。这将要求我的ScrollViewer监听其子节点(递归地),并在其中任何一个处于错误状态时调用该方法。

  3. 所以我的问题是:

    1. 这两个选项中哪一个更好,你会如何实现它们?

    2. 有更好的方法吗? (行为等?)必须是MVVM。

    3. NB。 GetFirstChildControlWithError是根据这个问题改编的。 How can I find WPF controls by name or type?

1 个答案:

答案 0 :(得分:1)

在以下假设下工作:

  • 您的视图模型正确实现了INotifyPropertyChangedIDataErrorInfo
  • 当至少有一个属性出现验证错误时,IDataErrorInfo.Error属性不为空。
  • 您希望保持严格的M-VM分离;因此,ViewModel不应该调用仅用于调整视图的方法。

基本上,您希望监听DataContext属性更改并查明是否存在DataError。

如果您查看behaviors,则可以在不继承ScrollViewer的情况下解决此问题。

以下是一个示例实现:

using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

public class ScrollToFirstInvalidElementBehavior : Behavior<ScrollViewer>
{
    protected override void OnAttached()
    {
        ResetEventHandlers(null, AssociatedObject.DataContext);
        AssociatedObject.DataContextChanged += OnDataContextChanged;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.DataContextChanged -= OnDataContextChanged;
    }

    private void OnDataContextChanged(object sender, 
          DependencyPropertyChangedEventArgs e)
    {
        ResetEventHandlers(e.OldValue, e.NewValue);
    }

    private void ResetEventHandlers(object oldValue, object newValue)
    {
        var oldContext = oldValue as INotifyPropertyChanged;
        if (oldContext != null)
        {
            oldContext.PropertyChanged -= OnDataContextPropertyChanged;
        }

        var newContext = newValue as INotifyPropertyChanged;
        if (newContext is IDataErrorInfo)
        {
            newContext.PropertyChanged += OnDataContextPropertyChanged;
        }
    }

    private void OnDataContextPropertyChanged(object sender, 
         PropertyChangedEventArgs e)
    {
        var dataError = (IDataErrorInfo) sender;

        if (!string.IsNullOrEmpty(dataError.Error))
        {
            var controlInError = GetFirstChildControlWithError(AssociatedObject);
            if (controlInError != null)
            {
                controlInError.BringIntoView();
            }

        }
    }

    private Control GetFirstChildControlWithError(ScrollViewer AssociatedObject)
    {
        //...
    }
}