Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Approach to Provide Modal UI Components (e.g. Dialogs) Without Blocking

0.00/5 (No votes)
6 May 2016 1  
How to provide modal UI application components without leaving the UI thread or pause/block it

Introduction

I want to introduce a very easy solution (might look ugly but works well for me) to show modal dialog boxes without negative retroactive effect to the main window (that shall process messages unaffected). This solution shall work for any UI framework, especially for a UI framework that doesn't depend on Win32, Windows Forms, WPF, GTK+, KDE or anything else.

Background

Currently, I deal with the question: Is it possible to create a professional GUI based on OpenGL (target is Linux, Mono & Mesa) that can be compared to WPF based on DirectX? (see Reflections on a GUI toolkit based on OpenGL/OpenTK in MONO/.NET for serious applications)

Since every OpenGL window implements its own message loop, there is a need for a central instance to coordinate the interaction between multiple windows, especially between non-modal application windows and modal dialog boxes. The solution for this problem must fit into the STA (Single-Threaded Apartment) approach, almost all GUI frameworks use.

I choose to implement a ComponentDispatcher, that handles 1 ... n DispatcherFrame(s). The default dispatcher frame cares for all (or any number of) non-modal application windows. Every modal dialog box uses an own dispatcher frame.

This is how a typical program flow could look like. The application, one exemplary non-modal application component and one exemplary modal application component are displayed in swim lanes.

Some comments concerning the ComponentDispatcher and DispatcherFrame(s):

  1. The application creates the ComponentDispatcher which will schedule control to its DispatcherFrame(s) one after the other later on (using an 'almost infinite' loop).
  2. The first (and default) DispatcherFrame cares for the first non-modal application component/the main window. Other non-modal components can either be handled by this DispatcherFrame too, or can use their own DispatcherFrame(s). This (default) DispatcherFrame is created by the caller/requester of the first non-modal application component/main window.
  3. Every ComponentDispatcher runs schedules control to the DispatcherFrames on its stack one after the other. Application components, controlled by DispatcherFrames, can create modal application components, if needed. The ComponentDispatcher is not responsible for managing a maximum of one modal application component at any time. This is up to the component implementation.
  4. A newly created modal application component registers its own new DispatcherFrame. This (further) DispatcherFrame is created by the callee/agent of the modal application component/the dialog box. The new DispatcherFrame will be processed by ComponentDispatcher runs from now on.
  5. A disposing modal application component signals its DispatcherFrame not to continue. The next ComponentDispatcher run clears this DispatcherFrame and removes it from its stack.
  6. The last disposing non-modal application component signals the last remaining (not necessary but typical the default) DispatcherFrame not to continue. The next ComponentDispatcher run clears this DispatcherFrame and removes it from its stack. The ComponentDispatcher's stack goes empty. The application recognizes an empty DispatcherFrame stack and will dispose.

To realize modal behavior for modal components (e.g. dialog boxes, that handle their complete lifetime within one call to the ShowDialog() method), the related method call must return only after the modal component has finished.

This requirement eliminates the possibility to return the flow control back to the current DispatcherFrame within the current ComponentDispatcher run. Instead a new ComponentDispatcher run ins initiated. Since there is only one ComponentDispatcher's stack, this new ComponentDispatcher run schedules control to the same DispatcherFrames from now on until the modal component has finished. Meanwhile, the primary  ComponentDispatcher run is suspended and flow control happens inside the modal method call. Afterwards,  the flow control can go back to the primary DispatcherFrame within the primary ComponentDispatcher run and suspension is resumed.

The image describes the suspended/resumed ComponentDispatcher run as Dispatcher run level 1 and the new/finished ComponentDispatcher run as Dispatcher run level 2. There is no theoretical limit for run levels. Modal dialog boxes can call modal dialog boxes themselves.

Using the Code

The ComponentDispatcher class is designed as a singleton:

/// <summary>Transform modal calls (within the one and only GUI thread) into a stack
/// of dispatcher frames.</summary>
public class ComponentDispatcher
{
    /// <summary>The one and only current dispatcher instance.</summary>
    private static ComponentDispatcher _current = null;

    /// <summary>The stack of dispatcher frames.</summary>
    public List<DispatcherFrame> _frames = new List<DispatcherFrame>();

    /// <summary>The number of components on this thread, that have been gone modal.</summary>
    private int                    _modalCount;

    /// <summary>The registered delegates to call when the first component of this thread
    /// enters thread modal.</summary>
    private event EventHandler    _enterThreadModal;

    /// <summary>The registered delegates to call when all component of this thread are done
    /// with thread modal.</summary>
    private event EventHandler    _leaveThreadModal;

    /// <summary>Components register delegates with this event to handle notification about
    /// the first component on this thread has changed to be modal.</summary>
    public event EventHandler EnterThreadModal
    {
        add        {    _enterThreadModal += value;    }
        remove    {    _enterThreadModal -= value;    }
    }

    /// <summary>Components register delegates with this event to handle notification about
    /// all components on this thread are done with being modal.</summary>
    public event EventHandler LeaveThreadModal
    {
        add        {    _leaveThreadModal += value;    }
        remove    {    _leaveThreadModal -= value;}
    }

    /// <summary>Get the current component dispatcher instance, that is a singleton.</summary>
    /// <value>The current dispatcher.</value>
    public static ComponentDispatcher CurrentDispatcher
    {
        get
        {
            if (_current == null)
                _current = new ComponentDispatcher();
            return _current;
        }
    }

    /// <summary>A component calls this to go modal. (Support for current thread wide
    /// modality only.)</summary>
    public static void PushModal()
    {
        CurrentDispatcher.PushModalInternal();
    }

    /// <summary>A component calls this to end being modal. (Support for current thread wide
    /// nodality only.)</summary>
    public static void PopModal()
    {
        CurrentDispatcher.PopModalInternal();
    }

    /// <summary>Add the indicated dispatcher frame to the stack.</summary>
    /// <value>The dispatcher frame to add to the stack.</value>
    public void PushFrame (DispatcherFrame frame)
    {
        Debug.Assert (frame != null, "To push an empty frame is senseless. Ignore request.");
        if (frame == null)
            return;
            
        _frames.Add (frame);

        // Enter a new execution level as a child of the current execution level.
        Run();
    }

    /// <summary>Process the stack of dispatcher frames.</summary>
    public void Run ()
    {
        // Consider that _frames can change within a loop!
        while(_frames.Count > 0)
        {
            // Loop backward because:
            // - The highest priority frames are at the end.
            // - It's easier to handle changes within _frames.
            for (int countFrame = _frames.Count - 1; countFrame >= 0; countFrame--)
            {
                DispatcherFrame dispatcherFrame = _frames[countFrame];
                if (dispatcherFrame.Continue == false)
                {
                    _frames.Remove(dispatcherFrame);
                    dispatcherFrame = null;

                    // Leave this execution level and return control to parent execution level.
                    return;
                }
        
                if (dispatcherFrame.ExecutionHandler != null)
                    dispatcherFrame. ExecutionHandler();

                if (countFrame > _frames.Count)
                    break;
            }
        }
    }

    /// <summary>A component calls this on going modal.</summary>
    private void PushModalInternal()
    {
        _modalCount += 1;
        if(_modalCount == 1)
        {
            if(_enterThreadModal != null)
                _enterThreadModal(null, EventArgs.Empty);
        }
    }

    /// <summary>A component calls this on end being modal.</summary>
    private void PopModalInternal()
    {
        _modalCount -= 1;
        if(_modalCount == 0)
        {
            if(_leaveThreadModal != null )
                _leaveThreadModal(null, EventArgs.Empty);
        }
        if(_modalCount < 0)
            _modalCount = 0;
    }
}

The DispatcherFrame class:

/// <summary>Represent an execution context/block within the current thread.</summary>
public class DispatcherFrame
{
    /// <summary>Determine whether to continue execution of this context/block within
    /// the current thread.</summary>
    private bool                        _continue = true;

    /// <summary>The delegate to execute on every frame activation.</summary>
    private DispatcherFrameFrameHandler    _executionHandler = null;

    /// <summary>Initialize a new instance of the DispatcherFrame class.</summary>
    /// <remarks>Some DispatcherFrames don't need an own delegate to execute, the
    /// initial/default dispatcher frame's delegate to execute is sufficient.</remarks>
    public DispatcherFrame ()
    {    ;    }

    /// <summary>Initialize a new instance of the DispatcherFrame class with execution
    /// handler.</summary>
    /// <param name=" executionHandler ">The delegate to execute on every frame activation.</param>
    public DispatcherFrame(DispatcherFrameFrameHandler executionHandler)
    {    
        Debug.Assert( executionHandler != null,
            "Creation of a new DispatcherFrame with empty handler.");
        _executionHandler =  executionHandler ;
    }

    /// <summary>Get or set a value indicating whether this DispatcherFrame is to
    /// continue.</summary>
    /// <value>Returns/set <c>true</c> if this instance is to continue, or <c>false</c>
    /// otherwise.</value>
    public bool Continue
    {    get { return _continue; }
        set { _continue = value; }
    }

    /// <summary>Get the delegate of the callback to execute on every frame activation.</summary>
    /// <value>The delegate of the callback to execute on every frame activation. Can be
    /// <c>null</c>.</value>
    public DispatcherFrameFrameHandler ExecutionHandler
    {    get { return _executionHandler; } }
}

/// <summary>Prototype of a callback to execute on every frame activation.</summary>
public delegate void DispatcherFrameFrameHandler ();

Points of Interest

What an easy and generic solution...

History

  • 2016/05/06: First version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here