Practical Programming Pearls For .NET Developers

 (How)
C^# You Are - 20 July 2008

This week's questions relate to WinForms.

  1. How do you draw an etched line to separate controls on a form?  Answer

    Surprisingly there is no built in control for this. The simplest solution that is generally recommended is to use a Label control that is 2 pixels tall. You will need to set AutoSize to false, clear any text in the label and set the border style to Fixed3D.  Here's an example.

    Label label1 = new System.Windows.Forms.Label();
    label1.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D;
    label1.Size = new System.Drawing.Size(295, 2);

    The Visual Basic Power Pack was released several years ago and added a few controls from VB to the mix.  VS2008 SP1 adds the controls to VS rather than requiring an extra download.  It still requires that you deploy the pack in addition to the framework.  The pack include a couple of shape controls including the line control.  The downside to the line control in the pack is that it does not support an etched line, only different styles of line.  Of course you could just write your own as creating such a control is pretty trivial. 

  2. You have associated a context menu strip with a treeview via the ContextMenuStrip property?  Some of the menu items should only be enabled if certain conditions are met.  How can you modify the menu to handle these conditions?  Answer

    A ContextMenuStrip raises the Opening and Opened events when it is displayed.  If you want to modify the contents of the context menu before it is displayed then you should handle the Opening event.  The following code disables the copy and cut menu items if no nodes are selected.  It disables the paste menu item if there is nothing in the clipboard.

    // in InitializeComponent
    this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.miCopy, this.miCut, this.miPaste}); this.contextMenuStrip1.Name = "contextMenuStrip1";
    this.contextMenuStrip1.Size = new System.Drawing.Size(153, 92);
    this.contextMenuStrip1.Opening += new System.ComponentModel.CancelEventHandler(this.OnContextMenuOpening);

    private void OnContextMenuOpening ( object sender, CancelEventArgs e )
    {
       miCopy.Enabled = (treeView1.SelectedNode != null);
       miCut.Enabled = (treeView1.SelectedNode == null);

       miPaste.Enabled = Clipboard.ContainsText();
    }

  3. You have added a DataGridView to a form.  You have elected to define custom columns rather than using the data source that you intend to bind to the control.  After you've defined all your columns you set the DataSource property to your data source and run the program.  You see that the grid has automatically added additional columns.  How can you prevent this?  Answer

    This is a flaw in the DGV.  The DGV automatically inserts the columns of the data source into the grid when the binding occurs.  This can be prevented by setting the AutoGenerateColumns to false.  The problem is that this property is hidden in the property grid for an unknown reason and the UI editor doesn't expose it either.  You are therefore in a difficuult situation. 

    The easiest solution is to never use the UI to bind the grid to a data source unless you want the auto generated columns.  If you don't want the columns then set AutoGenerateColumns to false after InitializeComponent but before you bind the control.  Perhaps Microsoft can someday explain the flawed logic of this design.  Given that it has not been fixed since its introduction in v2.0 we can only assume that it is by design.

  4. You have added a DataGridView to a form.  You want to test the grid with some bogus data before you hook it up to your business layer.  You add an untyped dataset to your project, define a table and some columns.  In the constructor for your form you add some data to the table.  In the designer you associate the dataset with the grid.  When you debug your form nothing shows up.  What is going on?  Answer

    For whatever reason the designer does not completely hook up the data source to the grid.  While the designer will associate a data source with the grid it does not handle sources that contain "child" sources.  A dataset contains such child sources as it is primarily a collection of tables.  You need to explicitly associate the table of interest with the grid via the DataMember property via the Property window.  Your data will then show up properly.

  5. You are building a custom message box for displaying errors.  You want to be able to display the error message and allow the user to copy the message (for sending to technical support).  You initially used a Label to display the text but your users cannot seem to be able to select the text.  How can you resolve this issue?  Answer

    The static label does not allow selection, and hence copying, of the text.  If you want to display static text that can be selected then you need to use something else, like a TextBox.  Unfortunately a text box contains functionality not suitable for static text so you need to modify the control to make it look and act more like a label.  The following code does just that.

    textBox1.BorderStyle = System.Windows.Forms.BorderStyle.None;
    textBox1.ReadOnly = true;

  6. You are feeling archiac so you decide to display the time in the status bar of your application just like days of old.  How do you update the time?  Answer

    Just like in the old days you should update your time when the application is idle.  For a WinForms app Application.Idle is raised whenever an application is idle.  In Windows an application is considered idle if the message queue is empty.  You should not assume that the idle event is raised with any sort of frequency.  Instead you should update the display based upon the current time like so.

    protected override void OnLoad ( EventArgs e )
    {
       base.OnLoad(e);

       Application.Idle += OnIdle;
    }

    private void OnIdle ( object sender, EventArgs e )
    {
       pnlTime.Text = DateTime.Now.ToLongTimeString();
    }

    An alternative is to set a timer to go off every second.  A timer message is still not completely accurate as Windows will generate the appropriate message, and hence you'll get the event, only when no other higher priority messages are in the queue.  Therefore you should still not assume the interval of the events.  Here's an example.

    tmrClock.Enabled = true;
    tmrClock.Interval = 1000;
    tmrClock.Tick += new System.EventHandler(this.OnIdle);