WPF - Delayed Multibinding

2019-06-17 15:34发布

I have a multibinding that looks something like this:

<UserControl.Visibility>
    <MultiBinding Converter="{StaticResource isMouseOverToVisibiltyConverter}">
        <Binding ElementName="otherElement" Path="IsMouseOver" />
        <Binding RelativeSource="{RelativeSource Self}" Path="IsMouseOver" />
    </MultiBinding>
</UserControl.Visibility>

And, I want to be able to add a delay between IsMouseOver going to false for both bindings, and the Visibility being set to Collapsed.

I found this DelayBinding implementation: http://www.paulstovell.com/wpf-delaybinding

But, that doesn't work for MultiBinding, and I've been unable to figure out how to make one that works with MultiBinding.

I do have the option of doing the changes to Visibility in events in the code-behind, and that would work, but it would be nice if there was some way to do this through the binding system.

Is there some way to add a delay to a MultiBinding?

EDIT: Ray, in order to get your class to compile & run, I had to make some fixes. However, something is still wrong, as the updates aren't being propagated. It seems to only update the target property once.

[ContentProperty("Bindings")]
public class DelayedMultiBindingExtension : MarkupExtension, IMultiValueConverter, INotifyPropertyChanged
{
    public Collection<BindingBase> Bindings { get; private set; }
    public IMultiValueConverter Converter { get; set; }
    public object ConverterParameter { get; set; }
    public CultureInfo ConverterCulture { get; set; }
    public BindingMode Mode { get; set; }
    public UpdateSourceTrigger UpdateSourceTrigger { get; set; }

    public object CurrentValue { get { return _delayedValue; } set { _delayedValue = _undelayedValue = value; _timer.Stop(); } }

    private object _undelayedValue;
    private object _delayedValue;

    private DispatcherTimer _timer;
    public int ChangeCount { get; private set; }  // Public so Binding can bind to it

    public DelayedMultiBindingExtension()
    {
        this.Bindings = new Collection<BindingBase>();
        _timer = new DispatcherTimer();
        _timer.Tick += _timer_Tick;
        _timer.Interval = TimeSpan.FromMilliseconds(500);
    }

    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;

            var multi = new MultiBinding { Converter = this, Mode = Mode, UpdateSourceTrigger = UpdateSourceTrigger };
            foreach (var binding in Bindings)
                multi.Bindings.Add(binding);
            multi.Bindings.Add(new Binding("ChangeCount") { Source = this, Mode = BindingMode.OneWay });

            var bindingExpression = BindingOperations.SetBinding(bindingTarget, bindingProperty, multi);

            return bindingTarget.GetValue(bindingProperty);
        }

        return null;
    }

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        object newValue =
          Converter.Convert(
            values.Take(values.Length - 1).ToArray(),
            targetType,
            ConverterParameter,
            ConverterCulture ?? culture);

        if (!object.Equals(newValue, _undelayedValue))
        {
            _undelayedValue = newValue;
            _timer.Stop();
            _timer.Start();
        }
        return _delayedValue;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return
          Converter.ConvertBack(value, targetTypes, ConverterParameter, ConverterCulture ?? culture)
          .Concat(new object[] { ChangeCount }).ToArray();
    }

    private void _timer_Tick(object sender, EventArgs e)
    {
        _timer.Stop();
        _delayedValue = _undelayedValue;
        ChangeCount++;
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs("ChangeCount"));
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

EDIT2: Even though I couldn't get Ray's code to work, I've marked it as the answer because it lead to me some code that does work. See my answer below for the code I used.

4条回答
聊天终结者
2楼-- · 2019-06-17 16:11

Using Ray's code as a starting point, I've written some code that works, but isn't entirely elegant.

XAML:

<local:IsMouseOverToVisibilityConverter x:Key="isMouseOverToVisibiltyConverter" />
<local:DelayingMultiConverter x:Key="delayedIsMouseOverToVisibiltyConverter" Delay="00:00:00.500" Converter="{StaticResource isMouseOverToVisibiltyConverter}" />

...

<UserControl.Visibility>
    <MultiBinding Converter="{StaticResource delayedIsMouseOverToVisibiltyConverter}">
        <Binding ElementName="otherElement" Path="IsMouseOver" />
        <Binding RelativeSource="{RelativeSource Self}" Path="IsMouseOver" />
        <Binding Source="{StaticResource delayedIsMouseOverToVisibiltyConverter}" Path="ChangeCount" />
    </MultiBinding>
</UserControl.Visibility>

DelayingMultiConverter:

internal class DelayingMultiConverter : IMultiValueConverter, INotifyPropertyChanged
{
    private object undelayedValue;
    private object delayedValue;
    private DispatcherTimer timer;

    private int changeCount;
    public int ChangeCount
    {
        get { return this.changeCount; }
        private set
        {
            this.changeCount = value;
            this.NotifyPropertyChanged("ChangeCount");
        }
    }

    public IMultiValueConverter Converter { get; set; }
    public CultureInfo ConverterCulture { get; set; }
    public object ConverterParameter { get; set; }

    public TimeSpan Delay
    {
        get { return this.timer.Interval; }
        set { this.timer.Interval = value; }
    }

    public DelayingMultiConverter()
    {
        this.timer = new DispatcherTimer();
        this.timer.Tick += Timer_Tick;
    }

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        object newValue =
          Converter.Convert(
            values.Take(values.Length - 1).ToArray(),
            targetType,
            ConverterParameter,
            ConverterCulture ?? culture);

        if (!object.Equals(newValue, undelayedValue))
        {
            undelayedValue = newValue;
            timer.Stop();
            timer.Start();
        }

        return delayedValue;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return
          Converter.ConvertBack(value, targetTypes, ConverterParameter, ConverterCulture ?? culture)
          .Concat(new object[] { ChangeCount }).ToArray();
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged(string info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }

    private void Timer_Tick(object sender, EventArgs e)
    {
        timer.Stop();
        delayedValue = undelayedValue;
        ChangeCount++;
    }
}
查看更多
爷、活的狠高调
3楼-- · 2019-06-17 16:27

I had a similar requirement in a project a while back so I created two markup extensions called DelayBindingExtension and DelayMultiBindingExtension.

They work like normal Bindings with the addition that you can specify UpdateSourceDelay and/or UpdateTargetDelay, both of which are TimeSpan properties. In your case you could use it like this

<UserControl.Visibility>
    <db:DelayMultiBinding Converter="{StaticResource yourConverter}"
                          UpdateTargetDelay="00:00:01">
        <Binding ElementName="otherElement" Path="IsMouseOver" />
        <Binding RelativeSource="{RelativeSource Self}" Path="IsMouseOver" />
    </db:DelayMultiBinding>
</UserControl.Visibility>

The source code and sample usage for DelayBinding and DelayMultiBinding can be downloaded here.
If you're interested in the implementation details, you can check out my blog post about it here: DelayBinding and DelayMultiBinding with Source and Target delay

查看更多
我欲成王,谁敢阻挡
4楼-- · 2019-06-17 16:28

The DelayBinding class you linked to only delays a source update, not a target update. To delay a target update, which is what you are asking for, is much simpler. Something like this should do the trick:

public class DelayedMultiBindingExtension : MarkupExtension, IMultiValueConverter, INotifyPropertyChanged
{
  public Collection<Binding> Bindings { get; set; }
  public IMultiValueConverter Converter { get; set; }
  public object ConverterParameter { get; set; }
  public CultureInfo ConverterCulture { get; set; }
  public BindingMode Mode { get; set; }
  public UpdateSourceTrigger UpdateSourceTrigger { get; set; }

  public object CurrentValue { get { return _delayedValue; } set { _delayedValue = _undelayedValue = value; _timer.Stop(); } }

  object _undelayedValue;
  object _delayedValue;

  DispatcherTimer _timer;
  public int ChangeCount { get; set; }  // Public so Binding can bind to it

  public DelayedMultiBindingExtension()
  {
    _timer = new DispatcherTimer();
    _timer.Tick += _timer_Tick;
  }

  public override object ProvideValue(IServiceProvider serviceProvider)
  {
    var multi = new MultiBinding { Converter = this, Mode = Mode, UpdateSourceTrigger = UpdateSourceTrigger };
    foreach(var binding in Bindings)
      multi.Bindings.Add(binding);
    multi.Bindings.Add(new Binding("ChangeCount") { Source = this });
    return multi;
  }

  public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
  {
    object newValue =
      Converter.Convert(
        values.Take(values.Length-1).ToArray(),
        targetType,
        ConverterParameter,
        ConverterCulture ?? culture);

    if(!object.Equals(newValue, _undelayedValue))
    {
      _undelayedValue = newValue;
      _timer.Stop();
      _timer.Start();
    }
    return _delayedValue;
  }

  public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
  {
    return
      Converter.ConvertBack(value, targetTypes, ConverterParameter, ConverterCulture ?? culture)
      .Concat(new object[] { ChangeCount }).ToArray();
  }

  void _timer_Tick(object sender, EventArgs e)
  {
    _timer.Stop();
    _delayedValue = _undelayedValue;
    ChangeCount++;
    if(PropertyChanged!=null)
      PropertyChanged(this, new PropertyChangedEventArgs("ChangeCount"));
  }

  public event PropertyChangedEventHandler PropertyChanged;
}

How it works: A MultiBinding is constructed that has one extra binding, to a ChangeCount property on the markup extension itself. Also the markup extension itself registers as the converter. Whenever a source value changes the, binding evaluates and the converter is called. This in turn calls the "real" converter to compute the value. Instead of updating the value immediately it just stores it in _undelayedValue, and returns the previous value (_delayedValue). Also, if the value has changed it starts (or restarts) a timer. When the timer fires the value is copied into _delayedValue and ChangeCount is incremented, forcing the binding to be re-evaluated. This time the new _delayedValue is returned.

查看更多
老娘就宠你
5楼-- · 2019-06-17 16:35

Note This only answers the "I've been unable to figure out how to make one that works with MultiBinding" part of the question by explaining how to do so. Others may find this information useful, so I will leave it here and add another answer that answers the main question.


It is pretty trivial to change the DelayBinding markup extension you linked to into a DelayMultiBinding class that works the same way but with MultiBinding.

In the markup extension:

  1. Rename to DelayMultiBindingExtension
  2. Add the Bindings property of type Collection<BindingBase>
  3. Change the type of the Converter property
  4. In ProvideValue, construct a DelayMultiBinding instead of a DelayBinding, passing in all Bindings.

In the delay binding class:

  1. Rename to DelayMultiBinding
  2. Take array of bindings instead of single binding
  3. Add value changed handlers to each property
  4. Build the MultiBinding just like you built the Binding

Now instead of writing MultiBinding, write DelayMultiBindingExtension:

<UserControl.Visibility> 
  <my:DelayMultiBindingExtension Delay="0:0:1" Converter="{StaticResource isMouseOverToVisibiltyConverter}">
    <Binding ElementName="otherElement" Path="IsMouseOver" /> 
    <Binding RelativeSource="{RelativeSource Self}" Path="IsMouseOver" /> 
  </my:DelayMultiBindingExtension> 
</UserControl.Visibility> 

Personally I would also clean it up by converting the two classes into a single class which is a MarkupExtension and also handles the timer.

Note that the DelayBinding class and this class both delay updates to the source, not updates to the target. If you want to delay updates to the target (which you do), see my other answer.

查看更多
登录 后发表回答