Alexander Ventura
Alexander Ventura

Reputation: 1160

Is it possible to create generic ValidationRule classes?

I am working on a .NET 4.0 project that deals with validation of the range of a particular data type. For example, a 32 bit int should only be between Int32.MinValue and Int32.MaxValue or any other value defined by the application. I want to be able to specify data types and ranges on a custom validator so they can be called directly from the xaml through binding: <CheckIfValueRangeValidator>

This is what I have in mind, I am not sure it will work or if it can even be done through the xaml.

class CheckIfValueInRangeValidator<T> : ValidationRule
{
    public T Max { get; set; }
    public T Min { get; set; }

    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        // Implementation...
    }
}

Upvotes: 3

Views: 1829

Answers (2)

mike
mike

Reputation: 1734

I had a similar problem: I needed a ValidationRule specific to the binding source, i.e. I wanted the source to define the rule.

I ended up using the following approach of an attached property that sets the binding incl. rule via code.

ICustomRuleProvider.cs: Interface the binding source (ViewModel) must implement.

public interface ICustomRuleProvider
{
    ValidationRule CreateRule();
}

CustomRule.cs: Attached property used in Xaml to create the required binding incl. rule.

public class CustomRule
{
    public static ICustomRuleProvider GetProvider(DependencyObject obj)
    {
        return (ICustomRuleProvider)obj.GetValue(ProviderProperty);
    }

    public static void SetProvider(DependencyObject obj, ICustomRuleProvider value)
    {
        obj.SetValue(ProviderProperty, value);
    }

    public static readonly DependencyProperty ProviderProperty =

        DependencyProperty.RegisterAttached("Provider", typeof(ICustomRuleProvider), typeof(CustomRule), new FrameworkPropertyMetadata(null, CustomRule.OnProviderPropertyChanged));

    public static void OnProviderPropertyChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        if (!(sender is TextBox fe)) return;

        var provider = e.NewValue as ICustomRuleProvider;

        if (provider != null)
        {
            var b = new Binding("Value");
            b.ValidationRules.Add(provider.CreateRule());

            fe.SetBinding(System.Windows.Controls.TextBox.TextProperty, b);
        }
        else
        {
            BindingOperations.ClearBinding(fe, TextBox.TextProperty);
        }
    }
}

Example: Be sure to define xmlns:customRule="clr-namespace:...;assembly=..."

<TextBox customRule:CustomRule.Provider="{Binding}"/>

This implementation is tailor-made for binding a the Text property of a TextBox the "Value" property of the binding source, but this can easily be extended.

Using this I was able to use a custom (generic) converter.

BTW: I do not agree that a custom generic ValidationRule is a "bad idea", although the use cases may be limited. In many cases it may be necessary to also implement a custom IValueConverter and assign that to the binding as well to have full control over the object value on its way from the control to the source.

Upvotes: 0

aybe
aybe

Reputation: 16652

My bad, actually you cannot use x:TypeArguments as it will raise x:TypeArguments is not allowed in object elements for XAML version lower than 2009, it's valid only on loose XAML files or the root element (Window in my case) ...

http://msdn.microsoft.com/en-us/library/ms750476.aspx

But as a workaround you could use the following pattern :

    <TextBox x:Name="textBox1">
        <Binding Path="MyValue"
                     Source="{StaticResource MyObject}"
                     UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <wpfApplication6:MyValidationRule ObjectType="{x:Type system:Int32}"   />
            </Binding.ValidationRules>
        </Binding>
    </TextBox>

Code behind :

public class MyObject
{
    public object MyValue { get; set; }
}

public class MyValidationRule : ValidationRule
{
    public Type ObjectType { get; set; }

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        throw new NotImplementedException();
    }
}

Take a look at this one too : Using a Generic IValueConverter from XAML

@Blam comment is worthwhile to consider, a range to check generally applies for an integer or a double, for other types I'd say you could just add a boolean that return the validity of that object and perform such validation inside the object itself.

For numbers you have RangeAttribute but it's not really part of the WPF validation infrastructure.

You also have an another option for validation : INotifyDataErrorInfo validation takes place inside the object in this case.

I wrote a lengthy answer here : https://softwareengineering.stackexchange.com/questions/203590/is-there-an-effective-way-for-creating-complex-forms you might find something useful on it.

From my experience I'd say that a generic validating rule is probably not wise to do.

You should edit your question to be less generic ;-) but rather more specific, you'd get more help from people here. Give one or two concrete cases of the objects you are trying to validate.

EDIT

You can also use a BindingGroup for validating an object :

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void TextBox1_OnTextChanged(object sender, TextChangedEventArgs e)
    {
        grid.BindingGroup.CommitEdit();
    }
}

public class Indices
{
    public int ColorIndex { get; set; }
    public string ColorPrefix { get; set; }
    public int GradientIndex { get; set; }
    public string GradientPrefix { get; set; }
}

public class ValidateMe : ValidationRule
{
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        var bindingGroup = value as BindingGroup;
        var o = bindingGroup.Items[0] as Indices;
        return new ValidationResult(true, null);
    }
}
<Window x:Class="WpfApplication6.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wpfApplication6="clr-namespace:WpfApplication6"
        Title="MainWindow"
        Width="525"
        Height="350">
    <Window.Resources>
        <wpfApplication6:Indices x:Key="Indices"
                                 ColorIndex="1"
                                 ColorPrefix="MyColor"
                                 GradientIndex="1"
                                 GradientPrefix="MyGradient" />
    </Window.Resources>
    <Grid x:Name="grid" DataContext="{StaticResource Indices}">
        <Grid.BindingGroup>
            <BindingGroup>
                <BindingGroup.ValidationRules>
                    <wpfApplication6:ValidateMe />
                </BindingGroup.ValidationRules>
            </BindingGroup>
        </Grid.BindingGroup>
        <TextBox TextChanged="TextBox1_OnTextChanged">
            <Binding Path="ColorIndex" UpdateSourceTrigger="PropertyChanged" />
        </TextBox>
    </Grid>
</Window>

Using RangeAtribute :

    private void test()
    {
        Indices indices = new Indices();
        indices.ColorIndex = 20;

        var validationContext = new ValidationContext(indices);
        var validationResults = new List<System.ComponentModel.DataAnnotations.ValidationResult>();
        var tryValidateObject = Validator.TryValidateObject(indices, validationContext, validationResults,true);
    }

public class Indices
{
    [Range(1, 10)]
    public int ColorIndex { get; set; }

    public string ColorPrefix { get; set; }
    public int GradientIndex { get; set; }
    public string GradientPrefix { get; set; }
}

Upvotes: 3

Related Questions