Using DispatchTimer to help in UI Behavior

Wednesday, October 21, 2009 12:49:49 AM (GMT Daylight Time, UTC+01:00)

I have ran into this problem a number of times developing UI.  You have business logic that needs to execute when a user is done entering some piece of data or criteria.  The business logic may be complicated and takes longer then what is considered an acceptable threshold (somewhere around 3 seconds). 

Your first thought might be to use the TextChanged, SelectionChanged or KeyUp events to detect change in the targeted control and run the business logic… but after you have implemented the approach you find out that the logic is just too expensive and the UI appears locked or sluggish.  The LostFocus event doesn’t work well either… it doesn’t get fired in some situations (clicking on a toolbar or selecting a menu).  

The approach that I have used both in WinForms and WPF development is to monitor the TextChanged event and when event has gone dormant for a specified duration, run the business logic. The key is not to wait an extended period of time to run the logic, but rather execute the logic at a tight enough interval that the application appears responsive after the user is has completed their data entry.

This is what the code does.

In the constructor we setup a DispatchTimer to fire at a default interval and subscribe to the TextChange event of our target control.

public ExecuteOnDormant(TextBox ctl, Action action)
{
    _control = ctl;
    _action = action;
    _dormantTimer = new DispatcherTimer();
 
    _defaultTimeout = new TimeSpan(0, 0, 0, 0, _timeOut);
 
    if (_timeOut == 0)
        _dormantTimer.IsEnabled = false;
    else
    {
        _dormantTimer.Interval = _defaultTimeout;
        _dormantTimer.IsEnabled = false;
        _dormantTimer.Tick += new EventHandler(RunBusinessLogic);
 
        //assumging that it is only a textbox we are watching... if we want to make this more generic... then add a  to KeyUp or SelectionChanged
        _control.TextChanged += new TextChangedEventHandler(ctl_TextChanged);
    }
}

The handler for the TextChange event then resets the DispatchTimer… this allows the user to complete typing the value before complex business logic is run.

/// <summary>
/// Handles the TextChanged event of the control that we are monitoring to run business logic on.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.Windows.Controls.TextChangedEventArgs"/> instance containing the event data.</param>
private void ctl_TextChanged(object sender, TextChangedEventArgs e)
{
    ProcessActivity();
}
 
/// <summary>
/// Processes the activity(keyboard and mouse) in the shell to ensure that it stays active and does not fire the 
/// IdleTimeOut event.
/// </summary>
private void ProcessActivity()
{
    //reset the timer, the control has changed.
    _dormantTimer.IsEnabled = false;
    _dormantTimer.Interval = _defaultTimeout;
 
    //now enable the timer, as we want it to fire when it times out.
    _dormantTimer.IsEnabled = true;
}

When the user has stopped typing long enough for the interval to expire the business logic is executed.  To make this class usable across different controls we use an Action delegate to define the business logic.

private void RunBusinessLogic(object sender, EventArgs e)
       {
           //disable the timer, so it doesn't continue to run the business logic
           _dormantTimer.IsEnabled = false;
           Dispatcher.CurrentDispatcher.InvokeOnUIThread(_action);
       }

The business logic definition looks like this:

public partial class Window1 : Window
    {
        private ExecuteOnDormant _dormantWatcher;
        private Random _randomVal = new Random();
        public Window1()
        {
            InitializeComponent();
 
            _dormantWatcher = new ExecuteOnDormant(txtCriteria, ExecuteComplexBusinessLogic(txtCriteria));
        }
 
        private Action ExecuteComplexBusinessLogic(TextBox control)
        {
           return  () =>
                       {
                           Mouse.SetCursor(Cursors.Wait);
                           
                           //run complex business logic...
                           int val = _randomVal.Next(1,3);
 
                           //make it seem like a long time
                           Thread.Sleep(1000);
                           
                           //give the rich feedback on whether the item passed validation
                           control.Background = (val == 1) ? Brushes.LightGreen : Brushes.Salmon;
 
                           Mouse.SetCursor(Cursors.None);
                       };
        }
    }

You will notice that it uses a extension method off of the CurrentDispatcher called InvokeOnUIThread.  This method ensures that the business logic is executed in the correct thread context.  Below is the implementation for the extension method.

/// <summary>
/// Invokes on the UI thread if necessary.
/// this.Dispatcher.InvokeIfNecessary(() =>
/// {
        //do something that might need to be invoked
/// });
/// </summary>
/// <param name="dispatcher">The dispatcher.</param>
/// <param name="action">The action.</param>
public static void InvokeOnUIThread(this Dispatcher dispatcher, Action action)
{
    if (Thread.CurrentThread == dispatcher.Thread)
    {
        action();
    }
    else
    {
        dispatcher.Invoke(DispatcherPriority.Normal, (Action)delegate { action(); });
    }
}

You can download the demo project.

Comments are closed.