O'Reilly logo

.NET Windows Forms in a Nutshell by Matthew Adams, Ian Griffiths

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

The .NET Type System

So what does a type look like in .NET? In many respects, types are very similar to C++ classes: just like a C++ class, a .NET type is a collection of members, which may be fields (i.e., they hold data of some type), methods (i.e., they contain code), or nested type definitions, and all members have some level of protection (e.g., public, private, protected). However there are a number of differences between the C++ and the .NET type systems. The following sections describe the main features of types in .NET.

Members of Types

Any type will need to define some members to be of any use. Members are either associated with data or behavior. In C++ this means fields and methods, respectively. In addition to these, which the CLR supports, the CLR adds some new member types. All these member types are described here.

Methods

Methods are where we define code. In most .NET languages, all code must be defined in a method of some type. (Because properties also can contain code, they would appear to be an exception, but they are actually implemented by .NET language compilers as method calls.) As with C++, the method must have a signature (consisting of its name and the types of parameters it takes), and that signature must be different from any other methods defined in the same class. Overloading is allowed, i.e., the names of two methods can be the same if their signatures are different. Methods must also have a return type (even if the method returns void or Nothing); overloading based on return type alone is not allowed. (C++ doesn’t allow this either.) Note that methods that return void or Nothing in VB are declared using the Sub statement rather than the Function statement.

Methods can either be instance methods or static methods. (Instance methods are the default, but you can use the static keyword in C# or the Shared keyword in Visual Basic to specify a static method.) Instance methods are invoked with respect to a particular object or value, and they have access to that object through the this keyword in C# and the Me operator in VB. They can also refer to members simply by their names—if they are instance members, the this or Me reference will be used implicitly. Static methods do not need an object in order to be invoked, but they will only have access to other static members of the class. Visual Basic is not trying to maintain any look-and-feel compatibility with C++, so it uses the rather more sensible Shared keyword for members that are shared across all instances of a class.

Here is an example C# method declaration in a class:

public class MyFirstClass

{

    public int MyMethod (string s)

    {

        return int.Parse(s);

    }

}

The equivalent VB code is:

Public Class MyFirstClass

    Public Function MyMethod(s As String) As Integer

        Return Integer.Parse(s)

    End Function

End Class

The method takes a string as a single parameter and returns an integer. The code for the method attempts to convert the string to an integer by using the C# int type’s or VB Integer type’s static Parse method. (Both int and Integer are identical to the .NET Framework’s System.Int32 type.) MyMethod is an instance method—users of MyFirstClass will need an instance of MyFirstClass to call this method.

The public keyword in both languages indicates that any code is allowed to call this method. We will talk more about such protection keywords towards the end of this section.

Fields

Fields hold data. As with methods, fields can be either instance or static. If a field is declared as static, it is singular—all instances of the class or value will share the same piece of data, and that data will be accessible to instance and static methods alike. But instance fields (the default) are stored as part of each instance of the type, so every instance has its own set.

A field must have a name and a type. Here is an example instance field, along with a method that uses the field:

public class MySecondClass

{

    private int x;



    public int IncrementTotal(int val)

    {

        x = x + val;

        return x;

    }

}

The equivalent VB code is:

Public Class MySecondClass

    Private x As Integer



    Public Function IncrementTotal(val As Integer) As Integer

       x = x + val

       return x

    End Function

End Class

This class defines a private instance field called x, which can store an integer. The method IncrementTotal adjusts this field and returns its value. The code does not use the this or Me reference; it just refers to x by name. The compiler will detect that the code refers to the instance field x and presume that the author meant this.x or Me.x.

Properties

It is considered good practice never to expose a data field as a public member of an object, because that would cause client code to become too tightly coupled with that type’s implementation. Exposing properties through get and set methods is a popular technique for allowing components’ implementations the flexibility to evolve while still providing public members that feel like fields.

Just as COM did, .NET specifies a standard way of exposing properties through methods. And as with COM, some languages (including Visual Basic .NET and C#) provide special syntax to support this, allowing field-like syntax to be used when reading or writing properties, even though they are implemented in terms of methods. So in C#, we can provide properties like this:

public class ClassWithProperties

{

    public int MyProp

    {

        get

        {

            return 42;

        }

        set

        {

            Console.WriteLine("MyProp set to {0}. That's nice",

                value);

        }

    }

}

And in VB, we can do it like this:

Public Class ClassWithProperties

    Public Property MyProp() As Integer

        Get

           Return 42

        End Get

        Set

            Console.WriteLine("MyProp set to {0}. That's nice", _

                Value)

        End Set

    End Property

End Class

This defines an int or Integer property called MyProp. Note that value is a keyword in C# and VB, and it is used in property set functions. It is the value that the caller is trying to give the property. (In this case, we are just writing that value to the console.)

Tip

The use of {0} in the string passed to Console.WriteLine indicates that the parameter following the string should be inserted into the output at this point. It has a similar role to placeholders such as %d in the format string for printf in C.

The syntax for using properties in C# is exactly the same as for accessing fields:

MyClass obj = new ClassWithProperties();

int val = obj.MyProp;

obj.MyProp += 99;

The same is true of VB:

Dim obj As New ClassWithProperties()

Dim val As Integer = obj.MyProp

objMyProp += 99

In this particular example, there is no field. (Feel free to implement your own properties using private fields internally.) This will just run the get and set methods defined for the property. In this case, reading the property will always get the value 42, and writing it will just cause a message to be printed. Most properties behave more usefully, of course, but the point is the client code is not dependent on how the property works—it could rely on a normal field, derive the value from those in other fields, or retrieve the value from a database. The client just accesses the property, and the object can handle that however it sees fit.

Event handling

Components often need to notify client code when something interesting has happened. This is particularly common in user interface code—applications need to know when buttons are clicked, when windows are resized, when text is typed in, and so on. .NET defines a standard way in which objects can deliver event notifications to their clients. Visual Basic and C# both have special syntax for declaring and consuming such events. These two syntaxes are quite different—C# presents the CLR’s event handling mechanisms directly, while VB uses a style that is much more like the event handling in previous versions of VB. However, both languages are based on the same fundamental mechanisms, so they have much in common.

A class that wishes to be able to raise events (most Windows Forms controls do this) must declare the fact by adding a special member for each type of event it can raise. In C#, we use the following syntax:

public class EventSource

{

    public event MouseEventHandler MouseDown;

    . . .

}

In Visual Basic, the equivalent event declaration looks like this:

Public Class EventSource

    Public Event MouseDown As MouseEventHandler

    . . .

End Class

Both examples declare an event whose name is MouseDown and whose type is MouseEventHandler. (The MouseEventHandler type is defined in the System.Windows.Forms namespace, and we will see its definition later.) As it happens, all Windows Forms controls support this event—it is raised whenever a mouse button is pressed while the cursor is over the control.

When an event occurs, the event source notifies the client by calling the relevant handler function. The way we determine which particular function it calls is different in VB and C#. In VB, the class that wishes to receive the event simply uses the WithEvents keyword to indicate that it is interested in events from the event source object. It then identifies a particular method as being the handler for a given event using the Handles keyword. The signature of the handler method must match the type of the event. In this case, the event is of type MouseEventHandler. (We will look at this type’s definition shortly.) So our code looks like this:

Public Class EventReceiver

    Private WithEvents src As EventSource

    . . .

    Private Sub src_OnMouseDown( _

        sender As Object, e As MouseEventArgs) _

        Handles src.MouseDown

        Console.WriteLine("src object raised MouseDown event")

End Class

This style is similar to how previous versions of Visual Basic handled events. However, it hides the details of how events really work. C# does not provide such a level of abstraction—it exposes the CLR’s underlying mechanisms directly. Consequently, we need to do slightly more work in C# to handle events. Moreover, we must understand the mechanism on which events are based.

The CLR provides a special kind of object that is used to connect an event source to its corresponding event handler method. These special objects are called delegates. Delegates are .NET’s nearest equivalent to function pointers—they hold typed references to functions. As with a C++ function pointer, a delegate’s type (MouseEventHandler, in this case) determines the signature that the client’s handler function must have. MouseEventHandler is defined (in System.Windows.Forms) thus:

public delegate void MouseEventHandler(

    object sender, MouseEventArgs e);

The equivalent Visual Basic definition is:

Public Delegate Sub MouseEventHandler( _

    sender As Object, e As MouseEventArgs

So if we wish to receive MouseDown event notifications from some control, we must provide a function with a matching signature:

private void OnMouseDown (object sender, MouseEventArgs e)

{

    ... handle the MouseDown event ...

}

Of course, we must also tell the control that we are interested in the MouseDown event and would like notifications to be delivered to our OnMouseDown method. In Visual Basic, we did this by using the Handles keyword, but in C#, we must create a MouseEventHandler delegate initialized with a reference to our method, and then attach it to the relevant event on the control, using the following rather strange syntax:

src.MouseDown += new MouseEventHandler(OnMouseDown);

This is roughly equivalent to passing the address of a callback function as a function pointer in C++; the delegate acts as a typed reference to a function that can be passed as a parameter or stored in a field so that the function can be called back later. But we can’t use function pointers as we would in C++, and not just for the ideological reason that it doesn’t enter into the spirit of the brave new pointerless world of the CLR. There is a rather more prosaic reason not to use raw function pointers: JIT compilation means that functions don’t necessarily remain in the same place for the life of a program. In fact, when the code above is run, there is every chance that the OnMouseDown method has not been JIT compiled at all yet, so it might not even have an address. So instead, we rely on delegates to provide us with behavior equivalent to function pointers, while shielding us from the complexities of using pointers in the world of movable code.

Delegates can hold an object reference as well as a function reference. (In C++ terms, this would mean that a delegate is really two pointers—a function pointer and a pointer to an object.) In the example above, OnMouseDown is not a static function, so it can only be invoked in conjunction with an object reference. (The value for the implicit this reference has to come from somewhere.) If a function requires an object reference, a suitable one must be supplied when a delegate to that function is created. This can be done explicitly, for example:

myDelegate = new MyDelegateType(myObj.MyMethod);

creates a new delegate whose type is MyDelegateType and attaches it to the MyMethod method on the object to which myObj refers. (Delegates store their own copy of the reference, so if the myObj variable is later modified to refer to a different object, the delegate will still refer to the original one.) Or the object reference can be inferred—if the delegate is created in the scope of a non-static method, the this reference will be used if no explicit reference is supplied. The MouseEventHandler example above illustrates this, and is typical of code inside a form’s initialization function: because an object reference has not been supplied explicitly, the C# compiler automatically supplies a reference to whichever form is being initialized. That code is shorthand for the following:

src.MouseDown += new MouseEventHandler(this.OnMouseDown);

This use of the += syntax, peculiar to C#, is simply shorthand for a method call. For each event that a class defines, the C# compiler will actually define two methods, one for adding a handler and one for removing it. C# hides this detail with the += syntax (and the corresponding -= syntax used for disconnecting an event handler), and it also shields us from the details of declaring events if we wish to raise them ourselves. If we add an event declaration such as the one shown above to our own class, the C# compiler will automatically generate the functions to add and remove event handlers for us; the code it generates is able to cope with multiple event handlers being attached simultaneously, as all events should.

Note that the -= syntax used for detaching an event handler is smart enough to work out which method a delegate refers to. It doesn’t require the same delegate object that was used in the += to be passed back in. So looking at the code above, you might have thought that we would need to store the delegate being created with the new operator to pass it back when we wish to detach. In fact, it works just fine if we create a new delegate at detachment time:

src.MouseDown -= new MouseEventHandler(this.OnMouseDown);

In Visual Basic, all these details of creating delegates and attaching them are hidden—using the WithEvents and Handles keywords causes all this code to be generated automatically. However, VB also supports the explicit style that C# requires. The syntax is different, but the meaning is the same. We can create a delegate object using VB’s AddressOf keyword. And VB’s equivalents to the += and -= event operators are the AddHandler and RemoveHandler keywords. So we can add a handler explicitly, just as we are required to in C#, with the following VB code:

AddHandler src.MouseDown, AddressOf Me.OnMouseDown

And the corresponding code to remove a handler is:

RemoveHandler src.MouseDown, AddressOf Me.OnMouseDown

Most of the time, you would not need to use this explicit style in Visual Basic. However, it can be useful for attaching handlers dynamically at runtime. In addition, if you want a single event handler to handle an event from every object in a collection, you will need to use this explicit style.

All the event handler delegates defined in the .NET Framework follow a common pattern. They define function signatures that take two parameters. The first parameter is always of type object, and is a reference to the object that raised the event. (So when a control raises the MouseDown event, it passes a reference to itself to the event handler. This can be useful it you want to have events from multiple controls on a form all handled by a single function—this parameter lets it know which control a particular event came from.) The second parameter contains information about the event. The various delegates defined in .NET all specify different types for this second parameter. For example, the drag-and-drop events use a delegate type called DragEventHandler, which defines the second parameter to be a DragEventArgs, while MouseEventHandler (see above) defines it to be a MouseEventArgs. Some events provide no special information—for example, the Click event raised by a button simply indicates that a particular button has been clicked, so there is no use for a final parameter. .NET defines a generic delegate for such methods:

public delegate void EventHandler(object sender, EventArgs e);

The second parameter is usually a special value, EventArgs.Empty. This may seem pointless—if the same value is passed every time, why not just leave off the second parameter? It is left there just in case peculiar circumstances arise in which it would be useful to be able to pass some information. For example, if you were to define a custom derivative of the standard Button class, you might wish to pass some information in your Click event. If you define a class that derives from EventArgs, you can pass it as the second parameter. If EventHandler didn’t provide this second argument, you would not be able to do this.

Note that you are not required to use this style of event handling for your own components. You can define classes whose events use a delegate of your own devising, which may have any signature you like. Of course, if you stick to the framework’s style, your code will look more consistent, so it is recommended that you do this. But there is nothing magic about delegates whose first parameter is an object and whose second parameter is some type deriving from EventArgs.

Protection levels

Encapsulation (making the implementation details of a class inaccessible to keep a clear division between a class’s public interface and its internal workings) is crucial in all object-oriented systems. Without proper encapsulation, client code can become dependent on arbitrary implementation details of an object, meaning that changes to the object that don’t change its external programming interface (e.g., bug fixes) still can end up breaking client code. This could happen in C++ because compiled code was implicitly dependent on features of a class that were not strictly part of its public interface, e.g., the number of bytes required to store an instance of the class, and the offsets of public fields. These values can change when private implementation details are modified. This feature of C++ reflects its origins in the world of monolithic software, where all client code can be rebuilt whenever a class’s implementation changes (assuming your build process detects such changes properly). In a dynamically linked world, this is simply not good enough.

Encapsulation is fundamentally important in component-oriented software because individual components tend to evolve independently both of each other, and of the code that uses them. To maintain the freedom to evolve, components must be able to draw a clear line between their internal workings and their public programming interface.

To enable this, .NET supports the protection levels familiar to C++ developers. Members of a type can be defined as public, indicating that they are available to all; private, indicating that they are for the type’s internal use only; and protected, indicating that they can be accessed by the type and by any types that derive from it. (We will talk about inheritance in the next section.) However, because .NET has a formal definition for a component, it is able to provide protection facilities at a wider scope than this. Unlike standard C++, .NET supports component-level encapsulation as well as class-level encapsulation.

It is common to want to write a class designed to be used inside a component, but that is not intended to be used by external clients of the component. One solution available in C++ (and supported in .NET) is to define a private nested class—a class defined inside another class that is only accessible to code within that class. The problem is this does not allow a class to be accessible to other classes within the component; in C++, it is an all or nothing choice—a class is either entirely private or is available to all classes. However, .NET offers another level of protection: internal (in C#) or Friend (in VB).

Types and their members can be marked as internal (in C#) or Friend (in VB), indicating that they are available only to code that is in the same assembly. So it is possible to define types or members that exist entirely for the benefit of the component in which they are defined, and that will not be accessible to clients of the component.

Tip

The assembly-level protection provided by internal or Friend is superficially similar to package-level protection in Java. However, although it serves the same purpose, it works rather differently. In Java, package-level protection is based on the naming of classes. In .NET, internal protection is based entirely on component membership—even if two classes belong to different namespaces, they can still access each other’s internal members if they belong to the same assembly.

Inheritance and Interfaces

The .NET type system supports inheritance, although unlike standard C++, it does not support multiple inheritance. However, one of the most common uses of multiple inheritance in C++ was to support an interface-based programming style. Fortunately, .NET supports interfaces directly, so the absence of multiple inheritance is not a problem.

This section describes the inheritance and interface-based features of the CLR.

Inheritance

The CLR supports single implementation inheritance—a type can have a single base type. In fact, use of inheritance is effectively mandatory in .NET—a user-defined type has to inherit from something. This is because .NET provides a unified type system in which every type is compatible with a single base type called System.Object. (System.Object is the only type not to have a base type—every other type in .NET, including intrinsic types, inherits either directly or indirectly from System.Object.)

By default, any user-defined type can act as a base class (unless it is a value type—see later), but this can be inhibited if necessary. A type may prevent further derivation by marking itself as sealed (in C#) or NonInheritable (in VB). Conversely, a type may mark itself with the abstract keyword (in C#) or the MustInherit keyword (in VB), indicating that it cannot itself be instantiated, and can only be used as a base class from which other classes are derived.

Unlike standard C++, inheritance in .NET can not only cross component boundaries, it can also span language boundaries—a C# class can derive from a Visual Basic class, for example.

Interface-based programming

As seasoned COM developers will be aware, it is possible to use an interface-based style of programming in C++ by defining pure abstract base classes. But in .NET, interfaces are directly supported by the runtime. Interfaces are not fully fledged types; they are wholly abstract. This means that although .NET only supports single inheritance, it is possible for a type to implement multiple interfaces, because interfaces are not really types. (So unlike C++, implementing an interface on a .NET type doesn’t involve inheritance at all.)

.NET languages typically have special syntax for dealing with interfaces, but in all other respects, .NET interface-based programming is very similar to using an interface idiom in C++. Example 1-1 defines an interface with two methods, followed by a class that implements the interface.

Example 1-1. Implementing an interface in C#

public interface IMyItf

{

    void MyMethod1(string s);

    int MyMethod2(string s, int x);

}

public class MyImplementation : IMyItf

{

    // Must implement the methods defined in IMyItf,

    // or the compiler will complain that we're not

    // honoring our claim to implement the interface

    // and refuse to compile the code

    public void MyMethod1(string s)

    {

        System.Console.WriteLine(s);

    }

    public int MyMethod2(string s, int x)

    {

        return int.Parse(s) + x;

    }

}

Example 1-2 shows the equivalent interface definition and implementation in Visual Basic.

Example 1-2. Implementing an interface in VB

Public interface IMyItf

    Sub MyMethod1(s As String)

    Function MyMethod2(s As String, x As Integer) As Integer

End Interface



Public Class MyImplementation

    Implements IMyItf



    Public Sub MyMethod1(s As String) Implements IMyItf.MyMethod1

        System.Console.WriteLine(s)

    End Sub

    Public Function MyMethod2(s As String, x As Integer) As Integer _

            Implements IMyItf.MyMethod2

        return Integer.Parse(s) + x

    End Function

End Class

The Different Types of Type

Types in .NET fall into two categories: value types and reference types. Instances of these are referred to as values and objects, respectively. The principal difference between value types and reference types is that variables of value types contain the bytes of data that make up the instance, while variables of reference type just contain the address of the instance. With reference types, the instance itself lives on the garbage-collected heap.

We will now look at the differences in behavior between reference types and value types.

Reference types

Reference types are defined in C# with the class keyword and in VB with the Class keyword. Each instance of any reference type has a distinct identity and lives on the heap. If you declare a variable of a reference type, it will refer to an object of that type on the heap. (Or the variable may be null in C# or Nothing in VB, a special value meaning that the variable isn’t referring to any object right now.)

The CLR uses garbage collection to determine when a particular object no longer has any variables referring to it. There is no equivalent of the C++ delete operator in .NET-based languages. You can simply lose track of objects you no longer care about, and the runtime will eventually notice that such objects have fallen out of use and reclaim the memory they occupied.

Objects are always annotated with type information. If you have a variable of type System.Object (or object, as it is usually abbreviated in C# and VB), it could refer to any kind of object at all, but you can always find out by calling the object’s GetType method. This relies on there being some information at the start of the object describing its type. In fact, lots of different services supplied by the runtime, including all the polymorphic features such as virtual functions and interfaces, rely on this type information.

Value types

In C++, intrinsic types (e.g., int, float, etc.) are fundamentally different from and unrelated to class types, whereas in .NET, everything belongs to a single type hierarchy: everything, including the intrinsic types, derives from System.Object. However, .NET does make a distinction between value-like types and object-like types—there is a special type called System.ValueType, and all types deriving from it have value-like behavior. The built-in types (System.Int32, System.Single, etc.) all derive from System.ValueType.

Value types don’t have any meaningful identity—because they are usually passed by value, they frequently get copied. This means that they don’t have to live in a distinct space on the heap. Value types usually live either on the stack or as fields inside some other type.

This distinction between values and objects is necessary for performance reasons—if every single integer in a program had to be allocated its own space on the heap, this would be disastrous for the program’s memory and CPU consumption. This becomes particularly important if large arrays are used. An array of reference types is roughly equivalent to an array of pointers in C++, and requires a heap block to be allocated for each element in the array if it is to be of any use. But for value types, a single heap block is allocated for the entire array, and the values are stored contiguously inside this block, just like in a C++ array.

The tradeoff is that value types are slightly less flexible, the principal limitation being that they cannot be derived from. This makes it possible for the runtime to know exactly how much memory will be required for a value type. If inheritance were allowed, how could the runtime be sure that 32 bits would be enough to hold an Int32? A derived type might require more room. Also, because the inheritance-based polymorphic features available to reference types will never be used, value types don’t need to carry the associated overhead of type information and virtual method tables. This means that a value type is only as large as it needs to be to hold its fields and no larger.

Tip

Despite the requirement for a value type to have a fixed size, it can still contain fields of reference type. This is fine because although those fields may refer to objects of indeterminate size, the value type will only contain the references, not the objects. A reference is always the same size (32 bits, in the current implementation), regardless of how large the object it refers to may be.

For example, it is allowed for a value type to contain a string—a string could be any length, but this doesn’t matter, because the value will just contain a reference to that string.

The set of value types is not restricted to the built-in types. It is possible to create user-defined value types. The C# language uses the struct keyword to define custom value types, while VB.NET uses the Structure...End Structure construct. This means that it is not just the intrinsic types that can benefit from the performance advantages that value types can offer in certain circumstances—user-defined types for things such as complex numbers and 3D coordinates can use exactly the same memory allocation strategies that are used for intrinsic types.

Values and boxing

Value types are not always more efficient than reference types. Although they don’t carry the normal overheads of reference types (heap blocks, type information, virtual method tables, etc.), there can be situations where they are nevertheless less efficient. One reason is they are passed by value—a value type will always be copied when passed as a method parameter. If it is large, this can get expensive. The other reason is that the runtime needs to perform a trick to cast a value type down to a base type. Remember that all types in .NET are compatible with System.Object, including all the value types. This sounds as though it should be impossible, because System.Object supports reference-like behavior—for example it defines the GetType method mentioned earlier.

The CLR performs a trick to make this work. When you cast, say, an integer to a System.Object, the runtime creates an object-like wrapper on the heap, and copies the value of the integer inside this wrapper. This operation is called boxing. (There is a corresponding unboxing operation when casting back to the original type to extract the wrapped value.) Boxing is also used to enable a value type to support interfaces. Interfaces are polymorphic by nature—the exact method that is called when you invoke a method on an interface is not determined by the type of variable you call it through, it is determined by the type of object that variable refers to—so they rely on the object type header being present. This means that if your value type implements any interfaces, it will be boxed every time you cast it to a reference of some interface type.

Boxing a value type has its costs—an object must be allocated on the heap. (The cost is similar to that of instantiating a reference type in the first place. The problem is that you pay this price every time boxing occurs, rather than just once when you create the object.) Any type that is often cast to System.Object is likely to be better off as a reference type to avoid the boxing overhead. For example, all the standard collection classes in the .NET Framework store references of type System.Object, so if you plan to store your objects in one of these containers, you should make them reference types, not value types (i.e., classes, not structs).

This caveat does not apply if you are simply using normal arrays. Although, say, a System.Collections.ArrayList of ints will box every item it contains, a simple int array (int[] or Integer()) will not use boxing.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required