Ask Kathleen

Use Enums Across Assembly Boundaries

Call a combo box across assembly boundaries with generics and enums; add contact information with Assembly Information; and drill down on FxCop spelling rules.

Technology Toolbox: VB.NET, C#

Q. I'm working on a tool to support my internal framework. This tool needs to include a combo box that contains a list that is specific to a particular project. The framework tool assembly doesn't know anything about the project, and thus doesn't know what types to include. Can I use an enum to do this?

A. The more generalized problem here is that a class in one project (a combo box in your case) needs to work with a type (in this case an enum) that it knows nothing about. The application project references the framework project, and thus knows its contents (Figure 1). But .NET doesn't allow circular references, so the framework project knows nothing about the application project. If the contents of the enum are fixed, you can create a third project holding the enum and reference it in both the application and the framework assemblies.

If the application project is dynamic and the framework tool is intended to work with applications that aren't yet designed, you must reach into the application project without knowing anything about it. While this might sound like an impossible request, it's relatively easy to do with generics. Generics solve this problem because they can create new concrete classes based on unknown and unreferenced types. This is one of the key benefits of .NET's runtime generics versus C++ templates.

Enums are special in .NET. They aren't fully classes (reference types) and not full structures (value types)—although they generally behave like their underlying data types, which are integer value types. The resulting restrictions mean you can't use inheritance or interfaces, which are two good approaches to crossing project boundaries, also known as assembly boundaries. Prior to .NET 2.0 and generics, your only choice would have been a redesign, perhaps holding value/string pairs in a collection. Approaches like this forfeit IntelliSense and compiler checking in other parts of your application, so the generics approach to retaining the enum is significantly better.

The easiest approach for displaying a combo box is to create a generic list of display items and databind to this list. You could fill the combo box manually, but the list approach is more reusable because it can bind to many other UI elements. You can hide all the details of unraveling the enum into a collection by embedding this code in an internal (Friend) class in the framework project.

This EnumMembers class is derived from List(Of EnumPair) (Listing 1). The EnumPair class contains only an integer/string pair that describes each enum value. The EnumMembers class does all of its work in the constructor.

The constructor's first step is to test the type. Because enum isn't a real base class, you can't constrain to enum, which means you can't prohibit type arguments with non-enum structures. You have to test explicitly in the constructor, which illustrates why a future explicit constraint would be so valuable. If a programmer messes up and passes a type that is not an enum, you get a runtime error instead of a compile-time error. The GetType operator (typeof in C#) returns an instance of the Type class that represents the enum. The IsEnum property tells you whether it's an enum, and a shared method on the Enum class returns the underlying type. I don't expect 64 bit enums and want a clear error prior to an overflow. If you use 64 bit enums, change all occurrences of Int32 to Int64. Int64 is less efficient, so you want to avoid it when you don't need the extra space.

Once the constructor knows its working with an enum, it uses a shared method to get the list of integer values. The constructor traverses this list to get the string values. Passing the integer and string to the EnumPair constructor creates a new instance to load into the list. Each concrete class built from the generic EnumMembers class displays a different enum and the EnumMembers class supports any enum in the world.

There are many ways to use instances of the EnumMembers class because they are standard lists. The user control in the online sample (download the code here) contains a combo box and has a generic Fill method. I didn't make the user control itself generic because generic user controls don't play nice with Visual Studio. The generic Fill method takes an enum as a type parameter:

Public Class TestUC
     Inherits System.Windows.Forms.UserControl
     Public Sub Fill(Of T As Structure)()
          Dim x As New EnumMembers(Of T)
          Me.comboBox.DataSource = x
     End Sub
End Class

The main application project references the framework project. After you build your solution, TestUC appears in the toolbox and you can drag it onto a form. The form's OnLoad method can call the Fill method of TestUC using the correct enum as a type argument:

Protected Overrides Sub OnLoad(ByVal e_
     As System.EventArgs)
     MyBase.OnLoad(e)
     Me.UserControl1.Fill(Of TestEnum)()
End Sub

I use the OnLoad overrides instead of the Load event because it's a little more efficient and provides more predictable behavior when there are derived forms that use either OnLoad or the Load event.

You use frameworks and utility assemblies to promote reuse, provide a single point of change, and isolate ugly details from the using programmer. Solving problems in a way that preserves enums gives IntelliSense and compiler checking to later programmers while letting them ignore the details of converting enums into a bindable list. Generics allow the unknown enums to cross project boundaries and remain strongly typed. In this case, an effective combo box displays enum values entirely unknown to its assembly.


Q. I like the way Assembly Information lets me define things about my application and use them automatically in my About Box and Splash screen. But, can I add contact information?

A. Both C# and VB create default AssemblyInfo files when you create an application. You can access this information through the "Assembly Information" button in Project properties or by displaying all files and editing the file directly. AssemblyInfo provides a standard way to store information such as company name and copyright. You can extend this information, but you can't change the dialog box in Project properties. You must edit the AssemblyInfo file manually to add your new information.

The AssemblyInfo file is just a location for attributes that apply to the entire assembly. The Assembly Information dialogs display the most interesting assembly-level attributes for you to update easily. You can actually put assembly-level attributes in any file as long as it's outside the class declaration. However, you'll confuse other programmers if you spread these out in multiple files in your project. It's a good idea to keep all your assembly attributes in Assembly.Info.

All assembly attributes are available for general use. The Login, Splash, and About Box default forms use some of the assembly attributes, while compilation and interop use other attributes. Setting these values should be the first thing you do after providing an adequate project namespace in new Visual Studio projects.

You create a new attribute by creating a class that derives from System.Attribute (Listing 2). You can supply parameters to accept one or more values in the constructor. Attribute classes generally just hold these values and retrieve them as read-only properties. You also specify the context where the attribute makes sense, defining it as an attribute on your derived custom attribute class. You can use your custom attributes anywhere you'd like to add more information to an assembly, type, or member.

Setting the value of an assembly level attribute is similar to setting the value of other attributes, except that you need to specify the assembly (VB would use angle brackets):

[assembly: AssemblyTitle("Attribute Demo")]
[assembly: AssemblyDescription(
     "Test app to show assembly attributes")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("GenDotNet")]
[assembly: AssemblyProduct("Attribute Demo")]
[assembly: AssemblyCopyright(
     "Copyright © GenDotNet 2007")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: Abcdef.Contact(
     null, null, "Fort Collins", "CO", "USA", null)]

The Abcdef namespace must be included because AssemblyInfo itself doesn't include a using statement for this namespace.

The last step required to include contact information is to update the Login, Splash, and About Box forms to display your new information. Microsoft offers defaults for all these forms and these defaults are cool because they use dynamic table layout instead of individually positioning the UI elements. You probably want to update these forms to reflect your application's look and feel, as well as to add the new contact information. You access the contact information through the GetCustomAttributes method of the assembly:

ContactAttribute _contactInfo;
Attribute[] attributes = 
     (Attribute[])this.GetType().
     Assembly.GetCustomAttributes(typeof(
     ContactAttribute),true); 
if (attributes.Length > 0) 

{
     _contactInfo =  attributes[0] as ContactAttribute;
}

You can access the contact object's properties once you have the contact attribute object:

this.address1Label.Text = this.ContactInfo.Address1;

The AboutBox in the sample download uses a dynamic TableLayout with Autosize rows for the contact information (download the sample code here). Empty rows disappear, which enables you to avoid blanks where the address isn't present. To avoid some rowspan issues, move the graphic outside the TableLayout and set the visibility of empty labels to false.

If you consider only one location, it might seem easier to hard code the contact information than to include the assembly attribute. But holding this information in an assembly attribute allows a single predictable point of update that appears simultaneously on the Login, Splash, and About Box forms. It also opens the door to creating a single set of supporting forms for all applications in your company.


Q. I want to use FxCop to check for misspelled names, but FxCop doesn't recognize my company name, which I use for a namespace. Can I get FxCop to recognize my company name so I don't have to turn off the spelling features?

A. The GotDotNet version of FxCop supports many naming rules, whereas the Team System version (Code Analysis) supports only a few. I tested against FxCop 1.35 from GotDotNet.

In most cases, you can teach FxCop to recognize your company name. You do this by creating a custom dictionary for FxCop. The dictionary is an XML file named CustomDictionary.xml, and you include your company name as a Word element:

<?xml version="1.0" encoding="utf-8" ?>
<Dictionary>
     <Words>
          <Recognized>
               <Word>Abcdef</Word>
               <Word>Name</Word>
          </Recognized>
     </Words>
</Dictionary>

FxCop finds custom dictionaries based on their location. You can put them in the project directory—adjacent to your .vbproj or .csproj file—if the customization is specific to one project. Alternatively, you can put them in the FxCop directory (usually [Program Files]/Microsoft FxCop) to use the same custom dictionary for all projects.

FxCop can be frustrating to use with certain company names. Problems arise when your company name contains unusual capitalization. Capitalization might be important if it's part of a trademark or special branding. If the trademarked form of your company name is FEdcba, FxCop raises both a spelling and a casing error. You might be tempted to include the name as-is in the custom dictionary:

<Word>FEdcba</Word>

Unfortunately, FxCop ignores this dictionary entry. FxCop splits words at capitals, and splits your company name to F and Edcba. Neither of these match this entry, so you get a spelling error. The nearest approach is to include the part FxCop can recognize and skip the single capital letter which FxCop does not see as a spelling error:

<Word>Edcba</Word>

This fixes the spelling error, but FxCop still sees this as a casing error. Short of renaming your company or ignoring trademarked casing, there aren't any good solutions in the current versions of FxCop, and you might have to turn off all tests for casing or put up with these bogus errors.

Note: FxCop and other tools on GotDotNet are being moved to MSDN.

About the Author

Kathleen is a consultant, author, trainer and speaker. She’s been a Microsoft MVP for 10 years and is an active member of the INETA Speaker’s Bureau where she receives high marks for her talks. She wrote "Code Generation in Microsoft .NET" (Apress) and often speaks at industry conferences and local user groups around the U.S. Kathleen is the founder and principal of GenDotNet and continues to research code generation and metadata as well as leveraging new technologies springing forth in .NET 3.5. Her passion is helping programmers be smarter in how they develop and consume the range of new technologies, but at the end of the day, she’s a coder writing applications just like you. Reach her at [email protected].

comments powered by Disqus

Featured

Subscribe on YouTube