如何等待WPF绑定延迟完成



My ViewModel实现INotifyPropertyChanged和INotifyDataErrorInfo接口。当属性更改时,验证将触发,从而启用\禁用"保存"按钮。

因为Validation步骤很耗时,所以我使用了Delay绑定属性。

我的问题是,在"名称"属性更新之前,我可以键入我的更改并按"保存"。

当我按下SaveChanges时,我想强制立即更新TextBox.Text。目前,我必须在执行之前添加睡眠,以确保ViewModel上发生了所有更改。

<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged, Delay=1000}" />
<Button Command="{Binding SaveChanges}" />

有人有什么建议吗?

由于.NET 4.5存在BindingOperations

BindingOperations.GetSourceUpdatingBindings(this).ToList().ForEach(x => x.UpdateSource());

您可以在viewModel上实现IPropertyChanged接口,然后从Name属性设置器检查值是否已更改,并引发该属性的OnPropertyChanged事件。

您可以使用该属性更改事件连接SaveChanges命令CanExecute方法,如果尚未更新,则返回false;如果延迟已过并且属性已更新,则将返回true。

因此,SaveChanges按钮将保持禁用状态,直到CanExecute返回true为止。

不确定在您的案例中延迟的目的。然而,我能想到的其他几个选择如下。

  1. 将UpdateSourceTrigger设置为显式并以自己的方式处理延迟。然后,您可以随时更新源代码。

  2. 使用Binding.IsAsync,它将异步获取和设置值。这里有一个很好的例子。

创建一个自定义控件文本框并设置延迟时间属性。

公共类DelayedBindingTextBox:TextBox{

  private Timer timer;
  private delegate void Method();
  /// <summary>
  /// Gets and Sets the amount of time to wait after the text has changed before updating the binding
  /// </summary>
  public int DelayTime {
     get { return (int)GetValue(DelayTimeProperty); }
     set { SetValue(DelayTimeProperty, value); }
  }
  // Using a DependencyProperty as the backing store for DelayTime.  This enables animation, styling, binding, etc...
  public static readonly DependencyProperty DelayTimeProperty =
      DependencyProperty.Register("DelayTime", typeof(int), typeof(DelayedBindingTextBox), new UIPropertyMetadata(667));

  //override this to update the source if we get an enter or return
  protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e) {
     //we dont update the source if we accept enter
     if (this.AcceptsReturn == true) { }
     //update the binding if enter or return is pressed
     else if (e.Key == Key.Return || e.Key == Key.Enter) {
        //get the binding
        BindingExpression bindingExpression = this.GetBindingExpression(TextBox.TextProperty);
        //if the binding is valid update it
        if (BindingCanProceed(bindingExpression)){
           //update the source
           bindingExpression.UpdateSource();
        }
     }
     base.OnKeyDown(e);
  }
  protected override void OnTextChanged(TextChangedEventArgs e) {
     //get the binding
     BindingExpression bindingExpression = this.GetBindingExpression(TextBox.TextProperty);
     if (BindingCanProceed(bindingExpression)) {
        //get rid of the timer if it exists
        if (timer != null) {
           //dispose of the timer so that it wont get called again
           timer.Dispose();
        }
        //recreate the timer everytime the text changes
        timer = new Timer(new TimerCallback((o) => {
           //create a delegate method to do the binding update on the main thread
           Method x = (Method)delegate {
              //update the binding
              bindingExpression.UpdateSource();
           };
           //need to check if the binding is still valid, as this is a threaded timer the text box may have been unloaded etc.
           if (BindingCanProceed(bindingExpression)) {
              //invoke the delegate to update the binding source on the main (ui) thread
              Dispatcher.Invoke(x, new object[] { });
           }
           //dispose of the timer so that it wont get called again
           timer.Dispose();
        }), null, this.DelayTime, Timeout.Infinite);
     }
     base.OnTextChanged(e);
  }
  //makes sure a binding can proceed
  private bool BindingCanProceed(BindingExpression bindingExpression) {
     Boolean canProceed = false;
     //cant update if there is no BindingExpression
     if (bindingExpression == null) { }
     //cant update if we have no data item
     else if (bindingExpression.DataItem == null) { }
     //cant update if the binding is not active
     else if (bindingExpression.Status != BindingStatus.Active) { }
     //cant update if the parent binding is null
     else if (bindingExpression.ParentBinding == null) { }
     //dont need to update if the UpdateSourceTrigger is set to update every time the property changes
     else if (bindingExpression.ParentBinding.UpdateSourceTrigger == UpdateSourceTrigger.PropertyChanged) { }
     //we can proceed
     else {
        canProceed = true;
     }
     return canProceed;
  }

}

我在WPF应用程序中遇到了同样的问题,并提出了以下解决方案:

public class DelayedProperty<T> : INotifyPropertyChanged
{
    #region Fields
    private T actualValue;
    private DispatcherTimer timer;
    private T value;
    #endregion
    #region Properties
    public T ActualValue => this.actualValue;
    public int Delay { get; set; } = 800;
    public bool IsPendingChanges => this.timer?.IsEnabled == true;
    public T Value
    {
        get
        {
            return this.value;
        }
        set
        {
            if (this.Delay > 0)
            {
                this.value = value;
                if (timer == null)
                {
                    timer = new DispatcherTimer();
                    timer.Interval = TimeSpan.FromMilliseconds(this.Delay);
                    timer.Tick += ValueChangedTimer_Tick;
                }
                if (timer.IsEnabled)
                {
                    timer.Stop();
                }
                timer.Start();
                this.RaisePropertyChanged(nameof(IsPendingChanges));
            }
            else
            {
                this.value = value;
                this.SetField(ref this.actualValue, value);
            }
        }
    }
    #endregion
    #region Event Handlers
    private void ValueChangedTimer_Tick(object sender, EventArgs e)
    {
        this.FlushValue();
    }
    #endregion
    #region Public Methods
    /// <summary>
    /// Force any pending changes to be written out.
    /// </summary>
    public void FlushValue()
    {
        if (this.IsPendingChanges)
        {
            this.timer.Stop();
            this.SetField(ref this.actualValue, this.value, nameof(ActualValue));
            this.RaisePropertyChanged(nameof(IsPendingChanges));
        }
    }
    /// <summary>
    /// Ignore the delay and immediately set the value.
    /// </summary>
    /// <param name="value">The value to set.</param>
    public void SetImmediateValue(T value)
    {
        this.SetField(ref this.actualValue, value, nameof(ActualValue));
    }
    #endregion
    #region INotifyPropertyChanged Members
    public event PropertyChangedEventHandler PropertyChanged;
    protected bool SetField<U>(ref U field, U valueField, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<U>.Default.Equals(field, valueField)) { return false; }
        field = valueField;
        this.RaisePropertyChanged(propertyName);
        return true;
    }
    protected void RaisePropertyChanged(string propertyName)
    {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion
}

要使用它,您需要创建一个属性,如:

public DelayedProperty<string> Name { get;set; } // Your choice of DP or INPC if you desire.

并将您的文本框更改为:

<TextBox Text="{Binding Name.Value, UpdateSourceTrigger=PropertyChanged}" />

然后在处理SaveChanges命令时,您可以调用:

this.Name?.FlushValue();

然后,您将能够从属性中访问ActualValue。我目前订阅了Name属性上的PropertyChanged事件,但我正在考虑为此创建一个特定的事件。

我希望能找到一个更简单的解决方案,但这是我目前能想到的最好的解决方案。

最新更新