【问题标题】:Create a delay before validation errors are presented to the user在向用户呈现验证错误之前创建延迟
【发布时间】:2011-09-22 21:55:39
【问题描述】:

在这个遵循 MVVM 模式的特定 WPF 应用程序中,视图模型实现了 IDataErrorInfo 接口来通知视图文本字段中的无效数据。

视图中存在一个文本框,您可以在其中输入卷。这已通过属性更改更新源指定,并验证数据错误:

<TextBox 
    Text="{Binding Volume, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />

这样做的问题是您在用户完成输入之前收到验证错误。例如,有效值为“25 ml”。但是在用户输入最后一个“l”之前,文本框中会出现“25 m”。这不是一个有效值,将导致 IDataError 实现这样说。

结果是,当用户键入时,文本框周围会出现一个红色标记。

我们希望在红色标记出现在文本框周围之前有一点延迟(0.5 秒),因此我们可以假设用户在显示验证错误之前已经完成了输入。

第一次尝试是创建一个专门的文本框,在更新视图模型中的属性之前等待 0.5 秒。但这并不好,因为如果用户确实输入了有效值,那么在启用提交按钮之前会经过 0.5 秒。

我有一个想法,您可以编写一个专门的绑定(即创建一个从 System.Windows.Data.Binding 派生的专门的类)来实现此行为,但我不知道该怎么做。

这是一种合理的方法,还是有更好的方法?

【问题讨论】:

  • 为什么需要UpdateSourceTrigger=PropertyChanged
  • 如果不是,则视图模型上的属性不会更新,直到字段失去焦点。并且因为提交按钮命令有一个 CanExecute() 实现来检查所有字段是否已输入,所以在字段失去焦点之前,您无法单击按钮。

标签: wpf data-binding mvvm


【解决方案1】:

我一直在寻找相同的解决方案,但没有找到解决方案,所以我自己构建了一些东西。我想延迟验证但不延迟设置属性。所以我用计时器和允许异步检查和事件通知的 INotifyDataErrorInfo 做到了。

我进一步改进了它,在输入时立即清除验证错误,并且在输入错误后仅一秒钟再次显示。

public abstract class NotifyDataErrorInfoViewModelBase : ViewModelBase, INotifyDataErrorInfo
{
    private ConcurrentDictionary<string, List<ValidationResult>> modelErrors = new ConcurrentDictionary<string, List<ValidationResult>>();
    private ConcurrentDictionary<string, Timer> modelTimers = new ConcurrentDictionary<string, Timer>();

    public bool HasErrors { get => modelErrors.Any(); }

    public IEnumerable GetErrors(string propertyName)
    {
        modelErrors.TryGetValue(propertyName, out var propertyErrors);
        return propertyErrors;
    }

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    protected NotifyDataErrorInfoViewModelBase() : base()
    { PropertyChanged += (s, e) => Validate(e.PropertyName); }

    private void NotifyErrorsChanged([CallerMemberName] string propertyName = "")
    {
        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
    }

    private void Validate([CallerMemberName] string propertyName = "")
    {
        var timer = modelTimers.AddOrUpdate(propertyName, new Timer(), (key, existingTimer) => { existingTimer.Stop(); return new Timer(); });
        timer.Interval = 1000;
        timer.AutoReset = false;
        modelErrors.TryRemove(propertyName, out var existingErrors);  // clear existing errors immediately
        if (existingErrors?.Count > 0)
            NotifyErrorsChanged(propertyName);
        timer.Elapsed += (s, e) => CheckForErrors(propertyName, existingErrors);
        timer.Start();
    }

    private async void CheckForErrors(string propertyName)
    {
        await Task.Factory.StartNew(() =>
        {
            var errorMessage = "";
            try
            {
                errorMessage = GetValidationMessage(propertyName);
            }
            catch (Exception ex) { errorMessage = "strValidationError"; }
            if (string.IsNullOrEmpty(errorMessage))
            {
                if (existingErrors?.Count > 0)
                    NotifyErrorsChanged(propertyName);
            }
            else
            {
                modelErrors[propertyName] = new List<ValidationResult> { new ValidationResult(errorMessage) };
                NotifyErrorsChanged(propertyName);
            }
        });
    }

    private string GetValidationMessage(string propertyName)
    {
        var property = GetType().GetProperty(propertyName).GetValue(this);
        var validationContext = new ValidationContext(this) { MemberName = propertyName };
        var validationResults = new List<ValidationResult>();
        if (!Validator.TryValidateProperty(property, validationContext, validationResults) && validationResults.Count > 0)
        {
            var messages = new List<string>();
            foreach (var validationResult in validationResults)
            {
                messages.Add(validationResult.ErrorMessage);
            }
            var message = string.Join(Environment.NewLine + "\u25c9 ", messages);
            if (messages.Count > 1)
                message = "\u25c9 " + message;  // add bullet point
            return message;
        }
        return null;
    }
}

我确实将它与 GalaSoft.MvvmLight 一起使用,但我确信您可以使用其他东西(或者根本不使用 ViewModelBase)。

函数 Validate("variableName") 开始验证(这里延迟 1 秒),在我的情况下,我已将其附加到事件 PropertyChanged,但如果您也可以在属性的设置器中调用 Validate()想。

我将它与它结合使用以在 WPF UI 中显示验证:https://stackoverflow.com/a/20394432/9758687

编辑: 或者,WPF 部分也可以使用动画而不使用上面的计时器来延迟。这样做的好处是验证会立即完成,这对于例如验证不成功时禁用按钮很有用。这是我在 ErrorTemplate 中使用的代码:

<Style.Triggers>
    <Trigger Property="IsVisible" Value="True">
        <Trigger.EnterActions>
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation BeginTime="0:0:0.8" Duration="0:0:0.5" To="1.0" Storyboard.TargetProperty="Opacity" />
                </Storyboard>
            </BeginStoryboard>
        </Trigger.EnterActions>
    </Trigger>
</Style.Triggers>

【讨论】:

    【解决方案2】:

    听起来您可以使用 Paul Stovell 在博客中提到的自定义 DelayBinding。我已经成功地使用它来实现延迟搜索/过滤。你可以在这里阅读:

    http://www.paulstovell.com/wpf-delaybinding

    【讨论】:

    • 澄清一下,DelayBinding 延迟更新目标,从而延迟验证,直到您的用户完成在文本框中输入有效值 - 您可以将延迟时间设置为您想要的任何值
    • 这样做的问题是,如果输入有效,它还会延迟提交按钮启用的时间。
    • 但用户可以按 Enter(或您指定的任何其他键)强制提交值并启用提交按钮。否则,我认为这是一种折衷:要么用户在每次击键后收到错误,直到输入有效值,要么在输入有效值后等待半秒以启用提交按钮?无论哪种方式,它都是创建延迟自定义绑定的一个很好的例子。
    猜你喜欢
    • 2020-01-31
    • 1970-01-01
    • 2018-05-25
    • 1970-01-01
    • 1970-01-01
    • 2010-12-30
    • 1970-01-01
    • 2011-10-28
    • 1970-01-01
    相关资源
    最近更新 更多