Library Zone Articles
External Articles
Byte Size

Discovery Zone Catalogue
Diary
Links
Bookstore
Interactive Zone Ask the Gurus
Discussion Groups
Newsletters
Feedback
Etc Cartoons
Humour
COMpetition
Advertising
Site Builder ASP Web Ring ASP Web Ring
Power your site with idr newswire
The Developer's Resource & Community Site
COM XML ASP Java & Misc. NEW: VS.NET
International This Week Forums Author Central Find a Job
C# Programming with the Public Beta - Buy the Book

(Reproduced with kind permision of Wrox Press: https://www.wrox.com)

C# Programming with the Public Beta - Chapter 7 - Namespaces and the Base Classes

In this chapter we're going to look at the base classes - the vast number of .NET classes that Microsoft has written for you, and also namespaces - the system by which classes are grouped together.

A significant part of the power of the .NET framework comes from the base classes supplied by Microsoft as part of the .NET framework. These classes are all callable from C# and provide the kind of basic functionality that is needed by many applications to perform, amongst other things, basic system, Windows, and file handling tasks. To a large extent the .NET base classes can be seen as replacing the previous Win32 API and a large number of MS-supplied COM components. The base classes are simpler to use, and you can easily derive from them to provide your own specialist classes. You can also use the base classes from any .NET-aware language (calling Win32 API functions from VB was possible but not easy). The types of purposes you can use the base classes to do include:

  • String handling
  • Arrays, lists, maps etc.
  • Accessing files and the file system
  • Accessing the registry
  • Security
  • Windowing
  • Windows messages
  • Connecting to other computers and to the Internet
  • Drawing
  • Directory Access
  • Database Access

You can see from the above list that besides giving access to basic Windows operations, the base classes define many useful data types, including strings and collections of data.

The base classes are not, of course, only available to C# programs - they can equally well be accessed from VB, C++ or any other .NET-compliant or (by use of some wrapper objects) COM-aware language, but we will concentrate on C# here.

The aim of this chapter is to give you an overview of the kinds of things you can do using the base classes and how to perform certain common operations. Clearly the scope of the base classes is so vast that we cannot give any kind of comprehensive guide in one chapter - instead we are going to pick on a few common programming tasks and present sample code to demonstrate how you can easily execute those tasks. However we will also show you how you can use the WinCV tool which is supplied with the .NET SDK to explore the base classes for yourself.

The tasks we're going to cover in this chapter include:

  • Manipulating dates and times
  • Navigating the file system
  • Reading and writing to files
  • Copying, Moving and Deleting files
  • Connecting to the Internet
  • Accessing the registry
  • Mathematical functions

Note that we are not covering windowing or data access in this chapter. These areas are important enough to warrant chapters in their own right and are respectively covered in Chapters 8 and 9.

Before we do that though, we need to switch topics for a while and understand how namespaces work in C#, since you need to be able to use and reference namespaces in order to be able to access the base classes.

Namespaces

A namespace can be seen as a container for some classes in much the same way that a folder on your file system contains files. Namespaces are needed because there are a lot of .NET classes. Microsoft has written many thousands of base classes, and any reasonably large application will define many more. By putting the classes into namespaces we can group related classes together, and also avoid the risk of name collisions: If your company happens to define a class that has the same name as the class written by another organization, and there were no namespaces, there would be no way for a compiler to figure out which class a program is actually referring to. With namespaces, there isn't a problem because the two classes will be placed in different namespaces, which compares with, say, the Windows files system where files with the same name can be contained in different folders.

It is also possible for namespaces to contain other namespaces, just as folders on your file system can contain other folders as well as files.

When Visual Studio generates your projects, it automatically puts your classes in a namespace. Say for example you use the developer environment to create a C# Windows Application project called MyProject. If you do this and look at the code generated for you, you'll see something like this.


namespace MyProject
{
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.WinForms;
using System.Data;

There's two C# keywords here that we need to understand: namespace and using. We'll look at namespace first then examine what using does.

The initial namespace command indicates that everything following the opening curly brace is part of a namespace called MyProject. Later on in the file, a class called Form1 is declared.

public class Form1 : System.WinForms.Form

Because this class has been declared inside the namespace, its 'real' name is not Form1 but MyProject.Form1, and any other code outside this namespace must refer to it as such. This name is correctly known as its fully-qualified name. Notice that in the above line, Form1 is derived from a class called Form. This class is defined inside the namespace WinForms, which in turn is defined inside the namespace System (recall we mentioned earlier that it is possible for namespaces to contain other namespaces). This code sample refers to the Form class using its fully qualified name.

Now we'll have a look at the purpose of the using command in the above code. It's basically a way of avoiding having to write fully-qualified names everywhere, since the fully-qualified names can get quite long and make your code hard to read. For example if we consider the line

using System.WinForms;

This line declares that I may later in the code use classes from the System.WinForms namespace, without indicating the fully-qualified name - and the same applied for every other namespace mentioned in a using command. For example consider the line of code, also generated by the developer environment

public class Form1 : System.WinForms.Form

Because of the earlier using command, we could equally well write this as

public class Form1 : Form

In this latter case the compiler will locate the class by searching all the namespaces that have been mentioned in a using command. If it finds a class named Form in more than one of these namespaces, it will generate a compilation error - in that case you would need to use the fully-qualified name in your source code.

Figure 1

Note that the only purpose of the using command in this context is to save you typing and make your code simpler. It doesn't, for example, cause any other code or libraries to be added to your project. If your code uses base classes or any other classes that are defined in libraries (recall in .NET these are stored in assemblies), you need to ensure separately that the compiler knows which assemblies to look in for the classes. If you are compiling from the Visual Studio developer environment, this is done through the project references, which are listed in the Solution Explorer window, as shown in this screenshot:

Some references are inserted automatically when your project is created - which ones depends on the type of project, and you can add others using the Project | Add Reference menu. The screenshot shows the situation for a newly created C# Windows Application. (Note that although it is assemblies rather than namespaces that are referenced, the solution explorer shows the namespaces that are found in these assemblies. A given assembly can contain more than one namespace and vice versa.)

If you are compiling from the command line then the assembly mscorlib.dll, which contains some of the most important base classes, is referenced implicitly. You will need to indicate any other assemblies to be referenced with the command line parameter /r, supplying the full file system path to the assembly. For example for the above project, the appropriate command is:

csc ReadFile.cs /r:c:\WinNT\Microsoft.NET\Framework\v1.0.2204\System.Drawing.dll
/r:c:\WinNT\Microsoft.NET\Framework\v1.0.2204\System.WinForms.dll
/r:c:\WinNT\Microsoft.NET\Framework\v1.0.2204\System.Data.dll
/r:c:\WinNT\Microsoft.NET\Framework\v1.0.2204\System.Diagnostics.dll
/r:c:\WinNT\Microsoft.NET\Framework\v1.0.2204\System.dll
/r:c:\WinNT\Microsoft.NET\Framework\v1.0.2204\Microsoft.Win32.Interop.dll

Now we've understood the concept of a namespace we can go on to look at the namespaces that contain the various base classes.

Using Aliases

If you have a very long namespace and you want to use it several times in your code, then you can substitute a short word for the long namespace name which you can refer to in the code as often as you want. The advantages of doing this are that the code becomes easier to read and maintain and it saves you typing out very long strings.

The syntax for declaring an alias is:

using Alias = Wrox.SampleCode.CSharpPreview.Examples;

and this sample code illustrates how you can use an alias:


namespace Wrox.SampleCode.CSharpPreview
{
   using System;
   using MathEx = Wrox.SampleCode.CSharpPreview.Examples;

   namespace ChBaseClasses
   {
      public class clsExample1
      {
         public static void Main()
         {
            MathEx.clsMath MyMathClass = new MathEx.clsMath();
            Console.WriteLine(MyMathClass.Add(3,4));
         }
      }
   }

   // The alias MathEx refers to this namespace
   namespace Examples
   {
      public class clsMath
      {
         public int Add(int x, int y)
         {
            int z = x + y;
            return (z);
         }
      }
   }
}

The Base Classes

As we've remarked there are a huge number of base classes, and there are also a large number of namespaces that contain these classes. We cannot hope to give comprehensive coverage in this chapter, but we'll give a quick summary of some of the more useful namespaces here.

Namespace

Namespace Classes Contained

System.Web

Classes that assist in the generation of web pages using ASP+

System.Net

Classes to make requests over the network and Internet

System.Data

ADO+ classes that make it easy to access data from relational databases and other data sources.

System.WinForms

Classes to display controls for standalone (as opposed to web) windows applications.

Microsoft.Win32

Functions that were previously accessible mainly through the Win32 API functions, such as registry access.

There are many more namespaces, and the best way to learn about them and the classes they contain is to explore them yourself - and Microsoft have provided a tool for just this purpose. That's what we'll look at next.

The WinCV Tool

WinCV is a class viewer tool, which can be used to examine the classes available in shared assemblies- including all the base classes. (A shared assembly is an assembly that has been marked for use by other applications - we'll cover shared assemblies in Chapter 10). For each class, it lists the methods, properties, events and fields using a C#-like syntax.

At the present time you start it by typing in wincv at the command prompt or clicking on the file in Windows Explorer. This gives you a basic window with two panes, as shown.

Figure 2

You then type in a search string that is contained in the class you are looking for in the Searching For box. For example if you want to find a class that handles dates, you might try typing in Date in this box. As you type, WinCV will display a list of classes that contain the string in question in their names:

Figure 3

As this screenshot shows, there are a lot of classes whose name contains the string date - then it's just a question of scanning through the names to find the one that looks most suitable. The definition of the selected class is then displayed in the right hand pane. The left hand pane also indicates the namespace the class is contained in - you'll need this in order to be able to use that class in your C# code. In the case of DateTime, WinCV tells us that it is part of the System namespace, which means that the line

using System;

that is placed by the AppWizard by default in most C# projects will be sufficient to give us access to the class without explicitly indicating the namespace.

The right hand pane of WinCV tells us which assembly the class is defined in - the screenshot shows us that System.DateTime is defined in mscorlib.dll. This information is useful to indicate if we need to link in any assemblies when we compile, in order to have access to the class. Again, the above screenshot tells us that we are OK for the DateTime class, since it is defined in mscorlib.dll, which is, by default, referenced anyway.

Now we've laid the groundwork, the rest of this chapter is devoted to presenting a number of sample C# projects which are designed to illustrate how to carry out some common programming tasks that are made easy by the base classes.

About the Samples

Before we go through the samples in this chapter, we'll quickly go over how we are structuring them. Since in most cases all we want to do is perform a few operations such as writing to a file or accessing some registry keys then displaying the results, it would be simplest to provide console samples - that way we minimize the extraneous code. However in practice, very few Windows applications are written as console applications, and it would be nice to put the output in a more realistic environment. Accordingly, the samples will all be presented as Windows applications, based on a form that has a list box, as shown here.

Figure 4

We haven't yet covered how to write Windows applications in C# - that's the subject of Chapter 8, but we'll give you just enough information here for you to understand how the samples are created.

Each project is created as a C# Windows application: So when we start up the Visual Studio developer environment and select New Project, we fill in the New Project dialog like this (though supplying our own name and path for the project).

Figure 5

Once the project has been created, we use the Toolbox in the developer environment to place a List box on the form - by default this will be called listBox1. The List box is actually an instance of a C# object of type ListBox, from the System.WinForms namespace.

We then examine the code the wizard has generated for us and make the following changes (shown highlighted).


public class Form1 : System.WinForms.Form
{
   ///  
   ///    Required designer variable
   /// 
   private System.ComponentModel.Container components;
   private System.WinForms.ListBox listBox1;
   
   private int nItems=0;
   public void AddItem(string sItem)
   {
      object oItem = sItem;
      listBox1.InsertItem(nItems,oItem);
      ++nItems;
   }

   public Form1()
   {
      //
      // Required for Win Form Designer support
      //
      InitializeComponent();
      //
      // TODO: Add any constructor code after InitializeComponent call
      //

      AddItem("An item of output");
      AddItem("2nd bit of output");
      AddItem("More output");
   }
   ...

The code that we are adding is to the Form1 class which represents the entire form (window) created when the program is run. We first add a member field, nItems, and a member function, AddItem(). These are to make it easier for us to add lines of text to the list box. This is normally done by using the member function, InsertItem() of the ListBox class. But in its simplest overload, this function takes two parameters - the zero-based index of where we wish to insert the item, and the item itself, passed as an object (InsertItem() will use the ToString() function to extract the string to be added). Since we wish to add each successive item to the end of the list box, this would require us to keep a count of how many items we have added. Our AddItem() function automates that process for us.


private int nItems=0;
   public void AddItem(string sItem)
   {
      object oItem = sItem;
      listBox1.InsertItem(nItems,oItem);
      ++nItems;
   }

I should mention we could also add the items to the list box with the line of code

listBox1.Items.Add(oItem);

though either way our function does marginally simplify the code.

We then actually add the items in the Form1 constructor. In this case we have simply added three strings.

AddItem("An item of output");
      AddItem("2nd bit of output");
      AddItem("More output");

In all the subsequent samples these three lines will be replaced by the code to do whatever processing is required to illustrate the task in hand, followed by AddItem() calls to display the results. Each of the projects in this chapter were created in essentially the same way to the above example. Note however that in the following samples, we've also changed the name of the Form1 class and the namespace that it appears in. The developer environment by default gives us a namespace that has the same name as the project, however Microsoft guidelines say that the namespace should start with the company name (which makes sense, as you would not have any name clashes with other company's classes). So for all the projects here we'll use the namespace Wrox.BookSamples.CSharpPreview.ChBaseClasses.

You should bear in mind when reading through the samples that some of the classes may be modified before the .NET SDK is finally released. Hence they will give you a good idea of how to carry out tasks, but some of the names or signatures of methods may be slightly different. Also in many cases there are several classes or methods whose functionality overlaps. Just because we've presented a certain way of doing something doesn't mean there won't be alternative ways of doing the same task.

Manipulating Dates and Times

Our first samples will demonstrate how to access the date-time functionality provided by the base classes. There are two classes of relevance here: DateTime and TimeSpan, both of which are in the System namespace. We start off by displaying the current date and time in various different formats, using this code:

namespace Wrox.SampleCode.CSharpPreview.ChBaseClasses
{
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.WinForms;
using System.Data;

public class FormDisplayDateTimes : System.WinForms.Form
{
... as before

/// <summary>
///    Summary description for FormDisplayDateTimes.
/// </summary>
   public FormDisplayDateTimes()
   {
      //
      // Required for Win Form Designer support
      //
 

      InitializeComponent();

      //
      // TODO: Add any constructor code after InitializeComponent call
      //
      DateTime dtCurrTime = DateTime.Now;
      AddItem("Current Time is " + dtCurrTime.ToString());
      AddItem("Year is " + dtCurrTime.Year.ToString());
      AddItem("Month is " + dtCurrTime.Month.ToString());
      AddItem("Day of month is " + dtCurrTime.Day.ToString());
      AddItem("Day of week is " + dtCurrTime.DayOfWeek.ToString());
      AddItem("Hour is " + dtCurrTime.Hour.ToString());
      AddItem("Minute is " + dtCurrTime.Minute.ToString());
      AddItem("Second is " + dtCurrTime.Second.ToString());
      AddItem("Millisecond is " + dtCurrTime.Millisecond.ToString());
      AddItem("ShortDateString is " + dtCurrTime.ToShortDateString());
      AddItem("LongDateString is " + dtCurrTime.ToLongDateString());
      AddItem("ShortTimeString is " + dtCurrTime.ToShortTimeString());
      AddItem("LongTimeString is " + dtCurrTime.ToLongTimeString());
   }

As explained earlier, we are adding our code to the constructor of the FormDisplayDateTimes class in our project. (FormDisplayDateTimes was Form1 in the generated code but we've changed its name to a more meaningful class name.)

We first instantiate a variable dtCurrTime of class DateTime, and initialize it using the static property of the DateTime class, Now, which returns the current date and time. We then use various properties, fields and methods to extract portions of the current date and time. This code produces the following output:

Figure 6
Figure 6
...

public class FormTimeSpans : System.WinForms.Form
{
... as before

   public FormTimeSpans()
   {
      //
      // Required for Win Form Designer support
      //
      InitializeComponent();

      //
      // TODO: Add any constructor code after InitializeComponent call
      //

      // constructor: TimeSpan(days, hours, minutes, seconds)
      TimeSpan Span = new TimeSpan(4,2,15,0);
      
      // initialize date to 1 Jan 2000, 12 pm
      // constructor: DateTime(year,month,day,hours,minutes,seconds, 
      // milliseconds)
      DateTime dtOld = new DateTime(2000,1,1,12,0,0,0);

      DateTime dtNew = dtOld + Span;

      AddItem("Original date was " + dtOld.ToLongDateString() + 
              "  " + dtOld.ToShortTimeString());
      AddItem("Adding time span of " + Span.ToString());
      AddItem("Result is " + dtNew.ToLongDateString() + "  " +
               dtNew.ToShortTimeString());
      AddItem("");
      AddItem("Time span broken down is:");
      AddItem("Days: " + Span.Days.ToString());
      AddItem("Hours: " + Span.Hours.ToString());
      AddItem("Minutes: " + Span.Minutes.ToString());
      AddItem("Seconds: " + Span.Seconds.ToString());
      AddItem("Milliseconds: " + Span.Milliseconds.ToString());
      AddItem("Ticks: " + Span.Ticks.ToString());
      AddItem("");
      AddItem("TicksPerSecond: " + TimeSpan.TicksPerSecond.ToString());
      AddItem("TicksPerHour: " + TimeSpan.TicksPerHour.ToString());

   }

In this sample we see the new operator being used to construct DateTime and TimeSpan instances of given value. Both these classes have several constructors with different numbers of parameters depending on how precisely you wish to specify the time. We add the span on to the time and display the results. In displaying the results we use the ToLongDateString() method of the DateTime class to get the date in plain English, but the ToShortTimeString() method to get the time (using ToLongTimeString() would give us milliseconds as well). We then use various properties of the TimeSpan class to show the days, hours, minutes, seconds, milliseconds and ticks. Ticks are the smallest unit allowed by the TimeSpan class and measure one ten-millionth of a second, as indicated by a couple of static fields that give conversion factors, which we also display.

The above code gives this output:

Figure 7
Figure 7

File and Folder Operations

One of the most common things you'll need to do is access the file system, to read and write to files, to move or copy files around, or to explore folders to check what files are there. The .NET base classes include a number of classes that provide a rich set of functionality to do these tasks. These classes are contained in the System.IO namespace. Since the AppWizard doesn't add code to refer to this namespace by default, we add the line using System.IO near the top of the Form1.cs source file for all the samples in this section:

namespace Wrox.SampleCode.CSharpPreview.ChBaseClasses
{
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.WinForms;
using System.Data;
using System.IO;

Note that (as you can find out from the WinCV tool) these classes are still defined in mscorlib.dll, so we don't need to add any files to the project references.

Finding Out Information About a File

Our first file operations sample, which we'll call FileInfo will demonstrate how to get information about a file, such as its last write time, and its size. To do this we use a class, System.IO.File. The code to obtain the information is as follows:

File fl = new File("C:\\dotNET Book\\Ch8_ADO.doc");
      AddItem("Connected to file : " + fl.Name);
      AddItem("In Folder: " + fl.Directory);
      AddItem("Full path: " + fl.FullName);
      AddItem("Is Directory: " + fl.IsDirectory.ToString());
      AddItem("Is File: " + fl.IsFile);
      AddItem("Last write time: " + fl.LastWriteTime.ToString());
      AddItem("Size in bytes: " + fl.Length);

This code should be fairly self-explanatory. We construct a File instance by supplying the full path name of the location of the file in the file system, and the File instance then refers to that file, and we can simply read off a number of properties of it. In this case I'm binding to the file for one of the other chapters of this book, the ADO+ chapter. Note that \ by itself is interpreted as the start of an escape sequence in strings in C#, so we need to use \\ to represent a single backslash in the pathname. You can also use an alternative syntax, in which the @ character precedes the string, which indicates that characters should not be escaped. Hence you would write:

File fl = new File(@"C:\dotNET Book\Ch8_ADO.doc");

This code gives us this output:

Figure 8
Figure 8

In general you can use a File object to connect to either a file or a folder, although if you connect to a folder then attempting to access those properties that don't make sense for a folder (such as Length or LastWriteTime) will raise an exception.

Listing Files in a Folder

To explore the contents of a folder, we need another base class - in this case the class Directory also in the System.IO namespace. Note that the .NET base classes generally refer to folders as directories in class and method names. This corresponds to normal terminology on web sites and on Unix and Linux machines, as well as on Windows 3.1 when it was around, but can be confusing if you're used to Windows terminology, in which a folder is an item in the file system that contains files, and a directory is a more sophisticated complete information store (such as Active Directory). I'll continue to use the term folder in this chapter, in accordance with normal usage for the Windows file system.

The following code sample connects to the folder C:\dotNET Book and separately lists the files and folders in it.

Directory fl = new Directory("C:\\dotNET Book");
      AddItem("Connected to folder: " + fl.Name);
      AddItem("Full path: " + fl.FullName);
      AddItem("Is Directory: " + fl.IsDirectory.ToString());
      AddItem("Is File: " + fl.IsFile);
      
      AddItem("");
      AddItem("Files contained in this folder:");
      File [] childfiles = fl.GetFiles();
      foreach (File childfile in childfiles)
      {
         AddItem("  " + childfile.Name);
      }

      AddItem("");
      AddItem("Subfolders contained in this folder:");
      Directory [] childfolders = fl.GetDirectories();
      foreach (Directory childfolder in childfolders)
      {
         AddItem("  " + childfolder.Name);
      }

This code starts off by showing that the way of connecting to a folder is the same as for a file, and that the Directory and File classes both share the Boolean IsDirectory and IsFile properties, which can be used to distinguish what the object is if you are unsure. This means that you do not know what an object is, you can for example bind to it as a file, then use the IsDirectory property to check if it is actually a folder - and re-bind to it as a Directory if IsDirectory returns true. The resulting code would look something like this:

File fl = new File("C:\\DotNET Book");
      if (fl.IsDirectory == true)
      {
         fl = null;
         Directory dr = new Directory("C:\\DotNET Book");
         // process as directory
      }
      else
      {
         // process as file
      }

In the above code we next use a method, GetFiles() to retrieve a list of the files in the directory. This method returns an array of File instances, each one already bound to a file - so we can use a foreach loop to iterate through the array and carry out whatever processing we need to do on each file. The Directory class has another method, GetDirectories(), which works in exactly the same way as GetFiles(), but returns an array of Directory instances that refer to each subfolder. In both cases in the sample we use these methods to simply display the names of the files and folders.

This code produces this output on my computer:

Figure 9
Figure 9

Copying and Deleting files

The next sample is the one in which we really get to have a bit of fun mucking about with the directory system. As usual, it's a Windows project in which we add the code to the form's constructor, though in this case there's no real output to display.

We start off by binding to the dotNET Book folder, and we create both a new empty file and a new empty subfolder there.

Directory fl = new Directory("C:\\dotNET Book");
      fl.CreateSubdirectory("SampleBackups");
      fl.CreateFile("MyNewFile.txt");

We're not going to write anything to the file yet - we'll do that soon.

Next we bind to one of the files in the C:\dotNET Book folder, rename it and copy it:

File adofile = new File("C:\\dotNET Book\\Ch8_ADO.doc");
      adofile.CopyTo("C:\\dotNET Book\\SampleBackups\\Ch8_Backup.doc");

Note that you should put the complete path in otherwise the file will be copied to the same directory as the executable.

Now we have a go at deleting things - first the file spec.doc, then the Samples folder.

File sparefile = new File("C:\\dotNET Book\\spec.doc");
      sparefile.Delete();

      Directory sparefolder = new Directory("C:\\dotNET Book\\Samples");
      sparefolder.Delete(true);

The File.Delete() method doesn't take any parameters. The Directory.Delete() method has two overloads. One (which we haven't demonstrated here) takes no parameters and does a simple delete and the file or directory goes to the recycle bin. The other takes one parameter - a Boolean which indicates whether the delete operation is recursive, which in this case we've specified that it is. If we'd wanted the delete not to be recursive we'd have written:

sparefolder.Delete(false);

In that case if sparefolder contained subfolders an exception would be raised.

Reading Text Files

Reading files is quite simple, since Microsoft have provided a large number of classes that represent streams, which may be used to transfer data. Transferring data here can include such things as reading and writing to files, or downloading from the Internet, or simply moving data from one location to another using a stream.

The available classes are all derived from the class System.IO.Stream, which can represent any stream, and the various classes represent different specializations, for example streams that are specifically geared to reading or writing to files. In general, for the examples in this chapter that involve using streams, there are potentially a number of different ways to write the code, using any of several of the available stream objects. However, in this chapter we'll just present one way that you could perform each of the processes of reading and writing to text and binary files.

Reading text files is quite simple - for this we're going to use the StreamReader class. The StreamReader represents a stream specifically geared to reading text. We'll demonstrate the process with a sample that reads in and displays the contents of the ReadMe.txt file generated by the developer environment's AppWizard for our earlier EnumFiles sample. The code looks like this.

File fIn = new File
                 ("C:\\dotNET Projects\\Namespaces\\EnumFiles\\ReadMe.txt");
      StreamReader strm = fIn.OpenText();

      // continue reading until end of file
      string sLine;
      do
      {
         sLine = strm.ReadLine();
         AddItem(sLine);
      }
      while (sLine != null);
      strm.Close();

We obtain a StreamReader instance using the OpenText() method of the File class. The StreamReader class contains several methods that either read or peek at differing amounts of data.

Peeking means looking ahead at the data, but without actually moving through the data. The best way to understand this is by imagining a pointer that indicates which bit of the file you are due to read next. If you read data, then the pointer will be moved to point at the byte following the last byte read, so the next read (or peek) will bring in the next block of data. If you peek at data, the pointer is not changed, so the next read (or peek) will retrieve the same data again.

The most useful method however for our purposes is ReadLine(), which reads as far as the next carriage return, returning the result in a string. If we have reached the end of the file, ReadLine() does not throw an exception, but simply returns a null reference - so we use this to test for the end of the file. Note that a null reference is not the same as an empty string. If we'd instead applied the condition

while (sLine != "");

to the do loop, the loop would have finished the moment we came to a blank line in the file, not at the end of the file. (StreamReader.ReadLine() returns the string without the trailing carriage return and line feed).

Running this sample produces this output, showing it's correctly read the ReadMe.Txt file:

Figure 10
Figure 10

Writing Text Files

Writing text files follows similar principles to reading from them - in this case we use the StreamWriter class.

What makes writing text files even easier than reading them is that the method we use to write a line of text output followed by a carriage return-line feed, StreamWriter.WriteLine(), has a number of overloads so we don't necessarily need to pass it just text. It will accept a string, an object, a Boolean or several of the numeric types. This can be seen from this code sample, which writes out some text to the blank file we created earlier, MyNewFile.txt.

StreamWriter strm = new StreamWriter
         ("C:\\dotNET Book\\MyNewFile.txt", false);

      strm.WriteLine("This is some text");
      strm.WriteLine("Next lines are numbers");
      strm.WriteLine(3);
      strm.WriteLine(4.55);
 

      strm.WriteLine("And the next line is a boolean");
      strm.WriteLine(true);
      strm.Close();

The results of this can be seen in Notepad:

Figure 11
Figure 11

There are a number of overrides to the constructor of the StreamWriter. The constructor we have picked in our code sample is quite flexible, taking two parameters: the full name of the file, and a Boolean that indicates whether data should be appended to the file. If this is false then the contents of the file will be overwritten by the StreamWriter. In either case, the file will be opened if it already exists or created if it doesn't. This behavior can be customized by using other more complex constructors to the StreamWriter.

Reading Binary Files

Once we get to binary files we need to abandon the text-specific StreamReader and StreamWriter classes in place of a more general-purpose class. There are actually two classes that will do the job, System.IO.Stream and System.IO.FileStream. FileStream is designed specifically for reading and writing to files, while Stream is able to transmit data between files or other objects. The two classes work in very similar ways, and for the sake of demonstrating both of them, we'll use Stream for reading data and then use FileStream in the following sample which writes data to a file.

This sample demonstrates how to use the Stream class to read data. It opens a file and reads it, a byte at a time, each time displaying the numeric value of the byte read.

File fl = new File("C:\\dotNET Book\\TestReader.txt");
      Stream strm = fl.OpenRead();
      int iNext;
      do
      {
         iNext = strm.ReadByte();
         if (iNext != -1)
            AddItem(iNext.ToString());
      }
      while (iNext != -1);
      strm.Close();

We obtain an instance of the Stream class by first instantiating a File object attached to the required file and calling its OpenRead() method. We then read through the file by calling the Stream.ReadByte() method. This method reads the next byte returning its value as an int. If we have reached the end of the file, then -1 is returned, but no exception is thrown - and it is this condition we use to test for the end of the file. Note that the Stream class also has a Read() method which can read a specified number of bytes in one go - I've chosen to use ReadByte() here as it leads to simpler code.

The file I've tested this code on looks like this - the first few letters of the alphabet followed by three carriage return-line feeds. Although the code will work on any file, I've demonstrated it on a text file because that makes it easier to see visually that the results are correct.

The file I've tested this code on looks like this - the first few letters of the alphabet followed by three carriage return-line feeds. Although the code will work on any file, I've demonstrated it on a text file because that makes it easier to see visually that the results are correct.

Figure 12
Figure 12

Writing Binary Files

Although we can use either the Stream or FileStream classes to perform this task, we'll use an instance of FileStream for this sample. This code writes out a short text file that contains the letters FGHIJK followed by a carriage return-line feed combination. Note that again although this code is capable of writing any data, we're using textual data in the sample so that we can easily use Notepad to check that the sample has worked.

The code looks like this:

byte [] bytes = {70,71,72,73,74,75,13,10};
      FileStream strm = new FileStream
         ("C:\\dotNET Book\\TestWriter.txt", 
         FileMode.OpenOrCreate, FileAccess.Write);
      foreach (byte bNext in bytes)
      {
         strm.WriteByte(bNext);
      }
      strm.Close();

We first define an array of bytes that contains the data to be written to the file - in this case the ASCII codes of the characters. True binary data would have simply meant changing some of the values in this array to represent non-printable characters.

Next we instantiate a FileStream object. The constructor we use takes three parameters: the full pathname of the file, the mode we are using to open it and the access required. The mode and access merit more consideration -- they are enumerated values respectively taken from two further classes in the System.IO namespace: FileMode and FileAccess. The possible values these can take are all self-explanatory. In the case of the mode the possible values are Append, Create, CreateNew, Open, OpenOrCreate and Truncate. For the access they are Read, ReadWrite and Write.

Finally we use the WriteByte method of the FileStream object to write out each byte before closing the file. Again there is a FileStream.Write method, which can write out a number of bytes at a time and which you may prefer to use. We've stuck with WriteByte because it makes it clearer what is going on as we loop through the array.

And finally the test whether this code has worked: After running it, opening the new file with Notepad gives this:

Figure 13
Figure 13

Browsing the Internet

Retrieving a file from the Internet really involves two processes: requesting the file, and reading it through a stream. We've already covered the latter process - the code to do it is essentially the same as our earlier sample to read text from a file using the StreamReader class. Loading and requesting the file is a little more complicated as it involves several new classes.

We're going to need a couple of classes concerned with web browsing: HttpWebRequest, HttpWebResponse and WebRequestFactory, which are all in the System.Net namespace. The assembly that defines this namespace is not by default loaded in C# projects so we need to add it to our references in the solution explorer. Recall we can do this by right-clicking on the References node in the explorer and selecting Add Reference from the context menu.

Figure 14
Figure 14

Next we add a couple of commands to refer to some new namespaces in the C# source file:

namespace WebRequest
{
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.WinForms;
using System.Data;
using System.Net;
using System.IO;
using System.Text;

System.Net is for the web classes just mentioned, System.IO is because we will need to use a StreamReader, and System.Text provides a helper enumeration value used in constructing the StreamReader. These latter two classes are both in mscorlib.dll so no new references need to be added at compilation time.

Now we can proceed to the code needed to make our request to the web server and display the results:

      HttpWebRequest webreq =
         (HttpWebRequest)WebRequestFactory.Create
         ("http://localhost/postinfo.html");
      HttpWebResponse webresp = 
         (HttpWebResponse)webreq.GetResponse();

      StreamReader strm = new StreamReader(
         webresp.GetResponseStream(), Encoding.ASCII);
      string sLine;
      do
      {
         sLine = strm.ReadLine();
         AddItem(sLine);
      }
      while (sLine != null);
      strm.Close();

The request is made through an instance of the class HttpWebRequest. However, it is not possible to construct this instance directly - instead it needs to be constructed by a static method of another class, WebRequestFactory. The WebRequestFactory.Create() method is designed to create a general request for a given URL - in this case we pass the URL of one of the files created on the default web site on my local machine when IIS is installed - postinfo.html. Note that WebRequestFactory.Create() actually returns a reference to a WebRequest object, not an HttpWebRequest object, so we need to cast the return value. HttpWebRequest is derived from WebRequest - the latter class is more general-purpose and able to deal with requests using other protocols besides HTTP.

Once we have the HttpWebRequest instance, we actually make the request by calling its GetResponse() method. GetResponse() returns a WebResponse object, which encapsulates information returned by a web server in response to a request. In a similar manner to WebRequestFactory.Create(), the return value is a reference to a WebResponse rather than an HttpWebResponse, so we need to cast it to the required data type.

Once we have the HttpWebResponse, we simply need to obtain a StreamReader instance that we can use to retrieve the contents of the file. To do this we use a StreamReader constructor that takes two parameters: a more general stream and a value that indicates the encoding type. The stream is obtained from the GetResponseStream() method of the HttpWebResponse class, and the encoding type is Encoding.ASCII, an enumerated value from the System.Text.Encoding class, which indicates that this stream contains ASCII text data.

Although there are a lot of classes involved with this and hence a lot to take in, the actual code is still reasonably short and simple. Running this sample produces this result:

Figure 15

This indicates that the page has been successfully downloaded and displayed.

Accessing the Registry

In this section we'll present a code sample that enumerates registry keys and reads their values.

There are several classes used to access the registry, of which we will use two: Registry, and RegistryKey, both of which are in the Microsoft.Win32 namespace. This namespace is defined in the same mscorlib assembly that contains the system namespace, so you have access to it without needing to add further references to your project. The remaining registry classes are concerned with security, which is beyond the scope of this chapter.

In order to access a given registry key using the base classes it is necessary to progressively navigate down the registry hierarchy to reach the key. We start off using the Registry class: This class contains static fields that allow access to any of the registry hives. The fields are named:

ClassesRoot
CurrentConfig
CurrentUser
DynData
LocalMachine
PerformanceData
Users

and are all of class RegistryKey. These hives should be familiar to most programmers, though we will comment that PerformanceData may be unfamiliar to some developers. This hive does exist, and is used to access performance counters, but it is not visible in Regedit. The RegistryKey class represents any given key in the registry and has methods to carry out all sorts of operations including accessing and enumerating subkeys, and reading and modifying values. Thus for example, to access the registry key at the top of the ClassesRoot hive, we would use the following:

RegistryKey hkcr = Registry.ClassesRoot;

and we would then be able to use the variable hkcr to perform operations on that key.

The sample, RegEnumKeys, binds to a registry key, enumerates its subkeys, and displays the name and all values of each subkey. The key we've chosen to bind to is the registry key whose subkeys contain details of all the ADSI providers installed on the computer, HKLM/SOFTWARE/Microsoft/ADs/Providers. When run, the sample gives this output on my machine:

Figure 16
Figure 16

We first ensure that we can access the registry classes without giving full namespace names:

namespace Wrox.SampleCode.CSharpPreview.ChBaseClasses
{
using System;
using Microsoft.Win32;using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.WinForms;
using System.Data;
using System.IO;

As usual the action takes place in the constructor to the main form class which we've renamed FormRegEnumKeys. We first need to navigate down to the required key. As mentioned earlier, we cannot do this in one go, but have to progressively step down through the registry hierarchy. Note that the name passed to the RegistryKey.OpenSubKey method is not case sensitive:

public FormRegEnumKeys()
    {
       //
       // Required for Win Form Designer support
       //
       InitializeComponent();

       //
       // TODO: Add any constructor code after InitializeComponent call
       //
      RegistryKey hklm = Registry.LocalMachine;
      RegistryKey software = hklm.OpenSubKey("SOFTWARE");
      RegistryKey microsoft = software.OpenSubKey("Microsoft");
      RegistryKey ads = microsoft.OpenSubKey("ADS");
      RegistryKey prov = ads.OpenSubKey("Providers");

Now we can display the number of subkeys (corresponding in this case to the number of installed ADSI providers), and start to enumerate over each of them in a foreach loop. The following code uses two foreach loops, one to enumerate through the subkeys, and the other to enumerate through each value associated with each subkey.

AddItem("no. of subkeys is " + prov.SubKeyCount);
      AddItem("");
      AddItem("ADSI Provider registry keys are:");
      string [] sProvNames = prov.GetSubKeyNames();
      foreach (string sName in sProvNames)
      {
         RegistryKey provkey = prov.OpenSubKey(sName);
         AddItem("");
         AddItem(sName + "  (" +
            provkey.ValueCount.ToString() + " values)" );
         foreach (string sValName in provkey.GetValueNames())
         {
            AddItem("   sValName:  " + 
               provkey.GetValue(sValName));
         }
      }

Mathematical Functions

Mathematical functions are included in the Math class, which is part of the System namespace. You don't therefore need to add any references to your project to make use of these functions.

The Math class basically contains a large number of static functions to perform operations such as calculating sines, logarithms etc. It also contains two static members, E and PI, which respectively return the values of the mathematical constants e and p. The class is quite basic: it contains all the important mathematical functions, but not much more. There is at present no support for, for example complex numbers or vector or matrix operations. Also the function names will probably annoy many mathematicians since their names often don't follow normal usage as far as case is concerned.

We'll demonstrate the use of this class with a simple application, Math, which displays the values of e and p and also the sines of angles between 0 and 90 degrees. The code that we add to the constructor to the main form class looks like this

AddItem("e is " + Math.E);
      AddItem("pi is " + Math.PI);
      AddItem("");
      AddItem("sines of angles from 0 to 90 degrees:");

      // display sines. Note we must convert from degrees to radians.
      for (double theta=0 ; theta<90.1 ; theta+=22.5)
         AddItem("sin " + theta + " = " + Math.Sin(theta*Math.PI/180));

Note in this code that we need to convert degrees to radians by multiplying by (p/180) since all the math trigonometric functions work in radians.

This code produces this output:

Figure 17
Figure 17

The main mathematical functions include the following:

Sin, Cos, Tan sine, cosine and tangent of an angle in radians
Asin, Acos, Atan Inverse trigonometric functions
Atan2 Inverse tangent, with x and y coordinates specified in order to find the angle of the vector between (0,0) and (x,y).
Sinh, Cosh, Tanh Hyperbolic sine, cosine and tangent. Note that no corresponding inverse functions are provided.
Sqrt square root
Pow number raised to a given power
Exp exponential
Log natural (base e) logarithm
Log10 base 10 logarithm
Abs absolute value of a number
Log lower of two numbers
Max natural (base e) logarithm
Sign whether a number is positive or negative

Note that these mathematical functions all take double as the parameter and return a double, apart from Abs, Max, Min and Sign, which returns 1, 0 or -1 - these functions make sense for any numeric type, for example it is equally valid to take the absolute value of an integer or a floating point value, and so Microsoft have provided overloaded functions that take different .NET numeric types.

Summary

In this chapter we've explained the concept of a namespace, and then gone on a quick tour of some of the functionality available via the .NET SDK base classes. Hopefully these examples will have shown you that the base classes make it very easy to carry out a lot of Windows tasks. Some of these tasks were previously only available via the Win32 SDK, which was not only conceptually harder to program with, but also made it difficult to carry out those tasks from any language other than C++. With the new base classes these tasks can be accomplished with ease from any .NET-aware or COM-aware language.


Power your site with idr newswire

Contribute to IDR:

To contribute an article to IDR, a click here.

To contact us at IDevResource.com, use our feedback form, or email us.

To comment on the site contact our webmaster.

Promoted by CyberSavvy UK - website promotion experts

All content © Copyright 2000 IDevResource.com, Disclaimer notice

Visit the IDR Forums

WTL Architecture by Richard Grimes



Code Project

WTL Introduction