Tuesday, February 15, 2011

Why does my Silverlight application sometimes hang after closing a ChildWindow?

I’ve been working on a Silverlight application the last several months, and we use the ChildWindow control in a number of places in our application as a modal popup window.   We were running into an issue sporadically where, after closing a ChildWindow, the entire UI of the application would remain in a ‘disabled’ state (though without the overlay).  This had been occasionally annoying us (the developers), but as we got to user testing, it became obvious we needed to figure out the source.

My first real clue came while testing some logic I added to let the user hit escape to close the window.  I opened a dialog, then on a whim, held down the escape key instead of just pressing it.  Sure enough, the window closed like I wanted – awesome, feature added, task done, right?  I opened the window again, clicked the X, and I had the ‘hung app’ scenario!  After refreshing and trying a few scenarios, I had a repro case: open a dialog, hold escape until it closes, then open and close any other dialog and the interface remains frozen.  A bit more digging around and experimentation yielded the answer – calling Close() on a dialog twice meant that the next dialog to open wouldn’t properly restore the IsEnabled property on the application root visual to ‘true’.

The question that remained was why…  So, what did I do?  Why pull up Reflector of course.  Here’s the ‘Close’ method of ChildWindow:

image

There’s a bunch of code here to fire off events and mess with the visual state, but it’s basically this: When close is called, if “IsOpen” is true,

  • Decrement the count of open windows.  If it is now 0, restore the “IsEnabled” state of the root visual.
  • If there is a ‘Closed’ visual state defined, transition to it, otherwise, close the popup wrapping this window (which sets IsOpen to false)

(There is an event handler on the Closed visual state that sets IsOpen to false when its transition completes).

 

And there is the problem.  If I call Close() twice in quick succession, the first call decrements the count of open child windows to 0, restores the root visual’s IsEnabled value, and starts the transition to the Closed visual state.  The second call then decrements the count of open windows *again* – this time to -1 (since the transition to ‘Closed’ hasn’t completed yet), and triggers the transition to the ‘Closed’ visual state again (though it appears to be ignored). Then, later on, when another child window is opened, it increments the count of open windows back to 0.  When it closes, it decrements it to –1, which isn’t 0, so it doesn’t restore the IsEnabled value on the RootVisual.

So, the answer is to not call Close multiple times.  The first step was to look for places where I assigned DialogResult and then immediately called Close() – the setter for DialogResult calls Close(), so there’s no need to do it again.  However, that didn’t solve all my problems.  Since I can’t trust the value of ‘IsOpen’ to tell me I shouldn’t call Close() again, I decided to just subclass ChildWindow and add the logic I needed in a there, and thus was born ChildWindowEx.  This allowed me to not have to do detailed testing of each of our dialogs to see if it exhibited the issue, and didn’t require re-working any other logic.  The code is actually pretty simple for fixing this odd bug:

ChildWindowEx.cs
  1. /// <summary>
  2. /// Protect against calling Close() twice in quick succession that will mess up ChildWindow's count of how many windows are open, and lead it to fail to re-activate the root visual
  3. /// </summary>
  4. public class ChildWindowEx : ChildWindow
  5. {
  6.     private bool _canBeClosed = false;
  7.  
  8.     protected override void OnOpened()
  9.     {
  10.         base.OnOpened();
  11.         _canBeClosed = true;
  12.     }
  13.  
  14.     protected override void OnClosed(EventArgs e)
  15.     {
  16.         base.OnClosed(e);
  17.         _canBeClosed = false;
  18.     }
  19.  
  20.     protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
  21.     {
  22.         if (!_canBeClosed)
  23.             e.Cancel = true;
  24.         base.OnClosing(e);
  25.     }
  26. }

Basically, I set a bool when the window opens, clear it when it closes, and use it to cancel a close attempt if the window can no longer be safely Closed.  Change all the dialogs to inherit from this class instead of directly from ChildWindow and the problem is solved.

 

Sure, I could have been more careful in the code about the double-close thing, but every future developer on the project would then need to be versed in this issue and how to avoid it, and it can be tricky to notice the problem and compensate for it.  Or, I could make this class, make it project policy to always use this class instead of using ChildWindow directly, and the problem won’t ever pop up again.

 

Note: this was not tested with a window that is repeatedly shown and closed – might work for that case, might not.