Thursday, October 1, 2009

Really Missing Serialization Callbacks

I just ran into another feature whose absence from Silverlight is sorely missed.

I’m using the WCF generated proxy classes as the basis for binding to some UI objects.  If you’re using a full MVVM pattern, the way that you’d normally do this is wrap the proxy-generated classes with a ViewModel, so that instead of binding to, say, the User class generated by “Add Service Reference”, you’d bind to a UserViewModel class that acts as a facade for the User class.  So far, I haven’t been willing to go that route.  The only point to having a ViewModel is if you’re making extensive use of databinding, and databinding happens to be my least favorite Silverlight technology, for reasons that I’ve explained elsewhere.  And if I’m only doing databinding occasionally, it seems like a lot of more-or-less pointless work to recreate a facade for my complete object model on the client, and then keep it synchronized with the classes that the Entity Framework has already helpfully generated for me.  So I’ve been making do with partial classes to add any additional properties or methods that seem appropriate.

But as I said, I ran into a problem today.  One of my classes, SharedFile, has a bindable property called StatusText whose value depends on a complicated graph of other object properties; and to get them all working, I’ve had to string together an unpleasant chain of INotifyPropertyChanged notifications, sorta like so (this is just one of numerous chained handlers):

   1: void uploadCommand_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
   2: {
   3:     if (e.PropertyName == "State")
   4:     {
   5:         UpdateStatusText();
   6:     }
   7: }

OK, so far so good – but I have to wire up these event handlers somewhere.  Since the definition of SharedFile in reference.cs didn’t define a default constructor, I thought it would be simple to create one in my partial class, and that would be the end of it:

   1: public SharedFile()
   2: {
   3:     this.PropertyChanged += SharedFile_PropertyChanged;
   4: }

But although the objects get created, that constructor never gets called.  WTF?  Well, it turns out that when Silverlight deserializes the XML from my WCF service, it uses FormatterServices.GetUninitializedObject to create the object, which skips calling the constructor.  I guess that makes a certain sort of sense – the WCF service is handing you an object that, in theory, is already constructed, so you shouldn’t need to call the constructor again.  I get that.  But then where do I put this code?

OK, I know, I can put that code in a method that I tag with the [OnDeserialized] attribute.  That’s the normal way of doing it, right?  Oh – except Silverlight doesn’t support serialization callbacks.

Huh?

I get that some features need to be left out of Silverlight.  But this seems like a really odd one.  Serializing and deserializing objects is what you do in Silverlight.  You can write real-world WPF or WinForm or ASP.NET applications all day long and never once have to deal with object serialization and deserialization.  But you can’t use Silverlight for five minutes without needing to touch an object that’s just been deserialized from some web service.  So why choose that particular set of features to cut?  It sure seems like a bizarre design choice.

At any rate, my choices were either to implement a full-blown ViewModel layer, which I really don’t want to do, or write a hack of some sort to initialize the event handlers manually.  Uggh.

What I’ve done for now is to throw an Initialize() method on the containing object (User), which in turn initializes any object that gets added to its SharedFiles ObservableCollection:

   1: // This is necessary because Silverlight doesn't call a constructor when deserializing classes,
   2: // and also doesn't support on the [OnDeserialized] attribute.  Damn annoying.
   3: public void Initialize()
   4: {
   5:     InitializeSharedFileList(this.SharedFiles);
   6:     SharedFiles.CollectionChanged += SharedFiles_CollectionChanged;
   7: }
   8:  
   9: private void SharedFiles_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
  10: {
  11:     if (e.Action == NotifyCollectionChangedAction.Add)
  12:     {
  13:         InitializeSharedFileList(e.NewItems);
  14:     }
  15: }
  16:  
  17: private void InitializeSharedFileList(IList sharedFileList)
  18: {
  19:     foreach (SharedFile sharedFile in sharedFileList)
  20:     {
  21:         sharedFile.Initialize();
  22:     }
  23: }

And then I call User.Initialize() when I first retrieve it from the web service:

   1: private User user;
   2: public User User
   3: {
   4:     get
   5:     {
   6:         return user;
   7:     }
   8:     set
   9:     {
  10:         if (!object.ReferenceEquals(user, value))
  11:         {
  12:             user = value;
  13:             if (user != null)
  14:             {
  15:                 user.Initialize();
  16:                 UserId = user.UserId;
  17:             }
  18:             else
  19:             {
  20:                 UserId = string.Empty;
  21:             }
  22:         }
  23:     }
  24: }

Like I said, uggh.  But it works.  I just wish that MS had thought through their deserialization scenarios a little better.  I don’t like being forced into creating another abstraction layer if I don’t have to.