Friday, August 6, 2010

Adventures while building a Silverlight Enterprise application part #35

In this post we’ll look into something I ran into while doing rework on our framework. It involves the order in which code gets executed when using events and such.

The situation

We’re currently in the process of getting our first deliverable out the door on time. One of the things that needed to be addressed before we can actually start testing this code properly, is to get a backlog of issues out of the way. I decided to do that myself as it gives me a better feel for what are common problems and what is the quality of work we as a team put out. Also I realized that issues may had arisen because of wrinkles in the framework architecture, where we’ve had to take shortcuts to get to our first deliverable and I wanted to be able to address these as needed.

Some architecture insight

For you to make sense of what I did to solve one of the problems I encountered, it is important to have some understanding of how our application works (at least at the client side). We host several ‘applications’ insight our Silverlight client. In the end we will have probably about six of these applications. Each application follows the same design. On the left there is navigation, consisting of a ‘menu’ and some UI to select records. Depending on the type of record the user selects and whatever authorization the user has we load modules to work with the data.

To create new records, we use a concept we call a task. This is basically a wizard like UI, which runs as a popup on top of the other UI. It consists of some of the same modules as used when the user navigates to a record, only in a different visual state. This makes for some interesting scenario’s as it is likely that there are two instances of the same module using and updating resources that only have a single instance.

One of these scenario’s is updating the navigation after creating a record through a task. As the task does not have any knowledge on what the module does, it is the modules responsibility to update navigation after creating a new record. To do that, it updates something we named ViewState, which in turn updates both the navigation and the modules that are needed to present this new record to the user.

However, creating the record is usually done in the first module of the task, before other modules can save their data (because they might need to create a relationship with the new record). This means that whenever the modules get the signal to load new data, that most of them can’t because the data is not there yet. It has everything to do with the order in which things are executed, especially because we use events to directly update our modules.

A design oversight

The fix is actually simple once you realize we had a design oversight in our ViewState. Why should you update navigation and load data into modules, when you have a popup presented to the user and the user can’t interact with the newly loaded data? It doesn’t make any sense. So the fix was to get the ViewState to hold any updates to the rest of the application if a popup is being presented and trigger the events once the popup is closed.

The first step to do that was to create a generic way to trigger the events in the ViewState. This is what I came up with:

   1: private void InvokeEvent<T>(MulticastDelegate target, T args) where T:EventArgs



   2: {



   3:     if (target != null)



   4:     {



   5:         if (PopupBridge.Bridge.IsPopupOpen)



   6:         {



   7:             EventHandlerQueueItem<EventArgs> queueItem = new EventHandlerQueueItem<EventArgs>();



   8:             queueItem.EventHandler = target;



   9:             queueItem.EventHandlerArguments = args;



  10:             _eventQueue.Enqueue(queueItem);



  11:         }



  12:         else



  13:         {



  14:             target.DynamicInvoke(new object[] { this, args });



  15:         }



  16:     }



  17: }




The InvokeEvent<T> method takes a MulticastDelegate (which represents the actual event instance) and an args argument based on T where T should always be an EventArgs or derive from it. To make this as easy to use as possible I first check to see if there is actually a valid target (which is null if no handlers are available). Next I check to see if there is any popup open. In our case there is a PopupBridge that has that information for us. Now if there is no poup open I simply call DynamicInvoke on the target and pass in a reference to the ViewState and the args argument.



If there is a popup open, though, I create a EventHandlerQueueItem. This is how that struct looks:





   1: internal struct EventHandlerQueueItem<T> where T:EventArgs



   2: {



   3:     public MulticastDelegate EventHandler;



   4:     public T EventHandlerArguments;



   5: }






As you can see it is a simple struct that has both a MulticastDelegate and can contain arguments to pass off to any handlers. Once we had that, we could now declare a Queue<T> to hold these instances. As we can’t write something like Queue<EventHandlerQueueItem<T>> I decided to use covariance and go with Queue<EventHandlerQueueItem<EventArgs>>.



Now all that remains is to trigger the events once the popup is closed. In our case the PopupBridge knows about this and triggers an event for it. The handler looks like this:





   1: void Bridge_PopupClosing(object sender, PopupClosingEventArgs e)



   2: {



   3:     if (!PopupBridge.Bridge.IsPopupOpen)



   4:     {



   5:         while (_eventQueue.Count > 0)



   6:         {



   7:             EventHandlerQueueItem<EventArgs> item = _eventQueue.Dequeue();



   8:             if (item.EventHandler != null)



   9:             {



  10:                 item.EventHandler.DynamicInvoke(new object[] { this, item.EventHandlerArguments });



  11:             }



  12:         }



  13:     }



  14: }






As there can be multiple popups open I still need to check if there is a popup open at that point. If they are all closed, we can just keep calling Dequeue and invoke the handlers until the queue is empty.



Now that we have solved this design oversight, modules that need to load data that was created in a task, will always be able to get that data from the server and everything works as expected.



I hope you find these pieces of code useful. If you have any questions or comments, please leave them below.

No comments:

Post a Comment