CHAPTER 38

image

.NET Base Class Library Overview

The .NET Base Class Library (BCL) contains many functions normally found in language-specific runtime libraries. It is therefore important to understand what classes are available in the BCL.

Numeric Formatting

Numeric types are formatted through the Format() member function of that data type. This can be called directly, through String.Format(), which calls the Format() function of each data type, or through Console.WriteLine(), which calls String.Format().

Adding formatting to a user-defined object is discussed in the “Custom Object Formatting” section later in this chapter. This section discusses how formatting is done with the built-in types.

There are two methods of specifying numeric formatting. A standard format string can be used to convert a numeric type to a specific string representation. If further control over the output is desired, a custom format string can be used.

Standard Format Strings

A standard format string consists of a character specifying the format, followed by a sequence of digits specifying the precision. The formats in Table 38-1 are supported.

Table 38-1. Standard Format Strings

Format Character Description
C, c Currency
D, d Decimal
E, e Scientific (exponential)
F, f Fixed-point
G, g General
N, n Number
R, r Round-trip
X, x Hexadecimal

Currency

The currency format string converts the numerical value to a string containing a locale-specific currency amount. By default, the format information is determined by the current locale, but this may be changed by passing a NumberFormatInfo object. This example:

Console.WriteLine("{0:C}", 33345.8977);
Console.WriteLine("{0:C}", -33345.8977);

gives the following output:

$33,345.90
($33,345.90)

An integer following the C specifies the number of decimal places to use; two places are used if the integer is omitted.

Decimal

The decimal format string converts the numerical value to an integer. The minimum number of digits is determined by the precision specifier. The result is left-padded with zeroes to obtain the required number of digits. This example:

Console.WriteLine("{0:D}", 33345);
Console.WriteLine("{0:D7}", 33345);

gives the following output:

33345
0033345

Scientific (Exponential)

The scientific (exponential) format string converts the value to a string in the following form:

m.dddE + xxx

One digit always precedes the decimal point, and the number of decimal places is specified by the precision specifier, with six places used as the default. The format specifier controls whether E or e appears in the output. This example:

Console.WriteLine("{0:E}", 33345.8977);
Console.WriteLine("{0:E10}", 33345.8977);
Console.WriteLine("{0:e4}", 33345.8977);

gives the following output:

3.334590E + 004
3.3345897700E + 004
3.3346e + 004

Fixed-Point

The fixed-point format string converts the value to a string, with the number of places after the decimal point specified by the precision specifier. Two places are used if the precision specifier is omitted. This example:

Console.WriteLine("{0:F}", 33345.8977);
Console.WriteLine("{0:F0}", 33345.8977);
Console.WriteLine("{0:F5}", 33345.8977);

gives the following output:

33345.90
33346
33345.89770

General

The general format string converts the value to either a fixed-point or scientific format, whichever one gives a more compact format. This example:

Console.WriteLine("{0:G}", 33345.8977);
Console.WriteLine("{0:G7}", 33345.8977);
Console.WriteLine("{0:G4}", 33345.8977);

gives the following output:

33345.8977
33345.9
3.335E + 04

Number

The number format string converts the value to a number that has embedded commas, such as the following:

12,345.11

By default, the number is formatted with two digits to the right of the decimal point. This can be controlled by specifying the number of digits after the format specifier. This example:

Console.WriteLine("{0:N}", 33345.8977);
Console.WriteLine("{0:N4}", 33345.8977);

gives the following output:

33,345.90
33,345.8977

It’s possible to control the character used for the decimal point by passing a NumberFormatInfo object to the Format() function.

Round-Trip

Consider the following code:

class RoundTrip
{
    public static void Main()
    {
       Random random = new Random();
       while (true)
       {
            double value = random.NextDouble();
            if (value != ToStringAndBack(value))
            {
                Console.WriteLine("Different: {0}", value);
            }
       }
    }
    public static double ToStringAndBack(double value)
    {
       string valueAsString = value.ToString();
       return Double.Parse(valueAsString);
    }
}

The ToStringAndBack() method converts a double to a text value and then converts it back to a double. This is the same operation that might be occur when writing a double value to a file and then reading it back in again. Does this code write anything to the console?

The surprising answer is yes. The double type uses the IEEE 754 floating-point representation,1 and that representation stores nearly (but not quite) 16 decimal digits. When you convert a double to a string, the conversion routine will return only the 15 digits of precision that it is sure are correct, and the extra bits get lost.

If you need to preserve the exact number, you can use the R format.

string valueAsString = value.ToString("R");

This ensures that the value read back will be identical2 to the one that is saved.

image Note  If you are doing numerical calculations, this loss of precision can be important. For more information about this and other properties of floating-point numbers, search for what every computer scientist should know about floating-point arithmetic online.

Hexadecimal

The hexadecimal format string converts the value to hexadecimal format. The minimum number of digits is set by the precision specifier; the number will be zero-padded to that width.

Using X will result in uppercase letters in the converted value; x will result in lowercase letters. This example:

Console.WriteLine("{0:X}", 255);
Console.WriteLine("{0:x8}", 1456);

gives the following output:

FF
000005b0

NumberFormatInfo

The NumberFormatInfo class is used to control the formatting of numbers. By setting the properties in this class, the programmer can control the currency symbol, decimal separator, and other formatting properties.

Custom Format Strings

Custom format strings are used to obtain more control over the conversion than is available through the standard format strings. In custom format strings, special characters form a template that the number is formatted into. Any characters that do not have a special meaning in the format string are copied verbatim to the output. Table 38-2 describes the custom strings available.

Table 38-2. Custom Format Strings

Character Description Result
0 Display zero placeholder Displays leading zero if a number has fewer digits than there are zeroes in the format.
# Display digit placeholder Replaces # with the digit only for significant digits.
. Decimal point Displays the decimal point.
, Group separator and multiplier Separates number groups, such as 1,000. When used after a number, divides it by 1,000.
% Display % notation Displays the percent character.
; Section separator Uses different formats for positive, negative, and zero values.

Digit or Zero Placeholder

The zero (0) character is used as a digit or zero placeholder. If the numeric value has a digit in the position at which the 0 appears in the format string, the digit will appear in the result. If not, a zero appears in that position. This example:

Console.WriteLine("{0:000}", 55);
Console.WriteLine("{0:000}", 1456);

gives the following output:

055
1456

Digit or Space Placeholder

The pound (#) character is used as the digit or space placeholder. It works exactly the same as the 0 placeholder, except that the character is omitted if there is no digit in that position. This example:

Console.WriteLine("{0:#####}", 255);
Console.WriteLine("{0:#####}", 1456);
Console.WriteLine("{0:###}", 32767);

gives the following output:

255
1456
32767

Decimal Point

The first period (.) character that appears in the format string determines the location of the decimal separator in the result. The character used as the decimal separator in the formatted string is controlled by a NumberFormatInfo instance. This example:

Console.WriteLine("{0:#####.000}", 75928.3);
Console.WriteLine("{0:##.000}", 1456.456456);

gives the following output:

75928.300
1456.456

Group Separator

The comma (,) character is used as a group separator. If a comma appears in the middle of a display digit placeholder and to the left of the decimal point (if present), a group separator will be inserted in the string. The character used in the formatted string and the number of numbers to group together is controlled by a NumberFormatInfo instance. This example:

Console.WriteLine("{0:##,###}", 2555634323);
Console.WriteLine("{0:##,000.000}", 14563553.593993);
Console.WriteLine("{0:#,#.000}", 14563553.593993);

gives the following output:

2,555,634,323
14,563,553.594
14,563,553.594

Number Prescaler

The comma (,) character can also be used to indicate that the number should be prescaled. In this usage, the comma must come directly before the decimal point or at the end of the format string.

For each comma present in this location, the number is divided by 1,000 before it is formatted. This example:

Console.WriteLine("{0:000,.##}", 158847);
Console.WriteLine("{0:000,,,.###}", 1593833);

gives the following output:

158.85
000.002

Percent Notation

The percent (%) character is used to indicate that the number to be displayed should be displayed as a percentage. The number is multiplied by 100 before it is formatted. This example:

Console.WriteLine("{0:##.000 %}", 0.89144);
Console.WriteLine("{0:00 %}", 0.01285);

gives the following output:

89.144 %
01 %

Exponential Notation

When E + 0, E-0, e + 0, or e-0 appears in the format string directly after a # or 0 placeholder, the number will be formatted in exponential notation. The number of digits in the exponent is controlled by the number of 0 placeholders that appear in the exponent specifier. The E or e is copied directly into the formatted string, and a + means that there will be a plus or minus sign in that position, while a minus sign means there is a character there only if the number is negative. This example:

Console.WriteLine("{0:###.000E-00}", 3.1415533E + 04);
Console.WriteLine("{0:#.0000000E + 000}", 2.553939939E + 101);

gives the following output:

314.155E-02
2.5539399E + 101

Section Separator

The semicolon (;) character is used to specify different format strings for a number, depending on whether the number is positive, negative, or zero. If there are only two sections, the first section applies to positive and zero values, and the second applies to negative values. If there are three sections, they apply to positive values, negative values, and the zero value. This example:

Console.WriteLine("{0:###.00;0;(###.00)}", -456.55);
Console.WriteLine("{0:###.00;0;(###.00)}", 0);
Console.WriteLine("{0:###.00;0;(###.00)}", 456.55);

gives the following output:

457
(.00)
456.55

Escapes and Literals

The slash () character can be used to escape characters so they aren’t interpreted as formatting characters. Because the slash already has meaning within C# literals, it will be easier to specify the string using the verbatim literal syntax; otherwise, a double slash (\) is required to generate a single slash in the output string.

A string of uninterpreted characters can be specified by enclosing them in single quotes; this may be more convenient than using the slash character. This example:

Console.WriteLine("{0:###\#}", 255);
Console.WriteLine(@"{0:####}", 255);
Console.WriteLine("{0:###'#0 %;'}", 1456);

gives the following output:

255#
255#
1456#0 %;

Date and Time Formatting

The DateTime class provides flexible formatting options. Several single-character formats can be specified, and custom formatting is also supported. Table 38-3 specifies the standard DateTime formats.

Table 38-3. Date and Time Formats

Character Pattern Description
D MM/dd/yyyy ShortDatePattern
D dddd, MMMM dd, yyy LongDatePattern
F dddd, MMMM dd, YYYY HH:mm Full (long date + short time)
F dddd, MMMM dd, yyyy HH:mm:ss FullDateTimePattern (long date + long time)
G MM/dd/yyyy HH:mm General (short date + short time)
G MM/dd/yyyy HH:mm:ss General (short date + long time)
m, M MMMM dd MonthDayPattern
r, R ddd, dd MMM yy HH':'mm':'ss 'GMT' RFC1123Pattern
S yyyy-MM-dd HH:mm:ss SortableDateTimePattern (ISO 8601)
S YYYY-mm-DD hh:MM:SS GMT Sortable with time zone information
T HH:mm ShortTimePattern
T HH:mm:ss LongTimePattern
U yyyy-MM-dd HH:mm:ss Same as s, but with universal instead of local time
U dddd, MMMM dd, yyyy HH:mm:ss UniversalSortableDateTimePattern

Custom DateTime Format

Table 38-4 lists the patterns that can be used to build a custom format.

Table 38-4. Custom Date Formats

Pattern Description
D Day of month as digits with no leading zero for single-digit days
Dd Day of month as digits with leading zero for single-digit days
Ddd Day of week as a three-letter abbreviation
dddd Day of week as its full name
M Month as digits with no leading zero for single-digit months
MM Month as digits with leading zero
MMM Month as three-letter abbreviation
MMMM Month as its full name
Y Year as last two digits, no leading zero
Yy Year as last two digits, with leading zero
Yyyy Year represented by four digits

The day and month names are determined by the appropriate field in the DateTimeFormatInfo class.

Custom Object Formatting

Earlier examples have overridden the ToString() function to provide a string representation of a function. An object can supply different formats by defining the IFormattable interface and then changing the representation based upon the string of the function.

For example, an employee class could add additional information with a different format string. This example:

using System;
class Employee: IFormattable
{
    public Employee(int id, string firstName, string lastName)
    {
       m_id = id;
       m_firstName = firstName;
       m_lastName = lastName;
    }
    public string ToString (string format, IFormatProvider fp)
    {
       if ((format != null) && (format.Equals("F")))
       {
            return String.Format("{0}: {1}, {2}",
                m_id, m_lastName, m_firstName);
       }
       else
       {
            return(m_id.ToString(format, fp));
       }
    }
    int m_id;
    string m_firstName;
    string m_lastName;
}
class Test
{
    public static void Main()
    {
       Employee fred = new Employee(123, "Fred", "Morthwaite");
       Console.WriteLine("No format: {0}", fred);
       Console.WriteLine("Full format: {0:F}", fred);
    }
}

produces the following output:

No format: 123
Full format: 123: Morthwaite, Fred

The ToString() function looks for the F format. If it finds it, it writes out the full information. If it doesn’t find it, it uses the default format for the object.

The Main() function passes the format flag in the second WriteLine() call.

Numeric Parsing

Numbers are parsed using the Parse() method provided by the numeric data types. Flags from the NumberStyles class can be passed to specify which styles are allowed, and a NumberFormatInfo instance can be passed to control parsing.

A numeric string produced by any of the standard format specifiers (excluding hexadecimal) is guaranteed to be correctly parsed if the NumberStyles.Any style is specified. This example:

using System;
class Test
{
    public static void Main()
    {
       int value = Int32.Parse("99953");
       double dval = Double.Parse("1.3433E + 35");
       Console.WriteLine("{0}", value);
       Console.WriteLine("{0}", dval);
    }
}

produces the following output:

99953
1.3433E35

Using TryParse to Avoid Exceptions

If there is a reasonable chance that the input string contains invalid characters, which means that Parse() will be unable to convert to the appropriate type and throw an exception, the TryParse() method should be used instead. Rather than throwing an exception if the input cannot be successfully converted, TryParse() instead returns a boolean that indicates the success of the conversion, with the result of the conversion returned as an out parameter.

Console.WriteLine("Please enter an integer and press Enter");
int numberEntered;
while(!int.TryParse(Console.ReadLine(), out numberEntered))
{
    Console.WriteLine("Please try again");
}
Console.WriteLine("You entered " + numberEntered.ToString());

Input and Output

The .NET Common Language Runtime provides I/O functions in the System.IO namespace. This namespace contains classes for doing I/O and for other I/O-related functions, such as directory traversal, file watching, and so on.

Reading and writing are done using the Stream class, which merely describes how bytes can be read and written to some sort of backing store. Stream is an abstract class, so in practice classes derived from Stream will be used. Table 38-5 lists the available stream classes.

Table 38-5. Basic .NET Stream Classes

Class Description
FileStream A stream on a disk file
MemoryStream A stream that is stored in memory
NetworkStream A stream on a network connection
BufferedStream Implements a buffer on top of another stream
GZipStream A stream that can compress or decompress data passing through it using GZip (RFC 1952)
DeflateStream A stream that can compress or decompress data passing through it using LZW77 (RFC 1951)

With the exception of BufferedStream, GZipStream, and DeflateStream, which sit on top of another stream, each stream defines where the written data will go.

The Stream class provides raw functions to read and write at a byte level, both synchronously and asynchronously. Usually, however, it’s nice to have a higher-level interface on top of a stream, and there are several supplied ones that can be selected depending on what final format is desired.

Binary

The BinaryReader and BinaryWriter classes are used to read and write values in binary (or raw) format. For example, a BinaryWriter can be used to write an int, followed by a float, followed by another int. These classes operate on a stream.

Text

The TextReader and TextWriter abstract classes define how text is read and written. They allow operations on characters, lines, blocks, and so on. Two different implementations of TextReader are available.

The somewhat strangely named StreamWriter class is the one used for “normal” I/O (open a file, read the lines out) and operates on a Stream.

The StringReader and StringWriter classes can be used to read and write from a string.

XML

The XmlTextReader and XmlTextWriter classes are used to read and write XML. They are similar to TextReader and TextWriter in design, but they do not derive from those classes because they deal with XML entities rather than text. They are low-level classes used to create or decode XML from scratch. See Chapter 29 for more information on dealing with XML.

Serial Ports

The serial ports on a machine can be accessed using the SerialPort class in the System.IO.Ports namespace. The SerialPort class allows properties such as baud rate, parity, and timeouts to be set. SerialPort has methods that provide direct access to the data that is flowing through the port, and it also supports stream-based access so helper streams like BufferedStream or asynchronous operations can be used.

This sample shows both the direct and stream-based approaches:

using System.IO.Ports;
...
byte[] buffer = new byte[256];
using (SerialPort sp = new SerialPort("COM1", 19200))
{
    sp.Open();
    //read directly
    sp.Read(buffer, 0, (int)buffer.Length);
    //read using a Stream
    sp.BaseStream.Read(buffer, 0, (int)buffer.Length);}

Writing Files

It is simple to create a file using the File and StreamWriter3 classes.

using (StreamWriter writer = File.CreateText("output.txt"))
{
    writer.WriteLine("{0} {1}", "test", 55);
}

Reading Files

It is simple to read a file; this time you will use the File and StreamReader classes.

using (StreamReader reader = File.OpenText("output.txt"))
{
    string line = reader.ReadLine();
    Console.WriteLine(line);
}

Traversing Directories

This example shows how to traverse a directory structure. It defines both a DirectoryWalker class that takes delegates to be called for each directory and file and a path to traverse.

public static class DirectoryWalker
{
    public static void DoWalk(
       Action <DirectoryInfo, int> directoryCallback,
       Action <FileInfo, int> fileCallback,
       string rootDirectory)
    {
       DoWalk(
            directoryCallback,
            fileCallback,
            new DirectoryInfo(rootDirectory),
            0);
    }
    static void DoWalk(
       Action <DirectoryInfo, int> directoryCallback,
       Action <FileInfo, int> fileCallback,
       DirectoryInfo dir,
       int level)
    {
       foreach (FileInfo file in dir.EnumerateFiles())
       {
            if (fileCallback != null)
            {
                fileCallback(file, level);
            }
       }
       foreach (DirectoryInfo directory in dir.EnumerateDirectories())
       {
            if (directoryCallback != null)
            {
                directoryCallback(directory, level);
            }
            DoWalk(directoryCallback, fileCallback, directory, level + 1);
       }
    }
}
public class DirectoryWalkerTest
{
    public static void PrintDir(DirectoryInfo d, int level)
    {
       Console.WriteLine(new string(' ', level * 2));
       Console.WriteLine("Dir: {0}", d.FullName);
    }
    public static void PrintFile(FileInfo f, int level)
    {
       Console.WriteLine(new string(' ', level * 2));
       Console.WriteLine("File: {0}", f.FullName);
    }
    public static void Main()
    {
       DirectoryWalker.DoWalk(
            PrintDir,
            PrintFile,
            "..");
    }
}

Starting Processes

The .NET BCL provides the Process class, which is used to start processes. The following example shows how to start Notepad:

using System.Diagnostics;
class Test
{
    public static void Main()
    {
       ProcessStartInfo startInfo = new ProcessStartInfo();
       startInfo.FileName = "notepad.exe";
       startInfo.Arguments = "process.cs";
       Process.Start(startInfo);
    }
}

The arguments used in starting the process are contained in the ProcessStartInfo object.

Redirecting Process Output

Sometimes it’s useful to get the output from a process. This can be done with the following code:

using System;
using System.Diagnostics;
class Test
{
    public static void Main()
    {
       Process process = new Process();
       process.StartInfo.FileName = "cmd.exe";
       process.StartInfo.Arguments = "/c dir *.cs";
       process.StartInfo.UseShellExecute = false;
       process.StartInfo.RedirectStandardOutput = true;
       process.Start();
       string output = process.StandardOutput.ReadToEnd();
       Console.WriteLine("Output:");
       Console.WriteLine(output);
    }
}

Detecting Process Completion

It’s also possible to detect when a process exits.

using System;
using System.Diagnostics;
class Test
{
    static void ProcessDone(object sender, EventArgs e)
    {
       Console.WriteLine("Process Exited");
    }
    public static void Main()
    {
       Process process = new Process();
       process.StartInfo.FileName = "notepad.exe";
       process.StartInfo.Arguments = "process3.cs";
       process.EnableRaisingEvents = true;
       process.Exited += ProcessDone;
       process.Start();
       process.WaitForExit();
       Console.WriteLine("Back from WaitForExit()");
    }
}

This example shows two different ways of detecting process completion. The ProcessDone() function is called when the Exited event is fired, and the WaitForExit() function also returns when the process is done.

Serialization

Serialization is the process used by the runtime to persist objects in some sort of storage or to transfer them from one location to another.

The metadata information on an object contains sufficient information for the runtime to serialize the fields, but the runtime needs a little help to do the right thing.

This help is provided through two attributes. The [Serializable] attribute is used to mark an object as OK to serialize. The [NonSerialized] attribute can be applied to a field or property to indicate that it shouldn’t be serialized. This is useful if it is a cache or derived value.

The following example has a container class named MyRow that has elements of the MyElement class. The cacheValue field in MyElement is marked with the [NonSerialized] attribute to prevent it from being serialized.

In this example, the MyRow object is serialized and deserialized to a binary format:

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
[Serializable]
public class MyElement
{
    public MyElement(string name)
    {
       m_name = name;
       m_cacheValue = 15;
    }
    public override string ToString()
    {
       return String.Format("{0}: {1}", m_name, m_cacheValue);
    }
    string m_name;
    // this field isn't persisted.
    [NonSerialized]
    int m_cacheValue;
}
[Serializable]
public class MyRow
{
    public void Add(MyElement myElement)
    {
       m_elements.Add(myElement);
    }
    public override string ToString()
    {
       return String.Join(
            " ",
            m_elements
                .Select(element => element.ToString())
                .ToList());
    }
    List <MyElement> m_elements = new List <MyElement> ();
}
public class SerializationTest
{
    public static void Main()
    {
       MyRow row = new MyRow();
       row.Add(new MyElement("Gumby"));
       row.Add(new MyElement("Pokey"));
       Console.WriteLine("Initial value");
       Console.WriteLine("{0}", row);
       // write to binary, read it back
       using (Stream streamWriter = File.Create("MyRow.bin"))
       {
            BinaryFormatter binaryWriter = new BinaryFormatter();
            binaryWriter.Serialize(streamWriter, row);
       }
       MyRow rowBinary = null;
       using (Stream streamReader = File.OpenRead("MyRow.bin"))
       {
            BinaryFormatter binaryReader = new BinaryFormatter();
            rowBinary = (MyRow)binaryReader.Deserialize(streamReader);
       }
       Console.WriteLine("Values after binary serialization");
       Console.WriteLine("{0}", rowBinary);
    }
}

The example produces the following output:

Initial value
Gumby: 15
Pokey: 15
Values after binary serialization
Gumby: 0
Pokey: 0

The field cacheValue is not preserved since it was marked as [NonSerialized]. The file MyRow.Bin will contain the binary serialization. To serialize to XML, use the XmlSerializer class.

Custom Serialization

If the standard serialization doesn’t do exactly what is desired or doesn’t give sufficient control, a class can define exactly how it wants to be serialized,4 like in this example:

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
[Serializable]
class Employee : ISerializable
{
    int m_id;
    string m_name;
    string m_address;
    public Employee(int id, string name, string address)
    {
       m_id = id;
       m_name = name;
       m_address = address;
    }
    public override string ToString()
    {
       return (String.Format("{0} {1} {2}", m_id, m_name, m_address));
    }
    Employee(SerializationInfo info, StreamingContext content)
    {
       m_id = info.GetInt32("id");
       m_name = info.GetString("name");
       m_address = info.GetString("address");
    }
    // called to save the object data...
    public void GetObjectData(SerializationInfo info, StreamingContext content)
    {
       info.AddValue("id", m_id);
       info.AddValue("name", m_name);
       info.AddValue("address", m_address);
    }
}
class CustomSerializationTest
{
    public static void Serialize(Employee employee, string filename)
    {
       using (Stream streamWrite = File.Create(filename))
       {
            IFormatter writer = new BinaryFormatter();
            writer.Serialize(streamWrite, employee);
       }
    }
    public static Employee Deserialize(string filename)
    {
       Employee employee = null;
       using (Stream streamRead = File.OpenRead(filename))
       {
            IFormatter reader = new BinaryFormatter();
            employee = (Employee)reader.Deserialize(streamRead);
       }
       return (employee);
    }
    public static void Main()
    {
       Employee employee = new Employee(15, "Fred", "Bedrock");
       Serialize(employee, "emp.dat");
       Employee employeeBack = Deserialize("emp.dat");
       Console.WriteLine("Employee: {0}", employeeBack);
    }
}

To perform custom serialization, an object must implement the ISerializable interface. The GetObjectData() method is the only method on that interface. The implementation of that method stores each value by calling AddValue() on each value and passing in a name for the field and the field value.

To deserialize an object, the runtime relies on a special constructor. This constructor will call the appropriate get function to fetch a value based on the name.

Although this approach does take some extra space to store the names—and a bit of time to look them up—it versions very well, allowing new values to be added without invalidating existing stored files.

XML Serialization

The XmlSerializer class can be used to serialize an object to an XML representation. Here’s an example:

public class Employee
{
    public string Name { get; set; }
    public Decimal Salary { get; set; }
    public DateTime DateOfBirth { get; set; }
}
public static void SaveEmployee()
{
    Employee employee = new Employee();
    employee.Name = "Peter";
    employee.Salary = 15123M;
    employee.DateOfBirth = DateTime.Parse("12/31/1994");
    XmlSerializer serializer = new XmlSerializer(typeof(Employee));
    using (Stream writeStream = File.OpenWrite("Employee.xml"))
    {
       serializer.Serialize(writeStream, employee);
    }
}

This code will generate the following output:

<?xml version="1.0"?> 
  <Employee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" >
  <Name> Peter</Name>
  <Salary> 15123</Salary>
  <DateOfBirth> 1994-12-31T00:00:00</DateOfBirth>
</Employee>

Reading the object back in is equally simple.

public static Employee LoadEmployee()
{
    XmlSerializer serializer = new XmlSerializer(typeof(Employee));
    using (Stream readStream = File.OpenRead("Employee.xml"))
    {
       return (Employee) serializer.Deserialize(readStream);
    }
}

The XmlSerializer class provides a considerable amount of control over the format of the XML representation. Here’s an example:

[XmlType(TypeName = "Employee2005")]
public class Employee
{
    [XmlElement("FullName")]
    public string Name { get; set; }
    [XmlAttribute("Salary")]
    public Decimal Salary { get; set; }
    public DateTime DateOfBirth { get; set; }
    [XmlIgnore]
    public int Weight { get; set; }
}

The attributes have the following effects:

  • The XmlType attribute changes the name of the Employee element to Employee2005.
  • The XmlElement attribute changes the name of the Name property to FullName.
  • The XmlAttribute attribute saves the salary as an attribute instead of an element.
  • The XmlIgnore attribute skips the weight value during serialization.

Here is the generated XML:

<?xml version="1.0"? > 
  <Employee2005 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:xsd="http://www.w3.org/2001/XMLSchema" Salary="15123">
  <FullName> Peter</FullName>
  <DateOfBirth> 1994-12-31T00:00:00</DateOfBirth>
</Employee2005>

XmlSerializer or XElement?

Reading and writing XML can be done through using the XmlSerializer class or using the XElement class discussed in Chapter 29. Using XmlSerializer is generally a bit easier but may be harder to understand than the same code written using XElement. My recommendation is to use XmlSerializer if the destination of the data is a class that is easily expressed using the serialization attributes and to use XElement for the remainder of the scenarios.

Reading Web Pages

The following example demonstrates how to fetch a web page using C#5:

using System;
using System.Net;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
static class PageFetcher
{
    public static string Fetch(string url)
    {
       WebRequest req = WebRequest.Create(new Uri(url));
       WebResponse resp = req.GetResponse();
       string contents = null;
       using (Stream stream = resp.GetResponseStream())
       using (StreamReader reader = new StreamReader(stream))
       {
            contents = reader.ReadToEnd();
       }
       return (contents);
    }
}
class WebPageTest
{
    public static void Main()
    {
       string page = PageFetcher.Fetch("http://www.microsoft.com");
       Console.WriteLine(page);
    }
}

Accessing Environment Settings

The System.Environment class can be used to obtain information about the machine and environment, as the following example demonstrates:

using System;
using System.Collections;
class EnvironmentTest
{
    public static void Main()
    {
       Console.WriteLine("Command Line: {0}", Environment.CommandLine);
       Console.WriteLine("Current Directory: {0}", Environment.CurrentDirectory);
       Console.WriteLine("Machine Name: {0}", Environment.MachineName);
       Console.WriteLine("Environment Variables");
       foreach (DictionaryEntry var in Environment.GetEnvironmentVariables())
       {
            Console.WriteLine("    {0}={1}", var.Key, var.Value);
       }
    }
}

1 Actually, pretty much everything uses IEEE 754.

2 Identical means “has exactly the same bit pattern.”

3 At this point, you may be asking, “Why do I read and write text files using the StreamWriter class? Why don’t I use the TextWriter class?” Many people have asked that question, and the answer is that it’s just one of the few unfortunate naming choices in the framework.

4 If you’re familiar with how MFC serialization worked in Visual C++, this approach will seem fairly familiar.

5 See Chapter 35 for more information on this subject.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.116.43