次の方法で共有


The evil WinForms Splitter

Beware of SplitPosition.
Today I spent quite some time debugging an issue in the new product I am working on.
Well, to summarize what I was seeing in our UI is that for some reason certain information that I was expecting to be there when a TreeNode was expanded, it just wasn’t there. It was completely surprising to me, since in that particular code path, we do not start multiple threads or use Application.DoEvents nor anything like that, basically all we do is a simple assignment in the TreeView after select event, something like:
private void OnTreeViewAfterSelect(object sender, TreeViewEventArgs e) {
_myObject = DoSomeProcessing();
}

However, for some reason in another event handler of our TreeView, _myObject was not set. How can this be?

Well, after quite some interesting time with VS 2005 (which rocks!), the problem was due to an interesting raise condition caused by (believe it or not) a WinForms Splitter. What was happening is that DoSomeProcessing changed some properties, that caused the UI to perform a layout and inside that code, we set the SplitterPosition property of the Splitter. Well, surprise-surprise, Splitter calls Application.DoEvents in its property setter!!!.
What DoEvents does is basically lets Windows pop the next message from the windows message pump and process it, so the next event was actually fired, and _myObject ended up not being set.

To illustrate the problem with a simple sample, try this code:
(Just copy the code and paste it into notepad.
Save it as TestApp.cs and compile it using “csc.exe /target:winexe TestApp.cs”
)

using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
namespace TestApp {
public class Form1 : Form {
private TreeView _treeView;
private Label _label;
private Splitter _splitter;
private Button _someButton;

[STAThread]
static void Main() {
Application.Run(new Form1());
}
public Form1() {
InitializeComponent();
// Just add some nodes...
TreeNode node = _treeView.Nodes.Add("Node 1");
node.Nodes.Add("Node 1.1");
node.Nodes.Add("Node 1.2");
_treeView.Nodes.Add("Node 2");
}
private void InitializeComponent() {
_treeView = new TreeView();
_splitter = new Splitter();
_label = new Label();
_someButton = new Button();
SuspendLayout();
// treeView1
_treeView.Dock = DockStyle.Left;
_treeView.Location = new Point(5, 28);
_treeView.TabIndex = 1;
_treeView.AfterSelect += new TreeViewEventHandler(OnTreeViewAfterSelect);
_treeView.BeforeSelect += new TreeViewCancelEventHandler(OnTreeViewBeforeSelect);

// splitter
_splitter.Location = new Point(126, 28);
_splitter.TabIndex = 1;
_splitter.TabStop = false;

// label1
_label.BackColor = SystemColors.Window;
_label.BorderStyle = BorderStyle.Fixed3D;
_label.Dock = DockStyle.Fill;
_label.Location = new Point(129, 28);
_label.TabIndex = 2;

// button1
_someButton.Dock = DockStyle.Top;
_someButton.Location = new Point(5, 5);
_someButton.TabIndex = 0;

// Form
ClientSize = new Size(500, 400);
Controls.Add(_label);
Controls.Add(_splitter);
Controls.Add(_treeView);
Controls.Add(_someButton);
ResumeLayout(false);
}
private void OnTreeViewAfterSelect(object sender, TreeViewEventArgs e) {
_label.Text = "Node selected:" + e.Node.Text;
}
private void OnTreeViewBeforeSelect(object sender, TreeViewCancelEventArgs e) {
// Just sleep 500ms to simulate some work
Thread.Sleep(500);
// Now update the SplitPosition
_splitter.SplitPosition = 100;
// simulate 500ms of more work ...
Thread.Sleep(500);
}
}
}

Colorized by: CarlosAg.CodeColorizer


Run it and select the TreeView, notice how ugly everything works.
Basically every time you select a different node you will get an ugly flickering, getting to see how selection jumps from the newly selected node to the last selected node, and then back to the new selected node.

Well, luckily in Visual Studio 2005, there is a new class called SplitContainer that simplifies everything.
It even adds new features, such as letting you set a MaxSize for both the left panel and the right panel, and many more features. Best of all, there is no Application.DoEvents in their code, so you can have code that behaves deterministically.
Bottom line, you do want to use SplitContainer if at all possible.

Comments