Catel is a brand new framework (or enterprise library, just use whatever you like) with data handling, diagnostics, logging, WPF controls, and an MVVM Framework. So, Catel is more than "just" another MVVM Framework or some nice Extension Methods that can be used. It's more like a library that you want to include in all the (WPF) applications you are going to develop in the near future.
This article will explain the data handling of the framework.
Article browser
Table of contents
- Introduction
- Features
- Background
- Using the classes
- What else is possible with these classes?
- History
1. Introduction
Welcome to the introduction of Catel. Catel is a brand new framework (or enterprise library, just use whatever you like) with data handling, diagnostics, logging, WPF controls, and an MVVM-framework. So, Catel is more than "just" another MVVM-framework or some nice Extension Methods that can be used. It's more like a library that you want to include in all the (WPF) applications you are going to develop in the near future.
The framework is developed using C# (.NET Framework 3.5 SP1).
There will be a series of articles that will explain the several features of Catel in detail. Below is an overview of the articles:
- Catel (1/n): Data handling the way it should
- Catel (2/n): Using WPF controls and themes
- Catel (3/n): The MVVM framework
- Catel (4/n): Unit Testing with Catel
- Catel (5/n): Building a WPF example application with Catel in 1 hour
It's important to realize that Catel is not just another Extension Methods library, nor only an MVVM-framework, but it is a combination of basic data handling, useful controls, and an MVVM-framework.
1.1. Why another framework?
You might be thinking: why another framework, there are literally thousands of them out there. Well, first of all, thousands of them is quite a lot, let's just say there are hundreds of them. A few years ago, the lead developer of Catel was using serialization to serialize data from/to disk. But, as he noticed, he had to take care of different versions after every release. After every release, he had to take care of the serialization and backwards compatibility. Also, he had to implement some very basic interfaces (such as INotifyPropertyChanged
) for every data object. Then, he decided to write a base class for data handling which can take care of different versions and serialization by itself, and implements the most basic interfaces of the .NET Framework out of the box. The article was published on CodeProject as DataObjectBase.
Then, he was working on a WPF project with five other developers and needed an MVVM-framework since the application was not using MVVM at the moment. Writing an MVVM-framework was no option because there were so many other frameworks out there. But, after looking at some Open-Source MVVM-frameworks (such as the excellent Cinch framework, which was the best one we could find), none of them seemed to be a real option. Creating the View Models was too much work, and the View Models still contained lots of repetitive code in, for example, the property definitions. After taking a closer look at the source code of Cinch and other frameworks, the lead developer thought: if we use the DataObjectBase published before as the base for a View Model class, it should be possible to create a framework in a very short amount of time.
Then, all other developers of the team he was working on the project got enthusiastic, and then the whole team decided to merge their personal libraries into one big enterprise library, and Catel was born.
1.2. Why use this framework?
Before reading any further, it's important to know why you should use the framework. Below are a few reasons why Catel might be interesting for you:
- Catel is Open-Source. This way, you can customize it any way you want. If you want a new feature request, and the team does not respond fast enough, you can simply implement it yourself.
- The codebase for Catel is available on CodePlex. This way, you have the option to either download the latest stable release, or live on the edge by downloading the latest source code.
- Catel uses unit tests to make sure that new updates do not break existing functionality.
- Catel is very well documented. Every method and property has comments, and the comments are available in a separate reference help file. There is also a lot of documentation available on CodePlex, and in the future, in-depth articles will be written.
- Catel is developed by a group of talented software developers, and is heavily under development. This is a great advantage because the knowledge is not at just one person, but at a whole group. The developers of Catel all have more than three years of development experience with WPF, and are using Catel in real life applications for at least a year.
1.3. Basics of the framework
At the moment of writing, the framework consists of two projects:
- Catel.Core - This is the core of the framework, and contains the most important class of Catel:
DataObjectBase
. Most of the data handling and MVVM-framework fully rely on this class. The core also contains Extension Methods, and features such as diagnostics, Reflection, logging, and more.
- Catel.Windows - This is the WPF part of Catel. It includes Extension Methods, WPF controls, and most importantly, the MVVM-framework.
The goal of the framework is to minimize the repetitive writing of boring code and to concentrate on the actual (functional) development of an application. By using Catel, the development time of an application can be reduced by at least 50%, but if you become more experienced with Catel, the effect might be even better. As an example, writing a window with an OK and Cancel button can be accomplished by simply using the DataWindow
that ships with Catel. The DataWindow
also includes validation automatically. This way, you can immediately start focusing on the actual behavior of the window instead of the OK and Cancel buttons and the data validation.
In part 4 of this article series, an example application will be written using most parts of Catel.
1.4. What is to be expected in the near future?
The team is currently polishing the code of the core and the WPF library. As soon as it is stable, version 1.0 will be released (to be expected before the end of the year). Then, the following points/ideas might be realized:
- Add web (MVC) library - Some members of the team are also experienced in developing MVC applications. In the near future, their personal library will be documented and tested, and then made public as part of Catel as well.
- Add more WPF controls and themes - As time flies by, new controls will be introduced. There are also plans to add new color schemes to support several themes for Catel out of the box.
- Add full support for Silverlight - At the moment, support for Silverlight is not a top-priority (however, the MVVM-framework should be fully compatible). When version 1.0 is released, the team will look into the support for Silverlight in detail.
1.5. Data handling
One of the problems every developer faces is data persistence, and that’s what the first article is all about. 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 that 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) code 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 things and is very hard to maintain.
Catel has the solution to all the problems described below: the DataObjectBase
class.
2. Features
The DataObjectBase
class is a generic base class that can be used for all your data classes.
- Fully serializable - It is now really easy to store 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.
- Support property changed notifications - The class supports the
INotifyPropertyChanging
and INotifyPropertyChanged
interfaces so this class can easily be used in WPF and MVC applications to reflect changes to the user.
- Backwards compatibility - When serializing your objects to 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.
- Validation - The class implements the
IDataErrorInfo
interface 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.
- 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 cancelled.
3. Background
If you are not interested in the background and how the DataObjectBase
works internally, you should skip this part of the article.
3.1. DataObjectBase
The DataObjectBase
class is the most important class of Catel. The class is pretty complicated, but the basics are explained in this article. The class itself only has properties that are required for the basics of the class. The actual properties of deriving classes are stored in an internal dictionary. These properties can be accessed from derived classes by using the SetValue
and GetValue
methods. These methods internally access the dictionary of properties, but also take care of the events that should occur during a property change such as the PropertyChanging
event, the PropertyChanged
event, and validation. If the DataObjectBase
contains properties that support the INotifyPropertyChanged
interface, it will automatically subscribe to these objects. This way, derived objects will be notified in case a registered property changes as well.
Properties can be registered by using one of the static RegisterProperty
methods. The method returns a PropertyData
object which contains the information about the property, such as the name, the type, and the default value. Then, when the class is constructed, it uses Reflection to find all the PropertyData
objects. This way, it knows what properties are registered on the class and registers the type on the PropertyDataManager
(more on this later).
Validation is supported out of the box. It was a deliberate choice not to use attributes for validation (lots of frameworks use attributes to validate properties), because error and warning messages cannot be localized via satellite assemblies. Also, most frameworks support a few basic attributes, but when a more complex business rule comes into play, the attributes won't be enough and the user still has to implement custom validation. Therefore, the DataObjectBase
supports two important methods:
ValidateFields
- Field rules are rules for specific fields. An example is that a person cannot have a negative age.
ValidateBusinessRules
- Business rules are rules that span multiple properties. An example is that a red car (Color
property) older than 10 years (Age
property) cannot be sold. None of the fields themselves are valid, but the combination is.
Cloning of objects is supported out of the box by DataObjectBase
by the implementation of the IClonabl
e interface. When the Clone
method is called, a deep copy is created. This means that the class does not simply make a copy of the first-level properties, and thus still holds a reference to the child objects. The DataObjectBase
serializes the current state of the object and then deserializes the state into a new object. This way, a completely new object is created without any references to the original object.
The DataObjectBase
class also implements the IEditableObject
interface. When a call to BeginEdit
is invoked, the class serializes the current state of the object into memory and holds the state in an internal
property. Then, when the user wants to cancel the editing of the properties, the old values are restored in the internal dictionary that holds the actual values of the properties.
Last but not least, the class also implements the IDisposable
interface. When the object is disposed, it cleans up any references that it holds to make sure it causes no memory leaks. Derived classes get the chance to clean up unmanaged memory as well.
3.2. Support classes
There are several support classes that are used by the DataObjectBase
class. These will not be explained in detail, but they are still important enough to be noticed.
PropertyData
- The PropertyData
class contains information about a property, and can only be constructed by a call to the protected RegisterProperty
method. This class is used to get default values, type information, and registered callback methods of a property.
PropertyDataManager
- The PropertyDataManager
class is responsible for the registration of the properties per type. Besides the actual registration of the types, it is also responsible for holding the XML mappings for all the properties.
3.3. SavableDataObjectBase
SavableDataObjectBase
is a class that derives from the DataObjectBase
class. The class diagram below shows the extensions that the SavableDataObjectBase
provides:
The class diagram makes clear that the SavableDataObjectBase
is capable of saving itself from/to disk and memory. The class is serializable in two modes:
Binary
- For binary serialization, the class relies on the BinaryFormatter
class. By default, the binary formatter of the .NET Framework will break if the version of the assembly changes (even when the type itself does not change). This is due to the fact that during binary serialization, the version number of the assembly is stored as well. And, at deserialization time, the type cannot be found (since the version has changed) and the deserialization will fail.
To solve the version changes issue, one of the Load
method overrides accepts the enableRedirects
parameter. When redirects are enabled, the class will create a custom SerializationBinder
to strip the version from the type descriptors. It then tries to load the type (which should succeed if the class is located in the assembly). When a type is completely moved to another assembly, the RedirectTypeAttribute
can be used. This way, the RedirectSerializationBinder
will use the new assembly and type name to redirect an old-style type found in the serialized data to a new type which is available in the latest version.
Xml
- For XML serialization, the class implements the IXmlSerializable
interface. The reason that the class does not rely on the default XmlSerializer
class is due to the fact that the default serializer included lots of trash, such as unwanted namespaces. The IXmlSerializable
implementation still internally uses the XmlSerializer
class, but makes sure that the XML output is correctly formatted.
4. Using the classes
First of all, it is very important to realize that you shouldn't bore yourself with writing all the code below yourself. Catel contains lots of code snippets that allow you to create data objects very easily in a short amount of time.
4.1. Creating your first data object
Explanation
This example shows the simplest way to declare a data object using the DataObjectBase
class. By using a code snippet, the class is created in just 10 seconds.
Code snippets
dataobject
- Declares a data object based on the DataObjectBase
class
Steps
- Create a new class file called FirstDataObject.cs.
- Inside the namespace, use the
dataobject
codesnippet and fill in the name of the class, in this case FirstDataObject
.
Code
[Serializable]
public class FirstDataObject : DataObjectBase<FirstDataObject>
{
#region Variables
#endregion
#region Constructor & destructor
public FirstDataObject()
{ }
protected FirstDataObject(SerializationInfo info, StreamingContext context)
: base(info, context) { }
#endregion
#region Properties
#endregion
#region Methods
protected override void ValidateFields()
{
}
protected override void ValidateBusinessRules()
{
}
#endregion
}
4.2. Declaring properties
4.2.1. Simple property
Explanation
This example shows how to declare the simplest property. In this example, a string
property with a default value will be declared with the use of a code snippet.
Code snippets
dataprop
- Declares a simple property on a data object
Steps
- Open FirstDataObject.cs created in the previous example.
- In the
Properties
region, use the code snippet dataprop
, and use the following values:
Code snippet item |
Value |
description
|
Gets or sets the simple property
|
type |
string |
name |
SimpleProperty |
defaultvalue
|
“Simple property” |
Code
public string SimpleProperty
{
get { return GetValue<string>(SimplePropertyProperty); }
set { SetValue(SimplePropertyProperty, value); }
}
public static readonly PropertyDataSimplePropertyProperty =
RegisterProperty("SimpleProperty", typeof(string), "Simple property");
4.2.2. Property with property changed callback
Explanation
Sometimes you need to know when a property has changed. You can do this by overriding the OnPropertyChanged
method and checking if the specific property has changed, but it’s even simpler to register a callback that is only invoked when that specific property has changed.
Code snippets
datapropchanged
- Declares a simple property on a data object with a property changed callback
Steps
- Open FirstDataObject.cs created in a previous example.
- In the Properties region, use the code snippet
datapropchanged
, and use the following values:
Code snippet item |
Value |
description |
Gets or sets the callback property |
type |
string |
name |
CallbackProperty |
defaultvalue |
“Callback property” |
Code
public string CallbackProperty
{
get { return GetValue<string>(CallbackPropertyProperty); }
set { SetValue(CallbackPropertyProperty, value); }
}
public static readonly PropertyDataCallbackPropertyProperty =
RegisterProperty("CallbackProperty", typeof(string), "Callback property",
(sender, e) => ((FirstDataObject)sender).OnCallbackPropertyChanged());
private void OnCallbackPropertyChanged()
{
}
4.3. Adding validation
Explanation
This example shows how to use the integrated validation of the DataObjectBase
class. It creates a new object, declares two different properties to show both the warning and error types, and shows how to validate. This example does not include business rule validation, but it can be used exactly the same.
A field error is mapped to the IDataErrorInfo.Item
property, a business error is mapped to the IDataErrorInfo.Error
property.
Code snippets
dataobject
- Declares a data object based on the DataObjectBase
class
dataprop
- Declares a simple property on a data object
Steps
- Create a new class file called ValidatingObject.cs.
- Inside the namespace, use the
dataobject
codesnippet and fill in the name of the class, in this case ValidatingObject
.
- In the
Properties
region, use the code snippet dataprop
, and use the following values:
Code snippet item |
Value |
description |
Gets or sets the field warning property |
type |
string |
name |
FieldWarning |
defaultvalue |
“Invalid field value” |
- In the
Properties
region, use the code snippet dataprop
, and use the following values:
Code snippet item |
Value |
description |
Gets or sets the field error property |
type |
string |
name |
FieldError |
defaultvalue |
“Invalid field value” |
- Now that all properties are declared, it’s time to validate the fields. Add the following code to the body of the
ValidateFields
method:
if (FieldWarning == "Invalid field value")
{
SetFieldWarning(FieldWarningProperty,
"Property 'FieldWarning' is probably wrong");
}
if (FieldError == "Invalid field value")
{
SetFieldError(FieldErrorProperty, "Property 'FieldError' is wrong");
}
Code
[Serializable]
public class ValidatingObject : DataObjectBase<ValidatingObject>
{
#region Variables
#endregion
#region Constructor & destructor
public ValidatingObject()
{ }
protected ValidatingObject(SerializationInfo info, StreamingContext context)
: base(info, context) { }
#endregion
#region Properties
public string FieldWarning
{
get { return GetValue<string>(FieldWarningProperty); }
set { SetValue(FieldWarningProperty, value); }
}
public static readonly PropertyDataFieldWarningProperty =
RegisterProperty("FieldWarning",
typeof(string), "Invalid field value");
public string FieldError
{
get { return GetValue<string>(FieldErrorProperty); }
set { SetValue(FieldErrorProperty, value); }
}
public static readonly PropertyDataFieldErrorProperty =
RegisterProperty("FieldError", typeof(string), "Invalid field value");
#endregion
#region Methods
protected override void ValidateFields()
{
if (FieldWarning == "Invalid field value")
{
SetFieldWarning(FieldWarningProperty,
"Property 'FieldWarning' is probably wrong");
}
if (FieldError == "Invalid field value")
{
SetFieldError(FieldErrorProperty, "Property 'FieldError' is wrong");
}
}
protected override void ValidateBusinessRules()
{
}
#endregion
}
4.4. Saving objects to disk or memory
Explanation
Saving and loading objects out of the box has never been so easy. SavableDataObjectBase
can automatically save/load objects in several ways, such as memory, file in different modes (binary and XML). This example shows that making your objects savable is very easy and does not take any time!
Code snippets
dataobject
- Declare a data object based on the DataObjectBase
class
dataprop
- Declare a simple property on a data object
Steps
- Create a new class file called SavableObject.cs.
- Inside the namespace, use the
dataobject
codesnippet and fill in the name of the class, in this case SavableObject
.
- Change the base class from
DataObjectBase
to SavableDataObjectBase
.
- In the
Properties
region, use the code snippet dataprop
, and use the following values:
Code snippet item |
Value |
description |
Gets or sets the name |
type |
string |
name |
Name |
defaultvalue |
“MyName” |
- You can now save the created object by using any of the
Save
methods. Loading can be done by using the static SavableObject.Load
methods.
Code
[Serializable]
public class SavableObject : SavableDataObjectBase<SavableObject>
{
#region Variables
#endregion
#region Constructor & destructor
public SavableObject()
{ }
protected SavableObject(SerializationInfo info, StreamingContext context)
: base(info, context) { }
#endregion
#region Properties
public string Name
{
get { return GetValue<string>(NameProperty); }
set { SetValue(NameProperty, value); }
}
public static readonly PropertyDataNameProperty =
RegisterProperty("Name", typeof(string), "MyName");
#endregion
#region Methods
#endregion
}
4.5. Backwards compatibility
Explanation
The class seems nice, but what if you already have all the serialization built into your software and custom objects? No problem, Catel fully supports backwards compatibility, and allows you to add custom deserialization in case the object cannot be deserialized by the DataObjectBase
itself.
This way, you can safely migrate to using Catel, and you don’t have to worry about serialization in the next versions of your software any longer.
Code snippets
dataobject
- Declare a data object based on the DataObjectBase
class
dataprop
- Declare a simple property on a data object
Steps
- Create a new class file called BackwardsCompatibleObject.cs.
- Inside the namespace, use the
dataobject
codesnippet and fill in the name of the class, in this case BackwardsCompatibleObject
.
- Change the base class from
DataObjectBase
to SavableDataObjectBase
.
- In the
Properties
region, use the code snippet dataprop
, and use the following values:
Code snippet item |
Value |
description |
Gets or sets the name |
type |
string |
name |
Name |
defaultvalue |
“Unknown” |
- Let’s assume the old version of the object serialized before has serialized the
Name
property as _name
in the serialization info. Override the GetDataFromSerializationInfo
method and add the following code. As you can see, if the deserialization did not succeed, the information is retrieved manually from the old object. This only needs to be done once; in future, the DataObjectBase
class will know how to deserialize the object correctly.
protected override void GetDataFromSerializationInfo(SerializationInfo info)
{
if (DeserializationSucceeded) return;
Name = SerializationHelper.GetString(info, "_name",
NameProperty.GetDefaultValue<string>());
}
Code
[Serializable]
public class BackwardsCompatibleObject : SavableDataObjectBase<BackwardsCompatibleObject>
{
#region Variables
#endregion
#region Constructor & destructor
publicBackwardsCompatibleObject()
{ }
protected BackwardsCompatibleObject(SerializationInfo info, StreamingContext context)
: base(info, context) { }
#endregion
#region Properties
public string Name
{
get { returnGetValue<string>(NameProperty); }
set { SetValue(NameProperty, value); }
}
public static readonly PropertyDataNameProperty =
RegisterProperty("Name", typeof(string), "Unknown");
#endregion
#region Methods
protectedoverridevoidGetDataFromSerializationInfo(SerializationInfo info)
{
if (DeserializationSucceeded) return;
Name = SerializationHelper.GetString(info, "_name",
NameProperty.GetDefaultValue<string>());
}
#endregion
}
4.6. Supporting moved/renamed types and properties
Explanation
This example shows how to use RedirectTypeAttribute
. The attribute can be used to inform DataObjectBase
that a type has moved or renamed and should be correctly redirected to the new type. This way, you can safely move/rename objects and still be able to deserialize your old objects.
For this example, you should assume that a previous class named PreviousTypeName
existed in the namespace Catel.Articles
. But since that was completely wrong, it is decided that the class is now named RenamedObject
and is located in a new namespace.
Note: For the sake of simplicity, no properties are declared on this class since it would only cause overhead for the example.
Code snippets
dataobject
- Declare a data object based on the DataObjectBase
class
dataprop
- Declare a simple property on a data object
Steps
- Create a new class file called RenamedObject.cs.
- Inside the namespace, use the
dataobject
codesnippet and fill in the name of the class, in this case RenamedObject
.
- Change the base class from
DataObjectBase
to SavableDataObjectBase
.
- Add the
RedirectType
attribute with the right values on top of the class declaration:
[RedirectType("Catel.Articles", "PreviousTypeName")]
- During deserialization, the type
Catel.Articles.PreviousTypeName
will automatically be directed to Catel.Articles._02__Data_handling.Models.RenamedObject
.
Code
[Serializable]
[RedirectType("Catel.Articles", "PreviousTypeName")]
public class RenamedObject : DataObjectBase<RenamedObject>
{
#region Variables
#endregion
#region Constructor & destructor
public RenamedObject()
{ }
protected RenamedObject(SerializationInfo info, StreamingContext context)
: base(info, context) { }
#endregion
}
5. What else is possible with these classes?
Since the base class DataObjectBase
implements all of the most commonly used interfaces, the possibilities are (almost) endless. For example, this class also serves as a base class for the ViewModelBase
class that ships with the MVVM-framework of Catel. But, the classes can also be used in MVC to add validation.
6. History
- 25 November, 2010: Added article browser and brief introduction summary
- 23 November, 2010: Removed disclaimer, added link to CodePlex
- 13 September, 2010: Initial version