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.
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 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 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
.
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.
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
.
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.
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.
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.
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
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
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.
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.
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
int
s will box every item it contains, a simple
int
array (int[]
or
Integer()
) will not use boxing.
Get .NET Windows Forms in a Nutshell now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.