Brendan Lynn
Brendan Lynn

Reputation: 468

Run Form simultaneously with Program

I have a class that attempts to create an better alternative to the System.Console class. It started as follows:

public class SuperConsole
{
    private readonly Form _Form;
    public SuperConsole()
    {
        _Form = new()
        {
            Text = DefaultTitle,
            BackColor = DefaultBackColor,
        };
        Application.Run(_Form);
    }
    public string Title
    {
        get => _Form.Invoke(() => _Form.Text);
        set => _Form.Invoke(() => _Form.Text = value);
    }
    //more stuff
}

Obviously, if I ran the constructor, it would freeze on Application.Run. The form would run, of course, but the caller of the constructor would have to wait a bit. So, I changed the constructor to run Application.Run on a new thread:

public SuperConsole()
{
    _Form = new()
    {
        Text = DefaultTitle,
        BackColor = DefaultBackColor,
    };
    Thread t = new(() => Application.Run(_Form));
    t.Start();
}

But this throws a System.InvalidOperationException.

System.InvalidOperationException: 'Invoke or BeginInvoke cannot be called on a control until the window handle has been created.'

I have no idea how to proceed. I have tried several variations to this, to no effect.

I would like both the form and the calling function to be able to run simultaneously, and be able to still use Form.Invoke normally. I also would seek to have the user of the class not have to worry about any of this. How might I do this?

Upvotes: 0

Views: 133

Answers (2)

Brendan Lynn
Brendan Lynn

Reputation: 468

I found the following solution, similar to user @TheodorZoulias's hypothesis.

public SuperConsole()
{
    _Form = new()
    {
        Text = DefaultTitle,
        BackColor = DefaultBackColor,
    };
    ManualResetEventSlim mres = new();
    _Form.HandleCreated += HandleCreated;
    Thread t = new(() => Application.Run(_Form));
    t.Start();
    mres.Wait();
    void HandleCreated(object? Sender, EventArgs E)
    {
        mres.Set();
        _Form.HandleCreated -= HandleCreated;
    }
}

Special thanks to user @TheodorZoulias for pointing out why my last attempt was suboptimal. I borrowed his idea of using the ManualResetEventSlim class and the Form.HandleCreated event.

However, the form does not require being constructed on the same thread as on which the Application.Run function is called.

Upvotes: -1

Theodor Zoulias
Theodor Zoulias

Reputation: 43384

I think that you have to create the Form on the dedicated SuperConsole thread, not on the current thread, because UI components are thread-affine. You also have to declare the SuperConsole thread as STA, and wait until the form is created. Something like this should work:

public class SuperConsole
{
    private Form _form;

    public SuperConsole()
    {
        ManualResetEventSlim mres = new();
        Thread t = new(() =>
        {
            _form = new()
            {
                Text = DefaultTitle,
                BackColor = DefaultBackColor,
            };
            mres.Set();
            Application.Run(_form);
        });
        t.Name = "SuperConsole";
        t.SetApartmentState(ApartmentState.STA);
        t.Start();
        mres.Wait();
    }

    public string Title
    {
        get => _form.Invoke(() => _Form.Text);
        set => _form.Invoke(() => _Form.Text = value);
    }
}

The ManualResetEventSlim is used for signaling that the Form instance has been created, and assigned to the _form field. Otherwise the current thread could observe the _form to be null.

I haven't tested the above code. It is possible that the form might not be ready for calling Invoke before the message loop has started. In that case you may have to move the mres.Set(); inside the HandleCreated or Load or Shown event of the form. To learn how to subscribe to an event for one notification only, see this answer.

Upvotes: 4

Related Questions