Designer Serialization Overview

Caution

This content was written for .NET Framework. If you're using .NET 6 or a later version, use this content with caution. The designer system has changed for Windows Forms and it's important that you review the Designer changes since .NET Framework article.

With designer serialization, you can persist the state of your components at design time or run time.

Serialization for Objects

The .NET Framework supports several types of serialization, such as code generation, SOAP serialization, binary serialization, and XML serialization.

Designer serialization is a special form of serialization that involves the kind of object persistence usually associated with development tools. Designer serialization is the process of converting an object graph into a source file that can later be used to recover the object graph. A source file can contain code, markup, or even SQL table information. Designer serialization works for all common language runtime objects.

Designer serialization differs from typical object serialization in several ways:

  • The object performing the serialization is separate from the run-time object, so that design-time logic can be removed from a component.

  • The serialization scheme was devised under the assumption that the object will be created in a fully initialized state and then modified through property and method invocations during deserialization.

  • Properties of an object that have values that were never set on the object are not serialized. Conversely, the deserialization stream may not initialize all property values. For a more detailed description of serialization rules, see the "General Serialization Rules" section later in this topic.

  • Emphasis is placed on the quality of the content within the serialization stream, rather than the full serialization of an object. If there is no defined way to serialize an object, that object will be passed over rather than raising an exception. Designer serialization has ways to serialize an object in a simple, human-readable form, rather than as an opaque blob.

  • The serialization stream may have more data than is needed for deserialization. For example, source code serialization has user code mixed in with the code needed to deserialize an object graph. This user code must be preserved on serialization and passed over on deserialization.

Note

Designer serialization can be used at run time as well as design time.

The following table shows the design goals fulfilled with the .NET Framework designer serialization infrastructure.

Design goal

Description

Modular

The serialization process can be extended to cover new data types, and these data types can provide useful, human-readable descriptions of themselves.

Easily extensible

The serialization process can be extended to cover new data types easily.

Format-neutral

Objects can participate in many different file formats, and designer serialization is not tied to a particular data format.

Architecture

Designer serialization architecture is based on metadata, serializers, and a serialization manager. The following table describes of the role of each aspect of the architecture.

Aspect

Description

Metadata attributes

An attribute is used to relate a type T with some serializer S. Also, the architecture supports a "bootstrapping" attribute that can be used to install an object that can provide serializers for types that do not have them.

Serializers

A serializer is an object that can serialize a particular type or a range of types. There is a base class for each data format. For example, there may be a DemoXmlSerializer base class that can convert an object into XML. The architecture is independent of any specific serialization format, and it also includes an implementation of this architecture built on the Code Document Object Model (CodeDOM).

Serialization manager

A serialization manager is an object that provides an information store for all the various serializers that are used to serialize an object graph. A graph of 50 objects may have 50 different serializers that all generate their own output. The serialization manager is used by these serializers to communicate with each other.

The following illustration and procedure show how objects in a graph, in this case A and B, can be serialized.

Serializing an Object Graph

Serializing objects in a graph

  1. Caller requests a serializer for object A from the serialization manager:

    MySerializer s = manager.GetSerializer(a);
    
  2. Metadata attribute on A’s type binds A to a serializer of the requested type. Caller then asks serializer to serialize A:

    Blob b = s.Serialize(manager, a);
    
  3. Object A’s serializer serializes A. For each object it encounters while serializing A, it requests additional serializers from the serialization manager:

    MySerializer s2 = manager.GetSerializer(b);
    Blob b2 = s2.Serialize(manager, b);
    
  4. The result of the serialization is returned to the caller:

    Blob b = ...
    

General Serialization Rules

A component usually exposes a number of properties. For example, the Windows Forms Button control has properties like BackColor, ForeColor, and BackgroundImage. When you place a Button control on a form in a designer and you view the generated code, you will find only a subset of the properties is persisted in code. Typically, these are the properties for which you have explicitly set a value.

The CodeDomSerializer associated with the Button control defines the serialization behavior. The following list describes some of the rules used by the CodeDomSerializer to serialize the value of a property:

  • If the property has a DesignerSerializationVisibilityAttribute attached to it, the serializer uses this to determine if the property is serialized (like Visible or Hidden), and how to serialize (Content).

  • The Visible or Hidden values specify if the property is serialized. The Content value specifies how the property is serialized.

  • For a property called DemoProperty, if the component implements a method called ShouldSerializeDemoProperty, the designer environment makes a late-bound call to this method to determine whether to serialize. For example, for the BackColor property, the method is called ShouldSerializeBackColor.

  • If the property has a DefaultValueAttribute specified, the default value is compared with the current value on the component. The property is serialized only if the current value is non-default.

  • The designer associated with the component can also play a part in making the serialization decision by shadowing properties or by implementing ShouldSerialize methods itself.

Note

The serializer defers the serialization decision to the PropertyDescriptor associated with the property and PropertyDescriptor uses the rules listed previously.

If you want to serialize your component in a different way, you can write your own serializer class that derives from CodeDomSerializer and associate it with your component using the DesignerSerializerAttribute.

Smart Serializer Implementation

One of the requirements of the serializer design is that when a new serialization format is needed, all data types must be updated with a metadata attribute to support that format. However, through the use of serialization providers coupled with serializers that use generic object metadata, this requirement can be met. This section describes the preferred way to design a serializer for a particular format so that the need for many custom serializers is minimized.

The following schema defines a hypothetical XML format to which an object graph will be persisted.

<TypeName>
    <PropertyName>
        ValueString
    </PropertyName>
</TypeName>

This format is serialized using an invented class called DemoXmlSerializer.

public abstract class DemoXmlSerializer 
{
    public abstract string Serialize(
        IDesignerSerializationManager m, 
        object graph);
}

It is important to understand that DemoXmlSerializer is a modular class that builds up a string from pieces. For example, the DemoXmlSerializer for the Int32 data type would return the string "23" when passed an integer of value 23.

Serialization Providers

The previous schema example makes it clear that there are two fundamental types to be handled:

  • Objects that have child properties.

  • Objects that can be converted to text.

It would be difficult to adorn every class with a custom serializer that can convert that class to text or XML tags. Serialization providers solve this by providing a callback mechanism in which an object is given the opportunity to provide a serializer for a given type. For this example, the available set of types is restricted by the following conditions:

  • If the type can be converted to a string by using the IConvertible interface, the StringXmlSerializer will be used.

  • If the type cannot be converted to a string, but is public and has an empty constructor, the ObjectXmlSerializer will be used.

  • If neither of these is true, the serialization provider will return null, indicating that there is no serializer for the object.

The following code example shows how the calling serializer resolves what happens with this error occurs.

internal class XmlSerializationProvider : IDesignerSerializationProvider 
{
    object GetSerializer(
        IDesignerSerializationManager manager, 
        object currentSerializer, 
        Type objectType, 
        Type serializerType) 
    {

        // Null values will be given a null type by this serializer.
        // This test handles this case.
        if (objectType == null) 
        {
            return StringXmlSerializer.Instance;
        }

        if (typeof(IConvertible).IsSubclassOf(objectType)) 
        {
            return StringXmlSerializer.Instance;
        }

        if (objectType.GetConstructor(new object[]) != null) 
        {
            return ObjectXmlSerializer.Instance;
        }

        return null;
    }
}

Once a serialization provider is defined, it must be put to use. A serialization manager can be given a serialization provider through the AddSerializationProvider method, but this requires that this call be made to the serialization manager. A serialization provider can be automatically added to a serialization manager by adding a DefaultSerializationProviderAttribute to the serializer. This attribute requires that the serialization provider have a public, empty constructor. The following code example shows the necessary change to DemoXmlSerializer.

[DefaultSerializationProvider(typeof(XmlSerializationProvider))]
public abstract class DemoXmlSerializer 
{
}

Now, whenever a serialization manager is asked for any type of DemoXmlSerializer, the default serialization provider will be added to the serialization manager if it has not happened already.

Serializers

The sample DemoXmlSerializer class has two concrete serializer classes named StringXmlSerializer and ObjectXmlSerializer. The following code example shows the implementation of StringXmlSerializer.

internal class StringXmlSerializer : DemoXmlSerializer
{
    internal StringXmlSerializer Instance = new StringXmlSerializer();

    public override string Serialize(
        IDesignerSerializationManager m, 
        object graph) 
    {

        if (graph == null) return string.Empty;

        IConvertible c = graph as IConvertible;
        if (c == null) 
        {
            // Rather than throwing exceptions, add a list of errors 
            // to the serialization manager.
            m.ReportError("Object is not IConvertible");
            return null;
        }

    return c.ToString(CultureInfo.InvariantCulture);
    }
}

The ObjectXmlSerializer implementation is more involved, because it is necessary to enumerate the public properties of the object. The following code example shows the implementation of ObjectXmlSerializer.

internal class ObjectXmlSerializer : DemoXmlSerializer 
{
    internal ObjectXmlSerializer Instance = new ObjectXmlSerializer();

    public override string Serialize(
        IDesignerSerializationManager m, 
        object graph) 
    {

        StringBuilder xml = new StringBuilder();
        xml.Append("<");
        xml.Append(graph.GetType().FullName);
        xml.Append(">");

        // Now, walk all the properties of the object.
        PropertyDescriptorCollection properties;
        Property p;

        properties = TypeDescriptor.GetProperties(graph);

        foreach(p in properties) 
        {
            if (!p.ShouldSerializeValue(graph)) 
            {
                continue;
            }

            object value = p.GetValue(graph);
            Type valueType = null;
            if (value != null) valueType = value.GetType();

            // Get the serializer for this property
            DemoXmlSerializer s = m.GetSerializer(
                valueType, 
                typeof(DemoXmlSerializer)) as DemoXmlSerializer;

            if (s == null) 
            {
                // Because there is no serializer, 
                // this property must be passed over.  
                // Tell the serialization manager
                // of the error.
                m.ReportError(string.Format(
                    "Property {0} does not support XML serialization",
                    p.Name));
                continue;
            }

            // You have a valid property to write.
            xml.Append("<");
            xml.Append(p.Name);
            xml.Append(">");

            xml.Append(s.Serialize(m, value);

            xml.Append("</");
            xml.Append(p.Name);
            xml.Append(">");
        }

        xml.Append("</");
        xml.Append(graph.GetType().FullName);
        xml.Append(">");
        return xml.ToString();
    }
}

ObjectXmlSerializer invokes other serializers for each property value. This has two advantages. First, it allows ObjectXmlSerializer to be very simple. Second, it provides an extensibility point for third-party types. If ObjectXmlSerializer is presented with a type that cannot be written out by either of these serializers, a custom serializer can be provided for that type.

Usage

To use these new serializers, an instance of IDesignerSerializationManager must be created. From this instance, you ask for a serializer and then ask the serializer to serialize your objects. For the following code example, Rectangle will be used for an object to serialize, because this type has an empty constructor and has four properties that support IConvertible. Instead of implementing IDesignerSerializationManager yourself, you can use the implementation provided by DesignerSerializationManager.

Rectangle r = new Rectangle(5, 10, 15, 20);
DesignerSerializationManager m = new DesignerSerializationManager();
DemoXmlSerializer x = (DemoXmlSerializer)m.GetSerializer(
    r.GetType(), typeof(DemoXmlSerializer);

string xml = x.Serialize(m, r);

This would create the following XML.

<System.Drawing.Rectangle>
<X>
5
</X>
<Y>
10
</Y>
<Width>
15
</Width>
<Height>
15
</Height>
</System.Drawing.Rectangle>

Note

The XML is not indented. This could be easily accomplished through the Context property on IDesignerSerializationManager. Each level of serializer could add an object to the context stack that contains the current indent level, and each serializer could search for that object in the stack and use it to provide an indent.

See Also

Reference

IDesignerSerializationManager

DesignerSerializationManager

DefaultSerializationProviderAttribute

IConvertible

CodeDomSerializerBase

Other Resources

Extending Design-Time Support