26

我开始在我的 WPF 应用程序中使用 ValidationRules,但很困惑。

我有以下简单的规则:

class RequiredRule : ValidationRule
{
    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        if (String.IsNullOrWhiteSpace(value as string))
        {
            return new ValidationResult(false, "Must not be empty");
        }
        else
        {
            return new ValidationResult(true, null);
        }

    }
}

在 XAML 中使用如下:

<TextBox>
    <TextBox.Text>
        <Binding Path="Identity.Name">
            <Binding.ValidationRules>
                <validation:RequiredRule/>
            </Binding.ValidationRules>
         </Binding>
     </TextBox.Text>
</TextBox>

主要按我的预期工作。我惊讶地发现我的源属性 ( Identity.Name) 没有被设置;我有一个永远不会看到更改的撤消功能,除了重新键入它(不好)之外,没有办法恢复该值。

微软的数据绑定概述在底部附近描述了验证过程,很好地解释了这种行为。基于此,我希望将我的ValidationStep设置为UpdatedValue.

<validation:RequiredRule ValidationStep="UpdatedValue"/>

这就是我觉得奇怪的地方。我没有使用对象值作为设置的属性值(即字符串)调用 Validate(),而是得到一个System.Windows.Data.BindingExpression! 我在 Microsoft 的文档中没有看到任何描述此行为的内容。

在调试器中,我可以看到源对象( 的DataContextTextBox,导航到属性的路径,并看到值已设置。但是,我没有看到任何在验证规则中获取正确属性的好方法。

注意:使用ValidationStepas ConvertedProposedValue,我得到输入的字符串(我没有使用转换器),但它也会在验证失败时阻止源属性更新,正如预期的那样。使用CommittedValue,我得到的BindingExpression是字符串而不是字符串。

这里有几个问题:

  1. 为什么我会根据 ValidationStep 设置将不一致的参数类型传递给 Validate()?

  2. 如何从 BindingExpression 获得实际值?

  3. 或者,是否有一种好方法可以让用户将 TextBox 恢复到以前的(有效)状态?(正如我所提到的,我自己的撤消功能永远不会看到变化。)

4

4 回答 4

19

我已经解决了从 中提取值的问题,但BindingExpression有一个小的限制。

首先,一些更完整的 XAML:

<Window x:Class="ValidationRuleTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ValidationRuleTest"
        Title="MainWindow" Height="100" Width="525">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <TextBlock Text="String 1"/>
        <TextBox Grid.Column="1">
            <TextBox.Text>
                <Binding Path="String1" UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <local:RequiredRule ValidationStep="RawProposedValue"/>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
        <TextBlock Text="String 2" Grid.Row="1"/>
        <TextBox Grid.Column="1" Grid.Row="1">
            <TextBox.Text>
                <Binding Path="String2" UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <local:RequiredRule ValidationStep="UpdatedValue"/>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
    </Grid>
</Window>

请注意,第一个 TextBox 使用ValidationStep="RawProposedValue"(默认),而第二个使用ValidationStep="UpdatedValue",但两者都使用相同的验证规则。

一个简单的 ViewModel(忽略 INPC 和其他有用的东西):

class MainWindowViewModel
{
    public string String1
    { get; set; }

    public string String2
    { get; set; }
}

最后,新的RequiredRule:

class RequiredRule : ValidationRule
{
    public override ValidationResult Validate(object value,
        System.Globalization.CultureInfo cultureInfo)
    {
        // Get and convert the value
        string stringValue = GetBoundValue(value) as string;

        // Specific ValidationRule implementation...
        if (String.IsNullOrWhiteSpace(stringValue))
        {
            return new ValidationResult(false, "Must not be empty"); 
        }
        else
        {
            return new ValidationResult(true, null); 
        }
    }

    private object GetBoundValue(object value)
    {
        if (value is BindingExpression)
        {
            // ValidationStep was UpdatedValue or CommittedValue (Validate after setting)
            // Need to pull the value out of the BindingExpression.
            BindingExpression binding = (BindingExpression)value;

            // Get the bound object and name of the property
            object dataItem = binding.DataItem;
            string propertyName = binding.ParentBinding.Path.Path;

            // Extract the value of the property.
            object propertyValue = dataItem.GetType().GetProperty(propertyName).GetValue(dataItem, null);

            // This is what we want.
            return propertyValue;
        }
        else
        {
            // ValidationStep was RawProposedValue or ConvertedProposedValue
            // The argument is already what we want!
            return value;
        }
    }
}

如果该方法获得 BindingExpression,则该GetBoundValue()方法将挖掘出我关心的值,或者如果不是,则简单地回退参数。真正的关键是找到“路径”,然后使用它来获取属性及其值。

限制:在我最初的问题中,我的绑定有Path="Identity.Name",因为我正在挖掘我的 ViewModel 的子对象。这不起作用,因为上面的代码期望路径直接指向绑定对象上的属性。幸运的是,我已经扁平化了我的 ViewModel,因此不再是这种情况,但一种解决方法可能是首先将控件的 datacontext 设置为子对象。

我想感谢 Eduardo Brites,因为他的回答和讨论让我重新开始研究这个问题,并且确实为他的难题提供了一块。此外,虽然我打算完全放弃 ValidationRules 并改用 IDataErrorInfo,但我喜欢他关于将它们一起用于不同类型和复杂性的验证的建议。

于 2012-05-18T01:14:55.050 回答
6

这是对 mbmcavoy 的回答的扩展。

我已经修改了该GetBoundValue方法以消除绑定路径的限制。BindingExpression 方便地具有属性 ResolvedSource 和 ResolvedSourcePropertyName,它们在调试器中可见,但不能通过普通代码访问。不过,通过反射获得它们是没有问题的,而且这个解决方案应该适用于任何绑定路径。

private object GetBoundValue(object value)
{
    if (value is BindingExpression)
    {
        // ValidationStep was UpdatedValue or CommittedValue (validate after setting)
        // Need to pull the value out of the BindingExpression.
        BindingExpression binding = (BindingExpression)value;

        // Get the bound object and name of the property
        string resolvedPropertyName = binding.GetType().GetProperty("ResolvedSourcePropertyName", BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance).GetValue(binding, null).ToString();
        object resolvedSource = binding.GetType().GetProperty("ResolvedSource", BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance).GetValue(binding, null);

        // Extract the value of the property
        object propertyValue = resolvedSource.GetType().GetProperty(resolvedPropertyName).GetValue(resolvedSource, null);

        return propertyValue;
    }
    else
    {
        return value;
    }
}
于 2013-03-07T15:58:17.800 回答
4

这是 mbmcavoy 和 adabyron 答案的替代扩展。

为了消除绑定路径的限制,我使用这种方法获取属性值:

public static object GetPropertyValue(object obj, string propertyName)
{
    foreach (String part in propertyName.Split('.'))
    {
        if (obj == null) { return null; }

        Type type = obj.GetType();
        PropertyInfo info = type.GetProperty(part);
        if (info == null) { return null; }

        obj = info.GetValue(obj, null);
    }

    return obj;
}

现在简单地改变

object propertyValue = dataItem.GetType().GetProperty(propertyName).GetValue(dataItem, null);

object propertyValue = GetPropertyValue(dataItem, propertyName);

相关文章使用 C# 中的反射从字符串中获取属性值

于 2013-05-08T09:26:31.447 回答
1

为了回答你的2个问题:

string strVal = (string)((BindingExpression)value).DataItem

于 2012-05-16T14:16:56.120 回答