在.NET 4.0中使用.NET 4.5的绑定延迟属性

13

我如何在.Net 4.0中将.Net 4.5中描述的Delay属性(在这里)应用于数据绑定?

我知道我无法从BindingBase继承,因为ProvideValue是sealed的。

我可以实现MarkupExtension,但这意味着我现在必须重写所有来自BindingExtension的属性,还有其他的方法吗?


你可不可以不继承BindingBase,而是创建一个新的方法,在该方法中简单地调用ProvideValue? - Security Hound
@Ramhound 不行,因为ProvideValue是sealed的,而且不会使用新方法。 - bartosz.lipinski
这是为特定应用程序还是通用实现?作为一次性的解决方案,可以调用一个属性来延迟执行并使用Thread.Sleep()方法吗?在代码后端延迟执行,并让代码后端调用真正的库。 - paparazzo
@BalamBalam 确实这是最简单的方法。我一开始就考虑过这种方式,但是对于像这样的多个属性,它会添加很多与我的viewModel几乎没有关联的逻辑。 - bartosz.lipinski
3个回答

7
最终,我决定采用组合的方式将DelayedBinding作为MarkupExtension进行实现。唯一的问题在于DataTemplates,如果IProvideValueTarget中的TargetProperty为空,则ProvideValue应该返回此内容。
[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
我将创建一个“附加属性”,用于指定延迟的时间。当绑定的值发生变化时,“附加属性”会启动(或重置)计时器,并在达到指定时间时手动更新绑定源。
您可以使用以下内容更新源绑定:
BindingOperations.GetBindingExpressionBase(
    dependencyObject, dependencyProperty).UpdateSource();

编辑

今天我在修复一些旧代码中的bug时注意到它使用了附加行为来实现延迟属性更改通知。我想起了这个问题,于是我按照之前在代码中注释的链接,找到了我之前在Stack Overflow上发布的一个关于延迟绑定的问题。目前,我已经实现了最佳答案,即使用一些附加属性,在经过X毫秒后更新绑定源。


@baalazamon 嗯,这是真的,我想如果你知道要延迟绑定什么,它才能起作用。你也可以尝试像Ramhound建议的那样覆盖BindingBase来实现相同的效果。我通常只会重写它们以避免反复设置默认值,但我不明白为什么你不能改变绑定行为。 - Rachel
无法通过继承它来修改 MarkupExtension 行为,因为 XAML 解析器使用的 ProvideValue 方法是封闭的 :( - bartosz.lipinski
当然会起作用,我知道如何做,但有一个小缺点(我在问题中提到了),那就是将绑定的所有属性添加到我的新扩展中。 - bartosz.lipinski
@baalazamon 看看我的修改。我很惊讶地发现我已经在我的一些旧代码中实现了延迟绑定,而它的源头是我一段时间以前提出的一个SO问题 :) - Rachel
我非常喜欢使用附加属性的想法(它很好,很干净)。我还没有检查过,但我有一种感觉它在DataTemplates中不起作用,但也许是可以的。还有一个与使用AddValueChanged相关的内存泄漏,请查看此链接:http://sharpfellows.com/post/Memory-Leaks-and-Dependency-Properties.aspx - bartosz.lipinski
显示剩余2条评论

2

直接移植是不可能的,但我们可以使用MultiBinding来“模拟”这个过程。

请注意,这是一个非常紧密耦合的解决方案,如果在页面上使用了许多这样的绑定,性能可能会受到影响...

有两个必须具备的条件...

  1. 它接受以毫秒为单位的延迟作为转换器参数的单个ArrayList项。
  2. 每个这样的延迟绑定都必须携带自己的转换器参数实例。

测试XAML如下...

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

在这个例子中,TextBlock 会在 TextBox 中输入内容后延迟2秒进行渲染。 TextBox.Text 是主要的数据来源。 DelayHelper 是一个多转换器,其工作方式如下...
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
}

所以这段代码利用了以下事实:

  1. MultiBinding 可以接受目标 UI 元素(TextBlock)及其依赖属性(TextBlock.TextProperty),该属性本身是多绑定的。
  2. 一旦绑定,多绑定就无法更改其属性,包括 ConveterParameter。但转换器参数本身可以是引用对象,在整个绑定过程中保持其引用,例如 ArrayList
  3. DispatcherTimer 在第一次 Tick 后必须停止。因此使用 temp 变量非常重要。
  4. 更新对每个源文本更新进行了 2 次转换器传递。无法避免这种行为。如果使用了许多延迟绑定,可能会导致速度变慢。
  5. 确保不要在多个延迟绑定之间共享相同的转换器参数

请告诉我这是否有帮助...


很不幸,据我所知,转换器是在UI线程中评估的,而IsAsync对其没有影响。 - bartosz.lipinski
谢谢!我稍后会尝试,但看起来有点复杂,绑定也很难理解。 - bartosz.lipinski
我使用一个IsAsync = true的转换器。必须这样做。FlowDocument派生自Dispatcher,不支持异步操作。我将其序列化为字符串,然后在转换器中进行反序列化。 - paparazzo

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