Ask Kathleen

Use Recursion to Clear out Textboxes

Clear out textboxes using recursion and LINQ; use VB literals to transform an XML document; and eliminate an annoying artifact of VB internals.

Q I recently encountered an issue in determining a control type. Normally I use the following code to clear textbox controls:

foreach (Control ctl in this.Controls)
{
   if (ctl is TextBox)
   {
      ctl.Text = String.Empty;
   }
}

This code works fine until I insert GroupBox containers and move the textbox controls into them. How do I perform an action on all controls on a form, regardless of how I arrange them in groups or tabs?

A As you discovered, the Controls collection of the form or any other container contains only direct child controls. The contents of nested container controls are not included. Solving this presents an opportunity for you to use a recursive method, take advantage of LINQ, and create an extension method. The recursive method works in any version of .NET, while the other approach is specific to .NET 3.5.

A utility method in a separate class lets you reuse the code. The ClearTextboxes method first clears each textbox that is a direct child of the container. It then calls back into itself -- a process known as recursion -- for any controls that contain additional child controls. Recursion is the best technique to access a logical tree of unknown depth:

public static void ClearTextboxes(Control container)
{ foreach (Control ctl in container.Controls)
   { var textBox = ctl as TextBox;
      if (textBox != null)
      { textBox.Text = String.Empty; }
      if (ctl.Controls.Count > 0)
      { ClearTextboxes(ctl); }
   }
}

You could test whether each child control is a container, but technically, any control can contain child controls and the safest way to catch all variations of containers is to simply test for the presence of child controls. The "as" cast operation returns nothing if the cast fails (similar to TryCast in VB). Performing this cast and then checking for nothing avoids duplicate casting and an FxCop warning.

Performing recursion isn't difficult, but you must do it correctly. If you don't, you'll encounter infinite recursion, which will cause a runtime crash. You could create a helper method that takes a delegate parameter, but in .NET 3.5, it's easier to return an IEnumerable containing all the controls on the form using a method such as AllControls. Placing this utility method in a static class and using the "this" keyword makes it an extension method (in VB, you'd place the method in a module and add the Extension attribute). Defining the method as an extension method lets you call it as though it were a method of the Form. Name methods intended only to return values as though they were read-only properties provide a straightforward syntax for calling code.

Once you have all the controls in an IEnumerable<Control> collection, you can use LINQ against this collection to provide any desired filter (see Listing 1). However, AllControls returns IEnumerable<Control>, so you need to filter and cast each control to TextBox. The OfType method returns an IEnumerable that contains only items of the specified type. The method filters the collection to include only textboxes and casts to IEnumerable<TextBox>.

Depending on which version of .NET you're using, one of these two approaches clears out those textboxes.

Q I want to transform an XML document using VB's XML literal syntax, but I can't figure out a clean way to copy elements and attributes. How do I do that?

A You can do this in both VB and C#, although the VB syntax is simpler. The type of code you write in either language depends on how much of the schema you want to hard-code into your conversion routine.

If you're building a document with an explicit structure, you can output it directly with LINQ. For example, you can transform an XML document that contains Object and Table elements in a namespace imported with the orm prefix along with whatever attributes they happen to contain:

Dim ret = _
<Root>
   <Objects>
      <%= From obj In xDoc...<orm:Object> _
         Select <Object <%= _
       obj.Attributes %>/> %>
   </Objects>
   <Tables>
      <%= From table In xDoc...<orm:Table> _
         Select <Table <%= _
       table.Attributes %>/> %>
   </Tables>
</Root>

In many cases, your XML will be more flexible, and you'll need to combine a couple of techniques to get the desired result. Simply copy the element when you need an element and all of its descendant elements:

      Dim xDoc = XDocument.Load("XmlFile1.xml")
      Dim out1 = <?xml version="1.0" encoding="utf-8"?>
                 <%= xDoc.Root %>

Including the XML declaration creates an XDocument rather than an XElement.

You can also copy the elements using LINQ syntax. To copy any element by name or calculate a name, surround the expression hole with angle brackets, and close the element without specifying the element name:

Private Function BuildChild2(ByVal element As XElement) _
   As XElement 
   Return _
      <<%= element.Name %> <%= element.Attributes %>>
   <%= From node In element.Nodes Where node.NodeType <> _
      Xml.XmlNodeType.Element %>
   <%= From child _
      In element.Elements Select BuildChild2(child) %>
</>
End Function

Recursively calling the method lets you control the output of your entire document. Both of these examples simply copy the XML document, which you could do as easily with a file copy. However, these techniques get you ready for more complex manipulation. You can combine these techniques to copy all customers and flatten the address by removing the FullAddress element and promoting its children (see Listing 2). You can also filter, by returning Nothing instead of an XElement.

You can use the XElement and XAttribute constructors in VB, but the VB literals offer better optimization and readability. You can download the VB and C# versions from VisualStudioMagazine.com.

Q I get the error, "Range variable cannot match the name of a member of the Object class," when I use this code:

Dim q = From row In dt.Rows _
   Select row(0).ToString() _
   Order By row(0).ToString()

Why can't I do this?

A The problem is that VB implicitly creates a variable for the contents of the projection (the Select). This implicit name is important when you return an anonymous type, but in other situations it's a surprising artifact of VB internals. The implicit naming attempts to create a variable named ToString, which conflicts with the method on the Object type.

You can fix this by naming the return value explicitly:

Dim q = From row In dt.Rows Select name = _
   row(0).ToString() Order By row(0).ToString()

However, this uncovers your next error -- "Name 'row' is either not declared or not in the current scope" -- and marks the row variable on the Order By line. This happens because in VB, LINQ works from start to finish, and after the Select, the row variable is no longer available because it's not part of the projection.

You can use one of two solutions. Either include the Order By before the projection, or sort by the name used in the projection:

Dim q = From row In dt.Rows Order By 
   row(0).ToString() Select Name = row(0).ToString()

q = From row In dt.Rows Select Name = _
   row(0).ToString() Order By Name

Q I'm creating a fairly complex VB XML literal, and I want to place comments inline and possibly comment out sections of the XML while I'm developing. I know I can create XML comments for output, but I want to create source code comments that will not be output. How can I do this?

A You cannot add source code comments in the current release of VB XML literals. However, you can choose from one of two hacks that will probably fill your needs. You can create a method that takes a string and returns String.Empty:

Public Module Utility
   <System.Runtime.CompilerServices. _
      Extension()> Public Function XmlComment(
      ByVal comment As String) As String
      Return String.Empty
   End Function
End Module

This code might appear rather moronic, but it lets you include a string in your source code that does nothing:

      Dim x = _
         <code>
   <Stuff>...</Stuff>
   <%= "Here is my comment".XmlComment %>
</code>

This solution isn't as nice as having true comments, particularly because the comments aren't colored green, but it does let you insert important information adjacent to the relevant code, rather than before and after the XML block.

To "comment out" code use the ternary operator and VB's ability to nest XML as deeply as you need:

      Dim y = _
         <code>
   <Stuff>...</Stuff>
   <%= If(True, Nothing, _
      <StuffToComentOut></StuffToComentOut> ) %>
</code>

Neither solution is pretty; both are ugly hacks. However, when you're doing complex things with XML literals, you sometimes need to put comments inline or temporarily comment out portions of the code.

Q How do I convert a hexadecimal string to its numeric equivalent?

A You convert from a hexadecimal string to a numeric through an overload of the Convert.ToInt32 method, which takes a base parameter:

int i = Convert.ToInt32("A2", 16);

Passing "A2" to the ToInt32 parameter results in 162 when you specify base 16.

To convert back to a hexadecimal string, use hexadecimal formatting of the ToString method:

this.textBox2.Text = _ 162.ToString("X");
this.textBox3.Text = _ 162.ToString("x");

Hexidecimal formatting of the integer 162 results in A2 and a2, depending on whether you requested an upper or lower case representation.

Q Why can't I use LINQ in the Immediate window? I have a big list, and I want to display only 20 lines or so from the middle of the list.

A LINQ relies on compiler magic to create classes and methods implicitly. These methods and classes actually exist in the intermediate language (IL), and the Immediate window in today's Visual Studio doesn't create sufficient code to support LINQ or lambda expressions.

You can use the ToArray() method to see the results of a query in the Immediate window. If you're working with a System.Collections.Generic.List(Of T), you don't need LINQ because the GetRange method lets you select portions of the list:

? list.GetRange(2,4)

If you're working with an IEnumerable(Of T), you can either create a list using ToList() and use the GetRange method, or you can use Skip and Take followed by ToArray to display the list:

? list.Skip(2).Take(4).ToArray()

I like the second approach because it works with all IEnumerables.

Q I know .NET has a new HashSet class, but IEnumerable supports the set operations that I'm interested in (Union and Intersect), so I don't see why I would need HashSet. Am I overlooking something?

A The System.Collections.Generic.HashSet<T> class, new to .NET 3.5, is one of a handful of changes to the Common Language Runtime (CLR) that aren't directly related to LINQ.

A HashSet is a class designed for set-based operations, but the addition of basic set operations as extension methods on all IEnumerables means you already had access to most of this functionality. The fact that HashSet is an IEnumerable and contains two versions of basic set operations further obscures the new class's purpose. The HashSet class has four characteristics that set it apart from the generic List class. First, all items in a HashSet must be unique. Second, the order of items has no meaning. Third, set operations are fast and alter the existing set. And finally, additional methods unique to HashSets are included.

The intrinsic HashSet methods for set operations are suffixed with the word "With" to differentiate them from the IEnumerable extension methods. For example, the HashSet includes the UnionWith method and supports the Union extension method. UnionWith is faster and alters the existing set, rather than providing a new set for the results. The performance benefits are small for small sets -- on the order of a tenth of a millisecond for sets of less than 100 integers (download the benchmark code). However, as the size of the set increases, the performance advantage of UnionWith becomes significant -- about a five- to 10-fold improvement.

HashSet provides subset and superset methods. IsProperSubSet and IsSubSet return the same value unless the two sets are identical. In set theory, identical sets are considered subsets but not proper subsets. Subset and Superset methods, like all HashSet methods, don't take the order of the set into account; sets are not ordered. A mixed group of integers in which all values of the first set are included in the second set is a SubSet.

HashSets become important when determining whether two groups contain the same items when those items are in random order. You can write code to determine this, but it tends to be slow and buggy. Copying items into a HashSet and using SetEquals gives you a quick, reliable answer. Similarly, the Overlaps method checks whether two sets share at least one common member. You can specify a custom comparison delegate to compare specific properties within objects.

You can compare the results of different HashSet operations (see Table 1). The addition of common set operations to IEnumerable offers functionality in common scenarios. HashSet is focused at less common scenarios of large sets with performance issues and a broader group of set operations.

***

In my September 2007 column ("Serialize Data to the Clipboard"), I recommended using StringComparison.InvariantIgnoreCase to perform case-insensitive string comparisons. But there's an even better approach. It turns out that StringComparison.Ordinal and StringComparison.OrdinalIgnoreCase provide better and safer culture-agnostic string matching, as well as better performance. StringComparison.Ordinal is the default for the static Equals method, the instance Equals method on string, and the string comparison operators (= in VB and == in C#). This means you encounter issues only when doing a case-insensitive comparison. OrdinalIgnoreCase is the preferred comparison mechanism, unless the comparison is explicitly related to user entry. In addition to letting you perform case-insensitive comparisons directly in your code, OridnalIgnoreCase lets you passStringComparer.OrdinalIgnoreCase when you need a comparison delegate, such as when creating a case-insensitive dictionary. If you've used extension methods to accomplish this task in the past, it's easy to update your code to reflect this better technique. If you haven't used extension methods, do a search for InvariantIgnoreCase and replace it with OrdinalIgnoreCase.

comments powered by Disqus
Upcoming Events

.NET Insight

Sign up for our newsletter.

I agree to this site's Privacy Policy.