63

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.

raven
  • 17,276
  • 15
  • 76
  • 110
Valentin V
  • 22,569
  • 28
  • 97
  • 147

6 Answers6

75

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>
Ken Smith
  • 19,697
  • 13
  • 93
  • 139
  • 4
    This property is really great, simple and does exactly what we need. – PaN1C_Showt1Me Apr 15 '10 at 11:47
  • 2
    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. – Jānis Gruzis Jul 30 '13 at 07:40
  • 2
    This should be selected as the answer as it's more appropriate in MVVM context. – Tae-Sung Shin Sep 16 '13 at 21:52
  • 1
    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. – uceumern Jan 09 '19 at 08:36
  • 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. – Istvan Heckl May 21 '20 at 04:55
67

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>
Robert Macnee
  • 11,310
  • 4
  • 38
  • 35
  • 11
    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? – Kilhoffer Aug 21 '09 at 20:07
  • 1
    what if you don't have a named element? what if it's a part of a template in the ItemsControl ? – PaN1C_Showt1Me Apr 15 '10 at 11:32
  • 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? thanks – black sensei May 13 '10 at 16:19
  • 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. – Simon D. Jun 23 '10 at 06:55
  • 3
    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. – Monstieur Nov 16 '12 at 19:45
2

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);
}
ryan
  • 1,252
  • 9
  • 24
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!

Alex Pollan
  • 854
  • 5
  • 13
  • 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 nonetheless – Valentin V Jul 25 '12 at 12:22
  • 1
    @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). – MasterMastic Feb 26 '13 at 11:54
0

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.

lukep
  • 43
  • 4
-3

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

Jenzo
  • 1