Reputation: 468
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
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
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