Force validation on bound controls in WPF
Asked Answered
T

6

65

I have a WPF dialog with a couple of textboxes on it. Textboxes are bound to my business object and have WPF validation rules attached.

The problem is that user can perfectly click 'OK' button and close the dialog, without actually entering the data into textboxes. Validation rules never fire, since user didn't even attempt entering the information into textboxes.

Is it possible to force validation checks and determine if some validation rules are broken?

I would be able to do it when user tries to close the dialog and prohibit him from doing it if any validation rules are broken.

Thank you.

Thornhill answered 27/1, 2009 at 13:40 Comment(0)
F
69

We have this issue in our application as well. The validation only fires when bindings update, so you have to update them by hand. We do this in the Window's Loaded event:

public void Window_Loaded(object sender, RoutedEventArgs e)
{
    // we manually fire the bindings so we get the validation initially
    txtName.GetBindingExpression(TextBox.TextProperty).UpdateSource();
    txtCode.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}

This will make the error template (red outline) appear, and set the Validation.HasError property, which we have triggering the OK button to disable:

<Button x:Name="btnOK" Content="OK" IsDefault="True" Click="btnOK_Click">
    <Button.Style>
        <Style TargetType="{x:Type Button}">
            <Setter Property="IsEnabled" Value="false" />
            <Style.Triggers>
                <!-- Require the controls to be valid in order to press OK -->
                <MultiDataTrigger>
                    <MultiDataTrigger.Conditions>
                        <Condition Binding="{Binding ElementName=txtName, Path=(Validation.HasError)}" Value="false" />
                        <Condition Binding="{Binding ElementName=txtCode, Path=(Validation.HasError)}" Value="false" />
                    </MultiDataTrigger.Conditions>
                    <Setter Property="IsEnabled" Value="true" />
                </MultiDataTrigger>
            </Style.Triggers>
        </Style>
    </Button.Style>
</Button>
Forequarter answered 30/1, 2009 at 19:28 Comment(5)
This works and achieves what I was looking for, but I have no codebehind. Form logic is encapsulated in a ModelView. Since the ModelView is not supposed to have references to specific screen elements, how can this be done and still have no codebehind? Is there a way in XAML to force binding?Connett
what if you don't have a named element? what if it's a part of a template in the ItemsControl ?Wend
Hello i'm a newby but i'm certain this is what i want as functionalities.Now my problem is that my application is connecting to a webservice for any of its functionalities.That means i don't have the datamodel in my app.I'm honestly looking for a way to do validation without data binding.Can somebody show me the way to follow? thanksEphemerid
Working, but I would not like to use it directly. Instead one could attach it as a 'behavior' via attached property to the bound controls.Gliadin
This goes against standard UI guidelines. An empty form should show multiple validation errors only when the user clicks save and can also optionally show them one by one as the user tabs OUT of fields. You should do this before saving and not after loading.Inkling
T
76

In 3.5SP1 / 3.0SP2, they also added a new property to the ValidationRule base, namely, ValidatesOnTargetUpdated="True". This will call the validation as soon as the source object is bound, rather than only when the target control is updated. That may not be exactly what you want, but it's not bad to see initially all the stuff you need to fix.

Works something like this:

<TextBox.Text>
    <Binding Path="Amount" StringFormat="C">
        <Binding.ValidationRules>
            <validation:RequiredValidationRule 
                ErrorMessage="The pledge amount is required." 
                ValidatesOnTargetUpdated="True"  />
            <validation:IsNumericValidationRule 
                ErrorMessage="The pledge amount must be numeric." 
                ValidationStep="ConvertedProposedValue" 
                ValidatesOnTargetUpdated="True"  />
        </Binding.ValidationRules>
    </Binding>
</TextBox.Text>
Twerp answered 21/3, 2009 at 6:49 Comment(5)
This property is really great, simple and does exactly what we need.Wend
It can cause a performance issue. Example - a grid with >=1000 rows, where validation of each row makes >=1 requests to database. After DataContext set each row will be validated and it will fire >=1000 requests to database. Also, if validation contains some sort of memory leak, it will be multiplied by >=1000.Elfriedeelfstan
This should be selected as the answer as it's more appropriate in MVVM context.Comfrey
This is exactly what I was looking for. Thanks! @JānisGruzis: sure, but this is true for every code. And if your validation contains a memory leak, you are at least more likely to find it, if it is multiplied by 1000. So, yeah, while you are right, you are also stating the obivous without contributing to the individual issue here.Fujimoto
Worked like a charm! Unfortunatly, when used in a DataGrid it run for the bottom line, the empty line which is for inserting a new line.Redvers
F
69

We have this issue in our application as well. The validation only fires when bindings update, so you have to update them by hand. We do this in the Window's Loaded event:

public void Window_Loaded(object sender, RoutedEventArgs e)
{
    // we manually fire the bindings so we get the validation initially
    txtName.GetBindingExpression(TextBox.TextProperty).UpdateSource();
    txtCode.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}

This will make the error template (red outline) appear, and set the Validation.HasError property, which we have triggering the OK button to disable:

<Button x:Name="btnOK" Content="OK" IsDefault="True" Click="btnOK_Click">
    <Button.Style>
        <Style TargetType="{x:Type Button}">
            <Setter Property="IsEnabled" Value="false" />
            <Style.Triggers>
                <!-- Require the controls to be valid in order to press OK -->
                <MultiDataTrigger>
                    <MultiDataTrigger.Conditions>
                        <Condition Binding="{Binding ElementName=txtName, Path=(Validation.HasError)}" Value="false" />
                        <Condition Binding="{Binding ElementName=txtCode, Path=(Validation.HasError)}" Value="false" />
                    </MultiDataTrigger.Conditions>
                    <Setter Property="IsEnabled" Value="true" />
                </MultiDataTrigger>
            </Style.Triggers>
        </Style>
    </Button.Style>
</Button>
Forequarter answered 30/1, 2009 at 19:28 Comment(5)
This works and achieves what I was looking for, but I have no codebehind. Form logic is encapsulated in a ModelView. Since the ModelView is not supposed to have references to specific screen elements, how can this be done and still have no codebehind? Is there a way in XAML to force binding?Connett
what if you don't have a named element? what if it's a part of a template in the ItemsControl ?Wend
Hello i'm a newby but i'm certain this is what i want as functionalities.Now my problem is that my application is connecting to a webservice for any of its functionalities.That means i don't have the datamodel in my app.I'm honestly looking for a way to do validation without data binding.Can somebody show me the way to follow? thanksEphemerid
Working, but I would not like to use it directly. Instead one could attach it as a 'behavior' via attached property to the bound controls.Gliadin
This goes against standard UI guidelines. An empty form should show multiple validation errors only when the user clicks save and can also optionally show them one by one as the user tabs OUT of fields. You should do this before saving and not after loading.Inkling
M
3

Here is an alternative way that doesn't require calling "UpdateSource()" or "UpdateTarget()":

var binding = thingToValidate.GetBinding(propertyToValidate);
foreach (var rule in binding.ValidationRules)
{
    var value = thingToValidate.GetValue(propertyToValidate);
    var result = rule.Validate(value, CultureInfo.CurrentCulture);
    if (result.IsValid) 
         continue;
    var expr = BindingOperations.GetBindingExpression(thingToValidate, propertyToValidate);
    if (expr == null)  
        continue;
    var validationError = new ValidationError(rule, expr);
    validationError.ErrorContent = result.ErrorContent;
    Validation.MarkInvalid(expr, validationError);
}
Melnick answered 24/6, 2016 at 23:0 Comment(0)
H
1

Just in case anyone happens to find this old question and is looking for an answer that addresses Monstieur's comment about UI guidelines, I did the following:

Xaml

<TextBox.Text>
    <Binding Path="TextValue" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
        <Binding.ValidationRules>
            <local:RequiredFieldValidationRule>
                    <local:RequiredFieldValidationRule.IsRequiredField>
                    <local:BoolValue Value="{Binding Data.Required, Source={StaticResource proxy}}" />
                </local:RequiredFieldValidationRule.IsRequiredField>
                <local:RequiredFieldValidationRule.ValidationFailed>
                    <local:BoolValue Value="{Binding Data.HasValidationError, Mode=TwoWay, Source={StaticResource proxy}}" />
                </local:RequiredFieldValidationRule.ValidationFailed>
            </local:RequiredFieldValidationRule>
        </Binding.ValidationRules>
    </Binding>
</TextBox.Text>

RequiredFieldValidationRule:

public class RequiredFieldValidationRule : ValidationRule
{
    private BoolValue _isRequiredField;
    public BoolValue IsRequiredField
    {
        get { return _isRequiredField; }
        set { _isRequiredField = value; }
    }
    private BoolValue _validationFailed;
    public BoolValue ValidationFailed
    {
        get { return _validationFailed; }
        set { _validationFailed = value; }
    }

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        ValidationFailed.Value = IsRequiredField.Value && (value == null || value.ToString().Length == 0);
        return new ValidationResult(!ValidationFailed.Value, ValidationFailed.Value ? "This field is mandatory" : null);
    }
}

In the class that the Xaml binds to

private bool _hasValidationError;
public bool HasValidationError
{
    get { return _hasValidationError; }
    set { _hasValidationError = value; NotifyPropertyChanged(nameof(HasValidationError)); }
}


public void InitialisationMethod() // Or could be done in a constructor
{
    _hasValidationError = Required; // Required is a property indicating whether the field is mandatory or not
}

I then hide my Save button using a bound property, if any of my objects has HasValidationError = true.

Hope this is helpful to someone.

Hammond answered 24/5, 2018 at 16:32 Comment(0)
C
0

Use the method above proposed by Robert Macnee. For example:

//force initial validation
foreach (FrameworkElement item in grid1.Children)
{
    if (item is TextBox)
    {
        TextBox txt = item as TextBox;
        txt.GetBindingExpression(TextBox.TextProperty).UpdateSource();
    }
}        

But, BE SURE that the bound controls are Visibles before this code run!

Caudex answered 25/7, 2012 at 11:57 Comment(2)
It's curious, how I'm getting the comments on a question I asked 3.5 years ago, and I'm not programming on windows ever since. But thanks nonethelessThornhill
@ValentinVasilyev Probably because viewers with an answer want to help more people (other than the one asking the question). You probably know it by now, the reason I still replied (again, after a few years) is the same reason I mentioned).Maisiemaison
O
-4

using the INotifyPropertychanged on your data object

public class MyObject : INotifyPropertyChanged
{
    string _MyPropertyToBind = string.Empty;
    public string MyPropertyToBind
    {
        get
        {
            return _MyPropertyToBind;
        }
        set
        {
            _MyPropertyToBind = value;
            NotifyPropertyChanged("MyPropertyToBind");
        }
    }

    public void NotifyPropertyChanged(string property)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }
    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    #endregion

}

you can add the following code to your control

<TextBox Text="{Binding MyPropertyToBind, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" >

The textbox susbscribe to the propertychanged event of the datacontext object ( MyObjet in our example) and assumes it is fired when the source data has been updated

it automatically forces the refresh to the control

No need to call yourself the UpdateTarget method

Ostensive answered 11/10, 2010 at 21:26 Comment(1)
You paid no attention to the question. No property is being changed.Signpost

© 2022 - 2024 — McMap. All rights reserved.