Suggestions for making your managed dialogs snappier

Diagnosing Performance Problems with Layout

 

So you’ve built up your complex form, and starting it or resizing it is slightly more exciting than watching paint dry. It could very well be that the way everything is hooked up, the dialog is fighting layout rather than working with it. If you’re not familiar with what layout is and how it works, read this first.

 

The first question to ask is, is the dialog just performing layout way too many times? The best way to determine this is to add in some Debug.WriteLines and then analyze the output to see why layout is occurring and if you could prevent it by batching up some property sets under a SuspendLayout/ResumeLayout.

 

       public Form1() {

            InitializeComponent();

            SnapAllLayout(this);

        }

 

        private void SnapAllLayout(Control start) {

 

            start.Layout += new LayoutEventHandler(snap_Layout);

            foreach (Control c in start.Controls) {

                SnapAllLayout(c);

            }

        }

 

        void snap_Layout(object sender, LayoutEventArgs e){

                Control c = sender as Control;

                System.Diagnostics.Debug.WriteLine(String.Format("Control: {0}\r\nBounds: {1}\r\nReason {2}\r\n Where {3}",

                    c.Name,

                    c.Bounds,

                    e.AffectedProperty,

                    new System.Diagnostics.StackTrace().ToString()));

 

         }

 

         void Form1_Layout(object sender, LayoutEventArgs e) {

            MessageBox.Show("Form1_Layout!");

         }

 

         void Panel1_Layout(object sender, LayoutEventArgs e) {

            MessageBox.Show("Panel1_Layout!");

         }

A note about SuspendLayout and ResumeLayout

 

SuspendLayout and ResumeLayout only prevent OnLayout from being called. Additionally they only prevent OnLayout from being called for that particular control. So if you have a Form with a Panel in it, and call SuspendLayout on the Form, the Panel's layout is not suspended.

 

       // simple example showing how suspending on the parent does

       // not suspend the child control

       private void button1_Click(object sender, EventArgs e) {

            this.Layout += new LayoutEventHandler(Form1_Layout);

            panel1.Layout += new LayoutEventHandler(Panel1_Layout);

       

            // Test one - calling PerformLayout here does not call Form1_Layout

            this.SuspendLayout();

            this.PerformLayout();

            this.ResumeLayout(false);

 

            // Test two - calling PerformLayout here calls Panel1_Layout

            // Child controls are not suspended when the parent is suspended.

          this.SuspendLayout();

            panel1.PerformLayout();

            this.ResumeLayout(false);

 

            // Test three, properly suspending layout

            this.SuspendLayout();

            panel1.SuspendLayout();

            panel1.PerformLayout(); // <--- Layout event on Panel NOT called

            panel1.ResumeLayout(false);

            this.ResumeLayout(false);

 

            panel1.Layout -= new LayoutEventHandler(Panel1_Layout);

            this.Layout -= new LayoutEventHandler(Form1_Layout);

        

        }

 

What properties/methods cause layout?

 

The "reasons" for layout that are passed to LayoutEventArgs.AffectedProperty usually match the property name that has been set.

 

Additionally, changing the control collection can cause a layout. Methods that do this include but are not limited to:

       control.Controls.Add(..)

       control.Controls.Clear()

       control.Controls.Remove and RemoveAt

       control.BringToFront()

       control.SendToBack()

       control.Controls.SetChildIndex

       control.Parent

 

Inefficient layout practices

Changing the UI in OnLoad

Problem: Changing properties such as Bounds, Size, Location, Visible and Text/Image/etc for AutoSized controls InitializeComponent and/or Suspend/ResumeLayout can cause perf problems. Each time one of these properties are touched a layout is forced. Changing any of these in Form.Load is particularly bad, at that point the handles have all been created, and window messages have to be sent to change size/locations.

 

Solution: Add a Suspend/Resume Layout to prevent extra layouts from occurring. If possible, make all of these changes within InitializeComponent – this way only one layout is ever needed.

 

Visual Inheritance

Problem: The code generated for visual inheritance (class Form2 : Form1) is actually not that efficient, as the constructor for the base form executes, then the constructor for the derived form executes. This essentially forces two layouts as ResumeLayout is called on the form twice. Visual Inheritance also has problems for localization, as the localization tools don’t work well with this kind of situation.

 

Solution: For performance reasons, if you can avoid using it, this is the best solution. If you must, you may want to consider using a base form and swapping in a panel with the derived controls as needed.

 

Setting the Size/Location of child controls in the Resize event

Problem: Using the Resize event to size/position child controls circumvents the Suspend/Resume Layout perf protection.

 

Solution: Use the Layout event to size/position child controls.

 

       // simple example to show the difference between using

// the Resize event versus using the Layout event

        private void button1_Click(object sender, EventArgs e) {

 

            // Inefficient Layout practice

            this.Resize += new System.EventHandler(this.Form1_Resize);

            this.SuspendLayout();

            this.Size = new Size(500, 500); // SuspendLayout circumvented!

            this.ResumeLayout(false);

            this.Resize -= new System.EventHandler(this.Form1_Resize);

 

            // Efficent Layout pracitce

            this.Layout += new LayoutEventHandler(Form1_Layout);

            this.SuspendLayout();

            this.Size = new Size(500, 500);

            this.ResumeLayout(false);

            this.Layout -= new LayoutEventHandler(Form1_Layout);

 

        

        }

 

        void Form1_Layout(object sender, LayoutEventArgs e) {

            // for added performance, consider storing off the last size

            // that happened when we got here and only perform the layout if

     // the size has changed.

            Rectangle bounds = this.ClientRectangle;

            bounds.Inflate(-10, -10);

            outerPanel.Bounds = bounds;

            this.Text = "Resized in LAYOUT!";

        }

 

        private void Form1_Resize(object sender, EventArgs e) {

            Rectangle bounds = this.ClientRectangle;

            bounds.Inflate(-10, -10);

            outerPanel.Bounds = bounds;

            this.Text = "Resized on RESIZE!";

        }

 

 

Dynamically filling in data

Problem: Changing the text of child controls can cause layouts.

 

Solution: Call SuspendLayout on the parent control first, then set the text of all the controls, then call ResumeLayout. If this can be done before the handles are created, it will be even faster.

 

Setting the Size/Location of a control in multiple property sets

Problem: There are several properties in which you can change the size/location of a control. These include, but are not limited to: Width, Height, Top, Bottom, Left, Right, Size, Location, Bounds, and ClientSize. Setting panel1.Width = 10 and panel1.Height = 10 causes twice the work to occur than setting them both together (especially after the handle has been created as windows forms is chatting live with the operating system about what the size should really be.)

 

Solution: Do all your calculations, then set the property that reflects the most information you have. If you're just chaning the Size, set Size, if you're changing the Size and Location change Bounds.

 

       // simple example to show performance difference between

// setting width and height individually versus setting

// size

        private void button1_Click(object sender, EventArgs e) {

            Stopwatch sw = new Stopwatch();

 

            sw.Start();

  for (int i = 1; i < 1000; i++) {

                panel1.Width = i;

                panel1.Height = i;

            }

            sw.Stop();

 

            Stopwatch sw2 = new Stopwatch();

            sw2.Start();

            for (int i = 1; i < 1000; i++) {

                panel1.Size = new Size(i, i);

            }

            sw2.Stop();

            MessageBox.Show(String.Format("Trial 1 {0}\r\nTrial 2 {1}", sw.ElapsedMilliseconds, sw2.ElapsedMilliseconds));

        }

 

       // If you run this, the second one takes about 1/2 the time because

       // there's half the conversation going on.

Summing up

These are suggestions that have helped folks solve their dialog problems in the past. As always measure first, then fix, then measure again. If you are tuning your application, check out these articles:

Writing High-Performance Managed Applications : A Primer

Writing Faster Managed Code: Know What Things Cost

Garbage Collector Basics and Performance Hints

How To: Use CLR Profiler

How, When, Where and Why to use Dispose