【问题标题】:How to wait for WPF Binding Delay to complete如何等待 WPF 绑定延迟完成
【发布时间】:2015-06-24 01:30:49
【问题描述】:

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

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

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

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

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

有人指点一下吗?

【问题讨论】:

  • 我不确定我是否理解这个问题。为什么你需要睡觉绑定?如果您想在保存后立即更新文本框,延迟绑定不会得到您想要的。

标签: c# wpf binding


【解决方案1】:

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

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

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

【讨论】:

    【解决方案2】:

    从 .NET 4.5 开始存在 BindingOperations

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

    【讨论】:

    • 如果您从一开始就关注该主题,那么您会注意到问题是:“我想强制立即更新 TextBox” .现在看我的代码。你想要我进一步解释什么? BindingOperations 是一个 .net 类。 GetSourceUpdatingBindings(this) 正在从源“this”获取绑定。 ToList() 正在从绑定中创建一个列表。 ForEach() 正在循环列表。 UpdateSource() 正在触发更新源。
    • 正是我想要的。最好从按钮单击强制绑定,然后提供额外的延迟。
    【解决方案3】:

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

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

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

    【讨论】:

      【解决方案4】:

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

      公共类DelayedBindingTextBox:文本框{

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

      }

      【讨论】:

      • 我正在寻找一个更通用的解决方案来解决绑定“延迟”问题。因为我可能会使用不同的控件和不同的值。
      【解决方案5】:

      我在 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 事件,但我正在考虑为此创建一个特定事件。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-05-08
        • 1970-01-01
        • 1970-01-01
        • 2017-05-18
        • 2011-10-26
        相关资源
        最近更新 更多