My program is a CRM, I used Rad Ribbon Bar, so Many Buttons with images, RadGridView (which some columns contain images) and so many other controls which contain images. It's a mdi parent/child program.
In so many cases while loading a mdi child or working with some grid views the program will hang and give me this error:
OutOfMemoryException occurred in System.Drawing.dll
I tried GC.Collect() on certain parts but no success. For setting images there is no code! for example for setting an image for a button I used its properties in visual studio. I have set All other control images in this way using the properties panel in visual mode.
and These are some designer codes related to drawing:
btnCustomerList.Image = global::MyApp.Properties.Resources.CustomerList32;
gridViewCommandColumn1.Image = global::MyApp.Properties.Resources.ViewShop32;
and When The error comes after a while working with the app, it will appear in Program.cs and in the line Application.Run(new MainForm());:
static void Main()
{
AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", AppDomain.CurrentDomain.BaseDirectory + "\\Settings.config");
bool ok;
Mutex m = new Mutex(true, WindowsIdentity.GetCurrent().Name.ToString().Split('\\')[1] + "MyApp", out ok);
if (ok)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// The Error will cause HERE
Application.Run(new MainForm());
GC.KeepAlive(m);
}
else
Application.Exit();
}
MainForm is the mdi parent which contains Ribbon Bar.
and this is the Full stack trace:
at System.Drawing.Image.FromHbitmap(IntPtr hbitmap, IntPtr hpalette)
at System.Drawing.Image.FromHbitmap(IntPtr hbitmap)
at System.Drawing.Icon.ToBitmap()
at System.Windows.Forms.ThreadExceptionDialog..ctor(Exception t)
at System.Windows.Forms.Application.ThreadContext.OnThreadException(Exception t)
at System.Windows.Forms.Control.WndProcException(Exception e)
at System.Windows.Forms.Control.ControlNativeWindow.OnThreadException(Exception e)
at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(IntPtr dwComponentID, Int32 reason, Int32 pvLoopData)
at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
at System.Windows.Forms.Application.Run(Form mainForm)
at MyApp.Program.Main() in d:\\MyApp\\Application\\MyApp\\Program.cs:line 36"
UPADTED:
the code for calling mdi-children by clicking on ribbon bar buttons is here:
private void btnCustomerList_Click(object sender, EventArgs e)
{
OpenForm(new FormCustomerList(), "Customer List");
}
private void btnCustomerRelated_Click(object sender, EventArgs e)
{
OpenForm(new FormCustomerRelated(), "Customer Related");
}
and Here is OpenForm method:
private void OpenForm(Form formType, string Caption)
{
foreach (Form nform in Application.OpenForms)
{
if (nform.GetType() == formType.GetType())
{
nform.Activate();
return;
}
}
this.MdiChildren.OfType<Form>().ToList().ForEach(x => x.Dispose());
GC.Collect();
Form form = formType;
form.MdiParent = this;
form.Dock = DockStyle.Fill;
form.Show();
this.Text = Caption;
}
and in every mdi child's form constructor, after InitializeComponent(); I wrote GC.Collect(); also. But as told in comments, the GDI objects in task manager will increase and increase till 10000 objects and then application will crash.
UPADTED: THE MOST ISSUE
It seems I have found the part which cause the most GDI objects. In every form there are some controls like textboxes, drop down list etc. I have set some rules for them, for example if user enter a textbox, its back color should be yellow and after leave it should be white again. So there is a main method which I call in form load to literate through all controls and find the target ones and add for example enter and leave events with the defined rules. something Like This:
private void FormCustomerList_Load(object sender, EventArgs e)
{
ClassCRMControls.AddEventHandler(this);
}
and inside ClassCRMControls class:
public static void AddEventHandler(Control parent)
{
foreach (Control c in parent.Controls)
{
if (c.GetType() == typeof(RadTextBox))
{
c.Enter += new EventHandler(ClassCRMControls.EnterEvent);
c.Leave += new EventHandler(ClassCRMControls.LeaveEvent);
}
else
AddEventHandler(c);
}
}
private static void EnterEvent(object sender, EventArgs e)
{
(sender as RadTextBox).TextBoxElement.TextBoxItem.BackColor = Color.FromArgb(255, 251, 147);
}
private static void LeaveEvent(object sender, EventArgs e)
{
(sender as RadTextBox).TextBoxElement.TextBoxItem.ResetValue(LightVisualElement.BackColorProperty, ValueResetFlags.Local);
}
I have found the source of the problem and it was the custom animated cursor I used for grids and other controls too. I initialize it like this:
this.Cursor = ClassObjects.CreateAnimatedCursor("C:\\aniCur.ani"));
Since I loaded this cursor from the file every time I used it in any way, more and more GDI Objects got created.
So I declared a public static cursor the main() of the respective form like this:
public static Cursor animCur = ClassObjects.CreateAnimatedCursor("C:\\aniCur.ani"));
and then whenever I need to use this cursor I just reference this object public static cursor from the form.
this.Cursor = MainForm.animCur;
That's It :)
How did I find it? I just try to remove (commenting) some codes which I suspected them, then I checked GDI objects in task manager. After some testing it became apparent that the endless loading of new cursor objects was causing the problem.
There can be multiple reasons for OutOfMemoryExceptions. I have discussed 6 of them in another question.
In this case, after the comments and the edit, it became clear that GDI issues occur as well. You can detect these issues by showing an additional column in task manager:
GDIView is a far better application for GDI Leak analysis, because it also tells you the type of the GDI handle that got lost. It also has absolute and relative counters, so you can see how many of them get lost during a particular action.
The number of GDI handles can be configured in Registry. Do not use that as a permanent solution. Instead, with the additional info from GDIView, find the piece of code that leaks the GDI object.
When you run into the limit of GDI handles, the application typically starts looking bad: things are not painted any more and you get black rectangles in some places. However, this behavior is not necessary. In OP's case, black rectangles were not part of the description.
Related
I have a borderless form and I use the AnimateWindow() method to create animations for opening, closing, etc my Form. I use this code:
[Flags]
enum AnimateWindowFlags
{
AW_HOR_POSITIVE = 0x0000000
AW_HOR_NEGATIVE = 0x00000002,
AW_VER_POSITIVE = 0x00000004,
AW_VER_NEGATIVE = 0x00000008,
AW_CENTER = 0x00000010,
AW_HIDE = 0x00010000,
AW_ACTIVATE = 0x00020000,
AW_SLIDE = 0x00040000,
AW_BLEND = 0x00080000
}
[DllImport("user32.dll")]
static extern bool AnimateWindow(IntPtr hWnd, int time, AnimateWindowFlags flags);
When it comes to closing the form, this code seems to work:
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
AnimateWindow(this.Handle, 100, AnimateWindowFlags.AW_BLEND | AnimateWindowFlags.AW_HIDE);
}
However, when opening the form with this code:
private void Form1_Load(object sender, EventArgs e)
{
AnimateWindow(this.Handle, 100, AnimateWindowFlags.AW_BLEND);
}
Nothing seems to happen. After doing some guesses and tests I figured that using the AnimateWindow() method to make the form fade out works, but using it to make the form fade in doesn't do anything (regardless of the time parameter).
Any ideas?
This is pretty difficult to do correctly, there is an enormous amount of code involved that is very tricky to reason through. The Visible property, set by the Application class for the startup form and the Show() method when you create your own is a very big deal in Winforms. The native window creation is lazy in typical .NET fashion, lots and lots of stuff happens when the ball gets rolling.
The AnimateWindow() call must be injected in between the time the Show() method is called and Winforms gets a chance to pinvoke ShowWindow(). It is the latter call that ruins the animation effect when you try it in OnLoad(), the event fires too late.
You can try this code, paste it into your Form class:
protected override void SetVisibleCore(bool value) {
if (!this.IsHandleCreated) {
NativeMethods.AnimateWindow(this.Handle, 100, AnimateWindowFlags.AW_BLEND);
}
base.SetVisibleCore(value);
}
protected override void OnShown(EventArgs e) {
this.BringToFront();
base.OnShown(e);
}
But I cannot promise it will work in all possible cases and have not tested it extensively. Having to call BringToFront() was already an unpleasant hack. Don't try it on an MDI child form, not likely to come to a good end.
I'm trying to pick an object in Revit when clicking a button without closing the Form.
the problem is when i click the button i can't interact with Revit.
here's the main code calling the Form and passing Revit as owner.
IWin32Window revit_window = new JtWindowHandle(ComponentManager.ApplicationWindow);
Process process = Process.GetCurrentProcess();
IntPtr h = process.MainWindowHandle;
form.ShowDialog(revit_window);
public class JtWindowHandle : IWin32Window
{
IntPtr _hwnd;
public JtWindowHandle(IntPtr h)
{
Debug.Assert(IntPtr.Zero != h, "expected non-null window handle");
_hwnd = h;
}
public IntPtr Handle
{
get
{
return _hwnd;
}
}
}
and here is the Form code to select the element:
private void button1_Click(object sender, EventArgs e)
{
Hide();
SelectionFilter1 selfilter1 = new SelectionFilter1();
pickedRef1 = sel.PickObject(ObjectType.Element, selfilter1, "Select Family instance");
Show();
}
Your Windows form is presumably not running as a modal form within a valid Revit API context.
Consequently, you are trying to access Revit and its API from outside. This is basically not possible. A workaround exists via the use of an external event.
This issue is currently also being discussed in the Revit API discussion forum thread on Revit API with WPF.
The official approach is presented in the Revit SDK sample ModelessDialog/ModelessForm_ExternalEvent.
Many other discussions and soutions are listed by The Building Coder in the topic group on Idling and External Events for Modeless Access and Driving Revit from Outside.
I have a form with a DataGridView and I want to set the columns AutoSizeMode to Fill and the grids ColumnHeadersHeightSizeMode to AutoSize. My problem is that if the mouse cursor accidentally hovers the upper left cell of the grid when the form loads, the application throws an InvalidOperationException.
This is what I should see when the form loads:
(Note how the cursor is hovering the upper left cell).
This code will provoke the exception:
static class Program
{
[STAThread]
static void Main()
{
// Make sure the mouse will hover upper left cell when the form loads:
var form = new MyForm { StartPosition = FormStartPosition.Manual };
form.SetDesktopLocation(Cursor.Position.X - 30, Cursor.Position.Y - 40);
Application.Run(form);
}
class MyForm : Form
{
public MyForm()
{
var grid = new DataGridView { Dock = DockStyle.Fill };
grid.Columns.Add("ColumnName", "HeaderText");
// The form will load if I remove one of the two next lines:
grid.Columns[0].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
grid.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
Controls.Add(grid);
}
}
}
In my configuration Visual Studio swallows the exception, so I have to run the application from Windows Explorer or command prompt to see the error.
This is the stacktrace:
System.InvalidOperationException: This operation cannot be performed while an auto-filled column is being resized.
at System.Windows.Forms.DataGridView.PerformLayoutPrivate(Boolean useRowShortcut, Boolean computeVisibleRows, Boolean invalidInAdjustFillingColumns, Boolean repositionEditingControl)
at System.Windows.Forms.DataGridView.SetColumnHeadersHeightInternal(Int32 columnHeadersHeight, Boolean invalidInAdjustFillingColumns)
at System.Windows.Forms.DataGridView.AutoResizeColumnHeadersHeight(Boolean fixedRowHeadersWidth, Boolean fixedColumnsWidth)
at System.Windows.Forms.DataGridView.OnColumnHeadersGlobalAutoSize()
at System.Windows.Forms.DataGridView.set_TopLeftHeaderCell(DataGridViewHeaderCell value)
at System.Windows.Forms.DataGridView.get_TopLeftHeaderCell()
at System.Windows.Forms.DataGridView.GetCellInternal(Int32 columnIndex, Int32 rowIndex)
at System.Windows.Forms.DataGridView.OnCellMouseEnter(DataGridViewCellEventArgs e)
at System.Windows.Forms.DataGridView.UpdateMouseEnteredCell(HitTestInfo hti, MouseEventArgs e)
at System.Windows.Forms.DataGridView.OnColumnWidthChanged(DataGridViewColumnEventArgs e)
at System.Windows.Forms.DataGridView.OnBandThicknessChanged(DataGridViewBand dataGridViewBand)
at System.Windows.Forms.DataGridViewBand.set_ThicknessInternal(Int32 value)
at System.Windows.Forms.DataGridView.AdjustFillingColumns()
at System.Windows.Forms.DataGridView.ComputeLayout()
at System.Windows.Forms.DataGridView.PerformLayoutPrivate(Boolean useRowShortcut, Boolean computeVisibleRows, Boolean invalidInAdjustFillingColumns, Boolean repositionEditingControl)
at System.Windows.Forms.DataGridView.OnHandleCreated(EventArgs e)
at System.Windows.Forms.Control.WmCreate(Message& m)
at System.Windows.Forms.Control.WndProc(Message& m)
at System.Windows.Forms.DataGridView.WndProc(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
Two questions targets the same issue: Here and here, but the application still crashes when I apply the suggested answers.
Am I breaking some kind of best practice in the provided example?
Has anyone come across this behavior before and know a workaround?
This seems to be a bug - the code is trying to access dataGridView.TopLeftHeaderCell, which when happens for the first time actually creates that cell and triggers some layout actions not expected at that moment.
With all that in mind, the fix is simple. We need to make sure that the TopLeftHeaderCell is created before DataGridView handle, by adding the following line (before addding the grid to Controls for instance)
var topLeftHeaderCell = grid.TopLeftHeaderCell; // Make sure TopLeftHeaderCell is created
Thank you, Ulf, for the excellent sample showing how to reproduce this. One of my clients reported this bug to me and your sample has been invaluable.
Taking Ivan's excellent answer one step further, creating your own grid inheriting from the DataGridView should prevent this ridiculous bug permanently. Just be sure to always use the custom grid throughout your application.
public class Grid
: DataGridView
{
protected override void OnHandleCreated(EventArgs e)
{
// Touching the TopLeftHeaderCell here prevents
// System.InvalidOperationException:
// This operation cannot be performed while
// an auto-filled column is being resized.
var topLeftHeaderCell = TopLeftHeaderCell;
base.OnHandleCreated(e);
}
}
In .net core (3.1.9) and .net5.0 this is not reproducible. There was no directly fix, but somehow there are no exception any more. See (and post if you still have this issue): https://github.com/dotnet/winforms/issues/1830.
You issue is related with the fact that main thread draw interface.
Actually you handle drawing (starting position) before application run is launched and before layout operation (controls) are all completly initialize inside suspend layout block.
Changing your approach solve the issue (no need to change your code, only add some pieces).
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MyForm());
}
public class MyForm : Form
{
public MyForm()
{
this.InitializeComponents();
}
private void MyForm_Shown(object sender, EventArgs e)
{
this.SetDesktopLocation(Cursor.Position.X - 30, Cursor.Position.Y - 40);
}
private void InitializeComponents()
{
this.SuspendLayout();
this.StartPosition = FormStartPosition.Manual ;
var grid = new DataGridView { Dock = DockStyle.Fill };
grid.Columns.Add("ColumnName", "HeaderText");
// The form will load if I remove one of the two next lines:
grid.Columns[0].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
grid.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
Controls.Add(grid);
this.Shown += new System.EventHandler(this.MyForm_Shown);
this.ResumeLayout(false);
}
}
}
It is not like anything i have seen before.
When i call (new System.Windows.Forms.Form()).ShowDialog() a form shows for a millisecond or something and then vanishes.
I traced the calls and got This:
System.Windows.Forms.Form.Dispose
System.ComponentModel.Component.Dispose
System.Windows.Forms.Application.ThreadWindows.Dispose
System.Windows.Forms.Application.ThreadContext.DisposeThreadWindows
System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop
System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner
System.Windows.Forms.Application.ThreadContext.RunMessageLoop
System.Windows.Forms.Form.ShowDialog Esfand.Program.Main C#
I have tried anything that comes to mind to fix this.
Although I have showed a login form before trying to show this form.
I don't think there is anything special going on the login form(usual boring stuff, connect to server, send credentials, receive data).
I'm using main thread for forms. I have experience with Messaging and message loops. and i have used threads in the login form.
EDIT:
Clarification for what Cody Gray suggested:
This is what I have in void Main(string[]):
LoginForm login = new LoginForm ();
login.ShowDialog ();//works
if (login.DialogResult == DialogResult.OK)
{
MainForm f = new MainForm ();
f.ShowDialog ();//won't work
}
Creating and Showing the MainForm in a new thread made everything to just start working again.but random errors occur on each form that makes this solution not good enough.
EDIT 2:
The FormClosing event doesn't even trig.
System.Windows.Forms.Form A;
A = new Form();
A.FormClosing += new FormClosingEventHandler((sender, e) => { System.Diagnostics.Debugger.Break();/*won't work. tried Breakpoints and other stuff too*/ });
A.ShowDialog();
EDIT 3:
The HandleDestroyed event stack trace:
> Esfand.exe!Esfand.Program.Main.AnonymousMethod__1(object sender = {System.Windows.Forms.Form}, System.EventArgs e = {System.EventArgs}) Line 50 + 0x6 bytes C#
System.Windows.Forms.dll!System.Windows.Forms.Control.OnHandleDestroyed(System.EventArgs e) + 0x9e bytes
System.Windows.Forms.dll!System.Windows.Forms.Form.OnHandleDestroyed(System.EventArgs e) + 0x13 bytes
System.Windows.Forms.dll!System.Windows.Forms.Control.WmDestroy(ref System.Windows.Forms.Message m) + 0x54 bytes
System.Windows.Forms.dll!System.Windows.Forms.Control.WndProc(ref System.Windows.Forms.Message m) + 0x547 bytes
System.Windows.Forms.dll!System.Windows.Forms.Form.WndProc(ref System.Windows.Forms.Message m) + 0x6d bytes
System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.DebuggableCallback(System.IntPtr hWnd, int msg = 2, System.IntPtr wparam, System.IntPtr lparam) + 0x15e bytes
[Native to Managed Transition]
[Managed to Native Transition]
System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.DestroyHandle() + 0xf7 bytes
System.Windows.Forms.dll!System.Windows.Forms.Control.DestroyHandle() + 0x3e3 bytes
System.Windows.Forms.dll!System.Windows.Forms.Control.Dispose(bool disposing) + 0x347 bytes
System.Windows.Forms.dll!System.Windows.Forms.ContainerControl.Dispose(bool disposing) + 0x19 bytes
System.Windows.Forms.dll!System.Windows.Forms.Form.Dispose(bool disposing) + 0x26a bytes
System.dll!System.ComponentModel.Component.Dispose() + 0x1b bytes
System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadWindows.Dispose() + 0xb3 bytes
System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.DisposeThreadWindows() + 0x12d bytes
System.Windows.Forms.dll!System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(System.IntPtr dwComponentID, int reason, int pvLoopData) + 0x58e bytes
System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(int reason = 4, System.Windows.Forms.ApplicationContext context = {System.Windows.Forms.Application.ModalApplicationContext}) + 0x593 bytes
System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.RunMessageLoop(int reason, System.Windows.Forms.ApplicationContext context) + 0x81 bytes
System.Windows.Forms.dll!System.Windows.Forms.Form.ShowDialog(System.Windows.Forms.IWin32Window owner) + 0x765 bytes
Esfand.exe!Esfand.Program.Main(string[] a = {string[0]}) Line 51 + 0x10 bytes C#
this thing is making every single form in my program raising a unique error(e.g. `cannot register drag&drop event handler')
That's a strong hint to the core problem in your code. The RegisterDragDrop() native function will be called when you have any control whose AllowDrop property is set to true. It is called when the native window for your form is created. Unfortunately that is a very bad time for any exception to be raised if you have the 64-bit version of Windows and you forced your program to run in 32-bit mode. The subject of this answer.
There are very few good reasons for RegisterDragDrop() to fail. But one. We can already tell from your snippet that you've been tinkering with Program.cs. The boilerplate version of it looks like this:
static class Program {
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread] // <=== Here!!!
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
I put a Big Arrow at the line of code that matters. The [STAThread] attribute is essential in any GUI program. It tells the CLR that it should initialize COM and configure the main thread of your program to be a Single Threaded Apartment. Apartments are a very important COM implementation detail whose scope is a bit beyond this answer. If the attribute is missing then the main thread of your program joins the MTA, the multithreaded apartment. A hostile place for code that is not thread-safe, like drag and drop, the clipboard and the shell dialogs.
Forgetting to use the attribute can cause bewildering exceptions. Especially bad when your dev machine boots the 64-bit version of Vista or Win7, Windows versions that have trouble with exceptions that are raised at critical moments, as explained in the linked answer.
A proper version of your Program.Main() method that doesn't have this problem and otherwise uses recommended practices:
[STAThread] // <=== Don't forget this
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
using (var login = new LoginForm()) {
if (login.ShowDialog() != DialogResult.OK) return;
}
Application.Run(new MainForm());
}
Try to check if a thread exception error is being thrown. Check if you see anything in the Application_ThreadException event.
static void Main()
{
Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
try
{
//Your existing code
}
catch (Exception ex)
{
}
}
private static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
{
}
EDIT:
Another option would be to explicitly set the MainForm as the owner to the newForm being created.
newForm.ShowDialog(MainForm);
I have a feeling that the owner is being set to your Login form by default which was closed and this is auto-closing your new form
It looks like IMsoComponentManager.FPushMessageLoopP() will call Application.ThreadContext.DisposeThreadWindows() when there is a WM_QUIT message on the event queue.
Are you posting a quit message by any chance in your LoginForm's button event handlers?
Try to define the form as a class member. not inside a function.
Your Main method looks weird. I think you are missing a call to Application.Run(). Since you don't want the application to quit as soon as the login form is closed you might need an ApplicationContext. MSDN has an example: http://msdn.microsoft.com/en-us/library/ms157901.aspx
Another possiblity is to call Application.Run() with an invisible form as parameter which then shows the other forms (and is never closed until the application should exit), but this is a quite ugly hack in my opinion.
My understanding is that forms need to be run within an application context.
I inherited some code that launches several forms from the Main context, in the following manner:
var form1 = Form1();
Application.Run(form1);
// form returns, check public method form.ButtonPushed etc
if (form.Button1Pushed)
{
var form2 = Form2();
Application.Run(form2);
}
This would successfully launch several forms.
It doesn't feel like a very elegant way of doing things but it works...
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.