Vistritium
Vistritium

Reputation: 26

How to recreate windows form application when main window is closed

I have windows forms app. When it's closed the main window is disposed and then when user click on tray window is recreated - it works. However I have werid problem when I try to bring application back when using FileSystemWatcher. Idea is simple, when file is changed application is brought back. However in this case application appears but hangs and then dissapears. The shape of window comes back but is unresponsive, moving mouse on window shows like app is "thinking" or "hanging", the app doesn't have icon on taskbar. My guess is that this is connected with threads/synchronization but I have no idea how to make it work again. I tried many different things connected with threading but failed. I cannot create this window again in UI thread because as I understand I can write _mainWindow.BeginInvoke but I can't do that before I create this form. I have created the minimal working example that demonstrates the issue. It is available at https://gitlab.com/virtual92/getting-forms-up or here:

Program.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace GettingFormsUp
{
    static class Program
    {
        private static bool hideFlag = true;

        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            var initApplicationContext = new InitApplicationContext();

            Console.WriteLine();
            var fileSystemWatcher = new FileSystemWatcher(Path.GetFullPath("../.."), "watcher");
            fileSystemWatcher.Changed += (sender, args) =>
            {
                Console.WriteLine("Watched");
                initApplicationContext.Show();
            };
            fileSystemWatcher.EnableRaisingEvents = true;

            Application.Run(initApplicationContext);
        }

        private class InitApplicationContext : ApplicationContext
        {
            private static MainWindow _mainWindow;

            public InitApplicationContext()
            {
                NewForm();
            }

            private void NewForm()
            {
                Console.WriteLine("Creating new MainWindow");
                _mainWindow = new MainWindow();
                _mainWindow.Invoke((MethodInvoker) delegate
                {
                    _mainWindow.Show(); 
                });
            }

            public void Show()
            {
                if (_mainWindow == null || _mainWindow.IsDisposed)
                {
                    NewForm();
                }
                else if (!_mainWindow.Visible)
                {
                    _mainWindow.BeginInvoke((MethodInvoker) delegate
                    {
                        Console.WriteLine("showing");
                        _mainWindow.Show();
                    });
                }
            }

            public void Delete()
            {
                if (_mainWindow != null && !_mainWindow.IsDisposed)
                {
                    _mainWindow.Dispose();
                }
            }
        }
    }
}

MainWindow.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace GettingFormsUp
{
    public class MainWindow : Form
    {
        public MainWindow()
        {
            CreateHandle();
            InitializeComponent();
        }

        private System.ComponentModel.IContainer components = null;
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }
        private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.Text = "Form1";
        }
    }
}

How can I make it work?

Upvotes: 2

Views: 506

Answers (1)

Peter Duniho
Peter Duniho

Reputation: 70652

The problem with your code is that when you create the window again, it's being created in the thread used to raise the FileSystemWatcher.Changed event, which is a background thread, not the thread that the Application.Run() method is using to pump window messages. So that background thread winds up owning the window.

Messages for the window are sent to the thread that owns it, but that thread doesn't have a message pumping loop, so the window never sees the messages. Those messages are critical, as they are what handle everything about the interaction with the window, both user input and everything involved in drawing the window (except for the bare minimum, which the Windows desktop manager handles). Those messages are even used to handle things like calls to Control.Invoke() and Control.BeginInvoke(). Without the message-pumping loop, BeginInvoke() delegates will never be handled, and Invoke() will never even return.

There are a variety of ways to address this. Simply not disposing the window in the first place could be an option (you can override OnFormClosing(), canceling the event and hiding the window instead). Then the call to _mainWindow.Invoke() will always go to the correct thread and work like you expect:

public partial class MainWindow : Form
{
    // ...

    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        Visible = false;
        e.Cancel = true;

        base.OnFormClosing(e);
    }

    // ...
}

Alternatively, you can capture the main thread's SynchronizationContext object, which can be used to do the same thing as Control.Invoke()/BeginInvoke(). The key to that technique is that until you've created a Winforms component in a thread, that thread won't have a SynchronizationContext. In your code, the form is created when you create the InitApplicationContext object, so capturing the SynchronizationContext after that will work:

static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    var initApplicationContext = new InitApplicationContext();
    SynchronizationContext context = SynchronizationContext.Current;

    Console.WriteLine();
    string path = Path.GetFullPath("../..");
    Console.WriteLine($"Watching {path}");
    var fileSystemWatcher = new FileSystemWatcher(path, "watcher");
    fileSystemWatcher.Changed += (sender, args) =>
    {
        context.Post(o =>
        {
            Console.WriteLine("Watched");
            initApplicationContext.Show();
        }, null);
    };
    fileSystemWatcher.EnableRaisingEvents = true;

    Application.Run(initApplicationContext);
}

If you take this approach, then of course you don't need the call to Control.Invoke() when creating the window. It's superfluous the first time anyway, and because you'll be using SynchronizationContext from the FileSystemWatcher.Changed event handler in the subsequent instances, it's also unneeded then:

private void NewForm()
{
    Console.WriteLine("Creating new MainWindow");
    _mainWindow = new MainWindow();
    _mainWindow.Show();
}

Upvotes: 2

Related Questions