views:

1010

answers:

3

Background

I am trying to create a copy of a business object I have created in VB.NET. I have implemented the ICloneable interface and in the Clone function, I create a copy of the object by serializing it with a BinaryFormatter and then de-serializing straight back out into another object which I return from the function.

The class I am trying to serialize is marked as "Serializable" along with the child objects that are contained within the class.

I have tested that the clone method works by writing code similar to the following:

Dim obj as New Sheep()
Dim dolly as Sheep = obj.Clone()

All works fine at this point.

Problem

I have a custom windows forms control which inherits from a 3rd party control. This custom control basically contains the object which I want to clone (as this object ultimatly feeds the 3rd party control).

I want to create a clone of the object within the windows form control so that I can allow the user to manipulate the properties whilst having the option of cancelling the changes and reverting the object back to how it was before they made the changes. I would like to take the copy of the object before the user starts making changes and hold onto it so I have it ready if they press cancel.

My thought would be to write code along the lines of the following:

Dim copy as Sheep = MyControl.Sheep.Clone()

Then allow the user to manipulate the properties on MyControl.Sheep. When I attempt to do this however, the clone method throws an exception stating:

*Type 'MyControl' in Assembly 'My_Assembly_Info_Here' is not marked as serializable*

This error is thrown at the point where I call BinaryFormatter.Serialize(stream,Me).

I have tried creating a method on MyControl that returns a copy of the object and also first assigning MyControl.Sheep to another variable and then cloning the variable but nothing seems to work. However, creating a new instance of the object directly and cloning it works fine!

Any idea's where I am going wrong?

Solution

Marc's answer helped point me in the right direction on this one. This blog post from Rocky Lhotka explains the problem and how to solve it.

A: 

An obvious question, but are you sure that you don't have a reference to MyControl from your Sheep object; be it an object or a list or anything? If this is the case this is what is preventing you from cloning your business object.

The more than likely candidates would be a .Parent or .Tag property.

FryHard
this was my first thought too but I have double checked and there is no references of the control in the object otherwise I dont think I would have been able to serialize the object when I create a new instance of it
KevB
Re the above comment: if you create a *new* instance, there won't be any UI controls referencing it yet...
Marc Gravell
+2  A: 

Do you have an event that the UI is subscribing to? A {Foo}Changed event if data-binding, or perhaps INotifyPropertyChanged? You might have to mark the event backing field as [NonSerialized] (or however attributes look in VB - I'm a C# person...). If you are using field-like-events (i.e. the abbreviated syntax without add/remove), then mark the entire event with [field: NonSerialized] (again, translate to VB).

Marc

Marc Gravell
A: 

In third party libraries if something is not marked as serializable it should not be serialized for a good reason, but often its not serializable because the developer just simply didn't include it. You can use reflection to make a copy of the public properties of the control and return its state to your reflected version on a cancel. There are performance implications to this approach but because you are working at the UI tier I imagine it won't be much of a worry. This method is not guaranteed error free; public properties do not necessarily represent the entire state of a class and setting some properties may have side effects (they shouldn't, but you didn't write the code, so either ILDasm it and see or hope for the best).

Additionally not all of the types of the properties may be serializable, in which case you need to go further by manually writing serialization routines for those types (and possibly those type's properties).

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApplication1
{
 public class NonSerializableSheep
 {
  public NonSerializableSheep() { }

  public string Name { get; set; }
  public int Id { get; set; }
  // public read only properties can create a problem
  // with this approach if another property or (worse)
  // a group of properties sets it
  public int Legs { get; private set; }

  public override string ToString()
  {
   return String.Format("{0} ({1})", Name, Id);
  }
 }

 public static class GhettoSerializer
 {
  // you could make this a factory method if your type
  // has a constructor that appeals to you (i.e. default 
  // parameterless constructor)
  public static void Initialize<T>(T instance, IDictionary<string, object> values)
  {
   var props = typeof(T).GetProperties();

   // my approach does nothing to handle rare properties with array indexers
   var matches = props.Join(
    values,
    pi => pi.Name,
    kvp => kvp.Key,
    (property, kvp) =>
     new {
      Set = new Action<object,object,object[]>(property.SetValue), 
      kvp.Value
     }
   );

   foreach (var match in matches)
    match.Set(instance, match.Value, null);
  }
  public static IDictionary<string, object> Serialize<T>(T instance)
  {
   var props = typeof(T).GetProperties();

   var ret = new Dictionary<string, object>();

   foreach (var property in props)
   {
    if (!property.CanWrite || !property.CanRead)
     continue;
    ret.Add(property.Name, property.GetValue(instance, null));
   }

   return ret;
  }
 }

 public class Program
 {
  public static void Main()
  {
   var nss = new NonSerializableSheep
   {
    Name = "Dolly",
    Id = 12
   };

   Console.WriteLine(nss);

   var bag = GhettoSerializer.Serialize(nss);

   // a factory deserializer eliminates the additional
   // declarative step
   var nssCopy = new NonSerializableSheep();
   GhettoSerializer.Initialize(nssCopy, bag);

   Console.WriteLine(nssCopy);

   Console.ReadLine();

  }
 }
}
cfeduke