WPF - 延迟多绑定

7

我有一个类似于这样的多绑定:

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

我希望能在绑定的IsMouseOver属性值变为false和Visibility被设置为Collapsed之间增加一些延迟时间。

我找到了这个DelayBinding的实现方式:http://www.paulstovell.com/wpf-delaybinding。但是,这并不适用于MultiBinding,而且我一直无法弄清楚如何使其适用于MultiBinding。

我可以在代码后台中使用事件来更改Visibility,这样也能够实现目标。但如果有一种方法能够通过绑定系统来实现这一点,那就更好了。

是否有一些方法能够在MultiBinding上添加延迟?

编辑:Ray,为了让你的类编译和运行,我不得不进行一些修复。然而,还是有些问题,因为更新没有被传播。它似乎只更新了一次目标属性。

[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;
}

编辑2:虽然我无法让Ray的代码正常工作,但我将其标记为答案,因为它引导我找到了一些可行的代码。请查看下面的答案,以获取我使用的代码。


这个问题帮助我解决了我的问题,非常感谢! - worldpart
看起来现在已经将延迟属性添加到多绑定中了。 - horotab
4个回答

7
您提供的链接中的DelayBinding类仅延迟源更新,而不是目标更新。要延迟目标更新,也就是您所要求的,要简单得多。像这样做应该可以解决问题:
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;
}

它是如何工作的:构造了一个MultiBinding,其中有一项额外的绑定到标记扩展本身的ChangeCount属性上。同时,标记扩展本身注册为转换器。每当源值发生变化时,绑定会进行评估并调用转换器。这反过来又调用“真正”的转换器来计算值。它不会立即更新值,而是将其存储在_undelayedValue中,并返回前一个值(_delayedValue)。此外,如果值已更改,则开始(或重新开始)计时器。当计时器触发时,该值被复制到_delayedValue中,并增加ChangeCount,强制重新评估绑定。这次返回新的_delayedValue。


3

我曾在一个项目中有类似的需求,所以我创建了两个标记扩展,分别叫做DelayBindingExtensionDelayMultiBindingExtension

它们像普通的Bindings一样工作,但增加了你可以指定的UpdateSourceDelay和/或UpdateTargetDelay,两者都是TimeSpan属性。在您的情况下,您可以像这样使用它:

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

您可以在此处下载DelayBindingDelayMultiBinding的源代码和示例用法:这里
如果您对实现细节感兴趣,可以查看我关于它的博客文章:使用源延迟和目标延迟的 DelayBinding 和 DelayMultiBinding

3

注意:本文只回答了如何制作与MultiBinding兼容的延迟绑定的部分问题。其他人可能会发现这些信息很有用,因此我将把它留在这里,并添加另一个回答来回答主要问题。


将你链接到的DelayBinding标记扩展更改为DelayMultiBinding类,使其以相同的方式工作,但适用于MultiBinding是非常简单的。

在标记扩展中:

  1. 将其重命名为DelayMultiBindingExtension
  2. 添加类型为Collection<BindingBase>的Bindings属性
  3. 更改Converter属性的类型
  4. 在ProvideValue中,构建一个DelayMultiBinding而不是DelayBinding,并传入所有绑定。

在延迟绑定类中:

  1. 将其重命名为DelayMultiBinding
  2. 使用绑定数组而不是单个绑定
  3. 为每个属性添加值更改处理程序
  4. 像构建绑定一样构建MultiBinding

现在,您可以写DelayMultiBindingExtension而不是MultiBinding:

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

个人建议将这两个类转换为单个类,即MarkupExtension,并且处理定时器,使其更加简洁。

请注意,DelayBinding类和此类都会延迟更新到,而不是目标。如果您想要延迟更新目标(您确实想要),请参考我的其他答案。


这绝对有助于指引我正确的方向,但我不知道如何延迟调用UpdateTarget()。MultiBinding有UpdateSourceTrigger来更新源,但据我所知没有类似的东西来更新目标。 - Ashley
此外,我不完全确定如何为源上绑定的属性添加值更改处理程序。 - Ashley
我认为我已经基本实现了,但是当我尝试在计时器中调用UpdateTarget()时,会出现异常。"无法在绑定分离时执行此操作。" - Ashley
好的,那个异常是由于MultiBinding OneWay引起的。当MultiBinding为TwoWay时,它就消失了。但是,然后它开始立即使用TwoWay进行更新。在这种情况下,TwoWay没有任何意义,因为IsMouseOver是只读的。 - Ashley
我查看了你提供的DelayBinding类,并将其作为你想要做的指南。我应该更仔细地阅读问题。你提供的DelayBinding类不会延迟目标更新,而是延迟源更新。我上面给出的答案对于源更新来说是完美的,但并没有处理目标更新。我添加了另一个答案,解释了如何实现你所要求的功能。 - Ray Burns

2

作为起点,我使用了Ray的代码并编写了一些可用的代码,但不是完全优雅。

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>

延迟多转换器:

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++;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接