Delay property on Binding from .Net 4.5 in .Net 4.

2019-03-09 13:08发布

问题:

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?

回答1:

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.



回答2:

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();
        }
    }
}


回答3:

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...