Exception validating data with IDataErrorInfo with a MVVM implementation
Asked Answered
O

3

3

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!

Ophthalmologist answered 21/6, 2010 at 17:22 Comment(2)
Can you post code you have in your event handlers for the text box (TextBoxLostFocus, TextBoxPreviewKeyDown, TextBoxPreviewTextInput)? I tried running code you provided without those handlers and it worked fine for me. Also what .net version are you using?Continual
Added the information you asked for. Thanks for the helpOphthalmologist
C
7

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");
        }
Continual answered 22/6, 2010 at 19:46 Comment(4)
Well it seems Matt solution didn't work (see my response to his answer), but your converter did work. Of course I preferred a solution like his, with no code behind needed...Ophthalmologist
only a sidenote: use Path=Validation.Errors instead of Path=(Validation.Errors) and the code doesn't work "silently" (only a not-so-understandable debug message sent to the visual studio output window). Anyone wondering why parenthesys are needed? In other bindings (i.e. the mine in standard MVVM code) parentherys are not needed, like Text={Binding MyProp.MyMember}Exegetics
@D_Guidi: I think it's because Validation is not a property and Errors its subproperty, but Validation is the class that defines the attached property Errors. The parentheses are required to make the dot-separated string a single name.Flaunty
What revealed the true exception for me was making sure Common Language Runtime Exceptions in the Exception Settings screen has a check mark.Menses
C
5

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.

Cambyses answered 22/6, 2010 at 17:42 Comment(1)
Your assessment seems to be correct, but I still get the exception even after adding a trigger on Validation.HasError = false. But I have confined the exception a little more. I added a breakpoint on the property setter and on the validation method and the exception is risen when I hit Backspace and and the textbox becomes empty. The setter is not called and when the validation breakpoint reaches the exception was already thrown. Thanks for the helpOphthalmologist
R
0

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!

Regardful answered 23/6, 2015 at 12:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.