Workaround - WindowWorkspace closing sequence
There have been several discussions about issues when closing a view in a WindowWorkspace, but not regarding the CloseView() method, but the sequence launched when pressing ALT+F4 or clicking on the “X”. Some of these discussions dealt with the intention to handle this sequence in order to abort it. However, some others dealt with issues regarding the resulting state of the closing view. This last subject will be addressed in this article.
In order to get started, let’s have an overview on both closing sequences: the one that is launched when the CloseView() method is called, and the one that is launched when you press ALT+F4 or click on the “X”.
Figure 1. The CloseView() method closing sequence.
Figure 2. The “X” closing sequence.
Do I have to say that they don’t look quite the same?
Now that it’s shown that these sequences are not alike, it’s time to realize that we should not treat them alike! For example, there have been several threads saying that with one sequence worked, but with the other it didn’t. Coincidence? No! Now, we know better.
But let me address two known issues caused by this second sequence.
The first scenario: ObjectDisposedException
When you close a form by clicking the “X” button or by pressing ALT+F4, the SmartPart (view) contained in the form gets disposed, so when you try to reuse it, you get the following exception:
ObjectDisposedException: Cannot access a disposed object. Object name: ‘%name%’.
NOTE: %name% represents the type of the disposed object.
If we check the SmartPart contained in the WorkItem.SmartParts collection, we’ll see that it is disposed (the IsDisposed property is true):
Figure 3. The disposed SmartPart.
Quite messy, huh? Why does this happen when we go through the “X” sequence and not with the CloseView() sequence? This occurs because the SmartPart is not removed from the form when it closes, so the SmartPart gets disposed when the form disposes itself and its components.
So, if you want to modify the CAB source code, just removing the only control that the form has (the view) should do the trick.
The second scenario: ArgumentOutOfRangeException
This exception generally occurs when you try to get the view from the form and when the form tries to get it, there’s none, so the index is out of range. I shall address this issue in the scenario in which you try to show the view in another Workspace (by calling the IWorkSpace.Show() method) in a subscription to the SmartPartClosing event. In this case, the view is removed from the form when it’s closing, so when the form tries to get its child control (the view) in order to hide it (some code will be provided below to clarify), the following exception is thrown:
ArgumentOutOfRangeException: Index 0 is out of range. Parameter name: index.
This is thrown at the commented line (which is located in the WindowForm class located inside the WindowWorkspace class) as the control has been removed from the Form.Controls collection before it reaches that line:
protected override void OnClosing(CancelEventArgs e)
{
if (this.Controls.Count > 0)
{
WorkspaceCancelEventArgs cancelArgs = FireWindowFormClosing(this.Controls[0]);
e.Cancel = cancelArgs.Cancel;
if (cancelArgs.Cancel == false)
{
this.Controls[0].Hide(); //exception thrown here!
}
}
base.OnClosing(e);
}
Again, in this case, the fastest fix would be to add a conditional check to verify if the form still contains any control.
The Workaround
Quick bug fixes, don’t they? However, the post is titled “workaround”, not “quick bug fix by altering the source”, so I’ll provide a workaround!
What I actually did was to create a new WindowWorkspace class, which inherits from the CAB WindowWorkspace and created a SmartPartClosed event for it.
So, let’s start with some quick steps!
Step 1
First of all, I’ve created a new WindowWorkspace class, which inherits from the CAB WindowWorkspace, so I needed the owner form and the two base constructors. Therefore, the code started looking like this:
public class WindowWorkspace : Microsoft.Practices.CompositeUI.WinForms.WindowWorkspace
{
IWin32Window _owner;
public WindowWorkspace()
: base()
{
}
public WindowWorkspace(IWin32Window owner)
: base(owner)
{
_owner = owner;
}
}
Step 2
Secondly, I needed the necessary code to show a view in a new form. So, I’ve overridden OnShow() method in order to use a new implementation of the GetOrCreatForm() method.
protected override void OnShow(Control smartPart, WindowSmartPartInfo smartPartInfo)
{
GetOrCreateForm(smartPart);
base.OnShow(smartPart, smartPartInfo);
}
protected new Form GetOrCreateForm(Control smartPart)
{
Form form = base.GetOrCreateForm(smartPart);
form.ShowInTaskbar = (_owner == null);
return form;
}
So far, nothing new.
Step 3
Then, I started wondering where I could attach my new event in order to get safely the SmartPart. I realized that I could attach this new event to the FormClosing event of the form, so that it’s executed after the FormClosing, SmartPartClosing and the WindowFormClosing events are fired, but before the FormClosed and the WindowFormClosed events are fired and the view gets disposed. Therefore, I added the necessary code for that: I attached the handler in the GetOrCreateForm before returning the newly created form, and then wrote its implementation.
The final code for the whole class is:
public class WindowWorkspace : Microsoft.Practices.CompositeUI.WinForms.WindowWorkspace
{
IWin32Window _owner;
public WindowWorkspace()
: base()
{
}
public WindowWorkspace(IWin32Window owner)
: base(owner)
{
_owner = owner;
}
protected override void OnShow(Control smartPart, WindowSmartPartInfo smartPartInfo)
{
GetOrCreateForm(smartPart);
base.OnShow(smartPart, smartPartInfo);
}
protected new Form GetOrCreateForm(Control smartPart)
{
Form form = base.GetOrCreateForm(smartPart);
form.ShowInTaskbar = (_owner == null);
form.FormClosing += new FormClosingEventHandler(form_FormClosing);
return form;
}
void form_FormClosing(object sender, FormClosingEventArgs e)
{
Form form = (Form)sender;
Control smartPart = GetSmartPart(form);
if (form.Controls.Count > 0)
form.Controls.Remove(smartPart);
if (SmartPartClosed != null)
SmartPartClosed(this, new WorkspaceEventArgs(smartPart));
}
private Control GetSmartPart(Form containerForm)
{
foreach (KeyValuePair<Control, Form> pair in this.Windows)
{
if (pair.Value == containerForm)
return pair.Key;
}
return null;
}
public event EventHandler<WorkspaceEventArgs> SmartPartClosed;
}
Once I had this, I stopped using the SmartPartClosing event for some scenarios and started using the SmartPartClosed event. With the last one, I’ve been able to:
- + Toggle views between a WindowWorkspace and another workspace (e.g.: DeckWorkspace).
- + Reuse the SmartPart.
- + Reopen the form containing the SmartPart.
Nevertheless, there are scenarios that I’m sure I didn’t consider, so any feedback is more than welcome!
Cheers,
- Nacho!