13

How can I implement Delay property from .Net 4.5 (described here) on binding in .Net 4.0?

I know I cannot inherit from BindingBase as ProvideValue is sealed.

I could implement MarkupExtension but it means I now have to rewrite all properties from BindingExtension is there any other way?

Kirk Broadhurst
  • 27,836
  • 16
  • 104
  • 169
bartosz.lipinski
  • 2,627
  • 2
  • 21
  • 34
  • Could you not inherit BindingBase and create a new method and simply call ProvideValue within that method? – Security Hound Oct 05 '11 at 13:30
  • @Ramhound No as ProvideValue is sealed and new method will not be used. – bartosz.lipinski Oct 05 '11 at 13:44
  • Is this just for a specific application or for a general implementation. As a one off could have one property call another and just delay with a Thread.Sleep()? Delay in the code behind and have the code behind call the real library. – paparazzo Oct 05 '11 at 16:14
  • @BalamBalam sure it would be the simplest way of doing it. I actually considered that at the beginning but with multiple properties like that it's adding a lot of logic which is not connected to my viewModel almost at all. – bartosz.lipinski Oct 05 '11 at 16:26

3 Answers3

7

t the end I've decided to implement DelayedBinding as MarkupExtension using composition.

The only problem I had was with DataTemplates ProvideValue should return this if TargetProperty from IProvideValueTarget is null.

[MarkupExtensionReturnType(typeof(object))]
public class DelayedBindingExtension : MarkupExtension
{
    private readonly Binding _binding = new Binding();

    public DelayedBindingExtension()
    {
        //Default value for delay
        Delay = TimeSpan.FromSeconds(0.5);
    }

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

    #region properties

    [DefaultValue(null)]
    public object AsyncState
    {
        get { return _binding.AsyncState; }
        set { _binding.AsyncState = value; }
    }

    [DefaultValue(false)]
    public bool BindsDirectlyToSource
    {
        get { return _binding.BindsDirectlyToSource; }
        set { _binding.BindsDirectlyToSource = value; }
    }

    [DefaultValue(null)]
    public IValueConverter Converter
    {
        get { return _binding.Converter; }
        set { _binding.Converter = value; }
    }

    [TypeConverter(typeof(CultureInfoIetfLanguageTagConverter)), DefaultValue(null)]
    public CultureInfo ConverterCulture
    {
        get { return _binding.ConverterCulture; }
        set { _binding.ConverterCulture = value; }
    }

    [DefaultValue(null)]
    public object ConverterParameter
    {
        get { return _binding.ConverterParameter; }
        set { _binding.ConverterParameter = value; }
    }

    [DefaultValue(null)]
    public string ElementName
    {
        get { return _binding.ElementName; }
        set { _binding.ElementName = value; }
    }

    [DefaultValue(null)]
    public object FallbackValue
    {
        get { return _binding.FallbackValue; }
        set { _binding.FallbackValue = value; }
    }

    [DefaultValue(false)]
    public bool IsAsync
    {
        get { return _binding.IsAsync; }
        set { _binding.IsAsync = value; }
    }

    [DefaultValue(BindingMode.Default)]
    public BindingMode Mode
    {
        get { return _binding.Mode; }
        set { _binding.Mode = value; }
    }

    [DefaultValue(false)]
    public bool NotifyOnSourceUpdated
    {
        get { return _binding.NotifyOnSourceUpdated; }
        set { _binding.NotifyOnSourceUpdated = value; }
    }

    [DefaultValue(false)]
    public bool NotifyOnTargetUpdated
    {
        get { return _binding.NotifyOnTargetUpdated; }
        set { _binding.NotifyOnTargetUpdated = value; }
    }

    [DefaultValue(false)]
    public bool NotifyOnValidationError
    {
        get { return _binding.NotifyOnValidationError; }
        set { _binding.NotifyOnValidationError = value; }
    }

    [DefaultValue(null)]
    public PropertyPath Path
    {
        get { return _binding.Path; }
        set { _binding.Path = value; }
    }

    [DefaultValue(null)]
    public RelativeSource RelativeSource
    {
        get { return _binding.RelativeSource; }
        set { _binding.RelativeSource = value; }
    }

    [DefaultValue(null)]
    public object Source
    {
        get { return _binding.Source; }
        set { _binding.Source = value; }
    }

    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public UpdateSourceExceptionFilterCallback UpdateSourceExceptionFilter
    {
        get { return _binding.UpdateSourceExceptionFilter; }
        set { _binding.UpdateSourceExceptionFilter = value; }
    }

    [DefaultValue(UpdateSourceTrigger.Default)]
    public UpdateSourceTrigger UpdateSourceTrigger
    {
        get { return _binding.UpdateSourceTrigger; }
        set { _binding.UpdateSourceTrigger = value; }
    }

    [DefaultValue(null)]
    public object TargetNullValue
    {
        get { return _binding.TargetNullValue; }
        set { _binding.TargetNullValue = value; }
    }

    [DefaultValue(null)]
    public string StringFormat
    {
        get { return _binding.StringFormat; }
        set { _binding.StringFormat = value; }
    }

    [DefaultValue(false)]
    public bool ValidatesOnDataErrors
    {
        get { return _binding.ValidatesOnDataErrors; }
        set { _binding.ValidatesOnDataErrors = value; }
    }

    [DefaultValue(false)]
    public bool ValidatesOnExceptions
    {
        get { return _binding.ValidatesOnExceptions; }
        set { _binding.ValidatesOnExceptions = value; }
    }

    [DefaultValue(null)]
    public string XPath
    {
        get { return _binding.XPath; }
        set { _binding.XPath = value; }
    }

    [DefaultValue(null)]
    public Collection<ValidationRule> ValidationRules
    {
        get { return _binding.ValidationRules; }
    }

    #endregion

    [DefaultValue(null)]
    public TimeSpan Delay { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        try
        {
            _binding.Mode = BindingMode.TwoWay;
            _binding.UpdateSourceTrigger = UpdateSourceTrigger.Explicit;
        }
        catch (InvalidOperationException)  //Binding in use already don't change it
        {
        }

        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)
            {
                var result = (BindingExpression)_binding.ProvideValue(serviceProvider);

                new DelayBindingManager(result, bindingTarget, bindingProperty, Delay);
                return result;
            }
        }

        return this;
    }

    private class DelayBindingManager
    {
        private readonly BindingExpressionBase _bindingExpression;
        private readonly DependencyProperty _bindingTargetProperty;
        private DependencyPropertyDescriptor _descriptor;
        private readonly DispatcherTimer _timer;

        public DelayBindingManager(BindingExpressionBase bindingExpression, DependencyObject bindingTarget, DependencyProperty bindingTargetProperty, TimeSpan delay)
        {
            _bindingExpression = bindingExpression;
            _bindingTargetProperty = bindingTargetProperty;

            _descriptor = DependencyPropertyDescriptor.FromProperty(_bindingTargetProperty, bindingTarget.GetType());
            if (_descriptor != null)
                _descriptor.AddValueChanged(bindingTarget, BindingTargetTargetPropertyChanged);

            _timer = new DispatcherTimer();
            _timer.Tick += TimerTick;
            _timer.Interval = delay;
        }

        private void BindingTargetTargetPropertyChanged(object sender, EventArgs e)
        {
            var source = (DependencyObject)sender;
            if (!BindingOperations.IsDataBound(source, _bindingTargetProperty))
            {
                if (_descriptor != null)
                {
                    _descriptor.RemoveValueChanged(source, BindingTargetTargetPropertyChanged);
                    _descriptor = null;
                }
                return;
            }

            _timer.Stop();
            _timer.Start();
        }

        private void TimerTick(object sender, EventArgs e)
        {
            _timer.Stop();
            _bindingExpression.UpdateSource();
        }
    }
}
bartosz.lipinski
  • 2,627
  • 2
  • 21
  • 34
3

I would create an AttachedProperty that specifies the amount of time to Delay. The AttachedProperty would start (or reset) a timer when the bound value changes, and would manually update the bound source when the specified amount of time gets reached.

You can use the following to update the source binding:

BindingOperations.GetBindingExpressionBase(
    dependencyObject, dependencyProperty).UpdateSource();

Edit

I was fixing a bug in some old code today and noticed it implemented a delayed property change notification using an Attached Behavior. I thought of this question, so followed the link that I had commented in the code, and found myself at a question I had posted a while ago on SO about delaying a binding. The top answer is the one I have implemented currently, which is some attached properties that updates the source of a binding after X milliseconds have passed.

Community
  • 1
  • 1
Rachel
  • 130,264
  • 66
  • 304
  • 490
  • I think you need then 2 attached properties one with Delay and second for identifying on which property binding should be delayed. Additionally I have a feeling it will be hard to get BindingExpression with attached properties if binding was done on the datatemplate or in style. – bartosz.lipinski Oct 05 '11 at 13:42
  • @baalazamon Hrrm that's true, I guess it would only work if you knew what binding you wanted to delay. You could also try overwriting a `BindingBase` to accomplish the same thing like Ramhound suggests. I usually only overwrite those to avoid having to set defaults repeatedly, but I don't see why you couldn't alter the binding behavior. – Rachel Oct 05 '11 at 13:44
  • you cannot modify binding makupextension behaviour by inheriting from it as ProvideValue method used by XAML parser is sealed :( – bartosz.lipinski Oct 05 '11 at 14:30
  • @baalazamon What about a custom MarkupExtension instead of a Binding? Here's an example.. http://www.hardcodet.net/2008/04/wpf-custom-binding-class – Rachel Oct 05 '11 at 14:38
  • sure it will work I know how to do that but there is small disadvantage (I mentioned this in question) which is adding all properties from Binding to my new extension. – bartosz.lipinski Oct 05 '11 at 14:57
  • @baalazamon See my edit. I was quite surprised to discover I already had a delay binding implemented in some old code of mine, and the source of it was a SO question I had asked a while back :) – Rachel Oct 05 '11 at 20:06
  • I really like the idea of using attached properties (it's nice and clean). I haven't checked that but I have a feeling it is not working with DataTemplates but maybe it is. There is also memory leak in there connected to using AddValueChanged take a loook at this link: http://sharpfellows.com/post/Memory-Leaks-and-Dependency-Properties.aspx – bartosz.lipinski Oct 06 '11 at 09:07
2

Straightaway porting is not possible but can we "simulate" this using MultiBinding

Mind you that this is very tightly coupled solution and may not perform well if many of such bindings are used on a page...

Two must haves ...

  1. It accepts the delay in milliseconds in a single item ArrayList as a converter parameter.
  2. Every such delayed binding must carry its own instance of converter parameter.

The Test XAML is as below...

    <TextBlock xmlns:Collections="clr-namespace:System.Collections;assembly=mscorlib"
               xmlns:System="clr-namespace:System;assembly=mscorlib" >
        <TextBlock.Resources>
            <local:DelayHelper x:Key="DelayHelper"/>
            <Collections:ArrayList x:Key="MultiConverterParameter">
                <System:Int32>2000</System:Int32>
            </Collections:ArrayList>
        </TextBlock.Resources>
        <TextBlock.Text>
            <MultiBinding UpdateSourceTrigger="LostFocus"
                 Converter="{StaticResource DelayHelper}"
                 ConverterParameter="{StaticResource MultiConverterParameter}">
                <Binding Path="Text" ElementName="MyTextBox" Mode="OneWay" />
                <Binding RelativeSource="{RelativeSource Self}"/>                    
                <Binding BindsDirectlyToSource="True"
                         Source="{x:Static TextBlock.TextProperty}"/>
            </MultiBinding>
        </TextBlock.Text>
    </TextBlock>

    <TextBox x:Name="MyTextBox" Text="Test..."/>

In this example a TextBlock renders of what is typed in TextBox below after a 2 seconds delay. The TextBox.Text is primary source of data.

DelayHelper is multi converter that works as shown below...

public class DelayHelper : IMultiValueConverter
{
    #region IMultiValueConverter Members

    public object Convert(
         object[] values,
         Type targetType,
         object parameter,
         System.Globalization.CultureInfo culture)
    {
        var sourceElement = values[1] as FrameworkElement;
        var dp = values[2] as DependencyProperty;
        var paramArray = parameter as ArrayList;
        var existingValue
                = paramArray != null && paramArray.Count == 2
                      ? paramArray[1] : sourceElement.GetValue(dp);

        var newValue = values[0];

        var bndExp = BindingOperations.GetMultiBindingExpression(sourceElement, dp);

        var temp = new DispatcherTimer() { IsEnabled = false };
        var dspTimer
            = new DispatcherTimer(
                new TimeSpan(0,0,0,0, int.Parse(paramArray[0].ToString())),
                DispatcherPriority.Background,
                new EventHandler(
                    delegate
                    {
                        if (bndExp != null && existingValue != newValue)
                        {
                            var array
                                 = bndExp.ParentMultiBinding.ConverterParameter
                                     as ArrayList;
                            var existingInterval = array[0];
                            array.Clear();
                            array.Add(existingInterval);
                            array.Add(newValue);
                            bndExp.UpdateTarget();
                        }

                        temp.Stop();
                    }),
                sourceElement.Dispatcher);

        temp = dspTimer;
        dspTimer.Start();
        return existingValue;
    }

    public object[] ConvertBack(
         object value,
         Type[] targetTypes,
         object parameter,
         System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion
}

So this code makes use of the facts that

  1. MultiBinding can accept the target UI element (TextBlock) and its dependency property (TextBlock.TextProperty) that itself is multi-bound.
  2. Once bound the multi binding cannot alter its properties including the ConveterParameter. But the converter parameter itself can be a reference object that maintains its reference throughout the binding is active e.g. ArrayList.
  3. The DispatcherTimer has to stop after its first Tick. Hence we use of the temp variable is very essential.
  4. The updates make 2 converter passes for each source text update. There is no escpae from this behavior. This may cause slowness is many delayed bindings are used.
  5. Make sure you do not share the same converter parameter among multiple delayed bindings

Let me know if this helps...

WPF-it
  • 19,625
  • 8
  • 55
  • 71
  • Unfortunately as far as I know converter is evaluated in UI thread and IsAsync is not affecting it. – bartosz.lipinski Oct 05 '11 at 14:16
  • Thx! I will try that later but it looks a bit complicated and the binding is hard to understand. – bartosz.lipinski Oct 05 '11 at 16:23
  • I use a converter with IsAsync = true. Had too. FlowDocument derives from Dispatcher and will not work with Async. I serialze to string and then de-serialize in a converter. – paparazzo Oct 05 '11 at 17:05