来源的延迟绑定

时间:2011-12-30 18:35:53

标签: c# .net wpf data-binding

考虑以下ViewModel属性:

private string _slowProperty;
public string SlowProperty
{
    get { return _slowProperty; }
    set
    {
        _slowProperty = value;
        RaisePropertyChanged("SlowProperty");
    }
}

绑定到文本框,如下所示:

<TextBox Text="{Binding SlowProperty}" />

现在,问题在于,每次SlowProperty的值发生变化时,它都经常发生变化,文本框将会尝试获取其值,这很慢。我可以使用异步绑定来缓解这种情况,但这仍然会浪费CPU周期。

相反,我想拥有的是:

<TextBlock Text="{z:DelayedSourceBinding SlowProperty}" />

哪会在一定延迟后试图获得绑定。例如,如果SlowProperty在短时间内连续更改了5次,那么只有最后一个文本会在文本框中显示。

我发现following project执行了类似的操作,所以我的例子我可以这样使用它:

<TextBox Text="{z:DelayBinding Path=SearchText}" />

问题在于,它只会在延迟后更新绑定目标。但是,会对源路径进行评估,并在每次更改源时执行其getter。在SlowProperty的情况下,仍然会浪费CPU周期。

我试图制作自己的延迟绑定类,但是got stuck。还有其他任何可以做类似事情的活页夹吗?

为了完整起见,这里有两个执行类似任务的项目,但是,没有一个解决我遇到的问题:

DeferredBinding - 与DelayBinding类似的解决方案。但是,它使用起来有点复杂。

DelayedBindingTextBox - 使用自定义文本框控件实现延迟绑定。

谢谢!

5 个答案:

答案 0 :(得分:3)

为什么不在视图模型中解决这个问题?如果您的属性变化很快,但速度很慢,您可能会在视图模型中显示第二个“延迟”属性。您可以使用计时器定期更新此“延迟”属性。

或者,更干净的实现可以使用响应式扩展框架提供的Throttle函数。

答案 1 :(得分:2)

我有一个类似的要求,我需要能够延迟源和目标,所以我创建了两个名为DelayBindingDelayMultiBinding的标记扩展。要延迟对源的更新,请指定UpdateSourceDelay

<TextBox Text="{db:DelayBinding SlowProperty,
                                UpdateSourceDelay='00:00:01'}" /> 

DelayBindingDelayMultiBinding的源代码和示例用法可以是downloaded here
如果您对实施细节感兴趣,可以在这里查看我的博客文章:
DelayBinding and DelayMultiBinding with Source and Target delay

答案 2 :(得分:1)

在我看来你真正想要的是延迟调用RaisePropertyChanged()时的点。
所以我试了一下,这是一个解决方案:

XAML:

<StackPanel>
    <TextBox Text="{Binding DelayedText, UpdateSourceTrigger=PropertyChanged}" />
    <TextBlock Text="{Binding DelayedText}" />
</StackPanel>

C#:

public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = this;
        }

        private String m_DelayedText;
        public String DelayedText 
        {
            get
            {
                return m_DelayedText;
            }
            set
            {
                if (m_DelayedText != value)
                {
                    String delayedText;
                    m_DelayedText = delayedText = value;
                    Task.Factory.StartNew(() =>
                        {
                            Thread.Sleep(2000);
                            if (delayedText == m_DelayedText)
                            {
                                RaisePropertyChanged("DelayedText");
                            }
                        });
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void RaisePropertyChanged(String _Prop)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(_Prop));
            }
        }
    }

您可以通过在RaisePropertyChanged("DelayedText")

设置断点来检查它是否有效

我知道它对于“只是”一个属性来说可能看起来很多代码。
但是您可以使用代码片段,或者在运行时使用Resharper等注入样板代码 无论如何,你可能不会经常需要它。

此外,如果您要使用其他方法(例如,通过调整TextBox),您将必须处理绑定到该属性的每个位置。
通过这种方式,在属性的setter中,您可以确保访问此属性的每个人都受限于收到的更新。

HTH,

巴布。

答案 3 :(得分:1)

值得注意的是,从.Net 4.5开始,框架中添加了delay property,可让您设置绑定延迟量(以毫秒为单位)。在Microsoft示例中,强调twoway模式,但绑定延迟适用于任何绑定模式。

例如,我在数据网格中使用它,其中必须从自定义用户控件中的文本框以及显然来自数据网格内更改所选项/值。由于我在此不提及的原因,文本框必须绑定到与视图模型不同的属性,但两个属性必须在一天结束时具有相同的值,并且其中一个属性的任何更改都必须是反映在另一方面。当在datagrid中更改选定的值时,也必须更新文本框,并检查SelectedValue绑定属性的setter中的实际值更改,以防止无限循环。当更改太快时,在文本框内由SelectedValue setter更改的文本更改时,将数据保存回源时出错。两帧延迟解决了问题,没有任何复杂的解决方案,并且没有UI感觉太迟钝:

SelectedValue="{Binding SampleNumberSelect, Mode=OneWayToSource, Delay=33}"

这非常方便,可以省去在视图模型中实现任何此类更改的麻烦,这会不必要地使代码混乱,包括必须在窗口关闭时处置任何计时器。当然,它甚至不必在像我这样的相对复杂的场景中使用,但它可能有助于防止CPU /资源繁重的代码在UI中的每个小变化时不必要地运行。

答案 4 :(得分:0)

绑定中有一个Delay属性,但是它仅在目标到源之间有效。 无论如何,在这个工作示例中,我将向您展示如何在相反的方向上使用Delay属性

在后面的视图代码中,我将来自原始源的更新转发到控件的Tag(请参阅Foo_DataContextChanged)。 标签绑定指定延迟,并绑定到视图上的DelayedViewProperty属性。 控件内容的实际内容绑定到DelayedViewProperty

<Window x:Class="DelayedControl.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<StackPanel>
    <Button Click="Button_Click">Set time</Button>
    <TextBox Text="{Binding ViewModelProperty}" IsReadOnly="True" ToolTip="Not delayed"></TextBox>

    <!-- The DelayedViewProperty is on the code behind (Window in this case), set DataContext as needed -->
    <TextBox x:Name="foo" DataContextChanged="Foo_DataContextChanged" IsReadOnly="True" ToolTip="Delayed"
             Tag="{Binding DelayedViewProperty, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window},
                    Delay=500, Mode=OneWayToSource}"
             Text="{Binding DelayedViewProperty, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}, 
                    Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>

using System;
using System.ComponentModel;
using System.Windows;

namespace DelayedControl
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this; // treat this as a view model for simplification..
        }

        public string DelayedViewProperty
        {
            get => delayedViewProperty;
            set
            {
                delayedViewProperty = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DelayedViewProperty)));
            }
        }
        private string delayedViewProperty;


        private void Foo_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue is INotifyPropertyChanged vm)
            {
                // Forward updates of the view model property to the foo.Tag
                vm.PropertyChanged += (s, args) =>
                {
                    var propertyName = nameof(ViewModelProperty);
                    if (args.PropertyName == propertyName && s?.GetType().GetProperty(propertyName)?.GetValue(s) is var tag)
                    {
                        foo.Tag = tag;  // Dispatcher might be needed if the change is triggered from a different thread..
                    }
                };
            }
        }

        public string ViewModelProperty
        {
            get => viewModelProperty;
            set
            {
                viewModelProperty = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ViewModelProperty)));
            }
        }
        private string viewModelProperty;

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            ViewModelProperty = DateTime.Now.ToString("hh:mm:ss.fff");
        }
    }
}