WPF - отложенное мультисвязывание - PullRequest
7 голосов
/ 10 ноября 2010

У меня есть мультисвязь, которая выглядит примерно так:

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

РЕДАКТИРОВАТЬ: Рэй, чтобы заставить ваш класс компилироваться и запускаться, мне пришлось внести некоторые исправления.Однако, что-то все еще не так, поскольку обновления не распространяются.Похоже, что свойство цели обновляется только один раз.

[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: Несмотря на то, что я не смог заставить работать код Рэя, я пометил его как ответ, потому что это привело меня к некоторому коду, которыйРабота.Смотрите мой ответ ниже для кода, который я использовал.

Ответы [ 4 ]

6 голосов
/ 13 ноября 2010

Класс 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 голосов
/ 11 ноября 2010

Примечание Это только отвечает на вопрос «Я не смог понять, как сделать тот, который работает с MultiBinding», объясняя, как это сделать.Другие могут найти эту информацию полезной, поэтому я оставлю ее здесь и добавлю еще один ответ, который отвечает на главный вопрос.


Довольно просто изменить расширение разметки DelayBinding, с которым вы связаны, в класс DelayMultiBinding, которыйработает так же, но с MultiBinding.

В расширении разметки:

  1. Переименовать в DelayMultiBindingExtension
  2. Добавить свойство Bindings типа Collection<BindingBase>
  3. Измените тип свойства Converter
  4. В ProvideValue вместо DelayBinding создайте DelayMultiBinding, передавая все привязки.

В классе привязки с задержкой:

  1. Переименовать в DelayMultiBinding
  2. Взять массив привязок вместо одиночной привязки
  3. Добавить обработчики измененных значений к каждому свойству
  4. Создайте MultiBinding точно так же, как вы создали Binding

Теперь вместо записи MultiBinding напишите 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> 

Лично я бы также очистил его, преобразовавдва клоценивает в один класс, который является MarkupExtension, а также обрабатывает таймер.

Обратите внимание, что и класс DelayBinding, и этот класс оба задерживают обновления для source , а не обновляют для цели .Если вы хотите отложить обновление до target (что вы делаете), см. Мой другой ответ.

2 голосов
/ 03 мая 2012

Некоторое время назад в проекте было похожее требование, поэтому я создал два расширения разметки: DelayBindingExtension и DelayMultiBindingExtension.

Они работают как обычно 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>

Исходный код и пример использования для DelayBinding и DelayMultiBinding можно загрузить здесь .
Если вас интересуют подробности реализации, вы можете ознакомиться с моим сообщением в блоге об этом здесь: DelayBinding и DelayMultiBinding с задержкой источника и цели

2 голосов
/ 15 ноября 2010

Используя код Рэя в качестве отправной точки, я написал код, который работает, но не совсем элегантно.

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