强制 Silverlight 中的必填字段进行验证
强制验证,即使用户没有输入任何内容,以便必填字段显示适当的消息。
引言
在 Silverlight 中使用 MVVM 的常见做法是通过在 View Model 中绑定属性的 setter 来验证用户数据。 尽管这在大多数情况下都有效,但当用户未更改数据而单击按钮时,它不起作用。 例如,想象一个表单提示用户输入他的/她的姓名(不能为空),然后单击“下一步”按钮。 如果用户只是单击“下一步”按钮,而没有在“姓名”字段中输入任何内容,则永远不会执行通过绑定属性 setter 的验证。
解决此问题的一种方法是在用户单击“下一步”时在 View Model 中重复验证。 这样做的问题是如何以一致的方式向用户显示错误消息(例如,在 ValidationSummary
和红色工具提示中,以及任何其他错误)。
解决方案
我提出的解决方案基于 Josh Twist 的解决方案(请参阅他的文章:http://www.thejoyofcode.com/Silverlight_Validation_and_MVVM_Part_II.aspx)。 他的解决方案的前提是允许 View Model 指示 View 刷新其绑定。 这告诉 View 设置任何绑定属性,这允许你的验证代码运行,即使用户没有输入数据。 我已经采用了 Josh 的代码并对其进行了简化,通过将其与验证框架分离,并消除了将附加属性添加到参与验证范围的每个元素的需求。
使用代码
该解决方案基于一个附加行为,我将其称为 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
是可以刷新的控件的列表。 你可以更改此列表以适合你的情况。 此列表消除了在 XAML 中为每个控件附加选择性属性的需求。
Scope
是一个依赖属性。 Scope
最重要的事情是调用 RefreshBindings
方法,并传入它所绑定的 UI 元素。 RefreshBindings
方法获取 UI 元素并遍历视觉树,查找与 BoundProperties
列表匹配的任何控件。 找到一个后,它会检查该控件是否具有绑定表达式,如果是,则在绑定表达式上执行 UpdateSource
方法。 这会刷新重新绑定。 为了减少任何性能影响,此视觉树遍历会在某些元素(如 ValidationSummary
)上停止,这些元素通常不会参与验证。 它也会在找到绑定表达式后停止查找子元素。
下一步是通过在 XAML 中附加行为来定义要刷新的 UI 元素的范围。 RefreshBindingScope
可以附加到任何 UI 元素(例如,包含 TextBoxes
的 Grid
)。
<Grid helpers:RefreshBindingScope.Scope="{Binding RefreshBindingScope}">
上面的 XAML 假设你有一个名为 helpers
的命名空间,该命名空间指向你的 RefreshBindingScope
命名空间。
xmlns:helpers="clr-namespace:RefreshBindingExample.Helpers"
Scope
依赖属性绑定到 View Model 中 RefreshBindingScope
的一个实例(我使用了带有此属性的接口,因此如果你使用依赖注入,则可以注入它)。
public IRefreshBindingScope RefreshBindingScope { get; set; }
当用户执行需要已验证数据的命令(例如,单击按钮)时,可以使用 View Model 中的 RefreshBindingScope
来请求 View 刷新绑定,方法是执行 Scope
方法。
RefreshBindingScope.Scope();
如上所示,这会在范围内的元素上执行 UpdateSource
方法。 如果你使用 IDataErrorInfo
或在属性 setter 中引发异常,刷新绑定将告诉 View 显示红色边框、错误工具提示以及验证摘要中的错误,以获取属性 setter 中由于刷新而发生的任何错误。 你还可以检查 View Model 中是否存在错误,具体取决于你的实现。 在附带的示例中,我使用了简单的 IDataErrorInfo
实现,并且有一个 HasErrors()
方法,我可以查询该方法以查看是否应该继续执行该命令。
public void OnSave(object parameter)
{
ClearErrors();
RefreshBindingScope.Scope();
if (!HasErrors())
{
// do the Save
}
}
遍历视觉树
你可能已经注意到,RefreshBindingScope
类中的 RefreshBinding
方法使用了一个扩展方法,名为 GetChildren()
,该方法在依赖属性上使用。 这是一个辅助方法,用于更轻松地访问子控件。 扩展方法的代码如下所示。
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);
}
}
历史
- 2011 年 8 月 - 初始版本。