programming with esskar

Just another WordPress.com weblog

Update as fast as possible but slow down a bit

leave a comment »

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 ( :) ), let’s see how you can use it by changing the above client example:

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

Advertisement

Written by esskar

February 15, 2010 at 2:17 pm

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.