How to wait for WPF binding to complete

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

Since the validation step takes a long time, I used the Delay binding property.

My problem is that I can enter my changes and click Save before the Name property is updated.

I want to force the TextBox.Text to update immediately when I click SaveChanges. At this point I have to add a sleep before execution to make sure all changes have taken place in the ViewModel.

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

      

Does anyone have pointers?

+3


source to share


4 answers


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

You can use this property changed event to bind your SaveChanges command to the CanExecute method to return false if it has not yet been updated and return true if the delay timed out and the property was updated.



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

+1


source


Not sure if you have a delay in your case. However, a couple of other options I can think of are below.



  • Set the UpdateSourceTrigger explicitly and handle the delay differently. Then you can UpdateSource whenever you want.

  • Use Binding.IsAsync , which will get and set values ​​asynchronously. Here's a good example .

0


source


Create a custom control text box and set the timeout.

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

      

}

0


source


I had the same problem 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, you 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, while processing the command, SaveChanges

you can call:

this.Name?.FlushValue();

      

Then you will be able to access ActualValue from the property. I am currently subscribing to the PropertyChanged event on the Name property, but I am considering creating a specific event for this.

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

0


source







All Articles