4

I have a wpf application with all viewModel inheriting from a class NotifyPropertyChangeClass implementing INotifyPropertyChanged (see below).

I want to throttle the notifications sent to the view for avoiding lagging due to too much notification sent. What I achieve so far is to delay for each property using the method Sample of reactive extention.

The problem is that the Gui is refreshed late by the throttlingPeriod. It would feel more responsive to have a first event raised at the starting of the period like this :

More graphic explanation

The code of NotifyPropertyChangeClass :

using System;
using System.ComponentModel;
using System.Reactive.Linq;

namespace MyNameSpace
{
    public class NotifyPropertyChangeClass : INotifyPropertyChanged
    {
        public NotifyPropertyChangeClass(int throttlingPeriod)
        {
            var obs = Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
               h => this.privatePropertyChanged += h, h => this.privatePropertyChanged -= h);
            var groupedByName = obs.Select(o => o.EventArgs.PropertyName).GroupBy(x => x).SelectMany(o => o.Sample(TimeSpan.FromMilliseconds(throttlingPeriod)));
            groupedByName.Subscribe(o =>
            {
                PropertyChangedEventHandler handler = PropertyChanged;
                if (handler != null)
                    handler(this, new PropertyChangedEventArgs(o));
            });
        }
        public event PropertyChangedEventHandler PropertyChanged;
        private event PropertyChangedEventHandler privatePropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = privatePropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

How can I achieve what I want?

Thomas
  • 5,603
  • 5
  • 32
  • 48
  • I tend to think that there is no solution that will satisfy your request exactly. Obviously there is a lag introduced somewhere between event notifications being received in your View. Do you know exactly where and why that lag occurs? But even if you do know the reason for this lag, it's not certain you can solve it. You would probably have to compensate for it by reducing your throttle period or something. – o_weisman Dec 03 '14 at 14:07

2 Answers2

1

I haven't tried, but you may try this:

using System;
using System.ComponentModel;
using System.Reactive.Linq;

namespace MyNameSpace
{
    public class NotifyPropertyChangeClass : INotifyPropertyChanged
    {
        private bool _isThrottling = false;

        public NotifyPropertyChangeClass(int throttlingPeriod)
        {
            var obs = Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
               h => this.privatePropertyChanged += h, h => this.privatePropertyChanged -= h);
            var groupedByName = obs.Select(o => o.EventArgs.PropertyName).GroupBy(x => x).SelectMany(o => o.Sample(TimeSpan.FromMilliseconds(throttlingPeriod)));
            groupedByName.Subscribe(o =>
            {
                PropertyChangedEventHandler handler = PropertyChanged;
                if (handler != null)
                    handler(this, new PropertyChangedEventArgs(o));
                _isThrottling = false;
            });
        }
        public event PropertyChangedEventHandler PropertyChanged;
        private event PropertyChangedEventHandler privatePropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            // Will fire the first time, the event is raised
            if (!_isThrottling)
            {
                PropertyChangedEventHandler handler = PropertyChanged;
                if (handler != null)
                    handler(this, new PropertyChangedEventArgs(o));
            }

            // Setting to true here will suppress raising the public event 
            // for every subsequent call, until the event is raised
            // by the observable pattern and the flag is set to false again.
            _isThrottling = true;

            // Will always be raised
            PropertyChangedEventHandler handler = privatePropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
Marc
  • 12,706
  • 7
  • 61
  • 97
  • Yes it's work! Please note that one bool `_isThrottling` is not enough, it should be a dictionary with for key the PropertyName. Thank you! – Thomas Dec 03 '14 at 15:35
  • @Thomas - the Rx version I've done should avoid you have to manage that state. – James World Dec 03 '14 at 15:36
  • @JamesWorld Where's your Rx version? – stromms Aug 13 '17 at 22:56
  • @stromms - I've deleted it, because it had some problems and I didn't have time to fix it; thought it best to remove it to avoid leading anyone astray. – James World Aug 13 '17 at 23:49
  • @JamesWorld Thanks. This thing is really hindering my UI's performance. I'm getting thousands of property change notification on several properties and the user doesn't need to see these. I wonder why WPF didn't think about throttling these messages since it cannot possibly render at that speed. It seems nonsense to do this in the ViewModel. – stromms Aug 15 '17 at 11:27
  • @stromms Yes, conflation should definitely be done as close to the source as possible. This post and the linked discussion might help: http://www.zerobugbuild.com/?p=192 – James World Aug 28 '17 at 14:55
0

I also wanted to throttle PropertyChanged notifier when a lot of changes were being notified at the same time (it was bogging down the listview)

I ended up using a time check so if a new event was being raised before 500ms had passed since the last event, it will block this and any further events for 500ms before triggering an update on all properties. Code below

private DateTime _lastPropertyChange = DateTime.Now - TimeSpan.FromMinutes(1);
private readonly TimeSpan _propertyChangeBuffer = TimeSpan.FromMilliseconds(500);
private bool _propertyChangeBlock = false;

public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        if (_propertyChangeBlock)
            return;

        if (_lastPropertyChange < DateTime.Now - _propertyChangeBuffer)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            _lastPropertyChange = DateTime.Now;
        }
        else
        {
            _propertyChangeBlock = true;
            ThreadPool.QueueUserWorkItem(p =>
            {
                Thread.Sleep(_propertyChangeBuffer);
                _propertyChangeBlock = false;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null));
            });
        }
    }
Johan Sonesson
  • 401
  • 4
  • 4