The iMotion Edgeware framework provides initialization parameters for event controls as well as registration arguments for the link between a producer and consumer. The initialization parameters and registration arguments only support string values, because they are saved as XML elements within the project configuration file.
Sometimes it is necessary to save an object that may have many fields as either an initialization parameter or a registration argument. Or worse if you want an array of objects. In my case, I wanted the ability to assign an array of my objects that would serve as registration arguments. Fundamentally, you have three options if you want to persist an object or an array of objects:
- Write each field of the object to a separate name-value pair
- Create a ToString and Parse formatting implementation
- Serialize the object graph
The first two options are too much work and too much code unless your class has very few fields. And the more code the greater chance for bugs. So, serialization is really the only viable choice. Unfortunately, serialization comes with its own headaches.
There are also three built-in options for serialization:
- Binary serialization
- XML serialization
- SOAP serialization
I was pretty sure that binary serialization wouldn't work, because there are some characters within the output that would not be encoded correctly into a string. I didn't explore all possible options with encoding, so there is a possibly that some encoding like ANSI would work, but that could bring its own problems depending on the object being serialized.
XML serialization sounded promising, because its easy and requires fewer permissions than standard serialization using a BinaryFormatter or a SoapFormatter. And more importantly it doesn't store version information on the serialized type. However, XML serialization comes with some limitations including:
- All properties to be serialized must have get and set accessors
- Certain types cannot be serialized (e.g. TimeSpan -- this blows my mind, since the XML duration type is the same thing)
- Markup is required if your object contains other types
- Supporting an object hierarchy requires complex setup of the XmlSerializer
I wrestled with using the XmlSerializer for awhile before realizing that it just wasn't flexible enough. I actually had it working except for my TimeSpan objects. But, it came with the cost of a very messy marked up class.
I had never used the SoapFormatter before, because in most cases I could use either an XmlSerializer or a BinaryFormatter. After some initial testing it was clear that the SoapFormatter didn't come with any of the drawbacks of the other options. The one requirement in my case was implementing ISerializable on my types, because I had some read-only fields.
The issue with versioning was the first thing I had to tackle. I discovered that both formatters have the AssemblyFormat property which has two options: Simple and Full. Simple outputs just the name of the assembly; whereas Full specifies the fully qualified name including the version and public key. It's critical that the serialized data not be version dependent with initialization parameters and registration arguments; otherwise, all of your data will be invalid when the version of the components is changed.
Once the choice of serialization was settled the implementation flew together very easily. The first thing I did was isolate the implementation of the serialization and deserialization into a special class. The class doesn't require member fields; therefore, I made the class static. I implemented the class using two public methods with a few helper methods that perform some of the core work.
Typically, serialization occurs to a file. In my case I want to serialize to a string. This is no problem thanks to the MemoryStream class and the Encoding classes. Here are the steps for the serialization:
- Create a MemoryStream
- Create the SoapFormatter
- Serialize the object graph to memory
- Encode the bytes from memory into a string
The process for deserialization is for all intents and purposes the exact opposite. Here are the steps for deserialization:
- Decode the string into bytes
- Create a MemoryStream from the bytes
- Create the SoapFormatter
- Deserialize the object graph
Here is the completed code for the serializer class:
public static class ScheduleSerializer
{
public static Schedule[] Deserialize(string data)
{
if (data == null || data.Length == 0)
throw new ArgumentNullException("data");
using (MemoryStream stream = GetStreamFromString(data))
{
SoapFormatter formatter = CreateFormatter();
Schedule[] schedules = (Schedule[])formatter.Deserialize(stream);
return schedules;
}
}
public static string Serialize(Schedule[] schedules)
{
if (schedules == null || schedules.Length == 0)
throw new ArgumentNullException("schedules");
using (MemoryStream stream = new MemoryStream())
{
SoapFormatter formatter = CreateFormatter();
formatter.Serialize(stream, schedules);
return GetStringFromStream(stream);
}
}
private static SoapFormatter CreateFormatter()
{
SoapFormatter formatter = new SoapFormatter();
formatter.AssemblyFormat = FormatterAssemblyStyle.Simple;
return formatter;
}
private static MemoryStream GetStreamFromString(string data)
{
byte[] dataBytes = Encoding.UTF8.GetBytes(data);
MemoryStream stream = new MemoryStream(dataBytes);
return stream;
}
private static string GetStringFromStream(MemoryStream stream)
{
byte[] dataBytes = stream.ToArray();
string data = Encoding.UTF8.GetString(dataBytes);
return data;
}
This class is a great utility when it comes to using complex initialization parameters and registration arguments in the iMotion Edgeware platform. Using the utility is extremely easy. For initialization parameters you would call Deserialize during the OnLoad method and then Serialize in the SaveProperties method. And then Deserialize again in the Init method for the event control. For registration arguments you would Deserialize in the OnLoad method, Serialize before exiting your EventProducerLinkDialog, and then Deserialize again in the AddEventConsumer method within the event producer.
Here's the relevant code from within my EventProducerLinkDialog:
protected override void OnLoad
{
string serializedData = this.RegistrationArgs[SCHEDULEDATA_ARG];
if (serializedData != null && serializedData.Length > 0)
{
Schedule[] schedules = ScheduleSerializer.Deserialize(serializedData);
foreach (Schedule schedule in schedules)
{
AddScheduleListItem(schedule); // code for this method not shown
}
}
}
private void okayButton_Click(object sender, EventArgs e)
{
if (HasChanged)
{
Schedule[] schedules = GetSchedulesFromItems(); // code for this method not shown
string serializedSchedules = ScheduleSerializer.Serialize(schedules);
RegistrationArgs[SCHEDULEDATA_ARG] = serializedSchedules;
RegistrationArgsChanged = true;
}
DialogResult = DialogResult.OK;
Close();
}
Here's the relevant code from the event producer:
public void AddEventConsumer(IEventConsumer eventConsumer, NameValueCollection args)
{
IScheduledEventConsumer newConsumer = eventConsumer as IScheduledEventConsumer;
if (newConsumer != null)
{
string serializedData = args[SCHEDULEDATA_ARG];
Schedule[] schedules = ScheduleSerializer.Deserialize(serializedData);
List enabledSchedules = new List(schedules.Length);
// filter out disabled schedules
foreach (Schedule schedule in schedules)
{
if (schedule.Enabled)
enabledSchedules.Add(schedule);
}
// add the consumer and it's schedules
_consumers.Add(newConsumer, enabledSchedules.ToArray());
}
}
This article demonstrated how to use complex objects and object arrays for more advanced use in initialization parameters and registration arguments for your iMotion Edgeware event controls. You'll quickly see how much more customization you can offer to the workflow users using these techniques.