Learn-C-Sharp-Course-Book
Learn-C-Sharp-Course-Book
INTRODUCTION
This is the course book to accompany “C# Programming (in ten easy steps)”, a pro-
gramming course available on Udemy. It is aimed at newcomers to C# programming
and it is suitable for people who have never programmed before or who may need to
revise the basics of programming before going on to more complex programming
topics.
The core lessons in this course are the online videos. This eBook is a second-
ary resource which summarizes and expands upon the topics in the videos. You
should also be sure to download the source code archive which contains all the
sample programs described in the videos and in this eBook.
When the text refers to code in a sample program, the name of the Visual
Studio ‘solution’ (the file that contains the program) will be shown like this:
ASampleProgram.sln
To follow along with the tutorial, you can load the named solution into your
copy of Visual Studio.
1
STEP ONE
GETTING READY
http://www.microsoft.com/express
There are many ‘Express’ products available. Be sure that you download the
latest version of ‘Visual C# Express’. Once you have a version of Visual Studio
installed you are all ready to get started.
If this is the first time you’ve used C#, follow these instructions to create your first
program.
Start Visual Studio.
Select File, New, Project
In the left-hand pane, make sure Visual C# is selected.
In the right-hand pane, select Windows Forms application
In the Name field at the bottom of the dialog, enter a name for the project. I
suggest:
HelloWorld
Click the Browse button to find a location (a directory on disk) in which to
save the project.
2
You may want to store all your projects beneath a specific directory or
‘folder’. Or you may click ‘New Folder’ at the top of the dialog to create a
new directory. For example, you might create a directory on your C: drive
called \CSharpProjects. Once you have located a suitable directory, click
the ‘Select Folder’ button.
In the New Project dialog, verify that the Name and Location are correct then
click OK.
Visual Studio will now create a new Project called Hello World and this will be
shown in the Solution Explorer panel.
You will now see a blank form in the centre of Visual Studio. This is where
you will design the user interface. Make sure the Toolbox (containing ready-to-use
‘controls’ such as Button and Checkbox) is visible. Normally the Toolbox is shown at
the left of the screen. If you can’t see it, click the View menu then Toolbox.
In the Toolbox, click Button. Hold down the left mouse button to drag it onto
the blank form. Release the mouse button to drop it onto the form. The form should
now contain a button labelled ‘button1’.
In a similar way, drag and drop a TextBox from the Toolbox onto the form.
On the form, double-click the button1 Button.
This will automatically create this block of C# code:
Don’t worry what this all means for the time being. Just make sure your
cursor is placed between the two curly brackets and type in this code:
Make sure you type the code exactly as shown. In particular, make sure that
the case of the letters is correct. C# is a ‘case-sensitive’ programming language so it
considers an uppercase letter such as ‘T’ to be different from a lowercase letter such
as ‘t’. So if you entered TextBox1.Text you won’t be able to run the program because
3
the name of the TextBox control, textBox1, begins with a lowercase ‘t’ and C# will fail
to find a control called TextBox1 with an uppercase ‘T’.
The code above tells C# to display the words “Hello World” as the text inside
textBox1. The complete code of this code block should now look like this:
This code block is called a ‘function’ or ‘method’ and its name is button1_click.
It will be run when the button1 control is clicked.
Let’s try it out….
Press CTRL+F5 to run the application (you can also run it by selecting the
Debug menu then, ’Start Without Debugging’).
The form you designed will pop up in its own window.
Click button1.
“Hello World” should now appear as the text inside textBox1.
Click the close-button in the right of the caption bar to close the program and
return to Visual Studio.
4
VISUAL STUDIO OVERVIEW
5
STEP TWO
LANGUAGE ELEMENTS
DataTypes.sln
When your programs do calculations or display some text, they use data. The data
items each have a data type. For example, to do calculations you may use integer
numbers such as 10 or floating point number such as 10.5. In a program you can
assign values to named variables. Each variable must be declared with the appro-
priate data type. This is how to declare a floating-point variable named mydouble
with the double data-type:
double mydouble;
mydouble = 100.75;
Alternatively, you can assign a value at the same time you declare the
variable:
If you want to make sure that a value cannot be changed, you should declare
a constant using the keyword const, like this:
mydouble = 500.75;
taxrate = 0.4;
6
Common data types include:
constants Comments
variables
7
NAMESPACES AND USINGS
At the top of a code file such as Form1.cs in the DataTypes.sln solution you will see a
number of lines beginning with the keyword using. The using directive gives C#
code access to classes and types inside another code module or ‘namespace’. Here,
for example, we are accessing the System namespace:
using System;
using System.Windows.Forms;
When you design a user interface using Visual Studio’s form designer, the
necessary using statements are added automatically.
8
TYPE CONVERSION (NUMBER/STRING)
Many classes and structures (or structs) in the .NET framework include built-in
conversion routines. For example, number structs such as Double and Int16 include a
method called Parse() which can convert a string into a number.
TaxCalc.sln
Example. This converts the string “15.8” to the Double 15.8 and assigns the
result to the variable, d:
This converts the string “15” to the Int16 value, 15, and assigns the result to
the variable, i:
But beware. If the string cannot be converted, an error occurs! This will cause
an error:
grandTotalTB.Text = grandtotal.ToString( );
Note that .NET also has a class called Convert which can be used to convert
between a broad range of different data types. For example, this converts, the Text (a
string) in the subtotalTB text box to a double:
And this converts the value of the subtotal variable (a double) to a string and
displays it in the text box, subtotalTB:
9
This is the finished version of the function that computes and displays the
values when the button is clicked:
10
STEP THREE
OPERATORS
Operators are special symbols that are used to do specific operations such as the
addition and multiplication of numbers or the concatenation (adding together) of
strings. One of the most important operators is the assignment operator, =, which
assigns the value on its right to a variable on its left. Note that the type of data
assigned must be compatible with the type of the variable.
Beware. While one equals sign = is used to assign a value, two equals signs,
==, are used to test a condition. This is a simple test:
11
TESTS AND COMPARISONS
C# can perform tests using the if statement. The test itself must be contained within
round brackets and should be capable of evaluating to true or false. If true, the
statement following the test executes. A single-expression statement is terminated by
a semicolon. A multi-line statement may be enclosed within curly brackets. Option-
ally, an else clause may follow the if clause and this will execute if the test evaluates
to false. You may also ‘chain together’ multiple if..else if sections so that when one
test fails the next condition following else if will be tested. Here is an example:
Tests.sln
Note that test conditions must be placed between parentheses. You may use
other operators to perform other tests. For example, this code tests if the value of i is
less than or equal to the value of j. If it is, then the conditional evaluates to true and
“Test is true” is displayed. Otherwise the condition evaluates to false and “Test is
false is displayed:
int i = 100;
int j = 200;
if( i <= j ) {
output.Text = "Test is true";
} else {
output.Text = "Test is false";
}
These are the most common comparison operators that you will use in
tests:
== // equals
!= // not equals
> // greater than
< // less than
<= // less than or equal to
>= // greater than or equal to
12
If you need to perform any tests, it is often quicker to write them as ‘case
statements’ instead of multiple if..else if tests. In C# a case statement begins with the
keyword switch followed by a test value. Then you put a value after one or more
case tests. The case value is compared with the test value and, if a match is made, the
code following the case value and a colon (e.g. case "hi":) executes. When you want
to exit the case block you need to use the keyword break. If case tests are not
followed by break, sequential case tests will be done one after the other until break
is encountered. You may specify a default which will execute if no match is made by
any of the case tests. Here’s an example:
switch( userinput.Text ) {
case "":
output.Text = "You didn't enter anything";
break;
case "hello":
case "hi":
output.Text = "Hello to you too!";
break;
default:
output.Text = "I don't understand that!";
break;
}
13
COMPOUND ASSIGNMENT OPERATORS
Some assignment operators in C#, and other C-like languages, perform a calculation
prior to assigning the result to a variable. This table shows some examples of comm-
on 'compound assignment operators' along with the non-compound equivalent.
Operators.sln
It is up to you which syntax you prefer to use in your own code. If you are
familiar with other C-like languages, you will probably already have a preference.
Many C/C++ programmers prefer the terser form as in a += b. Basic and Pascal
programmers may feel more comfortable with the slightly longer form as in a = a + b.
14
STEP FOUR
FUNCTIONS OR METHODS
Functions provide ways of dividing your code into named ‘chunks’. A function is
declared using a keyword such as private or public (which controls the degree of
visibility of the function to the rest of the code in the program) followed by the data
type of any value that’s returned by the function or void if nothing is returned. Then
comes the name of the function, which may be chosen by the programmer. Then
comes a pair of parentheses.
The parentheses may contain one or more ‘arguments’ or ‘parameters’
separated by commas. The argument names are chosen by the programmer and each
argument must be preceded by its data type. When a method returns some value to
the code that called it, that return value is indicated by preceding it with the return
keyword.
Methods.sln
15
Note: In other languages, functions that return nothing may be called
‘subroutines’ or ‘procedures’. In Object Oriented terminology, a function
is generally called a ‘method’ and, in this course, the terms ‘function’ and
‘method’ will regarded as synonymous.
To execute the code in a method, your code must ‘call’ it by name. In C#, to
call a method with no arguments, you must enter the method name followed by an
empty pair of parentheses like this:
sayHello( );
To call a method with arguments, you must enter the method name followed
by the correct number and data-type of values or variables, like this:
If a method returns a value, that value may be assigned (in the calling code) to
a variable of the matching data type. Here, the addNumbers() method returns a
string and this is assigned to the calcResult string variable:
string calcResult;
calcResult = addNumbers( 100, 200 );
By default, when you pass variables to methods these are passed as ‘copies’. That is,
their values are passed but any changes made within the method affect only the
copies that have been passed to the method. The original variable (outside the
method) retains its original value.
Sometimes, however, you may want any changes made within a method to
affect the original variables in the code that called the method. If you want to do that
you can pass the variables ‘by reference’. When variables are passed by reference,
the original variables themselves (or, to be more accurate, the references to the
location of those variables in your computer’s memory) are passed to the function.
So any changes made to the arguments inside the method will also change the
variables that were used when the method was called. To pass by reference, both the
arguments defined by the method and the variables passed when the method is
called must be preceded by the keyword ref.
16
These examples should clarify the difference between ‘by value’ and a ‘by
reference’ arguments:
You may also use out parameters which must be preceded by the out
keyword instead of the ref keyword. Out parameters are similar to ref parameters.
However, it is not obligatory to assign a value to an out variable before you pass it to
a method (it is obligatory to assign a value to a ref variable). However, it is obligatory
to assign a value to an out argument within the method that declares that argument
(this is not obligatory with a ref argument).
While you need to recognise ref and out arguments when you see them, you
may not have any compelling reason to use them in your own code. As a general
rule, I suggest that you normally use the default ‘by value’ arguments for C#.
17
STEP FIVE
OBJECT ORIENTATION
MyOb.sln
class MyClass {
private string _s;
public MyClass( ) {
_s = "Hello world";
}
Before I can use the object, I need to create it. I do that by using the new key-
word. This calls the MyClass constructor method. This returns a new object which I
here assign to the ob variable:
ob = new MyClass( );
A constructor is a special method which, in C#, has the same name as the class
itself. When you create a class with new, a new object is created and any code in the
constructor is executed. This is the MyClass constructor:
public MyClass( ) {
_s = "Hello world";
}
The code in my constructor assigns the string “Hello world” to the variable,
_s. You aren’t obliged to write any code in a constructor. However, it is quite
common – and good practice – to assign default values to an object’s variables in the
constructor. You can change the default value of the objects _s string variable using
the MyClass object’s setStr() method:
And you can retrieve the value of the _s variable using the getStr() method:
textBox1.Text = ob.getStr( );
A ‘class’ is the blueprint for an object. It defines the data an object contains
and the way it behaves. Many different objects can be created from a
single class. So you might have one Cat class but three cat objects: tiddles,
cuddles and flossy. A method is like a function or subroutine that is defined
inside the class.
19
CLASS HIERARCHIES
To create a descendent of a class, put a colon : plus the ancestor (or ‘base’) class name
after the name of the descendent class, like this:
GameClasses.sln
A descendent class inherits the features (the methods and variables) of its
ancestor so you don’t need to recode them. In this example, the Thing class has two
private variables, _name and _description (being private they cannot be accessed
from outside the class) and two public properties to access those variables. The
Treasure class is a descendent of the Thing class so it automatically has the _name
and _description variables and the Name and Description accessor properties and it
adds on the _value variable and the Value property:
20
A property is a group of one or two special types of method to get or set
the value of a variable. This is a typical definition for a property:
Here _name is private but Name is public. This means that code outside the
class is able to refer to the Name property but not to the _name variable. It is
generally better to keep variables private and provide public properties in this way.
If you omit the get part of a property, it will be impossible for code outside the class
to retrieve the current value of the associated variable. If you omit the set part it will
be impossible for code outside the class to assign a new value to the associated
variable.
Even though properties are like pairs of methods, they do not use the same
syntax as methods when code refers to them. This is how to might get or set a value
using a pair of methods named getName() and setName():
myvar = ob.getName( );
ob.setName( "A new name" );
But this is how you would get and set a value using a property such as Name:
myvar = ob.Name;
ob.Name = "A new name";
21
STEP SIX
One of the fundamental arts of programming is repetition. Whether you are devel-
oping a payroll application or a shoot-em-up game, you will need to deal with num-
erous objects of the same fundamental type - 100 employees' salaries, perhaps. Or
500 Bugblatter Beasts of Traal. We’ll look here at just a few of the ways in which
multiple objects can be arranged and manipulated using C#.
ARRAYS
An array is a sequential list. You can think of it as a set of slots in which a fixed
number of items can be stored. The items should all be of the same type. As with
everything else in C#, an array is an object. Strictly speaking, a C# array is an
instance of the .NET class, System.Array and it comes with a built-in set of methods
and properties. Just like other objects, an array has to be created before it can be
used. To see a simple example of this, load up the Arrays.sln project and find the
button1_Click() method. The first line here creates and initialises an array of three
strings:
Arrays.sln
The definition of an array begins with the type of the objects it will hold,
followed by a pair of square braces. This array contains strings, so it begins string[].
Next comes the name of the array, myArray. The new keyword is responsible for
creating the array object. I specify that this array is capable of storing three strings by
appending string[3]. This part (the number of items) is optional. If you look at the
declaration of myArray3, you will see that the number of strings has been omitted:
This is because C# can determine the actual number of strings from the items
(the three strings) which I have placed between curly brackets. Putting comma-
separated array items between curly brackets provides a quick way of initializing an
array. Notice that I have even omitted new string[] when declaring and initializing
22
myArray3. Again, this is a shorthand alternative provided by C#. As a general rule,
however, no harm is done by writing code that explicitly creates typed arrays of a
defined length. The shorthand way of creating an initializing an array is only
available when you do the process in a single line of code. This is how to declare an
array first and initialize it later:
string[] myArray2;
myArray2 = new string[3] { "four", "five", "six" };
An array can hold any type of object. The myObjectArray array, for example,
holds three objects of the class, MyClass:
LOOPS
In the code of Arrays.sln you will see several for loops. The first for loop, fills
MyObjectArray with three objects of the MyClass type:
Arrays in C# are 0-based, which means that the first item of a three-slot array is
at index 0 and the last item is at index 2. A position in an array is indicated by the
index number between square brackets.
The code between the round brackets after the word for is used to control the
execution of the loop. This code is divided into three parts. The first part initialises
an int variable, i, with the value of 1. The second section contains a test (i < 100). The
23
loop executes as long as this test remains true. Here the test states that the value of
the variable i must be less than 100. Finally, we have to be sure to increment the
value of i at each turn through the loop. That happens in the third part of the loop
control statement (i++)which adds 1 to i.
We could paraphrase the code of the loop shown above like this:
Let i equal 1. While i is less than 100 execute the code inside the loop, then add 1 to i.
Note that this loop will execute 99 times, not 100, since it will fail when the
value of i is no longer less than 100.
Now look at loop 2 in the sample code:
This loop iterates through the list of strings in myArray and displays them in
textBox1. Note the expression in second part of the for loop. Instead of using a fixed
number, such as 2, to specify final index of the array, I’ve used the array object’s
Length property. However, since Length is always 1 greater than the index (arrays
being zero-based), I have subtracted 1 from Length.
The third loop uses two of the methods that are available for use with arrays,
GetLowerBound(0) and GetUpperBound(0), to determine the start and end indices
of myArray2:
These methods return the actual indices (here 0 to 2) of the array’s first and
last elements. Using these two methods makes the code more generic since it will
work with arrays of any size. The 0 argument here indicates that I am testing the first
dimension of an array. In fact, this array only has one dimension. C# can work with
multi-dimensional arrays (arrays containing other arrays) too.
The fourth loop shows how similar techniques can be used to iterate through
an array of user-defined objects and retrieve values (here the Name property) from
each in turn:
24
FOREACH LOOPS
The fifth loop is more interesting. Instead of using for it uses foreach. The foreach
statement iterates through each element in an array without any need for an
indexing variable. A foreach loop is controlled by specifying the data type (here
MyClass), an identifier to be used within the loop, here myob, and the name of the
array or the collection:
At each turn through the loop, the next MyClass object in myObjectArray is
assigned to the variable, myob. I can then access this object’s methods or properties,
such as Name.
WHILE LOOPS
Sometimes you may want to execute some code an uncertain number of times – that
is, while some test condition remains true. To do that, use a while loop.
WordCount.sln
The WordCount project uses several while loops. These may include a single
condition as here:
25
STRINGS AND CHARS
Strings.sln
char c1 = s[0];
char c2 = s[11];
To see some working examples, try out the code in the strings.sln solution.
26
DIALOGS AND MESSAGE BOXES
This step’s projects use several different types of dialog box. You may have noticed
that the WordCount.sln project uses the MessageBox dialog. This is defined entirely in
code. The MessageBox.Show() method is passed a number of arguments to display
a string, one or more buttons and an icon. Refer to the .NET help system for a full
explanation.
Editor.sln
The Editor.sln project uses OpenFile and SaveFile dialogs which are displayed
when menu items are clicked in the application’s File menu. These dialogs are
configured by setting properties in code – such as the default file extension,
DefaultExt, and the Filter (the file extensions available from the dialog’s File Type
selector.
You can, if you wish, create Open and Save dialogs in code, like this:
But in this project, I simply dropped the two dialog controls from the
Toolbox. This has the benefit that their properties can be set using the Properties
panel. In fact, if you wish, you can delete the Filter and DefaultExt assignments from
the code and use the Properties panel to set them instead.
27
STEP SEVEN
FILES AND IO
Incidentally, the final parameter in the above code specifies that rich text
formatting is to be retained. To save it as simple ASCII text (text that does not retain
colours, font style and so on), you would need to replace RichText with PlainText.
This is all well and good if you happen to be saving data from a rich text component,
which has all this behaviour ‘built in’. But what if you happen to be saving data from
your own, non-visual objects or loading data from a non-text file?
To do this we need to learn a bit about the IO (Input/Output) features of the
.NET framework. In particular, I want to explain the concept of a ‘stream’. A stream
is a sequence of bytes (a byte is a chunk of data approximately corresponding to a
single character) that can flow from one place to another. Often it will flow from the
hard disk into memory or vice versa. But a stream could equally flow from one place
in memory to another place in memory or from one computer to another.
28
VARIATIONS ON A STREAM
There are various different of more or less ‘specialised’ types of stream in .NET. Here
I want to concentrate only on the essential features of streams which you will need to
read and write data to and from disk. Be warned: the nitty-gritty details of reading
and writing streams may be quite hard to understand at first. Here I describe the
main options but you may not need to use all of these in your own code. You may
want to try out my sample project so that you have a general overview of streaming
but you certainly don’t need to memorise all the details!
Let’s start by seeing how to use basic Stream objects to make a copy of a file.
In this project, we will be making copies of the file, Text.txt. Our code assumes that
this file can be found in the \Test directory on the C drive.
Before continuing, use the Windows Explorer to create the \Test directory
on your C drive and copy the Test.txt file (from \Step07\streams) into it. If
you fail to do this, the sample program will not work! Be sure to keep the
Windows Explorer open on the C:\Test directory when you run the
program so you can see which files are created.
Steams.sln
Now load up the Streams.sln solution. Scroll to the top of the code editor.
You will see that I have specified the source file and directory here:
Note that I have also added System.IO to the using section at the top of the
code. The System.IO namespace contains all the essential IO classes that we’ll need
such as File, StreamWriter, StreamReader and the entire hierarchy of Streams.
Switch to the form designer and double-click the top button, labelled ‘Write
Stream’. This will take you to the button’s event-handling method which is called
WriteStreamBtn_Click (). I declare the target output file name, OUTPUTFN, and I
create two stream objects:
29
Notice that I use double-slashes ‘\\’ for directory separators. That’s
because a single slash in a string is used to indicate that the letter follow-
ing it represents a special character. For example "\n" is a new line and "\t"
is a tab. When I want to put an actual ‘\’ character in a string, I need to
precede it by another ‘\’ character which is what I’ve done here.
Here, each stream is created by the File class. The OpenRead() and
OpenWrite() methods create files for reading and writing and they each return a
FileStream object. Note that these methods are ‘static’ which means that you can use
them by referring to the File class itself rather than to a specific File object. You can
think of a static method as a method that ‘belongs’ to the class itself rather than to an
individual object created from the class. This is why it is not necessary to create an
instance (an object) of the File class using new.
Now I create a ‘buffer’ into which to read data. This buffer is an array of 1024
bytes (I set the BUFFSIZE constant to 1024 at the top of the code):
A while loop continually reads bytes from the input stream, instream, into
this buffer as long as there are more bytes to be read (the reading is all done inside
the parentheses at the start of this while loop and the bytes read are assigned to the
numbytes variable until the value is 0 when there are no more bytes to be read). The
bytes that have been read (whose total number is stored in the numbytes variable)
are then written into the output stream, outstream:
int numbytes;
while ((numbytes=instream.Read(buffer,0,BUFFSIZE)) > 0)
{
outstream.Write(buffer,0,numbytes);
}
If you are interested in finding out more on the Read() and Write() methods
of Stream, refer to the .NET help (press F1 over the method names in the code
editor). When all the reading and writing is completed, the two streams are closed:
instream.Close();
outstream.Close();
The real problem with this code is that it is inefficient since it reads and writes
data one byte at a time. You may not notice this when working with a small file. But
30
it might be noticeable when working with very long files. We could make the code
more efficient by reading larger blocks of bytes in one go. We can do this by creating
a BufferedStream object. This is done in the BuffStreamWriteBtn_Click() method.
This is how we’ve created a BufferedStream buffInput from the FileStream object,
instream:
The rest of the code is much as before. The only difference is that it uses the
buffered stream objects rather than the basic stream objects. To ensure that all data is
written (‘flushed’) to disk, the BufferedStream class provides the Flush() method.
However, when the output stream is closed, any buffered data is automatically
flushed, so it is not necessary to call the Flush() method here.
The FileStreamBtn_Click() method implements an equivalent file-copying
routine to the one we’ve just looked at. It combines the efficiency of a
BufferedStream with the simplicity of an unbuffered Stream. There is, in fact, much
more to a FileStream than we have space to explore here. It comes with many
methods that can assist in file handling. Refer to the .NET help documentation for
more information.
Most of the code in this method needs no explanation. The only new feature is
the FileStream constructor:
The FileStream class has several constructors. The one I use here takes the
three arguments: the file name parameter, the file mode parameter (which is a pre-
declared constant that specifies how to open or create a file) and the file access
parameter which is a constant that determines whether the file can be read from or
written to.
The three file copying methods I’ve created up to now, operate on bytes and
can be used to copy data of any type. I’ve used them to copy text files but they could
just as well be used to copy an executable file. If the input file were wordpad.exe and
the output file were named mycopy.exe, all three routines would make a correct copy
of the wordpad program.
31
READING AND WRITING TEXT FILES
There is another way of handling text files. The StreamReader and StreamWriter
classes can read and write a text file one line at a time. This makes it pretty easy to
deal with files of plain text. A line is defined as a sequence of characters followed by
a line feed (“\n”) or a carriage return immediately followed by a line feed ("\r\n").
The string that is returned does not contain the terminating carriage return or line
feed. Unlike the classes I’ve used previously, these classes are not descendants of the
Stream class. Instead, they operate upon Stream objects.
The File class provides the methods, OpenText() and CreateText() which,
when passed a file name as an argument, will return a StreamReader or a
StreamWriter object. An example of this can be seen in the StreamWriterBtn_Click ()
method.
To read a line, use StreamReader’s ReadLine() method. To write a line use,
StreamWriter’s WriteLine(). In my code, a while loop reads through the lines of text
from the input file. The lines of text are read in by the StreamReader object, sread, as
long as there are more lines to be read (that is, until null is returned). The string
variable, aline, is initialised with the current line which is then written by the
StreamWriter object, swrite, to the output file:
StreamReader sread;
StreamWriter swrite;
String aline;
sread = File.OpenText( SOURCEFN );
swrite = File.CreateText( OUTPUTFN );
while( ( aline = sread.ReadLine( ) ) != null ) {
swrite.WriteLine( aline );
}
sread.Close( );
swrite.Close( );
In this test, I use the C# ‘not’ operator, ‘!’, so that the code above could be read
as “if not File.Exists(SOURCEFN)”.
32
APPENDING DATA TO A FILE
There may be occasions when you want to modify an existing file. There are various
ways in which this can be done using C#. If you are working with plain text files (or
other file types such as RTF, which contain ordinary ASCII ‘text’ characters), there is
a very easy way of appending data. Look at the AppendBtn_Click() method in
Streams.sln. This creates the StreamWriter object, swrite, using the File class’s
AppendText() method. This cause any text that is subsequently written to be added
to the end of the specified file if that file already exists. If the file does not exist, it is
created ready for text to be written into it:
If you are dealing with files in .NET, it is worth getting to know the File class. All the
methods of this class are static so you won’t have to create new File objects in order
to use them. In the example code I have used the CreateText() and AppendText()
and Exist() methods.
The methods of the File class need to be passed a string argument indicating
the directory path and file name. The directory separators take the form of a double
backslash “\\”. The File class has other useful capabilities. Its GetAttributes() method
retrieve a file’s attributes, Delete() deletes a file, Move() and Copy()move or copy a
file. I could have used File.Copy() to do all the copying which I have laboriously
coded in this step. Of course, if I had done that, I would never have had the oppor-
tunity to try out the Stream and StreamReader classes. You will need to use the
Stream class and its descendants when you want to process data in some way or
save data from user-defined objects. So, while the File methods are useful to know
about, they are no substitute for a full grasp of the inner life of .NET’s streams!
33
THE DIRECTORY CLASS
The Directory class can be used to create, move and enumerate through directories
and subdirectories. It provides static methods which means that (just like the
methods of the File class) you do not need to create an object from the class in order
to use its methods.
Dir.sln
Load the Dir.sln solution and locate the dirBtn_Click() method. This retrieves
the names of the drives that are active or mapped on your system. To do this it calls
Directory.GetLogicalDrives() method to initialise a string array of the drive names:
To display those names, the code iterates through the strings using a foreach
loop. Retrieving the name of the currently active directory is even simpler.
In order to find the top-level directory, you can use the GetDirectoryRoot()
method. This takes a string argument representing the path of a file or directory. My
code uses the currdir string, which I have already initialised to the current directory:
If you want to retrieve the names of any subdirectories too, use the
GetDirectories() method to return an arrays of strings. Then use a foreach loop to
iterate through them. My code iterates through all the directories immediately below
the root:
You can also use the Environment class to obtain paths to special directories
such as the System or Program Files directories. The Directory class could then be
used to work with the files or subdirectories in those directories.
For example, this code will display the subdirectories in the Program Files
folder:
34
string[] subdirs = Directory.GetDirectories( Environment.GetFolderPath(
Environment.SpecialFolder.ProgramFiles ) );
foreach( string sd in subdirs ) {
richTextBox1.AppendText( sd + "\n" );
}
Having obtained a string representing the path to a subdirectory or file, you may
want to do certain operations such as parse out the file name or extension or the path
(the full directory specification) minus the file name. The Path class has these and
other capabilities built in.
Load the Dir.sln solution and find the pathBtn_Click() method. This starts by
getting the path to the executable file of the Dir.exe program itself (the program that
is created when you compile or run this project). The Executable property of the
Application class supplies this:
Now, if you want to get a string initialised with the directory name, minus the
file name, you can just pass the path variable to the GetDirectoryName() method of
the Path class as follows:
Path.GetDirectoryName( path )
In a similar manner, you can pass the path variable to the GetExtension(),
GetFileName(), GetFileNameWithoutExtension() and GetPathRoot() methods of
the Path class. Each of these methods returns a modified version of the original
string. The names of the methods are self-explanatory and you can view the results
by running the Dir application and clicking the ‘Path Test’ button.
35
STEP EIGHT
In this step, I look at some additional features of classes and structures (structs). I
also explain Enums. The text here summarizes the essential features.
While you may put more than one class into a single code file, it is often considered
better practice to give each class its own code file. For example, if you create a class
named MyClass you can write its code in a file named MyClass.cs and avoid adding
any other classes to that file.
ClassesAndMore.sln
public MyClass( ) {
_str = "A Default String";
}
// more code here. . .
}
A class may contain both ordinary methods (which are called by referring to an
object created from the class) and static methods (which are called by referring to the
class itself. Typically, normal methods act upon an object’s internal data while static
methods may be defined to act upon ‘external’ data that’s passed to them for some
sort of processing. For example, the .NET File.Exists() method is static. You can pass
a file name to it as an argument and it tells you whether or not that file exists on
disk. In the MyClass class, I’ve defined the method ToUpCase() as static and
ToLowCase() as a normal (or ‘instance’) method. This is how I call the static method:
string s;
s = MyClass.ToUppCase( "abc" );
// s now equals "ABC"
MyClass ob1;
ob1 = new MyClass( "Hello World" );
string s;
s = ob1.ToLowCase( );
// s now equals "hello world"
If you want to create a class that contains nothing but static methods you can
make the class itself static. I’ve done this with the MyStaticClass class:
A static class does not permit objects to be created from it. The .NET frame-
work includes several static classes such as File and Directory.
37
OVERLOADED METHODS
This class also has two constructors. The first takes no arguments so it sets a
default value for _str. The second takes a string argument which is assigned to _str:
public MyClass( ) {
_str = "A Default String";
}
STRUCTS
As an alternative to a class, you may create a struct. Unlike a class, a struct cannot
have descendants. In addition, its constructor cannot have an empty argument list.
You will see an example of a struct in the file MyStruct.cs. This implements a
structure that contains an x and a y coordinate, similar to a .NET Point which its
itself a struct.
38
ENUMS
You can create a group of constants called an Enum. To do this you define a coma-
separated list of identifiers between curly braces like this:
CardSuits selectedSuit;
selectedSuit = CardSuits.Clubs;
If you want to display one of the Enum’s identifiers as a string, you can use
the ToString() method:
textBox1.Text = selectedSuit.ToString( );
You may also assign specific numeric values to elements of an Enum like this:
Enums are used in various places throughout the .NET framework. For
example, if you select a control in the Form designer, you can change its docking
behaviour by clicking the Dock property. You can anchor a control (so that its edges
are auto-resized when the control containing it is resized) using the Anchor
property. These two properties are assigned values from the DockStyle and Anchor-
Styles Enums like this:
textBox1.Dock = DockStyle.Top;
textBox1.Anchor = AnchorStyles.Top;
39
You can “add” Enums together using the single upright bar | operator. This
will not necessarily result in a sensible value for all Enums. However, some Enums
expect this sort of operation. The AnchorStyles Enum, for example, can anchor a
control at the Bottom and Left of its container like this:
40
STEP NINE
EXCEPTIONS
There is one important little problem I have not yet considered in any depth in this
course. Namely: bugs! Few programs of any ambition are totally bug-free. At least,
they aren’t at the outset. But, with a little care, and a lot of debugging, it should be
possible to squash most of the most troublesome bugs before the end-user is let loose
on your application. In some respects you could say that the programmer’s greatest
enemy is the user. The trouble with users is that they don’t play to the rules. Your
code expects them to do one thing but they go and do something else altogether.
It is your responsibility, therefore, to assume that the user will, at some time
or another, do the most stupid things conceivable. And you have to build into your
code some means of recovering from potential disaster. Fortunately, C# and .NET
make this fairly easy to do using exception-handling.
Imagine that you’ve created a calculator of some kind. Obviously, you expect
the user to do calculations using numbers. But what happens if, instead of entering
the letters 1 and 0 (one and zero), the user enters I and O (that is, the capital letters I
and O)? The user may be dumb, but that’s no excuse. Your program has to be clever
enough to cope with it.
In C# and .NET, runtime errors (errors that occur when the program is
running) are handled by exceptions. As with everything else in C#, an exception is an
object. When an error happens, an instance (and object) of the Exception class is
created. Your code can make use of this by trapping the exception object and
accessing its properties and methods. Load and run the Exceptions.sln solution. Enter
‘IO’ (letters, not numbers) into the subtotal edit box at the top of the form. Now click
the first button, labelled ‘Calc’. A system error pops up to tell you that there is an
unhandled exception. This may be acceptable when you are developing an app-
lication. But is not the kind of message an end user expects to see when running it.
Click the ‘Continue’ button.
As before, make sure the two letters ‘IO’ are in the subtotal edit box. This time
click the second button, ‘calc2Btn’. This time you will see quite a different, and
altogether more polite, error message. This is because the code that executes when
this button is clicked ‘catches’ the exception object handles the exception.
41
Take a look at the code of calc2Btn_Click() in the editor. This is the exception-
handling code:
try {
st = Convert.ToDouble( subTotBox.Text );
} catch( Exception exc ) {
MessageBox.Show( "Awfully sorry to bother you, but apparently the " +
exc.Message, "Oops! There has been an error of the type: " + exc.GetType( ),
MessageBoxButtons.OK, MessageBoxIcon.Error );
}
Here the bit of code that could potentially cause a problem is the first line
which attempts to convert the text in subTotBox to a double value. This code has
been put between the curly brackets following the try keyword. So, during
execution, C# tries to run this code and convert the data. If this attempt fails (i.e. if
the text cannot be converted to a double) then the code block following the catch
keyword is run. Note that the catch block takes an argument, exc (the name here is
not important) of the type Exception.
When a problem occurs in the try block, the variable exc is initialised with the
exception object which contains all kinds of useful information about the error that
has just occurred. For example, it contains a Message property which gives access to
a string describing the error. In the present case, Message equals “Input string was
not in a correct format”. I display this message in a MessageBox. The exception object
also has a GetType() method. This returns a string description of the exact error
type. Here this is “System.FormatException” and I display this in the caption of the
message box.
An exception object has many other properties and methods that can provide
even more detailed information on an error. Try, for example, editing the code
shown above by replacing exc.Message with exc.ToString().
42
EXCEPTION TYPES
It is also possible to catch specific types of exception. For an example of this, look at
the code in calc3Btn_Click():
try {
st = Convert.ToInt32( subTotBox.Text );
} catch( OverflowException exc ) {
MessageBox.Show( "Try a smaller number! " + exc.Message,
"Yikes! Overflow error: " + exc.GetType( ),
MessageBoxButtons.OK, MessageBoxIcon.Warning );
} catch( Exception exc ) {
MessageBox.Show( "Awfully sorry to bother you, but apparently the " +
exc.Message, "Oops! There seems to have been a slight error of the type:
" + exc.GetType( ),
MessageBoxButtons.OK, MessageBoxIcon.Error );
}
This expects the user to enter a 32-bit integer into the subtotal edit box. This
means that if the user enters a double such as 1.5 or an alphanumeric character such
as ‘X’, a FormatException will occur. However, another type of exception is also poss-
ible. An int32 value must fall between the range of -2,147,483,648 and 2,147,483,647.
If the user enters a number larger or smaller than these values, an OverflowException
will occur. Try it. Enter ‘X’ into the subtotal box and click the Calc3 button. You will
see the same error message as previously. Now enter eleven or more digits (e.g.
999999999999) into the subtotal box and once again click calc3. This time, you will see
a different error message which states ‘Try a smaller number!’.
To handle a particular exception type, just specify that type in a catch section.
Here I have specified OverflowException. I could subsequently add separate blocks
specifying other exceptions such as FormatException. Each block will only execute if
the specified exception object is found. If not, the code moves on to the next catch
block. The final catch block should normally specify the base Exception class and this
will execute if an exception of any type has occurred which has not already been
handled by an earlier catch block.
You may also nest one try..catch block inside another. And you may use a
finally block instead of a catch block or place a finally block after one or more catch
blocks. A finally block will execute whether or not an exception has occurred and it
may be used to reset values of any data which may be left in an unpredictable state
following an exception. You will find examples of a nested try..catch and finally
block in calc5Btn_Click().
43
DEBUGGING
Visual Studio has one of the best debuggers available anywhere, so be sure to make
good use of it. Typically you will start by placing breakpoints in your code to pause
execution of your program at one or more points where you want to examine the
values of variables.
To place a breakpoint, click in the left-hand shaded margin of a code file. The
breakpoint will be shown as a red circle in the margin and a highlighted red line in
the code. Now press F5 to run your program in the debugger. The program will stop
when a breakpoint is encountered.
Use the Locals window to view variables that are in scope or the Watch
window to view selected variables. You can add variables to the Watch window by
entering them as text or by dragging them out of a code window. You can also
evaluate variables or expressions (for example, mathematical expressions such as 10
* 5) in the Immediate window. You can view the calls that have been made (that is
the sequence of methods that have executed prior to arriving at the current method)
in the Call Stack window.
You can find debugging commands on the Debug menu or on the Debug
Toolbar which will normally appear above the editing window during a debugging
session. If the Debug toolbar is not visible, right-click the Toolbar area and select
‘Debug’ from the drop-down menu.
44
STEP TEN
GENERIC COLLECTIONS
In this step, I plan to start work on creating an adventure game in which the player
can move around a map taking and dropping objects. In order to do that I need to
manipulate lists of objects so that, for example, an item can be transferred from one
list (belonging to a Room) to another list (belonging to the Player) when it is taken.
To do this, I shall use some special .NET collection classes. We looked at simple
arrays earlier in this course. Arrays are sequential lists of items. The .NET
framework also supplies several other collection classes such as List and Dictionary.
These are called ‘generic collection’ classes.
LISTS
List<T>
Here T is the name of the type of the items in the list. In real code you might
declare lists of strings or Thing objects like this:
List<string>
List<Thing>
This is how I declare and construct a List typed to hold Thing objects:
Generics.sln
thingList.RemoveAt( i );
45
DICTIONARIES
The .NET Framework also has a Dictionary class. A Dictionary is a type of list in
which the items are indexed not by a sequential numerical index but by a key which
may be a unique object of any type. You can think of a Dictionary as the
programming equivalent of a real-world dictionary in which each entry has a unique
name as a ‘key’ – for example, “Dog” – followed by a definition which is its ‘value’ –
for example: “A furry mammal that woofs and gnaws on bones.”
The declaration of a Dictionary is a bit like the declaration of a List but it
requires two types (the key and the value) between a pair of angle-brackets. This is
how I might declare and construct a Dictionary with a string key and string value
and add a single item to it:
public Dictionary< string, string > petDictionary = new Dictionary<string, string >();
petDictionary.Add( "Dog", "A furry mammal that woofs and gnaws on bones" );
This is how I would declare and construct a Dictionary with a string key and
Room value and add a single item to it:
I can also use other methods to (for example), determine whether a Dictionary
object contains a specific key and remove an object if it exists:
46
This is an example of iterating over the items in the Dictionary and accessing
the key and the value from each item:
In this case, as the Dictionary has been typed to hold Room items I am able to
access the Room objects’ description property without having to ‘cast’ the item to a
Room type inside the loop.
OVERRIDDEN METHODS
The descendant classes must then override this virtual method. To do this you must
add the keyword override before the describe method name in the descendant
classes like this:
To understand why virtual methods are needed, let’s consider how a normal
‘non-virtual’ method works. Let’s suppose you have defined a class that has a
method with the same name and argument list as a method of its ancestor class.
Imagine, for example, that your program contains an ancestor class, A, and a
descendant class, B. A descendant class is compatible with its ancestor. You might
say that class B is a type of class A. For simplicity, let’s suppose that A.method1()
returns the string, “class A: method1” whereas B.method1() returns “class B: method1”.
Now let’s suppose that class A has a method called method1() and class B
also has a method called method1(). Instances of these classes are created as follows:
A mya1 = new A( );
B myb1 = new B( );
47
Were my code now to call mybOb.method1(), it would, of course, return the
string “class B: method1”. But now consider what would happen if you were
dealing with a collection of mixed objects, some of which might be A objects, others
B objects and others C, D and E objects.
All of the objects in this collection are either A objects or are descendants of A
objects. You add these objects, to a generic List that is typed to be compatible with
the A class called oblist. Now you write a foreach loop that iterates through all the
objects in the list, oblist. This loop has to know which base type of object it is dealing
with (here that’s class A). Since all descendant objects are compatible with class A, it
is possible to call method1() for each object encountered, like this:
This time consider what will happen if the foreach loop processes the two
objects: the A object mya1 and the B object myb1. The foreach loop has been told it is
dealing with instances of the A class. When myb1 is processed within the loop, will
b.method1() or a.method1() be called?
In fact, a.method1() will be called. So all the objects processed within the loop
will return the string “class A: method1”, even though some of the objects may
actually be B objects with their own unique method1() implementations.
How can we get around this problem? The answer is to make a.method1() a
‘virtual’ method and to make all the method1() implementations in descendent
objects ‘overridden’ methods. You do this by adding the keywords virtual and
override before the method type and name, like this:
If you call the virtual method2() inside the foreach loop, C# works out the
exact type of the object being processed before calling the specified method. So when
the B object, myb1, goes through the loop, the A.method1() non-virtual method is
executed, but the B.method2() virtual overridden method is executed.
48
MethodTest.sln
If virtual methods are new to you, you may find this much easier to
understand by running the project in the MethodTest.sln solution. Re-read the
explanation given above and see how each step has been coded. Note that I have
used the new keyword in b.method1(). This is recommended for clarity when an
ancestor class defines a non-virtual method of the same name and argument list.
When preceded by the keyword new a method is specifically declared to be a
replacement for a method with the same name in its ancestor class. In fact, if you omit
the new keyword in a non-virtual method, the method will operate in the same way
as if the new keyword had been used. However, in that case, Visual Studio will
show a warning.
STRING.FORMAT
The String class includes a method called Format() that lets you insert values at
marked points in a string. This avoid the necessity of concatenating values and
variables using the + operator. Instead you place index number (from 0 upwards)
between a pair of curly brackets into a string, and follow the string with a comma-
delimited list of items whose values will replace the placeholders.
For example, assuming you have these variables declared:
Using concatenation:
textBox1.Text = "Item " + 1 + " is a " + toy + " worth $" + price ;
Using String.Format():
49
AN ADVENTURE GAME
To end this course, I’ve written a small adventure game. This takes the form of a
Map which is a generic List containing Room objects. Each room may itself contain a
list of Thing Objects implemented as a ThingList. You’ll find my definition of the
ThingList class in ListManagers.cs. It is a generic List of Things. The player can
wander around the game from room to room taking and dropping objects. All that
remains is for you to add a few puzzles.
adventure-game.sln
BINARYFORMATTER
Before leaving tis game, I’d like to say a few words about how the ‘game
state’ (the position of the player and treasure objects, for example) is saved and
loaded from disk.
As in previous projects, I ‘serialize’ (deconstruct) objects so that they can be
stored in streams. In this case I have decided to use the BinaryFormatter class. This
class serializes and deserializes an object, or an entire graph of connected objects, in
binary format. That means that I can give it a ‘top level’ object – one that contains
many other objects, each of which may themselves contain objects – and let it work
out all the details of saving and restoring them to and from disk. Note too that I have
had to mark all the classes I want to save with the [Serializable] attribute. An
attribute is a special directive placed between square brackets. The BinaryFormatter
requires this attribute to be placed before the definition of any class to be serialized.
This is currently a very simple game and you can try to improve it. See my
TODO comments for ideas. You may also extend it by creating new rooms and
treasures and adding some puzzles – limited only by your imagination.
Have fun!
50