Reputation: 161
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
Reputation: 260
Since .NET 4.5 there exists BindingOperations
BindingOperations.GetSourceUpdatingBindings(this).ToList().ForEach(x => x.UpdateSource());
Upvotes: 3
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
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
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.
Set UpdateSourceTrigger to explicit and handle the delay in your own way. Then you can UpdateSource when ever you want.
Use Binding.IsAsync which will get and set values asynchronously. Here is a good example.
Upvotes: 0
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