More Tutorial - C#
More Tutorial - C#
More Tutorial - C#
1
COM Interop Part 2 Shows how to use C# to interoperate with COM objects. Advanced
Covers a C# server.
Attributes Shows how to create custom attribute classes, use them in Advanced
code, and query them through reflection.
Security Discusses .NET Framework security and shows two ways Advanced
to modify security permissions in C#: permission classes
and permission attributes.
Threading Demonstrates various thread activities such as creating and Advanced
executing a thread, synchronizing threads, interacting
between threads, using a thread pool, and using a mutex
object.
Unsafe Code Shows how to use pointers. Advanced
OLE DB Shows how to use OLE DB in C# by connecting to a Advanced
Microsoft Access database.
Example 1
// Hello1.cs
public class Hello1
{
public static void Main()
{
System.Console.WriteLine("Hello, World!");
}
}
Output
Hello, World!
Code Discussion
• Every Main method must be contained inside a class (Hello1 in this case).
• The System.Console class contains a WriteLine method that can be used to display a
string to the console.
2
Example 2
To avoid fully qualifying classes throughout a program, you can use the using directive as shown
below:
// Hello2.cs
using System;
Output
Hello, World!
Example 3
If you need access to the command line parameters passed in to your application, simply change
the signature of the Main method to include them as shown below. This example counts and
displays the command line arguments.
// Hello3.cs
// arguments: A B C D
using System;
3
}
Output
Hello, World!
You entered the following 4 command line arguments:
A
B
C
D
Example 4
To return a return code, change the signature of the Main method as shown below:
// Hello4.cs
using System;
Output
Hello, World!
This tutorial shows how the command line can be accessed and two ways of accessing the array
of command line parameters.
The following examples show two different approaches to using the command line arguments
passed to an application.
Example 1
This example demonstrates how to print out the command line arguments.
4
// cmdline1.cs
// arguments: A B C
using System;
Output
Example 2
Another approach to iterating over the array is to use the foreach statement as shown in this
example. The foreach statement can be used to iterate over an array or over a .NET Framework
collection class. It provides a simple way to iterate over collections.
// cmdline2.cs
// arguments: John Paul Mary
using System;
5
public static void Main(string[] args)
{
Console.WriteLine("Number of command line parameters = {0}",
args.Length);
foreach(string s in args)
{
Console.WriteLine(s);
}
}
}
Output
Run the program using some arguments like this: cmdline2 John Paul Mary.
Arrays Tutorial
This tutorial describes arrays and shows how they work in C#.
• Arrays in General
• Declaring Arrays
• Initializing Arrays
Arrays in General
C# arrays are zero indexed; that is, the array indexes start at zero. Arrays in C# work similarly to
how arrays work in most other popular languages There are, however, a few differences that you
should be aware of.
6
When declaring an array, the square brackets ([]) must come after the type, not the identifier.
Placing the brackets after the identifier is not legal syntax in C#.
Declaring Arrays
C# supports single-dimensional arrays, multidimensional arrays (rectangular arrays), and array-
of-arrays (jagged arrays). The following examples show how to declare different kinds of arrays:
Single-dimensional arrays:
int[] numbers;
Multidimensional arrays:
string[,] names;
Array-of-arrays (jagged):
byte[][] scores;
Declaring them (as shown above) does not actually create the arrays. In C#, arrays are objects
(discussed later in this tutorial) and must be instantiated. The following examples show how to
create arrays:
Single-dimensional arrays:
Multidimensional arrays:
7
You can also have larger arrays. For example, you can have a three-dimensional rectangular
array:
int[][,,][,] numbers;
Example
The following is a complete C# program that declares and instantiates arrays as discussed above.
// arrays.cs
using System;
class DeclareArraysSample
{
public static void Main()
{
// Single-dimensional array
int[] numbers = new int[5];
// Multidimensional array
string[,] names = new string[5,4];
8
Output
Length of row 0 is 3
Length of row 1 is 4
Length of row 2 is 5
Length of row 3 is 6
Length of row 4 is 7
Initializing Arrays
C# provides simple and straightforward ways to initialize arrays at declaration time by enclosing
the initial values in curly braces ({}). The following examples show different ways to initialize
different kinds of arrays.
Note If you do not initialize an array at the time of declaration, the array members are
automatically initialized to the default initial value for the array type. Also, if you declare the array
as a field of a type, it will be set to the default value null when you instantiate the type.
Single-Dimensional Array
Multidimensional Array
9
You can initialize jagged arrays like this example:
int[][] numbers = new int[2][] { new int[] {2,3,4}, new int[] {5,6,7,8,9} };
You can also omit the size of the first array, like this:
int[][] numbers = new int[][] { new int[] {2,3,4}, new int[] {5,6,7,8,9} };
-or-
int[,] numbers = { {1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10} };
numbers[1, 1] = 5;
The following is a declaration of a single-dimension jagged array that contains two elements. The
first element is an array of two integers, and the second is an array of three integers:
int[][] numbers = new int[][] { new int[] {1, 2}, new int[] {3, 4, 5}
};
The following statements assign 58 to the first element of the first array and 667 to the second
element of the second array:
numbers[0][0] = 58;
numbers[1][1] = 667;
10
int LengthOfNumbers = numbers.Length;
The System.Array class provides many other useful methods/properties, such as methods for
sorting, searching, and copying arrays.
int[,] numbers = new int[3, 2] {{9, 99}, {3, 33}, {5, 55}};
foreach(int i in numbers)
{
Console.Write("{0} ", i);
}
The output of this example is:
9 99 3 33 5 55
However, with multidimensional arrays, using a nested for loop gives you more control over the
array elements.
Properties Tutorial
This tutorial shows how properties are an integral part of the C# programming language. It
demonstrates how properties are declared and used.
This tutorial includes two examples. The first example shows how to declare and use read/write
properties. The second example demonstrates abstract properties and shows how to override
these properties in subclasses.
Example 1
This sample shows a Person class that has two properties: Name (string) and Age (int). Both
properties are read/write.
// person.cs
11
using System;
class Person
{
private string myName ="N/A";
private int myAge = 0;
12
// Create a new Person object:
Person person = new Person();
// Print out the name and the age associated with the person:
Console.WriteLine("Person details - {0}", person);
Output
Simple Properties
Person details - Name = N/A, Age = 0
Person details - Name = Joe, Age = 99
Person details - Name = Joe, Age = 100
Code Discussion
• Notice the way that the properties are declared, for example, consider the Name property:
13
The Set and Get methods of a property are contained inside the property declaration. You
can control whether a property is read/write, read-only, or write-only by controlling
whether a Get or Set method is included.
• Once the properties are declared, they can be used as if they were fields of the class.
This allows for a very natural syntax when both getting and setting the value of a
property, as in the following statements:
person.Name = "Joe";
person.Age = 99;
• Note that in a property Set method a special value variable is available. This variable
contains the value that the user specified, for example:
myName = value;
• Notice the clean syntax for incrementing the Age property on a Person object:
person.Age += 1;
If separate Set and Get methods were used to model properties, the equivalent code
might look like this:
person.SetAge(person.GetAge() + 1);
Example 2
The following example shows how to define abstract properties. An abstract property declaration
does not provide an implementation of the property accessors. The example demonstrates how to
override these properties in subclasses.
14
This sample consists of three files. In the Properties Sample, these files are compiled into a single
compilation but in this tutorial, each file is compiled individually and its resulting assembly
referenced by the next compilation:
File 1 - abstractshape.cs
This file declares the Shape class that contains the Area property of the type double
// abstractshape.cs
// compile with: /target:library
// csc /target:library abstractshape.cs
using System;
public Shape(string s)
{
Id = s; // calling the set accessor of the Id property
}
public string Id
{
get
{
return myId;
}
set
{
myId = value;
}
15
}
Code Discussion
• Modifiers on the property are placed on the property declaration itself, for example:
• When declaring an abstract property (such as Area in this example), you simply indicate
what property accessors are available, but do not implement them. In this example, only a
Get accessor is available, so the property is read-only.
File 2 - shapes.cs
The following code shows three subclasses of Shape and how they override the Area property to
provide their own implementation.
// shapes.cs
// compile with: /target:library /reference:abstractshape.dll
public class Square : Shape
{
private int mySide;
16
public override double Area
{
get
{
// Given the side, return the area of a square:
return mySide * mySide;
}
}
}
17
public override double Area
{
get
{
// Given the width and height, return the area of a rectangle:
return myWidth * myHeight;
}
}
}
File 3 - shapetest.cs
The following code shows a test program that creates a number of Shape-derived objects and
prints out their areas.
// shapetest.cs
// compile with: /reference:abstractshape.dll;shapes.dll
public class TestClass
{
public static void Main()
{
Shape[] shapes =
{
new Square(5, "Square #1"),
new Circle(3, "Circle #1"),
new Rectangle( 4, 5, "Rectangle #1")
};
System.Console.WriteLine("Shapes Collection");
foreach(Shape s in shapes)
{
System.Console.WriteLine(s);
}
}
}
Output
Shapes Collection
Square #1 Area = 25.00
18
Circle #1 Area = 28.27
Rectangle #1 Area = 20.00
Libraries Tutorial
This tutorial demonstrates how to create a managed DLL file by using the necessary compiler
options, and how to use the library by a client program.
Example
• The DLL library (Functions.dll), which is built from the following source files:
• The client program (FunctionTest.exe), which uses the DLL, is compiled from the source
file FunctionClient.cs. The program displays the factorial of the input arguments.
To build the library, make Functions the current directory and type the following at the command
prompt:
/target:library Specifies that the output is a DLL and not an executable file (this also
stops the compiler from looking for a default entry point).
/out:Functions.dll Specifies that the output file name is Functions.dll. Normally the output
name is the same name as the first C# source file on the command line
(in this example, Factorial).
Factorial.cs Specifies the files to compile and place in the DLL.
DigitCounter.cs
19
To compile the program, make FunctionTest the current directory and type the following at the
command prompt:
copy ..\Functions\Functions.dll .
csc /out:FunctionTest.exe /R:Functions.DLL FunctionClient.cs
where:
File 1 - Factorial.cs
The following code calculates the factorial of the integer passed to the method (unlike Libraries
Sample, compile this to a library).
// Factorial.cs
// compile with: /target:library
using System;
File 2 - DigitCounter.cs
20
The following code is used to count the number of digit characters in the passed string:
// DigitCounter.cs
// compile with: /target:library /out:Functions.dll /reference:Factorial.dll
using System;
File 3 - FunctionClient.cs
Once you build a library, it can be used by other programs. The following client program uses the
classes defined in the library. The basic function of the program is to take each command-line
parameter and attempt to compute the factorial value for each argument.
// FunctionClient.cs
// compile with: /reference:Functions.dll,Factorial.dll /out:FunctionTest.exe
// arguments: 3 5 10
using System;
// The following using directive makes the types defined in the Functions
21
// namespace available in this compilation unit:
using Functions;
class FunctionClient
{
public static void Main(string[] args)
{
Console.WriteLine("Function Client");
if ( args.Length == 0 )
{
Console.WriteLine("Usage: FunctionTest ... ");
return;
}
Output
The command line FunctionTest 3 5 10 uses the program FunctionTest to calculate the factorial
of the three integers 3, 5, and 10. It also displays the number of digits for each argument.
Function Client
The Digit Count for String [3] is [1]
22
The Factorial for [3] is [6]
The Digit Count for String [5] is [1]
The Factorial for [5] is [120]
The Digit Count for String [10] is [2]
The Factorial for [10] is [3628800]
Note To run the client executable (FunctionTest.exe), the file Functions.DLL must be in the
current directory, a child directory, or in the Global Assembly Cache. For more information see
Global Assembly Cache.
Versioning Tutorial
This tutorial demonstrates versioning in C# through the use of the override and new keywords.
Versioning maintains compatibility between base and derived classes as they evolve.
The C# language is designed such that versioning between base and derived classes in different
libraries can evolve and maintain backwards compatibility. This means, for example, that the
introduction of a new member in a base class with the same name as a member in a derived
class is not an error. It also means that a class must explicitly state whether a method is intended
to override an inherited method, or whether a method is a new method that simply hides a
similarly named inherited method.
In C#, methods are by default, not virtual. To make a method virtual, the virtual modifier has to be
used in the method declaration of the base class. The derived class can then override the base
virtual method by using the override keyword or hide the virtual method in the base class by
using the new keyword. If neither the override keyword nor the new keyword is specified, the
compiler will issue a warning and the method in the derived class will hide the method in the base
class. The following example shows these concepts in action.
Example
// versioning.cs
// CS0114 expected
public class MyBase
{
public virtual string Meth1()
{
return "MyBase-Meth1";
}
public virtual string Meth2()
{
return "MyBase-Meth2";
23
}
public virtual string Meth3()
{
return "MyBase-Meth3";
}
}
System.Console.WriteLine(mB.Meth1());
System.Console.WriteLine(mB.Meth2());
System.Console.WriteLine(mB.Meth3());
}
}
Output
24
MyDerived-Meth1
MyBase-Meth2
MyBase-Meth3
Code Discussion
Hiding a base class member from a derived class isn't an error in C#. This feature enables you to
make changes in the base class without breaking other libraries that inherit this base class. For
example, at some point you could have the following classes:
class Base {}
class Derived: Base
{
public void F() {}
}
At some later point, the base class could evolve to add a void method F() as follows:
class Base
{
public void F() {}
}
class Derived: Base
{
public void F() {}
}
Thus, in C#, both the base and derived classes can evolve freely and maintain binary
compatibility.
This tutorial shows how to implement a collection class that can be used with the foreach
statement.
The foreach statement is a convenient way to iterate over the elements of an array. It can also
enumerate the elements of a collection, provided that the collection class has implemented the
System.Collections.IEnumerator and System.Collections.IEnumerable interfaces.
Example 1
The following code sample illustrates how to write a collection class that can be used with
foreach. The class is a string tokenizer, similar to the C run-time function strtok.
// tokens.cs
25
using System;
// The System.Collections namespace is made available:
using System.Collections;
public TokenEnumerator(Tokens t)
{
this.t = t;
}
26
return true;
}
else
{
return false;
}
}
Output
27
This
is
a
well
done
program.
Code Discussion
In the preceding example, the following code is used to Tokens by breaking "This is a well-done
program." into tokens (using ' ' and '-' as separators) and enumerating those tokens with the
foreach statement:
In C#, it is not strictly necessary for a collection class to inherit from IEnumerable and
IEnumerator in order to be compatible with foreach; as long as the class has the required
GetEnumerator, MoveNext, Reset, and Current members, it will work with foreach. Omitting the
interfaces has the advantage of allowing you to define the return type of Current to be more
specific than object, thereby providing type-safety.
For example, starting with the sample code above, change the following lines:
28
You can have the best of both worlds — type-safety within C# and interoperability with other
common language runtime-compatible languages — by inheriting from IEnumerable and
IEnumerator and using explicit interface implementation, as demonstrated in the following
example.
Example 2
// tokens2.cs
using System;
using System.Collections;
29
public TokenEnumerator(Tokens t)
{
this.t = t;
}
30
// Test Tokens, TokenEnumerator
Structs Tutorial
This tutorial presents the syntax and usage of structs. It also covers the important differences
between classes and structs.
This tutorial includes two examples. The first example shows you how to declare and use structs,
and the second example demonstrates the difference between structs and classes when
instances are passed to methods. You are also introduced to the following topics:
• Heap or Stack?
• Attributes on Structs
Example 1
This example declares a struct with three members: a property, a method, and a private field. It
creates an instance of the struct and puts it to use:
// struct1.cs
using System;
struct SimpleStruct
{
private int xval;
public int X
{
get
31
{
return xval;
}
set
{
if (value < 100)
xval = value;
}
}
public void DisplayX()
{
Console.WriteLine("The stored value is: {0}", xval);
}
}
class TestClass
{
public static void Main()
{
SimpleStruct ss = new SimpleStruct();
ss.X = 5;
ss.DisplayX();
}
}
Output
Heap or Stack?
When you call the New operator on a class, it will be allocated on the heap. However, when you
instantiate a struct, it gets created on the stack. This will yield performance gains. Also, you will
not be dealing with references to an instance of a struct as you would with classes. You will be
working directly with the struct instance. Because of this, when passing a struct to a method, it's
passed by value instead of as a reference.
32
Example 2
This example shows that when a struct is passed to a method, a copy of the struct is passed, but
when a class instance is passed, a reference is passed.
// struct2.cs
using System;
class TheClass
{
public int x;
}
struct TheStruct
{
public int x;
}
class TestClass
{
public static void structtaker(TheStruct s)
{
s.x = 5;
}
public static void classtaker(TheClass c)
{
c.x = 5;
}
public static void Main()
{
TheStruct a = new TheStruct();
TheClass b = new TheClass();
a.x = 1;
b.x = 1;
structtaker(a);
classtaker(b);
Console.WriteLine("a.x = {0}", a.x);
Console.WriteLine("b.x = {0}", b.x);
}
}
33
Output
a.x = 1
b.x = 5
Code Discussion
The output of the example shows that only the value of the class field was changed when the
class instance was passed to the classtaker method. The struct field, however, did not change by
passing its instance to the structtaker method. This is because a copy of the struct was passed to
the structtaker method, while a reference to the class was passed to the classtaker method.
When you create a struct object using the New operator, it gets created and the appropriate
constructor is called. Unlike classes, structs can be instantiated without using the New operator. If
you do not use New, the fields will remain unassigned and the object cannot be used until all the
fields are initialized.
There is no inheritance for structs as there is for classes. A struct cannot inherit from another
struct or class, and it cannot be the base of a class. Structs, however, inherit from the base class
object. A struct can implement interfaces, and it does that exactly as classes do. Here's a code
snippet of a struct implementing an interface:
interface IImage
{
void Paint();
}
34
Attributes on Structs
By using attributes you can customize how structs are laid out in memory. For example, you can
create what's known as a union in C/C++ by using the StructLayout(LayoutKind.Explicit) and
FieldOffset attributes.
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Explicit)]
struct TestUnion
{
[FieldOffset(0)]
public int i;
[FieldOffset(0)]
public double d;
[FieldOffset(0)]
public char c;
[FieldOffset(0)]
public byte b1;
}
In the preceding code segment, all of the fields of TestUnion start at the same location in memory.
The following is another example where fields start at different explicitly set locations:
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Explicit)]
struct TestExplicit
{
[FieldOffset(0)]
public long lg;
[FieldOffset(0)]
public int i1;
[FieldOffset(4)]
public int i2;
[FieldOffset(8)]
public double d;
[FieldOffset(12)]
public char c;
[FieldOffset(14)]
public byte b1;
}
35
The two int fields, i1 and i2, share the same memory locations as lg. This sort of control over
struct layout is useful when using platform invocation.
Conclusion
Structs are simple to use and can prove to be useful at times. Just keep in mind that they're
created on the stack and that you're not dealing with references to them but dealing directly with
them. Whenever you have a need for a type that will be used often and is mostly just a piece of
data, structs might be a good option.
Indexers Tutorial
This tutorial shows how C# classes can declare indexers to provide array-like access to the
classes.
Defining an indexer allows you to create classes that act like "virtual arrays." Instances of that
class can be accessed using the [] array access operator. Defining an indexer in C# is similar to
defining operator [] in C++, but is considerably more flexible. For classes that encapsulate array-
or collection-like functionality, using an indexer allows the users of that class to use the array
syntax to access the class.
For example, suppose you want to define a class that makes a file appear as an array of bytes. If
the file were very large, it would be impractical to read the entire file into memory, especially if you
only wanted to read or change a few bytes. By defining a FileByteArray class, you could make the
file appear similar to an array of bytes, but actually do file input and output when a byte was read
or written.
Example
In this example, the class FileByteArray makes it possible to access a file as if it were a byte
array. The Reverse class reverses the bytes of the file. You can run this program to reverse the
bytes of any text file including the program source file itself. To change the reversed file back to
normal, run the program on the same file again.
// indexer.cs
// arguments: indexer.txt
using System;
using System.IO;
36
public class FileByteArray
{
Stream stream; // Holds the underlying stream
// used to access the file.
// Create a new FileByteArray encapsulating a particular file.
public FileByteArray(string fileName)
{
stream = new FileStream(fileName, FileMode.Open);
}
37
{
get
{
return stream.Seek(0, SeekOrigin.End);
}
}
}
file.Close();
}
}
38
Input: indexer.txt
To test the program you can use a text file with the following contents (this file is called Test.txt in
the Indexers Sample).
indexer indexer.txt
To display the reversed file, enter the command:
Type indexer.txt
Sample Output
}
}
;)"!dlroW ,olleH"(eniLetirW.elosnoC.metsyS
{
)(niaM diov citats cilbup
{
1olleH ssalc cilbup
Code Discussion
• Since an indexer is accessed using the [] operator, it does not have a name. For indexer
declaration syntax, see Indexers.
• In the example above, the indexer is of type byte and takes a single index of type long
(64-bit integer). The Get accessor defines the code to read a byte from the file, while the
Set accessor defines the code to write a byte to the file. Inside the Set accessor, the
predefined parameter value has the value that is being assigned to the virtual array
element.
• An indexer must have at least one parameter. Although it is comparatively rare, an
indexer can have more than one parameter in order to simulate a multidimensional
"virtual array." Although integral parameters are the most common, the indexer parameter
39
can be of any type. For example, the standard Dictionary class provides an indexer with a
parameter of type Object.
• Although indexers are a powerful feature, it is important to use them only when the array-
like abstraction makes sense. Always carefully consider whether using regular method(s)
would be just as clear. For example, the following is a bad use of an indexer:
class Employee
{
// VERY BAD STYLE: using an indexer to access
// the salary of an employee.
public double this[int year]
{
get
{
// return employee's salary for a given year.
}
}
}
Although legal, an indexer with only a Get accessor is rarely good style. Strongly consider
using a method in this case.
• Indexers can be overloaded (for more information, see 10.8.1 Indexer overloading).
This tutorial shows how to implement a class that uses indexed properties. Indexed properties
allow you to use a class that represents an array-like collection of several different kinds of things.
You should complete the Indexers Tutorial before working through this tutorial.
Suppose you want to write a class, Document, which encapsulates a lengthy section of text. To
allow easy implementation of various operations such as checking spelling, you might want to
view the document as a virtual array of words, as well as of characters.
The following example shows a technique for implementing such a class. For each "indexed
property," you define a nested class, which contains a reference back to the main class instance.
A readonly field on the main class provides access to an instance of the nested class that defines
each virtual array. Each of the nested classes defines an indexer, as well as other collection-like
methods (a Count property, for example). The following example shows this for "Words" and
"Characters."
40
Note Use this technique sparingly! Only use this pattern if the abstraction provided by using
array indexing operations significantly clarifies code that uses your class, and if the indexers have
both Get and Set accessors.
Example
In this example the Document class is defined. Two indexed properties, Words and Characters,
are used to perform some text operations on the Document object.
// indexedproperty.cs
using System;
internal WordCollection(Document d)
{
document = d;
}
if (inWord >= 0)
41
{
if (!isLetter)
{
if (count++ == wordCount)
{
start = inWord;
length = i - inWord;
return true;
}
inWord = -1;
}
}
else
{
if (isLetter)
inWord = i;
}
}
return false;
}
42
// string "value":
if (length == value.Length)
{
Array.Copy(value.ToCharArray(), 0,
document.TextArray, start, length);
}
else
{
char[] newText =
new char[document.TextArray.Length +
value.Length - length];
Array.Copy(document.TextArray, 0, newText,
0, start);
Array.Copy(value.ToCharArray(), 0, newText,
start, value.Length);
Array.Copy(document.TextArray, start + length,
newText, start + value.Length,
document.TextArray.Length - start
- length);
document.TextArray = newText;
}
}
else
throw new IndexOutOfRangeException();
}
}
43
// Type allowing the document to be viewed like an "array"
// of characters:
public class CharacterCollection
{
readonly Document document; // The containing document
internal CharacterCollection(Document d)
{
document = d;
}
44
private char[] TextArray; // The text of the document.
class Test
{
static void Main()
{
Document d = new Document(
"peter piper picked a peck of pickled peppers. How many pickled peppers did peter piper
pick?"
);
45
Console.WriteLine(d.Text);
}
}
Output
PeneloPe PiPer Picked a Peck of Pickled PePPers. How many Pickled PePPers did PeneloPe
PiPer Pick?
This tutorial shows how to define and use conversions to or from classes or structs.
In C#, conversions can be declared either as implicit, which occur automatically when required,
or explicit, which require a cast to be called. All conversions must be static, and must either take
the type the conversion is defined on, or return that type.
This tutorial introduces two examples. The first example shows how to declare and use
conversions, and the second example demonstrates conversions between structs.
Example 1
In this example, a RomanNumeral type is declared, and several conversions to and from it are
defined.
// conversion.cs
using System;
struct RomanNumeral
{
public RomanNumeral(int value)
{
this.value = value;
}
// Declare a conversion from an int to a RomanNumeral. Note the
// the use of the operator keyword. This is a conversion
// operator named RomanNumeral:
static public implicit operator RomanNumeral(int value)
46
{
// Note that because RomanNumeral is declared as a struct,
// calling new on the struct merely calls the constructor
// rather than allocating an object on the heap:
return new RomanNumeral(value);
}
// Declare an explicit conversion from a RomanNumeral to an int:
static public explicit operator int(RomanNumeral roman)
{
return roman.value;
}
// Declare an implicit conversion from a RomanNumeral to
// a string:
static public implicit operator string(RomanNumeral roman)
{
return("Conversion not yet implemented");
}
private int value;
}
class Test
{
static public void Main()
{
RomanNumeral numeral;
numeral = 10;
47
Console.WriteLine(s);
}
}
Output
10
Conversion not yet implemented
10
Example 2
This example defines two structs, RomanNumeral and BinaryNumeral, and demonstrates
conversions between them.
// structconversion.cs
using System;
struct RomanNumeral
{
public RomanNumeral(int value)
{
this.value = value;
}
static public implicit operator RomanNumeral(int value)
{
return new RomanNumeral(value);
}
static public implicit operator RomanNumeral(BinaryNumeral binary)
{
return new RomanNumeral((int)binary);
}
static public explicit operator int(RomanNumeral roman)
{
return roman.value;
}
static public implicit operator string(RomanNumeral roman)
{
return("Conversion not yet implemented");
}
48
private int value;
}
struct BinaryNumeral
{
public BinaryNumeral(int value)
{
this.value = value;
}
static public implicit operator BinaryNumeral(int value)
{
return new BinaryNumeral(value);
}
static public implicit operator string(BinaryNumeral binary)
{
return("Conversion not yet implemented");
}
static public explicit operator int(BinaryNumeral binary)
{
return(binary.value);
}
class Test
{
static public void Main()
{
RomanNumeral roman;
roman = 10;
BinaryNumeral binary;
// Perform a conversion from a RomanNumeral to a
// BinaryNumeral:
binary = (BinaryNumeral)(int)roman;
// Performs a conversion from a BinaryNumeral to a RomanNumeral.
// No cast is required:
roman = binary;
Console.WriteLine((int)binary);
Console.WriteLine(binary);
49
}
}
Output
10
Conversion not yet implemented
Code Discussion
binary = (BinaryNumeral)(int)roman;
performs a conversion from a RomanNumeral to a BinaryNumeral. Because there is no
direct conversion from RomanNumeral to BinaryNumeral, a cast is used to convert from a
RomanNumeral to an int, and another cast to convert from an int to a BinaryNumeral.
roman = binary;
performs a conversion from a BinaryNumeral to a RomanNumeral. Because
RomanNumeral defines an implicit conversion from BinaryNumeral, no cast is required.
Example 1
This example shows how you can use operator overloading to create a complex number class
Complex that defines complex addition. The program displays the imaginary and the real parts of
the numbers and the addition result using an override of the ToString method.
// complex.cs
using System;
50
public struct Complex
{
public int real;
public int imaginary;
// Print the numbers and the sum using the overriden ToString method:
Console.WriteLine("First complex number: {0}",num1);
Console.WriteLine("Second complex number: {0}",num2);
Console.WriteLine("The sum of the two numbers: {0}",sum);
}
}
51
Output
Example 2
This example shows how operator overloading can be used to implement a three-valued logical
type. The possible values of this type are DBBool.dbTrue, DBBool.dbFalse, and DBBool.dbNull,
where the dbNull member indicates an unknown value.
Note Defining the True and False operators is only useful for types that represent True, False,
and Null (neither True nor False), as used in databases.
// dbbool.cs
using System;
52
// true or false:
public static explicit operator bool(DBBool x)
{
if (x.value == 0) throw new InvalidOperationException();
return x.value > 0;
}
53
public static DBBool operator |(DBBool x, DBBool y)
{
return new DBBool(x.value > y.value? x.value: y.value);
}
54
// Override the Object.GetHashCode() method:
public override int GetHashCode()
{
return value;
}
class Test
{
static void Main()
{
DBBool a, b;
a = DBBool.dbTrue;
b = DBBool.dbNull;
55
Console.WriteLine("b is not definitely true");
}
}
Output
!DBBool.True = DBBool.False
!DBBool.Null = DBBool.Null
DBBool.True & DBBool.Null = DBBool.Null
DBBool.True | DBBool.Null = DBBool.True
b is not definitely true
Delegates Tutorial
This tutorial demonstrates the delegate types. It shows how to map delegates to static and
instance methods, and how to combine them (multicast).
A delegate declaration defines a type that encapsulates a method with a particular set of
arguments and return type. For static methods, a delegate object encapsulates the method to be
called. For instance methods, a delegate object encapsulates both an instance and a method on
the instance. If you have a delegate object and an appropriate set of arguments, you can invoke
the delegate with the arguments.
An interesting and useful property of a delegate is that it does not know or care about the class of
the object that it references. Any object will do; all that matters is that the method's argument
types and return type match the delegate's. This makes delegates perfectly suited for
"anonymous" invocation.
Note Delegates run under the caller's security permissions, not the declarer's permissions.
This tutorial includes two examples:
56
• Delegates and Events
Example 1
The following example illustrates declaring, instantiating, and using a delegate. The BookDB class
encapsulates a bookstore database that maintains a database of books. It exposes a method
ProcessPaperbackBooks, which finds all paperback books in the database and calls a delegate
for each one. The delegate type used is called ProcessBookDelegate. The Test class uses this
class to print out the titles and average price of the paperback books.
The use of delegates promotes good separation of functionality between the bookstore database
and the client code. The client code has no knowledge of how the books are stored or how the
bookstore code finds paperback books. The bookstore code has no knowledge of what
processing is done on the paperback books after it finds them.
// bookstore.cs
using System;
57
// Declare a delegate type for processing a book:
public delegate void ProcessBookDelegate(Book book);
58
{
countBooks += 1;
priceBooks += book.Price;
}
59
Console.WriteLine("Average Paperback Book Price: ${0:#.##}",
totaller.AveragePrice());
}
Output
Code Discussion
• Instantiating a delegate Once a delegate type has been declared, a delegate object
must be created and associated with a particular method. Like all other objects, a new
delegate object is created with a new expression. When creating a delegate, however,
60
the argument passed to the new expression is special — it is written like a method call,
but without the arguments to the method.
bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(PrintTitle));
creates a new delegate object associated with the static method Test.PrintTitle. The
following statement:
bookDB.ProcessPaperbackBooks(new
ProcessBookDelegate(totaller.AddBookToTotal));
creates a new delegate object associated with the nonstatic method AddBookToTotal on
the object totaller. In both cases, this new delegate object is immediately passed to the
ProcessPaperbackBooks method.
Note that once a delegate is created, the method it is associated with never changes —
delegate objects are immutable.
• Calling a delegate Once a delegate object is created, the delegate object is typically
passed to other code that will call the delegate. A delegate object is called by using the
name of the delegate object, followed by the parenthesized arguments to be passed to
the delegate. An example of a delegate call is:
processBook(b);
A delegate can either be called synchronously, as in this example, or asynchronously by
using BeginInvoke and EndInvoke methods.
Example 2
This example demonstrates composing delegates. A useful property of delegate objects is that
they can be composed using the "+" operator. A composed delegate calls the two delegates it
was composed from. Only delegates of the same type can be composed.
The "-" operator can be used to remove a component delegate from a composed delegate.
// compose.cs
using System;
class MyClass
{
public static void Hello(string s)
61
{
Console.WriteLine(" Hello, {0}!", s);
}
Output
Invoking delegate a:
Hello, A!
62
Invoking delegate b:
Goodbye, B!
Invoking delegate c:
Hello, C!
Goodbye, C!
Invoking delegate d:
Goodbye, D!
• An event-like design pattern is desired (for more information, see the Events Tutorial).
• The caller has no need to know or obtain the object that the method is defined on.
• The provider of the implementation wants to "hand out" the implementation of the
specification to only a few select components.
• Easy composition is desired.
• The caller of the interface wants to cast to or from the interface type to obtain other
interfaces or classes.
63
Events Tutorial
This tutorial shows how to declare, invoke, and hook up to events in C#.
An event in C# is a way for a class to provide notifications to clients of that class when some
interesting thing happens to an object. The most familiar use for events is in graphical user
interfaces; typically, the classes that represent controls in the interface have events that are
notified when the user does something to the control (for example, click a button).
Events, however, need not be used only for graphical interfaces. Events provide a generally
useful way for objects to signal state changes that may be useful to clients of that object. Events
are an important building block for creating classes that can be reused in a large number of
different programs.
Events are declared using delegates. If you have not yet studied the Delegates Tutorial, you
should do so before continuing. Recall that a delegate object encapsulates a method so that it
can be called anonymously. An event is a way for a class to allow clients to give it delegates to
methods that should be called when the event occurs. When the event occurs, the delegate(s)
given to it by its clients are invoked.
In addition to the examples on declaring, invoking, and hooking up to events, this tutorial also
introduces the following topics:
• Events in Interfaces
Example 1
The following simple example shows a class, ListWithChangedEvent, which is similar to the
standard ArrayList class, but also invokes a Changed event whenever the contents of the list
change. Such a general-purpose class could be used in numerous ways in a large program.
For example, a word processor might maintain a list of the open documents. Whenever this list
changes, many different objects in the word processor might need to be notified so that the user
interface could be updated. By using events, the code that maintains the list of documents doesn't
need to know who needs to be notified — once the list of documents is changed, the event is
automatically invoked and every object that needs to be notified is correctly notified. By using
events, the modularity of the program is increased.
// events1.cs
using System;
namespace MyCollections
64
{
using System.Collections;
65
base[index] = value;
OnChanged(EventArgs.Empty);
}
}
}
}
namespace TestEvents
{
using MyCollections;
class EventListener
{
private ListWithChangedEvent List;
class Test
{
// Test the ListWithChangedEvent class.
public static void Main()
66
{
// Create a new list.
ListWithChangedEvent list = new ListWithChangedEvent();
Output
Code Discussion
• Declaring an event To declare an event inside a class, first a delegate type for the
event must be declared, if none is already declared.
• Invoking an event Once a class has declared an event, it can treat that event just like a
field of the indicated delegate type. The field will either be null, if no client has hooked up
a delegate to the event, or else it refers to a delegate that should be called when the
event is invoked. Thus, invoking an event is generally done by first checking for null and
then calling the event.
67
if (Changed != null)
Changed(this, e);
Invoking an event can only be done from within the class that declared the event.
• Hooking up to an event From outside the class that declared it, an event looks like a
field, but access to that field is very restricted. The only things that can be done are:
• Compose a new delegate onto that field.
This is done with the += and -= operators. To begin receiving event invocations, client
code first creates a delegate of the event type that refers to the method that should be
invoked from the event. Then it composes that delegate onto any other delegates that the
event might be connected to using +=.
In the preceding example, this has been done with the OnChanged method. A derived class could
call or override this method if it needed to.
Events in Interfaces
One other difference between events and fields is that an event can be placed in an interface
while a field cannot. When implementing the interface, the implementing class must supply a
corresponding event in the class that implements the interface.
68
.NET Framework Guidelines
Although the C# language allows events to use any delegate type, the .NET Framework has
some stricter guidelines on the delegate types that should be used for events. If you intend for
your component to be used with the .NET Framework, you probably will want to follow these
guidelines.
The .NET Framework guidelines indicate that the delegate type used for an event should take two
parameters, an "object source" parameter indicating the source of the event, and an "e"
parameter that encapsulates any additional information about the event. The type of the "e"
parameter should derive from the EventArgs class. For events that do not use any additional
information, the .NET Framework has already defined an appropriate delegate type:
EventHandler.
Example 2
The following example is a modified version of Example 1 that follows the .NET Framework
guidelines. The example uses the EventHandler delegate type.
// events2.cs
using System;
namespace MyCollections
{
using System.Collections;
69
public override int Add(object value)
{
int i = base.Add(value);
OnChanged(EventArgs.Empty);
return i;
}
namespace TestEvents
{
using MyCollections;
class EventListener
{
private ListWithChangedEvent List;
70
private void ListChanged(object sender, EventArgs e)
{
Console.WriteLine("This is called when the event fires.");
}
class Test
{
// Test the ListWithChangedEvent class:
public static void Main()
{
// Create a new list:
ListWithChangedEvent list = new ListWithChangedEvent();
Output
A class that implements an interface can explicitly implement a member of that interface. When a
member is explicitly implemented, it cannot be accessed through a class instance, but only
71
through an instance of the interface. This tutorial contains two examples. The first example
illustrates how to explicitly implement and access interface members. The second example shows
how to implement two interfaces that have the same member names.
Example 1
This example declares an interface IDimensions, and a class Box, which explicitly implements the
interface members Length and Width. The members are accessed through the interface instance
myDimensions.
// explicit1.cs
interface IDimensions
{
float Length();
float Width();
}
72
Box myBox = new Box(30.0f, 20.0f);
// Declare an interface instance "myDimensions":
IDimensions myDimensions = (IDimensions) myBox;
// Print out the dimensions of the box:
/* The following commented lines would produce compilation
errors because they try to access an explicitly implemented
interface member from a class instance: */
//System.Console.WriteLine("Length: {0}", myBox.Length());
//System.Console.WriteLine("Width: {0}", myBox.Width());
/* Print out the dimensions of the box by calling the methods
from an instance of the interface: */
System.Console.WriteLine("Length: {0}", myDimensions.Length());
System.Console.WriteLine("Width: {0}", myDimensions.Width());
}
}
Output
Length: 30
Width: 20
Code Discussion
• Notice that the following lines, in the Main method, are commented out because they
would produce compilation errors. An interface member that is explicitly implemented
cannot be accessed from a class instance:
• Notice also that the following lines, in the Main method, successfully print out the
dimensions of the box because the methods are being called from an instance of the
interface:
Example 2
Explicit interface implementation also allows the programmer to inherit two interfaces that share
the same member names and give each interface member a separate implementation. This
73
example displays the dimensions of a box in both metric and English units. The Box class inherits
two interfaces IEnglishDimensions and IMetricDimensions, which represent the different
measurement systems. Both interfaces have identical member names, Length and Width.
// explicit2.cs
// Declare the English units interface:
interface IEnglishDimensions
{
float Length();
float Width();
}
// Declare the metric units interface:
interface IMetricDimensions
{
float Length();
float Width();
}
// Declare the "Box" class that implements the two interfaces:
// IEnglishDimensions and IMetricDimensions:
class Box : IEnglishDimensions, IMetricDimensions
{
float lengthInches;
float widthInches;
public Box(float length, float width)
{
lengthInches = length;
widthInches = width;
}
// Explicitly implement the members of IEnglishDimensions:
float IEnglishDimensions.Length()
{
return lengthInches;
}
float IEnglishDimensions.Width()
{
return widthInches;
}
// Explicitly implement the members of IMetricDimensions:
float IMetricDimensions.Length()
{
return lengthInches * 2.54f;
74
}
float IMetricDimensions.Width()
{
return widthInches * 2.54f;
}
public static void Main()
{
// Declare a class instance "myBox":
Box myBox = new Box(30.0f, 20.0f);
// Declare an instance of the English units interface:
IEnglishDimensions eDimensions = (IEnglishDimensions) myBox;
// Declare an instance of the metric units interface:
IMetricDimensions mDimensions = (IMetricDimensions) myBox;
// Print dimensions in English units:
System.Console.WriteLine("Length(in): {0}", eDimensions.Length());
System.Console.WriteLine("Width (in): {0}", eDimensions.Width());
// Print dimensions in metric units:
System.Console.WriteLine("Length(cm): {0}", mDimensions.Length());
System.Console.WriteLine("Width (cm): {0}", mDimensions.Width());
}
}
Output
Length(in): 30
Width (in): 20
Length(cm): 76.2
Width (cm): 50.8
Code Discussion
If you want to make the default measurements in English units, implement the methods Length
and Width normally, and explicitly implement the Length and Width methods from the
IMetricDimensions interface:
// Normal implementation:
public float Length()
{
return lengthInches;
}
public float Width()
75
{
return widthInches;
}
// Explicit implementation:
float IMetricDimensions.Length()
{
return lengthInches * 2.54f;
}
float IMetricDimensions.Width()
{
return widthInches * 2.54f;
}
In this case, you can access the English units from the class instance and access the metric units
from the interface instance:
This tutorial demonstrates conditional methods, which provide a powerful mechanism by which
calls to methods can be included or omitted depending on whether a preprocessor symbol is
defined.
Conditional methods allow developers to create methods whose calls can be placed in the code
and then either included or omitted during compilation based on a preprocessing symbol.
Suppose that you want to enable some assertion code in the debug builds and disable it in the
retail builds. In C++, there is more than one way to have this functionality in your code, for
example:
• Using #ifdef, define both debug and release versions of a macro. The debug version calls
the tracing code, and the release version does nothing. Because C# doesn't support
macros, this method doesn't work.
• Have two implementations of the code being called. That is, in the debug version, have
full functionality, and in the retail version, have empty stubs for the methods. Users then
choose which one to include when linking the project. The problem with this approach is
that the retail builds contain calls to empty methods, and the configuration is more
complex.
76
C# conditional methods provide a simple solution to this problem that is similar to the first
approach listed above. There are two basic mechanisms to do this:
• #define the preprocessing identifier in the source code directly. (See an example of this
approach in Conditional.)
• Define the preprocessing identifier on the C# command line via the /define option (/d).
This approach is used in the following example.
Conditional methods are used in the .NET Framework. The System.Diagnostics namespace
contains a number of classes that support tracing and debugging in applications. Use the
System.Diagnostics.Trace and System.Diagnostics.Debug classes to add sophisticated
tracing and debugging to your application (functionality that can be compiled out of your retail
builds through the use of conditional methods).
The example below shows how to implement a very simple tracing mechanism using conditional
methods. System.Diagnostics.Trace provides much more sophisticated tracing mechanisms,
but it uses the fundamental mechanism below to provide this functionality.
Example
This example consists of two source files: the first file is the library that provides a tracing
mechanism, and the second file is the client program that uses this library.
The code below shows a simple library that provides a tracing mechanism that displays trace
messages to the system console. Clients can embed trace calls in the code and then be able to
control whether the tracing gets called by defining symbols in their own compilation phase.
Copy Code
// CondMethod.cs
// compile with: /target:library /d:DEBUG
using System;
using System.Diagnostics;
namespace TraceFunctions
{
public class Trace
{
[Conditional("DEBUG")]
public static void Message(string traceMessage)
{
77
Console.WriteLine("[TRACE] - " + traceMessage);
}
}
}
Code Discussion
[Conditional("DEBUG")]
marks the Message method as being conditional (via the Conditional attribute). The Conditional
attribute takes one parameter — the preprocessing identifier that controls whether the method call
is included when clients are compiled. If the preprocessing identifier is defined, the method is
called; otherwise, the call is never inserted in the client's code.
There are restrictions on which methods can be marked as conditional; see 17.4.2 The
Conditional attribute in the C# Language Specification for more information.
The following client program uses the Trace class defined in file #1 to do some simple tracing.
// TraceTest.cs
// compile with: /reference:CondMethod.dll
// arguments: A B C
using System;
using TraceFunctions;
if (args.Length == 0)
{
Console.WriteLine("No arguments have been passed");
}
else
{
for( int i=0; i < args.Length; i++)
{
78
Console.WriteLine("Arg[{0}] is [{1}]",i,args[i]);
}
}
Trace.Message("Main Ending");
}
}
Code Discussion
Conditional code is included in client code depending on whether the preprocessing identifier is
defined when the client code gets compiled.
Compiling with client code with the /d:DEBUG flag means that the compiler inserts the call to the
Trace method. If the symbol is not defined, the call is never made.
Sample Run
The command:
tracetest A B C
gives the following output:
tracetest
gives the following output:
79
C# provides a mechanism for developers to document their code using XML. In source code files,
lines that begin with /// and that precede a user-defined type such as a class, delegate, or
interface; a member such as a field, event, property, or method; or a namespace declaration can
be processed as comments and placed in a file.
Example
The following sample provides a basic overview of a type that has been documented. To compile
the example, type the following command line:
// XMLsample.cs
// compile with: /doc:XMLsample.xml
using System;
/// <summary>
/// Class level summary documentation goes here.</summary>
/// <remarks>
/// Longer comments can be associated with a type or member
/// through the remarks tag</remarks>
public class SomeClass
{
/// <summary>
/// Store for the name property</summary>
private string myName = null;
/// <summary>
/// The class constructor. </summary>
public SomeClass()
{
// TODO: Add Constructor Logic here
}
/// <summary>
/// Name property </summary>
/// <value>
/// A value tag is used to describe the property value</value>
public string Name
80
{
get
{
if ( myName == null )
{
throw new Exception("Name is null");
}
return myName;
}
}
/// <summary>
/// Description for SomeMethod.</summary>
/// <param name="s"> Parameter description for s goes here</param>
/// <seealso cref="String">
/// You can use the cref attribute on any tag to reference a type or member
/// and the compiler will check that the reference exists. </seealso>
public void SomeMethod(string s)
{
}
/// <summary>
/// Some other method. </summary>
/// <returns>
/// Return results are described through the returns tag.</returns>
/// <seealso cref="SomeMethod(string)">
/// Notice the use of the cref attribute to reference a specific method </seealso>
public int SomeOtherMethod()
{
return 0;
}
/// <summary>
/// The entry point for the application.
/// </summary>
/// <param name="args"> A list of command line arguments</param>
public static int Main(String[] args)
{
// TODO: Add code to start application here
81
return 0;
}
}
Code Discussion
XML documentation starts with ///. When you create a new project, the wizards put some
starter /// lines in for you. The processing of these comments has some restrictions:
• The documentation must be well-formed XML. If the XML is not well-formed, a warning is
generated and the documentation file will contain a comment saying that an error was
encountered. For more information on well-formed XML, see XML Glossary.
• Developers are free to create their own set of tags. There is a recommended set of tags
(see the Further Reading section). Some of the recommended tags have special
meanings:
• The <param> tag is used to describe parameters. If used, the compiler will verify
that the parameter exists and that all parameters are described in the documentation.
If the verification failed, the compiler issues a warning.
• The cref attribute can be attached to any tag to provide a reference to a code
element. The compiler will verify that this code element exists. If the verification failed,
the compiler issues a warning. The compiler also respects any using statements when
looking for a type described in the cref attribute.
• The <summary> tag is used by IntelliSense inside Visual Studio to display
additional information about a type or member.
Sample Output
<?xml version="1.0"?>
<doc>
<assembly>
<name>xmlsample</name>
</assembly>
<members>
<member name="T:SomeClass">
<summary>
Class level summary documentation goes here.</summary>
<remarks>
82
Longer comments can be associated with a type or member
through the remarks tag</remarks>
</member>
<member name="F:SomeClass.myName">
<summary>
Store for the name property</summary>
</member>
<member name="M:SomeClass.#ctor">
<summary>The class constructor.</summary>
</member>
<member name="M:SomeClass.SomeMethod(System.String)">
<summary>
Description for SomeMethod.</summary>
<param name="s"> Parameter description for s goes here</param>
<seealso cref="T:System.String">
You can use the cref attribute on any tag to reference a type or member
and the compiler will check that the reference exists. </seealso>
</member>
<member name="M:SomeClass.SomeOtherMethod">
<summary>
Some other method. </summary>
<returns>
Return results are described through the returns tag.</returns>
<seealso cref="M:SomeClass.SomeMethod(System.String)">
Notice the use of the cref attribute to reference a specific method </seealso>
</member>
<member name="M:SomeClass.Main(System.String[])">
<summary>
The entry point for the application.
</summary>
<param name="args"> A list of command line arguments</param>
</member>
<member name="P:SomeClass.Name">
<summary>
Name property </summary>
<value>
A value tag is used to describe the property value</value>
</member>
</members>
</doc>
83
Note The XML file does not provide full information about the type and members (for example, it
does not contain any type information). To get full information about a type or member, the
documentation file must be used in conjunction with reflection on the actual type or member.
Platform Invocation Services (PInvoke) allows managed code to call unmanaged functions that
are implemented in a DLL.
This tutorial shows you what you need to do to be able to call unmanaged DLL functions from C#.
The attributes discussed in the tutorial allow you to call these functions and have data types be
marshaled correctly.
There are two ways that C# code can directly call unmanaged code:
• Call an interface method on a COM object (for more information, see COM Interop Part 1:
C# Client Tutorial).
For both techniques, you must provide the C# compiler with a declaration of the unmanaged
function, and you may also need to provide the C# compiler with a description of how to marshal
the parameters and return value to and from the unmanaged code.
84
• Attach the DllImport attribute to the method. The DllImport attribute allows you to
specify the name of the DLL that contains the method. The common practice is to name
the C# method the same as the exported method, but you can also use a different name
for the C# method.
• Optionally, specify custom marshaling information for the method's parameters and return
value, which will override the .NET Framework default marshaling.
Example 1
This example shows you how to use the DllImport attribute to output a message by calling puts
from msvcrt.dll.
// PInvokeTest.cs
using System;
using System.Runtime.InteropServices;
class PlatformInvokeTest
{
[DllImport("msvcrt.dll")]
public static extern int puts(string c);
[DllImport("msvcrt.dll")]
internal static extern int _flushall();
Output
Test
Code Discussion
The preceding example shows the minimum requirements for declaring a C# method that is
implemented in an unmanaged DLL. The method PlatformInvokeTest.puts is declared with the
static and extern modifiers and has the DllImport attribute which tells the compiler that the
implementation comes from msvcrt.dll, using the default name of puts. To use a different name for
85
the C# method such as putstring, you must use the EntryPoint option in the DllImport attribute,
that is:
[DllImport("msvcrt.dll", EntryPoint="puts")]
For more information on the syntax of the DllImport attribute, see DllImportAttribute Class.
For every .NET Framework type there is a default unmanaged type, which the common language
runtime will use to marshal data across a managed to unmanaged function call. For example, the
default marshaling for C# string values is to the type LPTSTR (pointer to TCHAR char buffer).
You can override the default marshaling using the MarshalAs attribute in the C# declaration of
the unmanaged function.
Example 2
This example uses the DllImport attribute to output a string. It also shows you how to override the
default marshaling of the function parameters by using the MarshalAs attribute.
// Marshal.cs
using System;
using System.Runtime.InteropServices;
class PlatformInvokeTest
{
[DllImport("msvcrt.dll")]
public static extern int puts(
[MarshalAs(UnmanagedType.LPStr)]
string m);
[DllImport("msvcrt.dll")]
internal static extern int _flushall();
86
Output
Hello World!
will display at the console.
Code Discussion
In the preceding example, the default marshaling for the parameter to the puts function has been
overridden from the default of LPTSTR to LPSTR.
The MarshalAs attribute can be placed on method parameters, method return values, and fields
of structs and classes. To set the marshaling of a method return value, place the MarshalAs
attribute in an attribute block on the method with the return attribute location override. For
example, to explicitly set the marshaling for the return value of the puts method:
...
[DllImport("msvcrt.dll")]
[return : MarshalAs(UnmanagedType.I4)]
public static extern int puts(
...
For more information on the syntax of the MarshalAs attribute, see MarshalAsAttribute Class.
Note The In and Out attributes can be used to annotate parameters to unmanaged methods.
They behave in a similar manner to the in and out modifiers in MIDL source files. Note that the
Out attribute is different from the C# parameter modifier, out. For more information on the In and
Out attributes, see InAttribute Class and OutAttribute Class.
Example 3
This example demonstrates how to specify custom marshaling attributes for a struct.
87
LONG lfWidth;
LONG lfEscapement;
LONG lfOrientation;
LONG lfWeight;
BYTE lfItalic;
BYTE lfUnderline;
BYTE lfStrikeOut;
BYTE lfCharSet;
BYTE lfOutPrecision;
BYTE lfClipPrecision;
BYTE lfQuality;
BYTE lfPitchAndFamily;
TCHAR lfFaceName[LF_FACESIZE];
} LOGFONT;
In C#, you can describe the preceding struct by using the StructLayout and MarshalAs
attributes as follows:
// logfont.cs
// compile with: /target:module
using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public class LOGFONT
{
public const int LF_FACESIZE = 32;
public int lfHeight;
public int lfWidth;
public int lfEscapement;
public int lfOrientation;
public int lfWeight;
public byte lfItalic;
public byte lfUnderline;
public byte lfStrikeOut;
public byte lfCharSet;
public byte lfOutPrecision;
public byte lfClipPrecision;
public byte lfQuality;
public byte lfPitchAndFamily;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=LF_FACESIZE)]
public string lfFaceName;
88
}
For more information on the syntax of the StructLayout attribute, see StructLayoutAttribute
Class.
// pinvoke.cs
// compile with: /addmodule:logfont.netmodule
using System;
using System.Runtime.InteropServices;
class PlatformInvokeTest
{
[DllImport("gdi32.dll", CharSet=CharSet.Auto)]
public static extern IntPtr CreateFontIndirect(
[In, MarshalAs(UnmanagedType.LPStruct)]
LOGFONT lplf // characteristics
);
[DllImport("gdi32.dll")]
public static extern bool DeleteObject(
IntPtr handle
);
if (IntPtr.Zero == handle)
{
Console.WriteLine("Can't creates a logical font.");
}
else
{
if (IntPtr.Size == 4)
Console.WriteLine("{0:X}", handle.ToInt32());
else
89
Console.WriteLine("{0:X}", handle.ToInt64());
Sample Run
C30A0AE5
Code Discussion
In the preceding example, the CreateFontIndirect method is using a parameter of the type
LOGFONT. The MarshalAs and In attributes are used to qualify the parameter. The program
displays the numeric value returned by the method as a hexadecimal uppercase string.
For example, consider the following unmanaged function, MyFunction, which requires callback as
one of the arguments:
90
COM Interop Part 1: C# Client Tutorial
COM Interop provides access to existing COM components without requiring that the original
component be modified. When you want to incorporate COM code into a managed application,
import the relevant COM types by using a COM Interop utility (TlbImp.exe) for that purpose. Once
imported, the COM types are ready to use.
In addition, COM Interop allows COM developers to access managed objects as easily as they
access other COM objects. Again, COM Interop provides a specialized utility (RegAsm.exe) that
exports the managed types into a type library and registers the managed component as a
traditional COM component.
At run time, the common language runtime marshals data between COM objects and managed
objects as needed.
COM Interop Part 2: C# Server Tutorial covers using a C# server with a C++ COM client. For an
overview of both tutorials, see COM Interop Tutorials.
C# uses .NET Framework facilities to perform COM Interop. C# has support for:
The .NET Framework handles reference-counting issues with COM Interop so there is no need to
call or implement AddRef and Release.
91
(Type Library Importer), a command-line tool included in the .NET Framework SDK. TlbImp
converts a COM type library into .NET Framework metadata — effectively creating a managed
wrapper that can be called from any managed language. .NET Framework metadata created with
TlbImp can be included in a C# build via the /R compiler option. If you are using the Visual Studio
development environment, you only need to add a reference to the COM type library and the
conversion is done for you automatically.
A great way to check the output of TlbImp is to run the .NET Framework SDK command-line tool
Ildasm.exe (Microsoft Intermediate Language Disassembler) to view the result of the conversion.
Although TlbImp is the preferred method for converting COM definitions to C#, it is not always
possible to use it (for example, if there is no typelib for the COM definitions, or if TlbImp cannot
handle the definitions in the typelib). In these cases, the alternative is to manually define the COM
definitions in C# source code using C# attributes. Once you have created the C# source mapping,
you simply compile the C# source code to produce the managed wrapper.
The main attributes you need to understand to perform COM mapping are:
• Guid – Used to specify a universally unique identifier (UUID) for a class or an interface.
• PreserveSig – specifies whether the native return value should be converted from an
HRESULT to a .NET Framework exception.
Each of these attributes is shown in the context of an actual example in this tutorial.
• The class must also have a Guid attribute that sets the globally unique identifier (GUID)
for the class.
92
//
// declare FilgraphManager as a COM coclass
//
[ComImport, Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770")]
class FilgraphManager
{
}
The C# compiler will add a parameterless constructor that you can call to create an instance of
the COM coclass.
class MainClass
{
public static void Main()
{
//
// Create an instance of a COM coclass - calls
//
// CoCreateInstance(E436EBB3-524F-11CE-9F53-0020AF0BA770,
// NULL, CLSCTX_ALL,
// IID_IUnknown, &f)
//
// returns null on failure.
//
FilgraphManager f = new FilgraphManager();
}
}
COM interfaces declared in C# must include declarations for all members of their base interfaces
with the exception of members of IUnknown and IDispatch — the .NET Framework
automatically adds these. COM interfaces which derive from IDispatch must be marked with the
InterfaceType attribute.
93
When calling a COM interface method from C# code, the common language runtime must
marshal the parameters and return values to/from the COM object. For every .NET Framework
type, there is a default type that the common language runtime will use to marshal when
marshaling across a COM call. For example, the default marshaling for C# string values is to the
native type LPTSTR (pointer to TCHAR char buffer). You can override the default marshaling
using the MarshalAs attribute in the C# declaration of the COM interface.
In COM, a common way to return success or failure is to return an HRESULT and have an out
parameter marked as "retval" in MIDL for the real return value of the method. In C# (and the .NET
Framework), the standard way to indicate an error has occurred is to throw an exception.
By default, the .NET Framework provides an automatic mapping between the two styles of
exception handling for COM interface methods called by the .NET Framework.
• The return value changes to the signature of the parameter marked retval (void if the
method has no parameter marked as retval).
• The parameter marked as retval is left off of the argument list of the method.
This example shows a COM interface declared in MIDL and the same interface declared in C#
(note that the methods use the COM error-handling approach).
[
odl,
uuid(56A868B1-0AD4-11CE-B03A-0020AF0BA770),
helpstring("IMediaControl interface"),
dual,
oleautomation
]
interface IMediaControl : IDispatch
{
[id(0x60020000)]
HRESULT Run();
[id(0x60020001)]
HRESULT Pause();
[id(0x60020002)]
HRESULT Stop();
94
[id(0x60020003)]
HRESULT GetState( [in] long msTimeout, [out] long* pfs);
[id(0x60020004)]
HRESULT RenderFile([in] BSTR strFilename);
[id(0x60020005)]
HRESULT AddSourceFilter( [in] BSTR strFilename, [out] IDispatch** ppUnk);
[id(0x60020006), propget]
HRESULT FilterCollection([out, retval] IDispatch** ppUnk);
[id(0x60020007), propget]
HRESULT RegFilterCollection([out, retval] IDispatch** ppUnk);
[id(0x60020008)]
HRESULT StopWhenReady();
};
Here is the C# equivalent of this interface:
using System.Runtime.InteropServices;
void Pause();
void Stop();
void RenderFile(
[In, MarshalAs(UnmanagedType.BStr)] string strFilename);
95
void AddSourceFilter(
[In, MarshalAs(UnmanagedType.BStr)] string strFilename,
[Out, MarshalAs(UnmanagedType.Interface)] out object ppUnk);
[return : MarshalAs(UnmanagedType.Interface)]
object FilterCollection();
[return : MarshalAs(UnmanagedType.Interface)]
object RegFilterCollection();
void StopWhenReady();
}
Note how the C# interface has mapped the error-handling cases. If the COM method returns an
error, an exception will be raised on the C# side.
96
The examples in this section represent two approaches:
This example shows you how to create an AVI viewer using TlbImp. The program reads an AVI
filename from the command line, creates an instance of the Quartz COM object, then uses the
methods RenderFile and Run to display the AVI file.
• Run TlbImp over the TLB. The Media Player used in this example is contained in
Quartz.dll, which should be in your Windows system directory. Use the following
command to create the .NET Framework DLL:
• You can use the Ildasm tool to examine the resulting DLL. For example, to display the
contents of the file QuartzTypeLib.dll, use the following command:
Ildasm QuartzTypeLib.dll
• Build the program using the C# compiler option /R to include the QuartzTypeLib.dll file.
You can then use the program to display a movie (an example movie to try is Clock.avi, which
resides in your Windows directory).
// interop1.cs
// compile with: /R:QuartzTypeLib.dll
using System;
class MainClass
{
/************************************************************
Abstract: This method collects the file name of an AVI to
show then creates an instance of the Quartz COM object.
To show the AVI, the program calls RenderFile and Run on
IMediaControl. Quartz uses its own thread and window to
display the AVI.The main thread blocks on a ReadLine until
97
the user presses ENTER.
Input Parameters: the location of the AVI file it is
going to display
Returns: void
**************************************************************/
public static void Main(string[] args)
{
// Check to see if the user passed in a filename
if (args.Length != 1)
{
DisplayUsage();
return;
}
if (args[0] == "/?")
{
DisplayUsage();
return;
}
try
{
QuartzTypeLib.FilgraphManager graphManager =
new QuartzTypeLib.FilgraphManager();
98
QuartzTypeLib.IMediaControl mc =
(QuartzTypeLib.IMediaControl)graphManager;
// Show file.
mc.Run();
}
catch(Exception ex)
{
Console.WriteLine("Unexpected COM exception: " + ex.Message);
}
Sample Run
interop1 %windir%\clock.avi
This will display the movie on your screen after you press ENTER.
99
This example uses the same Main method as Example 1, but instead of running TlbImp, it simply
maps the Media Player COM object using C#.
// interop2.cs
using System;
using System.Runtime.InteropServices;
namespace QuartzTypeLib
{
// Declare IMediaControl as a COM interface which
// derives from IDispatch interface:
[Guid("56A868B1-0AD4-11CE-B03A-0020AF0BA770"),
InterfaceType(ComInterfaceType.InterfaceIsDual)]
interface IMediaControl // Cannot list any base interfaces here
{
// Note that IUnknown Interface members are NOT listed here:
void Run();
void Pause();
void Stop();
void RenderFile(
[In, MarshalAs(UnmanagedType.BStr)] string strFilename);
void AddSourceFilter(
[In, MarshalAs(UnmanagedType.BStr)] string strFilename,
[Out, MarshalAs(UnmanagedType.Interface)]
out object ppUnk);
[return: MarshalAs(UnmanagedType.Interface)]
object FilterCollection();
[return: MarshalAs(UnmanagedType.Interface)]
object RegFilterCollection();
void StopWhenReady();
}
100
// Declare FilgraphManager as a COM coclass:
[ComImport, Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770")]
class FilgraphManager // Cannot have a base class or
// interface list here.
{
// Cannot have any members here
// NOTE that the C# compiler will add a default constructor
// for you (no parameters).
}
}
class MainClass
{
/**********************************************************
Abstract: This method collects the file name of an AVI to
show then creates an instance of the Quartz COM object.
To show the AVI, the program calls RenderFile and Run on
IMediaControl. Quartz uses its own thread and window to
display the AVI.The main thread blocks on a ReadLine until
the user presses ENTER.
Input Parameters: the location of the AVI file it is
going to display
Returns: void
*************************************************************/
if (args[0] == "/?")
{
DisplayUsage();
return;
}
101
String filename = args[0];
// Show file.
mc.Run();
}
catch(Exception ex)
{
Console.WriteLine("Unexpected COM exception: " + ex.Message);
}
// Wait for completion.
Console.WriteLine("Press Enter to continue.");
Console.ReadLine();
}
102
{
// User did not provide enough parameters.
// Display usage.
Console.WriteLine("VideoPlayer: Plays AVI files.");
Console.WriteLine("Usage: VIDEOPLAYER.EXE filename");
Console.WriteLine("where filename is the full path and");
Console.WriteLine("file name of the AVI to display.");
}
}
Sample Run
interop2 %windir%\clock.avi
This will display the movie on your screen after you press ENTER.
COM Interop allows COM developers to access managed code as easily as they access other
COM objects. This tutorial demonstrates using a C# server with a C++ COM client. It also
explains the following activities:
The tutorial also briefly demonstrates the marshaling that is automatically applied between
managed and unmanaged components.
COM Interop Part 1: C# Client Tutorial shows the fundamentals of using C# to interoperate with
COM objects and is a prerequisite for this tutorial. For an overview of both tutorials, see COM
Interop Tutorials.
• How to use the Guid attribute on interfaces and classes to expose them as COM objects
and how to generate a globally unique identifier (GUID) for the Guid attribute.
• How to use RegAsm to register a .NET Framework program for use by COM clients and
create a type library (.tlb file) from a .NET Framework program.
The tutorial also demonstrates the following activities to create the COM client:
103
• How to export managed servers and how to use them to create COM objects.
• How to import the .tlb file, generated by RegAsm, into the COM client and how to use
CoCreateInstance to create an instance of a .NET Framework coclass.
Note To create a GUID for interfaces and coclasses that you export to COM clients, use
the tool Guidgen.exe, shipped as part of Visual Studio. Guidgen allows you to choose the
format in which the GUID is expressed so you don't have to retype it. For more
information on Guidgen, see the Knowledge Base article Q168318 "XADM: Guidgen.exe
Available Only for Intel Platforms." KB articles are available in the MSDN Library and on
the Web at http://support.microsoft.com.
Example
• A C# file, CSharpServer.cs, that creates the CSharpServer.dll file. The .dll is used to
create the file CSharpServer.tlb.
• A C++ file, COMClient.cpp, that creates the executable client, COMClient.exe.
File 1: CSharpServer.cs
// CSharpServer.cs
// compile with: /target:library
// post-build command: regasm CSharpServer.dll /tlb:CSharpServer.tlb
using System;
using System.Runtime.InteropServices;
namespace CSharpServer
{
// Since the .NET Framework interface and coclass have to behave as
// COM objects, we have to give them guids.
[Guid("DBE0E8C4-1C61-41f3-B6A4-4E2F353D3D05")]
public interface IManagedInterface
{
int PrintHi(string name);
}
[Guid("C6659361-1625-4746-931C-36014B146679")]
public class InterfaceImplementation : IManagedInterface
{
104
public int PrintHi(string name)
{
Console.WriteLine("Hello, {0}!", name);
return 33;
}
}
}
File 2: COMClient.cpp
// COMClient.cpp
// Build with "cl COMClient.cpp"
// arguments: friend
#include <windows.h>
#include <stdio.h>
// For simplicity, we ignore the server namespace and use named guids:
#if defined (USINGPROJECTSYSTEM)
#import "..\RegisterCSharpServerAndExportTLB\CSharpServer.tlb" no_namespace named_guids
#else // Compiling from the command line, all files in the same directory
#import "CSharpServer.tlb" no_namespace named_guids
#endif
int main(int argc, char* argv[])
{
IManagedInterface *cpi = NULL;
int retval = 1;
105
if (FAILED(hr))
{
printf("Couldn't create the instance!... 0x%x\n", hr);
}
else
{
if (argc > 1)
{
printf("Calling function.\n");
fflush(stdout);
// The variable cpi now holds an interface pointer
// to the managed interface.
// If you are on an OS that uses ASCII characters at the
// command prompt, notice that the ASCII characters are
// automatically marshaled to Unicode for the C# code.
if (cpi->PrintHi(argv[1]) == 33)
retval = 0;
printf("Returned from function.\n");
}
else
printf ("Usage: COMClient <name>\n");
cpi->Release();
cpi = NULL;
}
Output
The executable client can be invoked with the command line: COMClient <name>, where <name>
is any string you want to use, for example, COMClient friend.
Calling function.
Hello, friend!
Returned from function.
In the sample IDE project, set the Command Line Arguments property in the project's Property
Pages to the desired string (for example, "friend").
106
Attributes Tutorial
This tutorial shows how to create custom attribute classes, use them in code, and query them
through reflection.
Attributes provide a powerful method of associating declarative information with C# code (types,
methods, properties, and so forth). Once associated with a program entity, the attribute can be
queried at run time and used in any number of ways.
Once the attribute has been created, you then associate the attribute with a particular
program element.
Once the attribute has been associated with a program element, you use reflection to
query its existence and its value.
using System;
[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute : System.Attribute
{
public readonly string Url;
107
{
get
{
return topic;
}
set
{
topic = value;
}
}
Code Discussion
• The attribute AttributeUsage specifies the language elements to which the attribute can
be applied.
• Attributes classes are public classes derived from System.Attribute that have at least
one public constructor.
• Attribute classes have two types of parameters:
• Simple types (bool, byte, char, short, int, long, float, and double)
• string
• System.Type
• enums
108
• object (The argument to an attribute parameter of type object must be a constant
value of one of the above types.)
• One-dimensional arrays of any of the above types
The attribute AttributeUsage provides the underlying mechanism by which attributes are
declared.
• AllowOn, which specifies the program elements that the attribute can be assigned to
(class, method, property, parameter, and so on). Valid values for this parameter can be
found in the System.Attributes.AttributeTargets enumeration in the .NET Framework.
The default value for this parameter is all program elements (AttributeElements.All).
[HelpAttribute("http://localhost/MyClassInfo")]
class MyClass
{
}
In this example, the HelpAttribute attribute is associated with MyClass.
Note By convention, all attribute names end with the word "Attribute" to distinguish them from
other items in the .NET Framework. However, you do not need to specify the attribute suffix when
using attributes in code. For example, you can specify HelpAttribute as follows:
[Help("http://localhost/MyClassInfo")] // [Help] == [HelpAttribute]
class MyClass
{
}
109
System.Reflection.MemberInfo class (GetCustomAttributes family of methods). The following
example demonstrates the basic way of using reflection to get access to attributes:
class MainClass
{
public static void Main()
{
System.Reflection.MemberInfo info = typeof(MyClass);
object[] attributes = info.GetCustomAttributes(true);
for (int i = 0; i < attributes.Length; i ++)
{
System.Console.WriteLine(attributes[i]);
}
}
}
Example
The following is a complete example where all pieces are brought together.
// AttributesTutorial.cs
// This example shows the use of class and method attributes.
using System;
using System.Reflection;
using System.Collections;
110
// It can be applied to classes and struct declarations only.
// It takes one unnamed string argument (the author's name).
// It has one optional named argument Version, which is of type int.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class AuthorAttribute : Attribute
{
// This constructor specifies the unnamed arguments to the attribute class.
public AuthorAttribute(string name)
{
this.name = name;
this.version = 0;
}
111
string value = "Author : " + Name;
if (version != 0)
{
value += " Version : " + Version.ToString();
}
return value;
}
class MainClass
{
private static bool IsMemberTested(MemberInfo member)
112
{
foreach (object attribute in member.GetCustomAttributes(true))
{
if (attribute is IsTestedAttribute)
{
return true;
}
}
return false;
}
113
DumpAttributes(typeof(Order));
Output
Security Tutorial
This tutorial discusses .NET Framework security and shows two ways to modify security
permissions in C#: imperative and declarative security.
114
Most application and component developers should not need to do anything special in order to
work with the .NET Framework security system and benefit from the safety protection it provides.
One exception that requires more in-depth knowledge and special consideration of the security
system is secure libraries. This code represents the boundary between secure managed code
and unrestricted code, such as native code (that is outside the ability of the .NET Framework
security infrastructure to enforce). These libraries typically must be highly trusted to work, and are
the one place in managed code where a programming error can potentially expose a security
vulnerability. Code access security can't eliminate the potential for human error, but compared to
the much larger volume of application code that uses a few secure libraries, the amount of code
that requires close scrutiny is dramatically reduced.
Examples
The tutorial includes the following examples:
Security
The .NET Framework security protects your code and data from being misused or damaged by
other code by enforcing security restrictions on managed code. When a .NET Framework
application requests permission, the security policy established by the administrator grants the
permission or refuses to run the code. Trust is based on evidence about the code such as a
digital signature, where the code comes from, and so forth. Once granted, security enforces
permissions that control what code is (and by not being granted, what code is not) allowed to do.
Permissions
The .NET Framework security allows code to use protected resources only if it has "permission"
to do so. To express this, the .NET Framework uses the concept of permissions, which represent
the right for code to access protected resources. Code requests the permissions it needs, and the
security policy applied by the .NET Framework determines which permissions the code is actually
granted.
The .NET Framework provides code access permission classes, each of which encapsulates the
ability to access a particular resource. You use these permissions to indicate to the .NET
Framework what your code needs to be allowed to do and to indicate what your code's callers
must be authorized to do. Policy also uses these objects to determine what permissions to grant
to code.
115
Policy
Enforcement of security policy is what makes .NET Framework managed code safe. Every
assembly that loads is subject to security policy that grants code permissions based on trust, with
trust based on evidence about the code. See the .NET Framework documentation in the Reading
List for information on administering security policy.
The following two examples demonstrate these two approaches. For more information on
demanding security permissions, see Demands.
The following is an example of using .NET Framework calls to deny the UnmanagedCode
permission.
// ImperativeSecurity.cs
using System;
using System.Security;
using System.Security.Permissions;
using System.Runtime.InteropServices;
class NativeMethods
{
// This is a call to unmanaged code. Executing this method requires
// the UnmanagedCode security permission. Without this permission
// an attempt to call this method will throw a SecurityException:
[DllImport("msvcrt.dll")]
public static extern int puts(string str);
[DllImport("msvcrt.dll")]
internal static extern int _flushall();
}
class MainClass
{
private static void CallUnmanagedCodeWithoutPermission()
{
116
// Create a security permission object to describe the
// UnmanagedCode permission:
SecurityPermission perm =
new SecurityPermission(SecurityPermissionFlag.UnmanagedCode);
try
{
Console.WriteLine("Attempting to call unmanaged code without permission.");
NativeMethods.puts("Hello World!");
NativeMethods._flushall();
Console.WriteLine("Called unmanaged code without permission. Whoops!");
}
catch (SecurityException)
{
Console.WriteLine("Caught Security Exception attempting to call unmanaged code.");
}
}
117
// calls Assert for unmanaged code, you still cannot call native
// code. Because you use Deny here, the permission gets
// overwritten.
perm.Assert();
try
{
Console.WriteLine("Attempting to call unmanaged code with permission.");
NativeMethods.puts("Hello World!");
NativeMethods._flushall();
Console.WriteLine("Called unmanaged code with permission.");
}
catch (SecurityException)
{
Console.WriteLine("Caught Security Exception attempting to call unmanaged code.
Whoops!");
}
}
Output
118
Caught Security Exception attempting to call unmanaged code.
Attempting to call unmanaged code with permission.
Hello World!
Called unmanaged code with permission.
This is the same example using attributes for the security permissions.
// DeclarativeSecurity.cs
using System;
using System.Security;
using System.Security.Permissions;
using System.Runtime.InteropServices;
class NativeMethods
{
// This is a call to unmanaged code. Executing this method requires
// the UnmanagedCode security permission. Without this permission,
// an attempt to call this method will throw a SecurityException:
[DllImport("msvcrt.dll")]
public static extern int puts(string str);
[DllImport("msvcrt.dll")]
internal static extern int _flushall();
}
class MainClass
{
// The security permission attached to this method will deny the
// UnmanagedCode permission from the current set of permissions for
// the duration of the call to this method:
// Even though the CallUnmanagedCodeWithoutPermission method is
// called from a stack frame that already calls
// Assert for unmanaged code, you still cannot call native code.
// Because this function is attached with the Deny permission for
// unmanaged code, the permission gets overwritten.
[SecurityPermission(SecurityAction.Deny, Flags =
SecurityPermissionFlag.UnmanagedCode)]
private static void CallUnmanagedCodeWithoutPermission()
{
try
119
{
Console.WriteLine("Attempting to call unmanaged code without permission.");
NativeMethods.puts("Hello World!");
NativeMethods._flushall();
Console.WriteLine("Called unmanaged code without permission. Whoops!");
}
catch (SecurityException)
{
Console.WriteLine("Caught Security Exception attempting to call unmanaged code.");
}
}
120
{
SecurityPermission perm = new
SecurityPermission(SecurityPermissionFlag.UnmanagedCode);
Output
Normally the common language runtime verifies that the caller of an unmanaged method has
unmanaged code access permission at run time for every call to the unmanaged method. This
can be very expensive for applications that make many calls to unmanaged code. The
SuppressUnmanagedCodeSecurityAttribute changes this default behavior. When an
unmanaged method is declared with this attribute, the security demand is checked when code
that calls the method is loaded into the common language runtime.
121
needs to verify that he/she is using the unmanaged API safely and that callers cannot influence or
abuse the call. Alternatively, the developer can add an appropriate demand to ensure that all
callers have the appropriate permissions. For example, if a call was made into native code to
access a file (to take advantage of structured storage such as extended file properties, and so
forth) and the unmanaged code demand was suppressed, then a file IO demand should be made
explicitly to ensure that the code cannot be misused.
In this example, the check for the unmanaged code permission is executed once at load time,
rather than upon every call to the unmanaged method. If the unmanaged method is called
multiple times, this could yield a significant performance gain.
// SuppressSecurity.cs
using System;
using System.Security;
using System.Security.Permissions;
using System.Runtime.InteropServices;
class NativeMethods
{
// This is a call to unmanaged code. Executing this method requires
// the UnmanagedCode security permission. Without this permission,
// an attempt to call this method will throw a SecurityException:
/* NOTE: The SuppressUnmanagedCodeSecurityAttribute disables the
check for the UnmanagedCode permission at runtime. Be Careful! */
[SuppressUnmanagedCodeSecurityAttribute()]
[DllImport("msvcrt.dll")]
internal static extern int puts(string str);
[SuppressUnmanagedCodeSecurityAttribute()]
[DllImport("msvcrt.dll")]
internal static extern int _flushall();
}
class MainClass
{
// The security permission attached to this method will remove the
// UnmanagedCode permission from the current set of permissions for
// the duration of the call to this method.
// Even though the CallUnmanagedCodeWithoutPermission method is
// called from a stack frame that already calls
122
// Assert for unmanaged code, you still cannot call native code.
// Because this method is attached with the Deny permission for
// unmanaged code, the permission gets overwritten. However, because
// you are using SuppressUnmanagedCodeSecurityAttribute here, you can
// still call the unmanaged methods successfully.
// The code should use other security checks to ensure that you don't
// incur a security hole.
[SecurityPermission(SecurityAction.Deny, Flags =
SecurityPermissionFlag.UnmanagedCode)]
private static void CallUnmanagedCodeWithoutPermission()
{
try
{
// The UnmanagedCode security check is disbled on the call
// below. However, the unmanaged call only displays UI. The
// security will be ensured by only allowing the unmanaged
// call if there is a UI permission.
UIPermission uiPermission =
new UIPermission(PermissionState.Unrestricted);
uiPermission.Demand();
123
// permission for unmanaged code, the permission gets overwritten.
// Because you are using SuppressUnmanagedCodeSecurityAttribute here,
// you can call the unmanaged methods successfully.
// The SuppressUnmanagedCodeSecurityAttribute will let you succeed,
// even if you don't have a permission.
[SecurityPermission(SecurityAction.Assert, Flags =
SecurityPermissionFlag.UnmanagedCode)]
private static void CallUnmanagedCodeWithPermission()
{
try
{
Console.WriteLine("Attempting to call unmanaged code with permission.");
NativeMethods.puts("Hello World!");
NativeMethods._flushall();
Console.WriteLine("Called unmanaged code with permission.");
}
catch (SecurityException)
{
Console.WriteLine("Caught Security Exception attempting to call unmanaged code.
Whoops!");
}
}
124
// permission in this stack frame. Because you are using
// SuppressUnmanagedCodeSecurityAttribute, you can call the
// unmanaged methods successfully.
// The SuppressUnmanagedCodeSecurityAttribute will let you succeed,
// even if you don't have a permission.
perm.Deny();
CallUnmanagedCodeWithPermission();
}
}
Output
Code Discussion
Note that the above example allows both unmanaged calls to succeed even though the first call
doesn't have UnmanagedCode permission. When using the
SuppressUnmanagedCodeSecurityAttribute, you should use other security checks to ensure
that you don't incur a security hole. In the above example, this is done by adding the Demand for
the UIPermission:
uiPermission.Demand();
before the unmanaged call, which ensures that the caller has permission to display UI.
Threading Tutorial
The advantage of threading is the ability to create applications that use more than one thread of
execution. For example, a process can have a user interface thread that manages interactions
with the user and worker threads that perform other tasks while the user interface thread waits for
user input.
• Synchronization of threads
125
• Interaction between threads
This example demonstrates how to create and start a thread, and shows the interaction between
two threads running simultaneously within the same process. Note that you don't have to stop or
free the thread. This is done automatically by the .NET Framework common language runtime.
The program begins by creating an object of type Alpha (oAlpha) and a thread (oThread) that
references the Beta method of the Alpha class. The thread is then started. The IsAlive property of
the thread allows the program to wait until the thread is initialized (created, allocated, and so on).
The main thread is accessed through Thread, and the Sleep method tells the thread to give up its
time slice and stop executing for a certain amount of milliseconds. The oThread is then stopped
and joined. Joining a thread makes the main thread wait for it to die or for a specified time to
expire (for more details, see Thread.Join Method). Finally, the program attempts to restart
oThread, but fails because a thread cannot be restarted after it is stopped (aborted). For
information on temporary cessation of execution, see Suspending Thread Execution.
// StopJoin.cs
using System;
using System.Threading;
126
}
}
};
Console.WriteLine();
Console.WriteLine("Alpha.Beta has finished");
try
{
Console.WriteLine("Try to restart the Alpha.Beta thread");
127
oThread.Start();
}
catch (ThreadStateException)
{
Console.Write("ThreadStateException trying to restart Alpha.Beta. ");
Console.WriteLine("Expected since aborted threads cannot be restarted.");
}
return 0;
}
}
Example Output
The following example shows how synchronization can be accomplished using the C# lock
keyword and the Pulse method of the Monitor object. The Pulse method notifies a thread in the
waiting queue of a change in the object's state (for more details on pulses, see the Monitor.Pulse
Method).
The example creates a Cell object that has two methods: ReadFromCell and WriteToCell. Two
other objects are created from classes CellProd and CellCons; these objects both have a method
ThreadRun whose job is to call ReadFromCell and WriteToCell. Synchronization is accomplished
by waiting for "pulses" from the Monitor object, which come in order. That is, first an item is
produced (the consumer at this point is waiting for a pulse), then a pulse occurs, then the
consumer consumes the production (while the producer is waiting for a pulse), and so on.
// MonitorSample.cs
// This example shows use of the following methods of the C# lock keyword
// and the Monitor class
// in threads:
128
// Monitor.Pulse(Object)
// Monitor.Wait(Object)
using System;
using System.Threading;
try
{
producer.Start( );
consumer.Start( );
129
// was interrupted during a Wait
result = 1; // Result says there was an error
}
// Even though Main returns void, this provides a return code to
// the parent process.
Environment.ExitCode = result;
}
}
130
// Consume the result by placing it in valReturned.
valReturned=cell.ReadFromCell( );
}
}
131
lock(this) // Enter synchronization block
{
if (readerFlag)
{ // Wait until Cell.ReadFromCell is done consuming.
try
{
Monitor.Wait(this); // Wait for the Monitor.Pulse in
// ReadFromCell
}
catch (SynchronizationLockException e)
{
Console.WriteLine(e);
}
catch (ThreadInterruptedException e)
{
Console.WriteLine(e);
}
}
cellContents = n;
Console.WriteLine("Produce: {0}",cellContents);
readerFlag = true; // Reset the state flag to say producing
// is done
Monitor.Pulse(this); // Pulse tells Cell.ReadFromCell that
// Cell.WriteToCell is done.
} // Exit synchronization block
}
}
Example Output
Produce: 1
Consume: 1
Produce: 2
Consume: 2
Produce: 3
Consume: 3
...
...
Produce: 20
Consume: 20
132
Example 3: Using a thread pool
The following example shows how to use a thread pool. It first creates a ManualResetEvent
object, which enables the program to know when the thread pool has finished running all of the
work items. Next, it attempts to add one thread to the thread pool. If this succeeds, it adds the rest
(four in this example). The thread pool will then put work items into available threads. The
WaitOne method on eventX is called, which causes the rest of the program to wait until the event
is triggered (with the eventX.Set method). Finally, the program prints out the load (the thread that
actually executes a particular work item) on the threads.
// SimplePool.cs
// Simple thread pool example
using System;
using System.Collections;
using System.Threading;
// Useful way to store info that can be passed as a state on a work item
public class SomeState
{
public int Cookie;
public SomeState(int iCookie)
{
Cookie = iCookie;
}
}
// Beta is the method that will be called when the work item is
// serviced on the thread pool.
// That means this method will be called when the thread pool has
133
// an available thread for the work item.
public void Beta(Object state)
{
// Write out the hashcode and cookie for the current thread
Console.WriteLine(" {0} {1} :", Thread.CurrentThread.GetHashCode(),
((SomeState)state).Cookie);
// The lock keyword allows thread-safe modification
// of variables accessible across multiple threads.
Console.WriteLine(
"HashCount.Count=={0}, Thread.CurrentThread.GetHashCode()=={1}",
HashCount.Count,
Thread.CurrentThread.GetHashCode());
lock (HashCount)
{
if (!HashCount.ContainsKey(Thread.CurrentThread.GetHashCode()))
HashCount.Add (Thread.CurrentThread.GetHashCode(), 0);
HashCount[Thread.CurrentThread.GetHashCode()] =
((int)HashCount[Thread.CurrentThread.GetHashCode()])+1;
}
134
public static int Main(string[] args)
{
Console.WriteLine("Thread Pool Sample:");
bool W2K = false;
int MaxCount = 10; // Allow a total of 10 threads in the pool
// Mark the event as unsignaled.
ManualResetEvent eventX = new ManualResetEvent(false);
Console.WriteLine("Queuing {0} items to Thread Pool", MaxCount);
Alpha oAlpha = new Alpha(MaxCount); // Create the work items.
// Make sure the work items have a reference to the signaling event.
oAlpha.eventX = eventX;
Console.WriteLine("Queue to Thread Pool 0");
try
{
// Queue the work items, which has the added effect of checking
// which OS is running.
ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Beta),
new SomeState(0));
W2K = true;
}
catch (NotSupportedException)
{
Console.WriteLine("These API's may fail when called on a non-Windows 2000 system.");
W2K = false;
}
if (W2K) // If running on an OS which supports the ThreadPool methods.
{
for (int iItem=1;iItem < MaxCount;iItem++)
{
// Queue the work items:
Console.WriteLine("Queue to Thread Pool {0}", iItem);
ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Beta),new
SomeState(iItem));
}
Console.WriteLine("Waiting for Thread Pool to drain");
// The call to exventX.WaitOne sets the event to wait until
// eventX.Set() occurs.
// (See oAlpha.Beta).
// Wait until event is fired, meaning eventX.Set() was called:
eventX.WaitOne(Timeout.Infinite,true);
135
// The WaitOne won't return until the event has been signaled.
Console.WriteLine("Thread Pool has been drained (Event fired)");
Console.WriteLine();
Console.WriteLine("Load across threads");
foreach(object o in oAlpha.HashCount.Keys)
Console.WriteLine("{0} {1}", o, oAlpha.HashCount[o]);
}
return 0;
}
}
Example Output
Note The following output will vary from one computer to another.
Thread Pool Sample:
Queuing 10 items to Thread Pool
Queue to Thread Pool 0
Queue to Thread Pool 1
...
...
Queue to Thread Pool 9
Waiting for Thread Pool to drain
98 0 :
HashCount.Count==0, Thread.CurrentThread.GetHashCode()==98
100 1 :
HashCount.Count==1, Thread.CurrentThread.GetHashCode()==100
98 2 :
...
...
Setting eventX
Thread Pool has been drained (Event fired)
136
You can use a mutex object to protect a shared resource from simultaneous access by multiple
threads or processes. The state of a mutex object is either set to signaled, when it is not owned
by any thread, or nonsignaled, when it is owned. Only one thread at a time can own a mutex
object. For example, to prevent two threads from writing to shared memory at the same time,
each thread waits for ownership of a mutex object before executing the code that accesses the
memory. After writing to the shared memory, the thread releases the mutex object.
This example demonstrates how to use the classes Mutex, AutoResetEvent, and WaitHandle in
processing threads. It also demonstrates the methods used in processing the mutex object.
// Mutex.cs
// Mutex object example
using System;
using System.Threading;
137
Thread t1 = new Thread(new ThreadStart(tm.t1Start));
Thread t2 = new Thread(new ThreadStart(tm.t2Start));
Thread t3 = new Thread(new ThreadStart(tm.t3Start));
Thread t4 = new Thread(new ThreadStart(tm.t4Start));
t1.Start( ); // Does Mutex.WaitAll(Mutex[] of gM1 and gM2)
t2.Start( ); // Does Mutex.WaitOne(Mutex gM1)
t3.Start( ); // Does Mutex.WaitAny(Mutex[] of gM1 and gM2)
t4.Start( ); // Does Mutex.WaitOne(Mutex gM2)
Thread.Sleep(2000);
Console.WriteLine(" - Main releases gM1");
gM1.ReleaseMutex( ); // t2 and t3 will end and signal
Thread.Sleep(1000);
Console.WriteLine(" - Main releases gM2");
gM2.ReleaseMutex( ); // t1 and t4 will end and signal
// Waiting until all four threads signal that they are done.
WaitHandle.WaitAll(evs);
Console.WriteLine("... Mutex Sample");
}
138
}
Sample Output
139
Unsafe Code Tutorial
This tutorial demonstrates how to use unsafe code (code using pointers) in C#.
The use of pointers is rarely required in C#, but there are some situations that require them. As
examples, using an unsafe context to allow pointers is warranted by the following cases:
• Advanced COM or Platform Invoke scenarios that involve structures with pointers in them
• Performance-critical code
The use of unsafe context in other situations is discouraged. Specifically, an unsafe context
should not be used to attempt to write C code in C#.
Caution Code written using an unsafe context cannot be verified to be safe, so it will be
executed only when the code is fully trusted. In other words, unsafe code cannot be executed in
an untrusted environment. For example, you cannot run unsafe code directly from the Internet.
This tutorial includes the following examples:
• Example 3 Shows how to print the Win32 version of the executable file.
Example 1
The following example uses pointers to copy an array of bytes from src to dst. Compile the
example with the /unsafe option.
// fastcopy.cs
// compile with: /unsafe
using System;
class Test
{
// The unsafe keyword allows pointers to be used within
// the following method:
static unsafe void Copy(byte[] src, int srcIndex,
byte[] dst, int dstIndex, int count)
{
if (src == null || srcIndex < 0 ||
140
dst == null || dstIndex < 0 || count < 0)
{
throw new ArgumentException();
}
int srcLen = src.Length;
int dstLen = dst.Length;
if (srcLen - srcIndex < count ||
dstLen - dstIndex < count)
{
throw new ArgumentException();
}
141
static void Main(string[] args)
{
byte[] a = new byte[100];
byte[] b = new byte[100];
for(int i=0; i<100; ++i)
a[i] = (byte)i;
Copy(a, 0, b, 0, 100);
Console.WriteLine("The first 10 elements are:");
for(int i=0; i<10; ++i)
Console.Write(b[i] + " ");
Console.WriteLine("\n");
}
}
Example Output
Code Discussion
• Notice the use of the unsafe keyword, which allows pointers to be used within the Copy
method.
• The fixed statement is used to declare pointers to the source and destination arrays. It
pins the location of the src and dst objects in memory so that they will not be moved by
garbage collection. The objects will be unpinned when the fixed block completes
• Unsafe code increases the performance by getting rid of array bounds checks.
Example 2
This example shows how to call the Windows ReadFile function from the Platform SDK, which
requires the use of an unsafe context because the read buffer requires a pointer as a parameter.
// readfile.cs
// compile with: /unsafe
// arguments: readfile.cs
142
using System.Runtime.InteropServices;
using System.Text;
class FileReader
{
const uint GENERIC_READ = 0x80000000;
const uint OPEN_EXISTING = 3;
IntPtr handle;
[DllImport("kernel32", SetLastError=true)]
static extern unsafe IntPtr CreateFile(
string FileName, // file name
uint DesiredAccess, // access mode
uint ShareMode, // share mode
uint SecurityAttributes, // Security Attributes
uint CreationDisposition, // how to create
uint FlagsAndAttributes, // file attributes
int hTemplateFile // handle to template file
);
[DllImport("kernel32", SetLastError=true)]
static extern unsafe bool ReadFile(
IntPtr hFile, // handle to file
void* pBuffer, // data buffer
int NumberOfBytesToRead, // number of bytes to read
int* pNumberOfBytesRead, // number of bytes read
int Overlapped // overlapped buffer
);
[DllImport("kernel32", SetLastError=true)]
static extern unsafe bool CloseHandle(
IntPtr hObject // handle to object
);
143
0,
0,
OPEN_EXISTING,
0,
0);
if (handle != IntPtr.Zero)
return true;
else
return false;
}
class Test
{
public static int Main(string[] args)
{
if (args.Length != 1)
{
Console.WriteLine("Usage : ReadFile <FileName>");
return 1;
}
144
if (! System.IO.File.Exists(args[0]))
{
Console.WriteLine("File " + args[0] + " not found.");
return 1;
}
if (fr.Open(args[0]))
{
int bytesRead;
do
{
bytesRead = fr.Read(buffer, 0, buffer.Length);
string content = Encoding.GetString(buffer,0,bytesRead);
Console.Write("{0}", content);
}
while ( bytesRead > 0);
fr.Close();
return 0;
}
else
{
Console.WriteLine("Failed to open requested file");
return 1;
}
}
}
Example Input
The following input from readfile.txt produces the output shown in Example Output when you
compile and run this sample.
line 1
145
line 2
Example Output
line 1
line 2
Code Discussion
The byte array passed into the Read function is a managed type. This means that the common
language runtime garbage collector could relocate the memory used by the array at will. The
fixed statement allows you to both get a pointer to the memory used by the byte array and to
mark the instance so that the garbage collector won't move it.
At the end of the fixed block, the instance will be marked so that it can be moved. This capability
is known as declarative pinning. The nice part about pinning is that there is very little overhead
unless a garbage collection occurs in the fixed block, which is an unlikely occurrence.
Example 3
This example reads and displays the Win32 version number of the executable file, which is the
same as the assembly version number in this example. The executable file, in this example, is
printversion.exe. The example uses the Platform SDK functions VerQueryValue,
GetFileVersionInfoSize, and GetFileVersionInfo to retrieve specified version information from the
specified version-information resource.
This example uses pointers because it simplifies the use of methods whose signatures use
pointers to pointers, which are common in the Win32 APIs.
// printversion.cs
// compile with: /unsafe
using System;
using System.Reflection;
using System.Runtime.InteropServices;
146
int handle, int size, byte[] infoBuffer);
[DllImport("version.dll")]
public static extern int GetFileVersionInfoSize (string sFileName,
out int handle);
public class C
{
// Main is marked with 'unsafe' because it uses pointers:
unsafe public static int Main ()
{
try
{
int handle = 0;
// Figure out how much version info there is:
int size =
Win32Imports.GetFileVersionInfoSize("printversion.exe",
out handle);
147
short *subBlock = null;
uint len = 0;
// Get the locale info from the version info:
if (!Win32Imports.VerQueryValue (buffer, @"\VarFileInfo\Translation", out subBlock,
out len))
{
Console.WriteLine("Failed to query version information.");
return 1;
}
return 0;
}
}
Example Output
ProductVersion == 4.3.2.1
OLE DB Tutorial
148
OLE DB is a COM-based application programming interface (API) for accessing data. OLE DB
supports accessing data stored in any format (databases, spreadsheets, text files, and so on) for
which an OLE DB provider is available. Each OLE DB provider exposes data from a particular
type of data source (for example SQL Server databases, Microsoft Access databases, or
Microsoft Excel spreadsheets).
This tutorial demonstrates how to use a Microsoft Access database from C#. It shows how you
can create a dataset and add tables to it from a database. The BugTypes.mdb database used in
the sample program is a Microsoft Access 2000 .MDB file.
Example
This program accesses the BugTypes.mdb database, creates a dataset, adds the tables to it, and
displays the number of tables, columns, and rows. It also displays the titles of each row.
// OleDbSample.cs
using System;
using System.Data;
using System.Data.OleDb;
using System.Xml.Serialization;
149
{
myAccessConn = new OleDbConnection(strAccessConn);
}
catch(Exception ex)
{
Console.WriteLine("Error: Failed to create a database connection. \n{0}",
ex.Message);
return;
}
try
{
myAccessConn.Open();
myDataAdapter.Fill(myDataSet,"Categories");
}
catch (Exception ex)
{
Console.WriteLine("Error: Failed to retrieve the required data from the
DataBase.\n{0}", ex.Message);
return;
}
finally
{
myAccessConn.Close();
}
150
// The next two lines show two different ways you can get the
// count of tables in a dataset:
Console.WriteLine("{0} tables in data set", myDataSet.Tables.Count);
Console.WriteLine("{0} tables in data set", dta.Count);
// The next several lines show how to get information on
// a specific table by name from the dataset:
Console.WriteLine("{0} rows in Categories table",
myDataSet.Tables["Categories"].Rows.Count);
// The column info is automatically fetched from the database,
// so we can read it here:
Console.WriteLine("{0} columns in Categories table",
myDataSet.Tables["Categories"].Columns.Count);
DataColumnCollection drc = myDataSet.Tables["Categories"].Columns;
int i = 0;
foreach (DataColumn dc in drc)
{
// Print the column subscript, then the column's name
// and its data type:
Console.WriteLine("Column name[{0}] is {1}, of type {2}",i++ , dc.ColumnName,
dc.DataType);
}
DataRowCollection dra = myDataSet.Tables["Categories"].Rows;
foreach (DataRow dr in dra)
{
// Print the CategoryID as a subscript, then the CategoryName:
Console.WriteLine("CategoryName[{0}] is {1}", dr[0], dr[1]);
}
}
}
Output
The Categories table of the BugTypes.mdb database contains the following information.
151
When you run the sample, the following output will be displayed on the screen:
152