English

How To: Implement IPersistStream in a .NET class

Summary

When writing customized objects, many cases need to support serialization. For example, a custom symbol or element needs to be saved with the map document and load when the map document is opened. This functionality is available by implementing the interface IPersistVariant.

To support cloning through serialization, temporarily save the object to an ObjectSteam and then duplicate the object by creating a new instance of the class and loading its properties from the temporary ObjectStream. use an ObjectCopy class, which uses ObjectStream internally. This class requires that the cloned object, or clonee, support IPersistStream.

IPersistStream is a Microsoft interface which provides methods for saving and loading objects that use a simple serial stream for their storage needs. In order to use the structured stream given by these methods using .NET, the objects must be converted into a byte array.

There are several ways to convert objects into byte arrays in .NET, however each method is object type specific. For objects that only have managed class members, use MemoryStream in conjunction with a BinaryFormatter, assuming that all of these members support serialization. The reality is that the object has different types of class members, including both managed and unmanaged types, i.e., ArcObjects components. This makes it a challenge to implement the IPersistStream .Save() and IPersistStream.Load() methods. Writing this code as a part of the custom object can also be cumbersome and difficult to read and maintain.

The solution is to write a helper static class with helper methods Save() and Load () to delegate the calls when implementing IPersistStream in an object.

Procedure

Implementing PersistStream helper class
The idea behind a helper class is that each object passed to it should eventually become serializeable. This means that all managed objects must support serialization and that unmanaged objects must be converted into a managed serializable type. In the case of ArcObjects, the solution is to use XMLSerialize in order to write the ArcObjects unmanaged class members into an xml stream, and then convert this stream into a simple string which is serializabe.

All the code in this article are from the Implementing persistence sample. See the link in the Related Information section below.

Note:
Some class members should not be directly copied, for example window handles (hWnd), device contexts (hDC), file handles, and Graphical Device Interface (GDI).


Warning:
The following code uses unsafe code since interface IStream requires usage of pointers and therefore only given in C#.

  1. Implementing method PersistStreamHelp.Save()

    Convert ArcObjects types into a string. Use XMLWriter in order to serialize the ArcObject into an XMLStream. This is done by an XMLSerializer, which writes the object to the XMLStream. Note the name given to the object, as well as the namespace which identify the object as an ArcObject.

    Code:
    if (Marshal.IsComObject(data))
    {
    //*** Create XmlWriter ***
    IXMLWriter xmlWriter = new XMLWriterClass();

    //*** Create XmlStream ***
    IXMLStream xmlStream = new XMLStreamClass();

    //*** Write the object to the stream ***
    xmlWriter.WriteTo(xmlStream as IStream);

    //*** Serialze object ***
    IXMLSerializer xmlSerializer = new XMLSerializerClass();
    xmlSerializer.WriteObject(xmlWriter, null, null, "arcobject", "http://www.esri.com/schemas/ArcGIS/9.2", data);

  2. Convert the XMLStream into a string.

    Code:
    string str = xmlStream.SaveToString();
    data = (object)str;
    if (null == data)
    return;

  3. Write the objects into a managed MemoryStream. This section is common to all types, so the condition must be that all input objects must support serialization.

    Code:
    //make sure that the object is serializable
    if (!data.GetType().IsSerializable)
    throw new Exception("Object is not serializable.");

    // Convert the string into a byte array
    MemoryStream memoryStream = new MemoryStream();
    BinaryFormatter binaryFormatter = new BinaryFormatter();
    binaryFormatter.Serialize(memoryStream, data);

  4. Convert the MemoryStream into a byte array and close the MemoryStream.

    Code:
    byte[] bytes = memoryStream.ToArray();
    memoryStream.Close();

  5. Get the length of the bytes array of the object and store this information as a byte array.

    Note:
    This is very important, since when reading objects from the structured stream in .NET, the size of the object must be specified in bytes. The length of the byte array is given as an integer therefore converting this information into a byte array always results in an array of 4 bytes. When reading the object from the structured stream, first read the four bytes specifying the length of the object in bytes and only then read the actual object from the stream.


    Code:
    // Get Byte Length
    byte[] arrLen = BitConverter.GetBytes(bytes.Length);

  6. Write the object length and the object's byte array to the structured stream.

    Code:
    // Get Memory Pointer to Int32
    int cb;
    int* pcb = &cb;

    // Write Byte Length
    stream.Write(arrLen, arrLen.Length, new IntPtr(pcb));
    // Write Btye Array
    stream.Write(bytes, bytes.Length, new IntPtr(pcb));

  7. Verify that the object's byte array was written successfully to the stream. When calling method IStream.Write(), the returning pointer store the number of bytes written. Make sure that the numbers of bytes written to the stream match the number of bytes of the object.

    Code:
    if (bytes.Length != cb)
    throw new Exception("Error writing object to stream");

  8. Implementing method PersistStreamHelp.Load()

    Reading the object from the structured stream is the opposite technique from writing. First, read the object's length and then read the object's byte array. Use a BinaryFormatter in order to deserialize the object and in the case of an ArcObject, use XMLSerializer in order to re-hydrate the object.

    Get the size of the object’s byte array. This information was written to the structured stream as four bytes array.

    Code:
    // Get Pointer to Int32
    int cb;
    int* pcb = &cb;

    // Get Size of the object's Byte Array
    byte[] arrLen = new Byte[4];

    stream.Read(arrLen, arrLen.Length, new IntPtr(pcb));
    cb = BitConverter.ToInt32(arrLen, 0);

  9. Allocate a new byte array in the size of the read on the previous step.

    Code:
    // Read the object's Byte Array
    byte[] bytes = new byte[cb];
    stream.Read(bytes, cb, new IntPtr(pcb));

  10. Verify that the number of bytes read match the size of the object.

    Code:
    if (bytes.Length != cb)
    throw new Exception("Error reading object from stream");

  11. Use MemoryStream in conjunction with a BinaryFomatter in order to deserialize the object.

    Code:
    // Deserialize byte array
    object data = null;
    MemoryStream memoryStream = new MemoryStream(bytes);
    BinaryFormatter binaryFormatter = new BinaryFormatter();
    object objectDeserialize = binaryFormatter.Deserialize(memoryStream);
    if (objectDeserialize != null)
    {
    data = objectDeserialize;
    }
    memoryStream.Close();

  12. In case that the object is a string, verify if it is an xml string of an ArcObject. In this case, use XMLSerializer in order to get an instance of the ArcObject.
    Code:
    //deserialize arcobjects
    if (data is string)
    {
    string str = (string)data;
    if (str.IndexOf("http://www.esri.com/schemas/ArcGIS/9.2") != -1)
    {
    IXMLStream readerStream = new XMLStreamClass();
    readerStream.LoadFromString(str);

    IXMLReader xmlReader = new XMLReaderClass();
    xmlReader.ReadFrom((IStream)readerStream);

    IXMLSerializer xmlReadSerializer = new XMLSerializerClass();
    object retObj = xmlReadSerializer.ReadObject(xmlReader, null, null);
    if (null != retObj)
    data = retObj;
    }
    }

  13. Return the object.

    Code:
    return data;

  14. Using the PersistStream helper class in your object

    For example, an object has different types of class members, and must support IPersistStream. These members are simple types as strings, doubles, integers. Other managed types include DataTables and ArrayLists and in addition ArcObjects members such as geometries (e.g. IPoint) and spatial reference (ISpatialReference).

    Code:
    //class members
    private int m_version = 1;
    private ISpatialReference m_spatialRef = null;
    private IPoint m_point = null;
    private string m_name = string.Empty;
    private ArrayList m_arr = null;
    private Guid m_ID;

    For each of the managed class member that to serialize, verify first that it supports serialization. Do that by opening MSDN and see if the object has the 'SerialisableAttribute' in its definition. Here is an example Guid member:
    [O-Image] example Guid member
    The 'SerialisableAttribute' confirms that the object is serializable and therefore is safe to use. With ArcObjects, make sure that the object does not reference non-serializabe properties such as Windows handles, device contexts etc., such as IScreenDisplay.

    With the exception of the IPersistStream.Save() and IPersist.Load()methods, make sure that the method IPersistStream .GetClassID() returns a valid Guid of the class:

    Code:
    public void GetClassID(out Guid pClassID)
    {
    pClassID = new Guid(ClonableObjClass.GUID);
    }

  15. Implementing IPersistStream.Save() and IPersistStream.Load()

    In both methods, cast the input ESRI.ArcGIS.IStream into System.Runtime.InteropServices.Comtypes.IStream and then use the helper methods in order to save or load the class members.

    Code:
    public void Save(IStream pStm, int fClearDirty)
    {
    //cast the ESRI.ArcGIS.IStream
    System.Runtime.InteropServices.ComTypes.IStream stream = (System.Runtime.InteropServices.ComTypes.IStream)pStm;

    //save the different objects to the stream
    PeristStream.PeristStreamHelper.Save(stream, m_version);
    PeristStream.PeristStreamHelper.Save(stream, m_ID.ToByteArray());
    PeristStream.PeristStreamHelper.Save(stream, m_name);
    PeristStream.PeristStreamHelper.Save(stream, m_spatialRef);

    //save the guid
    PeristStream.PeristStreamHelper.Save(stream, m_ID);

    //save the point to the stream
    if (null == m_point)
    m_point = new PointClass();

    PeristStream.PeristStreamHelper.Save(stream, m_point);

    if (null == m_arr)
    m_arr = new ArrayList();

    PeristStream.PeristStreamHelper.Save(stream, m_arr);
    }
    public void Load(IStream pStm)
    {
    // cast the ESRI.ArcGIS.IStream
    System.Runtime.InteropServices.ComTypes.IStream stream = (System.Runtime.InteropServices.ComTypes.IStream)pStm;

    //load the information from the stream
    object obj = null;
    obj = PeristStream.PeristStreamHelper.Load(stream);
    m_version = Convert.ToInt32(obj);

    obj = PeristStream.PeristStreamHelper.Load(stream);
    byte[] arr = (byte[])obj;
    m_ID = new Guid(arr);

    obj = PeristStream.PeristStreamHelper.Load(stream);
    m_name = Convert.ToString(obj);

    obj = PeristStream.PeristStreamHelper.Load(stream);
    m_spatialRef = obj as ISpatialReference;

    obj = PeristStream.PeristStreamHelper.Load(stream);
    m_ID = (Guid)obj;

    obj = PeristStream.PeristStreamHelper.Load(stream);
    m_point = obj as IPoint;

    obj = PeristStream.PeristStreamHelper.Load(stream);
    m_arr = obj as ArrayList;
    }

Related Information