Cloning(?) a BO

Cloning(?) a BO

Old forum URL: forums.lhotka.net/forums/t/1704.aspx


albruan posted on Tuesday, November 07, 2006

My client has come back to me with a list of enhancements they need implemented on their application.  Among those requirements is the ability to take an existing Project, the top-level business object, creating a copy of it, modifying a few values such as the job number, and saving it to the database.  I'd like to use something like the Clone method, but it won't allow me to assign a new Guid as the project ID and, without a new ID, it causes a collision with the original object when trying to save it to the database.

I've searched both this forum and the original one and just can't wrap my mind around the process.

RRorije replied on Tuesday, November 07, 2006

I have been thinking about this too.

My suggestion would be to call MarkNew after you clone the object. Yet I do not know how you could dispatch this MarkNew function to all child objects. This can be done, by loosening the constraints on MarkNew, but I do not think that is a good idea. Furthermore you also need a MarkNew function on the childcollections, which is not so neat also.


RockfordLhotka replied on Tuesday, November 07, 2006

In CSLA .NET 2.0, the cloning method (GetClone()) is virtual/Overridable, so you can do this using Clone().

However, I personally don't think that's the best approach. I think it is better to have a factory method like CopyObject() that does the work. So with a Product it would be like:

Product prod = Product.GetProduct(5);
Product copy = Product.CopyProduct(prod);

If you do use the BinaryFormatter to do the copy, the issue of marking the object and its children as new is an issue. And the only real solution is to have an internal/Friend method that can be called to cascade the operation through the graph, much like I do with n-level undo. That's not too hard.

There's also the OnDeserialized() method, which you can override. The problem with this, is that it is called on any deserialize - including n-level undo and the data portal - so you'd have to come up with a way to indicate that the object is in the process of being cloned, and I don't know how you'd do that.

A more "pure" way to approach this is to not use the BinaryFormatter at all, but rather have the CopyProduct() factory call the data portal, passing the original object through a Criteria object to DataPortal_Create(). In DataPortal_Create() you'd write code to copy the properties from the original object into the fields of the new object, and you'd cascade this to the child objects just like you would with normal ADO.NET data access.

The drawback to this approach is that the original object may have fields that aren't exposed as properties, and you'd need to use reflection to copy them.

david.wendelken replied on Tuesday, November 07, 2006

Depending upon what you are using for a database back-end, it might be preferable to just code the logic in a stored procedure in the database and invoke that from your Clone() method.

Pluses:

Reduced network traffic

Reduced load on the application server.

Probably run way faster if it's done in SQL using a set-based approach rather than a procedural, record-based approach. 

Less chance of a timeout in a web app if the call is done asynchronously. 

Minuses:

Increased load on the database server.  (Maybe! If more efficiently written inside the database, it may require less database effort!)

Requires more SQL and T-SQL (or PL/SQL, etc.) knowledge.

Unknown:

Haven't worked with guids yet, so I don't actually know if you can set them from inside the database.  (Pretty sure you can do it from SqlServer 2005, but don't know if there are any performance issues.) If you only need one (for the master project record) you could pass it in...

 

hurcane replied on Tuesday, November 07, 2006

RockfordLhotka:
In CSLA .NET 2.0, the cloning method (GetClone()) is virtual/Overridable, so you can do this using Clone().
The drawback to this approach is that the original object may have fields that aren't exposed as properties, and you'd need to use reflection to copy them.


You don't have to use reflection if the code is all in the same class. This is legal code (at least in VB.NET), and is the style we use. I'd post more functional code, but I'm at home right now.

Public Class MyClass
Public Shared Function NewMyClass() As MyClass
    ' Use normal Return DataPortal... line of code here.
End Function
Public Shared Function NewMyClass(ByVal SourceClass as MyClass) As MyClass
    Dim newClass As MyClass = DataPortal... (you can fill out the rest of this line)
    newclass.CopyFields(SourceClass)
End Function
Private Sub CopyFields(ByVal Source as MyClass)
    ' Copy a private field between the objects.
    Me.mSomeField = Source.mSomeField
End Sub
End Class

This pattern can be repeated through any child collections and child objects, if needed.

Of course, if you want to do the copying in a non-specific manner, reflection is the answer. Many of our copied objects have to also do custom behavior. When we create a new order line from an existing order line, for example, we don't copy the comments child collection. We default the comments from the product ID as we would if the user had simply clicked the New button and typed in the product ID. This isn't possible in a generic fashion.

stephen.fung replied on Friday, November 10, 2006

We do something just like the approach hurcane suggested, and it's worked quite well so far.  It is rather tedious to implement without code generation, though.

Something to consider is that some metadata properties you might not want to copy to the new object, like a timestamp or primary key property.  If you're implementing a generic approach using reflection or something, you could consider explicitly maintaining a list of properties not to copy.

brucep replied on Monday, November 13, 2006

Hi albruan:

Use an interface that looks like this:

public interface CloneableIdentity
{
      bool CanCloneIdentity { get; }
      object CloneNewIdentity(object parentKey);
}

[Serializable()]
public class ProjectBusinessBase : BusinessBase, CloneableIdentity
{
      //

      # region CloneableIdentity Members

      public virtual bool CanCloneIdentity
      {
         get { return false; }
      }

      object CloneableIdentity.CloneNewIdentity(object parentKey)
      {
         if(!CanCloneIdentity)
            throw new InvalidOperationException();

         ProjectBusinessBase clone = Clone() as ProjectBusinessBase;
         clone.MarkNew();
         clone.ChangeInfoForIdentityClone(parentKey);

         return clone;
      }

      protected virtual void ChangeInfoForIdentityClone(object parentKey)
      {
      }
       #endregion //--CloneableIdentity Members

}

[Serializable()]
public class SalesOrder : ProjectBusinessBase
{
      //

      # region CloneableIdentity Members

      public override bool CanCloneIdentity
      {
         get { return (IsClosed || IsAllocated) && !IsDirty && IsValid; }
      }

      protected override void ChangeInfoForIdentityClone(object parentKey)
      {
            SetKey(Guid.NewGuid());
            for (int index = m_Items.Count - 1; index >= 0; index--)
               m_Items[index] = ((CloneableIdentity)m_Items[index]).CloneNewIdentity(m_Key) as SalesOrderItem;
            m_DateClosed = DateHelper.GetNullDate();
      }
       #endregion //--CloneableIdentity Members

}

[Serializable()]
public class SalesOrderItem : ProjectBusinessBase
{
      //

      # region CloneableIdentity Members

      public override bool CanCloneIdentity
      {
         get { return true; }
      }

      protected override void ChangeInfoForIdentityClone(object parentKey)
      {
            SetKey(Guid.NewGuid());
            m_ParentKey = parentKey;
      }
       #endregion //--CloneableIdentity Members

}

In the UI, have a Clone or Copy button on your base form, and do something like this:

public class BaseForm : Form
{
      public BaseForm()
      {
            InitializeComponent();
      }

      protected override void OnLoad(EventArgs e)
      {
            if (HasDataSource && DataSource is CloneableIdentity &&
                  ((CloneableIdentity)DataSource).CanCloneIdentity)
            {
                  m_BtnCopy.Enabled = true;
            }
      }
}

In the form code, you would probably want to bind the Enabled property of the Copy button to the CanCloneIdentity property so that it will enable and disable as the CanCloneIdentity property changes, as it could in the case of our contrived SalesOrder example.

Also as far as collections go, it's a good idea to have them implement the CloneableIdentity interface as well, then you can just say "m_Items = (BusinessCollection<SalesOrderItem>)m_Items.CloneNewIdentity(m_Key);", and ensure that the collection "cleans itself up" by quiescently removing items from its DeletedList, etc. After all, you don't want to accidentally delete the items in the deleted list from the database when you save the brand spanking new SalesOrder. Because of the side effects of adding, removing or setting items in a collection, it's always a good idea to provide an explicit method (preferably defined in a special-purpose interface), and call that instead of just using the standard collection mutators (Add, Remove, indexed set, etc.)

This example is a bit contrived and incomplete because I haven't used CSLA for awhile and don't know the current state of the art, but I have used a pattern like this for literally years with great success.

Here's a picture of what mine looks like in action.

id_clone.jpg

Good luck,

--Bruce

albruan replied on Friday, November 17, 2006

Hi Bruce,

I've been seriously looking at using such an interface since seeing your posting in the old forum at http://groups.msn.com/CSLANET/general.msnw?action=get_message&mview=0&ID_Message=21957&LastModified=4675545836911824873

I have a few questions about using the interface as described in your post at the old forum and here and they are as follow:

  1. I notice you have a function called SetKey in which you are passing a new Guid; what is the purpose of this function?
  2. In the ChangeInfoForIdentityClone method in the SalesOrder class, you reference m_Key; how is m_Key derived?
  3. You mention "quiescently removing items" from the collection's DeletedList. Am I safe in assuming you loop through the DeletedList deleting the items one-by-one or do you clear the list with one call?
  4. You also mention providing an explicit method for use in lieu of the standard Add, Remove methods on collections.  I noticed your post in the old forum made a call to clone.AddItem() for each SalesOrderItem.  I guess my question is what exactly would be an explicit method for collection.Add?
  5. You mention implementing the CloneableIdentity interface in each of the collections.  In my case, I have a Departments collection of Department objects, each of which implements CloneableIdentity.  So I should also have the Departments object implement CloneableIdentity as well?

Thanks for taking the time to answer these questions.

BTW, I like your name...my middle name is Bruce, hence the "bru" portion of my username here.

Allen

brucep replied on Friday, November 17, 2006

Hi Allen:

I have to take off on a field trip for my kids in a few minutes here, but I'll get back to your questions sometime this weekend or Monday.

--Bruce

Copyright (c) Marimer LLC