1

我有一个控件,它最基本的级别是一个带有 StackPanel(方向=垂直)的 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 (递归地)侦听其子项并在其中任何一个处于错误状态时调用该方法。

所以我的问题是:

  1. 这两个选项中哪一个更好,您将如何实施它们?

  2. 有没有更好的方法来做到这一点?(行为等?)它必须是 MVVM。

注意。GetFirstChildControlWithError 改编自这个问题。如何按名称或类型查找 WPF 控件?

4

1 回答 1

1

在以下假设下工作:

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

基本上,您想监听 DataContext 属性更改并找出是否存在 DataError。

如果您查看行为,您可以在不继承自的情况下解决此问题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)
    {
        //...
    }
}
于 2013-10-18T14:21:07.523 回答