Overview of .NET Framework Classes
It is impossible to cover in one chapter, or even in one book, all of the .NET Framework classes. Although coverage is incomplete, the .NET classes cover a large fraction of the Win32 API, as well as much else. While a lot of attention has been focused on the Internet-related functionality, the development model for Windows applications has changed as well.
This chapter focuses on those classes that illustrate the key concepts and patterns that appear throughout the .NET Framework. You will find this approach more fruitful over the long run than our attempting to explain a little about every class that you might need without giving you much insight. Other chapters will go into more depth about other parts of the framework, such as Windows Forms, ASP.NET, ADO.NET security, and Web Services.
We start out by exploring the concept of reflection and metadata. Metadata appears everywhere in .NET and is critical to understanding how the Common Language Runtime (CLR) can provide services for your applications. Next, we explore file input/output for several reasons. First, it introduces the important topic of serialization. Second, the Path class provides an example of how some framework classes provide some or all of their functionality through static methods. Third, the formatter classes are used in several places in .NET.
Understanding serialization will give you a concrete idea of how the framework can handle objects transparently for you. It also appears in a supporting role any place where objects have to be stored or transported. Our discussion of the ISerializable interface again demonstrates how much easier it is to implement an interface in .NET than with COM.
To further develop an understanding of the .NET model for applications, we introduce programming with threads under .NET and several .NET synchronization techniques to handle multithreading conflicts. The various synchronization techniques illustrate the tradeoffs of using attributes supplied by the framework versus doing it your self.
To further your understanding of the .NET programming model, we introduce context and the use of proxies and stubs to implement system services. We also look at using application domains, which are more efficient than Win32 processes in achieving application isolation.
The asynchronous design pattern appears throughout .NET and is discussed in some detail. We give some examples of remoting because it is a key technology and it summarizes many of the concepts developed in this chapter. The chapter uses several attributes provided by the .NET Framework, and we show how to implement and use custom attributes. We discuss finalization so that you can understand how to make sure resources are properly freed in your applications.
Metadata and Reflection
The Serialization example in Chapter 2 demonstrates how metadata makes many of the services of the CLR possible. Many of the technologies we cover in the rest of the book rely on metadata, although we will not always stop and point this out.
Metadata is information about the assemblies, modules, and types that constitute .NET programs. If you have ever had to create IDL to generate a type library so that your C++ COM objects could be called by Visual Basic, or to create proxies and stubs, you will appreciate how useful metadata is and will be grateful that it comes "for free."
Compilers emit metadata, and the CLR, the .NET Framework, or your own programs can use it. Since we want to give you an understanding of how metadata works, we will focus our discussion on the use of metadata, not the creation of metadata. Metadata is read using classes in the System::Reflection namespace.1
When you load an assembly and its associated modules and types, the metadata is loaded along with it. You can then query the assembly to get those associated types. You can also call GetType on any CLR type and get its metadata. GetType is a method on System::Object, which every CLR type inherits from. After you get the Type associated with an object, you can use the reflection methods to get the related metadata.
The Reflection sample program takes the case study's Customer assembly and prints out some of the metadata available. You should examine the output and source code as you read the next sections. You should especially compare the output of the program with the source code in the file customer.h.
The program clearly shows that it is possible to retrieve all of the types in an assembly and reconstruct the structures, interfaces, properties, events, and methods associated with those types.
First we load the assembly into memory and write out its name.
Assembly *a = Assembly::Load(assemblyName); Console::WriteLine(
"Assembly {0} found.", a->FullName);
The output for this statement is appropriate for an unsigned assembly:
Assembly Customer, Version=1.0.643.18973, Culture=neutral, @@PublicKeyToken=null found.
One of the properties of the Assembly class is the CodeBase, discussed Chapter 7, "Assemblies and Deployment." The security evidence associated with this assembly is another property.
The following code tries to get the entry point for the assembly:
MethodInfo *entryMethodInfo = a->EntryPoint;
Since this is a typical C++ component assembly, the entry point is __DllMainCRTStartup@12. If this was an executable program, we could use the Invoke method on the MethodInfo class to run the startup code in the assembly.2
The sample uses the assembly's GetModules method to find associated modules with this assembly. In this case we have only one, named customer.dll. We could then find the types associated with the module. Instead, we use the assembly's GetTypes method to return an array of the assembly's types.
Type
The abstract class Type in the System namespace defines .NET types. Since there are no functions outside of classes or global variables in .NET, getting all the types in an assembly will allow us to get all the metadata about the code in that assembly. Type represents all the types present in .NET: classes, structs, interfaces, values, arrays, and enumerations.
The Type class is also returned by the GetType method on the System::Object class and the static GetType method on the Type class itself. The latter method can be used only with types that can be resolved statically.
One of Type's properties is the assembly to which it belongs. You can get all the types in the containing assembly once you have the Type of one object. Type is an abstract class, and at runtime an instance of System::RuntimeType is returned.
If you examine the program's output, you will see that each type in the assembly, CustomerListItem, ICustomer, Customer, and Customers, is found and its metadata is printed out. We can find out the standard attributes and the type from which the class derives for each type through the Attributes and BaseType properties.
The methods associated with the Type class enable you to get the associated fields, properties, interfaces, events, and methods. For example, the Customer type has no interfaces, properties, or events, but it has four fields, three constructors, and the methods inherited from its BaseType System::Object:
Interfaces: Fields: CustomerId FirstName LastName EmailAddress Properties: Events: Constructors: public .ctor(System.String first, System.String last, @@System.String email) public .ctor() public .ctor(System.Int32 id) Methods: public Int32 GetHashCode() public Boolean Equals(System.Object obj) public String ToString() public Type GetType()
The type Customers inherits from one interface and has one constructor and four of its own methods in addition to the four it inherited from its BaseType System::Object:
Interfaces: ICustomer Fields: Properties: Events: Constructors: public .ctor() Methods: public Void ChangeEmailAddress(System.Int32 id, @@System.String emailAddress) public ArrayList GetCustomer(System.Int32 id) public Void UnregisterCustomer(System.Int32 id) public Int32 RegisterCustomer(System.String firstName, @@ System.String lastName, System.String emailAddress) public Int32 GetHashCode() public Boolean Equals(System.Object obj) public String ToString() public Type GetType()
These were obtained with the GetInterfaces, GetFields, GetProperties, GetEvents, GetConstructors, and GetMethods methods on the Type class. Since an interface is a type, GetInterfaces returns an array of Types representing the interfaces inherited or implemented by the Type queried. Since fields, properties, events, and methods are not types, their accessor methods do not return Types. Each of their accessor methods returns an appropriate class: FieldInfo, PropertyInfo, EventInfo, ConstructorInfo, and MethodInfo. All these classes, as well as the Type class, inherit from the MemberInfo class that is the abstract base class for member metadata.
Let us examine some of the metadata associated with a class method. Using the reflection methods, we were able to reconstruct the signatures for all the classes and interfaces in the Customer assembly. Here is the output for the methods of the Customers class:
public Void ChangeEmailAddress(System.Int32 id, @@ System.String emailAddress) public ArrayList GetCustomer(System.Int32 id) public Void UnregisterCustomer(System.Int32 id) public Int32 RegisterCustomer(System.String firstName, @@ System.String lastName, System.String emailAddress) public Int32 GetHashCode() public Boolean Equals(System.Object obj) public String ToString() public Type GetType()
Here is the code from the example that produced the output:
for (int j = 0; j < methodInfo.Length; j++) { if (methodInfo[j]->IsStatic) Console::Write(" static "); if (methodInfo[j]->IsPublic) Console::Write(" public "); if (methodInfo[j]->IsFamily) Console::Write(" protected "); if (methodInfo[j]->IsAssembly) Console::Write(" internal "); if (methodInfo[j]->IsPrivate) Console::Write(" private "); Console::Write( "{0} ", methodInfo[j]->ReturnType->Name); Console::Write( "{0}(", methodInfo[j]->Name); ParameterInfo *paramInfo [] = methodInfo[j]->GetParameters(); long last = paramInfo->Length - 1; for (int k = 0; k<paramInfo->Length; k++) { Console::Write( "{0} {1}", paramInfo[k]->ParameterType, paramInfo[k]->Name); if (k != last) Console::Write(", "); } Console::WriteLine(")"); }
Except that a constructor does not have a return type, the exact same code reconstitutes the calling sequences for the class's constructors.
The MethodInfo class has properties that help us determine if the method is static, public, protected, internal, or private as well as determine the return type and method name. The method parameters are stored in a property array of type ParameterInfo class.
This example should also make it clear that types are assembly relative. The same type name and layout in two different assemblies are treated by the runtime as two separate types. When versioning assemblies, you have to be careful when mixing versioned types or the same types in two different assemblies.
All this metadata allows the CLR and the framework to provide services to your applications, because they can understand the structure of your types.
Late Binding
Reflection can also be used to implement late binding. Late binding is where the method to be called is determined during execution rather than at compilation time. It is one example of how metadata can be used to provide functionality. As the previous example demonstrates, you can extract the signature of a method associated with a type. The MethodInfo object has all the needed metadata for a class method. The Dynamic sample demonstrates a very simple example of late binding.3
We dynamically load an assembly and get the metadata for a method of a particular type:
// Load Customer assembly Assembly *a = Assembly::Load("Customer"); // Get metadata for Customers class and one method Type *t = a->GetType("OI.NetCpp.Acme.Customers"); MethodInfo *mi = t->GetMethod("GetCustomer");
One thing that C++ programmers need to remember when doing reflection programming is that when you deal with strings that contain namespaces or classes, you must use properly formatted strings that are understood by the reflection class methods. So, the fully qualified class name in the code above is OI.NetCpp.Acme.Customers rather than the C++ style format OI::NetCpp::Acme::Customers. Thus, the format used is like that of C#, not C++.
Using the reflection classes, we could have made this completely dynamic by arbitrarily picking types, methods, and constructors from the Customer assembly using the techniques of the last example, but we wanted to keep the Dynamic example simple. A more ambitious program could do something much more interesting, such as implement an assembly decompiler that generates Managed C++, C#, or VB.NET source code directly from a compiled assembly.
The System namespace has an Activator class that has overloaded CreateInstance methods to create an instance of any .NET type using the appropriate constructor. The Activator class is discussed in this chapter's section on remoting. We invoke a constructor with no arguments to create an instance of the Customers object.
Type *t = a->GetType("OI.NetCpp.Acme.Customers"); ... Object *customerInstance = Activator::CreateInstance(t);
We then build an argument list and use the Invoke method of the MethodInfo instance to call the GetCustomer method.
// invoke the method Object *arguments [] = new Object*[1]; int customerId = -1; arguments[0] = __box(customerId); Object *returnType = mi->Invoke( customerInstance, arguments);
Using the reflection methods, we get the type information for each field in a return structure. Note that the GetValue method of FieldInfo returns the data for a particular field in an object.
if (returnType->GetType() == Type::GetType("System.Collections.ArrayList")) { ArrayList *arrayList = dynamic_cast<ArrayList *>(returnType); for (int i = 0; i<arrayList->Count; i++) { Type *itemType = arrayList->get_Item(i)->GetType(); FieldInfo *fi [] = itemType->GetFields(); for (int j = 0; j < fi->Length; j++) { Object *fieldValue = fi[j]->GetValue(arrayList->get_Item(i)); Console::Write( "{0, -10} = {1, -15}", fi[j]->Name, fieldValue); } Console::WriteLine(); } }
Again, note the use of the single periods rather than double colons in the string System.Collections.ArrayList.
This code did not use any specific objects or types from the Customer assembly. We did use some knowledge about the assembly to keep the code simple to illustrate the main points. It should be clear, however, how to make this completely general.
You can take this one step further and use the classes that emit metadata (in System::Reflection::Emit). You can even dynamically create an assembly in memory, and then load and run it!