The last chapter introduced Microsoft's .NET architecture. Now it is time to focus on the Delphi for .NET Preview that ships with Delphi 7. This chapter will cover specific changes that were made to the Delphi language to make it compatible with the Common Language Runtime. There have been some important additions to the language, such as namespaces and new visibility specifiers. Other long-standing features of the language had to be dropped because they are not supported in a type-safe environment.
This chapter discusses areas of the new compiler including the use of some of Microsoft's own class libraries for .NET and ASP support.
Bear in mind that the Delphi for .NET compiler is still being crafted as I write this. Borland's plan was to release the Preview compiler with Delphi 7 and to provide periodic updates to registered Delphi customers throughout the remainder of 2002 and in the early 2003 (before the Galileo project is completed). Some of the language features discussed here may not be implemented, or may be in varying states of completeness, depending on which version of the compiler you are working with. At the end of this chapter I list web resources you can monitor to keep abreast of the changes and latest news regarding Borland's Delphi for .NET compiler.
Note |
As was true in Chapter 24, most of this material comes from John Bushakra. |
We will begin by looking at some of the Delphi features that had to be dropped (deprecated) in order to be compatible with the Common Language Runtime (CLR). Then we'll look at new features added (or planned) in Delphi for .NET, which will shape the Delphi language in the near future, possibly also on the Win32 and Linux platforms.
Some of Delphi's types will not survive the transition to a managed, virtual execution system. So far, the following types are either known to be deprecated or have uncertain fates:
Pointers Pointers are considered unsafe types by the CLR, and all forms of pointer arithmetic are forbidden. Unsafe means the code cannot be verified for type safety. The final version of the Delphi for .NET compiler may support unsafe, unmanaged pointers, but in the meantime you can use dynamic arrays to get back some of the functionality of the (also deprecated) GetMem, FreeMem, and ReallocMem functions.
Types Based on file of File types based on the old Pascal file of syntax cannot be supported because the compiler has no way to determine the size of a given type on the target platform.
Pre-Delphi Object Syntax The pre-Delphi object syntax has been deprecated and will not be supported in the final release of the compiler. This syntax was introduced back in the Turbo Pascal era and allowed you to declare a new class with the syntax type MyClass = object;. Variables of this type were stack-based, contrary to the heap-based class type objects.
Real48 and Comp These types won't be supported. Real48 type is a 6-byte floating-point type. The Comp type will be replaced by Int64 in the future, as noted in the Delphi 7 Language Reference.
The following types, although not candidates for deprecation, will have changes made to their underlying implementation. These changes are almost transparent, but you can expect slightly different behavior in some circumstances:
Strings In Delphi for .NET, strings map to the CLR type System.String and are wide by default. This means they use 16 bits per character, like the WideString type in Delphi 7. In addition, all characters are wide by default.
Records Records are mapped to value types. Chapter 24, "The Microsoft .NET Architecture from the Delphi Perspective," talked about the two main categories of types specified by the Common Type System (CTS): reference types and value types. A record will be a value type on the .NET platform. The CLR demands that value types cannot have inheritance, but you can define methods on them (which, of course, is completely new to the Delphi language). Methods defined on value types must be declared as final. (The new final keyword is discussed in "New Delphi Language Features" section.)
TDateTime In Delphi, this type is based on the same implementation as Microsoft's DATE type (see Chapter 2, "The Delphi Language," for more details). The .NET platform uses a different implementation. The System.DateTime structure (a descendant of System.ValueType) measures time from midnight, January 1, 0001 C.E. (Common Era) to 11:59:59 p.m., December 31, 9999 C.E. On this clock, one tick equals 100 nanoseconds. Going forward, Delphi for .NET will transition to the .NET platform standard for measuring time. Date calculations that depend on the floating-point implementation will require your attention when porting to the .NET platform. In particular, if you are using Delphi's Trunc and Frac functions to separate the date and time portions of the floating-point value, then you could be in for some interesting bugs in the future.
Currency Currency will be mapped to the CLR type System.Decimal.
As is the case with the types listed in the previous section, some Delphi features that have been part of the language a long time cannot be ported to the .NET platform:
Variant Records Variant records with overlapping fields are not supported by the CLR. In general, you can't make any assumptions regarding the layout of fields in a record declaration, because the Just In-Time (JIT) compiler reserves the right to optimize things to suit the underlying platform.
ExitProc Things don't always happen when you'd like them to, or in the order you'd like them to. Such problems with unit initialization and finalization have been overcome (although there are still issues to be aware of, as I discuss later in this chapter), but ExitProc is not supported.
Dynamic Aggregation of Interfaces The CLR doesn't support dynamic aggregation of interfaces using the implements keyword, because it cannot be verified for type safety. A class must declare all interfaces it will implement.
ASM Statements ASM statements and inline assembly language are not supported by the Delphi for .NET Preview compiler. The future of asm in the final version is doubtful. The compiler would have to be able to mix managed IL (Intermediate Language) and unmanaged native CPU instructions, like Microsoft's Visual C++ compiler.
automated Keyword The automated keyword was created to support OLE Automation. It is not needed in the .NET environment. The same is true for the dispid keyword, which is used to dispatch COM Automation methods by number instead of by name. Notice that although they're no longer explicitly required, GUIDs are supported on the .NET platform; they appear as custom attributes on a type.
Direct Memory Access Functions Direct memory access functions like BlockRead, BlockWrite, GetMem, FreeMem, and ReallocMem, as well as Absolute, and Addr, all deal with unmanaged pointers, and so cannot be used with managed, safe code. The @ operator is available in the current Preview version of the compiler (but is not expected to remain in the final version), although you cannot type cast pointers or do any pointer arithmetic.
Note |
As discussed in Chapter 2, Delphi 7 provides a new set of compiler warnings to help you get ready to port your code. These warnings flag certain features and language constructs that are known to be unsafe on the .NET platform and therefore should be avoided. The warnings are turned off by default for a new Delphi 7 project but are active when you recompile an existing project. You can also turn them on with the {$WARN UNSAFE_CODE ON} compiler directive and similar directives. Refer to Chapter 2 for more details. |
The first release of the dccil compiler included new features required by the CLR, and more have been added in subsequent updates.
Namespaces play an important role in the .NET Framework. They allow the class hierarchy to be extended by multiple third parties without fear of conflicting symbol names. Windows and COM use a 16-byte GUID to uniquely identify components, and this magic number must be recorded in the system registry. On the .NET platform, the concept of namespaces—plus metadata and the hard-and-fast rules about locating assemblies—makes GUIDs obsolete.
Ironically, the idea of a Delphi unit is similar to the CLR's namespaces. It's not too far a leap, if you think of a unit as a container of symbols, and a namespace as a container of units. In Delphi for .NET, the namespace to which a unit belongs is declared in the unit clause:
unit NamespaceA.NamespaceB.UnitA;
The dots indicate the containment of one namespace within another, and ultimately of the unit within the namespace. The dots separate the declaration into components, and each component—up to but not including the rightmost one—is a namespace. The entire declaration taken as a whole, dots and all, is the unit name. The dots simply serve as separators; no new symbols are introduced by the declaration. In this example, NamespaceA.NamespaceB is the namespace, and NamespaceA.NamespaceB.UnitA is the name of the unit. NamespaceA.NamespaceB.UnitA.pas would be the name of the source file, and the compiler would produce an output file called NamespaceA.NamespaceB.UnitA.dcuil.
The program statement (and eventually the package and library statements) optionally declares the default namespace for the entire project. Otherwise, the project is called a generic project, and the default namespace is that specified by the –ns compiler option. If no default project namespace is specified with compiler options, then behavior reverts to not using namespaces, like in Delphi 7 (and prior releases).
The unit clause does not have to declare membership in any explicit namespace. It might look like a traditional Delphi statement:
unit UnitA;
A unit that does not declare membership in a namespace is called a generic unit. Generic units automatically become members of the project namespace. Note, however, that this does not affect the source filename.
Warning |
At the time of this writing, namespace support is very limited; this section describes how it should work in the future, rather than how it works now. |
In the project file, you can specify a namespaces clause to list a set of namespaces for the compiler to search when it is trying to resolve references to generic units. The namespaces clause must appear immediately after the program (or package or library) statement and before any other clause or block type. The namespaces are separated by commas, and the list is terminated with a semicolon. For example:
program NamespaceA.MyProgram namespaces Foo.Bar, Foo.Frob, Foo.Nitz;
This example adds the namespaces Foo.Bar, Foo.Frob, and Foo.Nitz to the generic unit search space.
This discussion leads up to showing you how the compiler searches for generic units when you build your program. When you use a unit and fully qualify its name with the full namespace declaration, there is no problem:
uses Foo.Frob.Gizmos;
The compiler knows the name of the dcuil file (or the .pas file) in this case. But suppose you only said the following:
uses Gizmos;
This is called a generic unit reference, and the compiler must have a way to find its dcuil file.
The compiler searches namespaces in the following order:
For the first item, if the current unit specifies a namespace, then subsequent generic unit references in the current unit's uses clause are looked for first in the current unit's namespace. Consider this example:
unit Foo.Frob.Gizmos; uses doodads;
The first search location for the unit doodads would be in the namespace Foo.Frob. So, the compiler would try to open Foo.Frob.Doodads.dcuil. Failing this, the compiler would move on and prefix the unit name doodads with the default project namespace, and so on down the list.
The same symbol name can appear in different namespaces. When such ambiguity occurs, you must refer to the symbol by its full namespace and unit name. If you have a symbol named Hoozitz in unit Foo.Frob.Gizmos, you can refer to the symbol with either
Hoozitz; // if the name is unambiguous Foo.Frob.Gizmos.Hoozitz;
but not with
Gizmos.Hoozitz; // error! Frob.Gizmos.Hoozitz; // error!
Unit and namespace names can become quite long and unwieldy. You can create an alias for the fully qualified name with the as keyword in the uses clause:
uses Foo.Frob.DepartmentOfRedundancyDepartment.UIToys as ToyUnit;
Unit aliases introduce new identifiers, so their names cannot conflict with any other identifiers in the same unit (aliases are local to their unit). Even if you declare an alias, you can still use the original, longer name to refer to the unit.
Note |
The case of a namespace declaration is preserved and emitted into assembly metadata as is. However, as far as Delphi is concerned, two namespaces that differ only in case are equivalent. |
The cross-language integration of the CTS and CLR brings up some interesting situations for compiler developers. For example, what if the name of an identifier in an assembly is the same as one of your language keywords? Consider the Delphi language keyword type. Type is also the name of a CLR class. Because type is a language keyword, it cannot be used as the name of an identifier. You can avoid this problem two ways in Delphi for .NET (these techniques were not implemented in Delphi 7 and previous versions).
First, you can use the fully qualified name of the identifier:
var T: System.Type;
The second, shorter way is to use the new ampersand operator (&) to prefix the identifier. The following has the same effect as the previous example:
var T: &Type;
In this statement the ampersand tells the compiler to look for a symbol with the name Type and to not consider it as a keyword. The compiler will look for the Type symbol in the available units, finding it in System (the same mechanism works regardless of the unit defining the symbol).
Two more concepts specified by the Common Language Infrastructure (CLI) have been added to the Delphi for .NET compiler: the class attribute sealed and the method attribute final. Putting the sealed attribute on a class effectively ends the class's ability to be used as a base class. Here is a sample code snippet:
type TDeriv1 = class (TBase) procedure A; override; end sealed;
A class cannot derive from a class that has been sealed. Similarly, a virtual method marked with the final attribute cannot be overridden in any descendant class, as in the following sample code.
type TDeriv1 = class (TBase) procedure A; override; final; end; TDeriv2 = class (TDeriv1) procedure A; override; // error: "cannot override a final method" end;
Borland added the sealed and final keywords to map an existing feature of .NET, but why did Microsoft introduce these attributes? The final and sealed attributes give users of your code important insights into how you intend your classes to be used. Moreover, these attributes give the compiler hints that allow it to generate more efficient Common Intermediate Language (CIL).
Delphi's notion of visibility—public, protected, and private—is a bit different from that of the CLI. In languages like C++ and Java, when you specify a visibility of private or protected on a class member, that class member is only visible to descendants of the class in which it is defined. As you saw in Chapter 2, however, Delphi enforces the idea of private and protected only for classes in different units, because everything is visible within a single unit. To be CTS compliant, the language required new visibility specifiers:
class private A member declared with class private visibility follows the C++ and Java rules. That is, class private members can be accessed only in methods or properties of the declaring class. Procedures and functions declared at the unit level and methods of other classes do not have access.
class protected Similarly, class protected members are visible only within the declaring class, and to descendants of the declaring class. Other classes in the same unit have access only if they inherit from this class.
See the ProtectedPrivate example in the LanguageTest folder of the chapter's source code for a trivial test case.
Delphi has long supported class methods—methods you can apply to a class as a whole and also as a specific instance, even if the methods' code cannot refer to the current object (the Self parameter of a class methods references the current class, not the current object). Delphi for .NET extends this idea by adding the class static specifier, class properties, class static fields, and class constructors:
Class Static Methods Like Delphi 7 class methods, class static members can be called without an object instance, and no Self parameter refers to an object. Unlike in Delphi 7, however, you cannot refer to the class itself. For example, calling the ClassName method will fail. Also unlike in Delphi 7, you cannot use the virtual keyword with class static methods.
Class Static Properties Like class methods, class static properties can be accessed without an object instance. The access methods or backing fields for class static properties must be declared class static themselves. Class static properties cannot be published, nor can they have stored or default value definitions.
Class Static Fields A class static field can be accessed without an object instance. Class static fields and properties are typically used as design tools; they allow you to declare variables and constants within the meaningful context of a class declaration.
Class Constructor A class constructor is a private constructor (it must be declared with class private visibility) that runs prior to the first use of the declaring class. The CLR offers no guarantee of when this will happen, except to say it will happen before the first use of the class. In CLR terms, this can get a bit tricky, because code is not considered "used" unless (and until) it is executed. A class can declare only one class constructor. Descendants can declare their own class constructors, but only one can be declared in any class.
You can't call a class constructor from source code; it is called automatically as a way to initialize class static fields and properties. Even the inherited keyword is prohibited, because the compiler takes care of this for you.
The following example class declaration illustrates the syntax for these new specifiers:
TMyClass = class class private // can only be accessed within TMyClass // Class constructor must have class private visibility class constructor Create; class protected // can be accessed in TMyClass and in descendants // Class static accessors for class static property P1, below class static function getP1 : Integer; class static procedure setP1(val : Integer); public // fx can be called without an object instance class static function fx(p : Integer) : Integer; // Class static property P1 must have class static accessors class static property P1 : Integer read getP1 write setP1; end;
Nested types are similar to class fields, in that they can be accessed through a class reference; an object instance is not needed. Declared within the scope of a class, nested types give you a way to use the enclosing class as a kind of namespace for the type.
Delphi has always had the ability to set an event listener—a function that is called when an event is fired. The CLR supports the use of multiple event listeners so that more than one function can respond when an event is fired. These are called multicast events. Delphi for .NET introduces two new property access methods, add and remove, to support multicast events. The add and remove methods can be used only on properties that are events.
To support multicast events, you must have a way to store all the functions that register themselves as listeners. As stated in Chapter 24, multicast events are implemented using the CLR MulticastDelegate class. And, as discussed there, the compiler hides a lot of complexity behind the scenes. The add and remove keywords handle the storage and removal of event listeners, but the containment mechanism is an implementation detail you aren't expected to deal with. The compiler automatically generates add and remove methods for you, and these methods implement storage of event listeners in an efficient way.
In the final release of Delphi for .NET, the add and remove methods should work hand in hand with an overloaded version of the standard functions Include and Exclude. In your source code, when you'd want to register a method as an event listener, you call Include. To remove a method, call Exclude. For example:
Include(EventProp, eventHandler); Exclude(EventProp, eventHandler);
Behind the scenes, Include and Exclude will call the methods assigned to the add and remove access functions, respectively. At the time of this writing, this technology wasn't working, so the book examples don't use it.
To support legacy code, the Delphi assignment operator (:=) still works as a way to assign a single event handler. The compiler generates code to go back and replace the last event handler (and only that event handler) that was set with the assignment operator. The assignment operator works separately and independently from the add/remove (or Include/Exclude) mechanism. In other words, the use of the assignment operator does not affect the list of event handlers that have been added to the MulticastDelegate.
As an example, you can refer to the XmlDemo program. The following code snippet (the working code at the time of this writing) creates a button at run time and installs two event handlers for its Click event:
MyButton := Button.Create; MyButton.Location := Point.Create ( Width div 2 - MyButton.Width div 2, 2); MyButton.Text := 'Load'; MyButton.add_Click (OnButtonClick); MyButton.add_click (OnButtonClick2); Controls.Add (MyButton);
Recall from Chapter 24 that one of the requirements of the CLI is an extensible metadata system. All .NET language compilers are required to emit metadata for the types defined within an assembly. The extensible part of extensible metadata means that programmers can define their own attributes and apply them to just about anything: assemblies, classes, methods, and more. The compiler emits these into the assembly's metadata. At run time, you can query for the attributes that were applied to an entity (assembly, class, method, and so on) using the methods of the CLR class System.Type.
Custom attributes are reference types derived from the CLR class System.Attribute. Declaring a custom attribute class is just like declaring any other class (this code snippet is extracted from the trivial NetAttributes project part of the LanguageTest folder):
type TMyCustomAttribute = class(TCustomAttribute) private FAttr : Integer; public constructor Create(val: Integer); property customAttribute : Integer read FAttr write FAttr; end; ... constructor TMyCustomAttribute.Create(val: Integer) begin inherited Create; customAttribute := val; end;
The syntax for applying the custom attribute is similar to that of C#:
type [TMyCustomAttribute(17)] TFoo = class public function P1(X : Integer) : Integer; end;
The custom attribute is applied to the construct immediately following it. In the example, it is applied to the class TFoo. No doubt you noticed that the custom attribute syntax is nearly identical to that of Delphi's GUID syntax. Here we have a problem: GUIDs are applied to interfaces; they must immediately follow the interface declaration. Custom attributes, on the other hand, must immediately precede the declaration to which they apply. How can the compiler determine whether the thing in the square brackets is a traditional Delphi-style GUID (which should be applied to the preceding interface declaration) or a .NET-style custom attribute (which should be applied to the first member of the interface)?
There is no way to tell, so you have to punt—make a special case for custom attributes and interfaces. If you apply a GUID to an interface, it must immediately follow the declaration of the interface, and it must follow the established Delphi syntax:
type interface IMyInterface ['(12345678-1234-1234-1234-1234567890ab)']
CLR's GuidAttribute custom attribute is used to apply GUIDs; it is part of the System.Runtime.InteropServices namespace. If you use this custom attribute to apply a GUID, then you must follow the CLR standard and put the attribute declaration before the interface.
Class helpers are an intriguing new language feature added to Delphi for .NET. The main reason for supporting class helpers is the way Borland maps .NET core classes with its own RTL classes, as covered later in the section "Class Helpers for the RTL." Here I will focus on this feature from a language perspective.
A class helper gives you a way to extend a class without using derivation, by adding new methods (but not new data). The odd fact, compared to inheritance, is that you can create objects of the original class, which is extended maintaining the same name. This means you can plug-in methods to an existing object of an existing class. A simple example will help clarify the idea.
Suppose you have a class (probably one you haven't written yourself—otherwise you could have extended it right away) like this:
type TMyObject = class private Value: Integer; Text: string; public procedure Increase; end;
Now you can add a Show method to objects of this class by writing a class helper to extend it:
type TMyObjectHelper = class helper for TMyObject public procedure Show; end; procedure TMyObjectHelper.Show; begin WriteLn (Text + ' ' + IntToStr (Value) + ' -- ' + Self.ClassType.ClassName + ' -- ' + ToString); end;
Notice that Self in the class helper method is the object of the class that the helper is for. You can use it like this:
Obj := TMyObject.Create; ... Obj.Show;
You'll end up seeing the name of the TMyObject class in the output. If you inherit from the class, however, the class helper will also be usable on the derived class (so you end up adding a method to an entire hierarchy), and everything will work properly. For your experiments, refer to the ClassHelperDemo example in the LanguageTest folder.
Located in the source tl directory of the Delphi for .NET Preview installation, you will find the source files for the run-time library (RTL). You can already see the migration of units into CLR namespaces, as reflected in the source filenames.
Borland is taking the approach (for the most part) of preserving the original unit name and prefixing it with the namespace name Borland.Delphi. Operating system–specific things (such as registry and ini file utilities) go in the Borland.Win32 namespace, because these classes, procedures, and functions are Borland-specific wrappers of Windows-specific features. The naming trend should continue, although not all units will make the transition, and some will have their contents reorganized into an appropriate namespace.
Perusing the RTL source files is both highly educational and highly recommended; however, remember that you are looking at a preview release of a product, not the final version. The contents of the RTL source files are still subject to change—you should not make any assumptions, and you should definitely not introduce dependencies into your own code based on what you see there.
With the warnings out of the way, let's see what has been done to the RTL so far. The most interesting change is perhaps the introduction of class helpers.
Borland.Delphi.System.pas includes the following declaration:
type TObject = System.Object;
It tells you that Delphi's TObject class is an alias for the CLR class System.Object. This is important: TObject is not a descendant of System.Object—it is semantically equivalent. What happened to the methods that used to be defined in TObject, such as ClassName and ClassParent? That's where the class helper comes in.
The methods that used to be directly declared and implemented in TObject are now declared and implemented in a class called TObjectHelper. TObjectHelper is then declared to be a class helper for TObject. In Borland.Delphi.System.pas is the following:
type TObjectHelper = class helper for TObject procedure Free; function ClassType: TClass; class function ClassName: string; class function ClassNameIs(const Name: string): Boolean; class function ClassParent: TClass; class function ClassInfo: TObject; class function InheritsFrom(AClass: TClass): Boolean; class function MethodAddress(const Name: string): TObject; class function SystemType: System.Type; function FieldAddress(const Name: string): TObject; procedure Dispatch(var Message); end;
A class helper gives you a way to extend a class without using derivation.
You might want to extend a CLR class but not derive from it in order to use the CLR class with existing Delphi code. No doubt you've noticed that Delphi's class framework and the .NET Framework share a fair amount of functionality. In some cases there are name clashes between the two—for example, Borland's Exception class and the CLR's System.Exception class. On one hand, the two classes do basically the same thing, but they expose that functionality in different ways. On the other hand, large amounts of existing Delphi code have been using Borland's Exception class for a long time.
The only workable solution was to create a mechanism that would allow developers (including Borland) to leverage the CLR classes, and that would also allow the CLR classes to be extended to include long-standing behaviors expected by existing Delphi code.
The .NET Framework classes in the System.Windows.Forms namespace are not a replacement for the GUI portion of the Win32 API. The same happens with most other portions of the .NET Framework: its role is to make the underlying API easier to use by providing an easier object-oriented interface compatible with the core services of the .NET environment. The GUI subset of Win32 is still there, occupying the same place it always has. System.Windows.Forms organizes this GUI subset of the Win32 API, presents it in an object-oriented way, and layers an event model on top of it; but classes in System.Windows.Forms call the unmanaged code in Win32. When you use System.Windows.Forms, you are still calling the Win32 APIs, but now you have a large layer of software called the CLR sitting between your code and Win32.
This entire preamble is important to acknowledge why the VCL takes this same approach. TObject is rooted (if you will) from System.Object through the use of a class helper. TPersistent and TComponent still descend from there. Thus the VCL class TForm, for example, is not a descendant of the class System.Windows.Forms.Form. Instead, the entire VCL hierarchy remains much as it is today. TForm will ultimately descend from TWinControl, which itself is a descendant of TControl and then TComponent.
If you consider the entire System.Windows.Forms namespace as a single entity, the VCL then becomes a kind of sibling to it, rather than a child of it. Both frameworks ultimately rely on the native, unmanaged Win32 API for the underlying implementation of the user interface controls.
The update of the Delphi .NET Preview compiler made available by Borland in November 2002, although still preliminary, provides details about the architecture the company is planning. If you open the Borland.Vcl.Controls unit, you'll be surprised by its similarity to the Win32 version. The source code is almost identical; the differences exist behind the scenes at the TObject and TComponent level. I've already covered the former, so let's focus on the core component class, which is defined in three steps:
type TComponent = System.ComponentModel.Component; TComponentHelper = class helper (TPersistentHelper) for TComponent TComponentSite = class(TObject, ISite, IServiceProvider)
The TComponent class corresponds to the .NET Framework class, with a helper providing extra methods and properties and a further class offering the extra data required by the helper class. The situation is complex, and I don't want to get into the details because the TComponent class is marked as experimental and may change in further updates.
Getting back to the VCL, a large set of components is already available, so you can begin porting code. The only trouble you'll face with the November 2002 update is that streaming is not supported; so, you must add the component-creation code in the form constructor (an action that will not be required by the final version of Delphi for .NET).
As an interesting example to help you figure out the architecture of the VCL classes, I've ported to .NET the ClassInfo example from Chapter 3, "The Run-Time Library." The NetClassInfo example uses this modified code in the project source (again, something you won't have to do in the future):
Application.Initialize; Form1 := TForm1.Create (Application); Application.MainForm := Form1;
The code for the form, as I've mentioned, has an extra method called by the constructor and used to initialize the controls. This method is quite long, so I'll provide only a few excerpts here:
procedure TForm1.InitializeControls; begin // creating all controls... Label3:= TLabel.Create(Self); Panel1:= TPanel.Create(Self); Label1:= TLabel.Create(Self); Label2:= TLabel.Create(Self); ... // setting form properties and events Left:= 217; Top:= 109; Caption:= 'Class Info'; OnCreate:= FormCreate; // initializing controls (only one is listed here) with Label3 do begin Parent:= Self; Left:= 8; Top:= 8; Width:= 56; Height:= 13; Caption:= 'Class Name'; end;
The rest of the application's code remains almost identical, which is surprising considering that this is a low-level example. I had to remove the call to InstanceSize, because the compiler cannot resolve the size of an object given the architecture of .NET, and I had to test for the base class against Object instead of TObject. Here is the code snippet that produces the output shown in Figure 25.1:
procedure TForm1.ListClassesClick(Sender: TObject); var MyClass: TClass; begin MyClass := ClassArray [ListClasses.ItemIndex]; EditInfo.Text := Format ('Name: %s - Size: %d bytes', [MyClass.ClassName, 0 {MyClass.InstanceSize}]); with ListParent.Items do begin Clear; while MyClass.ClassName <> 'Object' do begin MyClass := MyClass.ClassParent; Add (MyClass.ClassName); end; end; end;
Figure 25.1: The NetClassInfo example shows the base classes of a given component.
To provide starting points for your own experiments with the VCL under .NET, I've built two more examples. NetEuroConv is a port of the EuroConv example from Chapter 3 based on the RTL's conversion engine. NetLibSpeed is a port of the LibSpeed example used in Chapter 5 ("Visual Controls") to compare the VCL and VisualCLX libraries' speed in creating visual components. The number you'll see makes little sense in such a preliminary version of a library, although the fact that VCL.NET takes four to five times as long for the same purpose may worry you.
As I mentioned, these examples are meant to be only starting points for your experiments. They may not work with further updates of the Delphi for .NET Preview.
Note |
Stay tuned to my website for updates of this section of the book and the related examples. |
The VCL is not quite ready, but you can use the .NET Framework class library as a basis for experimentation with the Delphi for .NET Preview compiler. It can be educational to build programs with the compiler and then inspect them with Intermediate Language Disassembler (ILDASM), for instance. This will be the aim of this section. If you want to look at a simpler example using XML support, refer to the XmlDemo mentioned earlier in the chapter.
The CLRReflection program opens an assembly and then uses reflection to inspect the modules and types defined within that assembly. This program demonstrates using a common dialog box (the OpenFileDialog), constructing menus, handling events, using Delphi's dynamic arrays, and, of course, reflection. Let's look at the project file first:
program CLRReflection; uses System.Windows.Forms, ReflectionUnit; var reflectForm : ReflectionForm; begin reflectForm := ReflectionForm.Create; System.Windows.Forms.Application.Run(reflectForm); end.
The code looks almost like a good old VCL application. You define a variable for your main form, and then you create the form. Then you use the Run method of the .NET Framework class System.Windows.Forms.Application. Here the code is analogous (at least in concept) to the way it is done in the VCL.
Note that throughout this example I have given the fully qualified name for .NET Framework classes. I did so to make sure you know where these classes are located. Because the uses clause includes System.Windows.Forms, you could shorten the expression
System.Windows.Forms.Application.Run(reflectForm);
to
Application.Run(reflectForm);
Now, look at Listing 25.1, which shows the unit where the main form is defined. Note that this code compiles with the November 2002 update of the Delphi for .NET Preview, but not with the version originally shipping with Delphi 7.
Listing 25.1: The ReflectionUnit Unit of the CLRReflection Example
unit ReflectionUnit; interface uses System.Windows.Forms, System.Reflection, System.Drawing, Borland.Delphi.SysUtils; type ReflectionForm = class(System.Windows.Forms.Form) private mainMenu: System.Windows.Forms.MainMenu; fileMenu: System.Windows.Forms.MenuItem; separatorItem: System.Windows.Forms.MenuItem; openItem: System.Windows.Forms.MenuItem; exitItem: System.Windows.Forms.MenuItem; showFileLabel: System.Windows.Forms.Label; typesListBox: System.Windows.Forms.ListBox; openFileDialog: System.WIndows.Forms.OpenFileDialog; protected procedure InitializeMenu; procedure InitializeControls; procedure PopulateTypes(fileName: String); { Event Handlers } procedure exitItemClick(sender: TObject; Args: System.EventArgs); procedure openItemClick(sender: TObject; Args: System.EventArgs); public constructor Create; end; implementation constructor ReflectionForm.Create; begin inherited Create; SuspendLayout; InitializeMenu; InitializeControls; { Initialize the form and other member variables } openFileDialog := System.Windows.Forms.OpenFileDialog.Create; openFileDialog.Filter := 'Assemblies (*.dll;*.exe)|*.dll;*.exe'; openFileDialog.Title := 'Open an assembly'; AutoScaleBaseSize := System.Drawing.Size.Create(5, 13); ClientSize := System.Drawing.Size.Create(631, 357); Menu := mainMenu; Name := 'reflectionForm'; Text := 'Reflection in Delphi for .NET'; { Add the controls to the form's collection. } Controls.Add(showFileLabel); Controls.Add(typesListBox); ResumeLayout; end; { Build the main menu } procedure ReflectionForm.InitializeMenu; var menuItemArray : array of System.Windows.Forms.MenuItem; begin mainMenu := System.Windows.Forms.MainMenu.Create; fileMenu := System.Windows.Forms.MenuItem.Create; openItem := System.Windows.Forms.MenuItem.Create; separatorItem := System.Windows.Forms.MenuItem.Create; exitItem := System.Windows.Forms.MenuItem.Create; { Initialize mainMenu } mainMenu.MenuItems.Add(fileMenu); { Initialize fileMenu } fileMenu.Index := 0; SetLength(menuItemArray, 3); menuItemArray[0] := openItem; menuItemArray[1] := separatorItem; menuItemArray[2] := exitItem; fileMenu.MenuItems.AddRange(menuItemArray); fileMenu.Text := '&File'; // openItem openItem.Index := 0; openItem.Text := '&Open...'; openItem.add_Click(openItemClick); // separatorItem separatorItem.Index := 1; separatorItem.Text := '-'; // exitItem exitItem.Index := 2; exitItem.Text := 'E&xit'; exitItem.add_Click(exitItemClick); end; { Create the controls and populate the form } procedure ReflectionForm.InitializeControls; begin { Initialize showFileLabel } showFileLabel := System.Windows.Forms.Label.Create; showFileLabel.Location := System.Drawing.Point.Create(5, 6); showFileLabel.Name := 'showFileLabel'; showFileLabel.Size := System.Drawing.Size.Create(616, 37); showFileLabel.TabIndex := 0; showFileLabel.Anchor := System.Windows.Forms.AnchorStyles.Top or System.Windows.Forms.AnchorStyles.Left or System.Windows.Forms.AnchorStyles.Right showFileLabel.Text := 'Showing types in: '; { Initialize typesListBox } typesListBox := System.Windows.Forms.ListBox.Create; typesListBox.Anchor := System.Windows.Forms.AnchorStyles.Top or System.Windows.Forms.AnchorStyles.Bottom or System.Windows.Forms.AnchorStyles.Left or System.Windows.Forms.AnchorStyles.Right; typesListBox.Location := System.Drawing.Point.Create(8, 46); typesListBox.Name := 'typesListBox'; typesListBox.Size := System.Drawing.Size.Create(610, 303); typesListBox.Font := System.Drawing.Font.Create('Lucida Console', 8.25, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0); typesListBox.TabIndex := 1; end; { Event handler for the Exit menu item } procedure ReflectionForm.exitItemClick(sender: TObject; Args: System.EventArgs); begin System.Windows.Forms.Application.Exit; end; { Event handler for the Open menu item } procedure ReflectionForm.openItemClick(sender: TObject; Args: System.EventArgs); begin if openFileDialog.ShowDialog = DialogResult.OK then begin showFileLabel.Text := 'Showing types in: ' + openFileDialog.FileName; PopulateTypes(openFileDialog.FileName); end; end; { Open the given assembly, and reflect over its modules } { and types. } procedure ReflectionForm.PopulateTypes(fileName : String); var assy: System.Reflection.Assembly; modules: array of System.Reflection.Module; module: System.Reflection.Module; types: array of System.Type; t: System.Type; members: array of System.Reflection.MemberInfo; m: System.Reflection.MemberInfo; i,j,k: Integer; s: String; begin try { Clear the listbox } typesListBox.BeginUpdate; typesListBox.Items.Clear; { Load the assembly and get its modules } assy := System.Reflection.Assembly.LoadFrom(fileName); modules := assy.GetModules; {For every module, get all types } for i := 0 to High(modules) do begin module := modules[i]; types := module.GetTypes; { For every type, get all of its members } for j := 0 to High(types) do begin t := types[j]; members := t.GetMembers; { for every member, get type information and add to list box } for k := 0 to High(members) do begin m := members[k]; s := module.Name + ':' + t.Name + ': ' + m.Name + ' (' + m.MemberType.ToString + ')'; typesListBox.Items.Add(s); end; end; end; typesListBox.EndUpdate; except System.Windows.Forms.MessageBox.Show('Could not load the assembly.'); end; end; end.
The unit begins by declaring its dependency on .NET Framework dcuil files and on the Borland.Delphi.SysUtils unit. From there it goes straight into declaring the class for the main form, which is a descendent of the .NET Framework class, System.Windows.Forms.Form. The form class layout looks familiar: You have member variables for all the controls, and these are declared to be of types found in the .NET Framework class library.
The functions exitItemClick and openItemClick are event handler declarations. The signature of event handler methods is specified by the CLR. All event handlers are procedures that take two parameters: the object that fired the event (a derivative of System.Object) and the event arguments, which are wrapped in the System.EventArgs (or a derived) class. (You will see how to hook up these event handlers in a moment.)
Let's move on to the class constructor. I must call attention to the first statement in the constructor, which calls inherited Create.
Warning |
Here you see a major departure from the .NET Framework way of life, compared to what you are used to with the VCL and with Delphi in general. In Delphi, the constructor initializes member variables, putting the object instance into a known-good state; it does not do any memory allocation. So, it is not uncommon to see a constructor make assignments and then call the inherited constructor. Indeed, you might not call the inherited constructor at all. In Delphi for .NET, you can't get away with this approach. In your constructor, you must call inherited Create, and it must be the method's first executable statement. Currently, if you fail to do so, you will get a compiler error saying that Self is uninitialized and that the inherited constructor must be called prior to accessing any ancestor fields. |
After calling the inherited constructor, you are back in familiar territory. Although this code uses a different class hierarchy, it should be clear to any Delphi programmer. You make an instance of System.Windows.Forms.OpenFileDialog by calling the Create constructor—this is how you create an instance of any .NET Framework class.
The next few lines demonstrate setting properties, both of the OpenFileDialog object instance and of the form itself. Finally, you add two controls (a label for the filename and the ListBox that will hold the assembly) to the form's Controls collection, which is a property of type Control.ControlCollection.
The InitializeMenu procedure demonstrates allocation and layout of a System.Windows.Forms
.MainMenu object instance. Where the File menu is initialized, a dynamic array holds each menu item. The dynamic array is then passed to the AddRange method. This code could have been accomplished by calling the Add method separately for each menu item.
The next interesting thing in InitializeMenu is the wiring of the menu item event handlers. In Chapter 24 and earlier in this chapter, I mentioned the behind-the-scenes complexity involved with delegates and multicast events. Here you see some of that complexity coming to the foreground.
You can't do it yet in Delphi for .NET, but in other .NET languages such as C#, you can use the language keyword event to introduce an event handler delegate. The event declaration specifies a delegate to use as a callback mechanism. Because the event is a System.MulticastDelegate derivative (a System.EventHandler delegate in this case), other objects can add and remove event handlers, and these handlers are called when the event fires.
The C# language adds a bit of syntactic sugar to help this pill go down more easily. C# defines += and -= operators for adding and removing event handlers, respectively. Eventually Delphi will get its own spoonful of sugar, with the Include/Exclude mechanism mentioned previously. CTS mandates that all .NET compilers targeting this event model must generate methods named add_ and remove_. These add_ and remove_ methods wrap the Combine and Remove methods declared in System.Delegate.
For now, to assign an event handler, you must use these add_ and remove_ methods; ordinarily, you would not concern yourself with them, because the compiler would hide this complexity. In the current class declaration, you introduce two methods whose signatures match the System.EventHandler delegate: openItemClick and exitItemClick. You then call the add_Click method on the respective menu item, passing your event handler as the callback method.
Now that the setup is out of the way, let's look at the code that reflects over the types defined within an assembly. You can load any assembly (thus creating an object instance), given its filename, with the static LoadFrom method. Once you have an assembly object, the keys to the kingdom are yours; you can use reflection to look over the assembly from any angle.
The collection of modules contained within an assembly is available with the GetModules method. From there you can drill down to the types defined in the module with GetTypes. As you saw in the InitializeMenu procedure, you can use dynamic arrays for properties that expose a collection with a System.Array.
Finally, each individual member of the module and types arrays contains a Name property, which you can use to build a string to display in the ListBox. The final effect of the code is visible in Figure 25.2.
Figure 25.2: The CLRReflection example, with an assembly loaded
You might like it or not (I'm not that fond of it), but Microsoft's ASP technology plays a significant role in the development of web applications, as least on the Windows platform. With the transition to ASP.NET, this technology has fully embraced the .NET Framework; now, with the availability of a Delphi compiler for .NET, the Delphi language can be your language of choice in the development of ASP applications.
To create a test, configure IIS to support ASP.NET (I won't cover these steps, which are beyond the scope of this book, but you can find more information on www.asp.net) and then place in the target folder the web.config file distributed by Borland along with the Delphi for .NET Preview (and available in the aspx subfolder). This configuration file defines the mapping of the language to a specific library, again provided by Borland. The core of the file (which uses XML format) has the following elements:
To test that the configuration is correct, nothing is better than trying an example. Create a new file (I've called mine aspbase.aspx, available in the DelphiAsp folder of the chapter source code) and type something like the following:
<% HelloMessage('Delphi for .NET Preview made this'); %>
The effect is to execute the Delphi code after transforming this file into a .NET source, compiling it with the Delphi preview compiler, and compiling the IL into assembly code (remember, even scripts in .NET are compiled before they are executed). If everything goes well, the browser should display output like that shown in Figure 25.3.
Figure 25.3: The aspbase.aspx example in a browser
From this point on, you can do anything an ASP.NET application provides. The only other example I want to show you is the use of controls with event handlers, which I've covered in a different situation for .NET applications based on Windows forms.
This example, saved in the aspui.aspx file in the AspDelphi folder, uses HTML to define a form with a textbox (that is, an edit control) and a button, plus an output label. The button has a Delphi language event handler attached to it, which moves the user input to the label (the output of the program appears in Figure 25.4):
runat="server">
Figure 25.4: The output of the aspui.aspx example, after typing in the edit box and clicking the button
This has been a limited introduction to ASP.NET with the Delphi language provider. However, it should give you a feeling for the possibilities opening up for Delphi programmers in this new world of .NET.
While you're waiting for the Delphi for .NET product, currently code-named Galileo, you can begin experimenting with the Delphi for .NET Preview that ships with Delphi 7 (and the subsequent updates made available by Borland). Of course, you should stay tuned to Borland's Developer Network website (bdn.borland.com) and newsgroups and to the author's site for update information about this area of Delphi, which is definitely a work in progress.
Just as Borland wants to provide the best tools to developers, I hope this book has helped you master Delphi, the most successful tool Borland has brought to the market in the last few years. Remember to check from time to time the reference, foundations, and advanced material I've collected on my website (www.marcocantu.com). Much of this material could not be included in this book, because of space constraints; see Appendix C, "Free Companion Books on Delphi," for more information.
Appendixes A and B discuss some of the add-ins I've built, which are freely available on my site, and a few other notable free Delphi tools. Also check my site for updates and integration of the material in the book, and feel free to use the newsgroups hosted there for your questions about the book and about Delphi in general.
Part I - Foundations
Part II - Delphi Object-Oriented Architectures
Part III - Delphi Database-Oriented Architectures
Part IV - Delphi, the Internet, and a .NET Preview