Update as fast as possible but slow down a bit
You may think updateing your forms or controls in realtime is easy.
Let’s consider the following scenario: you have a server-client application where the server sends his clients messages. Your clients displays those messages in a Label, displaying always that newest message. The messages are send asynchronously.
So, here is some code that updates our Label
class ClientForm : Form
{
private Label m_label;
public ClientForm()
{
m_label = new Label();
// add code to initialize and position the label
this.Controls.Add(m_label);
}
// this message is called by some other thread, so we have to
// ensure that we dispatch the call to the UI thread
private delegate void DisplayServerStatusMessageCallback(string msg);
public void DisplayServerStatusMessage(string serverStatusMessage)
{
if (!this.InvokeRequired)
{
m_label = serverStatusMessage;
}
else
{
this.Invoke(new DisplayServerStatusMessageCallback(this.DisplayServerStatusMessage), serverStatusMessage);
}
}
}
The above code is fine as long as the server message come in slowly (let’s say 1-5 per second).
Now consider the scenario when the server sends 1000 messages a second, then the code above would probably freeze your application (even though you call Invoke). The problem is that your code will invoke all the time and has no time to do someting else. Bummer!
So, i came up with a little class called UITimerUpdater. The idea behind that class is that it uses a timer to notify you when it is okay to update your form or controls.
Have a look:
public sealed class UITimerUpdater : IDisposable
{
private object m_sync = new object();
private Timer m_timer;
private Control m_control;
private Action m_updateCallback;
private int m_syncing = 0;
private bool m_disposed = false, m_notify = false, m_notifyAlways = false, m_fireAfterSync = false;
public UITimerUpdater(Control control, int updateTimeout, Action updateCallback)
: this(control, updateTimeout, updateCallback, false)
{
}
public UITimerUpdater(Control control, int updateTimeout, Action updateCallback, bool notifyAlways)
{
if (control == null)
throw new ArgumentNullException("control");
if (updateCallback == null)
throw new ArgumentNullException("updateCallback");
m_control = control;
m_updateCallback = updateCallback;
m_notifyAlways = notifyAlways;
m_timer = new Timer();
m_timer.Interval = updateTimeout;
m_timer.Tick += new EventHandler(OnTimerTick);
m_timer.Start();
}
~UITimerUpdater()
{
this.Dispose(false);
}
private void Fire()
{
if (m_notify || m_notifyAlways)
{
m_control.UIThread(m_updateCallback); // see ControlExtensions below
m_notify = false;
}
}
public void Sync(Action action)
{
this.Sync(action, false);
}
public void Sync(Action action, bool uisafe)
{
this.Sync(action, uisafe, false);
}
public void SyncAndFire(Action action)
{
this.SyncAndFire(action, false);
}
public void SyncAndFire(Action action, bool uisafe)
{
this.Sync(action, uisafe, true);
}
private void Sync(Action action, bool uisafe, bool fire)
{
if (action == null)
throw new ArgumentNullException("action");
this.BeginSyncing();
try
{
if (uisafe) m_control.UIThread(action);
else action.Invoke();
}
finally { this.EndSyncing(fire); }
}
private void OnTimerTick(object sender, EventArgs e)
{
m_timer.Stop();
lock (m_sync)
{
if (m_syncing <= 0)
this.Fire();
}
m_timer.Start();
}
public void BeginSyncing()
{
lock (m_sync) { m_syncing++; }
}
public bool IsUpdateing
{
get { lock (m_sync) { return m_syncing > 0; } }
}
public void EndSyncing()
{
this.EndSyncing(true);
}
public void EndSyncing(bool tryToFire)
{
lock (m_sync)
{
if (m_syncing <= 0)
throw new InvalidOperationException("You have to call BeginUpdate first.");
m_notify = true;
m_syncing--;
m_fireAfterSync = m_fireAfterSync || tryToFire;
if (m_fireAfterSync)
{
if (m_syncing == 0)
{
this.Fire();
m_fireAfterSync = false;
}
}
}
}
#region IDisposable Members
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!m_disposed)
{
if (disposing)
{
m_timer.Stop();
m_timer.Dispose();
}
m_disposed = true;
}
}
#endregion
}
// an extension to System.Windows.Forms.Control to allow calling Control.Invoke more easyly.
namespace System.Windows.Forms
{
public delegate void ActionControl(Control c);
static public class ControlExtensions
{
static public void UIThread(this Control control, Action code)
{
if (control.InvokeRequired) control.BeginInvoke(code);
else code.Invoke();
}
static public void UIThread(this Control control, ActionControl code)
{
if (control.InvokeRequired) control.BeginInvoke(code, control);
else code.Invoke(control);
}
static public void UIThreadInvoke(this Control control, Action code)
{
if (control.InvokeRequired) control.Invoke(code);
else code.Invoke();
}
}
}
As i always post code without any comments (
class ClientForm : Form
{
private Label m_label;
private UITimerUpdater m_updater;
private string m_newestServerStatusMessage;
public ClientForm()
{
m_label = new Label();
// add code to initialize and position the label
this.Controls.Add(m_label);
m_updater = new UITimerUpdater(this, 1000, this.UpdateServerStatusMessage);
}
// this is now called once every second by the UITimerUpdater
private void UpdateServerStatusMessage()
{
// display the newest message
m_label.Text = m_newestServerStatusMessage;
}
public void DisplayServerStatusMessage(string serverStatusMessage)
{
// save the newest serverStatusMessage
m_updater.Sync(delegate() { m_newestServerStatusMessage = serverStatusMessage; });
// you could also call
// m_update.BeginSyncing();
// try { m_newestServerStatusMessage = serverStatusMessage; }
// finally { m_update.EndSyncing(); }
}
}
HTH