在TextBox输入暂停期间引发PropertyChanged事件?

5
我想知道是否有可能在用户输入文本框时暂停时引发PropertyChanged事件?更具体地说,我想在用户停止在文本框中输入X秒后运行一个方法。
例如,我有一个只有文本框的表单。用户在文本框中输入1-9位ID值,这会导致一个相当费资源的后台进程加载记录。
我不想使用UpdateSouceTrigger=PropertyChanged,因为这会导致每次键入字符时都运行费资源的后台进程,所以9位ID号将启动9个这样的进程。
我也不想使用UpdateSourceTrigger=LostFocus,因为表单上没有其他内容可以使文本框失去焦点。
那么,有没有办法让我的后台进程仅在用户在输入ID号码时暂停后运行?

1
这是使用自定义的TextBox/Timer的一个例子:在这里 - CodeNaked
5个回答

10

设置UpdateSourceTrigger=PropertyChanged,然后每次属性变化时启动一个所需的延迟时间。如果在计时器滴答之前再次更改属性,则取消旧计时器并启动新计时器。如果计时器滴答,则知道属性在X秒内未更改,并且可以启动后台进程。


@Rachel 我的回答是第一个(现在发布了39分钟,而其他人则是38分钟),但只要你解决了问题,这就是 SO 的目的 :) - dlev
你们都很菜。只有真正的男人才能使用附加的DependencyProperties来创建可重用的行为,这个...呃,随便了。 - user1228
@Will,你的实现很好,充分利用了框架的优势,但是如果不小心使用,它可能会很危险。如果我正确地阅读了你的代码,属性更改事件将在延迟满足之前不会被触发。我个人喜欢dlev的解决方案,因为有些处理程序可能不希望事件有延迟。 - Jay
@Jay:不确定你在说什么。您可以将行为应用于希望延迟绑定更新的任何DependencyObject上。您还可以配置延迟时间。这就是要求。可能会出现一些引用实例挂起的问题,但我对此并不100%确定(我创建了该行为作为学习经验,从未超过原型阶段)。 - user1228
1
让我给你举个例子:我有两个处理程序需要连接到此事件。1)用于后台进程的踢球手(在这里您已经准备好了)。2)一个验证器,需要防止输入超过1个数字(如果您延迟此事件,则用户可以随意输入,只要他们快速输入)。我的意思是处理程序需要提供延迟,而不是触发事件的属性。 - Jay

8

准备进行代码转储。

我使用了一个WPF伪行为(一种像行为的附加DP)。这段代码可以工作,但它并不美观,可能会导致泄漏。可能需要将所有引用替换为弱引用等。

这是行为类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Threading;
using System.Windows.Data;
using System.ComponentModel;

namespace BehaviorForDelayedTrigger
{
    public static class DelayedUpdateBehavior
    {
        #region TargetProperty Attached DependencyProperty
        /// <summary>
        /// An Attached <see cref="DependencyProperty"/> of type <see cref="DependencyProperty"/> defined on <see cref="DependencyObject">DependencyObject instances</see>.
        /// </summary>
        public static readonly DependencyProperty TargetPropertyProperty = DependencyProperty.RegisterAttached(
          TargetPropertyPropertyName,
          typeof(DependencyProperty),
          typeof(DelayedUpdateBehavior),
          new FrameworkPropertyMetadata(null, OnTargetPropertyChanged)
        );

        /// <summary>
        /// The name of the <see cref="TargetPropertyProperty"/> Attached <see cref="DependencyProperty"/>.
        /// </summary>
        public const string TargetPropertyPropertyName = "TargetProperty";

        /// <summary>
        /// Sets the value of the <see cref="TargetPropertyProperty"/> on the given <paramref name="element"/>.
        /// </summary>
        /// <param name="element">The <see cref="DependencyObject">target element</see>.</param>
        public static void SetTargetProperty(DependencyObject element, DependencyProperty value)
        {
            element.SetValue(TargetPropertyProperty, value);
        }

        /// <summary>
        /// Gets the value of the <see cref="TargetPropertyProperty"/> as set on the given <paramref name="element"/>.
        /// </summary>
        /// <param name="element">The <see cref="DependencyObject">target element</see>.</param>
        /// <returns><see cref="DependencyProperty"/></returns>
        public static DependencyProperty GetTargetProperty(DependencyObject element)
        {
            return (DependencyProperty)element.GetValue(TargetPropertyProperty);
        }

        /// <summary>
        /// Called when <see cref="TargetPropertyProperty"/> changes
        /// </summary>
        /// <param name="d">The <see cref="DependencyObject">event source</see>.</param>
        /// <param name="e"><see cref="DependencyPropertyChangedEventArgs">event arguments</see></param>
        private static void OnTargetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var prop = e.NewValue as DependencyProperty;
            if(prop == null)
                return;
            d.Dispatcher.BeginInvoke(
                (Action<DependencyObject, DependencyProperty>)
                    ((target, p) => new PropertyChangeTimer(target, p)), 
                DispatcherPriority.ApplicationIdle, 
                d, 
                prop);

        }
        #endregion
        #region Milliseconds Attached DependencyProperty
        /// <summary>
        /// An Attached <see cref="DependencyProperty"/> of type <see cref="int"/> defined on <see cref="DependencyObject">DependencyObject instances</see>.
        /// </summary>
        public static readonly DependencyProperty MillisecondsProperty = DependencyProperty.RegisterAttached(
          MillisecondsPropertyName,
          typeof(int),
          typeof(DelayedUpdateBehavior),
          new FrameworkPropertyMetadata(1000)
        );

        /// <summary>
        /// The name of the <see cref="MillisecondsProperty"/> Attached <see cref="DependencyProperty"/>.
        /// </summary>
        public const string MillisecondsPropertyName = "Milliseconds";

        /// <summary>
        /// Sets the value of the <see cref="MillisecondsProperty"/> on the given <paramref name="element"/>.
        /// </summary>
        /// <param name="element">The <see cref="DependencyObject">target element</see>.</param>
        public static void SetMilliseconds(DependencyObject element, int value)
        {
            element.SetValue(MillisecondsProperty, value);
        }

        /// <summary>
        /// Gets the value of the <see cref="MillisecondsProperty"/> as set on the given <paramref name="element"/>.
        /// </summary>
        /// <param name="element">The <see cref="DependencyObject">target element</see>.</param>
        /// <returns><see cref="int"/></returns>
        public static int GetMilliseconds(DependencyObject element)
        {
            return (int)element.GetValue(MillisecondsProperty);
        }
        #endregion
        private class PropertyChangeTimer
        {
            private DispatcherTimer _timer;
            private BindingExpression _expression;
            public PropertyChangeTimer(DependencyObject target, DependencyProperty property)
            {
                if (target == null)
                    throw new ArgumentNullException("target");
                if (property == null)
                    throw new ArgumentNullException("property");
                if (!BindingOperations.IsDataBound(target, property))
                    return;
                _expression = BindingOperations.GetBindingExpression(target, property);
                if (_expression == null)
                    throw new InvalidOperationException("No binding was found on property "+ property.Name + " on object " + target.GetType().FullName);
                DependencyPropertyDescriptor.FromProperty(property, target.GetType()).AddValueChanged(target, OnPropertyChanged);
            }

            private void OnPropertyChanged(object sender, EventArgs e)
            {
                if (_timer == null)
                {
                    _timer = new DispatcherTimer();
                    int ms = DelayedUpdateBehavior.GetMilliseconds(sender as DependencyObject);
                    _timer.Interval = TimeSpan.FromMilliseconds(ms);
                    _timer.Tick += OnTimerTick;
                    _timer.Start();
                    return;
                }
                _timer.Stop();
                _timer.Start();
            }

            private void OnTimerTick(object sender, EventArgs e)
            {
                _expression.UpdateSource();
                _expression.UpdateTarget();
                _timer.Stop();
                _timer = null;
            }
        }
    }
}

以下是一个使用示例:

<Window
    x:Class="BehaviorForDelayedTrigger.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:t="clr-namespace:BehaviorForDelayedTrigger">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition
                Height="auto" />
        </Grid.RowDefinitions>
        <Viewbox>
            <TextBlock
                x:Name="TargetTextBlock"
                Background="Red" />
        </Viewbox>
        <TextBox
            t:DelayedUpdateBehavior.TargetProperty="{x:Static TextBox.TextProperty}"
            t:DelayedUpdateBehavior.Milliseconds="1000"
            Grid.Row="1"
            Text="{Binding Text, ElementName=TargetTextBlock, UpdateSourceTrigger=Explicit}" />
    </Grid>
</Window>

这段话的核心思想是...
您需要在绑定的UIElement上设置附加属性,传入您想要延迟的DP。这时,我已经有了附加属性的目标和要延迟的属性,所以我可以进行设置。但我必须等到绑定可用后才能使用Dispatcher实例化我的观察器类。如果不这样做,您将无法获取绑定表达式。
观察器类获取绑定并向DependencyProperty添加更新侦听器。在侦听器中,我设置一个计时器(如果我们尚未更新)或重置计时器。一旦计时器滴答声响起,我就会启动绑定表达式。
再次强调,它确实有效,但肯定需要清理。此外,您可以通过以下代码片段仅使用DP名称:
FieldInfo fieldInfo = instance.GetType()
                             .GetField(name, 
                                 BindingFlags.Public | 
                                 BindingFlags.Static | 
                                 BindingFlags.FlattenHierarchy);
return (fieldInfo != null) ? (DependencyProperty)fieldInfo.GetValue(null) : null;

你可能需要在name后面添加“Property”,但与使用x:Static相比,这很容易。

@Will 这是一种有趣的编写行为方式,使得Xaml用户可以传递多个相关数据。您使用嵌套类似乎是使其仅使用附加属性即可工作的关键。我很想知道您是否考虑过编写Blend行为,如果是,是什么让您选择了这种方法?我也曾经为这些决策而苦恼(我在这里写过:http://stackoverflow.com/a/14506568/718325),对于这些多参数情况,我通常发现编写Blend行为更容易,但有时我更喜欢使用AP。 - Jason Frank
1
@jason 我有点偏见混合行为,因为它们不是BCL的一部分。而且我不清楚你如何在没有安装Blend的情况下首先获取程序集。换句话说,这只是有点可疑。 - user1228

5

如果使用的是 .NET 4.5 或以上版本,则可以使用Binding的Delay属性。非常简单:

<TextBox Text="{Binding Name, Delay=500, UpdateSourceTrigger=PropertyChanged}"/>

4
我认为这正是您所需要的:WPF延迟绑定
这是一种定制绑定,可以完全实现上述两个答案所建议的功能。使用它非常简单,只需编写<TextBox Text="{z:DelayBinding Path=SearchText}" />来指定文本框内容,或者指定延迟间隔<TextBox Text="{z:DelayBinding Path=SearchText, Delay='00:00:03'}" />

2

为什么不使用UpdateSourceTrigger=PropertyChanged,而是让它重置一个定时器,在3秒后触发该过程。这样,如果在3秒内输入了其他内容,则定时器会被重置,并且后台处理将在+3秒后发生。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接