Introduction
Sorry, but that’s the best title I could come up with. A better title might have been – “Examining the concepts provided in CAL and comparing them to how I’ve done things in the past and how CAL might make my life easier.” But I think you’ll agree, it would have been a little too ‘wordy’ for a title. However, that is the intent of the article. I started reading the Composite Application Guidance because I was curious about its capability to facilitate multi-platform development. Well, it's a lot more than that. The 'guidance', as I'll call it from now on, is a collection of suggested practices for building complex WPF applications. It includes sample code, a reusable library (Composite Application Library - CAL) to facilitate implementations of the guidance, and a reference implementation application of the guidance.
The Patterns & Practices group, which released the guidance, suggests the following when evaluating the guidance:
- Do a fit analysis to determine if any part of the library might fit your requirements,
- Perform an initial evaluation by reviewing some of the sample code to understand the various concepts, and
- Perform an in-depth evaluation by using the sample application provided with the guidance, or develop a proof of concept application.
This article is step #3 for me. Following a ready-built application provides a lot of insight into how others might do something. That in itself is a powerful learning tool. But I like to get my hands dirty. And, the requirements of the day job are usually different than those of reference applications. One of the easiest ways to learn is by comparison. So, I decided to use a 'composable' application framework I had developed in the past as a reference for the guidance. This way, I could compare what I had done in the past, and see how each of the concepts in the guidance could have made my work easier or allowed me to develop a better solution.
In order for some of this to make sense for you, you will probably have to do some homework. I would guess it might be a good idea for you to get a hold of the library and read the guidance at some point. And, most likely in the end, you will probably want to develop your own proof of concept application.
Pass it Forward
I have learned a tremendous amount from reading articles published by other people. I also know that they spent a lot more time writing them than I do reading them. So, this is my contribution or payment, if you will, for all I’ve received. And, I know that I’ve received much more than I have paid back. There is also a second, partly selfish motivation on my part. When I wrote SWAT a while back, I thought I was never going to be done. I was sure I had dug myself a hole that I was not going to be able to climb out of. But in the end, I saw that it was worth it because of all that I had learned. So writing an article, like teaching, helps us in the learning process. Composing the format of an article forces you to form a broader perspective. I also find that it forces me to delve deeper into details than I would otherwise. Finally, there is also a lot of satisfaction in receiving a comment from someone that has benefited from the article. That, cannot be measured. Along those lines, if you find anything in the article that could be done better, please put in a comment. This way, everyone benefits, and I am always ready to learn. OK, enough of the small talk...
First Impressions
When I first looked at the guidance, I said, "Whoa, that’s a hellova Hello World!". No 'drag-drop, click, click, F5' and you are done here. It takes some effort to get those two little words on the screen. So, my first reaction was that it will take a little chewing in order to absorb it. And, to be honest, that’s what the guidance recommends, as is indicated above. I also mentioned above that the guidance is for building 'complex' applications. What I meant is that there is a minimum threshold of application complexity below which it does not make sense to use the library. That is, it is not intended for a simple dialog based application. That said, it also does not have to be used as an all-or-none solution. You can use the library in an 'a la carte' mode. Just pick and choose which functionality might fit the need. You can even modify the code to tweak the functionality to match your specific requirement.
One other thing, and let's keep this between you and me, because I really don't have any authority to say it. But, I did get the feeling that parts of the library should have been included in WPF. That’s just my gut feeling. Maybe, they were intended for the release, but some of the other more glamorous aspects of WPF got priority. I was very impressed with WCF because it seemed to me to be such a complete solution. Of course, WPF was probably a much larger challenge, but... Anyway, we'll get into more details once we get into the meat of the matter, and I'm not there yet.
Modular Design
One of the main focuses of the guidance is on modularity. And modular design is good, right? We all know the benefits.
- It allows us to develop and test in parallel,
- It facilitates independent fixes and upgrades,
- It promotes re-use, and
- It helps us in making complex applications more manageable and easier to understand (design).
However, the minute you break up an application into pieces, you break open Pandora's box. Those application pieces still need to communicate and interact with each other as if they were still one monolithic unit. It is still one application from the user's point of view. And, all the pieces need to play in harmony like an orchestra. That communication and interaction is a hard nut to crack, and that's part of what the guidance addresses.
In addition to the benefits listed above, sometimes there are design requirements that dictate a modular design. In the scenario that's described below, the application is a distributed application which almost by definition has to be modular in composition. Some additional requirements of the application include:
- Allowing updates to be made without recompiling the whole application
- Adding new functionality without recompiling the application
- Allowing dynamic updates of the application
But haven't we been building modular applications all along? VB became popular in the early 90's by making it easy to build pluggable components. OLE and COM were created to facilitate component based design. What's changed? Isn't component based design modular? I think we have terminology issue here. And, I'm going to take a crack at it, but keep in mind, this is just my take on it. I think modular still means the same thing. In my mind, a module is a functional entity. It can be made up of different pieces, but they are all there to do one thing or provide a set of related services. And, you can readily define or identify it, like 'the security module'. Maybe the distinction is simply that we have been working with big chunks and now we can deal with smaller ones. So, here's my summary on this. WPF gives us the ability to build more modular applications, or in more better English, applications with finer granularity. And, CAL provides us with some of the plumbing to be able to 'compose' an application from those smaller pieces as well as facilitate the communication and interaction between those smaller pieces. That's my 10,000 foot view.
WidgeNet, Inc.
The sample code is a ‘fictionalized’ application (it is completely made up, any reference to real companies is most definitely coincidental). It is primarily intended to show the mechanisms that I used to build ‘composeable’ applications in the past. And as mentioned previously, it will provide a platform to compare the concepts provided by CAL. I should mention that some of the functionality for the application was predicated by the system requirements so that not all may be generally applicable. It is a production automation application which means that it will have some unique requirements. For example, it is very important that these types of applications maintain their state in order to be able to recover 'easily' from abnormal conditions. They are also long running applications, which means they are not shut down. And, since they are not shut down, making updates to the application while it is running becomes an important feature. These applications are more often than not distributed, which means that modules of the application will be installed on different machines and need to coordinate their activities. But, even though these may, at first, seem to be unique requirements, I think they are becoming more applicable given the current application development trends (SOA, cloud computing).
So, to provide some context for the application, I’ll present a fictional scenario. WidgeNet is a company that manufactures Weebles. Weebles come in all sorts of configurations, and are made up of Weebits. In other words, Weebits is the raw material that WidgeNet uses to produce Weebles. Up until now, WidgeNet has been assembling Weebles manually. However, they have been experiencing a tremendous increase in demand for their products. As a result, they have contracted Production Automation Partners, Inc. (or PAPI for short) to automate their production facilities. Fig. 1 shows the layout of the facility that will be installed.
Fig. 1 WidgeNet System View
The system consists of three robots (R) that will take Weebits and assemble them into Weebles. When a robot completes assembling a Weeble, an automated fork lift vehicle (A) picks up the Weeble from the robot assembly position and delivers it to the shipping conveyor. Weebits are delivered to the robots as they are needed. When a robot needs more of a particular Weebit, the system sends a message to a terminal on a manned fork lift vehicle (F) operating in the warehouse. The operator then delivers a stack of the requested Weebit to a conveyor that is used to move products into the system. When the stack of Weebit arrives at the other end of the conveyor, an automated fork lift picks up the stack and delivers it to the appropriate robot. All system operations are automated and controlled by the system. The only two manual operations are delivering the raw product to the system and loading the finished products for shipment. And, WidgeNet has plans to automate those operations so that everyone can have a Weeble!
DCAF
Continuing with our scenario, PAPI has developed a framework that allows them to provide customized solutions to clients that is based on a common extensible framework. The framework is composed of an industry specific object model library that allows them to provide the same code base regardless of the specific requirements of any customer. On top of the object model library, they have also incorporated a Dynamically ‘Composable’ Application Framework (DCAF) which provides them additional flexibility in the deployment and management of their solutions. (Note: I’m taking a little liberty here with the acronym. ‘Framework’ might be a little bit of a stretch, but it makes such a nice acronym.)
Fig. 2 shows an overview of DCAF. In general terms, DCAF is composed of one or more UI container modules and one or more service hosting modules. The UI can be ‘distributed’ across different machines depending on the requirements. Distributed here means that the functions could be deployed on more than one machine, not that they communicate with each other. Likewise, the service hosts can be deployed across multiple machines. WCF is the ‘loose’ glue that ties everything together.
Fig. 2 DCAF Overview
Both the UI container and the service hosts are generic, and are agnostic as to the modules that they are hosting. As a result, one or more instances can be deployed, and each will take on a different personality.
DCAF UI Container
Fig. 1 above shows one version of a DCAF container. The one included with the demo code uses a list box as the selection mechanism for the various views hosted by the container. With minor changes, a tree view control could be used instead of the list box. The tree view would allow for categorization of the views for logical groupings. Another variation could use a tab control where there are only a few views to render or where multiple containers are being deployed.
The views themselves are implemented as user controls, and can be developed and tested independently. The container ‘discovers’ what views it is supposed to load from the configuration file. This way, views can be added, removed, or replaced without having to recompile the application. As mentioned above, this also allows multiple containers to be deployed for logical separation of functionality. For example, suppose that the container depicted in Fig. 1 above was deployed to the client. At a later time, it was decided that it might be more efficient if some of the functions were available on another machine. Perhaps, the reporting and the order processing functions were to be performed by different personnel. All that would be required is to copy the container to the other machine, move the appropriate user controls to the second machine, and copy/modify the config file as necessary. To the client, it’s like magic, he now has two for the price of one.
Let’s begin by looking at the code for the UI container. The config file is where the view definitions are identified. A new section is defined which is used to identify the list of modules that make up the application.
<configSections>
<section name="WidgeNetModules" type="WidgeNet.WidgeNetUI.ModulesSection, WidgeNetUI" />
</configSections>
<WidgeNetModules>
<modules>
<module name="Weeble Editor"
ctlmod="WidgeNet.Weeble.WeebleEditor" modassy="WeebleEditorCtl.dll"/>
<module name="Weebit Editor"
ctlmod="WidgeNet.Weebit.WeebitEditor" modassy="WeebitEditorCtl.dll"/>
<module name="System Viewer"
ctlmod="WidgeNet.SystemViewer.SystemViewer"
modassy="SystemViewerCtl.dll"/>
<module name="Weeble Orders"
ctlmod="WidgeNet.WeebleOrders.OrdersViewer"
modassy="WeebleOrdersCtl.dll"/>
<module name="Production Control"
ctlmod="WidgeNet.WeebleProduction.WeebleProduction"
modassy="WeebleProductionCtl.dll"/>
<module name="Robot Control"
ctlmod="WidgeNet.Robot.RobotControl" modassy="RobotCtl.dll"/>
<module name="Weebit Inventory"
ctlmod="WidgeNet.WeebitInventory.WeebitInventory"
modassy="WeebitInventoryCtl.dll"/>
<module name="Status Viewer"
ctlmod="WidgeNet.StatusViewer.StatusViewer"
modassy="StatusViewerCtl.dll"/>
<module name="Report Viewer"
ctlmod="WidgeNet.Reports.ReportViewer"
modassy="ReportViewerCtl.dll"/>
</modules>
</WidgeNetModules>
When the form (container) is loaded, it looks in the config file for the modules it is to load, and loads them. To facilitate the processing of the config file section, a helper class is defined to handle that work. But, there really is not much to it, so you can look at the code in the demo. Here’s the code executed when the form loads:
private void Form1_Load(object sender, EventArgs e)
{
try
{
ModulesSection modSection =
ConfigurationManager.GetSection("WidgeNetModules") as ModulesSection;
ModulesCollection modCollection = modSection.Modules;
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
foreach (ModuleElement modElement in modCollection)
{
if (LoadAssembly(modElement.ControlModule, modElement.Assembly))
listModules.Items.Add(new ModuleItem(modElement.Name,
modElement.ControlModule));
}
}
catch (System.Exception ex)
{
MessageBox.Show("Failed reading config! " + ex.Message);
}
}
private bool LoadAssembly(string ctl, string assy)
{
bool returnVal = false;
try
{
Assembly assembly = Assembly.LoadFrom(assy);
Type t = assembly.GetType(ctl);
Control c = (Control)Activator.CreateInstance(t);
splitContainer1.Panel2.Controls.Add(c);
c.Parent = splitContainer1.Panel2;
c.Visible = false;
returnVal = true;
}
catch(Exception ex)
{
MessageBox.Show("Failed loading module-" + ctl +" Error:"+ex.Message);
}
return returnVal;
}
Pretty straightforward; we use the config file helper class, iterate through each item, load the assembly, create an instance of the module, and assign it as a child control to the right pane. In the sample code, the views are all loaded together and set to not visible. The user's selection is what shows/hides each view. The views could also be loaded when first used, but that may complicate things a little bit. In this scenario, all views are expected to be available all the time. Here is the event handler for the listbox which controls the visibility of the views:
private void listModules_SelectedIndexChanged(object sender, EventArgs e)
{
foreach (UserControl c in splitContainer1.Panel2.Controls)
{
if (c.GetType().ToString() ==
((ModuleItem)(listModules.SelectedItem)).ctlName)
{
if (c.Width > splitContainer1.Panel2.Width)
this.Width += c.Width - splitContainer1.Panel2.Width;
else
c.Width = splitContainer1.Panel2.Width;
if (c.Height > splitContainer1.Panel2.Height)
this.Height += c.Height - splitContainer1.Panel2.Height;
else
c.Height = splitContainer1.Panel2.Height;
c.Visible = true;
}
else
c.Visible = false;
}
}
The only complication is how to handle the size of the container since the views will be of different sizes. I don't think there is a clean solution. If the container re-sizes itself to the size of the control, it will be a little distracting to the user. So the demo code simply sizes the container to the size of the largest view. It's not the best, so we'll have to wait for WPF to provide a better solution. One other thing, the demo code expects the controls to be in the same location as the container, but of course, that is not mandatory. You can specify the location in the config file and modify the code accordingly.
As mentioned previously, other flavors of the UI could be implemented. If a tab control was used, then you would determine the number of tabs required during the loading process and then assign each control to its own tab. The code will be somewhat simpler since the tab control provides the selection and show/hide functionality. If a tree control were to be used, it would require some additional information in the config file that would indicate a categorization scheme for the views.
DCAF Service Host
The DCAF Service Host plays a similar role as the DCAF UI container. It provides for dynamically loading service modules in the form of data modules, business logic modules, and object library modules. The identification of the modules is done the same way, using the config file. The DCAF service host can be implemented as any type of executable, but more likely, it is implemented as a Windows Service (formerly called NT Service). These are well suited for long running applications that are managed by the Operating System. The demo code implements the host as a console app, but is mainly for the convenience of the demo.
Since the host would normally be implemented as a Windows Service, there is one additional functionality that the service host provides. And, that is that it allows for modules to be unloaded dynamically at run time. That is, without shutting down the application. Although this is neat, it probably is not a requirement for most applications. It is for environments where systems can’t be shut down since they operate 24/7 and are only shut down at specific times of the year.
To accomplish this feature, the host exposes an interface through which an external application can control the loading and unloading of the modules. This is a feature that would normally be used to upgrade or update modules and even to add new functionality to the application. Of course, this is not a facility that can be used with disregard to the state of the application. Just like it would be unwise to delete a DLL from the machine without knowing what uses it. Using this facility requires knowledge of the state of the system as well as the functionality provided by the module. Modules also need to be designed with this capability in mind since they have to place themselves in an 'unloadable' mode. Any threads that a module creates have to be exited in order for the module to be released. However, this feature does provide for updating without shutting down the service, and can be done remotely.
Fig. 3 WidgeNet Dynamic Update
There is a control application in the sample code to demonstrate this facility. Make sure you start the DCAF service host first before starting the DCAF container UI. One of the modules that the DCAF service host loads is the WidgeNetProductionSvc module. This module exposes an interface that is controlled from the UI by the Production Control view. If you select the Production Control view, it displays a Start and Stop button to simulate the production. If you click on the 'Start' button, the production module will respond by sending a status message to the Status Viewer screen. Switch over to the Status Viewer screen after clicking the Start button, and you will see the messages sent by the production module. Press the Stop button on the Production Control screen to stop the production. You'll notice that the messages are no longer being sent. Now, start the WidgeNetControl application (shown in Fig. 3). When the application starts, it automatically communicates with the DCAF service host and displays all the modules that have been configured and the ones that are currently loaded into memory. Select the production service module from the Loaded Modules list. Then, click on the Unload button. You'll see that the item is removed from the list. This confirms that the module has been unloaded because the loaded list is retrieved from the service host and the listbox is updated. Now, open the WidgeNetProductionSvc project using Visual Studio, and modify the message that is being sent by the service. It is in the helper thread class. Re-compile the module, and make sure it gets saved to the same place so that the service host will find it. Now, select the production service module from the Configured Modules list in the WidgeNetControl window, and press the Reload button. The production module will be re-loaded by the DCAF service host. If you now click on the Start button in the Production Control screen, you should see your revised message from the production service module. We've just dynamically updated the application with new code!
One consequence of providing the unloading capability is that each module has to be loaded into its own AppDomain. There is no 'unload' facility for an AppDomain, so the only way is to dispose of it. Here's the code for loading modules for the service host:
private void LoadAllModules()
{
try
{
ModulesSection modSection =
ConfigurationManager.GetSection("WidgeNetModules") as ModulesSection;
ModulesCollection modCollection = modSection.Modules;
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
foreach (ModuleElement modElement in modCollection)
{
AppDomain appDomain = AppDomain.CreateDomain(modElement.Name);
WidgeNetLoader remoteLoader = (WidgeNetLoader)
appDomain.CreateInstanceFromAndUnwrap(setup.ApplicationBase +
"WidgeNetHost.exe",
"WidgeNet.WidgeNetHost.WidgeNetLoader");
remoteLoader.LoadAssembly(modElement.Name, modElement.Host);
modules.Add(new WidgeNetModule(appDomain,
remoteLoader,
modElement.Name));
}
}
catch (System.Exception ex)
{
EventLog.WriteEntry("WidgeNet",
"WidgeNetHost-Error:" + ex.Message);
}
}
You can see the similarity to the UI container. When the host starts up, it will load all the modules that have been identified in the config file. So, there is a similar helper class to process the config file section. And, as mentioned above, we also need a helper class to load the module into its own AppDomain. Here's the code for that class:
public class WidgeNetLoader : MarshalByRefObject, IDisposable
{
public object host = null;
public void LoadAssembly(string modName, string hostName)
{
try
{
Assembly assembly = AppDomain.CurrentDomain.Load(modName);
host = assembly.CreateInstance(hostName);
}
catch (System.Exception ex)
{
EventLog.WriteEntry("WidgeNet",
"WidgeNetHostSvc:Failed to load module-" + modName +
". Error:" + ex.Message);
}
}
public void Dispose()
{
if (host != null)
((IDisposable)host).Dispose();
}
public override object InitializeLifetimeService()
{
return null;
}
}
That's all that's needed to build and deploy a modular, distributed, dynamically 'composable' application. Of course, everything is not 'peachy'. The containers (UI and service host) do not provide any additional functionality to the modules that they host. It would be nice if the UI container could provide some generic UI functionality that the views could depend on. Another shortcoming is that there is no way for the views to communicate with each other, so they need to be self-sufficient. And, even though it's modular, the 'chunks' are pretty large. It would be better if it could be a little more granular. In other words, the views are still pretty much like mini-applications, and it would be much better if they could be broken down into more semi-independent parts. And, if we wanted to have a view that was 'shared' by more than one functional module, it can't be done without some duplication of code. Finally, even though each control can be tested independently by simply plopping into a dialog test harness, it still needs to be tested as a whole unit.
Along those lines, DCAF does provide for a pretty nice testing environment. Any service module can be replaced with a simulated version for the device services, or a stubbed version for the data services. This is useful during the development process, and later during debugging and troubleshooting. During development, simulated modules can help speed up development for two reasons. First, the real module can be developed in parallel, and second, the simulated version can be made to respond faster than the real version. As an example, a simulated robot module could simulate an operation in milliseconds where the real robot will take seconds to execute a request. This allows the whole application to be tested in a simulated mode and run production cycles in minutes instead of days, which is what it could take if the whole system had to be tested with the real hardware. And, it is also a lot safer to test new code in a simulated environment than with real hardware.
One of the easiest ways to find a bug is for it to be reproducible. Setting up a simulation environment where sequences can be repeated is an invaluable resource. Of course, all bugs won't be produced in a simulated environment. But, even there, it proves to be helpful because you can use the simulation to narrow down the areas of focus. If it's reproducible, then it's just a matter of time until it's found.
Now, let's turn our attention to some typical application 'opportunities for improvement' which are being addressed by WPF and CAL. I'm sure there is a plethora of others, so we'll just identify a couple of them. We'll first show how they were addressed in WidgeNet so that we can compare the solution later.
Multi-View Data Coherency
A typical situation is to have more than one view display some common data. Usually, there is a master or control view that creates and maintains some data object or entity, and there may be one or more dependent views that display all or part of that data object information. WidgeNet has that situation, so let's continue with our scenario.
WidgeNet has plans to upgrade their Weeble design process to an automated procedure. But in the meantime, Weebles must be designed manually using an editor provided on the operator console. Designing a Weeble is simply a matter of selecting one or more Weebits and positioning and orienting them using the editor (but that's not the point here). Fig. 4 shows the Weeble Editor window.
Fig. 4 Weeble Editor Window
The editor window has a list of the currently defined Weebits which can be used to define new Weebles. If the desired Weebit is not available, then it must be defined using the Weebit Editor screen. Both of these editor windows initialize the list of available Weebits when the control is initially loaded. The problem occurs when a new Weebit is defined in the Webit Editor, it is not available in the Weeble Editor list until the control is loaded again. Which means, you'd have to shut down the application. That won't make the user very happy. One solution is for the Weeble Editor view to poll the Weebit service periodically to refresh its list, or provide a Refresh button that the user can utilize. Yes, I know that is not a very laudable solution, but it does solve the problem and you see Refresh buttons all over the place. In fact, there's probably a Refresh button on the application you are using right now to view this. Another approach for the Weeble Editor is to add code in the VisibleChanged
event to reload the current list of Weebits. This way, each time the Weeble Editor is shown, it will get the current list. But, what about if you allow the user to view both editors at the same time? One of the features of the DCAF container is that if an item is double-clicked, it will be displayed in its own window. Fig. 5 shows this situation, each editor opens up in their own separate frames.
Fig. 5 WidgeNet UI Popped-Up
Now, the VisibleChanged
event won't provide a solution. The real solution is to have the Weebit data service tell you when the data has changed. This way, you get the information right from the horse's mouth. And, that is done by utilizing a Publisher/Subscriber pattern for the Weebit data service. The Weebit service module in the demo implements a subscription interface that clients can use to subscribe/unsubscribe to notifications of data changes. The client (Weeble Editor) must implement a callback interface which the service will use to send the notifications. The following code shows the interface definitions:
[ServiceContract(Namespace="http://WidgeNet",
CallbackContract = typeof(IWeebitChangedHandler),
SessionMode=SessionMode.Required)]
public interface IWeebitDataPub
{
[OperationContract(IsInitiating=true)]
void Subscribe();
[OperationContract(IsTerminating=true)]
void Unsubscribe();
}
public interface IWeebitChangedHandler
{
[OperationContract(IsOneWay=true)]
void DataChanged();
}
The Weebit service implements IWeebitDataPub
and the clients must implement IWeebitChangedHandler
. When the Weeble Editor is loaded, it subscribes to the Weebit data service for data changed publications, as shown below:
private void WeebleEditor_Load(object sender, EventArgs e)
{
...
pubClient = new WeebitDataPubProxy(dataChangedHandler);
pubClient.Subscribe();
...
}
Two things to note here. First, in order for the notifications to come back, the connection to the service must be maintained. The proxy (pubClient
) is a class variable that gets initialized here and released when the control gets destroyed. And, 'dataChangedHandler
' is the callback interface that is being passed to the service. Here is the code for the class that implements the callback interface:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
Namespace = "http://WidgeNet")]
public class DataChangedCallbackHandler : IWeebitChangedHandler
{
WeebleEditor parent;
public DataChangedCallbackHandler(WeebleEditor parent)
{
this.parent = parent;
}
public void DataChanged()
{
parent.context.Send(parent.dataChanged, null);
}
}
You might notice that the class is similar to how a service is declared. In fact, the callback handler shown above is really a service to the Weebit data service module if you want to think of it that way. And, if that is the case, then we need something to 'host' the callback service similar to what ServiceHost provides for services. And, that is what the InstanceContext
class provides. We create an instance of the InstanceContext
class and pass it an instance of the class that implements the callback interface. Here is the code for that, which is in the constructor:
public WeebleEditor()
{
InitializeComponent();
context = SynchronizationContext.Current;
dataChanged = new SendOrPostCallback(DataChanged);
handler = new DataChangedCallbackHandler(this);
dataChangedHandler = new InstanceContext(handler);
}
...
void DataChanged(object o)
{
using (WeebitDataClient client = new WeebitDataClient())
{
listWeebits.Items.Clear();
listWeebits.Items.AddRange(client.GetWeebitNames());
}
}
And, since the notifications will be coming back on a different thread, we set up a SynchronizationContext
and an event handler which the callback class will invoke. Now, every time the Weebit data changes, the listbox will be updated at the same time. I have to say that this is the shortcut solution. A better solution would have been to send the updated data as part of the notification. It costs a little more work, but it is much more efficient. If the editor had to retrieve a long list of items each time it got a notification, this would definitely not be a good approach. For those situations, the callback interface could include the updated object as well as a flag to indicate whether it was an update or a create (just to simplify the processing on the client side). Now, let's look at the other side and see what the service module needs to implement. The Weebit service implements the subscription interface which clients use to subscribe/unsubscribe to the notifications. All the service has to do is keep a list of who is interested in being notified. Here's the code for the service implementation:
public void Subscribe()
{
lock (subscribers)
{
IWeebitChangedHandler subCallback =
OperationContext.Current.GetCallbackChannel<iweebitchangedhandler>();
if (subscribers.Contains(subCallback) == false)
subscribers.Add(subCallback);
}
}
public void Unsubscribe()
{
lock (subscribers)
{
IWeebitChangedHandler subCallback =
OperationContext.Current.GetCallbackChannel<iweebitchangedhandler>();
foreach (IWeebitChangedHandler handler in subscribers)
{
if (handler == subCallback)
{
subscribers.Remove(subCallback);
break;
}
}
}
}
Just as we mentioned previously, the callback could be enhanced by sending back the changed data, the Subscribe()
method could also be enhanced by including parameters. For example, the client might want to only be notified when new Weebits are added, and not when an edit is made. It takes a little more work on the service side, but anything is possible. It just depends on the requirements; for the demo, it doesn't do too much harm.
Since there could be some delay in sending out the notifications (and possible exceptions), and since we also want to disconnect the service call that initiated the update from the notifications, we implement the notifications as a separate thread. Here is the code for that:
internal class PublishingThread
{
WeebitDataSvc parent;
public PublishingThread(WeebitDataSvc parent)
{
this.parent = parent;
Thread thread = new Thread(new ThreadStart(Publish));
thread.Start();
}
void Publish()
{
List<iweebitchangedhandler> bogusSubs = new List<iweebitchangedhandler>();
lock (parent.subscribers)
{
foreach (IWeebitChangedHandler handler in parent.subscribers)
{
try
{
handler.DataChanged();
}
catch (System.Exception ex)
{
bogusSubs.Add(handler);
}
}
foreach (IWeebitChangedHandler wh in bogusSubs)
{
parent.subscribers.Remove(wh);
}
}
}
}
And that's it. Each time the the Weebit data is edited or a new Weebit is created, an instance of the PublishingThread
is created which will send out notifications to all subscribers. If the data object were to be sent along with the notification, then it would be a simple matter to pass it in the constructor. And of course, the subscriber list would also need to indicate any preferences that may have been passed in to the Subscribe
method (if it were designed that way).
An alternate solution is to provide a generic publication service as part of the application which all modules would use. Clients would subscribe to a specific 'publication type', and services would publish their notifications to the publication service, which would then broadcast the notification to all subscribers. In the example above, a publication type 'WeebitDataChange
' could be defined for the system. Clients would use this type to subscribe with the publication service. And, the Weebit data service would publish to the publication service indicating that type as a parameter of the publication message. What this provides is for a central location for the functionality. For simple event notifications, the publication service would be very simple, and can even provide for automatically configuring notifications from the config file. If the event notification is complex and data objects are passed back, then the publication service becomes more complicated.
UI Visual State
A common problem in UI design is controlling what the user is allowed to do and/or see. You know that users are very impatient and will click at will. How many times have you pressed the 'Close Door' button in an elevator hoping your impatience will make it close the door sooner? Well, it's not listening! It is going to run its course, no matter what. For the UI, we also have to protect the application from the user. One way is to allow the user to press the button as many times as he wants, but nothing will happen because the application has already accepted the previous input. Obviously, applications can't operate the same way as the elevator. The UI needs to indicate to the user what options s/he has at any point in time. This is done by enabling and showing/hiding the controls appropriately. There are probably many ways to 'contrive' a solution, but I don't think that there is a nice clean solution (at least, at this point in the article). In Fig. 4 and Fig. 5 above, you can see the two editors that were described previously. You can see that there are a number of controls on each screen, and some are enabled and others are disabled. The demo code uses a pseudo state machine to control the 'state' of the UI. Depending on what the user is currently doing, the appropriate controls will be enabled and/or shown. Here is a portion of the code from the Weeble Editor:
...
if (currentState == ViewStates.EditingExisting ||
currentState == ViewStates.EditingNew)
{
textDescription.Enabled = true;
buttonSave.Enabled = true;
buttonCancel.Enabled = true;
listWeebles.Enabled = false;
listWeebits.Enabled = true;
buttonAdd.Enabled = true;
weebleDesignerCtl1.Enabled = true;
}
if (currentState == ViewStates.EditingNew)
{
textName.Visible = true;
labelName.Visible = true;
}
if (currentState == ViewStates.NoSelect)
{
buttonNew.Enabled = true;
}
if (currentState == ViewStates.Viewing)
{
buttonNew.Enabled = true;
buttonEdit.Enabled = true;
}
if (currentState == ViewStates.NoSelect ||
currentState == ViewStates.EditingNew)
{
textName.Text = "";
textDescription.Text = "";
weebleDesignerCtl1.Clear();
}
...
One of the difficulties with this problem is that the logic and the UI are very tightly coupled. Any changes to the UI will also require changes to the code. Some changes might be simple, other changes might have a large impact on the code to support it. A variant of this problem is where a UI control is dependent on the state of multiple views.
Fig. 6 WidgeNet Robot Control
In the Robot Control view shown in Fig. 6, the Start button is dependent on each robot being enabled. In the demo code, the tabs are simple containers, and the event handlers are all handled locally. But if each tab actually hosted a user control (the better way), then the aggregating logic would get a little more complicated. Each user control would handle the button click event, but it would then have to communicate that to the tab. In any case, controlling the state of the UI can turn into a big mess of spaghetti.
Event Hiding
I'm positive you've encountered this many times. You want to trap on an event that makes the UI more intuitive or the code simpler, and it's just not available. Not all controls are created equal. Not all are 'focusable', so the operations available vary. This means that, to obtain the desired event may not be possible, or you have to go through hoops to get the desired result. Other times, the events are handled by parent classes so they cannot be processed by the child. In some situations, you have to handle the event in one class and then pass it to another using a method call to simulate the event handler. And, this may actually turn out to make the UI somewhat less intuitive.
There is at least one example in the demo code. In the Weeble Editor view, there is a pane on which the Weebles are designed. The user can drag and rotate each Weebit to the desired position and orientation. To make the positioning more accurate, the user can 'zoom' into the contents of the pane. The slider under the pane controls the magnification of the pane. A more intuitive mechanism would have been to use the mouse wheel as the control mechanism for the 'zooming' operation. It would have been easier for the user, and the display would have been cleaner. Unfortunately, the MouseWheel
event was not available to that control, so the alternative solution had to be implemented.
I guess the bottom line is that, in our daily activities, we have to struggle with limitations of the infrastructure. Sometimes, that turns into a bigger challenge than the business problem to be solved. And, we may even have to settle for less than perfect. There is no criticism intended here. It is just the nature of our industry, ever increasing complexity, and constant change.
Deep-Nesting Dependency
Have you ever seen something like the following?
...
parent.parent.parent.DoSomething();
...
Come on, fess up, yes you have. One of the pillars of OOP is composition, which allows us to build complex objects out of simpler ones. This composition can sometimes result in a deep hierarchy. Of course, you try to minimize or eliminate any vertical dependencies that could result in the hierarchy. But sometimes, you just have to talk back to the parent. Even though, since we were very young we've been taught not to. What happens is that sometimes there is no choice but to have the parent create the resource and make it available to the children. I think this becomes more of an issue with the UI than with other situations, since there are probably more options in other cases. But, in any case, it's a common problem.
Fig. 7 WidgeNet Inventory View
Fig. 6 shown above shows the inventory screen for WidgeNet. Each of the buttons represent positions within their respective 'subsystem' where the Weebit inventory could exist. Each 'subsystem' is implemented as a user control, which in turn creates the appropriate number of position buttons. When the user moves the cursor over one of the button positions, the quantity of Weebits at that location is displayed on the screen. The problem then is that the InventoryPosition (button) processes the mouse events, and has the data, or knows how to get the data, but the data needs to be displayed on a control that is two levels higher. In this case, it's just a simple number which could easily be displayed on the button itself. But more likely, there would be more information displayed for each item. In this example, the above construct would work since all levels are controls that have a 'parent' member. But, the child would have to cast the parent to the appropriate type. Now, if the classes were not controls, then you'd have to pass a reference through all levels down to where it is required. This means that every object that needs to communicate back to the top level must maintain a reference to the top level. And, the top level must define a method through which the data is communicated, or make the resource public. Neither of these are good because there is a tight connection between the controls. Another solution is to define a couple of events that the position buttons would fire corresponding to the mouse enter and mouse leave events that it already processes. This eliminates the coupling between the top and the bottom since the lower level does not need to have any knowledge of the parent. However, this requires that multiple events be fired, one at each level in the hierarchy, in order to propagate the information up the hierarchy. And, the WidgeNet example is also a very simple situation, sometimes much more complex combinations are required by the design.
Asta La Vista, Baby
Well, that's it for now. We now have an application that we can use to analyze the guidance concepts. We also examined how problems were addressed in the fictional scenario. Next time, we'll see if CAL provides better ways to solve those problems. We've also highlighted some application 'opportunities for improvement' that we can use as focal points for the examination. So next time, we'll start dissecting the Composite Application Library and see how it might be able to make the design better. In the meantime, take a look at the guidance and see what you think.