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:
[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));
}
private void AddNewLine(Point position)
{
if (_lastPosition != null)
{
double angleOfMovement = RaidansToRadians(Math.Atan2(
_lastPosition.Value.Y - position.Y, _lastPosition.Value.X - position.X)) + 90;
double diff = DifferenceBetweenAngles(_angle, angleOfMovement);
_angle = _angle + DifferenceBetweenAngles(_angle, angleOfMovement) / 10;
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 }
};
LineContainer.Children.Add(line);
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:
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)
{
OnThrottledMouseMove(e);
_awaitingRender = true;
CompositionTarget.Rendering += CompositionTarget_Rendering;
}
}
private void CompositionTarget_Rendering(object sender, EventArgs e)
{
_awaitingRender = false;
CompositionTarget.Rendering -= CompositionTarget_Rendering;
}
protected void OnThrottledMouseMove(MouseEventArgs args)
{
if (ThrottledMouseMove!=null)
{
ThrottledMouseMove(_element, args);
}
}
}
The _awaitingRender
flag ensures 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:
MouseHandler.MouseMove += new MouseEventHandler(MainPage_MouseMove);
var throttledEvent = new ThrottledMouseMoveEvent(MouseHandler);
throttledEvent.ThrottledMouseMove +=
new MouseEventHandler(ThrottledEvent_ThrottledMouseMove);
As a result, the UI remains responsive:
[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:
public class ThrottledMouseWheelEvent
{
#region fields
private UIElement _source;
private int _combinedDelta;
private bool _awaitingRender = false;
#endregion
#region public API
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 (_combinedDelta != 0)
{
OnThrottledMouseWheel(_combinedDelta);
}
_combinedDelta = 0;
_awaitingRender = false;
}
CompositionTarget.Rendering -= CompositionTarget_Rendering;
}
private void Source_MouseWheel(object sender, MouseWheelEventArgs e)
{
Debug.WriteLine("Mousewheel Event e.Delta: " + e.Delta.ToString());
if (!_awaitingRender)
{
OnThrottledMouseWheel(e.Delta);
_combinedDelta = 0;
_awaitingRender = true;
CompositionTarget.Rendering += CompositionTarget_Rendering;
}
else
{
_combinedDelta += e.Delta;
}
}
protected void OnThrottledMouseWheel(int delta)
{
if (ThrottledMouseWheel != null)
{
ThrottledMouseWheel(_source, new ThrottledMouseWheelEventArgs()
{
Delta = delta
});
}
}
#endregion
}
public class ThrottledMouseWheelEventArgs : EventArgs
{
public int Delta { get; internal set; }
}
The following application combines both of these throttled mouse event handlers:
[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.