Archives

Archives / 2005 / November
  • 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(528);
                
    _treeView.TabIndex 1;
                
    _treeView.AfterSelect += new TreeViewEventHandler(OnTreeViewAfterSelect);
                
    _treeView.BeforeSelect += new TreeViewCancelEventHandler(OnTreeViewBeforeSelect);
                
                
    // splitter
                
    _splitter.Location = new Point(12628);
                
    _splitter.TabIndex 1;
                
    _splitter.TabStop = false;
                
                
    // label1
                
    _label.BackColor SystemColors.Window;
                
    _label.BorderStyle BorderStyle.Fixed3D;
                
    _label.Dock DockStyle.Fill;
                
    _label.Location = new Point(12928);
                
    _label.TabIndex 2;
                
                
    // button1
                
    _someButton.Dock DockStyle.Top;
                
    _someButton.Location = new Point(55);
                
    _someButton.TabIndex 0;
                
                
    // Form 
                
    ClientSize = new Size(500400);
                
    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.