Rob Dunbar
Rob Dunbar

Reputation: 161

How to wait for WPF Binding Delay to complete

My ViewModel implements the INotifyPropertyChanged and INotifyDataErrorInfo interfaces. When the property is changed, the validation triggers, which in turn enables\disable the Save button.

Because the Validation step is time consuming I've made use of the Delay binding property.

My problem is that I can type my changes and press Save before the 'Name' property is updated.

I'd like to force an immediate update on the TextBox.Text when I press SaveChanges. At the moment, I have to add a sleep before executing to ensure all changes have occurred on the ViewModel.

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

Does anyone have some pointers?

Upvotes: 6

Views: 6822

Answers (5)

jaz
jaz

Reputation: 260

Since .NET 4.5 there exists BindingOperations

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

Upvotes: 3

Bijington
Bijington

Reputation: 3751

I had the same issue in a WPF application and came up with the following solution:

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
}

To use this then you would need to create a property like:

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

And change your TextBox to:

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

Then when processing the SaveChanges Command you can call:

this.Name?.FlushValue();

You will then be able to access the ActualValue from the property. I currently subscribe to the PropertyChanged event on the Name property but I am considering making a specific event for this.

I was hoping to find a solution that would be simpler to use but this is the best I could come up with for now.

Upvotes: 0

Joby James
Joby James

Reputation: 429

Create a Custom Control Text Box and set delay time Property.

public class 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;
  }

}

Upvotes: 0

CharithJ
CharithJ

Reputation: 47570

Not sure the purpose of having a delay in your case. However, couple of other options that I can think of are below.

  1. Set UpdateSourceTrigger to explicit and handle the delay in your own way. Then you can UpdateSource when ever you want.

  2. Use Binding.IsAsync which will get and set values asynchronously. Here is a good example.

Upvotes: 0

Lin Song Yang
Lin Song Yang

Reputation: 1944

You can implement the IPropertyChanged interface on your viewModel, and then from your Name property setter to check if the value has changed, and raise an OnPropertyChanged event for that property.

You can use that property changed event to wire up your SaveChanges command CanExecute method to return false if not updated yet, and return true if the delay is elapsed and the property is updated.

Therefore, the SaveChanges button stays disabled until the CanExecute returns true.

Upvotes: 1

Related Questions