如何等待WPF绑定延迟完成

时间:2015-06-24 01:30:49

标签: c# wpf binding

我的ViewModel实现了INotifyPropertyChanged和INotifyDataErrorInfo接口。更改属性后,将触发验证,然后启用\禁用“保存”按钮。

因为验证步骤非常耗时,所以我使用了延迟绑定属性。

我的问题是我可以输入我的更改并在“名称”属性更新之前按“保存”。

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

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

有没有人有指示?

5 个答案:

答案 0 :(得分:1)

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

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

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

答案 1 :(得分:0)

不确定您的案件有延迟的目的。但是,我能想到的其他几个选项如下。

  1. 将UpdateSourceTrigger设置为explicit并以您自己的方式处理延迟。然后你可以随时使用UpdateSource。

  2. 使用Binding.IsAsync,它将异步获取和设置值。 Here is a good example

答案 2 :(得分:0)

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

公共类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;
  }

}

答案 3 :(得分:0)

我在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更改为:

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

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

this.Name?.FlushValue();

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

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

答案 4 :(得分:0)

.NET 4.5开始存在BindingOperations

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