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)
{
var oldScope = args.OldValue as RefreshBindingScope;
if (oldScope != null)
{
oldScope.ScopeElement = null;
}
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());
var validationSummary = dependencyObject as ValidationSummary;
if (validationSummary != null) return;
var button = dependencyObject as Button;
if (button != null) return;
var hyperLinkButton = dependencyObject as HyperlinkButton;
if (hyperLinkButton != null) return;
foreach (var item in dependencyObject.GetChildren())
{
var found = false;
DependencyProperty boundProperty;
if (BoundProperties.TryGetValue(item.GetType(), out boundProperty))
{
var be = ((FrameworkElement)item).GetBindingExpression(boundProperty);
if (be != null) be.UpdateSource();
found = true;
Debug.WriteLine(string.Format("{0} binding refreshed ({1}).",
item, item.GetValue(boundProperty)));
}
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())
{
}
}
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 (element.Name == name)
return element;
var innerElement = element.FindName(name) as DependencyObject;
if (innerElement != null)
return innerElement;
}
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.