The Goal:
On my current project, we need to disable the UI while performing long-running operations such as the loading and saving of data. We started off using some dirt-simple code that would wrap the long-running process in a try/finally block, disabling the form at the beginning of the "try", and re-enabling it in the "finally". It was simple, and worked just fine at first, but started showing its limitations as things have got a little more complicated. Our forms host UserControl "Views" which sometimes wrap other views as children. Any one of these views, parent or child, might need to trigger a long-running processes. To complicate matters further, some of the child views’ processes can be expected to be called as part of the parent views’ processing.
For instance, lets say we have an order processing system, and the customer details view is considered a parent view. Within that view are multiple child views for displaying things like address information, or a list of that customer’s order history. Loading, saving, or refreshing the parent view automatically results in the loading, saving, or refreshing of the child views. In addition, some child views can be loaded, saved, or refreshed individually, and therein lies the problem. I can’t just repeat my el cheapo try/finally trick in the child views because as each child view finishes its individual processing it will automatically re-enable itself, but the other views may not be finished with their processing yet. In short, each child should remain disabled until they’ve all finished their work. This scenario is a complete fabrication, but it illustrates the point.
It occurred to me today that the problem of disabling and enabling the forms was very much like the problem of changing the cursor to and from the hourglass. I’ve been using a customized and renamed variant of Doogal Bell’s CursorHandler class (http://www.doogal.co.uk/cursor.php) to handle setting and restoring the hourglass cursor on my last couple projects. It’s simple to use, and you can stack up as many calls to it as you want, and they will always unwind properly. I thought I’d try something similar to handle my enabling/disabling needs. Parent view controllers would perform their work inside a using block, which would automatically restore both the hourglass and the enabled/disabled state of a control when it’s finished.
The Difficulty: (Skip ahead if you want, I won’t be offended)
In theory, this should be very simple. In reality it requires a few tricks. First of all, if you disable a container control such as a UserControl, a Panel, or a GroupBox, you’ll see that all of the controls inside that container get disabled for you automatically. When you re-enable the container, the children will go back to their original states. This is very convenient, and takes a lot of work off our hands normally but it interferes with my plans in this particular scenario. If you ask the child controls of a disabled parent whether they are enabled or not, they will say they are disabled even when it’s really their parent that’s disabled.
So things would go pretty much like this. My parent view instantiates a new WaitContext (As I’m calling it), which looks at the parent control and memorizes that it was enabled when it started, and then disables it. The parent view initiates some process on the child view which in turn instantiates its own WaitContext which looks at the child and memorizes that it was disabled when it started. The child view’s process completes, and its WaitContext tries to "restore" the original setting, which it remembers as being disabled. The parent view then wraps up its work and unwinds its WaitContext, which properly sets the parent view back to enabled. Because the inner WaitContext disabled the child view we now have an enabled parent with a disabled child. This isn’t what we started with, though. What we really need is to know whether each individual control is really disabled, or is just disabled by virtue of being contained in a disabled parent.
Thanks to the miracle of Lutz Roeder’s Reflector, I found out that Enabled isn’t just a simple boolean property. When you ask a control whether it’s enabled, the answer you get back depends on a couple of things. First of all, it depends on whether that individual control is disabled, and secondly on whether that control’s parent is disabled. It’s that first part that we’re interested in. Determining whether a control has been explicitly disabled is done by checking bit 4 of the control’s "state" field via the GetState method. State contains a number of flags indicating all kinds of things like whether the control is Enabled, Visible, or in the middle of doing something. Unfortunately for us, the state field is private, and the GetState method is marked Internal, so we can’t see either one of them.
Since I know the name of the method I want, and I know which bit I’m looking for, I can still get what I want via Reflection. The call to get the original Enabled state of the control looks like this:
OriginalEnabled = (bool)Control.GetType().InvokeMember("GetState",
BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic,
null, Control, new Object[] { 4 });
Setting the enabled state actually takes two steps. The call to "SetState" sets the bitfield, and the call to "OnEnabledChanged" forces the control to notice the change and repaint itself.
Control.GetType().InvokeMember("SetState",
BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic,
null, Control, new Object[] { StateEnabled, enabled }); Control.GetType().InvokeMember("OnEnabledChanged",
BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic,
null, Control, new Object[] { EventArgs.Empty });
The Result:
Wrap your long-running code inside a Using block that instantiates a WaitContext object like this:
using (new WaitContext(this)) { // long-running code goes here.
}
There are three different constructors. The parameterless constructor will change the cursor to the hourglass without disabling anything, and makes the class work just like Doogal’s CursorHandler. The second constructor takes a Control reference, which is the control to disable. It changes the cursor to the hourglass (Or spinning donut for Vista users), as well as disabling the specified control. The third, and final constructor allows you to specify the control to disable as well as what cursor should be displayed. It’s probably not going to get used, but I like to include it for completeness and flexibility.
The Class:
Here is the finished WaitContext class.
public class WaitContext : IDisposable { #region Fields private const BindingFlags flags =
BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic; private const int StateEnabled = 4; #endregion #region Properties private Control Control { get; set; } private Cursor OriginalCursor { get; set; } private bool OriginalEnabled { get; set; } #endregion #region Constructors / Destructors public WaitContext() : this(null) { } public WaitContext(Control control) : this(control, Cursors.WaitCursor) { } public WaitContext(Control control, Cursor newCursor) { SaveState(control, newCursor); } ~WaitContext() { RestoreState(); } #endregion #region Methods [ReflectionPermission(SecurityAction.Demand, MemberAccess = true)] private void EnableControl(bool enabled) { Control.GetType().InvokeMember("SetState",
flags, null, Control, new Object[] { StateEnabled, enabled }); Control.GetType().InvokeMember("OnEnabledChanged",
flags, null, Control, new Object[] { EventArgs.Empty }); } private void RestoreState() { if (Control != null) { Control.Cursor = OriginalCursor; EnableControl(OriginalEnabled); } else { Cursor.Current = OriginalCursor; } } [ReflectionPermission(SecurityAction.Demand, MemberAccess = true)] private void SaveState(Control control, Cursor newCursor) { Control = control; if (control != null) { OriginalCursor = Control.Cursor; Control.Cursor = newCursor; OriginalEnabled = (bool)Control.GetType().InvokeMember("GetState",
flags, null, Control, new Object[] { StateEnabled }); EnableControl(false); } else { OriginalCursor = Cursor.Current; Cursor.Current = newCursor; } } #endregion #region IDisposable Members public void Dispose() { RestoreState(); GC.SuppressFinalize(this); } #endregion }
Footnote: Why yes, I am an adherent of "Oxford commas".
Wow, I didn\’t realize that what you were doing was that complicated. Thank God for Reflection!