Click here to Skip to main content
Click here to Skip to main content
Technical Blog

Throttling Silverlight Mouse Events to Keep the UI Responsive

, 15 Jun 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
Throttling Silverlight Mouse Events to Keep the UI Responsive

If your Silverlight application performs intensive updates to the UI during mouse events, the UI can freeze because it is invalidated before it has a chance to render. This post describes a technique for ‘throttling’ mouse events to ensure that each time an event occurs, the UI has the opportunity to render.

Introduction – The Problem

The visual tree of a Silverlight / WPF uses the retained mode graphics  rendering style, where the tree forms a model of the graphics which are rendered to the screen. Any changes that are made to the visual tree by adding / removing objects or changing the properties of an object within the tree, are automatically rendered to your computer screen at some point in the future.

This graphics style makes the development of complex graphics far easier than with the immediate mode counterpart. You add objects, make changes to them, and these changes are reflected on the screen by some background magic. This works just fine most of the time, however, if you continually make changes to the visual tree, without yielding, the ‘process’ which updates the UI does not have the opportunity to run, and your UI is starved.

I have found that this problem occurs most often when handling mouse events, particularly MouseMove and MouseWheel. Both of these events have the opportunity to fire very rapidly. If, when handling one of these events you make some change to the visual tree, there is the possibility that the event may be raised again before these changes are reflected on the screen. For a demonstration of this, have a play with the following simple example:

thing.PNG

[CodeProject does not support Silverlight content, see the above in action on my blog]

The application above creates funky patterns that track the current mouse position:

private void MainPage_MouseMove(object sender, MouseEventArgs e)
{
  AddNewLine(e.GetPosition(this));
}
 
/// <span class="code-SummaryComment"><summary>
</span>/// Adds a new line at the given position
/// <span class="code-SummaryComment"></summary>
</span>private void AddNewLine(Point position)
{
  if (_lastPosition != null)
  {
    // find the angle in which the mouse is moving
    double angleOfMovement = RaidansToRadians(Math.Atan2(
        _lastPosition.Value.Y - position.Y, _lastPosition.Value.X - position.X)) + 90;
 
    // move our current angle slightly in this direction
    double diff = DifferenceBetweenAngles(_angle, angleOfMovement);
    _angle = _angle + DifferenceBetweenAngles(_angle, angleOfMovement) / 10;
 
    // create a line 
    double dx = Math.Sin(DegreesToRadians(_angle)) * diff;
    double dy = Math.Cos(DegreesToRadians(_angle)) * diff;
    Line line = new Line()
    {
      X1 = position.X + dx,
      Y1 = position.Y + dy,
      X2 = position.X - dx,
      Y2 = position.Y - dy,
      Opacity = 0.7,
      Stroke = new SolidColorBrush(Colors.Black),
      Tag = new Data() { Angle = _angle, Length = diff, Position = position }
    };
 
    // add to canvas
    LineContainer.Children.Add(line);
 
    // rotates all the lines that are currently displayed
    RotateLines();
  }
  _lastPosition = position;
}

Everything works just fine when there are < 1000 lines on screen, however as more lines are added, the act of adding a new line to the visual tree becomes more expensive. As a result, the work needed to handle each event becomes so great that the UI is starved. If you move the mouse rapidly across the display, you will find that nothing happens until you stop moving your mouse, then all of a sudden the new lines appear.

Interestingly, the background animation (which is performed on the Tick of a DispatcherTimer) does not starve the UI.

Throttling – The Solution!

So, what to do? The answer is to throttle the event.

No, I don’t mean that kind of throttle, I mean throttle as in “to suppress or regulate the flow of”.

What we need to do is ensure that the changes we make to the visual tree within our MouseMove event are reflected in the UI before we make a subsequent change. Fortunately the Silverlight / WPF frameworks expose a static CompositionTarget.Rendering event which can be used for this purpose. This event is fired just before a frame is rendered. Therefore, to throttle the effect of the mouse event, we set a flag to indicate that we are waiting for a change to be rendered, then reset this flag just before rendering occurs.

I have wrapped this concept up in a simple class that provides a ThrottledMouseMove event:

/// <span class="code-SummaryComment"><summary>
</span>/// Creates a 'throttled' MouseMove event which ensures that the UI
/// rendering is not starved.
/// <span class="code-SummaryComment"></summary>
</span>public class ThrottledMouseMoveEvent
{
  private bool _awaitingRender = false;
 
  private UIElement _element;
 
  public ThrottledMouseMoveEvent(UIElement element)
  {
    _element = element;
    element.MouseMove += new MouseEventHandler(Element_MouseMove);
  }
 
  public event MouseEventHandler ThrottledMouseMove;
 
  private void Element_MouseMove(object sender, MouseEventArgs e)
  {
    if (!_awaitingRender)
    {
      // if we are not awaiting a render as a result of a previously handled event
      // raise a ThrottledMouseMove event, and add a Rendering handler so that
      // we can determine when this event has been acted upon.
      OnThrottledMouseMove(e);
      _awaitingRender = true;
      CompositionTarget.Rendering += CompositionTarget_Rendering;
    }      
  }
 
  private void CompositionTarget_Rendering(object sender, EventArgs e)
  {
    _awaitingRender = false;
    CompositionTarget.Rendering -= CompositionTarget_Rendering;
  }
 
  /// <span class="code-SummaryComment"><summary>
</span>  /// Raises the ThrottledMouseMove event
  /// <span class="code-SummaryComment"></summary>
</span>  protected void OnThrottledMouseMove(MouseEventArgs args)
  {
    if (ThrottledMouseMove!=null)
    {
      ThrottledMouseMove(_element, args);
    }
  }
}

The _awaitingRender flag ensure that we do not starve the UI. Note, the CompositionTarget.Rendering event handler removes itself after handling the event, this is because the presence of the handler causes Silverlight to continuously animate.

To use this code, simply substitute the regular MouseMove event handler with our throttled event:

// standard mouse move event handling
MouseHandler.MouseMove += new MouseEventHandler(MainPage_MouseMove);
 
// Replace with ...
 
// throttled mouse move event handling
var throttledEvent = new ThrottledMouseMoveEvent(MouseHandler);
throttledEvent.ThrottledMouseMove += 
	new MouseEventHandler(ThrottledEvent_ThrottledMouseMove);

As a result, the UI remains responsive:

thing.PNG
[CodeProject does not support Silverlight content, see the above in action on my blog.]

Throttling the Mouse Wheel Event

The MouseWheel event is another mouse event that fires very rapidly and can result in the UI becoming frozen. Again, the same technique can be used to throttle this event. However, with the MouseMove event, the event arguments carry the current mouse position, the MouseWheel event carries just the mouse wheel position delta. If we simply suppress events that occur before render, the UI will not respond correctly, with a smaller delta being applied than is required.

The code shown below is a slightly more complex class for throttling the MouseWheel event. This class aggregates all the MouseWheel events that occur before the UI is rendered, supplying a total delta:

 /// <span class="code-SummaryComment"><summary>
</span>/// A class that adapts the MouseWheel event for a UIElement to ensure that 
/// multiple events are not handled and acted upon without the UI being re-rendered.
/// <span class="code-SummaryComment"></summary>
</span>public class ThrottledMouseWheelEvent
{
  #region fields
 
  /// <span class="code-SummaryComment"><summary>
</span>  /// Source element for the MouseWheel event
  /// <span class="code-SummaryComment"></summary>
</span>  private UIElement _source;
 
  /// <span class="code-SummaryComment"><summary>
</span>  /// The combined mouse-wheel delta
  /// <span class="code-SummaryComment"></summary>
</span>  private int _combinedDelta;
 
  /// <span class="code-SummaryComment"><summary>
</span>  /// A flag that indicates that a ThrottledMouseWheel event has been raised 
  /// and we are waiting for
  /// the next Rendering event.
  /// <span class="code-SummaryComment"></summary>
</span>  private bool _awaitingRender = false;
 
  #endregion
 
  #region public API
 
  /// <span class="code-SummaryComment"><summary>
</span>  /// A 'throttled' MouseWheel event
  /// <span class="code-SummaryComment"></summary>
</span>  public event EventHandler<ThrottledMouseWheelEventArgs> ThrottledMouseWheel;
 
 
  public ThrottledMouseWheelEvent(UIElement source)
  {
    _source = source;
    _source.MouseWheel += new MouseWheelEventHandler(Source_MouseWheel);
  }
 
  #endregion
 
  #region private methods
 
  private void CompositionTarget_Rendering(object sender, EventArgs e)
  {
    Debug.WriteLine("CompositionTarget.Rendering Fired");
 
    if (_awaitingRender)
    {
      // if we have stored delta values, raise another ThrottledMouseWheel events
      if (_combinedDelta != 0)
      {
        OnThrottledMouseWheel(_combinedDelta);
      }
      _combinedDelta = 0;
      _awaitingRender = false;
    }
 
    CompositionTarget.Rendering -= CompositionTarget_Rendering;
  }
 
  /// <span class="code-SummaryComment"><summary>
</span>  /// Handles mouse wheel events from the source, either firing the 
  /// ThrottledMouseWheel event,
  /// or queueing the event until a render has occurred.
  /// <span class="code-SummaryComment"></summary>
</span>  private void Source_MouseWheel(object sender, MouseWheelEventArgs e)
  {
    Debug.WriteLine("Mousewheel Event e.Delta: " + e.Delta.ToString());
    if (!_awaitingRender)
    {
      // if we are not awaiting a render as a result of a previously handled event
      // raise a ThrottledMouseWheel event, and add a Rendering handler so that
      // we can determine when this event has been acted upon.
      OnThrottledMouseWheel(e.Delta);
      _combinedDelta = 0;
      _awaitingRender = true;
      CompositionTarget.Rendering += CompositionTarget_Rendering;
    }
    else
    {
      // if we are awaiting a render, store this delta value
      _combinedDelta += e.Delta;
    }
  }
 
  /// <span class="code-SummaryComment"><summary>
</span>  /// Raises the ThrottledMouseWheel event.
  /// <span class="code-SummaryComment"></summary>
</span>  protected void OnThrottledMouseWheel(int delta)
  {
    if (ThrottledMouseWheel != null)
    {
      ThrottledMouseWheel(_source, new ThrottledMouseWheelEventArgs()
      {
        Delta = delta
      });
    }
  }
 
  #endregion
}
 
/// <span class="code-SummaryComment"><summary>
</span>/// Provides data for the ThrottledMouseWheel event.
/// <span class="code-SummaryComment"></summary>
</span>public class ThrottledMouseWheelEventArgs : EventArgs
{
  public int Delta { get; internal set; }
}

The following application combines both of these throttled mouse event handlers:

thing.PNG
[CodeProject does not support Silverlight content, see the above in action on my blog.]

You can download the full source for this blog post: SilverlightEventThrottling.zip.

Regards, Colin E.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Colin Eberhardt
Architect Scott Logic
United Kingdom United Kingdom
I am CTO at ShinobiControls, a team of iOS developers who are carefully crafting iOS charts, grids and controls for making your applications awesome.
 
I am a Technical Architect for Visiblox which have developed the world's fastest WPF / Silverlight and WP7 charts.
 
I am also a Technical Evangelist at Scott Logic, a provider of bespoke financial software and consultancy for the retail and investment banking, stockbroking, asset management and hedge fund communities.
 
Visit my blog - Colin Eberhardt's Adventures in .NET.
 
Follow me on Twitter - @ColinEberhardt
 
-
Follow on   Twitter   Google+

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.1411023.1 | Last Updated 16 Jun 2010
Article Copyright 2010 by Colin Eberhardt
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid