PropertyChanged not raised with remote data portal

PropertyChanged not raised with remote data portal

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


David posted on Monday, August 28, 2006

I have just noticed that the PropertyChanged event is not raised on my BusinessBase objects when I configure my aplication to use the remote data portal. When I change the configuration back to local everything works fine. I use this event to detect when the user changes either the quantity or unit price and then recalculate the total.

Any hints as to why this is so?

 

ajj3085 replied on Monday, August 28, 2006

Where are you raising your property changed events?  The only way you should encounter this problem is if you're raising the event from code within a DataPortal_ method..

David replied on Monday, August 28, 2006

No I'm not raising the event from the data portal. I'm not actually raising the event directly, it's being raised from the standard call to PropertyHasChanged() within the property Set routine. That's the weird bit really - I don't see how changing the location of the data portal should have any effect on that part of my code.

I just stepped through the code using both options (remote and local) and found the differing behaviour follows this line of code in BindableBase:

mNonSerializableHandlers.Invoke(sender, e)

With a local data portal the next line of code executed is the PropertyChanged event in my BO. But with the remote dataportal it is not raised.

David

Brian Criswell replied on Tuesday, August 29, 2006

That is because your object has been serialised (copied) to the remote data portal.  It is a copy of your object that the data portal works with, not the original that all of your event handlers are linked to.

RockfordLhotka replied on Tuesday, August 29, 2006

In today's world, with the technologies in widespread use, it is not realistic to raise events from a server back to a client. The data portal doesn't even pretend to offer this sort of behavior, because it isn't realistic to create an implementation that can work in all (or even most) cases.

The reason is that most clients don't have "real" routable IP addresses. This means that the server can't reliably establish a TCP connection to the client, and thus can't use any of the "modern" network protocols (remoting, asmx or even WCF) to send an event to the client.

You can get a bi-directional channel for remoting - there are a couple 3rd party ones out there. And WCF has a bi-directional channel. Both use a persistent TCP socket connection and do some internal message routing to make this work, so both are connection-based and cost you scalability. There is no equivalent for asmx or WCF-when-used-like-asmx because the HTTP world doesn't believe in this sort of thing.

Regardless, I don't view this as a generally available capability, and so the data portal makes no effort or provision for raising events from server back to client.

If YOU use a bi-directional channel, you can make it work yourself though. All you need is a MarshalByRefObject ON THE CLIENT that subscribes to your business object's events. Your business object needs to keep an explicit reference to this object (or you can put a reference in ClientContext maybe), and then your OnDeserialized() method needs to rehook the events when the business object is deserialized onto the server. The reference to the client-side MBRO will remain valid, and the events should flow back automatically.

Again, this will only work if your client has a routable IP address (the server can ping the client basically), or if you have a bi-directional channel for remoting or WCF. And honestly I don't know if this will work with WCF - I've not tried playing with MBRO's and WCF at all, and they may not support the concept (i.e. it might be remoting only).

xal replied on Tuesday, August 29, 2006

Well, active objects does that, but not in real time.
Basically what it does is store all the channel events in a buffer, and refire them when it returns from the dataportal.

Andrés

David replied on Wednesday, August 30, 2006

Thanks Brian, Rocky and Andrés for the lengthy responses. However I'm confused about what this has to do with events raised on the server. This is the how I thought it worked:

  1. My form calls the GetObject factory method.
  2. The object is created on the server, serialized and sent back to the client, so my form now has a copy of the object that was created on the server. That's it for the server.
  3. I bind some of the properties of my object to controls on my form.
  4. The user edits a control which updates my object by calling the bound property Set.
  5. The property Set calls PropertyHasChanged().
  6. Somewhere in BusinessBase the PropertyChanged event is raised.
  7. My object responds to the PropertyChanged event.

So if the action that is supposed to raise the event occurs long after the object is returned from the server, why does it matter if the object was serialized?

 

RockfordLhotka replied on Wednesday, August 30, 2006

You are correct David, the object (on the client) should raise events that you and/or data binding can handle.

Assuming your code follows the order of steps you list, that should work fine. You have to hook up the event handlers after you get the object from the factory method to make sure you are hooking the correct object instance.

Also, Save() returns a new object, and you need to re-hook your event handlers to the object returned from Save() to continue to get events after a save operation. This is discussed in the book, but there's also errata on www.lhotka.net that is important in this case.

So it doesn't matter that the object was serialized (or not). The event hookups occur after deserialization.

We are all assuming that you are somehow hooking the events before serialization and expecting the events to continue to function. That won't happen, because the serialization/deserialization process unhooks the event handlers. But that doesn't appear to be what you are describing, so you must be facing some other issue.

David replied on Wednesday, August 30, 2006

Thanks Rocky. I’ve looked at the sequence of things but I am still none the wiser. Here’s the scenario:

I have a BO named Cost which has properties including Quantity and UnitCharge. Within the BO I have this event handler:

Private Sub Cost_PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Handles Me.PropertyChanged
  If e.PropertyName = "Quantity" OrElse e.PropertyName = "UnitCharge" Then
    ResetCalculatedFields()
  End If
End Sub

As I pointed out in an earlier post, when I step through the code in debug mode, I can see that a change on the form always triggers the property Set, which invokes PropertyHasChanged. I also have a break point on the first line of the event handler above, so I can see that it does get raised with the local data portal, but not with the remote one. Therefore I believe I can leave data binding (and my form) out of the equation here as the issue is within my BO.

In both cases (local and remote) the code execution path is identical up to this line in BindableBase:

mNonSerializableHandlers.Invoke(sender, e)

With a local data portal the next line of code executed is the PropertyChanged event handler (above). But with the remote dataportal it is not raised.

So in terms of hooking up events, I don’t see where I can change anything.

Any other clues?

RockfordLhotka replied on Wednesday, August 30, 2006

My guess is that the "automatic" WithEvents behavior of Me doesn't support serialization. I don't know that for sure, but that's my guess. I imagine that if you explicitly rehook the event handler in an OnDeserialized() override that it will work just fine.
 
Rocky
 

xal replied on Wednesday, August 30, 2006

Rocky,
I can confirm that. The Handles clause just makes the compiler create "AddHandler" code in the constructor.
I experienced this while working on the VB templates, and this obviously doesn't support (de)serialization.
The solution is obviously create the handlers with AddHandler and recreate them after deserialization has occured...


Andrés

David replied on Wednesday, August 30, 2006

Thanks guys. I have limited experience with events in .Net so any suggestions are most welcome.

I have added the following to my BO:

Protected Overrides Sub OnDeserialized(ByVal context As System.Runtime.Serialization.StreamingContext)
  MyBase.OnDeserialized(context)
  AddHandler PropertyChanged, AddressOf Cost_PropertyChanged
End Sub

It now works for both local and remote, but I had to leave the "Handles Me.PropertyChanged" statement in place otherwise it won't work for the local case. Do you think it would be more consistent to get rid of this and use AddHandler for both cases, and if so, where would you put it to cater for the local data portal?

Also, I read somewhere that you are supposed to explicitly remove any handlers added this way. Is this necessary and if so where is it best placed?

Although it doesn't affect my current requirement, I also noticed that there is still a difference in behaviour between the two cases. With the local data portal the PropertyChanged event is raised once when the object is first created with e.PropertyName = "". This doesn’t happen with the remote data portal and I assume this is a case of an event being raised on the server and me not seeing it on the client.

RockfordLhotka replied on Wednesday, August 30, 2006

There's a call to MarkDirty that triggers OnUnknownPropertyChanged, which raises the "" event you are seeing. That all happens on the app server with a remote data portal, and so no, you wouldn't see it on the client in that case.
 
If you do want to be consistent in setting up the event handler, you can override Initialize() to do this.
 
You don't have to worry about unhooking events that are handled by the same object. An event is an object reference, and so unhooking the event released that reference. But an object referencing itself doesn't matter, as that won't stop the GC from doing its job correctly.
 
Rocky

tymberwyld replied on Monday, August 28, 2006

Probably because your BO's are not inheriting from "MarshalByRefObject".  If you look at DataSets, they inherit from a MarshalByRefObject as well as implement ISerializable.  Doing this will enable you to raise events because the BO will exist on the Client Side and it's Proxy will transfer the event across the wire.  Granted, this is a noisy object, but it will work.

Currently, your BO's are only "Serializable".  This means that your event IS getting raised, but you app can't "hear" it because its not the same object reference once it goes through remoting / serialization.

Copyright (c) Marimer LLC