arrow-left arrow-right brightness-2 chevron-left chevron-right circle-half-full dots-horizontal facebook-box facebook loader magnify menu-down RSS star Twitter twitter GitHub white-balance-sunny window-close
DelayBinding: a custom WPF Binding
3 min read

DelayBinding: a custom WPF Binding

This is an old post and doesn't necessarily reflect my current thinking on a topic, and some links or images may not work. The text is preserved here for posterity.

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.

Enjoying these posts? Subscribe for more

Subscribe now
Already have an account? Sign in
Paul Stovell's Blog

Hello, I'm Paul Stovell

I'm a Brisbane-based software developer, and founder of Octopus Deploy, a DevOps automation software company. This is my personal blog where I write about my journey with Octopus and software development.

I write new blog posts about once a month. Subscribe and I'll send you an email when I publish something new.

Subscribe

Comments

You've successfully subscribed to Paul Stovell's Blog.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info is updated.