Interface WithEvents of a BusinessBase throws serialization exception on Clone

Interface WithEvents of a BusinessBase throws serialization exception on Clone

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


GPhillips posted on Thursday, August 12, 2010

I have a problem with is now occuring since I rewrote my CSLA objects to use Lambda expresions and the "(Of ...) syntax and had to add an Interface.  My Object structure looks like this:

Class Entity(Of E As Entity(Of E))
    Inherits BusinessBase(Of E)
    Implements IEntity
    Shadows Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements IEntity.PropertyChanged

    Private Sub Entity2008_PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Handles MyBase.PropertyChanged

        RaiseEvent PropertyChanged(sender, e)

    End Sub

Class CustomerBase
    Inherits Entity(Of CustomerBase)

Class Customer
    Inherits CustomerBase

The IEntity Interface is intended to operate on any object deriving from Entity in a generic fashion whithout having to know the type of object.  My intent is to allow access to various functions provided by CSLA as well. It looks like this:

Interface IEntity
    Property Name() As String
    ...
    ReadOnly Property IsDirty() as Boolean

    Function Clone() as IEntity
    Function Save() as IEntity
    Event PropertyChanged(sender as object, e as System.ComponentModel.PropertyChangedEventArgs)
End Interface

Code in the Entity Class to Support this looks Like this:

    Public Shadows Function Save() As IEntity Implements IEntity.Save

        Return CType(MyBase.Save(), IEntity)

    End Function

    Public Shadows Function Clone() As IEntity2008 Implements IEntity2008.Clone

        Return CType(MyBase.Clone(), IEntity2008)

    End Function

So here's the problem: If I use a form to test this and have a Dim ob as Customer and retrieve, change and Save, everything works fine.  If I do Dim ob as IEntity, everything works OK too.  If I do Dim WithEvents of Customer or IEntity, everything works.  BUT, if I add a PropertyChanged event handler (Private Sub PropertyChanged(sender as object, e as ...) Handles ob.PropertyChanged, to the form (and WithEvents to the Dim), the Save blows up with a Serialization Error when it tries to serialize the object.  It says the FORM is not serializable.  I need to get the PropertyChanged event when using IEntity.  I walked through the CSLA code, but I cannot see why it would attempt to include the form in the BusinesObject's serialization.  HELP!

It does not blow up with just the WithEvents, but blows up if the PropertyChanged event handler is defined.

Any help or thoughts would be greatly appreciated.

Gary

tmg4340 replied on Thursday, August 12, 2010

Once you attach a method to an event handler, the object that contains the method becomes part of your event source's object graph.  So once you attach a method on your form to your Customer object's PropertyChanged event (or the interface event, since they are the same thing), that adds a reference of the form to your Customer object.  Since the event holds a reference to the form, when CSLA attempts to serialize your Customer object, it will try to serialize the form as well.  And forms are not serializable.

Defining the object with the "WithEvents" keyword doesn't automatically hook the events to anything, which is why that doesn't cause the serialization exception.  It just makes the VB.NET compiler aware that the object is capable of raising events (and allows you to hook to those events using the "Handles" clause.)

That's why, when you look through the CSLA source code, you see that the events are either marked with the NonSerialized attribute or use the alternate event-management syntax that allows you more control over event wireups.  Since you're essentially replacing the default PropertyChanged event with your own, you'll need to implement the same scheme in your objects (including re-hooking your event handlers in the appropriate deserialize methods.)

Also, I might suggest that rather than re-implement PropertyChanged, you might just want to have your IEntity interface inherit from INotifyPropertyChanged.  This is the .NET interface that defines the PropertyChanged event that enables data-binding.  Doing so will make your interface a little cleaner, more intentional as to what you're trying to do, and possibly may let you leverage the existing CSLA code surrounding PropertyChanged management (which means you won't have to re-implement that code.)

HTH

- Scott

GPhillips replied on Friday, August 13, 2010

Thanks for your response, Scott.

It makes sense that the object would have to have a reference in order to call the event.  But if I remove the event from the interface (and the supporting code from Customer), and Dim WithEvents ob as Customer, and handle the PropertyChanged event that BusinesBase provides, everything works fine.  How can CSLA treat the event handler differently since it looks the same in the VB code?

What I'm really trying to accomplish here is a generic ability to access common properties and events of any object that derives from Entity, without knowing what the object actually is.  This worked fine when it was coded for CSLA 2.0 since my object model looked like BusinessBase -> Entity -> CustomerBase -> Customer (without the "(Of ...)").  In that scenario I could do a Dim WithEvents ob as Entity and everything was happy.  With the "Of" syntax, I have to specify the type (Dim WithEvents ob as Entity(Of CustomerBase))  which defeats the whole purpose - unless there is some way to pass that in as a variable?  I would rather not have to use the IEntity interface since that adds another level of complexity.

I tried to do it without the "Of"s:

Class Entity
    Inherits BusinessBase(Of Entity)

Class CustomerBase
    Inherits Entity

Class Customer
    Inherits CustomerBase

but the RegisterProperty statements don't work (I kept getting Locked messages when the downstream objects tried to add their properties).

Is there some other way to accomplish this that I'm missing?

Gary

 

tmg4340 replied on Friday, August 13, 2010

If you take a look at the BindableBase file in the CSLA source (and are comfortable reading C#), you'll see how Rocky gets around this problem.  He uses an alternative event-wiring syntax that lets him write code to determine whether the object containing the method being hooked to the event is a serializable object.  If it is, the method gets added to one event handler.  If it isn't, the method gets added to a different event handler which is decorated with the NonSerialized attribute.  That means that when CSLA serializes the object, the objects containing the methods hooked to that event handler don't get serialized.

The second part of this solution is that those non-serialized event handlers have to be re-hooked in the deserialization hook methods that are available in CSLA objects (the "OnDeserialized" methods).  Since those event wirings are lost in the serialization process, CSLA can't handle that for you - you have to do it in your code.  Which means you have to know when you hook a method contained in a non-serializable object, and deal with that in your business-object code.

In terms of your general problem, I would think that an interface (e.g. IEntity) that defines your common properties would be enough.  It depends on what your "common properties and events" consist of, but most likely the interface doesn't have to be generic.  You just create your interface and make sure that your business objects are marked as implementing that interface.  The nature of interface implementation is such that normal business-object property coding can likely fulfill the needs of the interface.

The potential downside is that you have to implement all these common properties in every business object.  But given what you're trying to do, plus the requirements of CSLA regarding the generic type (it must extend through the entire type hierarchy, otherwise you get the RegisterProperty errors you saw), that's probably a better idea anyway.  Plus, one of the "guiding principles" that Rocky has espoused about object design is that inheritance is primarily for behavior reuse - and properties are not behavior.  They are, for the most part, boilerplate plumbing code.

And as for passing the generic type in a variable, you're out of luck.  Generics are not polymorphic - they expect a static type.  That's usually the first disappointment people run into when they start working with generics... Smile

Finally, If you need your interface to participate in events that CSLA objects raise, the key there is that pretty much every event that's potentially interesting in a CSLA object comes from some other interface (e.g. PropertyChanged comes from the INotifyPropertyChanged interface.)  Some of them are .NET interfaces, while others are CSLA interfaces that Rocky created.  Since interfaces can inherit from other interfaces, you can make your life simple by letting your IEntity interface inherit from these other interfaces containing the event(s) you need.  Then your interface gets all those method signatures, plus the default CSLA implementations, for free.  You might end up with more events in your IEntity interface than you really care about, but I don't consider that a big problem.

HTH

- Scott

GPhillips replied on Friday, August 13, 2010

Scott,

 

Thanks again for your time assisting with this.

As a VB programmer I'm having a little trouble keeping up with this discussion, but my head is still above water.

First, I am intrigued by your last paragraph "you can make your life simple by letting your IEntity interface inherit from these other interfaces containing the event(s) you need.  Then your interface gets all those method signatures, plus the default CSLA implementations, for free".  If I just add, for instance "Inherits IUndoableObject" in the Interface does that automatically hook up all the Undoable Objects Events?

I am trying to create an EntityChanged Event, which fires if a change is made to either the Entity itself (Customer, in this case) or a change is made to any of its children (Addresses, for example).  Obviously if the first paragraph holds true, then I can just inherit the appropriate interfaces and check for both the PropertyChanged and Child_PropertyChanged events.  However, I am attempting to do this as a new event within the Entity(Of T) class and the IEntity interface.

What I tried is as follows:

Add an Event to the IEntity interface:   Event EntityChanged(sender as object, e as System.Eventargs)

Add the following code to the Entity(Of T) definition:

    <NonSerialized()> _
    Private _NonSerializableEntityChangedEventHandlers As EventHandler
    Private _SerializableEntityChangedEventHandlers As EventHandler

    ' See pg 277-279 of "Expert VB2008 Business Objects" for a discussion of this
    Public Custom Event EntityChanged As EventHandler Implements IEntity2008.EntityChanged
        AddHandler(ByVal value As EventHandler)
            If value.Method.IsPublic AndAlso (value.Method.DeclaringType.IsSerializable OrElse value.Method.IsStatic) Then
                _SerializableEntityChangedEventHandlers = DirectCast(System.Delegate.Combine(_SerializableEntityChangedEventHandlers, value), EventHandler)
            Else
                _NonSerializableEntityChangedEventHandlers = DirectCast(System.Delegate.Combine(_NonSerializableEntityChangedEventHandlers, value), EventHandler)
            End If
        End AddHandler

        RemoveHandler(ByVal value As EventHandler)
            If value.Method.IsPublic AndAlso (value.Method.DeclaringType.IsSerializable OrElse value.Method.IsStatic) Then
                _SerializableEntityChangedEventHandlers = DirectCast(System.Delegate.Remove(_SerializableEntityChangedEventHandlers, value), EventHandler)
            Else
                _NonSerializableEntityChangedEventHandlers = DirectCast(System.Delegate.Remove(_NonSerializableEntityChangedEventHandlers, value), EventHandler)
            End If
        End RemoveHandler

        RaiseEvent(ByVal sender As Object, ByVal e As System.EventArgs)
            OnEntityChanged()
        End RaiseEvent
    End Event

    Protected Overridable Sub OnEntityChanged()
        If _NonSerializableEntityChangedEventHandlers IsNot Nothing Then
            _NonSerializableEntityChangedEventHandlers.Invoke(Me, New EventArgs)
        End If
        If _SerializableEntityChangedEventHandlers IsNot Nothing Then
            _SerializableEntityChangedEventHandlers.Invoke(Me, New EventArgs)
        End If
    End Sub

    Private Sub Entity2008_ChildChanged(ByVal sender As Object, ByVal e As Csla.Core.ChildChangedEventArgs) Handles Me.ChildChanged

        RaiseEvent EntityChanged(sender, New EventArgs)

    End Sub

    Private Sub Entity2008_EntityChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Handles MyBase.PropertyChanged

        RaiseEvent EntityChanged(sender, New EventArgs)

    End Sub

(I thought it would be helpful for anyone else going down this path to see the code)

Two things happened with this:

1. If the event were not in the IEntity and the Implements is removed, everything worked fine until the object was saved/cloned, then event stopped working (expected), since I have no idea how to accomplish rehooking the event after the object is deserialized and I can't find anything in Rocky's code that makes sense (to me) as an example [an assist would be helpful here]

2. When I added the Event definition to the interface, and the Implements to the event definition (above), I get a compiler error: Event 'EntityChanged' cannot implement event 'EntityChanged' on interface 'IEntity' because their delegate types 'System.EventHandler' and 'IEntity2008.EntityChangedEventHandler' do not match.  (Huh?)

So that is were I am, incredibly further than I would have been without your help.  It looks like it just needs a little adjustment SOMEWHERE and it will work.

Thanks again for looking at this,

Gary

tmg4340 replied on Monday, August 16, 2010

Sort of.  If your IEntity interface inherits from IUndoableObject, then what happens is that interface contract becomes part of your IEntity contract.  When you say that your Customer business object implements IEntity, you're required to implement all the members of IUndoableObject.  However, the CSLA base class already does that.  So, you don't have to write code for those members - and you get to utilize the code Rocky wrote in the base classes to manage hooking and unhooking from events (plus all the other code he wrote to implement the rest of the interface.)

So, for example, if your IEntity inherited from INotifyPropertyChanged, then when you apply IEntity to your business object, you get to utilize Rocky's implementation of the PropertyChanged event - which means you don't have to replicate the code he wrote to deal with nonserializable event sources.

However, if all you're using your IEntity interface for is a way to get some event noitifications in a generic way, I don't think you'll need it.  PropertyChanged comes from the INotifyPropertyChanged interface, while the ChildChanged event comes from INotifyChildChanged.  You should be able to cast any business object to either of those interfaces and deal with them that way.

Having said that, if you still want your IEntity interface, you should be able to get what you want with something like this:

Interface IEntity
    Inherits INotifyPropertyChanged
    Inherits INotifyChildChanged
End Interface

Then when you apply IEntity to your business objects, everything Rocky has written in BusinessBase should just get picked up for the PropertyChanged and ChildChanged events.

In terms of dealing with event hooks from things like forms, look for the OnDeserialized method in your business objects.  It's a protected method that is called after the deserialization process is completed.  That's where you would need to re-hook your event methods for your non-serializable sources (like your Windows form).

HTH

- Scott

GPhillips replied on Monday, August 16, 2010

I was able to get rid of the compiler error by changing the event definition in the Interface to Event EntityChanged as System.EventHandler, omitting the parameters (which made it too specific).

The last sticking point I have is rehooking the event after it has been deserialized.  I found an example in the CSLA code as follows:

    protected override void OnDeserialized()
    {
      foreach (IEditableBusinessObject child in this)
      {
        child.SetParent(this);
        INotifyPropertyChanged c = child as INotifyPropertyChanged;
        if (c != null)
          c.PropertyChanged += new PropertyChangedEventHandler(Child_PropertyChanged);
      }
      base.OnDeserialized();
    }

The sticking point is that this code won't convert to VB.NET.  I tried duplicating this exact code inside a business object.  The c.PropertyChanged reference does not compile and does not appear in intellisense,

The way I have added handlers in the past is to use the AddHandler method:

AddHandler obj,EntityChanged, AddressOf EntityChangedHandler

But that would only work if I knew the routine that had to be hooked (which is in the Form, not in the business object) thus I cannot figure out how the business object would restore the form's handler.  When it deserializes, the entries in the private _NonSerializedEventHandlers are gone, so how is it possible to know what was there so that they can be rehooked?? 

Am I missing something here?

GPhillips replied on Monday, August 16, 2010

Ah, light bulb goes on (after another few hours of breakpoints and stepping code).

The problem is not that the external event handlers are unhooked when the business object is deserialized, but the object's internal event handlers are somehow unhooked.  The Form adds its handlers back when the new, saved object is assigned to its local, WithEvents variable, but the event handlers in the business object stop firing.  In this example, i was handling the PropertyChanged and the ChildChanged events within the business object so that either of them would fire my EntityChanged event.  After deserialization, those two handlers stopped working and thus the EntityChanged Event stopped working even though it actually had been rehooked automatically.

The final piece of the puzzle is to add the following to the business object:

    Protected Overrides Sub OnDeserialized(ByVal context As System.Runtime.Serialization.StreamingContext)
        MyBase.OnDeserialized(context)

        AddHandler PropertyChanged, AddressOf Entity_PropertyChanged
        AddHandler ChildChanged, AddressOf Entity_ChildChanged

    End Sub

This reconnects the internal handlers for the PropertyChanged and ChildChanged Events which are as follows:

    Private Sub Entity_ChildChanged(ByVal sender As Object, ByVal e As Csla.Core.ChildChangedEventArgs) Handles Me.ChildChanged

        RaiseEvent EntityChanged(sender, e)

    End Sub

    Private Sub Entity_PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Handles Me.PropertyChanged

        RaiseEvent EntityChanged(sender, e)

    End Sub

In retrospect, this may have worked earlier if I had overridden the OnPropertyChanged and OnChildChanged methods and did the RaiseEvents in those routines.  The possible benefit of that method (other than not stumbling on the unhooking event problem) is that I could control the order the events were fired if I needed the ChildChanged event fired before or after my EntityChanged Event.  The overrides method also does not require the events be rehooked since it does not use them.

Thanks for all your help on this, Scott.  You are a real trooper.  If anyone happens on this thread and would like a complete sample of the pieces, let me know.  We actually processed a lot of different issues in the post including:

Form is not serializable error on Save or Clone
Creating an Interface with custom Events
Inheriting CSLA interfaces in custom interfaces
Rehooking events after deserialization due to Save or Clone

Whew!

martinward replied on Friday, August 20, 2010

Hi Gary,

I have found this post very interesting and have learnt a lot form it. Scotts input was excellent, I would really appreciate a the copy of the code to play with.

It’s nice to see VB being used

Many thanks

Martin

 

 

 

Copyright (c) Marimer LLC