DelayBinding: a custom WPF Binding

When you use the Outlook 2007 search, Vista's start search, or the Search bar in Explorer, there's often a short delay between when you press a key, and when the search begins.

In WPF, we could simulate this through a series of event handlers, timers and code-behind directly on controls, but we usually want to be able to use this alongside WPF's data binding capabilities. To use WPF data binding in a delayed fashion, I created a simple markup extension which creates a binding and manages the timer delay between commits.

Here's how you can use it:

<TextBox Text="{z:DelayBinding Path=SearchText}" />

You can also set an explicit delay. By default, it uses 0.5 seconds, which felt consistent with Outlook, though I didn't spend that much time working out exactly how long Outlook waits. I did look to see if there was a SystemParameters class for something like "SearchDelay", but couldn't find one. Suggestions for a better default are welcome.

<TextBox Text="{z:DelayBinding Path=SearchText, Delay='00:00:01'}" />

Instead of creating a new type of Binding, I'm using the standard WPF Binding, but setting the UpdateSourceTrigger to Explicit. As the text changes, the timer is reset, and when it ticks the value is pushed to the source.

Delay binding - as the user types, the results do not change

After the short delay:

...but after the short delay, the results change

First, the code to the markup extension (XML-doc comments removed):

[MarkupExtensionReturnType(typeof(object))]
public class DelayBindingExtension : MarkupExtension
{
    public DelayBindingExtension()
    {
        Delay = TimeSpan.FromSeconds(0.5);
    }

    public DelayBindingExtension(PropertyPath path) 
        : this()
    {
        Path = path;
    }

    public IValueConverter Converter { get; set; }
    public object ConverterParamter { get; set; }
    public string ElementName { get; set; }
    public RelativeSource RelativeSource { get; set; }
    public object Source { get; set; }
    public bool ValidatesOnDataErrors { get; set; }
    public bool ValidatesOnExceptions { get; set; }
    public TimeSpan Delay { get; set; }
    [ConstructorArgument("path")]
    public PropertyPath Path { get; set; }
    [TypeConverter(typeof(CultureInfoIetfLanguageTagConverter))]
    public CultureInfo ConverterCulture { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var valueProvider = serviceProvider.GetService(typeof (IProvideValueTarget)) as IProvideValueTarget;
        if (valueProvider != null)
        {
            var bindingTarget = valueProvider.TargetObject as DependencyObject;
            var bindingProperty = valueProvider.TargetProperty as DependencyProperty;
            if (bindingProperty == null || bindingTarget == null)
            {
                throw new NotSupportedException(string.Format(
                    "The property '{0}' on target '{1}' is not valid for a DelayBinding. The DelayBinding target must be a DependencyObject, "
                    + "and the target property must be a DependencyProperty.", 
                    valueProvider.TargetProperty, 
                    valueProvider.TargetObject));
            }

            var binding = new Binding();
            binding.Path = Path;
            binding.Converter = Converter;
            binding.ConverterCulture = ConverterCulture;
            binding.ConverterParameter = ConverterParamter;
            if (ElementName != null) binding.ElementName = ElementName;
            if (RelativeSource != null) binding.RelativeSource = RelativeSource;
            if (Source != null) binding.Source = Source;
            binding.ValidatesOnDataErrors = ValidatesOnDataErrors;
            binding.ValidatesOnExceptions = ValidatesOnExceptions;

            return DelayBinding.SetBinding(bindingTarget, bindingProperty, Delay, binding);
        }
        return null;
    }
}

Now the DelayBinding class, which as you can see above is being instantiated by the DelayBindingExtension. You could also create it manually in code:

public class DelayBinding
{
    private readonly BindingExpressionBase _bindingExpression;
    private readonly DispatcherTimer _timer;

    protected DelayBinding(BindingExpressionBase bindingExpression, DependencyObject bindingTarget, DependencyProperty bindingTargetProperty, TimeSpan delay)
    {
        _bindingExpression = bindingExpression;

        // Subscribe to notifications for when the target property changes. This event handler will be 
        // invoked when the user types, clicks, or anything else which changes the target property
        var descriptor = DependencyPropertyDescriptor.FromProperty(bindingTargetProperty, bindingTarget.GetType());
        descriptor.AddValueChanged(bindingTarget, BindingTarget_TargetPropertyChanged);

        // Add support so that the Enter key causes an immediate commit
        var frameworkElement = bindingTarget as FrameworkElement;
        if (frameworkElement != null)
        {
            frameworkElement.KeyUp += BindingTarget_KeyUp;
        }

        // Setup the timer, but it won't be started until changes are detected
        _timer = new DispatcherTimer();
        _timer.Tick += Timer_Tick;
        _timer.Interval = delay;
    }

    private void BindingTarget_KeyUp(object sender, KeyEventArgs e)
    {
        if (e.Key != Key.Enter) return;
        _timer.Stop();
        _bindingExpression.UpdateSource();
    }

    private void BindingTarget_TargetPropertyChanged(object sender, EventArgs e)
    {
        _timer.Stop();
        _timer.Start();
    }

    private void Timer_Tick(object sender, EventArgs e)
    {
        _timer.Stop();
        _bindingExpression.UpdateSource();
    }

    public static object SetBinding(DependencyObject bindingTarget, DependencyProperty bindingTargetProperty, TimeSpan delay, Binding binding)
    {
        // Override some specific settings to enable the behavior of delay binding
        binding.Mode = BindingMode.TwoWay;
        binding.UpdateSourceTrigger = UpdateSourceTrigger.Explicit;

        // Apply and evaluate the binding
        var bindingExpression = BindingOperations.SetBinding(bindingTarget, bindingTargetProperty, binding);

        // Setup the delay timer around the binding. This object will live as long as the target element lives, since it subscribes to the changing event, 
        // and will be garbage collected as soon as the element isn't required (e.g., when it's Window closes) and the timer has stopped.
        new DelayBinding(bindingExpression, bindingTarget, bindingTargetProperty, delay);

        // Return the current value of the binding (since it will have been evaluated because of the binding above)
        return bindingTarget.GetValue(bindingTargetProperty);
    }
}

I imagine this would be useful for Silverlight, but since Silverlight does not support custom MarkupExtensions, and since Silverlight Binding's can't have an UpdateSourceTrigger (to set it to Explicit), I expect you would end up creating it through an attached dependency property and triggering the binding to push manually. Let me know if you write one.

A picture of me

Welcome, my name is Paul Stovell. I live in Brisbane and work full time bootstrapping my own product company around Octopus Deploy, an automated deployment tool for .NET applications.

Prior to Octopus Deploy, I worked for an investment bank in London building WPF applications, and before that I worked for Readify, an Australian .NET consulting firm, where I was lucky enough to work with some of the best in the business. I also worked on a number of open source projects and was an active user group presenter. I've been a Microsoft MVP for WPF since 2006.

02 Oct 2009

Hi Paul, Nice post, just a puntualization: Silverlight 3 support UpdateSourceTrigger but unfortunately still misses MarkupExtensions.

James Chaldecott
James Chaldecott
02 Oct 2009

Hi Paul. Glad to see you're blogging again!

I remember the first time you posted this, and had cause to use it a month or two ago. When I dug through the sources I found an issue, though, and I think it's still there. The AddValueChanged call on the DependencyPropertyDescriptor will stop the binding source from being garbage collected, as it ends up in a static HashTable.

We've had loads of problems with leaks of this nature in our app (not using DelayBinding), as we add & remove UI as the user navigates, and one rooted control will root everything it's connected to in the visual tree (through the Parent and Child/Children properties of all the controls).

Anyway, I've previously gone to a lot of effort with hooking Loaded & Unloaded events on controls and things like that to be able to add and remove the ValueChanged handlers at the right times. I've just found this blog post, though, which looks like it might offer an answer.

Cheers,

James

P.S. Did you lose all your old website content when it went down, or is there a chance you could restore your old blog posts? There was some gold in there, particularly w.r.t. Bindable.Linq, which we're using a lot.

02 Oct 2009

Hi James,

I'll read the post you linked to and see if I can improve the article - this one was a post of the original article, but I think I have modified code myself somewhere that also made it more reliable.

I still have the old content which I'm slowly converting over. Hopefully in a couple of weeks it will all be back up.

Paul

Reilly
Reilly
17 Nov 2009

Any idea how you could modify this code so that the extension could be used as part of a MultiBinding?

PV
PV
05 Dec 2009

I have a small problem with it. For me the BindingTargetTargetPropertyChanged gets called which starts the timer, and then BindingTargetKeyUp gets called stopping the timer. In BindingTarget_KeyUp is there a reason the _timer.Stop() isn't inside the if statement?

31 Dec 2009

Hi PV,

You are correct, that handler should only take effect when the Enter key is pressed. The PropertyChanged event is used to reset the trigger when the user presses any other keys. I've updated the code.

Paul

Ben
Ben
20 Apr 2010

Hi. First, thanks for this great delayed binding. It's exactly what I need, once I get it working. The problem I'm encountering is that bindingTarget in

var bindingTarget = valueProvider.TargetObject as DependencyObject;

is always null. The TargetObject is some kind of System.Windows.SharedDp, per the exception thrown: "The property 'Text' on target 'System.Windows.SharedDp' is not valid for a DelayBinding...." I'm using this DelayedBinding extension to bind a TextBox to a large string (of xml). Normal bindings work fine. The TextBox is in a DataTemplate. The binding is:

<TextBox Text="{local:DelayBinding Path=Content, ValidatesOnDataErrors=True}" ...

I'm using C# 4.0 on the VS2010 RC. Any suggestions on where the problem is?

07 May 2010

@Ben

This happens because the markup extension is evaluated when the template is parsed, before the controls have been created. See here for more information on how to workaround it: http://tomlev2.wordpress.com/tag/markup-extension/

Ben
Ben
09 May 2010

@Ian Voyce

Thanks a lot!