An Introduction to the .NET Framework Class Library

Fast Forward to Delphi for .Net - Part V

Article originally written by Corbin Dunn (Borland R&D Software Engineer).

This is the fifth article in the series (I, II, III, IV, VI) of articles designed with one goal in mind: to provide a quick and dirty introduction to the world of .Net programming with Delphi.

The .NET Framework Class Library (FCL) consists of a series of classes, interfaces, and value types that can be used to program with. The .NET Framework has types that allow one to easily:

  • Create extravagant graphical user interface applications
  • (System.Windows.Forms)
  • Access and manipulate data in various database formats (System.Data and System.Xml)
  • Dynamically query type information (System.Reflection)
  • Perform basic Input/Output operations (System.IO)
  • Perform operating system security checks (System.Security)
  • Create internet enabled applications (System.Net and System.Net.Sockets)
  • Create dynamic web based applications - also known as ASP.NET (System.Web)
  • Access basic data types, event handlers, and exceptions (System)

All types in the FCL are Common Language Specification (CLS) compliant. This allows any CLS compliant programming language to use the types, such as Delphi, C# or VB.

Basic FCL Namespaces and Types

Each class is arranged in a namespace that best describes its purpose, and the .NET Framework has a lot of namespaces. A basic fully qualified classname example would be System.Collections.ArrayList.
The part to the left of the last dot is generally the namespace, while the part to the right of the last dot is the classname.
An exception to this rule has to do with nested types. For example, if you have a namespace MyNamespace, with a type Foo that contains a nested type Bar, the fully qualified name of Bar is MyNamespace.Foo.Bar.
However, the namespace is just MyNamespace, not MyNamespace.Foo, which would be a classname.

All of the basic FCL classes are located in the root namespace System. Classes provided by third party developers are recommended to follow the namespace format of CompanyName.TechnologyName. For instance, Borland.Delphi.System is the fully qualified namespace for the Delphi System unit.

The FCL is based on a common type system. The common type system allows the framework to operate with multiple languages within a managed runtime.

Value Types versus Reference Types

Value types directly contain their data, and are generally allocated on the stack. Reference types store a reference to their data allocated on the heap. Value types generally are primitive types, such as basic ordinal types, while reference types are generally objects, such as System.Windows.Forms.Control. Since a value type contains a copy of its data, modifying a value type will only modify that particular instance. However, modifying a reference type will modify all instances that have a reference to that type.

For example, consider the following C# code using the value type System.Drawing.Point:

~~~~~~~~~~~~~~~~~~~~~~~~~
Point firstPoint = new Point(5, 5) ;
Point secondPoint = firstPoint;
firstPoint.X = 6;
~~~~~~~~~~~~~~~~~~~~~~~~~

On the second line, secondPoint is assigned the value of firstPoint; it receives a copy of the data in firstPoint, which in this case is the X value 5 and the Y value 5. On the third line, firstPoint.X is assigned a value of 6. This only modifies firstPoint.X; secondPoint.X still contains the value 5.

Consider the following C# code using a reference type, System.Windows.Forms.Control:

~~~~~~~~~~~~~~~~~~~~~~~~~
Control firstControl = new Control() ;
Control secondControl = firstControl;
firstControl.BackColor = Color.Red;
~~~~~~~~~~~~~~~~~~~~~~~~~

On the second line, secondControl is assigned the reference to firstControl. On the third line, modifying firstControl.BackColor also modifies secondControl.BackColor, since the type Control is a reference type.

In the FCL, all basic primitive types, such as an integer or boolean, are value types (including enumerations).

Most other classes are generally reference types, including System.String. When programming, creating a struct in C# or a record in Delphi allows you to create a value type, while creating a class allows you to make a reference type.

Interface types

Interfaces are abstract declarations that are implemented by classes. One can cast from an interface to an object and vice versa (unlike Win32 Delphi).

We now move to Boxing and using Delegates to create event handlers...

Boxing

It is possible to convert from a value type to a reference type, and vice versa, via boxing. Take this C# example:

~~~~~~~~~~~~~~~~~~~~~~~~~
object obj = null; // a reference type
int i = 5; // a value type
obj = i; // boxing
//i = obj; // compiler error
i = (int)obj; // unboxing: the value type is copied to i
~~~~~~~~~~~~~~~~~~~~~~~~~

On the third line, the value type is implicitly boxed to a reference type.

In C#, you cannot assign an object type to value type, so the fourth line is a compiler error. You must explicitly cast to the value type, which copies the data at that time.

Note that Delphi is very type safe, so consider the analogous Delphi .NET code:

~~~~~~~~~~~~~~~~~~~~~~~~~
var
   I: Integer;
  Obj: TObject;
begin
   I := 5;
   Obj := I; // Compiler error: Incompatible types
// I := Obj; // Expected compiler error
   I := Integer(Obj) ;
end.
~~~~~~~~~~~~~~~~~~~~~~~~~

The above code will not compile on the second line of code due to Delphi being very type safe. In this case, the programmer can do one of two things; explicitly cast I to a TObject:

Obj := TObject(I) ;

Or, add the compiler AUTOBOX directive any where in the source before the line:

{$AUTOBOX ON}

The AUTOBOX directive makes Delphi less type safe, and more like C#. Using it can add ease of use, and can also lead to more programming errors.

System.Delegate and System.EventHandler

The FCL provides delegates as a managed means for method pointers, which can be used as event handlers or callback functions in the .NET Framework. The basic implementation of System.Delegate allows one to add and subtract to a delegate, and call the methods contained in it.

System.Delegate is the base class for all delegates.

System.MulticastDelegate descends from System.Delegate and adds the ability to call multiple methods. If a delegate is non-multicast, it can contain a reference to only one method.

Declaring a Custom Delegate Type

Any .NET compiler should take care of delegate creation via a keyword or specific syntax. C# uses the delegate keyword:

~~~~~~~~~~~~~~~~~~~~~~~~~
public delegate void SampleEventHandler(object sender, EventArgs args) ;
~~~~~~~~~~~~~~~~~~~~~~~~~

First off, notice that the delegate's name ends with EventHandler. This is very common, and Microsoft recommends naming all delegates in this format for consistency; however, doing so is not required.

Also notice that the delegate’s method signature has a void return type, a first parameter of type object, and a second parameter of type EventArgs. This method signature is very common in the FCL, and is the Microsoft recommended format, but one is not required to use it. However, it is so common, that this event signature has its own delegate, System.EventHandler. By convention, the first parameter is always the instance who invoked the method. The second parameter contains arguments describing the event.

If you want to pass more information to delegate, you can descend from EventArgs and add the necessary properties in your custom EventArgs class:

~~~~~~~~~~~~~~~~~~~~~~~~~
public class MyEventArgs : EventArgs
{
    public MyEventArgs(string mySpecialValue)
   {
      MySpecialValue = mySpecialValue;
    }
    public string MySpecialValue;
}

public delegate void SampleSpecialEventHandler(object sender, MyEventArgs args) ;
~~~~~~~~~~~~~~~~~~~~~~~~~

When the delegate is then called, the callee can access args.MySpecialValue.

Declaring a custom delegate type in Delphi is very easy and familiar; you just make a standard method type declaration:

type
   TSampleEventHandler = procedure(Sender: TObject; Args: EventArgs) ;

Let's now see how to create and use a delegate instance...

Creating an instance of a delegate and using it

The standard Delphi event syntax creates a single-cast event:

~~~~~~~~~~~~~~~~~~~~~~~~~
type
   TSampleEventHandler = procedure(Sender: TObject; Args: EventArgs) ;

   TWinForm18 = class(System.Windows.Forms.Form)
   private
     FMultiSampleEventHandler: TSampleEventHandler;
     property MultiSampleEventHandler: TSampleEventHandler add FMultiSampleEventHandler remove FMultiSampleEventHandler;
   end;

   TWinForm18 = class(System.Windows.Forms.Form)
   private
     FSingleSampleEventHandler: TSampleEventHandler;
     property SampleEventHandler: TSampleEventHandler read FSingleSampleEventHandler write FSingleSampleEventHandler;
   end;
~~~~~~~~~~~~~~~~~~~~~~~~~

To create a multi-cast delegate, one must use the new keywords, add and remove:

~~~~~~~~~~~~~~~~~~~~~~~~~
type
   TSampleEventHandler = procedure(Sender: TObject; Args: EventArgs) ;

   TWinForm18 = class(System.Windows.Forms.Form)
   private
     FMultiSampleEventHandler: TSampleEventHandler;
     property MultiSampleEventHandler: TSampleEventHandler add FMultiSampleEventHandler remove FMultiSampleEventHandler;
   end;
~~~~~~~~~~~~~~~~~~~~~~~~~

As in standard Delphi property syntax, instead of simply referring to the field FMultiSampleEventHandler, one could refer to a method that does the actual add and remove operation and "roll out" the event. The implementation of the events is tricky if you don’t know the syntax:

~~~~~~~~~~~~~~~~~~~~~~~~~
procedure TWinForm18.AddMultiEvent(Value: TSampleEventHandler) ;
begin
   FMultiSampleEventHandlerByHand := TSampleEventHandler(Delegate.Combine(@FMultiSampleEventHandlerByHand, @Value)) ;
end;

procedure TWinForm18.RemoveMultiEvent(Value: TSampleEventHandler) ;
begin
   FMultiSampleEventHandlerByHand := TSampleEventHandler(Delegate.Remove(@FMultiSampleEventHandlerByHand, @Value)) ;
end;
~~~~~~~~~~~~~~~~~~~~~~~~~

Notice the use of @ to get the address of the event; if you don’t use this, the compiler will think you are trying to call the event. Also notice the cast to the event type.

In Delphi, one can easily assign to a single-cast delegate and get traditional Delphi behavior:

~~~~~~~~~~~~~~~~~~~~~~~~~
procedure TWinForm18.TWinForm18_Load(sender: System.Object; e: System.EventArgs) ;
begin
   SampleEventHandler := OnSampleEvent;
end;
~~~~~~~~~~~~~~~~~~~~~~~~~

But, to add to a multi-case delegate, one must uses Include(…) :

~~~~~~~~~~~~~~~~~~~~~~~~~
procedure TWinForm18.TWinForm18_Load(sender: System.Object; e: System.EventArgs) ;
begin
   Include(MultiSampleEventHandler, OnSampleEvent) ;
end;
~~~~~~~~~~~~~~~~~~~~~~~~~

The way to remove an event from a multi-cast delegate is to use Exclude(). First, check to make sure it isn’t null, and then call it like a method.

~~~~~~~~~~~~~~~~~~~~~~~~~
if Assigned(FSingleSampleEventHandler) then
     FSingleSampleEventHandler(Self, EventArgs.Create) ;
~~~~~~~~~~~~~~~~~~~~~~~~~

Our discussion continues with a view on .Net Attributes ...

Attributes are used to decorate types and type members, such as fields, properties and methods. Attributes are saved with the metadata inside a .NET Framework application or assembly. At runtime, one can query types or instances to see if they have certain attributes, or properties on those attributes.

General attribute syntax for both Delphi and C# is basically the same, and immediately precedes the symbol that it modifies:

~~~~~~~~~~~~~~~~~~~~~~~~~
[AttributeTypeName(Parameter, Parameter)]
public class Foo { ... }
~~~~~~~~~~~~~~~~~~~~~~~~~

One thing to note: Attributes, by convention, generally always end in the word Attribute. For example, DesignerAttribute and ComVisibleAttribute. Most compilers allow you to leave off the "Attribute" portion of the type name.

What is a good use of an attribute? Most development environments have a special design time class that allows you to manipulate a component or control at design time. The DesignerAttribute allows one to easily do this:

~~~~~~~~~~~~~~~~~~~~~~~~~
[DesignerAttribute("MyControlDesigner")]
public class MyControl : System.Windows.Forms.Control
~~~~~~~~~~~~~~~~~~~~~~~~~

When a WinForms designer attempts to create a designer for MyControl, it will find the DesignerAttribute on the class and create an instance of the MyControlDesigner for it.

Assembly level attributes are useful for other things, such as specifying the version, author, company name, etc.

for the assembly:

~~~~~~~~~~~~~~~~~~~~~~~~~
[assembly: AssemblyTitle("This is my cool assembly")]
[assembly: AssemblyDescription("It does neat stuff")]
[assembly: AssemblyCompany("Borland Software Corporation")]
[assembly: AssemblyVersion("1.0.3.5")]
~~~~~~~~~~~~~~~~~~~~~~~~~

Let's see how to get the attribute we are after from a class...

One can access attributes on a type or instance by using Reflection and the TypeDescriptor:

~~~~~~~~~~~~~~~~~~~~~~~~~
var
   Attributes: AttributeCollection;
begin
   Attributes := TypeDescriptor.GetAttributes(Self) ;
~~~~~~~~~~~~~~~~~~~~~~~~~

Note that you can pass an instance (such as Self, or this in C#) or a System.Type to TypeDescriptor.GetAttributes(..). It really only works on the Type, since attributes are meta-data on the type, and not the instance.

Once you have the AttributeCollection, you can browse them all with a few lines of code:

~~~~~~~~~~~~~~~~~~~~~~~~~
   for AnAttribute in Attributes do
   begin
     TextBox1.Text := TextBox1.Text + AnAttribute.ToString + #13#10;
   end;
~~~~~~~~~~~~~~~~~~~~~~~~~

In addition, if you want a particular attribute, you can directly access it by type:

~~~~~~~~~~~~~~~~~~~~~~~~~
var
   Attributes: AttributeCollection;
   DefaultEventAttr: DefaultEventAttribute;
begin
...
   DefaultEventAttr := DefaultEventAttribute(Attributes[TypeOf(DefaultEventAttribute)]) ;
   if DefaultEventAttr <> nil then Text := 'Has a Default Event name of: ' + DefaultEventAttr.Name;
~~~~~~~~~~~~~~~~~~~~~~~~~

Note that it may return null if the Type does not have that attribute applied to it.

If you need Attribute information for a particular member in a type, you can use reflection to find that event, and then use Attribute.GetCustomAttributes(..) to access the attributes that are on that method:

~~~~~~~~~~~~~~~~~~~~~~~~~
[TMyCustomMethodAttriute]
procedure TMainForm.Button1_Click(sender: System.Object; e: System.EventArgs) ;
var
   MyType: System.Type;
   ClickMethod: MethodInfo;
   MethodAttrs: array of Attribute;
   MethodAttr: Attribute;
   j: Integer;
begin
   { Use reflection to get the Button1_Click member}
   MyType := GetType;
   ClickMethod := MyType.GetMethod('Button1_Click', BindingFlags.NonPublic or BindingFlags.Instance) ;
   MethodAttrs := Attribute.GetCustomAttributes(ClickMethod) ;
   TextBox1.Text := '--- Method Attributes --- '#13#10;
   for j := 0 to Length(MethodAttrs) - 1 do
   begin
     MethodAttr := MethodAttrs[j];
     TextBox1.Text := TextBox1.Text + MethodAttr.ToString + #13#10;
   end;
~~~~~~~~~~~~~~~~~~~~~~~~~

When using Attribute.GetCustomAttributes(...), you can pass a second parameter telling to include attributes declared in ancestor types. In this example, it wouldn’t change anything, since the method isn’t inherited from an ancestor.

Your Own Attributes

It is very easy to create your own attribute. There are just a few things to keep in mind. First of all, every attribute must directly, or indirectly derive from System.Attribute:

~~~~~~~~~~~~~~~~~~~~~~~~~
{ This can only be applied to methods }
[AttributeUsage(AttributeTargets.Method)]
TMyCustomMethodAttriute = class(Attribute)
...
end;
~~~~~~~~~~~~~~~~~~~~~~~~~

Notice the AttributeUsageAttribute applied to the new custom attribute. This tells compilers where they can allow the attribute to be used.

Wrapping It Up

I hope you now better undertand the changes made to the Delphi language. If you have any questions, feel free to post on the Forum