Delayed Binding in Silverlight

While Delayed Binding doesn’t exist in Silverlight (new in WPF .NET 4.5), we can make do with UpdateSourceTrigger.Explicit and a little attached property.

Here’s the end result:


            <TextBox x:Name="FittedPartsFilter"
                     Margin="10,7,48,0"
                     HorizontalAlignment="Stretch"
                     VerticalAlignment="Top"
                     ext:DelayTextChangedExtension.TextChangedDelay="1000"
                     Style="{StaticResource SearchTextBoxStyle}"
                     Text="{Binding FittedPartsFilter,
                                    Mode=TwoWay}" />


    public class DelayTextChangedExtension
    {
        #region Private Properties
        internal static Lazy<DispatcherTimer> _timer = new Lazy<DispatcherTimer>(() => { return new DispatcherTimer(); });
        internal static WeakReference _textBox = null;
        #endregion

        #region TextChangedDelay

        /// <summary>
        /// TextChangedDelay Attached Dependency Property
        /// </summary>
        public static readonly DependencyProperty TextChangedDelayProperty =
            DependencyProperty.RegisterAttached("TextChangedDelay", typeof(double), typeof(TextBox),
                new FrameworkPropertyMetadata((double)1001,
                    new PropertyChangedCallback(OnTextChangedDelayChanged)));

        /// <summary>
        /// Gets the TextChangedDelay property. This dependency property 
        /// indicates number of milliseconds elapsed before updating the binding source.
        /// </summary>
        public static double GetTextChangedDelay(DependencyObject d)
        {
            return (double)d.GetValue(TextChangedDelayProperty);
        }

        /// <summary>
        /// Sets the TextChangedDelay property. This dependency property 
        /// indicates number of milliseconds elapsed before updating the binding source.
        /// </summary>
        public static void SetTextChangedDelay(DependencyObject d, double value)
        {
            d.SetValue(TextChangedDelayProperty, value);
            SetupBindingAndHandlers(d as TextBox, textBox_TextChanged2, true);
        }


        /// <summary>
        /// Handles changes to the TextChangedDelay property.
        /// </summary>
        private static void OnTextChangedDelayChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            double oldTextChangedDelay = (double)e.OldValue;
            double newTextChangedDelay = (double)d.GetValue(TextChangedDelayProperty);

            SetupBindingAndHandlers(d as TextBox, textBox_TextChanged2, true);
        }

        static void textBox_TextChanged2(object sender, TextChangedEventArgs e)
        {
            _textBox = new WeakReference(sender);
            _timer.Value.Interval = TimeSpan.FromMilliseconds(GetTextChangedDelay(_textBox.Target as TextBox));
            _timer.Value.Stop();
            _timer.Value.Start();
            _timer.Value.Tick += new EventHandler(UpdateSourceOnTimer);
        }

        static void UpdateSourceOnTimer(object sender, EventArgs e)
        {
            (_textBox.Target as TextBox).GetBindingExpression(TextBox.TextProperty).UpdateSource();
        }
        #endregion

        #region Helper

        internal static void SetupBindingAndHandlers(TextBox textBox, TextChangedEventHandler handler,
            bool firstTime)
        {
            BindingExpression be = textBox.GetBindingExpression(TextBox.TextProperty);
            if (be != null)
            {
                if (be.ParentBinding.UpdateSourceTrigger != UpdateSourceTrigger.Explicit)
                {
                    Binding bindingNew = Clone(be.ParentBinding);
                    bindingNew.UpdateSourceTrigger = UpdateSourceTrigger.Explicit;
                    textBox.SetBinding(TextBox.TextProperty, bindingNew);
                }
                textBox.TextChanged += new TextChangedEventHandler(handler);
            }
            else
            {
                // Drats, the attached property is attached before the Text
                // binding is set. Wait until textbinding is set then try again.
                // TODO: This routine will fail if UpdateSourceTrigger is already set to Explicit
                RegisterForNotification("Text", textBox, (s, e) =>
                {
                    BindingExpression be2 = textBox.GetBindingExpression(TextBox.TextProperty);
                    if (be2 != null && be2.ParentBinding.UpdateSourceTrigger != UpdateSourceTrigger.Explicit)
                        SetupBindingAndHandlers(textBox, handler, false);
                });
            }
        }

        internal static Binding Clone(Binding bindingOld)
        {
            Binding bindingNew = new Binding()
            {
                BindsDirectlyToSource = bindingOld.BindsDirectlyToSource,
                Converter = bindingOld.Converter,
                ConverterCulture = bindingOld.ConverterCulture,
                ConverterParameter = bindingOld.ConverterParameter,
                FallbackValue = bindingOld.FallbackValue,
                Mode = bindingOld.Mode,
                NotifyOnValidationError = bindingOld.NotifyOnValidationError,
                Path = bindingOld.Path,
                StringFormat = bindingOld.StringFormat,
                TargetNullValue = bindingOld.TargetNullValue,
                UpdateSourceTrigger = bindingOld.UpdateSourceTrigger,
                ValidatesOnDataErrors = bindingOld.ValidatesOnDataErrors,
                ValidatesOnExceptions = bindingOld.ValidatesOnExceptions,
                ValidatesOnNotifyDataErrors = bindingOld.ValidatesOnNotifyDataErrors,
            };

            if (bindingOld.RelativeSource != null)
                bindingNew.RelativeSource = bindingOld.RelativeSource;
            else if (bindingOld.Source != null)
                bindingNew.Source = bindingOld.Source;
            else if (bindingOld.ElementName != null)
                bindingNew.ElementName = bindingOld.ElementName;

            return bindingNew;
        }


        /// Listen for change of the dependency property
        public static void RegisterForNotification(string propertyName, FrameworkElement element, PropertyChangedCallback callback)
        {

            //Bind to a dependency property
            Binding b = new Binding(propertyName) { Source = element };
            var prop = System.Windows.DependencyProperty.RegisterAttached(
                "ListenAttached" + propertyName,
                typeof(object),
                typeof(UserControl),
                new System.Windows.PropertyMetadata(callback));

            element.SetBinding(prop, b);
        }
        #endregion
    }

About this entry