views:

77

answers:

2

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!

+2  A: 

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.

Matt Casto
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 help
jpsstavares
+2  A: 

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");
        }
Andrii V
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...
jpsstavares