Table LayoutPanel Forms From a File

You can extend the technique shown in the previous example to create more a customizable user interface that's built out of a collection of TableLayoutPanel and FlowLayoutPanel objects. Doing so allows you to create more sophisticated, variable interfaces, like those that model business forms. The following example demonstrates one such example with a survey application.

In this application, the code reads XML tags that define the required interface. These tags are stored in a file, although you could easily modify the code so that it reads a block of XML from another source, like a database. The code then loops through the elements in the XML document, translating them into the appropriate controls. As the control objects are created, they're added to the form. This logic is fairly involved, but it's not nearly as messy as it would have been with .NET 1.x. That's because the code uses autosizing and layout panels to avoid dealing directly with control sizes and position. As long as the control is added to the right container, the entire form flows without a problem.

The basic structure of the form is made up of two pieces:

• A TableLayoutPanel with one column holds all the sections of the survey form. The TableLayoutPanel has a single column, with a width of 100 percent.

• Each section of the survey form is inserted as another panel (either a TableLayoutPanel or a FlowLayoutPanel depending on the type of content). This panel control is placed in a separate cell.

The top-level TableLayout is also placed into an ordinary Panel container. This extra step is one approach to add a border around the TableLayoutPanel without adding a border between cells. (Another approach would be to perform some custom drawing in response to the Form.Paint event.)

Figure 21-21 shows a sample survey form.

Sample Survey Forms
Figure 21-21. Generating a data entry form from a .frm file

The Form File

All forms are modeled as XML files with a root <Form> element. In the root element, you can add one of four different panels, depending on the type of survey content you need:

• <TextBoxPanel>. Rendered as a two-column table, with labels on the left and text boxes for data entry on the right. Each row requires a separate <TextItem> tag.

• <GroupSelectionPanel>. Rendered as a group of radio buttons, one for each item, with a caption at the top. Each radio button requires a separate <SelectionItem> tag.

• <CheckBoxListPanel>. Rendered as a CheckBoxList control, which shows multiple items in a list box, each of which can be checked or unchecked. Each item requires a separate <SelectionItem> tag.

• <LargeTextBoxPanel>. Rendered as one or more full-width multiple line text boxes, with a caption at the top. Each text box requires a separate <TextItem> tag.

The form code defines an enumeration that represents the four allowed types of panels:

enum PanelTypes {

TextBoxPanel, GroupSelectionPanel, CheckBoxListPanel, LargeTextBoxPanel

Here's the sample survey form used to create the user interface in Figure 21-21:

<?xml version="1.0"?> <Form> <TextBoxPanel> <TextItem id="FirstName" caption="First Name" /> <TextItem id="LastName" caption="Last Name" /> </TextBoxPanel>

<GroupSelectionPanel caption="Choose the option that best describes your job role"> <SelectionItem id="Programmer" /> <SelectionItem id="Developer" />

<SelectionItem id="TechSupport" caption="Technical Support" /> <SelectionItem id="NetworkAdmin" caption = "Network Administrator" /> <SelectionItem id="Other" /> </GroupSelectionPanel>

<CheckBoxListPanel caption="Choose all the activities you have performed recently."> <SelectionItem id="Program" /> <SelectionItem id="Test" /> <SelectionItem id="Debug" /> <SelectionItem id="Manage" /> </CheckBoxListPanel>

<LargeTextBoxPanel caption="Fill in any comments about this survey (optional).">

<TextItem id="Comments" /> </LargeTextBoxPanel> </Form>

The caption attributes define text that's rendered in the user interface. The id attributes define the unique names that will be used for the automatically generated controls. The id also will be used when saving the filled-out survey data. Notice that you don't need to supply the caption attribute for the <SelectionItem> element—if you don't, the caption is set to match the id.

The Form Parsing Code

In the application, the first step is to choose a survey file by clicking the Browse button. At this point, a dialog box is shown with all the available survey files (which are given a .frm extension). Once a file is selected, the work is handed off to the SurveyDeserializer class:

private void cmdBrowse_Click(object sender, EventArgs e) {

if (openFileDialog.ShowDialog() == DialogResult.OK) {

txtFileName.Text = openFileDialog.FileName; SurveyDeserializer surveyReader = new

SurveyDeserializer(openFileDialog.FileName, tableLayoutPanell); tableLayoutPanel1.SuspendLayout(); surveyReader.LoadForm(); tableLayoutPanel1.ResumeLayout();

In its constructor, the SurveyDeserializer stores the file name and layout panel information for future reference:

private string fileName; private Panel targetContainer;

public SurveyDeserializer(string fileName, Panel targetContainer) {

this.fileName = fileName; this.targetContainer = targetContainer;

Next, the SurveyDeserializer.LoadForm() method begins by disposing any controls that exist inside the survey panel.

public void LoadForm() {

// Clear the current table content.

foreach (Control ctrl in targetContainer.Controls) {

ctrl.Dispose();

Note that it's not enough to call the Panel.Controls.Clear() method. This removes the control objects from the Controls collection, but it doesn't release them. As a result, you'll tie up control handles every time you generate a new form and reduce system performance.

■Tip In some layout situations, you may use a panel to show one of a small set of different views. In this scenario, the most efficient way to deal with your controls is to simply hide them (or their container), rather than dispose and re-create them.

The next step is to read the survey file into an in-memory XmlDocument object. The code then iterates over the panel elements, checking to make sure that the type matches one of the values defined in the enumeration.

// Load the form into memory.

XmlDocument doc = new XmlDocument();

doc.Load(fileName);

// Iterate over panel nodes.

foreach (XmlNode nodePanel in doc.DocumentElement.ChildNodes)

// Convert the element name into the appropriate enum value.

PanelTypes type =

(PanelTypes)Enum.Parse(typeof(PanelTypes), nodePanel.LocalName);

// Check for caption node.

string caption = CheckForAttribute(nodePanel, "caption");

You'll notice that the code also makes use of a simple CheckForAttribute() method, which looks for an attribute with a specific name. If the attribute is found, CheckForAttribute() returns it value. If not, CheckForAttribute() returns an empty string.

The work of actually creating the corresponding container is handed off to another method, called CreateContainer(). It generates the container control that will hold the content for that survey element.

// Create the container for this survey element.

// It's placed into the next available cell.

Control container = CreateContainer(type, caption);

Finally, the code loops through all the tags in the container. These are the <SelectionItem> and <TextItem> elements that define specific text boxes, check boxes, or radio buttons. Each time it finds a nested element, the code extracts the relevant id and caption information, and passes it to another private method—CreateContent(). CreateContent() creates the required child control and inserts it in the container.

// Remember, when there is more than one level of container at work,

// you need to call SuspendLayout() on each level to get the

// performance benefits.

container.SuspendLayout();

// Iterate over the nested nodes.

foreach (XmlNode nodeltem in nodePanel.ChildNodes) {

// Get the node information. string id = CheckForAttribute(nodeItem, "id"); caption = CheckForAttribute(nodeItem, "caption"); if (caption.Length == 0) caption = id;

// Create the content inside the survey element. CreateContent(type, nodeltem.LocalName, caption, id, container);

container.ResumeLayout();

catch (Exception err) {

MessageBox.Show("Failure parsing file.\n" + err.Message);

Essentially, the LoadForm() method takes care of parsing the XML document. The CreateContainer() and CreateContent() perform the real work—generating the controls and inserting them into the current position in the survey table.

Every survey element is stored inside a nested TableLayoutPanel or FlowLayoutPanel. This is referred to as the top-level container for the survey element. The rest of the survey content may be added to the top-level container, or it may be added to another control in the top-level container. For example, consider the <CheckBoxListPanel> survey element. For this element, a FlowLayoutPanel hosts the caption and a CheckBoxList. The FlowLayoutPanel is the top-level container, but the CheckBoxList is the container for survey elements. It's the control that's returned by the CreateContainer() method.

To make it easier to manipulate these ingredients, the CreateContainer() method defines them immediately, as shown here:

private Control CreateContainer(PanelTypes type, string caption) {

// Represents the top-level container // (a TableLayoutPanel or FlowLayoutPanel, // depending on the survey element). TableLayoutPanel pnlTable = null; FlowLayoutPanel pnlFlow = null;

// Represents the control object that contains // the rest of the survey content. Control container = null;

// Represents a caption that can be inserted at // the top of the panel. Label lblCaption;

The next step is to identify the type of survey element, and create the appropriate container. For example, if a TextBoxPanel is required, a new nested TableLayoutPanel is generated with two columns, one for the label text and one for the text box:

case PanelTypes.TextBoxPanel:

pnlTable = new TableLayoutPanel();

pnlTable.CellBorderStyle = TableLayoutPanelCellBorderStyle.Outset; pnlTable.ColumnCount = 2;

// Make sure the full width of the form is used // for the text box.

pnlTable.Anchor = AnchorStyles.Left | AnchorStyles.Right;

// Set the container, which will be used to add more content.

container = pnlTable;

break;

Note that, when created programmatically, a TableLayoutPanel has no ColumnStyle objects. (When created at design time, the ColumnStyle objects are generated automatically.) You don't necessarily need to add these objects, if the default AutoSize behavior makes sense. In this example, the AutoSize behavior does make sense, because you want the first column (the label) to be only as wide as necessary. The second column (with the text box) is the last column, so it automatically fills the remaining space.

The code for creating a <GroupSelectionPanel> is similar, except it uses a FlowLayoutPanel as the top-level container that's inserted in the cell. The FlowLayoutPanel uses FlowDirection. TopDown so that the contained caption (a Label control) and each radio button inside it will take a full line.

case PanelTypes.GroupSelectionPanel: pnlFlow = new FlowLayoutPanel(); pnlFlow.FlowDirection = FlowDirection.TopDown;

// Add a caption. lblCaption = new Label(); lblCaption.Text = caption; lblCaption.AutoSize = true; pnlFlow.Controls.Add(lblCaption);

container = pnlFlow; break;

The <CheckBoxListPanel> and <LargeTextBoxPanel> use the same approach. Here's the complete code:

case PanelTypes.CheckBoxListPanel: pnlTable = new TableLayoutPanel(); pnlTable.ColumnCount = 1;

// Add a caption. lblCaption = new Label(); lblCaption.Text = caption; lblCaption.AutoSize = true; pnlTable.Controls.Add(lblCaption);

// Add the check-box list. CheckedListBox checks = new CheckedListBox(); checks.AutoSize = true; pnlTable.Controls.Add(checks);

container = checks; break;

case PanelTypes.LargeTextBoxPanel: pnlTable = new TableLayoutPanel(); pnlTable.ColumnCount = 1;

pnlTable.Anchor = AnchorStyles.Left | AnchorStyles.Right;

// Add a caption. lblCaption = new Label(); lblCaption.Text = caption; lblCaption.AutoSize = true; pnlTable.Controls.Add(lblCaption);

container = pnlTable; break;

The last step is to add the top-level container to the table, and return the control container so the rest of the content can be inserted into it:

Panel pnl = null;

pnl = pnlTable;

pnl = pnlFlow;

// Ensure 7 pixels of spacing between each survey element. pnl.Margin = new Padding(7); pnl.AutoSize = true;

// Add the panel for this survey element to the top-level container

// for the whole survey.

targetContainer.Controls.Add(pnl);

// Return the container control, so more content can // be inserted inside it. return container;

The CreateContent() method is the last piece of the puzzle. It accepts several pieces of information, creates the corresponding input control, and adds it to the panel. It also performs basic validation by checking that the name of the element matches the expected content. Here's the full code:

private void CreateContent(PanelTypes type, string elementName, string caption, string id, Control container)

Control ctrl = null;

case PanelTypes.TextBoxPanel:

if (elementName != "Textltem") throw new XmlException("Element " + elementName + " not expected");

// Generate a Label and TextBox pair for the row.

ctrl.Text = caption;

container.Controls.Add(ctrl);

ctrl.Dock = DockStyle.Fill;

container.Controls.Add(ctrl);

break;

case PanelTypes.GroupSelectionPanel: if (elementName != "SelectionItem") throw new XmlException("Element " + elementName + " not expected");

ctrl = new RadioButton(); ctrl.Name = id; ctrl.Text = caption;

// Keep the adjacent radio buttons close together by shrinking

// the top and bottom margins.

container.Controls.Add(ctrl);

break;

case PanelTypes.CheckBoxListPanel:

if (elementName != "SelectionItem") throw new XmlException("Element " + elementName + " not expected");

// Add items to the CheckedListBox. ((CheckedListBox)container).Items.Add(

new CheckBoxListItem(caption, id)); break;

case PanelTypes.LargeTextBoxPanel: if (elementName != "TextItem") throw new XmlException("Element " + elementName + " not expected");

ctrl = new TextBox(); ctrl.Dock = DockStyle.Fill; ((TextBox)ctrl).WordWrap = true; ((TextBox)ctrl).AcceptsReturn = true; ((TextBox)ctrl).Multiline = true;

// Set the text box to a line height of 3 lines.

// This information could also be configurable through a

// class property, or it could be read from the XML document.

container.Controls.Add(ctrl);

break;

0 0

Responses

  • lee
    How to insert node values from xml file into the text boxes which are inside table layout panel?
    5 years ago
  • Saara
    How to use tablelayoutpanel for data entry?
    5 years ago

Post a comment