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):
- The application creates the
ComponentDispatcher
which will schedule control to its DispatcherFrame
(s) one after the other later on (using an 'almost infinite' loop).
- 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.
- Every
ComponentDispatcher
runs schedules control to the DispatcherFrame
s on its stack one after the other. Application components, controlled by DispatcherFrame
s, 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.
- 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.
- A disposing modal application component signals its
DispatcherFrame
not to continue. The next ComponentDispatcher
run clears this DispatcherFrame
and removes it from its stack.
- 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 DispatcherFrame
s 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:
public class ComponentDispatcher
{
private static ComponentDispatcher _current = null;
public List<DispatcherFrame> _frames = new List<DispatcherFrame>();
private int _modalCount;
private event EventHandler _enterThreadModal;
private event EventHandler _leaveThreadModal;
public event EventHandler EnterThreadModal
{
add { _enterThreadModal += value; }
remove { _enterThreadModal -= value; }
}
public event EventHandler LeaveThreadModal
{
add { _leaveThreadModal += value; }
remove { _leaveThreadModal -= value;}
}
public static ComponentDispatcher CurrentDispatcher
{
get
{
if (_current == null)
_current = new ComponentDispatcher();
return _current;
}
}
public static void PushModal()
{
CurrentDispatcher.PushModalInternal();
}
public static void PopModal()
{
CurrentDispatcher.PopModalInternal();
}
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);
Run();
}
public void Run ()
{
while(_frames.Count > 0)
{
for (int countFrame = _frames.Count - 1; countFrame >= 0; countFrame--)
{
DispatcherFrame dispatcherFrame = _frames[countFrame];
if (dispatcherFrame.Continue == false)
{
_frames.Remove(dispatcherFrame);
dispatcherFrame = null;
return;
}
if (dispatcherFrame.ExecutionHandler != null)
dispatcherFrame. ExecutionHandler();
if (countFrame > _frames.Count)
break;
}
}
}
private void PushModalInternal()
{
_modalCount += 1;
if(_modalCount == 1)
{
if(_enterThreadModal != null)
_enterThreadModal(null, EventArgs.Empty);
}
}
private void PopModalInternal()
{
_modalCount -= 1;
if(_modalCount == 0)
{
if(_leaveThreadModal != null )
_leaveThreadModal(null, EventArgs.Empty);
}
if(_modalCount < 0)
_modalCount = 0;
}
}
The DispatcherFrame
class:
public class DispatcherFrame
{
private bool _continue = true;
private DispatcherFrameFrameHandler _executionHandler = null;
public DispatcherFrame ()
{ ; }
public DispatcherFrame(DispatcherFrameFrameHandler executionHandler)
{
Debug.Assert( executionHandler != null,
"Creation of a new DispatcherFrame with empty handler.");
_executionHandler = executionHandler ;
}
public bool Continue
{ get { return _continue; }
set { _continue = value; }
}
public DispatcherFrameFrameHandler ExecutionHandler
{ get { return _executionHandler; } }
}
public delegate void DispatcherFrameFrameHandler ();
Points of Interest
What an easy and generic solution...
History
- 2016/05/06: First version