Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

DataObjectBase

0.00/5 (No votes)
18 May 2010 2  
DataObjectBase -> the new Object class for data objects!

Table of Contents

  1. Introduction
  2. Background
  3. Using the Code
  4. Points of Interest
  5. History

Introduction

One of the problems every developer faces with is data persistence. Most software that is written requires the ability to save objects to disk, or serialize them (binary or XML) to use .NET remoting or web services. The .NET Framework supports a lot of interfaces to implement this:

But, you also want to be notified when a property of the data object changes, and now you are writing this piece of software, you decide to implement data validation as well. The following .NET interfaces are required:

What happens is that you need to write a lot of (redundant) to support all these interfaces for all your data classes. Then you haven't even thought of versioning in binary serialized objects that are really difficult to deal with. Also, when (de)serializing objects, you will find yourself writing a lot of custom (repetitive) code which actually does the same and is very hard to maintain.

How to Declare the Class

The class requires a special declaration. The declaration seems like a lot of code, but a code snippet ‘dataobject’ is included in the package to easily create new objects.

/// <summary>
/// MyFirstObject Data object class which fully supports serialization, 
/// property changed notifications,
/// backwards compatibility and error checking.
/// </summary>
[Serializable]
public class MyFirstObject : DataObjectBase
{
    /// <summary>
    /// Initializes a new object from scratch.
    /// </summary>
    public MyFirstObject()
        : base() { }

    /// <summary>
    /// Initializes a new object based on .
    /// </summary>
    ///  that contains the information.
    /// .
    public MyFirstObject(SerializationInfo info, StreamingContext context)
        : base(info, context) { }
} 

How to Declare Properties

A data class without properties is like a bike without wheels. You can sit on it, but you can never ride the thing. So, it is very important to declare properties. However, since the class uses a special (de)serialization of the properties, a special declaration is required.

To declare a property, use the included code snippet ‘propdata’.

/// <summary>
/// Gets or sets the value of MyFirstProperty.
/// </summary>
public string MyFirstProperty
{
	get { return GetValue(MyFirstPropertyProperty); }
	set { SetValue(MyFirstPropertyProperty, value); }
}

/// <summary>
/// Register the property so it is known in the class.
/// </summary>
public readonly PropertyData MyFirstPropertyProperty = 
	RegisterProperty("MyFirstProperty", typeof(string), string.Empty); 

As you can see, a special wrapper for the property is created that uses the GetValue and SetValue methods of the base class. The most important part of the property definition is the call to RegisterProperty. The first argument is the name of the property, the second argument is the type of the property, the third argument is the default value, in case the property is not yet available or cannot be deserialized.

Background

The DataObjectBase class is a generic base class that can be used for all your data classes.

  1. Fully serializable
    It is now really easy to store the objects on disk or serialize them into memory, either binary or in XML. The data object supports this out of the box and automatically handles the (de)serialization.
  2. Support property changed notifications
    The class supports the INotifyPropertyChanged notifications so this class can easily be used in WPF and MVC applications to show error messages to the user.
  3. Backwards compatibility
    When serializating your objects binary, it is hard to maintain the right versions. When you add a new property to a binary class, or change a namespace, the object cannot be loaded any longer. The data object base takes care of this issue and supports backwards compatibility.
  4. Error checking
    The class implements IDataErrorInfo so it is possible to validate the data object and check the errors. This way, no custom validation code needs to be written outside the data class.
  5. Backup & revert
    The class implements the IEditableObject interface which makes it possible to create a state of the object. Then all properties can be edited, and finally the changes can be applied or canceled.

To support all this functionality out of the box, the class implements the following interfaces:

Interface Reason
ISerializable Required to make the object serializable.
IXmlSerializable Required to make the object serializable with a custom XML serializer.
INotifyPropertyChanging Required to notify other classes of property changes that are about to occur inside the object.
INotifyPropertyChanged Required to notify other classes of property changes that occur inside the object.
IDataErrorInfo Required to notify other classes about errors in the data object. Very useful in WPF to show errors automatically in the user interface.
IDataWarningInfo Custom interface, similar to IDataErrorInfo, to support warnings in the data object.
IClonable Required to support cloning of the data object.
IComparer Required to compare the object to other objects.
IEditableObject Required to create states in the data object. Therefore, it is possible to create a state before the user starts editing the object. Then, the changes can be confirmed or canceled.

Using the Code

All the examples listed below are available as code inside the downloadable package included in this article. Most of the examples have a real-life example which can be started to show that it actually works. :)

Example 01 – Simple object

This example shows how to create a basic object that can be used as a “normal” CLR object:

/// <summary>
/// SimpleObject Data object class which fully supports serialization, 
/// property changed notifications,
/// backwards compatibility and error checking.
/// </summary>
[Serializable]
public class SimpleObject : DataObjectBase
{
    #region Variables
    #endregion

    #region Constructor & destructor
    /// <summary>
    /// Initializes a new object from scratch.
    /// </summary>
    public SimpleObject()
        : base() { }

    /// <summary>
    /// Initializes a new object based on .
    /// </summary>
    ///  that contains the information.
    /// .
    public SimpleObject(SerializationInfo info, StreamingContext context)
        : base(info, context) { }
    #endregion

    #region Properties
    /// </summary>
    /// Gets or sets the simple property.
    /// </summary>
    public string SimpleProperty
    {
        get { return GetValue(SimplePropertyProperty); }
        set { SetValue(SimplePropertyProperty, value); }
    }

    /// <summary>
    /// Register the property so it is known in the class.
    /// </summary>
    public readonly PropertyData SimplePropertyProperty = 
	RegisterProperty("SimpleProperty", typeof(string), string.Empty);
    #endregion

    #region Methods
    #endregion
}

Example 02 – Error Validation

This example shows how to implement validation into the object. Validation will prevent objects that are not valid to be stored (invalid objects cannot be serialized). This example shows an object using WPF, which can show the current errors in the object real-time to the end-user.

Objects have 2 types of validation results. There can either be errors that prevent the object from being serialized, and warnings, that are just warnings so the object can still be serialized.

To enable validation, you must override at least one of the following methods:

/// <summary>
/// Validates the fields.
/// </summary>
protected override void ValidateFields()
{
    // Validate for warnings
    if (string.IsNullOrEmpty(Recommended)) SetFieldWarning(RecommendedProperty, 
	"The recommended property is missing!");

    // Validate for errors
    if (string.IsNullOrEmpty(Required)) SetFieldError
	(RequiredProperty, "The required property is missing!");
}

/// <summary>
/// Validates the business rules.
/// </summary>
protected override void ValidateBusinessRules()
{
    // Check for fields
    if (string.IsNullOrEmpty(Recommended) && string.IsNullOrEmpty(Required))
    {
        SetBusinessRuleError("A combination of field data is not valid, 
	make sure to enter at least one field!");
    }
}

The validation in WPF will result in the following output:

Example 03 – Serialization

This example shows how to use the Save and Load methods to serialize/deserialize objects to/from disk. The object itself can also directly serialize/deserialize into memory, but this is not shown in the example.

To save an object, call the Save method on the object itself:

myObject.Save("MyFile.dob", SerializationMode.Binary);

To load an object, call the static Load method on the class to load:

SerializableObject myObject = SerializableObject.Load
				("MyFile.dob", SerializationMode.Binary);

Example 04 – Nested Objects

The class is perfectly capable of nesting. This means that you can use objects, that are based on DataObjectBase, can contain other classes also based on DataObjectBase. This creates the ability to create parent/child objects where the whole tree is fully serializable. Nothing of interest to show here, just look into the example if you need more information on this topic.

Example 05 – Backward Compatibility

This example shows how an “old” (standard .NET) data class that uses custom binary serialization can easily be converted to a DataObjectBase to use the DataObjectBase even for all your existing classes.

Declare a new DataObjectBase class (remember the ‘dataobject’ code snippet). If the new class is in a new assembly, or has a new name or namespace, use theRedirectType attribute to let the DataObjectBase know that when it finds the old type, it should deserialize that type into the new type.

Then, by default, the DataObjectBase class will try to deserialize the old object. If it fails to do so, it will fall back on the default values provided by the property declarations. However, it is also possible to override the GetDataFromSerializationInfo method:

/// <summary>
/// Retrieves the actual data from the serialization info.
/// </summary>
/// .
/// <remarks>
/// This method should only be implemented if backwards 
/// compatibility should be implemented for
/// a class that did not previously implement the DataObjectBase class.
/// </remarks>
protected override void GetDataFromSerializationInfo(SerializationInfo info)
{
    // Check if deserialization succeeded
    if (DeserializationSucceeded) return;

    // Deserialization did not succeed for any reason, so retrieve the values manually
    // Luckily there is a helper class (SerializationHelper) 
    // that eases the deserialization of "old" style objects
    FirstName = SerializationHelper.GetString(info, 
		"FirstName", FirstNameProperty.GetDefaultValue());
    LastName = SerializationHelper.GetString(info, 
		"LastName", LastNameProperty.GetDefaultValue());
}

Example 06 - Revert and Apply

This example shows how to create a state for an object. This is extremely useful when an object is used in an edit mode. Just before the window is shown, a new state is created. Then, when the user clicks the OK button, the new state is applied. When the user clicks the Cancel button, the changes are cleared by reverting to the state created when the window just opened.

To create a state, use the following code:

myObject.BeginEdit(); 

To apply all changes made to the object since the last state, use the following code:

myObject.EndEdit();

To revert all changes made to the object since the last state, use the following code:

myObject.CancelEdit(); 

Points of Interest

How does it all work in the background? Well, the idea is pretty basic and simple, the way it is implemented is not because of the support for all the features and interfaces. The DataObjectBase class holds a dictionary of registered properties, and the current values for the properties. Then, the GetValue and SetValuemethods can be used to get or set the values inside the dictionary.

The biggest issue (actually, it was a challenge, not an issue) was to deserialize all the data automatically, especially with different version numbers of assemblies or renamed types. When deserializing binary objects, the .NET Framework is very, very strict and even the slightest change in the class or assembly will completely ruin the deserialization and thus the data of the user.

The DataObjectClass solves this in 2 ways. First of all, it checks for the RedirectType attribute to see if any types should be redirected to new types. Then, it initializes a custom SerializationBinder to redirect all the types. This solves the renaming of types, and can also take care of versioning of the types.

However, there are still problems left. When deserializing, you never know when your object (and its child objects) are completely deserialized. To solve this problem, the OnDeserialized attribute is implemented as well as the IDeserializationCallback interface. Using a single one of these will not solve the problem, but using both together has solved the problem.

History

2010/05/18 - 2.1.0

  • Added implementation for IComparer<T> interface
  • Added implementation for IXmlSerializable interface
  • Added implementation for INotifyPropertyChaning interface
  • Added interfaces (IDataObjectBase and IDataObjectBase<T>) to support mocking and passing non-generic objects to methods
  • Added Bytes and KeyName properties
  • Added OnCreating and OnCreated methods
  • AlwaysNotifyOnPropertyChanged is now set in the Initialize method so it is also set to default when deserializing an object
  • Class must now implement empty constructor (required for XML serialization)
  • XML namespaces are no longer written to the XML file when collections are written as properties
  • Moved the creation of binary formatters to a single method
  • Improved unit tests to check for serialization where default values are replaced correctly when serialization using XML
  • Fixed bug where properties of derived classes were not registered correctly due to the static state of IsInitialized
  • Fixed bug where non-public properties were not (de)serialized correctly

2010/04/29 - 2.0.0

  • First good release with real-life examples

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here