C# 使用MVVM将出错的WPF文本框滚动到视图中

C# 使用MVVM将出错的WPF文本框滚动到视图中,c#,wpf,mvvm,C#,Wpf,Mvvm,我有一个控件,最基本的级别是一个ScrollViewer,它有一个StackPanel(方向=垂直),里面有很多文本框 <ScrollViewer> <StackPanel x:Name="MyStackPanel" Orientation="Vertical"> <TextBox Text="{Binding PropertyA, ValidatesOnDataErrors=True}" />

我有一个控件,最基本的级别是一个ScrollViewer,它有一个StackPanel(方向=垂直),里面有很多文本框

<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的文本框出错,那么我希望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;
    }
public void ScrollErrorTextBoxIntoView()
{
var controlInError=GetFirstChildControlWithError(此);
如果(controlInError==null)
{
返回;
}            
controlInError.BringIntoView();
}
}
公共控件GetFirstChildControlWithError(DependencyObject父对象)
{
如果(父项==null)
{
返回null;
}
控件findChildInError=null;
var children=logicaltreeheloper.GetChildren(parent).OfType();
foreach(儿童中的儿童变量)
{
var childType=作为控件的子对象;
if(childType==null)
{
findChildInError=GetFirstChildControlWithError(子级);
if(findChildInError!=null)
{
打破
}
}
其他的
{
var frameworkElement=作为frameworkElement的子级;
//如果孩子出错了
if(Validation.GetHasError(frameworkElement))
{
findChildInError=(控制)子级;
打破
}
}
}
返回findChildiner错误;
}
我很难让它正常工作。在我看来,我有两个选择

  • 尝试获取ViewModel以执行ScrollErrorTextBoxIntoView方法。我不知道最好的方法是什么。我试图设置一个属性,并以此为基础进行操作,但它似乎不正确(而且无论如何也不起作用)

  • 让控件以自包含的方式执行此操作。这将要求我的ScrollViewer(递归地)侦听其子对象,并在其中任何一个处于错误状态时调用该方法

  • 因此,我的问题是:

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

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


  • 注意。GetFirstChildControlWithError是根据这个问题改编的

    在以下假设下工作:

    • 视图模型正确地实现了
      INotifyPropertyChanged
      IDataErrorInfo
    • 当至少有一个属性存在验证错误时,
      IDataErrorInfo.Error
      属性不为null
    • 您希望保持严格的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)
        {
            //...
        }
    }
    
    使用System.ComponentModel;
    使用System.Windows;
    使用System.Windows.Controls;
    使用System.Windows.Interactive;
    公共类ScrollToFirstInvalidElementBehavior:Behavior
    {
    受保护的覆盖无效附加()
    {
    ResetEventHandlers(null,AssociatedObject.DataContext);
    AssociatedObject.DataContextChanged+=OnDataContextChanged;
    }
    附加时受保护的覆盖无效()
    {
    AssociatedObject.DataContextChanged-=OnDataContextChanged;
    }
    私有void OnDataContextChanged(对象发送方,
    DependencyPropertyChangedEventArgs(附件e)
    {
    ResetEventHandlers(e.OldValue、e.NewValue);
    }
    私有void resetEventHandler(对象oldValue、对象newValue)
    {
    var oldContext=作为INotifyPropertyChanged的oldValue;
    if(oldContext!=null)
    {
    oldContext.PropertyChanged-=OnDataContextPropertyChanged;
    }
    var newContext=INotifyPropertyChanged的新值;
    if(新上下文为IDataErrorInfo)
    {
    newContext.PropertyChanged+=OnDataContextPropertyChanged;
    }
    }
    私有void OnDataContextPropertyChanged(对象发送方,
    PropertyChangedEventArgs(e)
    {
    var dataError=(IDataErrorInfo)发送方;
    如果(!string.IsNullOrEmpty(dataError.Error))
    {
    var controlInError=GetFirstChildControlWithError(关联对象);
    如果(controlInError!=null)
    {
    controlInError.BringIntoView();
    }
    }
    }
    私有控件GetFirstChildControlWithError(ScrollViewer关联对象)
    {
    //...
    }
    }
    
    在以下假设下工作:

    • 视图模型正确地实现了
      INotifyPropertyChanged
      IDataErrorInfo
    • 当至少有一个属性存在验证错误时,
      IDataErrorInfo.Error
      属性不为null
    • 您希望保持严格的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)
        {
            //...
        }
    }
    
    使用System.ComponentModel;
    使用System.Windows;
    使用System.Windows.Controls;
    使用System.Windows.Interactive;
    公共类ScrollToFirstInvalidElementBehavior:Behavior
    {
    受保护的覆盖无效附加()
    {
    ResetEventHandlers(空,关联到