Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A delayed binding from source

Consider the following ViewModel property:

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

Which is bound to a textbox, like so:

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

Now, the problem here is that every time the value of SlowProperty changes, and it does so quite often, the textbox would go and try to get its value, which is, quite slow. I could alleviate the situation using async binding, however, that would still be wasting CPU cycles.

Instead, what I'd like to have is something like:

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

Which would try to get the binding after a certain delay. So for example if the SlowPropertychanged 5 times in a row, within a short while, then only the last text would be visible in the textbox.

I've found the following project that performs something like that, so it my example I could use it like so:

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

The problem with it, is that it only updates the binding target after a delay. The source path, however, is evaluated and its getter is executed upon every change of the source. Which, in the case of SlowProperty would still waste CPU cycles.

I've tried to make my own delayed binding class, but got stuck. Is there any other binder that can do anything like that?

For completeness sake, here are 2 other projects that perform similar tasks, yet, none address the problem I am experiencing:

DeferredBinding - A similar solution to DelayBinding. However, it is a bit more complex to use.

DelayedBindingTextBox - Implements delayed binding using a custom textbox control.

Thanks!

like image 943
VitalyB Avatar asked Dec 30 '11 18:12

VitalyB


4 Answers

Why not solve this problem in the view model? If your property changes rapidly, but is slow to get, you could have a second 'delayed' property exposed by your view model. You could use a timer to update this 'delayed' property periodically.

Or, a cleaner implementation could use the Throttle function provided by the reactive extensions framework.

like image 121
ColinE Avatar answered Oct 05 '22 00:10

ColinE


I had a similar requirement a while back where I needed to be able to delay both source and target so I created two markup extensions called DelayBinding and DelayMultiBinding. To delay an update to the source you specify UpdateSourceDelay

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

The source code and sample usage for DelayBinding and DelayMultiBinding can be downloaded here.
If you're interested in the implementation details, you can check out my blog post about it here:
DelayBinding and DelayMultiBinding with Source and Target delay

like image 26
Fredrik Hedblad Avatar answered Oct 05 '22 01:10

Fredrik Hedblad


Of note, as of .Net 4.5, a delay property has been added to the framework, which lets you set the amount of binding delay by milliseconds. In the Microsoft example, the twoway mode is emphasized but the binding delay works in any binding mode.

For example, I used this in a datagrid where the selected item / value had to be changed both from a textbox within a custom usercontrol and obviously from within the datagrid. Because of reasons I won't mention here, the textbox had to bind to a property different from the view model, but both properties had to have the same value at the end of the day, and any change in one of them had to be reflected on the other. When the selected value changed in datagrid, the text box had to be updated too, and I checked for actual value changes in the setter of the SelectedValue binded property, to prevent infinite looping. When the change was too fast, there was an error saving the data back to source when the text changed inside the textbox which was changed by the SelectedValue setter. A two frame delay fixed the problem without any complex solutions and without the UI feeling too laggy:

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

This is very convenient and saves you the hassle of implementing any such changes in the view model, which would unnecessarily clutter the code, including having to dispose any timers on window closing. Of course it doesn't even have to be used in a relatively complex scenario like mine, but it could be helpful to prevent CPU / resource heavy code from running unnecessarily with each small change in the UI.

like image 41
Emrah 'hesido' Baskaya Avatar answered Oct 05 '22 02:10

Emrah 'hesido' Baskaya


Seems to me what you really want is delay the point when RaisePropertyChanged() is called.
So I tried it out, and here is a solution:

The XAML:

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

The 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));
            }
        }
    }

You can check that it works by setting a breakpoint at RaisePropertyChanged("DelayedText")

I understand that it can look like quite a lot of code for "just" a property.
But you can use code snippets, or inject the boilerplate code at runtime using Resharper and the like.
And anyway, you probably won't need it so often.

Also, if you were to use another approach (for example, by tweaking a TextBox), you will have to handle every place where you bind to the property.
By doing it this way, in the property's setter, you ensure everyone who accesses this property is restricted on the updates recieved.

HTH,

Bab.

like image 39
Louis Kottmann Avatar answered Oct 05 '22 01:10

Louis Kottmann