开发者

How to properly initialize a Windows Forms application

I have written this sample program - simplifying a very complex application. The same binary [.exe] throws a null pointer exception on some machines on startup. So, I want to know how to properly construct a Windows Forms form.

In our application the Form1_SizeChanged method is a result of this.ResumeLayout(false) method which is the last statement in InitializeComponents(). I don't know to simulate that, so I just changed the size myself for this test program.

public partial class Form1 : Form
{
    public class Logger {
        public Logger() { }
        public void log(string str) {
            Console.WriteLine("logging - " + str);
        }
    }

    Logger logger = null;

    public Form1()
    {
        InitializeComponent();
        this.Size = new Size(200, 300);
        MyInitialize();
    }

    private void MyInitialize() {

        // Just that it takes some time.
        Console.WriteLine("MyInitialize -- Enter");
        for (int count = 0; count <5 ; count++){
            Console.WriteLine("Sleeping -- " + count);
            Thread.Sleep(1000);
        }
        logger = new Logger();
    }

   开发者_如何学编程 private void sleepingPill(int millisec) {
        Thread.Sleep(millisec);
    }

    private void Form1_SizeChanged(object sender, EventArgs e)
    {
        logger.log("Form1_SizeChanged -- Enter");
    }
}

According to my understanding, Form1_SizeChanged should not be called unless Form1 is constructed properly. Can someone throw some light on how do the Windows Forms event architecture work in this scenario?


Original Stack Trace: from our complex application
System.NullReferenceException was unhandled
  Message=Object reference not set to an instance of an object.
  Source=ABCD
  StackTrace:
       at ABCD.Form1.AppendToLog(String s)
       at ABCD.Form1.Form1_SizeChanged(Object sender, EventArgs e)
       at System.Windows.Forms.Control.OnSizeChanged(EventArgs e)
       at System.Windows.Forms.Control.UpdateBounds(Int32 x, Int32 y, Int32 width, Int32 height, Int32 clientWidth, Int32 clientHeight)
       at System.Windows.Forms.Control.UpdateBounds(Int32 x, Int32 y, Int32 width, Int32 height)
       at System.Windows.Forms.Control.SetBoundsCore(Int32 x, Int32 y, Int32 width, Int32 height, BoundsSpecified specified)
       at System.Windows.Forms.Form.SetBoundsCore(Int32 x, Int32 y, Int32 width, Int32 height, BoundsSpecified specified)
       at System.Windows.Forms.Control.ScaleControl(SizeF factor, BoundsSpecified specified)
       at System.Windows.Forms.ScrollableControl.ScaleControl(SizeF factor, BoundsSpecified specified)
       at System.Windows.Forms.Form.ScaleControl(SizeF factor, BoundsSpecified specified)
       at System.Windows.Forms.Control.ScaleControl(SizeF includedFactor, SizeF excludedFactor, Control requestingControl)
       at System.Windows.Forms.ContainerControl.Scale(SizeF includedFactor, SizeF excludedFactor, Control requestingControl)
       at System.Windows.Forms.ContainerControl.PerformAutoScale(Boolean includedBounds, Boolean excludedBounds)
       at System.Windows.Forms.ContainerControl.PerformNeededAutoScaleOnLayout()
       at System.Windows.Forms.ContainerControl.OnLayoutResuming(Boolean performLayout)
       at System.Windows.Forms.Control.ResumeLayout(Boolean performLayout)
       at ABCD.Form1.InitializeComponent()
       at ABCD.Form1..ctor()
       at ABCD.Program.Main()
  InnerException:

Notice from the stack trace: Form1_sizeChanged is called from InitializeComponents() .. which I think should not happen. Form1_sizeChanged is an instance method of Form1 class and should not be called before the Form1 is constructed. If the .NET environment want to process this event, it should wait till Form1 is constructed properly, shouldn't it?


In line two of the constructor, you set the Size. This will inturn call Form1_SizeChanged() before the logger is created. Move the setting of the Size after the call to MyInitialize.


Persumably, Form1_SizeChanged is being called before MyInitialize and hence before logger is initialised. Change that Method to

   if (logger != null)
       logger.log("Form1_SizeChanged -- Enter");


I would guess that you are getting your null error on the SizeChanged event/

Your InitializeComponent() routine sets up the Form1_SizeChanged event to be mapped to the Form's SizeChanged event. Your this.Size code will fire said event. Since logger.log does not get initialized until your MyInitialize routine, you may see sporadic results.

Basically Windows will queue up the Form1_SizeChanged event and your app will respond to it asynchronously, so sometimes it might respond to it after the Logger() gets initialized and sometimes it will respond before the Logger gets initialized.

@Karephul: The reason you are seeing this behavior is that the message loop is at the application level, not the individual class level. A GUI window is a class like anything else and will receive events from the message loop as soon as the message loop can send them to the class and doesn't necessarily wait for the constructor to be finished.


You answered the question yourself in your question. It all has to do with the ResumeLayout() call.

The auto-generated "InitializeComponent()" code turns off layout, assembles the components, and then turns layout back on. This, I suppose, is an optimization because recalculating layout on every sub component addition could get slow. When layout gets turned back on, the resize events of lots of components will likely get called, including yours.

Note that the forms code plays fast and loose with what can get called from the constructor - calling all kinds of virtual methods that absolutely shouldn't get called until after the constructor is finished.

The fix is to initialize anything that must have a fully constructed object in the Load event. The Load event only gets called once and gets called after the instance is fully constructed. For you this means removing the Form1_SizeChanged event from what the development environment manages and add a Form1_Load event that includes

private void Form1_Load(object sender, EventArgs e)
{
    this.Load += Form1_SizeChanged;
}

Yes it is a pain. Yes, you are right and Forms is wrong. At least the work around isn't too onerous.

Oh, I'm not done. I've heard that Move events can occur before the Load event but after the constructor is finished so if you need all Move events for some reason, you may need to just do defensive coding in the Move event handler like sgmoore showed.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜