A Better UX: Time buffering program responses

 

Introduction:

 

This is the second post in my better UX blog series. In this post, I would like to talk about handling time consuming operations which must be done on frequent input.

Many programs today contain auto complete textboxes, in addition, almost all programs contain search and filter boxes. Some programs are able to provide the instant reaction expected by the user and others which don’t, should…

When the amount of data handled by the application becomes very large or when data fetching is slow due to communication channel problems or busy servers, reacting on each added character slows down the application and creates a sluggish and flaky feel to the user interface.

There are several initial solutions we can think of for this problem:

 

 

 

1. Starting to search only when you get X characters

  • This postpones the problem, and may reduce data size due to searching in a filtered data set.
  • However, this solution doesn’t help when the search mechanism is slow.
  • It also doesn’t have a large impact on the number of calls made to the server.

2. Caching parts of the data in memory

  • This helps with search times.
  • However, using this solution causes the application to consume a large amount of memory.
  • Also, the amount of data can be too large to store in memory and in memory search by itself can be too slow.

3. Chopping the data to pages, and searching within them

  • This  results in a generally bad User Experience

 

 

The simplest solution is just to keep the number of actions made by the program in response to the input to a comfortable minimum, which will benefit the user, as well as save the programmer the hassle of implementing the previously mentioned methods. This simple solution can be implemented by delaying the programs response to the user input, and requiring a small window of user inactivity before executing the actual search. I like to call this method Time Buffering. Although you may have used this method before, I would like to share the following class I have written recently, which makes time buffering very easy (and you are welcome to use it):

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace TimeBuffering
{
 
    public class TimeBufferByTimer
    {
        private Action<object> _action = null;
        private DateTime _when = DateTime.Now;
        private Exception _problem = null;
        Thread _runnerThread = null;
        private TimeSpan _initialTimeBuffer = TimeSpan.FromSeconds(1);
        SynchronizationContext _origSyncContext = null;
        public object ContentObject { get; set; }
 
        public TimeBufferByTimer(Action<object> act)
            : this(act, TimeSpan.FromSeconds(1))
        {
        }
 
        private System.Timers.Timer _timer;
        public TimeBufferByTimer(Action<object> act, TimeSpan timeBuffer)
        {
            _initialTimeBuffer = timeBuffer;
            _action = act;
            _origSyncContext = SynchronizationContext.Current;
            if (_origSyncContext == null)
                throw new Exception("Initialize time buffer from within the main thread, not in a static context");
 
 
        }
 
        void _timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            try
            {
                _origSyncContext.Send((SendOrPostCallback)delegate
                {
                    _action.Invoke(ContentObject);
                }, null);
            }
            catch (Exception ex)
            {
                _problem = ex;
            }
        }
 
        /// <summary>
        /// refills time buffer (used when user operation should postpone action)
        /// </summary>
        public void Restart()
        {
            Start(_initialTimeBuffer);
        }
 
        /// <summary>
        /// changes the waiting end time to be now + timeFromNow, ensures the thread is running
        /// </summary>
        /// <param name="timeFromNow"></param>
        public void Start(TimeSpan timeFromNow)
        {
            if (_timer != null)
            {
                _timer.Stop();
                _timer.Elapsed -= new System.Timers.ElapsedEventHandler(_timer_Elapsed);
            }
            _timer = new System.Timers.Timer(timeFromNow.TotalMilliseconds);
            _timer.Elapsed += new System.Timers.ElapsedEventHandler(_timer_Elapsed);
            _timer.AutoReset = false;
 
            _timer.Start();
        }
 
        /// <summary>
        /// stopps the waiting thread, no action will be performed
        /// </summary>
        public void Stop()
        {
            _timer.Stop();
        }
 
        /// <summary>
        /// starts waiting, will wait for the initial time and run action, if no changes are done until time expires.
        /// </summary>
        public void Start()
        {
            Start(_initialTimeBuffer);
        }
    }
}

 

For those of you that dislike timers (like me) since they have a  tendency to create random threads and the fact that there are 4 of them in the .Net framework (way too many in my opinion), here is a single spin thread implementation of the same idea:

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace TimeBuffering
{
    public enum TimeBuffStatus
    {
        Off,
        Waiting,
        Done,
        Error,
        Stopped
    }
 
    public class TimeBufferByThread
    {
        private Action<object> _action = null;
        private DateTime _when = DateTime.Now;
        private TimeBuffStatus _state = TimeBuffStatus.Off;
        private Exception _problem = null;
        Thread _runnerThread = null;
        private TimeSpan _initialTimeBuffer = TimeSpan.FromSeconds(1);
        SynchronizationContext _origSyncContext = null;
        public object ContentObject { get; set; }
 
        public TimeBufferByThread(Action<object> act)
            : this(act, TimeSpan.FromSeconds(1))
        {
        }
 
        public TimeBufferByThread(Action<object> act, TimeSpan timeBuffer)
        {
            _initialTimeBuffer = timeBuffer;
            _action = act;
            _origSyncContext = SynchronizationContext.Current;
            if (_origSyncContext == null)
                throw new Exception("Initialize time buffer from within the main thread, not in a static context");
 
            // a thread that will wait for the right time.
            CreateRunnerThread();
        }
 
        private void CreateRunnerThread()
        {
            _runnerThread = new Thread((ThreadStart)delegate
                {
                    _state = TimeBuffStatus.Waiting;
                    while (_state != TimeBuffStatus.Stopped)
                    {
                        //if the time for doing the action has come (or passed) - do the action
                        if (_when - DateTime.Now < TimeSpan.Zero)
                        {
                            try
                            {
                                _origSyncContext.Send((SendOrPostCallback)delegate
                                {
                                    _action.Invoke(ContentObject);
                                }, null);
 
                                _state = TimeBuffStatus.Done;
                                break;
                            }
                            catch (Exception ex)
                            {
                                _problem = ex;
                                _state = TimeBuffStatus.Error;
                                break;
                            }
                        }
                        Thread.Sleep(100);
                    }
                });
        }
 
        /// <summary>
        /// refills time buffer (used when user operation should postpone action)
        /// </summary>
        public void Restart()
        {
            Start(_initialTimeBuffer);
        }
 
        /// <summary>
        /// changes the waiting end time to be now + timeFromNow, ensures the thread is running
        /// </summary>
        /// <param name="timeFromNow"></param>
        public void Start(TimeSpan timeFromNow)
        {
            _when = DateTime.Now + timeFromNow;
            Start();
        }
 
        /// <summary>
        /// stopps the waiting thread, no action will be performed
        /// </summary>
        public void Stop()
        {
            _state = TimeBuffStatus.Stopped;
        }
 
        /// <summary>
        /// starts waiting, will wait for the initial time and run action, if no changes are done until time expires.
        /// </summary>
        public void Start()
        {
            if (_runnerThread.ThreadState != ThreadState.Running && _state != TimeBuffStatus.Waiting)
            {
                CreateRunnerThread();
                _runnerThread.Start();
            }
        }
    }
}

 

Implementation details:

 

My choice of System.Threading.Timer:

You can also implement a TimeBuffer class using a GUI timer (windows.forms.timer/ DispatcherTimer). However, that would require you to use different implementations for different UI types, whereas both the above implementations can be used in almost any case.

 

For those of you that may ask about the timers I had to choose from:

Usage:

 

Create a TimeBuffer object and give it a callback function to run when the idle time has elapsed, as seen in the example below:

//some initialization function or the constructor
public void Initialize()
{
     //the default triggering idle time is 0.5 seconds
     _timeBuffFind = new TimeBuffer(FindCallback);

     //you can also supply the length of idle time required to trigger the callback.
     _timeBuffFind = new TimeBuffer(FindCallback, TimeSpan.FromMilliseconds(700));
}

//the callback for the TimeBuffer
private void FindCallback(object searchQuery)
{
     //do stuff...
}

 

The callback function given to the TimeBuffer (found in the example) is the code that will be called when the time elapses. This function must also be a void which receives a single object as input. One input object should be enough, as you can create a class to wrap any number of input values that you require.

This can also be adapted to have an output as well, but I couldn’t see the value of doing this, seeing that:

  • The code calling the function is not your own
  • The callback function can change any internal state variables
  • It can also call any additional functions/events required

 

On the event handler, you should add a call to the TimeBuffer.Restart() function which will start (if new or stopped) or reset the countdown while the user continues to change the input text, as seen in the example below:

 

private void textBox1_TextChanged(object sender, EventArgs e)
{
    _timeBuffFind.ContentObject = new SearchInfo()
        {
            Query = textBox1.Text,
            Direction = this.Direction
        };
    _timeBuffFind.Restart();
}

 

Conclusion:

 

Buffering frequent input events can help in many UI use cases. In addition, it is also possible to think of some non UI cases where this method can be helpful, such as getting multiple events from a network connection, or reacting to change notification from DB systems (which exist in several leading database brands).

Any rapidly fired change event may cause congestion on the listening side, and can benefit from batching updates which may require heavy processing, data locking, or suspending normal operations in order to preserve data integrity and correct display.

 

I imagine other cases may require slightly different implementations, but time buffering is an idea worth considering.

Leave a Comment

We encourage you to share your comments on this post. Comments are moderated and will be reviewed
and posted as promptly as possible during regular business hours

To ensure your comment is published, be sure to follow the Community Guidelines.

Be sure to enter a unique name. You can't reuse a name that's already in use.
Be sure to enter a unique email address. You can't reuse an email address that's already in use.
Type the characters you see in the picture above.Type the words you hear.
Search
Showing results for 
Search instead for 
Do you mean 
About the Author
I've been all over the coding world since earning my degrees have worked several years in c++ and then several in java, finally setteling i...


Follow Us
The opinions expressed above are the personal opinions of the authors, not of HP. By using this site, you accept the Terms of Use and Rules of Participation