Chapter 16: Commonly Used Interfaces and Patterns


The recipes in this chapter show you how to implement patterns you will use frequently during the development of Microsoft .NET Framework applications. Some of these patterns are formalized using interfaces defined in the .NET Framework class library. Others are less rigid, but still require you to take specific approaches to the design and implementation of your types. The recipes in this chapter describe how to

  • Create serializable types that you can easily store to disk, send across the network, or pass by value across application domain boundaries (recipe 16.1).

  • Provide a mechanism that creates accurate and complete copies ( clones ) of objects (recipe 16.2).

  • Implement types that are easy to compare and sort (recipe 16.3).

  • Support the enumeration of the elements contained in custom collections (recipe 16.4).

  • Ensure that a type that uses unmanaged resources correctly releases those resources when they are no longer needed (recipe 16.5).

  • Display string representations of objects that vary based on format specifiers (recipe 16.6).

  • Correctly implement custom exception and event argument types, which you will use frequently in the development of your applications (recipes 16.7 and 16.8).

  • Implement the commonly used Singleton and Observer design patterns using the built-in features of C# and the .NET Framework class library (recipes 16.9 and 16.10).

16.1 Implement a Serializable Type

Problem

You need to implement a custom type that is serializable, allowing you to

  • Store instances of the type to persistent storage (for example, a file or a database).

  • Transmit instances of the type across a network.

  • Pass instances of the type "by value" across application domain boundaries.

Solution

For serialization of simple types, apply the attribute System.SerializableAttribute to the type declaration. For types that are more complex, or to control the content and structure of the serialized data, implement the interface System.Runtime.Serialization.ISerializable .

Discussion

Recipe 2.12 showed how to serialize and deserialize an object using the formatter classes provided with the .NET Framework class library. However, types aren't serializable by default. To implement a custom type that is serializable, you must apply the attribute SerializableAttribute to your type declaration. As long as all of the data fields in your type are serializable types, applying SerializableAttribute is all you need to do to make your custom type serializable. If you are implementing a custom class that derives from a base class, the base class must also be serializable.

Each formatter class contains the logic necessary to serialize types decorated with SerializableAttribute and will correctly serialize all public , protected , and private fields. This code excerpt shows the type and field declarations of a serializable class named Employee .

 using System; [Serializable] public class Employee {     private string name;     private int age;     private string address;          } 
Note  

Classes that derive from a serializable type don't inherit the attribute SerializableAttribute . To make derived types serializable, you must explicitly declare them as serializable by applying the SerializableAttribute attribute.

You can exclude specific fields from serialization by applying the attribute System.NonSerializedAttribute to those fields. As a rule, you should exclude the following fields from serialization:

  • Fields that contain nonserializable data types

  • Fields that contain values that might be invalid when the object is deserialized, for example, database connections, memory addresses, thread IDs, and unmanaged resource handles

  • Fields that contain sensitive or secret information, for example, passwords, encryption keys, and the personal details of people and organizations

  • Fields that contain data that is easily re-creatable or retrievable from other sources ” especially if the data is large

If you exclude fields from serialization, you must implement your type to compensate for the fact that some data won't be present when an object is deserialized. Unfortunately, you can't create or retrieve the missing data fields in an instance constructor because formatters don't call constructors during the process of deserializing objects. The most common solution to this problem is to implement the "Lazy Initialization" pattern, in which your type creates or retrieves data the first time it's needed.

The following code shows a modified version of the Employee class with NonSerializedAttribute applied to the address field, meaning that a formatter won't serialize the value of this confidential field. The Employee class implements public properties to give access to each of the private data members , providing a convenient place in which to implement lazy initialization of the address field.

 using System; [Serializable] public class Employee {     private string name;     private int age;     [NonSerialized]     private string address;         // Simple Employee constructor     public Employee(string name, int age, string address) {              this.name = name;         this.age = age;         this.address = address;     }                 // Public property to provide access to employee's name     public string Name {         get { return name; }         set { name = value; }     }     // Public property to provide access to employee's age     public int Age {         get { return age; }         set { age = value; }     }          // Public property to provide access to employee's address.     // Uses lazy initialization to establish address because     // a deserialized object will not have an address value.     public string Address {         get {             if (address == null) {                 // Load the address from persistent storage                 ;<$VE>             }                         return address;         }                  set {             address = value;         }     }  } 

For the majority of custom types, use of the attributes SerializableAttribute and NonSerializedAttribute will be sufficient to meet your serialization needs. If you require more control over the serialization process, you can implement the interface ISerializable . The formatter classes use different logic when serializing and deserializing instances of types that implement ISerializable . To implement ISerializable correctly you must

  • Declare that your type implements ISerializable .

  • Apply the attribute SerializableAttribute to your type declaration as just described; do not use NonSerializedAttribute because it will have no effect.

  • Implement the ISerializable.GetObjectData method (used during serialization), which takes the following argument types:

    • System.Runtime.Serialization.SerializationInfo

    • System.Runtime.Serialization.StreamingContext

  • Implement a nonpublic constructor (used during deserialization) that accepts the same arguments as the GetObjectData method. Remember, if you plan to derive classes from your serializable class, make the constructor protected .

  • If creating a serializable class from a base class that also implements ISerializable , your type's GetObjectData method and deserialization constructor must call the equivalent method and constructor in the parent class.

During serialization, the formatter calls the GetObjectData method and passes it SerializationInfo and StreamingContext references as arguments. Your type must populate the SerializationInfo object with the data you want to serialize. The SerializationInfo class provides the AddValue method that you use to add each data item. With each call to AddValue , you must specify a name for the data item ”you use this name during deserialization to retrieve each data item. The AddValue method has 16 overloads that allow you to add different data types to the SerializationInfo object.

The StreamingContext object provides information about the purpose and destination of the serialized data, allowing you to choose which data to serialize. For example, you might be happy to serialize secret data if it's destined for another application domain in the same process, but not if the data will be written to a file.

When a formatter deserializes an instance of your type, it calls the deserialization constructor, again passing a SerializationInfo and a StreamingContext reference as arguments. Your type must extract the serialized data from the SerializationInfo object using one of the SerializationInfo.Get* methods , for example, GetString , GetInt32 , or GetBoolean . During deserialization, the StreamingContext object provides information about the source of the serialized data, allowing you to mirror the logic you implemented for serialization.

Note  

During standard serialization operations, the formatters don't use the capabilities of the StreamingContext object to provide specifics about the source, destination, and purpose of serialized data. However, if you wish to perform customized serialization, your code can configure the formatter's StreamingContext object prior to initiating serialization and deserialization. Consult the .NET Framework SDK documentation for details of the StreamingContext class.

This example shows a modified version of the Employee class that implements the ISerializable interface. In this version, the Employee class doesn't serialize the address field if the provided StreamingContext object specifies that the destination of the serialized data is a file. The full code for this example is contained in the file SerializableExample.cs in the sample code for this chapter. The SerializableExample.cs file also includes a Main method that demonstrates the serialization and deserialization of an Employee object.

 using System; using System.Runtime.Serialization; [Serializable] public class Employee : ISerializable {     private string name;     private int age;     private string address;          // Simple Employee constructor     public Employee(string name, int age, string address) {              this.name = name;         this.age = age;         this.address = address;     }                 // Constructor required to enable a formatter to deserialize an      // Employee object. You should declare the constructor private or at     // least protected to ensure it is not called unnecessarily.     private Employee(SerializationInfo info, StreamingContext context) {                  // Extract the name and age of the Employee, which will always be          // present in the serialized data regardless of the value of the         // StreamingContext         name = info.GetString("Name");         age = info.GetInt32("Age");                  // Attempt to extract the Employee's address and fail gracefully         // if it is not available          try {              address = info.GetString("Address");         } catch (SerializationException) {             address = null;         }     }     // Name, Age, and Address properties not shown          // Declared by the ISerializable interface, the GetObjectData method     // provides the mechanism with which a formatter obtains the object     // data that it should serialize     public void GetObjectData(SerializationInfo inf, StreamingContext con){                  // Always serialize the Employee's name and age.         inf.AddValue("Name", name);         inf.AddValue("Age", age);         // Don't serialize the Employee's address if the StreamingContext          // indicates that the serialized data is to be written to a file.         if ((con.State & StreamingContextStates.File) == 0) {             inf.AddValue("Address", address);         }     }         } 



C# Programmer[ap]s Cookbook
C# Programmer[ap]s Cookbook
ISBN: 735619301
EAN: N/A
Year: 2006
Pages: 266

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net