Click here to Skip to main content
14,332,266 members

Forcing validation for required fields in Silverlight

Rate this:
5.00 (1 vote)
Please Sign up or sign in to vote.
5.00 (1 vote)
19 Aug 2011CPOL
Force validation, even if the user doesn't enter anything, so that fields that are required show an appropriate message.

Introduction

The common practice in Silverlight, when using MVVM, is to validate user data via the bound property setter in the View Model. Although this works well for most circumstances, it doesn't work when the user doesn't make a change to data and instead clicks on a button. For example, imagine a form that prompts the user for his/her name, which must not be blank, and then clicks a Next button. If the user just clicks the Next button without typing anything in the Name field, the validation via the bound property setter is never executed.

One way to address this is to repeat the validation in the View Model when the user clicks Next. The issue with this is how to display the error message to the user in a way that is consistent (e.g., shown in a ValidationSummary and red tool tip, with any other error).

Solution

The solution I'm proposing is based on a solution by Josh Twist (see his article here: http://www.thejoyofcode.com/Silverlight_Validation_and_MVVM_Part_II.aspx). The premise of his solution is to allow the View Model to instruct the View to refresh its bindings. This tells the View to set any bound properties, and this allows your validation code to run, even if the user has not entered data. I've taken Josh's code and simplified it, by decoupling it from the validation framework and removing the need to add attached properties to each element participating in the validation scope.

Using the code

The solution is based around an attached behaviour, which I've called RefreshBindingScope.

public class RefreshBindingScope
{
    private static readonly Dictionary<type, > BoundProperties =
        new Dictionary<type, >
        {
            { typeof(TextBox), TextBox.TextProperty },
            { typeof(ItemsControl), ItemsControl.ItemsSourceProperty },
            { typeof(ComboBox), ItemsControl.ItemsSourceProperty },
            { typeof(DataGrid), DataGrid.ItemsSourceProperty},
            { typeof(AutoCompleteBox), AutoCompleteBox.TextProperty},
            { typeof(DatePicker), DatePicker.SelectedDateProperty},
            { typeof(ListBox), ItemsControl.ItemsSourceProperty },
            { typeof(PasswordBox), PasswordBox.PasswordProperty },
        };

    public FrameworkElement ScopeElement { get; private set; }

    public static RefreshBindingScope GetScope(DependencyObject obj)
    {
        return (RefreshBindingScope)obj.GetValue(ScopeProperty);
    }

    public static void SetScope(DependencyObject obj, RefreshBindingScope value)
    {
        obj.SetValue(ScopeProperty, value);
    }

    public static readonly DependencyProperty ScopeProperty =
        DependencyProperty.RegisterAttached("Scope", 
        typeof(RefreshBindingScope), typeof(RefreshBindingScope), 
        new PropertyMetadata(null, ScopeChanged));

    private static void ScopeChanged(DependencyObject source, 
            DependencyPropertyChangedEventArgs args)
    {
        // clear old scope
        var oldScope = args.OldValue as RefreshBindingScope;
        if (oldScope != null)
        {
            oldScope.ScopeElement = null;
        }

        // assign new scope
        var scopeElement = source as FrameworkElement;
        if (scopeElement == null)
        {
            throw new ArgumentException(string.Format(
                "'{0}' is not a valid type.Scope attached property can " + 
                "only be specified on types inheriting from FrameworkElement.",
                source));
        }

        var newScope = (RefreshBindingScope)args.NewValue;
        newScope.ScopeElement = scopeElement;
    }

    public void Scope()
    {
        RefreshBinding(ScopeElement);
    }

    private static void RefreshBinding(DependencyObject dependencyObject)
    {
        Debug.WriteLine(dependencyObject.GetType());

        // stop if we've reached a validation summary
        var validationSummary = dependencyObject as ValidationSummary;
        if (validationSummary != null) return;

        // don't do buttons - should be nothing to validate
        var button = dependencyObject as Button;
        if (button != null) return;

        // don't do hyperlink buttons - should be nothing to validate
        var hyperLinkButton = dependencyObject as HyperlinkButton;
        if (hyperLinkButton != null) return;

        foreach (var item in dependencyObject.GetChildren())
        {
            var found = false;

            // get bound property (use list from BindingHelper,
            // so we don't repeat it in this class)
            DependencyProperty boundProperty;
            if (BoundProperties.TryGetValue(item.GetType(), out boundProperty))
            {
                // get BindingExpression and, if exists, force it to refresh
                var be = ((FrameworkElement)item).GetBindingExpression(boundProperty);
                if (be != null) be.UpdateSource();

                // binding refreshed, so don't look for children
                found = true;

                Debug.WriteLine(string.Format("{0} binding refreshed ({1}).", 
                                item, item.GetValue(boundProperty)));
            }

            // get children recursively if bound property has not already been found
            if (!found)
            {
                RefreshBinding(item);
            }
        }
    }
}

BoundProperties is a list of the controls that can be refreshed. You can change this list to suit your circumstances. This list removes the need to attach opt-in attributes to each control in XAML.

Scope is a dependency property. The most important thing that Scope does is call the RefreshBindings method, passing in the UI element that it is bound to. The RefreshBindings method takes the UI element and walks the visual tree, looking for any control that matches the list from BoundProperties. When one is found, it checks to see if the control has a binding expression and if so, it executes the UpdateSource method on the binding expression. This refreshes the rebinding. To reduce any performance hit, this visual tree walk is stopped on certain elements (like a ValidationSummary) that would not normally participate in validation. It also stops looking for child elements once it finds a binding expression.

The next step is to define the scope of UI elements that you want to refresh, by attaching the behaviour in the XAML. The RefreshBindingScope can be attached to any UI element (for example, a Grid that contains TextBoxes).

<Grid helpers:RefreshBindingScope.Scope="{Binding RefreshBindingScope}">

The XAML above assumes you have a namespace called helpers that is pointing at your RefreshBindingScope namespace.

xmlns:helpers="clr-namespace:RefreshBindingExample.Helpers"

The Scope dependency property is bound to an instance of a RefreshBindingScope in the View Model (I've used an interface with this so it can be injected if you are using Dependency Injection).

public IRefreshBindingScope RefreshBindingScope { get; set; }

When the user executes a command that expects validated data (e.g., clicks a button), the RefreshBindingScope in the View Model can be used to request the View to refresh bindings, by executing the Scope method.

RefreshBindingScope.Scope();

As shown above, this executes the UpdateSource method on the elements within the scope. If you are using IDataErrorInfo or raising exceptions in your property setters, refreshing the bindings will tell the View to show red borders, error tool tips, and errors in validation summaries for any error that occurred in the property setters due to the refresh. You can also check your View Model for the presence of errors, depending on your implementation. In the attached example, I've used a simple IDataErrorInfo implementation and have a HasErrors() method that I can query to see whether the command should continue or not.

public void OnSave(object parameter)
{
    ClearErrors();
    RefreshBindingScope.Scope();

    if (!HasErrors())
    {
        // do the Save
    }
}

Walking the Visual Tree

You may have noticed that the RefreshBinding method in the RefreshBindingScope class uses an extension method called GetChildren() on the dependency property. This is a helper method to make it easier to access child controls. The code for the extension methods is shown below.

public static class VisualTreeExtensions
{
    public static IEnumerable<dependencyobject> 
           GetChildren(this DependencyObject depObject)
    {
        int count = depObject.GetChildrenCount();
        for (int i = 0; i < count; i++)
        {
            yield return VisualTreeHelper.GetChild(depObject, i);
        }
    }

    public static DependencyObject GetChild(
           this DependencyObject depObject, int childIndex)
    {
        return VisualTreeHelper.GetChild(depObject, childIndex);
    }

    public static DependencyObject GetChild(
           this DependencyObject depObject, string name)
    {
        return depObject.GetChild(name, false);
    }

    public static DependencyObject GetChild(this DependencyObject depObject, 
                  string name, bool recursive)
    {
        foreach (var child in depObject.GetChildren())
        {
            var element = child as FrameworkElement;
            if (element != null)
            {
                // if it's a FrameworkElement check Name
                if (element.Name == name)
                    return element;

                // try to get it using FindByName
                var innerElement = element.FindName(name) as DependencyObject;
                if (innerElement != null)
                    return innerElement;
            }
            // if it's recursive search through its children
            if (recursive)
            {
                var innerChild = child.GetChild(name, true);
                if (innerChild != null)
                    return innerChild;
            }
        }
        return null;
    }

    public static int GetChildrenCount(this DependencyObject depObject)
    {
        return VisualTreeHelper.GetChildrenCount(depObject);
    }

    public static DependencyObject GetParent(this DependencyObject depObject)
    {
        return VisualTreeHelper.GetParent(depObject);
    }
}

History

  • August 2011 - Initial release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author


Comments and Discussions

 
GeneralMy vote of 5 Pin
jefsolis20-Sep-12 12:12
memberjefsolis20-Sep-12 12:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 19 Aug 2011

Stats

17.5K views
438 downloads
9 bookmarked