使用 MVVM 实现使用 IDataErrorInfo 验证数据时出现异常

发布于 2024-09-06 22:33:46 字数 8324 浏览 3 评论 0原文

我正在尝试使用 IDataErrorInfo 验证 MVVM 应用程序中的数据,但遇到了一些问题。

当我将 TextBox 设置为无效值时,验证工作正常。但是在我将 TextBox 的值设置为有效值后,我得到了这个异常:

A first chance exception of type 'System.ArgumentOutOfRangeException' occurred in mscorlib.dll
A first chance exception of type 'System.Reflection.TargetInvocationException' occurred in mscorlib.dll
System.Windows.Data Error: 16 : Cannot get 'Item[]' value (type 'ValidationError') from '(Validation.Errors)' (type 'ReadOnlyObservableCollection`1'). BindingExpression:Path=(0).[0].ErrorContent; DataItem='TextBox' (Name='txtRunAfter'); target element is 'TextBox' (Name='txtRunAfter'); target property is 'ToolTip' (type 'Object') TargetInvocationException:'System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
    at System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
    at System.ThrowHelper.ThrowArgumentOutOfRangeException()
    at System.Collections.Generic.List`1.get_Item(Int32 index)
    at System.Collections.ObjectModel.Collection`1.get_Item(Int32 index)
    at System.Collections.ObjectModel.ReadOnlyCollection`1.get_Item(Int32 index)
    --- End of inner exception stack trace ---
    at System.RuntimeMethodHandle._InvokeMethodFast(Object target, Object[] arguments, SignatureStruct& sig, MethodAttributes methodAttributes, RuntimeTypeHandle typeOwner)
    at System.RuntimeMethodHandle.InvokeMethodFast(Object target, Object[] arguments, Signature sig, MethodAttributes methodAttributes, RuntimeTypeHandle typeOwner)
    at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture, Boolean skipVisibilityChecks)
    at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
    at System.Reflection.RuntimePropertyInfo.GetValue(Object obj, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture)
    at MS.Internal.Data.PropertyPathWorker.GetValue(Object item, Int32 level)
    at MS.Internal.Data.PropertyPathWorker.RawValue(Int32 k)'

这是视图的代码:

    <UserControl x:Class="Telbit.TeStudio.View.Controls.TestStepListingStepView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Background="{Binding BackgroundColor}">

    <UserControl.Resources>
        <Style x:Key="TestStepTextBox" TargetType="{x:Type TextBox}">
            <Setter Property="Background" Value="Transparent" />
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="BorderBrush" Value="Transparent"/>
            <Setter Property="VerticalContentAlignment" Value="Center"/>
            <Setter Property="HorizontalContentAlignment" Value="Left"/>
            <Setter Property="TextElement.FontSize" Value="10"/>
            <Setter Property="TextElement.FontWeight" Value="Regular"/>
            <Setter Property="Validation.ErrorTemplate" Value="{x:Null}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBox}">
                        <Border x:Name="Bd" SnapsToDevicePixels="true" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
                            <ScrollViewer x:Name="PART_ContentHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="true">
                                <Setter Property="BorderBrush" Value="#3d62a9"/>
                            </Trigger>
                            <Trigger Property="IsFocused" Value="true">
                                <Setter Property="BorderBrush" Value="#3d62a9"/>
                                <Setter Property="Background" Value="White"/>
                            </Trigger>
                            <Trigger Property="Validation.HasError" Value="true">
                                <Setter Property="ToolTip"
                                    Value="{Binding RelativeSource={RelativeSource Self}, 
                                    Path=(Validation.Errors)[0].ErrorContent}"/>
                                <Setter Property="Background" Value="#33FF342D"/>
                                <Setter Property="BorderBrush" Value="#AAFF342D"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>

    ...

    <TextBox Name="txtRunAfter" Grid.Column="4" Text="{Binding RunAfter, ValidatesOnDataErrors=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource TestStepTextBox}"
         LostFocus="TextBoxLostFocus" PreviewKeyDown="TextBoxPreviewKeyDown" PreviewTextInput="TextBoxPreviewTextInput"/>

    ...

 </UserControl>

这是 ViewModel 的代码:

class TestStepListingStepViewModel : ViewModelBase, IDataErrorInfo
{
    private int _runAfter = 0;
    public int RunAfter
    {
        get
        {
            return _runAfter;
        }

        set
        {
            if (_runAfter != value)
            {
                _runAfter = value;
                OnPropertyChanged("RunAfter");
            }
        }
    }

string IDataErrorInfo.Error
    {
        get { return null; }
    }

    string IDataErrorInfo.this[string columnName]
    {
        get
        {
            string message = null;
            if (columnName == "RunAfter")
                message = validateRunAfter();

            return message;
        }
    }

    private string validateRunAfter()
    {
        if (_runAfter >= _order)
            return "Run After value must be less than its Step Order (#) value.";

        return null;
    }
}

我花了两天时间试图弄清楚这有什么问题!一个有一双新眼睛的人能弄清楚吗?

编辑: 以下是 TextBoxs 处理程序的代码:

public partial class TestStepListingStepView : UserControl
{
    private string mInvalidCharPattern = "[^0-9]";

    public TestStepListingStepView()
    {
        InitializeComponent();

        DataObject.AddPastingHandler(this.txtRunAfter, new DataObjectPastingEventHandler(TextBoxPasting));
    }

    private void TextBoxLostFocus(object sender, RoutedEventArgs e)
    {
        TextBox txt = sender as TextBox;

        if (txt != null && string.IsNullOrEmpty(txt.Text))
            txt.Text = "0";
    }

    // Catch the space character, since it doesn't trigger PreviewTextInput
    private void TextBoxPreviewKeyDown(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.Space) { e.Handled = true; }
    }

    // Do most validation here
    private void TextBoxPreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        if (ValidateTextInput(e.Text) == false) { e.Handled = true; }
    }

    // Prevent pasting invalid characters
    private void TextBoxPasting(object sender, DataObjectPastingEventArgs e)
    {
        string lPastingText = e.DataObject.GetData(DataFormats.Text) as string;
        if (ValidateTextInput(lPastingText) == false) { e.CancelCommand(); }
    }

    // Do the validation in a separate function which can be reused
    private bool ValidateTextInput(string aTextInput)
    {
        if (aTextInput == null) { return false; }

        Match lInvalidMatch = Regex.Match(aTextInput, this.mInvalidCharPattern);
        return (lInvalidMatch.Success == false);
    }

}

另外,我使用的是 .Net Framework 3.5 版本。 我的应用程序非常复杂,因此我无法创建一个仅重新创建这部分的小项目。我希望你们中的一些人已经遇到过这个问题并且知道如何解决它。

再次感谢大家!

I'm trying to validate data in my MVVM application using IDataErrorInfo, but I'm running into some problems.

When I set my TextBox with an invalid value, the validation works fine. But after I set the value of the TextBox to a valid value and I get this exception:

A first chance exception of type 'System.ArgumentOutOfRangeException' occurred in mscorlib.dll
A first chance exception of type 'System.Reflection.TargetInvocationException' occurred in mscorlib.dll
System.Windows.Data Error: 16 : Cannot get 'Item[]' value (type 'ValidationError') from '(Validation.Errors)' (type 'ReadOnlyObservableCollection`1'). BindingExpression:Path=(0).[0].ErrorContent; DataItem='TextBox' (Name='txtRunAfter'); target element is 'TextBox' (Name='txtRunAfter'); target property is 'ToolTip' (type 'Object') TargetInvocationException:'System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
    at System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
    at System.ThrowHelper.ThrowArgumentOutOfRangeException()
    at System.Collections.Generic.List`1.get_Item(Int32 index)
    at System.Collections.ObjectModel.Collection`1.get_Item(Int32 index)
    at System.Collections.ObjectModel.ReadOnlyCollection`1.get_Item(Int32 index)
    --- End of inner exception stack trace ---
    at System.RuntimeMethodHandle._InvokeMethodFast(Object target, Object[] arguments, SignatureStruct& sig, MethodAttributes methodAttributes, RuntimeTypeHandle typeOwner)
    at System.RuntimeMethodHandle.InvokeMethodFast(Object target, Object[] arguments, Signature sig, MethodAttributes methodAttributes, RuntimeTypeHandle typeOwner)
    at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture, Boolean skipVisibilityChecks)
    at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
    at System.Reflection.RuntimePropertyInfo.GetValue(Object obj, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture)
    at MS.Internal.Data.PropertyPathWorker.GetValue(Object item, Int32 level)
    at MS.Internal.Data.PropertyPathWorker.RawValue(Int32 k)'

Here is the code for the view:

    <UserControl x:Class="Telbit.TeStudio.View.Controls.TestStepListingStepView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Background="{Binding BackgroundColor}">

    <UserControl.Resources>
        <Style x:Key="TestStepTextBox" TargetType="{x:Type TextBox}">
            <Setter Property="Background" Value="Transparent" />
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="BorderBrush" Value="Transparent"/>
            <Setter Property="VerticalContentAlignment" Value="Center"/>
            <Setter Property="HorizontalContentAlignment" Value="Left"/>
            <Setter Property="TextElement.FontSize" Value="10"/>
            <Setter Property="TextElement.FontWeight" Value="Regular"/>
            <Setter Property="Validation.ErrorTemplate" Value="{x:Null}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBox}">
                        <Border x:Name="Bd" SnapsToDevicePixels="true" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
                            <ScrollViewer x:Name="PART_ContentHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="true">
                                <Setter Property="BorderBrush" Value="#3d62a9"/>
                            </Trigger>
                            <Trigger Property="IsFocused" Value="true">
                                <Setter Property="BorderBrush" Value="#3d62a9"/>
                                <Setter Property="Background" Value="White"/>
                            </Trigger>
                            <Trigger Property="Validation.HasError" Value="true">
                                <Setter Property="ToolTip"
                                    Value="{Binding RelativeSource={RelativeSource Self}, 
                                    Path=(Validation.Errors)[0].ErrorContent}"/>
                                <Setter Property="Background" Value="#33FF342D"/>
                                <Setter Property="BorderBrush" Value="#AAFF342D"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>

    ...

    <TextBox Name="txtRunAfter" Grid.Column="4" Text="{Binding RunAfter, ValidatesOnDataErrors=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource TestStepTextBox}"
         LostFocus="TextBoxLostFocus" PreviewKeyDown="TextBoxPreviewKeyDown" PreviewTextInput="TextBoxPreviewTextInput"/>

    ...

 </UserControl>

And here is the Code for the ViewModel:

class TestStepListingStepViewModel : ViewModelBase, IDataErrorInfo
{
    private int _runAfter = 0;
    public int RunAfter
    {
        get
        {
            return _runAfter;
        }

        set
        {
            if (_runAfter != value)
            {
                _runAfter = value;
                OnPropertyChanged("RunAfter");
            }
        }
    }

string IDataErrorInfo.Error
    {
        get { return null; }
    }

    string IDataErrorInfo.this[string columnName]
    {
        get
        {
            string message = null;
            if (columnName == "RunAfter")
                message = validateRunAfter();

            return message;
        }
    }

    private string validateRunAfter()
    {
        if (_runAfter >= _order)
            return "Run After value must be less than its Step Order (#) value.";

        return null;
    }
}

I'm trying to figure out what's wrong with this for two days! Can some one with a pair of fresh eyes figure it out?

EDIT:
Here is the code of the TextBoxs handlers:

public partial class TestStepListingStepView : UserControl
{
    private string mInvalidCharPattern = "[^0-9]";

    public TestStepListingStepView()
    {
        InitializeComponent();

        DataObject.AddPastingHandler(this.txtRunAfter, new DataObjectPastingEventHandler(TextBoxPasting));
    }

    private void TextBoxLostFocus(object sender, RoutedEventArgs e)
    {
        TextBox txt = sender as TextBox;

        if (txt != null && string.IsNullOrEmpty(txt.Text))
            txt.Text = "0";
    }

    // Catch the space character, since it doesn't trigger PreviewTextInput
    private void TextBoxPreviewKeyDown(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.Space) { e.Handled = true; }
    }

    // Do most validation here
    private void TextBoxPreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        if (ValidateTextInput(e.Text) == false) { e.Handled = true; }
    }

    // Prevent pasting invalid characters
    private void TextBoxPasting(object sender, DataObjectPastingEventArgs e)
    {
        string lPastingText = e.DataObject.GetData(DataFormats.Text) as string;
        if (ValidateTextInput(lPastingText) == false) { e.CancelCommand(); }
    }

    // Do the validation in a separate function which can be reused
    private bool ValidateTextInput(string aTextInput)
    {
        if (aTextInput == null) { return false; }

        Match lInvalidMatch = Regex.Match(aTextInput, this.mInvalidCharPattern);
        return (lInvalidMatch.Success == false);
    }

}

Also, I'm using version 3.5 of the .Net Framework.
My application is very complex so I won't be able to create a small project that recreates only this part. My hope is that some of you already had this problem and know how to solve it.

Thanks again everyone!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

似最初 2024-09-13 22:33:46

是的,马特是对的。我希望我一小时前就看过他的答案,而不是花时间自己寻找问题。

对我有用的另一个选项是使用转换器类来检查错误列表是否有项目。所以它看起来像

<Trigger Property="Validation.HasError" Value="true"> 
<Setter Property="ToolTip" 
   Value="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource validationConverter},
   Path=(Validation.Errors)}"/> 

public class ValidationConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            ReadOnlyObservableCollection<ValidationError> errors = value as ReadOnlyObservableCollection<ValidationError>;
            if (errors == null) return value;
            if (errors.Count > 0)
            {
                return errors[0].ErrorContent;
            }
            return "";            
        }


        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException("This method should never be called");
        }

Yep, Matt is right. I wish I looked his answer hour ago, not to spend time finding issue myself.

The other option that worked for me is to use converter class that checks if Errors list has items. So it will look like

<Trigger Property="Validation.HasError" Value="true"> 
<Setter Property="ToolTip" 
   Value="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource validationConverter},
   Path=(Validation.Errors)}"/> 

public class ValidationConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            ReadOnlyObservableCollection<ValidationError> errors = value as ReadOnlyObservableCollection<ValidationError>;
            if (errors == null) return value;
            if (errors.Count > 0)
            {
                return errors[0].ErrorContent;
            }
            return "";            
        }


        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException("This method should never be called");
        }
酸甜透明夹心 2024-09-13 22:33:46

我相信问题出在 Validation.HasError 触发器中的 TextBox 模板。

<Trigger Property="Validation.HasError" Value="true">
    <Setter Property="ToolTip"
            Value="{Binding RelativeSource={RelativeSource Self}, 
            Path=(Validation.Errors)[0].ErrorContent}"/>
    <Setter Property="Background" Value="#33FF342D"/>
    <Setter Property="BorderBrush" Value="#AAFF342D"/>
</Trigger>

您正在引用验证错误的第零项,当 Validation.HasError 为 True 时,这很好。但是,当 Validation.HasError 设置为 False 时,您的 ToolTip 属性的绑定将变得无效。

作为解决方法,您可以尝试在 Validation.HasError 上创建另一个触发器,其值为 False,以清除工具提示。

I believe the problem is with your TextBox's template in the Validation.HasError trigger.

<Trigger Property="Validation.HasError" Value="true">
    <Setter Property="ToolTip"
            Value="{Binding RelativeSource={RelativeSource Self}, 
            Path=(Validation.Errors)[0].ErrorContent}"/>
    <Setter Property="Background" Value="#33FF342D"/>
    <Setter Property="BorderBrush" Value="#AAFF342D"/>
</Trigger>

You're referencing item zero of the validation errors which is fine when Validation.HasError is True. However, when Validation.HasError is then set to False your ToolTip property's binding becomes invalid.

As a workaround you could try creating another trigger on Validation.HasError with a value of False which clears the tool tip.

卸妝后依然美 2024-09-13 22:33:46

您正在引用验证错误的第零项,当 Validation.HasError 为 True 时,这很好。但是,当 Validation.HasError 设置为 False 时,您的 ToolTip 属性的绑定将变得无效。

作为解决方法,您可以尝试在 Validation.HasError 上创建另一个触发器,其值为 False,以清除工具提示。

这个解决方案对我有用。感谢您的描述和帮助!

You're referencing item zero of the validation errors which is fine when Validation.HasError is True. However, when Validation.HasError is then set to False your ToolTip property's binding becomes invalid.

As a workaround you could try creating another trigger on Validation.HasError with a value of False which clears the tool tip.

This solution worked for me. Thank you for your description and your help!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文