来自源的延迟绑定

本文关键字:延迟 绑定 | 更新日期: 2023-09-27 18:19:10

考虑以下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次,那么只有最后一个文本将在文本框中可见。

我发现下面的项目执行类似的东西,所以在我的例子中,我可以这样使用它:

<TextBox Text="{z:DelayBinding Path=SearchText}" />
它的问题是,它只在延迟后更新绑定目标。但是,对源路径进行求值,并在每次更改源时执行其getter。在SlowProperty的情况下,这仍然会浪费CPU周期。

我试着做我自己的延迟绑定类,但是卡住了。还有其他的活页夹可以做这样的事吗?

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

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

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

谢谢!

来自源的延迟绑定

为什么不在视图模型中解决这个问题呢?如果你的属性变化很快,但获取缓慢,你可以让视图模型公开第二个"延迟"属性。你可以使用计时器来定期更新这个'delayed'属性。

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

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

延迟对源的更新
<TextBox Text="{db:DelayBinding SlowProperty,
                                UpdateSourceDelay='00:00:01'}" /> 

DelayBindingDelayMultiBinding的源代码和示例用法可以在这里下载。
如果你对实现细节感兴趣,你可以在这里查看我的博客文章:

值得注意的是,从。net 4.5开始,框架中增加了一个delay属性,它可以让你以毫秒为单位设置绑定延迟的数量。在Microsoft的示例中,twoway模式被强调,但是绑定延迟在任何绑定模式下都可以工作。

例如,我在一个数据网格中使用了这个方法,其中选择的项/值必须从自定义用户控件中的文本框和数据网格中的文本框中进行更改。由于我不会在这里提及的原因,文本框必须绑定到与视图模型不同的属性,但两个属性在一天结束时必须具有相同的值,并且其中一个属性的任何更改必须反映在另一个属性上。当数据网格中的选定值发生变化时,文本框也必须更新,并且我在SelectedValue绑定属性的setter中检查实际值的变化,以防止无限循环。当更改太快时,当SelectedValue设置器更改文本框内的文本时,将数据保存回源时会出现错误。两帧延迟解决了这个问题,没有任何复杂的解决方案,也不会让UI感觉太滞后:

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

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

在我看来你真正想要的是延迟点当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,

巴布。

有一个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");
        }
    }
}