Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Visual CSharp .NET Developer's Handbook (2002) [eng]

.pdf
Скачиваний:
43
Добавлен:
16.08.2013
Размер:
4.94 Mб
Скачать

Listing 6.1: Creating the PButton1 Control Dynamically

// Declare the new button.

private AxPButton1Lib.AxPButton1 btnNew;

private void btnTest_Click(object sender, System.EventArgs e)

{

// Create a copy of the new control. if (btnNew == null)

btnNew = new AxPButton1Lib.AxPButton1(); else

{

MessageBox.Show("Button Already Created!", "Button Error",

MessageBoxButtons.OK,

MessageBoxIcon.Error);

return;

}

//Add the control to the form. Controls.Add(btnNew);

//Configure the control. btnNew.Name = "btnNew";

btnNew.ClickEvent += new System.EventHandler(btnNew_Click); btnNew.Size = new System.Drawing.Size(75, 23); btnNew.Location = new System.Drawing.Point(208, 72); btnNew.OnOff = true;

}

private void btnNew_Click(object sender, System.EventArgs e)

{

//Determine which modal result was returned and display a message. switch (btnNew.ModalResult)

{

case PButton1Lib.ModalType.mrNone: MessageBox.Show("None button pressed",

"State of Control",

MessageBoxButtons.OK,

MessageBoxIcon.Information);

break;

//

//Other cases

//

case PButton1Lib.ModalType.mrOff: MessageBox.Show("Button is Off",

"State of Control",

MessageBoxButtons.OK,

MessageBoxIcon.Information);

break;

}

}

As you can see, we create the control as we would any .NET control; the control is added to the form, and then we configure it. You might wonder why the code performs the configuration second. Many unmanaged controls react better when you configure them after

adding the control to the form. PButton1 causes an unhandled exception if you configure the YesNo property before you add the control to the form.

The btnNew_Click() method simply tests the output of the button (which is set to alternate between on and off for the example). The ModalResult property relies on an enumeration (ModalType) to define the return results of a button click. The example button provides numerous modal states that enable you to use the same button for a variety of predefined tasks.

Moving Unmanaged Components to .NET

Working with unmanaged components is similar to working with unmanaged controls. However, you'll find that components come with fewer problems attached and you normally won't need to work as hard to get them to work within .NET. You can still create a local copy of the component by creating a reference to it. If you want to make the component globally available, you'll need to create a key using the SN utility as we did for the control. However, instead of using the AxImp utility to create the assembly DLL, you'll use the TlbImp command.

You'll find an example of an unmanaged component named MyAdd in the \Chapter 06\Unmanaged Component folder of the source code on the CD. If you wanted to create a managed DLL to access this component, you'd first create a key pair using the SN utility. Next, you'd type TlbImp MyAdd.DLL /namespace:Sample /keyfile:MyKey at the command prompt and press Enter. This command creates a new library named MyAddLib.DLL. You can use the GACUtil utility to register the resulting DLL for public use.

Calling the DoAdd method within the MyAdd component is similar to calling any other .NET component. You need to add a reference to the project, as usual. (You can find an example in the \Chapter 06\MyAddTest folder on the CD.) Here's an example of the code you might use to call the component in this example.

private void btnTest_Click(object sender, System.EventArgs e)

{

// Create the component.

Sample.DoMathClass DoAdd = new Sample.DoMathClass();

// Make the call. txtResult.Text =

DoAdd.DoAdd(Int16.Parse(txtValue1.Text), Int16.Parse(txtValue2.Text)).ToString();

}

As you can see, there's no difference in the code for this example than from any other .NET component. Notice how you can avoid the use of intermediate variables by using data conversion function calls. The Int16.Parse() function converts the text values in the input text boxes to numbers. Likewise, you can use the ToString() function to convert the result into a string for the txtResult text box.

Working with Metadata

All .NET applications include metadata as part of the assembly. The metadata describes the assembly elements, making it easier to learn about the assembly after it's put together. One of the most common forms of metadata is the assembly information found in the AssemblyInfo.CS file. Each of the [assembly:] attributes contains information about the assembly. Figure 6.5 shows an ILDASM view of the assembly information for the PButtonTest application discussed earlier.

Figure 6.5: Metadata provides descriptive information for every assembly you create.

As you can see, each metadata entry consists of code and data. In this case, the data is also readable text, but there's no requirement that the entry appear in human readable form. For example, the security metadata for an assembly often appears in non-readable encrypted form for the obvious reasons.

The C# compiler automatically adds some types of metadata to your assembly. For example, it includes a list of the assemblies used to construct the current assembly. The metadata also includes information about base classes and other particulars about assembly construction.

A developer can also add metadata to the assembly. Many of the attributes you use will automatically add descriptive metadata. In addition, a developer can create custom attributes to assist in the documentation of the assembly. Custom attributes could also perform tasks such as sending application documentation to a server for storage. The idea is that metadata is another form of essential documentation.

Visual Studio .NET uses metadata to promote language interoperability and reduce application complexity. An application can use a process known as reflect (described in "An Overview of Reflection") to query a component about its interface requirements. Contrast this with the complex methods required by COM. In addition, you don't need a separate file to create the description, as you would with languages such as Visual C++.

One of the best ways to learn how metadata works within C# is to try the various attributes this language provides. We discussed some of these attributes as part of the "COM" section of Chapter 3. As you work with the attributes, use ILDASM to see how they affect the content of the assembly.

An Overview of Reflection

As previously mentioned, reflection provides a means for reading metadata from within your application. Obviously, this means learning everything you need to know in order to use the assembly, which means reading every form of metadata that the assembly contains. However,

it's most common for developers to want to know the attributes that an assembly contains because the attributes describe assembly features. For example, you'll use an attribute to describe the interface for a component or control.

The following sections show both sides of the metadata equation. In the first section, we'll create a custom attribute that adds descriptive information to the test application. In the second section, we'll use reflection to read the content of the custom attribute entries and display them on screen. After you read through these two sections, you should have a better idea of what metadata means and why it's so useful to the .NET developer.

Creating the Custom Attribute

In Chapter 3, we discussed attributes and how you can use them within your applications to produce certain effects. .NET comes with a lot of built-in attributes that meet common needs for all developers. However, you'll find that .NET doesn't cover every base and the attributes it does supply aren't always complete. You'll also want to create some custom attributes just to ensure you can work with attributes in the way that's most convenient for you.

Interestingly enough, you use an attribute to begin creating a custom attribute. Here's an example of an attribute we'll use for the example in this section.

[AttributeUsage(AttributeTargets.All,

AllowMultiple=true,

Inherited=false)]

The [AttributeUsage] attribute has only one required parameter, the types that the attribute will affect. You'll find these types in the AttributeTargets enumeration. The optional AllowMultiple parameter determines if a user can have more than one attribute placed in front of a given entity. For example, you'd only want to allow one attribute that determines the configuration of a component. However, you'd want to allow multiple copies of an attribute that provides documentation for a class. The optional Inherited parameter determines if other attributes can inherit the features of the custom attribute. Here's a list of the types that you can support using attributes.

Assembly

Module

Class

Struct

Enum

Constructor

Method

Property

Field

Event

Interface

Parameter

Delegate

ReturnValue

All

Creating a custom attribute is relatively easy once you know about the [AttributeUsage] attribute. Unlike Visual C++ .NET, you can only use classes for attributes (Visual C++ also enables you to use structs). Listing 6.2 shows the code for the custom attribute used in this section. The example will enable developers to describe entities in various ways. For example, you could use the attribute to describe additional work the assembly requires to provide a list of bugs within the entity.

Listing 6.2: Creating a Custom Attribute

//Attribute contains three mandatory (date, author, and purpose)

//and one optional entry (comment). [AttributeUsage(AttributeTargets.All,

AllowMultiple=true,

Inherited=false)]

public class EntityDescribe : System.Attribute

{

private String

_Date;

// The date the comment is made.

private String

_Author;

// Comment author.

private String

_Purpose;

// Purpose of the element.

private String

_Comment;

// Comment about the element (optional).

public EntityDescribe(String Date, String Author, String Purpose)

{

//These three values are mandatory and appear as part of

//the constructor.

_Date = Date; _Author = Author; _Purpose = Purpose;

}

//The comment field is optional,

//so it gets treated as a property. public String Comment

{

get

{

return _Comment;

}

set

{

_Comment = value;

}

}

//Accessors for the mandatory fields. public String Date

{

get

{

return _Date;

}

}

public String Author

{

get

{

return _Author;

}

}

public String Purpose

{

get

{

return _Purpose;

}

}

};

As you can see, the code is relatively simple. However, you need to know about a few nuances of attribute construction. Notice that only three of the properties—Date, Author, and Purpose—appear in the constructor. These three properties represent the required entries for using the attribute. The Comment property is separate and is therefore optional.

Because Comment is optional, you need to provide it with both a get and a set property, as shown in the listing. You could add anything here that you wanted, such as range checking. The main thing that the get and set routines have to provide is some means of storing the data provided by the user to the custom attribute.

The final issue is providing accessors for the three mandatory properties. The assembly will still contain the data if you don't provide an accessor, but you won't be able to read it from within your code. In short, the data will become lost within the assembly, never to see the light of day.

The custom attribute is ready to use. All we need to do is add one or more entries to another class to see it at work. The example places attributes in two places to make it easier to discuss the effect of the attribute on application metadata. However, the three most important entries appear at the class level, because we'll use reflection to read them in the next section. Here are the three entries we'll add to the class.

[EntityDescribe("11/27/2001",

"John",

"Created the new form for test application.")] [EntityDescribe("11/28/2001",

"Rebecca",

"OK Button Click Event Handler", Comment="Needs more work")]

[EntityDescribe("11/29/2001",

"Chester",

"Added Attribute Test Button")]

As you can see, the three custom attribute entries all describe aspects of the target class, FrmAttribute. The second entry in the list also makes use of the optional Comment property. Notice that you need to provide the name of the custom property to use it by providing a <Property Name>=<Value> pair within the attribute entry. The example entry doesn't check the validity of these entries, but you'd want to add this feature to an attribute that you wanted to use for production purposes.

Tip One of the nice features of attributes is that they always use the same presentation—no matter which development language you use. The same attribute code will work in C#, Visual C++, and even Visual Basic without modification. This makes attributes one of the

only truly generic pieces of code you can use.

After you add some custom attribute entries to the application, you can compile it. Open the application using the ILDASM utility (which we've discussed in several places). Figure 6.6 shows the results of the three entries we made to the class. Notice that ILDASM doesn't make them particularly readable, but you can definitely see them in them list (starting with the highlighted entry in the figure).

Figure 6.6: ILDASM displays any custom attribute additions you make to the code.

If you want to see the full effect of an attribute entry, you need to attach it to a data member, such as a pushbutton. Open the disassembly of the data member by double-clicking it in ILDASM. Figure 6.7 shows an example of the [EntityDescribe] attribute attached to the btnOK object. Notice that you can see all three of the default entries.

Figure 6.7: Use a disassembly to see the full effect of any custom attribute you create.

It's important to realize that the compiler stores the property values as they'd appear for the constructor. In other words, the compiler doesn't change the attribute in any way. The data you add to an assembly is readable in the disassembly, even if the attribute would normally encrypt the data. This security problem means you shouldn't store company secrets in the attributes and you might want to remove attributes from applications before you send them to a customer. Figure 6.8 shows an example of the readable data for the btnOK object.

Figure 6.8: Some types of metadata could represent a security risk for your application.

A source dump of your application is even more revealing. You'll find the AssemblyDump.il file in the \Chapter 06\Attribute\bin\Debug folder. This file contains a lot of information about the example application. While the native code executables from the Windows 32 environment weren't that much safer than the IL files used by .NET, they also didn't include as much descriptive information. It pays to create custom attributes using information that you'll only use during application construction or that you don't mind others seeing.

Accessing the Custom Attribute Using Reflection

One of the reasons to include metadata within an assembly is to document the assembly features and problems. Of course, reading these comments with ILDASM is hardly the best way to make use of the information. You could easily view the source code instead and probably save time by doing so. Reflection enables an application to read its own metadata and the metadata contained in other assemblies. This feature enables you to create utilities to read the metadata and do something with it. The nice part of this feature is that it also extends to the Microsoft assemblies, which means you can finally search them for tidbits of information.

Creating an application that relies on reflection isn't difficult, but it does look a tad strange at first. Listing 6.3 shows an example of reflection at work. In this case, clicking the Attribute button on the example application will fill the txtOutput text box with the content of the higher level attributes for the sample application.

Listing 6.3: Adding Reflection to an Application is Relatively Easy

private void btnAttribute_Click(object sender, System.EventArgs e)

{

Object

[]Attr;

//

Array of attributes.

EntityDescribe

CurrAttr;

//

Current Attribute.

//Gain access to the member information for the class. MemberInfo Info = typeof(FrmAttribute);

//Grab the custom attribute information.

Attr = (Object[])(System.Attribute.GetCustomAttributes(Info,

typeof(EntityDescribe)));

// Look at each attribute in the array.

for (int Counter = 0; Counter < Attr.Length; Counter++)

{

// Obtain the current attribute.

CurrAttr = (EntityDescribe)(Attr[Counter]);

// Display the information. txtOutput.AppendText(CurrAttr.Date); txtOutput.AppendText("\t"); txtOutput.AppendText(CurrAttr.Author); txtOutput.AppendText("\t"); txtOutput.AppendText(CurrAttr.Purpose); txtOutput.AppendText("\t"); txtOutput.AppendText(CurrAttr.Comment); txtOutput.AppendText("\r\n\r\n");

}

}

The first task is to gain access to the member information that you want to use to search for attributes. In this case, we'll gain access to the FrmAttribute class and view the class level attributes. The Info variable contains this information.

The next task is to search for the attributes. You'll find a number of overrides for the GetCustomAttributes() method. Listing 6.3 shows just one option. In this case, we'll search for all of the [EntityDescribe] attributes found in the FrmAttribute class. Notice the use of casts within the code. You need to locate the information in the order shown, or C# will display an error message during the compile cycle and not provide much in the way of a helpful answer about what's wrong with the code.

Note The .NET Framework actually provides two methods for gaining access to a custom attribute's information. The first is GetCustomAttribute(), which returns a single attribute—the first one it finds. The second is GetCustomAttributes(), which returns an array of all of the attributes that fit within the search criteria.

After you gain access to the attributes, you need some way to convert them to readable or storable data. The first step in the for loop is to cast the individual attribute entry into an EntityDescribe data type. After that, you can use the data to display information on screen, send it to the printer for a report, or place it within a database for later use. In this case, we'll use the AppendText() method to place the data in the txtOutput text box. Figure 6.9 shows the output from the application.

Figure 6.9: The output of the example application shows the top-level attribute entries.

Creating Controls

C# provides a number of ways to create managed controls. In fact, you'll be surprised by the variety of controls you can create using the projects supplied as part of the Visual Studio

.NET IDE. The sections that follow will show you how to create some simple examples of controls using C#. You'll learn how to create additional controls as the book progresses, but

these controls provide you with the basics you need to create the complex controls found later in the book.

Note You can't create unmanaged component and controls directly with C#. This limitation means that many of the tools you used to test your controls in the past, such as the ActiveX Control Test Container, no longer have a useful purpose. You must test the components and controls using the debugger provided by the IDE. Fortunately, you can convert the managed C# components and controls to work in the unmanaged environment. We'll discuss this technique in the "COM Component Example" section of the chapter.

Windows Control Library Overview

The Windows Control Library project enables you to create managed controls for your applications. Microsoft has made the process of creating controls much easier than the unmanaged counterpart you might have created in the past. The following sections will show that C# provides low-level support for common control needs. We'll also discuss the requirements for debugging a control. The fact that you don't have direct access to the control means you need to know how to access the control code through a client application.

User Control Example

User controls aren't controls in the same sense as controls that you might have created using Visual C++ in the past. You use them like controls, but actually they're composites of other controls. A user control represents a method of bundling some level of control functionality and then using it within a form. User controls have the same functionality as the COM controls you created in the past, but you'll notice they require a lot less work to create.

You begin developing a user control by creating a new Windows Control Library project. (The example appears in the \Chapter 06\UserControl folder on the CD.) After you name the class and files, you can begin adding controls to the Designer display. The example uses a simple pushbutton and timer. If the user doesn't click the button within the time set by the timer, the button will automatically click itself. This particular control comes in handy for slide presentations where you want to maintain a certain pace throughout the presentation. However, you can use it for other purposes.

Figure 6.10 shows the layout for the AutoButton control. Notice that the user control area is just large enough to hold the button. The reason you want to size the user control area is to prevent it from overlapping other controls when displayed in an application. In addition, the default size makes moving and placing the control cumbersome.