Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Validate Multiple Controls Simultaneously in WPF

I have a form with two password fields - one where the user enters the password, and another where the user has to re-enter that password to confirm. Validation is used to confirm that both passwords match - if they do, a button is enabled to allow the user to proceed:

<PasswordBox Name="P1Box" src:PasswordBoxAssistant.BindPassword="True">
    <src:PasswordBoxAssistant.BoundPassword>
        <Binding Source="{StaticResource mybinding}" Path="Password.P1" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
            <Binding.ValidationRules>
                <DataErrorValidationRule ValidatesOnTargetUpdated="True"/>
            </Binding.ValidationRules>
        </Binding>
    </src:PasswordBoxAssistant.BoundPassword>
</PasswordBox>

Button Style:

<Setter Property="IsEnabled" Value="False"/>
<Style.Triggers>
    <MultiDataTrigger>
        <MultiDataTrigger.Conditions>
            <Condition Binding="{Binding ElementName=P1Box, Path=(Validation.HasError)}" Value="false"/>
        </MultiDataTrigger.Conditions>
        <Setter Property="IsEnabled" Value="true"/>
    </MultiDataTrigger>
</Style.Triggers>

My problem is that the user can alter the password in the first box, and it won't force the second password box to re-validate. For example, if the first password is entered as "password", and the user enters "password" into the second box, the validation passes and the button is enabled. If the user then changes the original password box to "PASSWORD", both boxes stay validated - the original because there are no constraints on a nonempty password, the second because nothing has forced the validation to update.

My password boxes use the attached property outlined here to allow binding to the password. Because of this, I can't find a way to access it in code-behind (since PasswordBox.Password is not a dependency property by itself) in the manner expressed in this solution. Or, perhaps it just doesn't work for the attached properties - the code below didn't do anything:

P2Box.GetBindingExpression(PasswordBoxAssistant.BoundPassword).UpdateSource();

I have a custom class which inherits IDataErrorInfo to allow for validation between the two controls - the binding is a PasswordData object, and the password boxes are set to PasswordData.P1 and PasswordData.P2:

public class PasswordData : IDataErrorInfo
{
    public string P1 { get; set; }
    public string P2 { get; set; }
    public string Error { get { return string.Empty; } }
    public string this[string propertyName]
    {
        get
        {
            string ret;
            if (propertyName == "P1")
            {
            if (P1 == null || P1 == "")
                ret = "Password cannot be null or empty.";
            else
                ret = "";
            }
            else if (propertyName == "P2")
            {
            if (P2 == null || P2 == "")
                ret = "Password cannot be null or empty.";
            else if (P2 != P1)
                ret = "The passwords do not match.";
            else
                ret = "";
            }
            return ret;
        }
    }
}

I have tried hopping in during the PasswordChanged event, creating a new PasswordData, and reassigning the binding. This solves the validation problem, but the caret in the passwordbox is always at the very beginning, ruining any data entered.

I would like have a xaml-only solution, but code behind is perfectly acceptable. I'm using .Net 4.0, if that matters.

EDIT:

OK, so it turns out I mistyped the event handler in the xaml, and the solution actually works:

private void PasswordChanged(object sender, RoutedEventArgs e)
{
    binding.Pass.P1 = ((PasswordBox)sender).Password;
    P2Box.GetBindingExpression(PasswordBoxAssistant.BoundPassword).UpdateSource();
}

I have to manually update the binding, because the event fires before the binding is updated.

I am curious if there is a proper, XAML only way using validation rules, IDataErrorInfo, or some other means to have a binding between the controls without having to hook into the events and manually update.

like image 236
Tom Avatar asked Jul 03 '12 18:07

Tom


2 Answers

For more complex validation, it is often necessary to push your validation into the ViewModel, or even the Model in many cases. IDataErrorInfo is a good start.

Here's a link to an excellent article on the subject:
http://msdn.microsoft.com/en-us/magazine/ff714593.aspx

like image 190
JDB Avatar answered Sep 24 '22 15:09

JDB


This may be helpful. I see a lot of posts where Data Annotations are used in your model classes. When you bind the model to the context in WPF you may notice validation fires off prior to any data not being entered.

This can be problematic and iterating over the binding expressions can be a pain. So I wired up a couple extensions to do this.

This will get an Enumerable your type like a TextBox. The DependencyObject might be your window or a grid. To reference your window use var window = Window.GetWindow(this). You can also use a control like a grid by name if you wish.

  public static IEnumerable<T> FindVisualChildren<T>(DependencyObject obj) where T : DependencyObject
    {
        if (obj != null)
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(obj, i);
                if (child != null && child is T)
                {
                    yield return (T)child;
                }

                foreach (T childOfChild in FindVisualChildren<T>(child))
                {
                    yield return childOfChild;
                }
            }
        }
    }   

Once you have your list of controls we use the "Revalidate" extension to iterate over them and fire off the Binding Expressions. We return a bool which will just tell us if the form had any failures. Note that T is your type you want to validate. Such as TextBox, ListBox and so on. Notice I show one example below on a TextBox. You'll need to build out any other controls your going to use and then cast "obj" to the correct type.

        public static Boolean Revalidate<T>(this Window depObj) where T : DependencyObject
    {
        bool isValid = true;
        foreach (T obj in FindVisualChildren<T>(depObj))
        {
            var name = typeof(T).Name.ToLower();
            BindingExpression exp = null;

            switch (name)
            {
                case "textbox":
                    var tb = obj as TextBox;
                    exp = tb.GetBindingExpression(TextBox.TextProperty);
                    exp.UpdateSource();
                    if (Validation.GetHasError(tb))
                        isValid = false;  
                    break;           
            }                             
        }

        return isValid;
    }

...and To call it use:

 valid = this.window.Revalidate<TextBox>(); // where this.window is a reference to our window.

From there we can just check if true or not and just return if failed.

if(!valid){
   return; // you could update a message or something as well obviously.
}
like image 28
origin1tech Avatar answered Sep 26 '22 15:09

origin1tech