Why is my ListBox throwing an exception?

2019-08-25 18:45发布

问题:

Okay, I was able to create a simple Windows Forms project that reproduces some strange behavior I found. In the designer, make a form with a ListBox (named lbx) anchored Top, Left, Right, and Bottom, and a button (button1). Now, the Form's code is here:

using System;
using System.Windows.Forms;

namespace ListBoxKaboom
{
    public partial class Form1 : Form
    {
        private bool _initFinished = false;

        public Form1()
        {
            InitializeComponent();

            this._initFinished = true;

            this.Height += 100;
            this.Height -= 50;
            this.Height += 50;
        }

        private void lbx_SelectedIndexChanged(object sender, EventArgs e)
        {
            this.button1.Enabled = (this.lbx.SelectedItem != null);
        }

        protected override void OnLayout(LayoutEventArgs e)
        {
            if (_initFinished)
            {
                int lines = (this.lbx.Height - 4) / this.lbx.ItemHeight;

                this.SuspendLayout();
                while (lines < this.lbx.Items.Count)
                {
                    this.lbx.Items.RemoveAt(this.lbx.Items.Count - 1);
                }

                while (lines > this.lbx.Items.Count)
                {
                    this.lbx.Items.Add("Item " + (this.lbx.Items.Count + 1).ToString());
                }
                this.ResumeLayout();
            }

            base.OnLayout(e);
        }
    }
}

PLEASE NOTE THE FOLLOWING INSTRUCTIONS:

Run this, click any of the items in the list box, and use the arrow keys to move down far enough to cause the list box to scroll. Kaboom.

Exception (sometimes NullReferenceException and sometimes IndexOutOfBoundsException). Any ideas why? Also, I would think that the items would be in order, but they're not. Is this just a goofy corner case that didn't get handled properly by Windows Forms, or am I doing something wrong?

Stack trace:

at System.Windows.Forms.ListBox.NativeUpdateSelection()

at System.Windows.Forms.ListBox.SelectedObjectCollection.EnsureUpToDate()

at System.Windows.Forms.ListBox.SelectedObjectCollection.get_InnerArray()

at System.Windows.Forms.ListBox.SelectedObjectCollection.get_Item(Int32 index)

at System.Windows.Forms.ListBox.get_SelectedItem()

回答1:

I Copy/Pasted it to an empty Form and get a StackOverflow exception. Looking at it, with manipulation of the Items inside a Layout event I would say you deserve little better.

I realize this may be a simplification of something else, but there simply are limits to what you can do in a EDS.

My best guess: The ResumeLayout triggers a recursive layout operation. You could try to stem it with a sibling to _initFinished but I would suggest rethinking tour design here.

I copy/pasted wrong, my bad (used layout event).


Second try:
based on the two while-loops I would expect the Item strings to be in order, and no vertical scrollbar. It is clear that the listbox is confused, showing vertical scroll range and with the items out-of-order. So some 'error' is already present in the internals of the Listbox, waiting for a Scroll. I can also reproduce it with the mouse.

A workaround: You should be able to get the desired effect using the Resize event.

Attempt to an explanation: The (unmanaged part of the) Listbox gets confused by (multiple) Add/RemoveAt operations with suspended Layout. The last items are drawn at the wrong place, and the Listbox can't compute pixel-to-item.



回答2:

You should not manipulate any GUI elements in the constructor, e.g. this.Height += 100; in your example. Strange things can happen. I have been bitten by this several times in legacy code.

Wait until form load time - handle the base.Load event and do the height manipulation there.


From "When does Form.Load event get raised?":

Q: "... I need to find out basically what the difference is between putting code in Load event's handler, versus putting code in the Form's constructor after the InitializeComponents() line ..."

A: "The Load event is fired once the control/form has been fully initialized and has a window handle created. Therefore once this event has fired it is a fully usable user interface control. Remember that inside the constructor the actual window handle for the control/form has not yet been created, you are only creating C# objects here and inside the InitializeComponent call."