Printer Friendly Version      Send     
Click to Rate and Give Feedback
Related Articles

Ken Getz shows how the CollectionChanged event lets you reflect changes to your underlying data source in your bound data controls.

Ken Getz

MSDN Magazine December 2008

...

Read more!

See how you can streamline your team projects using Process Templates in Visual Studio Team System (VSTS)

Brian A. Randell

MSDN Magazine December 2008

...

Read more!

John Papa tackles questions about calling services from Silverlight 2 applications.

John Papa

MSDN Magazine November 2008

...

Read more!

The heart of Windows Workflow Foundation is its declarative programming model. Here are some best practices to consider when using WF to realize software solutions in the real world.

Josh Lane

MSDN Magazine December 2008

...

Read more!

Getting the performance you want in concurrent applications is not as straightforward as you might think. See how common threading issues can affect your application.

Erika Fuentes and Eric Eilebrecht

MSDN Magazine December 2008

...

Read more!

Also by this Author

Although most developers and APIs use the RGB scheme when working with colors, it's not the only available way to represent or select colors. For instance, the standard Windows color-selection dialog box allows you to work with the HSL color scheme in an indirect way. In this article, the author describes several color selection schemes, and uses GDI+ (via the System.Drawing namespace) to create a component that makes it possible for your own applications to provide a simpler, friendlier color chooser. Along the way, you'll get tips to help you ...

Read more!

Ispeak at a lot of user groups and conferences where I field tech support questions. Recently, a conference attendee (I'll call him Adam) came up to me with a sheaf of printouts, along with the following question.

Ken Getz

MSDN Magazine September 2004

...

Read more!

Ken Getz prepares Visual Basic developers to use RibbonX.

Ken Getz

MSDN Magazine June 2007

...

Read more!

This month I found an interesting question in my mailbag. It went something like this: "I am writing some code working with points and other drawing objects in Visual Basic® 2003, and I just want to add an offset to a point, effectively moving the point.

Ken Getz

MSDN Magazine December 2004

...

Read more!

This month Advanced Basics flaunts the power of generics and reflection and shows how you get more flexible and efficient development by combining the two.

Ken Getz

MSDN Magazine January 2008

...

Read more!

Popular Articles

Learn how to automate custom SharePoint application deployments, use the SharePoint API, and avoid the hassle of custom site definitions.

E. Wilansky, P. Olszewski, and R. Sneddon

MSDN Magazine May 2008

...

Read more!

Jeff Prosise explains when it's better to use UpdatePanel and when it's better to use asynchronous calls to WebMethods or page methods instead.

Jeff Prosise

MSDN Magazine June 2007

...

Read more!

One-time passwords offer solutions to dictionary attacks, phishing, interception, and lots of other security breaches. Here's how it all works.

Dan Griffin

MSDN Magazine May 2008

...

Read more!

The MVP pattern helps you separate your logic and keep your UI layer free of clutter. This month learn how.

Jean-Paul Boodhoo

MSDN Magazine August 2006

...

Read more!

When incorporating the ASP.NET DataGrid control into your Web apps, common operations such as paging, sorting, editing, and deleting data require more effort than you might like to expend. But all that is about to change. The GridView control--the successor to the DataGrid-- extends the DataGrid's functionality it in a number of ways. First, it fully supports data source components and can automatically handle data operations, such as paging, sorting, and editing, as long as its bound data source object supports these capabilities. In addition, ...

Read more!

Our Blog

Windows Presentation Foundation (WPF) adds functionality to the Microsoft .NET Framework so that you actually can reliably keep bound controls synchronized with their data sources.

In the December 2008 issue of MSDN Magazine, Ken Getz demonstrates how to use the ObservableCollection class provided by WPF to keep bound controls in ...

Read more!

Every month, the CLR team gives us insight into the core of managed code, .NET programming best practices, technologies underlying the CLR and .NET Framework, and other tips and suggestions.

In the December 2008 issue of MSDN Magazine, Erika Fuentes and Eric Eilebrecht cover some common issues developers encounter when tuning ...

Read more!

So many factors can affect the performance of a Web page—the distance between server and client, the size of the elements on the page, how the browser loads these elements, available bandwidth. Finding those bottlenecks and identifying the culprits is no easy task.

In the November 2008 issue of MSDN Magazine, Jim Pierson introduces ...

Read more!

It’s helpful to think about secure design from a more holistic perspective by using threat models to drive your security engineering process.

In the November 2008 issue of MSDN Magazine, Michael Howard proposes using the threat model to help drive other SDL security requirements, primarily code review priority, fuzz testing priority, ...

Read more!

Windows Workflow Foundation (WF) imposes some restrictions on the developer authoring programs that target it. But in return WF offers a powerful, flexible, and extensible set of runtime services such as support for long-running code.

In the December 2008 issue of MSDN Magazine, Josh Lane provides some best practices to consider ...

Read more!

Advanced Basics
TableLayoutPanels
Ken Getz

Code download available at: AdvancedBasics2006_12.exe (169 KB)
Browse the Code Online
I've been writing quite a bit of courseware lately, and many of the demos for the courses require the presenter to step through a number of simple procedures, demonstrating various features of Visual Studio® or the Microsoft® .NET Framework. For example, I recently worked on a course in which the demos ran as console applications. The students saw a menu with lettered prompts, and by pressing a letter (A through Z), the demo ran the appropriate sample procedure.
On a long plane trip home one day, I crafted a simple menuing system that allowed the courseware author (generally, me) to supply a list of objects containing prompts and a delegate instance that indicates the procedure to run when the user selects the item. The code lays out the menu, even breaking the list into two columns if it gets too tall. It worked flawlessly, and I never thought about how to display a demo's menu again.
Now that I'm back to creating Windows®-based applications for demos, I thought it might be nice to have the same sort of self-generating demo system in place. The current course (and almost every conference talk I give) requires a large number of demo applications, and not having to lay out the main switchboard form for each application can save time. It's a lot simpler to modify a single line of code than it is to manage laying out the main form.
With this goal, I put together a demo-creation system for Windows-based applications and it's the basis for this month's column. Figure 1 shows sample code that can be used to generate the switchboard in Figure 2.
Public Sub Main()
  Dim menu As New DemoMenu
  menu.Add("Work with Strings", AddressOf WorkWithStrings)
  menu.Add("String Methods", AddressOf StringMethods)
  menu.Add("StringBuilder Class", AddressOf StringBuilderClass)
  menu.Add("Dates and Times", Address_Of DateAndTimes)
  menu.Add("DateTimeFormatInfo", _
           AddressOf DateTimeFormatInfoClass)

  menu.Add("", Nothing)

  menu.Add("TimeSpan", AddressOf TimeSpanDemo)
  menu.Add("StopWatch Class", AddressOf StopwatchDemo)

  menu.DemoFont = New Font("Verdana", 8, FontStyle.Regular)
  menu.MaxItemsInSingleColumn = 10

  menu.DisplayMenu("Demo Menu")
End Sub
Figure 2 Menu 
If you want your menu to display its items in more than one column, you can change the MaxItemsInSingleColumn property of the DemoMenu class. For example, if you change the property value to 4 (its default value is 10), the same form shown previously displays its buttons as you see in Figure 3.
Figure 3 Menu Items in Columns 
For use in courseware, I include the DemoMenu project along with the course samples, and have each demo refer to the compiled DemoMenu assembly in a known location. You can deploy this assembly as you deploy any other shared assembly-but that's a topic for a different column.
The DemoMenu class creates an internal list of objects that maintain the prompt text and the address of the procedure to be called by each button click. The code creates a new form, lays out the buttons, and hooks up the Click event handlers. My first attempt at writing this code (on the Boston-to-Minneapolis leg of my trip back to the West Coast) simply created buttons on a form. I used an internal offset value to space the controls on the form, and I didn't add support for multiple columns. During the layover, it occurred to me that multiple columns would be nice, and as soon as I got back on the plane again, I realized that laying the controls out manually was too much effort. The answer was obvious-the TableLayoutPanel control, new in the Microsoft® .NET Framework 2.0, makes this sort of thing relatively simple.

Using the TableLayoutPanel Control
The TableLayoutPanel control is the most advanced, and the most complex, of the set of container controls that you'll find in Windows Forms 2.0. (This set also includes the Panel, SplitContainer, TabControl, and FlowLayoutPanel controls.) This control allows you to arrange other controls in a grid-like fashion, with a fixed number of rows and columns. This control is richer than I can begin to demonstrate here, and its capabilities far outstrip the needs of the simple switchboard form. In this example, the TableLayoutPanel control provides one important function: it creates neatly spaced, multi-columned cells in which the code can place buttons. The code doesn't need to worry about aligning cells, or laying out the spacing-the TableLayoutPanel control does that formatting. (You may find it useful, before digging into this column, to create a simple form with a TableLayoutPanel control on it. Try working with the various user interface features of the control. Add rows and columns, place controls within cells, set the height of rows and the widths of columns, and try out the various sizing options.)
Because the sample project doesn't load its own form as it starts up (it calls the DemoMenu class's DisplayMenu procedure to display the main menu), the project doesn't use a startup form, as do most Visual Basic®-based applications. Instead, this application starts from a Sub named Main. In order to make this possible, the sample project, TestDemoMenu, disables the standard application framework, and sets the Startup object option to Sub Main, as shown in Figure 4.
Figure 4 Disable the App Framework to Start from Sub Main (Click the image for a larger view)
The sample application includes a reference to the DemoMenu project, which includes the DemoMenu class. In order to use the DemoMenu class, you start by creating an instance, and then adding items to the menu by specifying text for each button, along with an instance of the EventHandler delegate to be called by the menu when you click each button:
Dim menu As New DemoMenu
menu.Add("Work with Strings", AddressOf WorkWithStrings)
menu.Add("String Methods", AddressOf StringMethods)
' and so on...
If you want to insert an empty space within the list of demo buttons, you can add an empty item:
menu.Add("", Nothing)
(If you'd like, you can also create your own List of DemoItem objects, and assign this list to the MenuItems property of the DemoMenu instance. Most of the time, however, it's easiest to just call the Add method to create each individual item.)
You can also set any of the public properties of the DemoMenu class, including the Font, MaxItemsInSingleColumn, Text (of the menu form), or Form (this allows you to supply your own form on which to place the buttons, which you'll need to do before you call the DemoMenu.DisplayMenu method). The sample project includes this code to complete laying out the form before it calls the DisplayMenu method (setting the form's Text property in its call to DisplayMenu):
menu.Font = New Font("Verdana", 8, FontStyle.Regular)
menu.MaxItemsInSingleColumn = 4
menu.DisplayMenu("Demo Menu")
The DemoItem class describes the contents of each demo item, as shown here:
Public Class DemoItem
  Public ItemText As String
  Public ItemProc As EventHandler

  Friend Sub New(ByVal ItemText As String, _
      ByVal ItemProc As EventHandler)
    Me.ItemText = ItemText
    Me.ItemProc = ItemProc
  End Sub
End Class
Note that the DemoItem.ItemProc property contains an EventHandler delegate instance. This delegate type corresponds to the type required for the Button class's Click event handler. If you attempt to set any other type of delegate for the ItemProc property, your code won't compile-that's the point.
The DemoMenu class includes two private variables, along with two public fields:
Private Shared formCount As Integer = 0
Private frm As Form

Public MenuItems As New List(Of DemoItem)
Public MaxItemsInSingleColumn As Integer = 10
The shared formCount variable allows the class to provide unique behavior for the first instance of this class within a running application-you'll see more about this when you investigate the code in the class. The MenuItems variable contains the list of menu items (you can set this directly, if you like), and the variable frm maintains a reference to the form containing the buttons. Finally, the public MaxItemsInSingleColumn field allows the caller to override the default behavior, which, as you saw, allows up to 10 items in a vertical column on the form.
The class's constructor simply sets up the default form:
Public Sub New()
  ' Set up the default form
  frm = New Form
  frm.Font = New Font("Verdana", 10)
  frm.Text = "Main Menu"
  frm.FormBorderStyle = FormBorderStyle.FixedDialog
  frm.MaximizeBox = False
  frm.MinimizeBox = False
End Sub
The Add method provides support for adding menu items to the data structure that maintains the menu items:
Public Sub Add(ByVal itemText As String, ByVal itemProc As EventHandler)
  Dim item As New DemoItem(itemText, itemProc)
  MenuItems.Add(item)
End Sub
In addition, the DemoMenu class provides three simple property procedures for the Form, Font, and Text properties, for getting and setting the form reference, as well as for properties of the default form.
In the class, you'll find an override for the DisplayMenu procedure that accepts a caption for the form, and then calls the full DisplayMenu procedure:
Public Sub DisplayMenu(ByVal Text As String)
  Me.Text = Text
  DisplayMenu()
End Sub

Helper Procedures
Before examining the main DisplayMenu procedure, it's important to understand how each of its support procedures works. The CalculateDimensions procedure calculates how to break the demo items into multiple columns, attempting to minimize the number of columns given the value for MaxItemsInSingleColumn. The procedure (shown in Figure 5) allows a maximum of five columns-if it can't fit the number of demo items you've created into five columns, it simply raises an exception and quits. (You'll know the first time you run the demo if it's going to fail, at which time you can rethink the number of items to display on the main menu. Note that you can have a button on the menu display another dynamically generated menu, so the column limitation isn't terribly onerous.)
Private Function CalculateDimensions(ByVal totalCount As Integer) As Size
  ' Calculate the dimensions of the TableLayoutPanel control:
  Dim rows As Integer = totalCount
  Dim cols As Integer = 1

  ' If you can fit all the demos in a single column, you're all set.
  If totalCount > MaxItemsInSingleColumn Then
    cols = CInt(Math.Ceiling(totalCount / MaxItemsInSingleColumn))
    If cols > 5 Then
      ' Won't fit, so get out!
      Throw New InvalidOperationException("You have too many items!")
    End If
    rows = CInt(Math.Ceiling(totalCount / cols))
  End If
  Return New Size(cols, rows)
End Function
The CalculateDimensions procedure is passed an integer containing the total number of items to be displayed. It checks to see if the total number is less than the largest number per column and if so you're finished.
Once the procedure determines that it must calculate the number of columns, it looks for numbers of rows and columns that fit the number of items it must display, minimizing the number of columns. If it is unable to find an appropriate combination of rows and columns, it raises an exception back to the caller. The procedure completes by returning a new Size instance containing the numbers of rows and columns.
Given that the list of items is a one-dimensional array, but you may be working with buttons in a two-dimensional layout, much of the remainder of the code handles moving items into a set of two-dimensional data structures. The ArrangeItems procedure (see the code in Figure 6) takes items from the original list and returns a two-dimensional array of DemoItem instances, ready for use by other procedures.
Private Function ArrangeItems(ByVal rows As Integer, _
    ByVal cols As Integer) As DemoItem(,)

  ' Return array of DemoItem instances that matches
  ' the layout of the form:
  Dim items(cols - 1, rows - 1) As DemoItem

  ' Fill in the items array:
  Dim currentItem As Integer = 0
  For col As Integer = 0 To cols - 1
    For row As Integer = 0 To rows - 1
      If currentItem < MenuItems.Count Then
        items(col, row) = MenuItems(currentItem)
        currentItem += 1
      End If
    Next
  Next
  Return items
End Function
The ArrangeItems procedure starts by creating the output array. Note that because the buttons descend columns first, all the array access uses columns first (unlike most array processing, which is row by column). The code keeps track of the current item, loops through all the columns and rows, and copies references from the original List of items into the new array. As it's looping, the code checks to make sure it hasn't run out of items. Finally, the procedure returns the new array.
In order to make sure the buttons are wide enough to display their text, the code must calculate the size of the text on the buttons in the font of the form. In addition, the DemoMenu class attempts to allow different columns of buttons to have different widths. To make this possible, the CalcSizes procedure (shown in Figure 7) creates an array of SizeF structures, one for each column in the form.
Private Function CalcSizes( _
    ByVal rows As Integer, ByVal cols As Integer, _
    ByVal items As DemoItem(,)) As SizeF()

  ' Calculate the width of each column:
  Dim sizes(cols - 1) As SizeF

  ' Calculate the width of each column individually.
  ' Just keep track of the maximum width and height, 
  ' and use those values:
  Using g As Graphics = frm.CreateGraphics
    For col As Integer = 0 To cols - 1
      Dim maxSize As New SizeF(0, 0)
      For row As Integer = 0 To rows - 1
        ' Get the DemoItem object, and calculate
        ' its font metrics:
        Dim menuItem As DemoItem = items(col, row)
        If menuItem IsNot Nothing Then
          Dim fontSize As SizeF = _
            g.MeasureString(menuItem.ItemText, frm.Font)
          maxSize.Width = Math.Max(maxSize.Width, fontSize.Width)
          maxSize.Height = Math.Max(maxSize.Height, fontSize.Height)
        End If
      Next
      ' Use the maximum size you got for this column:
      sizes(col) = maxSize
    Next
  End Using
  Return sizes
End Function
The CalcSizes method first creates the output array. Next, the code asks the form to create a Graphics object to perform the font size calculations. The code loops through each column, initializing a temporary SizeF structure and then looping through each row in the column. Inside the loop, the code will calculate the size of the text for each button and store the maximum size required in each column. Once it's finished with each column, the code stores the maximum size in the array of sizes.
For each row, the code retrieves the menu item and calculates its size, using the form's font. The Graphics.MeasureString method does the work. Given the maximum size, the outer loop can store the SizeF structure for each column, for later use.
The FillTable procedure (see Figure 8) creates the actual buttons and hooks up their event handlers within the TableLayoutPanel control. This procedure loops through all the columns and rows, and if the item in the array of items isn't Nothing, checks the item's ItemText property. If it's not an empty string, the code creates a button; otherwise, it leaves blank the position within the TableLayoutPanel control. If the code determines that it needs to create a button, it creates the control, sets its properties, hooks up its event handler, and adds the control to the appropriate location within the TableLayoutPanel control.
Private Sub FillTable(ByVal items As DemoItem(,), _
    ByVal rows As Integer, ByVal cols As Integer, _
    ByVal tbl As TableLayoutPanel)

  For col As Integer = 0 To cols - 1
    For row As Integer = 0 To rows - 1
      Dim demo As DemoItem = items(col, row)
      If demo IsNot Nothing Then
        If Not String.IsNullOrEmpty(demo.ItemText) Then
          Dim btn As New Button
          btn.Text = demo.ItemText
          btn.Dock = DockStyle.Fill
          AddHandler btn.Click, demo.ItemProc
          tbl.Controls.Add(btn, col, row)
        End If
      End If
    Next
  Next
End Sub
The SetupPanel procedure creates the TableLayoutPanel control and sets its row heights and column widths (see Figure 9). It starts by creating TableLayoutPanel, docking it, setting the rows and columns, and clearing the default collections of row and column styles. In this control, it's the RowStyles and ColumnStyles properties that determine the behavior of the control. A row or column can have a fixed size, a percent size (that is, a percentage of the available space in the control), or it can be automatically sized (based on the contents of the row or column). For this app, the rows need to be of equal height and each column needs to be a distinct fixed width. In addition, the form must conform to the size of the TableLayoutPanel control's contents, so this procedure needs to calculate the size of the contents of the control.
Private Function SetupPanel( _
    ByVal rows As Integer, ByVal columns As Integer, _
    ByVal sizes As SizeF()) As TableLayoutPanel

  ' Create the TableLayoutPanel and set
  ' its properties:
  Dim tbl As New TableLayoutPanel
  tbl.Dock = DockStyle.Fill

  tbl.RowCount = rows
  tbl.ColumnCount = columns
  tbl.RowStyles.Clear()
  tbl.ColumnStyles.Clear()

  ' Keep track of the total width and height, 
  ' so you can set the form's size:
  Dim totalWidth As Single = 0
  Dim totalHeight As Single = 0

  ' Create variables for use within the loops:
  Dim height As Single
  Dim width As Single

  ' Set all rows to the same height.
  For i As Integer = 0 To rows - 1
    ' The 1.8 factor just gives a little space.
    ' It's an arbitrary value.
    height = CSng(Math.Ceiling(sizes(0).Height * 1.8))
    tbl.RowStyles.Add( _
      New RowStyle(SizeType.Absolute, height))
    totalHeight += height
  Next

  ' Set the width of each column appropriately:
  For i As Integer = 0 To columns - 1
    ' The 1.2 factor just gives a little space.
    ' It's an arbitrary value, and seems to work 
    ' with most font sizes:
    width = CSng(Math.Ceiling(sizes(i).Width * 1.2))
    tbl.ColumnStyles.Add( _
      New ColumnStyle(SizeType.Absolute, width))
    totalWidth += width
  Next

  ' Set up the form:
  frm.Controls.Clear()
  frm.Controls.Add(tbl)
  frm.ClientSize = _
    New Size(CInt(totalWidth), CInt(totalHeight))

  Return tbl
End Function
The code continues by setting up variables to track the total width and height of the table, along with variables to track the height and width of individual rows and columns. The code loops through all the rows, setting each to the height of the first row of text. Because all the buttons use the same font, they all fit within the same height. This code multiplies the height by a factor of 1.8, allowing room for space above and below the text on each button. Within the loop, the code sums up the heights and stores the values in the totalHeight variable.
The next block of code sets the width of each column, multiplied by an arbitrary factor of 1.2, giving room on either side of the text on the button. This code sums up the widths of all the columns in the totalWidth variable. The procedure finishes up by clearing any existing controls from the form, adding the TableLayoutPanel control to the form, setting the size of the form, and returning the TableLayoutPanel control to the caller.

The Display Menu Procedure
Given all these helper procedures, the DisplayMenu procedure doesn't have much work to do (see Figure 10). This procedure starts by calculating the dimensions of the table, calling the CalculateDimensions procedure shown earlier. The code continues by setting up temporary data structures containing the menu items in a two-dimensional structure and an array of column widths. The code creates, configures, and fills the TableLayoutPanel control. Finally, the code displays the form. If this is the first form in the application, the code must call the Application.Run method, but for further instances of the class the code simply needs to display the form. In order to make this possible, the code maintains a shared variable, formCount, which tracks the number of instances of the form that have been created. If the value is 1, the code calls Application.Run; otherwise, it calls the ShowDialog method of the form.
Public Sub DisplayMenu()
  ' Calculate the number of rows and columns:
  Dim tableSize As Size = CalculateDimensions(MenuItems.Count)
  Dim rows As Integer = tableSize.Height
  Dim cols As Integer = tableSize.Width

  ' Create temporary data structures to perform
  ' necessary calculations:
  Dim tempItems(,) As DemoItem = ArrangeItems(rows, cols)
  Dim tempSize() As SizeF = CalcSizes(rows, cols, tempItems)

  ' Set up the TableLayoutPanel control:
  Dim tbl As TableLayoutPanel = SetupPanel(rows, cols, tempSize)

  ' Fill in the TableLayoutPanel.
  FillTable(tempItems, rows, cols, tbl)

  ' Show the form
  formCount += 1
  If formCount = 1 Then
    Application.Run(frm)
  Else
    frm.ShowDialog()
  End If
End Sub
Using the TableLayoutPanel control in this way, the DemoMenu class can lay out its buttons in one or more columns without requiring you to perform exacting location calculations. You set the widths and heights of the columns and rows, and the TableLayoutPanel control does the rest. All you need to do is supply the list of DemoItem instances, either by creating the list yourself or (as shown here) calling the DemoMenu.Add method to add each item to the list. You supply the text of the button and the procedure you want to call when you click the button, and the DemoMenu class does the rest.
I can think of several ways in which you could extend this demonstration. You could support reading the menu items from a configuration file so that you could make changes to the application without recompiling it. You could add support for different control types besides buttons, and you could add behavior that would allow the TableLayoutPanel control to display within a container on the menu form. I'm sure I'll continue to enhance this functionality, so stay tuned. In the meantime, if I keep writing courseware and conference demos at the rate I've been going, this assembly will save me a ton of time. I'm sure it will save you time as well.

Send your questions and comments to basics@microsoft.com.


Ken Getz is a senior consultant with MCW Technologies, and a courseware author for AppDev (www.appdev.com). He is coauthor of ASP.NET Developers Jumpstart (Addison-Wesley, 2002), Access Developer's Handbook (Sybex, 2002), and VBA Developer's Handbook, 2nd Edition (Sybex, 2001). Reach him at keng@mcwtech.com.

Page view tracker