How do I respond to the last MDI childform closing? - c#

My goal is to respond to the last child form of an mdi container closing(for example, close the parent itself or show something new). The problem I face is that the mdi container's MdiChildren collection still indicates that the container contains children.
The approach I have tried is
void childMdiForm_FormClosed(object sender, FormClosedEventArgs e)
{
if (this.MdiChildren.Any())
{
//Do stuff
}
}
MdiChildren.Count() is still 1 after the last child form is closed.
I have the same results by trying to handle the parentform.MdiChildActivate event.
The MdiChildren collection appears to not be updated yet when the child form has closed. The same occurs when there are multiple children: it will still contain all the crildren, it appears to update the collection at a later moment.
Is this the right approach? If not, how can I get an accurate count of the number of mdi children after closing a form?

Yes, the MdiChildren property doesn't get updated until after the FormClosed event is delivered. There's a universal solution for event order issues like this, you can elegantly get code to run after event handing is completed by using the Control.BeginInvoke() method. This code solves your problem:
protected override void OnMdiChildActivate(EventArgs e) {
base.OnMdiChildActivate(e);
this.BeginInvoke(new Action(() => {
if (this.MdiChildren.Length == 0) {
// Do your stuff
//...
MessageBox.Show("None left");
}
}));
}

WinForms can be a little quirky at times and this is one example of that. I am not entirely sure why closing the last MDI child does not make the MdiChildren property return an empty array immediately thereafter.
In most cases you're going to be keeping track of either the children forms or the data models yourself, so I would simply keep a local array for managing this:
List<Form> childForms = new List<Form>();
void AddChildWindow()
{
var window = new ChildForm();
window.MdiParent = this;
window.Tag = Guid.NewGuid();
window.FormClosed += (sender, e) => { OnMdiChildClosed(sender as Form, e.CloseReason); };
window.Shown += (sender, e) => { OnMdiChildShown(sender as Form); };
window.Show();
}
void OnMdiChildShown(Form window)
{
childForms.Add(window);
Trace.WriteLine(string.Format("Child form shown: {0}", window.Tag));
Trace.WriteLine(string.Format("Number of MDI children: {0}", childForms.Count));
}
void OnMdiChildClosed(Form window, CloseReason reason)
{
childForms.Remove(window);
Trace.WriteLine(string.Format("Child form closed: {0} (reason: {1})", window.Tag, reason));
Trace.WriteLine(string.Format("Number of MDI children: {0}", childForms.Count));
if (childForms.Count == 0)
{
// Do your logic here.
}
}

Here's a full test example:
namespace WindowsFormsApplication1
{
public partial class mdiMainForm : Form
{
private List<Form> _children = new List<Form>();
public mdiMainForm()
{
InitializeComponent();
}
private void mdiMainForm_Shown(Object sender, EventArgs e)
{
Form3 f3 = new Form3();
f3.MdiParent = this;
f3.FormClosed += mdiChildClosed;
_children.Add(f3);
f3.Show();
Form4 f4 = new Form4();
f4.MdiParent = this;
f4.FormClosed += mdiChildClosed;
_children.Add(f4);
f4.Show();
Form5 f5 = new Form5();
f5.MdiParent = this;
f5.FormClosed += mdiChildClosed;
_children.Add(f5);
f5.Show();
}
private void mdiChildClosed(Object sender, FormClosedEventArgs e)
{
if (_children.Contains((Form)sender))
_children.Remove((Form)sender);
if (_children.Count == 0)
MessageBox.Show("all closed");
}
}
}

You're code seems to imply you are checking the child Form's MdiChildren collection. But perhaps I'm misreading your code.
Use the MDI Form's 'MdiChildActivate' event:
private void Form1_MdiChildActivate(object sender, EventArgs e) {
if(this.MdiChildren.Length == 0) {
// replace with your "Do Stuff" code
MessageBox.Show("All Gone");
}
}

I know this answer is very late, but I have a much simpler solution to the problem than what has been provided so I'll share it here.
private void ChildForm_FormClosing(object sender, FormClosingEventArgs e)
{
// If WinForms will not remove the child from the parent, we will
((Form)sender).MdiParent = null;
}

Related

Restore form from secondary form

I have a form that minimizes when I open a secondary form (using a button click event). I want to be able to restore the original form when closing the secondary form. I have not been able to figure out how to restore the original. Here is the code I have (in example form)
The code from the main form:
private void BtnSecondaryForm_Click(object sender, EventArgs e)
{
// Open the secondary form.
FrmSecondaryForm fsf = new FrmSecondaryForm ();
fsf.Show();
this.WindowState = FormWindowState.Minimized;
}
Code from the secondary form:
private void FrmSecondaryForm_FormClosing(object sender, FormClosingEventArgs e)
{
// I need this code to be able to restore the main form.
}
Please don't just redirect me to someone else's similar question without explaining how I can get this to work in my application. I have looked at the other similar questions here on Stack Overflow already and don't understand how to get this to work.
You should find the form you want or restore, e.g.
using System.Linq;
...
private void FrmSecondaryForm_FormClosing(object sender, FormClosingEventArgs e)
var mainForm = Application
.OpenForms
.OfType<MainForm>()
.LastOrDefault();
if (mainForm != null) {
mainForm.WindowState = FormWindowState.Normal;
}
}
Move FormClosing event handler to primary form. This form is interested in the event anyway. I also changed event from FormClosing to FormClosed. Former can be called many times, but latter is called only once, when the form is actually closed.
private FrmSecondaryForm Fsf = null;
private void BtnSecondaryForm_Click(object sender, EventArgs e)
{
// Open the secondary form.
Fsf = new FrmSecondaryForm();
Fsf.Show();
Fsf.FormClosed += PrimaryForm_SecondaryFormClosed;
this.WindowState = FormWindowState.Minimized;
}
private void PrimaryForm_SecondaryFormClosed(object sender, FormClosedEventArgs e)
{
Fsf.FormClosed -= PrimaryForm_SecondaryFormClosed;
Fsf.Dispose();
Fsf = null;
this.WindowState = FormWindowState.Normal;
}
So you need Form2 to interact with Form1, which means that Form2 needs a reference to it.
Best is to do this during construction. But with forms, you should always keep a default no-parameter constructor, so we need to add a new one and make the original private.
//Add a new property (or field if you wish)
private Form formToMinimise { get; }
//Change this to be private
private SecondaryForm()
{
InitializeComponent();
}
//Add this new constructor as public
public SecondaryForm(Form form): this()
{
formToMinimise = form;
}
Now closing and restore original. We do a null check, just in case
private void FrmSecondaryForm_FormClosing(object sender, FormClosingEventArgs e)
{
formToMinimise?.WindowState = FormWindowState.Normal ;
}
Now you amend the creation and calling of your second form like this
private void BtnSecondaryForm_Click(object sender, EventArgs e)
{
// Create the secondary form with reference to this form
FrmSecondaryForm fsf = new FrmSecondaryForm(this);
fsf.Show();
this.WindowState = FormWindowState.Minimized;
}

Change LayoutMdi from parent form when closing child form

I'm currently working with forms and mdi. In my project there is a mainform (a mdiContainer) which can have x subforms. I want to reach, that everytime, a subform is closed, all other subforms where arranged again.
You can do that with writing this into the mainform:
public void resetToolStripMenuItem_Click(object sender, EventArgs e)
{
this.LayoutMdi(System.Windows.Forms.MdiLayout.TileVertical);
}
In the subform, i do this:
private void subform_FormClosed(object sender, FormClosedEventArgs e)
{
try
{
Form1 mainform = new Form1();
mainform.resetToolStripMenuItem_Click(mainform, EventArgs.Empty);
}
catch
{
System.Windows.Forms.MessageBox.Show("error");
}
}
It does not give any error, but also wont arrange the subforms again. I also tried to call the method with other parameters.
Any idea how i can make this work?
This line should make you pause:
Form1 mainform = new Form1();
You made a new form, so you aren't referencing the existing one.
But I think there are issues trying to do this from the child form.
It's probably better to listen to the Closed event of the child from the MDIParent, like this:
ChildForm childForm = new ChildForm();
childForm.FormClosed += childForm_FormClosed;
childForm.MdiParent = this;
childForm.Show();
And then in the Closed method, call the code:
void childForm_FormClosed(object sender, FormClosedEventArgs e) {
this.BeginInvoke(new Action(() => {
resetToolStripMenuItem_Click(null, null);
}));
}
I used BeginInvoke because otherwise, the closed child form is still being included in the layout tiling.

Generic / reusable form open/close events

Whilst developing a current project which contains a number of WinForms, I'm finding myself becoming cluttered with lines of code simply to handle open / close events for the forms. Currently I'm handling them like so..
//Declare forms
myForm mForm1;
myForm2 mForm2;
private void btnSomething_Click(object sender, EventArgs e)
{
if (mForm1 == null)
{
mForm1 = new myForm();
mForm1.FormClosed += new FormClosedEventHandler(mForm1_FormClosed);
mForm1.Show();
}
else
if (mForm1.WindowState == FormWindowState.Minimized)
mForm1.WindowState = FormWindowState.Normal;
mForm1.Focus();
}
void mForm1_FormClosed(object sender, FormClosedEventArgs e)
{
mForm1 = null;
}
And then another set of voids to handle each forms open / close. Now imagine that instead of 2 forms, I've got, say, 5 forms. Now I'm even more cluttered. Is there a way to generalize this to have all forms have the same event handlers?
I've thought of perhaps using the object sender in an "as" statement, but i'm not sure how i'd find the relevant declared form instance from there.
sender as (form)
Any ideas?
Make your Forms implementing some IFormWithMyEvents.
You can generalize that code easily:
//Declare forms
myForm mForm1;
myForm mForm2;
private void btnSomething_Click(object sender, EventArgs e)
{
ShowOrUpdateForm<myForm>(ref mForm1);
}
void ShowOrUpdateForm<T>(ref Form form) where T : Form
{
if (form == null)
{
form = new T();
form.FormClosed += new FormClosedEventHandler(mForm1_FormClosed);
form.Show();
}
else if (form.WindowState == FormWindowState.Minimized)
form.WindowState = FormWindowState.Normal;
form.Focus();
}
void mForm1_FormClosed(object sender, FormClosedEventArgs e)
{
// you cannot refactor this easily
if (sender == mForm1)
mForm1 = null;
else if (sender == mForm2)
mForm2 = null;
}
If you want to further generalize the closed event handler, you should consider moving forms' declarations to some sort of an array, list of dictionary. That way you can easily generalize that method.
EDIT: converted the ShowOrUpdateForm function to generic.

Is there a way to show a "blocking" WinForms ContextMenu?

Is there a way to show a ContextMenu and block further execution until an item has been selected? In particular, I want to get behavior similar to ShowDialog() but for a ContextMenu.
The straight forward approach doesn't work:
ContextMenu cm = new ContextMenu();
cm.MenuItems.Add("1", (s,e) => {value = 1;});
cm.Show(control, location);
since the Click callback isn't called directly from Show() but instead at some later point when the message loop processes the click event.
If you are unlucky, menu is garbage collected before the event is processed and in that case the event is just silently lost. (Meaning you can't really use local variables for ContextMenus in this way.)
This seems to work, but feels "unclean":
using (ContextMenu cm = new ContextMenu()) {
cm.MenuItems.Add("1", (s,e) => {value = 1;});
cm.Show(control, location);
Application.DoEvents();
}
Is there a better way?
Sorry for the first answer. Here is what I've tried. I made another Form where I put the context menu and a timer.Form2 is displayed as modal from Form1 then the timer shows the context menu on Form2.
Note that Form 2 has some properties set : to not be visible in task bar, not have boarders and the size should be equal with the size of the context menu.
Hope this helps.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
Form2 ctxForm = new Form2();
ctxForm.Location = this.PointToScreen(e.Location);
ctxForm.Size = new Size(0, 0);
ctxForm.ShowDialog();
}
}
}
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
}
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
{
this.Close();
}
private void timer1_Tick(object sender, EventArgs e)
{
//show menu once
contextMenuStrip1.Show(this, PointToClient(Location));
contextMenuStrip1.Focus();
timer1.Enabled = false;
}
private void contextMenuStrip1_Closed(object sender, ToolStripDropDownClosedEventArgs e)
{
this.Close();
}
}
You can easily prevent the garbage collection of the ContextMenu whilst it is still being shown.
The problem is that you are using a lambda as the event handler for the menu item. This is an
anonymous method and so not itself attached to any object instance that would cause the ContextMenu to be referenced and so kept alive. Add a method to the enclosing object and then create a standard EventHandler. That way the existence of the enclosing instance will keep the ContextMenu alive. Not as concise and very C# 1.0 but it will solve the problem.
Just wait for the menu to not be visiable.
ContextMenu cm = new ContextMenu();
cm.MenuItems.Add("1", (s,e) => {value = 1;});
cm.Show(control, location);
while (cm.Visible == true) Application.DoEvents();

How to focus a control in an MDIParent when all child windows have closed?

I have a control in my parent mdi form which I wish to be given the focus if all mdi children have closed. I've tried hooking into the child form's FormClosed event and setting the focus from there but when I test it, my control is not left with focus when I close the mdi child.
Can anyone tell me what I'm missing?
In the sample below, "first focus" gets hit and does its job correctly (if I comment out the first focus line, my tree does not get focus on startup, so it must be doing its job, right?)
Unfortunately, even though "second focus" gets hit, my tree does not end up with the focus when I close the child window.
Has focus on startup.
Has focus http://www.programmingforpeople.com/images/stackoverflow/focus1.PNG
Not focussed after closing child window.
No focus http://www.programmingforpeople.com/images/stackoverflow/focus2.PNG
Sample
using System;
using System.Windows.Forms;
namespace mdiFocus
{
class ParentForm : Form
{
public ParentForm()
{
IsMdiContainer = true;
tree = new TreeView();
tree.Nodes.Add("SomeNode");
tree.Dock = DockStyle.Left;
Controls.Add(tree);
}
protected override void OnShown(EventArgs e)
{
Form child = new Form();
child.MdiParent = this;
child.Show();
child.FormClosed += new FormClosedEventHandler(child_FormClosed);
tree.Focus(); // first focus works ok
}
void child_FormClosed(object sender, FormClosedEventArgs e)
{
tree.Focus(); // second focus doesn't seem to work, even though it is hit :(
}
TreeView tree;
}
static class Program
{
[STAThread]
static void Main()
{
Application.Run(new ParentForm());
}
}
}
I repro, smells like the WF logic that hunts for a focusable item is running after the event and messes up the focus. These kind of problems can be elegantly solved by running code after the event is processed by using Control.BeginInvoke(). This worked well:
private void toolStripButton1_Click(object sender, EventArgs e) {
Form child = new Form2();
child.MdiParent = this;
child.FormClosed += new FormClosedEventHandler(child_FormClosed);
child.Show();
}
void child_FormClosed(object sender, FormClosedEventArgs e) {
if (this.MdiChildren.Length == 1) {
this.BeginInvoke(new MethodInvoker(() => treeView1.Focus()));
}
}

Categories

Resources