Click here to Skip to main content
15,881,413 members
Articles / General Programming / Threads
Tip/Trick

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

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
6 May 2016CPOL3 min read 12.8K   6   3
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.

Image 1

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:

C#
/// <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:

C#
/// <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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader Celonis SA
Germany Germany
I am currently the CEO of Symbioworld GmbH and as such responsible for personnel management, information security, data protection and certifications. Furthermore, as a senior programmer, I am responsible for the automatic layout engine, the simulation (Activity Based Costing), the automatic creation of Word/RTF reports and the data transformation in complex migration projects.

The main focus of my work as a programmer is the development of Microsoft Azure Services using C# and Visual Studio.

Privately, I am interested in C++ and Linux in addition to C#. I like the approach of open source software and like to support OSS with own contributions.

Comments and Discussions

 
Questionc'mon Pin
Member 1447228412-Aug-20 23:01
Member 1447228412-Aug-20 23:01 
cmon, post an example of how to use them as well...??

Questiondemo project Pin
Sacha Barber6-May-16 3:30
Sacha Barber6-May-16 3:30 
AnswerRe: demo project Pin
Steffen Ploetz7-May-16 4:31
mvaSteffen Ploetz7-May-16 4:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.