Click Here to Install Silverlight*
United StatesChange|All Microsoft Sites
MSDN
|Developer Centers|Library|Downloads|Code Center|Subscriptions|MSDN Worldwide
Search for


Advanced Search
MSDN Home > MSJ > July 1997
July 1997


Dynamic Runtime Objects: Building Applications Your Users Can Modify at Runtime

Ed Smetak and Jean Caputo

By forcing developers to segregate code in such a way that the tendency toward inappropriate object dependencies is greatly reduced, dynamic runtime objects go a long way toward keeping big projects under control. The result is an application that is well organized and easy to extend.

This article assumes you're familiar with C++ and COM

Code for this article: Dynamic.exe (242KB)
Ed Smetak is vice president of Engineering Software Consulting at NanoSoft Corporation. Jean Caputo is a project management specialist at NanoSoft Corporation. They can be reached at http://www.nanocorp.com.

Managing the development of large software applications can be a big headache. Often, the headache comes from inappropriate object dependencies that creep in as the result of a poor global design framework. If the application needs to be extensible enough to allow customization by users, things get worse as awkward object dependencies mount.
We have found that the concept of dynamic runtime objects can go a long way toward keeping big projects under control by isolating object dependencies. This concept forces developers to segregate code in such a way that the tendency toward inappropriate object dependencies is greatly reduced, while appropriate dependencies are handled in a uniform, understandable, and manageable fashion. The result is an application that is well organized and easy to extend—even by users, and even at runtime.
In this article, we will show you how to solve common problems of complicated application architectures by structuring classes to your utmost advantageæimplementing generic behavior high up in the inheritance chain and pushing context-specific behavior down to the most derived classes. With the concept of dynamic objects, you can take this strategy for structuring classes to the limit and allow new object classes to be introduced to the application at runtime.
Rather than focusing on an isolated technology area, this article takes a stab at the big picture using an application of arbitrary complexity. We are providing a large amount of example code to show you how it can all fit together. All of the concepts will be demonstrated through a series of dynamic runtime objects and views that live in separate MFC Extension DLLs. The objects are hosted by a generic container application called ObjectView. (You can get the complete source code, as well as the ObjectView container app here.) We hope that the ideas and components presented here will be a useful addition to your box of development tools.
We'll start by showing you how easy it is to build a DLL that houses a simple dynamic runtime object and an associated view. Then we'll look at three key components that make it work—a class broker, a view broker, and an object broker. Of particular interest is the object broker, which uses ActiveX™ structured storages and whisks objects in and out of memory as needed, minimizing your application's overhead.
We'll move on to introduce an extensible Dynamic Object design pattern that you can use to structure classes and manage object dependencies in your most complex application. The pattern is extensible to situations that require multiple views on complex objects with intricate interdependencies. We'll use the pattern to create three different dynamic objects that live
Figure 1 OMT Notation Key
Figure 1 OMT Notation Key
in MFC Extension DLLs. Each DLL houses a dynamic object class and associated graphical user interface (GUI) classes. The first DLL demonstrates how a view should interact with a dynamic object and how to handle persistent data for dynamic objects. The second DLL demonstrates how to handle object interdependencies—a common source of problems in many applications. The third DLL demonstrates the concept of interchangeable classes, which is useful to provide hooks for customizing apps at runtime. We'll finish up with a few words about how the dynamic object concepts presented here compare with COM, ActiveX, and OLE technologies.
We'll be using class diagrams and interaction diagrams to illustrate our design concepts. The class diagrams, based on the Object Modeling Technique (OMT), depict classes, their structures, and the static relationships between them (Figure 1 shows the key to the OMT diagrams.) The interaction diagrams, taken from the Booch method, depict the order in which requests between objects get executed.

Building a Simple Dynamic Object and View

Let's jump right in with a simple example to demonstrate how easy it is to use dynamic objects. We'll build an MFC Extension DLL called Simple.dll that will be incorporated into our host application at runtime—without recompiling—thanks to the services of our class, view, and object brokers. Simple.dll will house a dynamic object class called CSimpleObject and an associated view class called CSimpleView (see Figure 2).
Figure 2 CSimpleObject and CSimpleView
Figure 2 CSimpleObject and CSimpleView

Go to Visual C++®, open a new project workspace, and choose the MFC DLL AppWizard. Make sure to specify an MFC Extension DLL using MFC as a shared DLL. CSimpleObject is derived from a base class called CDynamicObject that we'll discuss later. For this example, all we'll include is a constructor, a destructor, and a Serialize function. All three of these functions will be just empty shells. We'll look at more interesting examples later. Make sure to use the DECLARE_ SERIAL macro in your class definition and the IMPLEMENT_SERIAL macro in your implementation file to allow the object to be dynamically created and serialized:
 IMPLEMENT_SERIAL(CSimpleObject,
                 CDynamicObject,1)
 
 CSimpleObject::CSimpleObject() 
 {
   // nothing required for this 
   // simple example
 }
 
 CSimpleObject::~CSimpleObject()
 {
   // nothing required for this 
   // simple example
 }
 
 void CSimpleObject::Serialize(CArchive& 
                          archive)
 {
   // remember to call the base class
   CDynamicObject::Serialize(archive);
 }
Next, add some code for the CSimpleView class. Use the MFC ClassWizard to create a class derived from a CFormView. ClassWizard automatically adds the DECLARE_DYNCREATE macro to the class definition and the IMPLEMENT_ DYNCREATE macro to the implementation file. Note that we've changed the base class from CFormView to CNSFlexFormView. CNSFlexFormView is from the NSViews C++ library. This library gives all the views in our sample code some distinctive resizing behavior:

 IMPLEMENT_DYNCREATE(CSimpleView, CNSFlexFormView)
 
 CSimpleView::CSimpleView() : CNSFlexFormView(CSimpleView::IDD)
 {
   // nothing required for this simple example
 }
A basic rule for good architectures is to keep user interface object classes separate from functional object classes. In our case, CSimpleView is our user interface class and CSimpleObject is our functional class. It's OK for CSimpleView to hold a reference to CSimpleObject, but CSimpleObject should have no knowledge of CSimpleView. How does CSimpleView get hold of the associated CSimpleObject? Here's where the view broker and object broker come in. The view broker will send a WM_OBJECTINFO custom message that includes a unique key associated with the dynamic object we need. Given the key, we can get hold of our dynamic object from the object broker. A pointer to the object broker is also passed with the message. CSimpleView:: OnObjectInfo (see Figure 3) shows how to handle the WM_OBJECTINFO message.
When we're done with the dynamic object, we need to call CDynamicObject::Release. A convenient place to do that is in the destructor for our view:
 CSimpleView::~CSimpleView()
 {
   m_pSimpleObject->Release();
 }
The object broker will load and unload DLLs as dynamic objects are created and deleted. The code for DllMain that AppWizard created for us didn't include a call to AfxTermExtensionModule—it's unusual for DLLs to be unloaded as we do here. But, if you're going to unload MFC Extension DLLs, you need to be sure to call AfxTermExtensionModule (see Figure 3). Some very mysterious (and bad) things will happen if you forget this detail.
Figure 4 ObjectView, no DLLs
Figure 4 ObjectView, no DLLs

The last thing we have to do is tell the class broker that our DLL houses a dynamic object and associated view. The class broker will load our DLL and search for an exported function called ClassInfo (see Figure 3). ClassInfo must return a class ID, a class category, and a short description for our dynamic object. In addition, ClassInfo must return the runtime classes of our dynamic object, frame, and view. The class ID and class category are easily generated using the Guidgen.exe program that ships with Visual C++. The class ID for each runtime DLL must be globally unique. Later we'll see that it is useful to identify classes that are interchangeable. The class category is used for this purpose and must be globally unique for each category of interchangeable classes.

Using Dynamic Objects

Now that we've built a DLL that contains a dynamic object, let's run it by using ObjectView, a generic container for dynamic objects. The behavior of the dynamic object and view can be as simple or as complicated as required. What's important is that we define all that behavior in the DLL. The main application doesn't know a thing about either of the classes—except that the dynamic object is derived from CDynamicObject. The main application doesn't even need to include any headers for the classes defined and implemented in the DLL. If you run ObjectView without any DLLs containing dynamic objects in ObjectView's directory, it's rather boringæ just an empty shell (see Figure 4). But copy Simple.dll into ObjectView's directory and suddenly ObjectView is hosting both the CSimpleObject object and the CSimpleView view (see Figure 5).
Figure 5 Hosting Dynamic Object and View
Figure 5 Hosting Dynamic Object and View

CSimpleObject and CSimpleView don't really do much more than display a bitmap that was included in the dialog resource associated with the CFormView. However, all the hooks are in place for more sophisticated behaviors. Later, in our more complicated examples, we'll let our imagination go a little further and create some spinning rotors that use threads and persist their state through CDynamicObject::Serialize. You'll be surprised how easy it is, and the objects can be as big and complicated as you want. The dynamic objects can even have complex interdependencies.
The three key components that support this extensibility aren't that complicated. The class broker registers classes for dynamic objects. The view broker manages views on dynamic objects. The object broker dispenses dynamic objects as needed. We'll take a look at each of them in detail. But first, we need to introduce one more concept that will be a recurring theme in almost every ingredient of our dynamic object architecture.

Observer Design Pattern

The well-known Observer design pattern provides a useful context for managing object interdependencies. The Observer pattern recognizes that applications are replete with interdependent objects, and a generic method of communicating changes between objects is essential. Communication between subject and observer is best accomplished when the subject object does not need to know anything specific about the observer object except that the observer must be notified of any change. MFC's document view architecture is a form of the Observer pattern.
Our implementation of the Observer pattern centers on two classes, CSubject and CObserver, which allow interested observers to register with and be notified about changes to interesting subjects (see Figure 6). CSubject::NotifyObservers broadcasts change notifications to all registered CObservers. CSubject::AttachToSubject and CSubject::DetachFromSubject register and unregister CObservers. CObserver::SubjectChanged is a virtual function that must be overridden to receive change notifications from CSubjects.
Figure 6  CSubject and Cobserver
Figure 6 CSubject and Cobserver

These classes are a snap to use and can make complex communication among objects manageable and bug-free. For example, the view broker is an observer, the object broker is a subject, and dynamic objects may be either or both. The Observer design pattern is a cornerstone for the Dynamic Object pattern, which we will use to support complicated cases where an object needs to observe multiple subjects as well as be observed by multiple observers.

Class Broker

We need a vehicle to whisk code for object classes into and out of the application's address space at runtime. CClassBroker and CClassInfo provide this service (see Figure 7).
Figure 7  CClassBroker and CclassInfo
Figure 7 CClassBroker and CclassInfo

CClassBroker::LoadDLLModules (see Figure 8) is called as part of the application's initialization. LoadDLLModules searches the application's directory, loads each DLL residing there via a call to ::AfxLoadLibrary, and inquires—via a call to ::GetProcAddress—whether a function called ClassInfo is exported from the DLL. If GetProcAddress locates ClassInfo, ClassInfo is called to obtain six pieces of information:
  • class ID associated with the object in the DLL (must be unique)
  • class category of the object (unique for each category of interchangeable classes)
  • short text description of the object
  • runtime class of the object
  • runtime class of the frame for the object's view
  • runtime class of the object's view
You've already seen a typical implementation of ClassInfo in the Simple.dll example. The information returned from ClassInfo, along with the full path to the associated DLL, is passed to CClassBroker::AddClassInfo. AddClassInfo instantiates a CClassInfo object and adds it to a static list of available CClassInfo objects.
Each DLL is then unloaded via a call to ::AfxFreeLibrary. A DLL only needs to stay loaded if objects defined in the DLL are actually instantiated by the application. CClassInfo::LoadLibrary and CClassInfo::FreeLibrary provide the mechanism to load and unload a DLL as required. It's important to note that each time a DLL is loaded, the addresses of the runtime classes in the DLL may change. Therefore, LoadLibrary will always need to call ClassInfo to get the current addresses. We'll see how LoadLibrary is called to do its work when we look at the object broker.

View Broker

MFC's AppWizard generates code that creates a single CMultiDocTemplate as part of the application object's InitInstance function. CMultiDocTemplate ties together the document, frame window, and view. But we know nothing about the type of view or frame our runtime object will need until the DLL is loaded at runtime. CViewBroker, CViewInfo, and CMultiDocTemplateEx provide a mechanism that will let us open a view, or even multiple views, on our dynamic object (see Figure 9).
Figure 9  CMultiDocTemplateEx, CViewBroker, and CviewInfo
Figure 9 CMultiDocTemplateEx, CViewBroker, and CviewInfo

In order to use these classes, the code that AppWizard generates for us needs to be altered to create a CMultiDocTemplateEx instead of a CMultiDocTemplate:

 CMultiDocTemplateEx* pDocTemplate = new CMultiDocTemplateEx(
   IDR_OBVIEWTYPE,
   RUNTIME_CLASS(CObjectViewDoc),
   RUNTIME_CLASS(CFlexibleChildFrame), 
   RUNTIME_CLASS(CObjectBrokerView));
 AddDocTemplate(pDocTemplate);
Here's the CMultiDocTemplateEx constructor. It saves the runtime classes of the frame and view for later use:

 CMultiDocTemplateEx::CMultiDocTemplateEx(UINT nIDResource,
                            CRuntimeClass* pDocClass,
                            CRuntimeClass* pFrameClass,
                            CRuntimeClass* pViewClass) : 
 CMultiDocTemplate(nIDResource,   
                   pDocClass,pFrameClass,pViewClass)
 {
   m_pDefaultFrameClass = pFrameClass;
   m_pDefaultViewClass = pViewClass;
 }
To open a view on one of our dynamic objects, call CViewBroker::OpenView (see Figure 10) with the key to the object. If a view is already open on the object, OpenView activates it. Otherwise, OpenView calls CViewBroker::OpenDuplicateView (see Figure 10) to create a new view.
CViewBroker::OpenDuplicateView asks the CClassBroker for the associated CClassInfo for the requested key. Now we can call CMultiDocTemplateEx::CreateNewFrame with the runtime classes of the frame and view we get from CClassInfo. After the view is created, we send it a WM_ OBJECTINFO custom message that includes the key for the object and a pointer to the object broker.
CMultiDocTemplateEx::CreateNewFrame (see Figure 10) performs some slight of hand to trick CMultiDocTemplate::CreateNewFrame into using the specified MDI frame and view.

Object Broker

The object broker conserves memory resources by keeping objects in memory only as long as they are needed. This also makes saving files almost instantaneous since there are fewer objects living in memory. In addition, the object broker allows you to store and serialize references to dependent objects as simple keys. The object broker is implemented using three classes: CObjectBroker, CObjectInfo, and CDynamicObject (see Figure 11).
Figure 11  CObjectBroker, CObjectInfo, and CdynamicObject
Figure 11 CObjectBroker, CObjectInfo, and CdynamicObject

CDynamicObject provides a base class for our dynamic objects. Each dynamic object derived from CDynamicObject is added to a CObjectBroker container object along with an associated CObjectInfo object. When the CDynamicObject object is created, it actually goes into an ActiveX structured storage living in the application's compound file. Each CDynamicObject object resides in the structured storage on disk until you need it. At that time, get hold of the associated CObjectInfo object and then ask it to bring your dynamic object from the structured storage into memory. Everything happens transparently, so it's easy to use. Let's see what makes it work.
A dynamic object derived from CDynamicObject is created and inserted into a CObjectBroker object along with an associated CObjectInfo object, using CObjectBroker::Add (see Figure 12 and Figure 13). Add takes one of the class IDs registered with CClassBroker and returns a unique key for the new dynamic object. First, Add generates a unique key, a unique default object name, and a unique ActiveX structured storage stream name. Then, Add instantiates a new CObjectInfo, assigns the key, object name, and stream name, and then calls CObjectInfo::SetClassID (see Figure 13).
One of the first things that CObjectInfo::SetClassID does is to call CClassInfo::LoadLibrary to insure the DLL containing the code for the dynamic object is loaded. Then, SetClassID calls CClassInfo::GetObjectClass to obtain the runtime class of the dynamic object to be created. Next, MFC's dynamic object creation facility is used to breathe life into the dynamic object via a call to CRuntimeClass::CreateObject. After the object is dynamically created, SetClassID gives you a hook to do whatever processing may be appropriate at this time via a call to the virtual function CDynamicObject::OnCreatedNewObject (which you can override). Finally, we send the object out of memory to the ActiveX structured storage with a call to CObjectInfo::StoreObject (see Figure 13).
Here's how CObjectInfo::StoreObject works. A pointer to the IStorage COM interface on a storage is obtained through a call to CObjectBroker::GetObjectStorage. With the IStorage interface in hand, we open or create a COleStreamFile named with the unique stream name obtained from CObjectInfo::GetStream. We create a CArchive on the COleStreamFile and dump the CDynamicObject into the archive. Finally, we clean up and free the memory where the CDynamicObject was living.
There's another thing to notice about CObjectBroker. It uses multiple inheritance to inherit from both CObject and CSubject. Since CObjectBroker contains all of our dynamic objects, CObjectBroker itself will be an interesting subject to many observers. As dynamic objects enter and leave the object broker, CObjectBroker uses CSubject::NotifyObservers to let registered observers know about completed additions, pending removals, completed removals, and renamed objects.
We'll see how to handle notifications from the object broker a little later. The important thing to recognize now is that, just by inheriting from CSubject, CObjectBroker is able to broadcast notifications in a very generic way. It can do this without having to know anything about whom the observers will be, other than that they are derived from CObserver. This means you write the object broker code once, compile it, and forget it. The Observer pattern leads to the creation of an object broker that is independent of any implementation details associated with objects to which it must send change notifications. Undesirable object dependencies are eliminated. To create an object that needs to know about the object broker, inherit from CObserver, register with the object broker as an observer, and it all just works.
So far, so good. But how do you get to an existing dynamic object from CObjectBroker? The process is summarized in Figure 14. CObjectBroker::GetObjectInfoByKey, along with the key returned from CObjectBroker::Add, returns a pointer to the CObjectInfo associated with our dynamic object. Call CObjectInfo::GetObject (see Figure 13) to get a pointer to a CDynamicObject, the base class for the dynamic object. If the object is already alive, GetObject simply increments a reference count and returns the pointer to the living object. Otherwise, we again use MFC's dynamic object creation facility to create the object, this time through serialization from a temporary CArchive object created using a COleStreamFile in our structured storage. We again give you a hook to do whatever processing may be appropriate via a call to the virtual function CDynamicObject::OnSerializedObjectFromArchive, which you can override.
CObjectBroker::GetObjectStorage gets a pointer to the IStorage COM interface for a storage in our application's compound file. Now here's the million dollar question: how does m_pRootStorage in CObjectBroker get set? The simple answer is that the document object, in our case CObjectDoc, needs to set it through a call to CObjectBroker::SetRootStorage (see Figure 13), but it's a little more complicated than that. CObjectDoc is derived from the MFC COleDocument. If you go snooping around the MFC header file for COleDocument, you will find a protected member variable called m_lpRootStg. If you call COleDocument::EnableCompoundFile in the constructor for your document, your app's file will be a compound file and m_lpRootStg will point to a valid IStorage interface for the root storage. Seems simple enough, but there's a fly in the ointment. When a new document is opened, m_lpRootStg gets set to a temporary storage. The first time you save the document, MFC suddenly changes m_lpRootStg to a permanent storage in the actual file. To get around this issue, CObjectBroker:: SetRootStorage needs to be called from both CObjectDoc:: OnNewDocument and CObjectDoc::Serialize (see Figure 13).
CObjectBroker::SetRootStorage checks to see if the incoming pointer pRootStorage is the same as the stored pointer m_pRootStorage. If it is, everything is cool and we just return. If m_pRootStorage is NULL, we need to create a storage for our dynamic objects and then set m_pRootStorage accordingly. Pretty straightforward. The weird and somewhat confusing condition occurs if m_pRootStorage has already been set and pRootStorage is different. This is the clue that we are doing an initial save of the document and both pointers point to valid storages—the temporary one and the permanent one. For this weird case, we need to move the storage we're using for our dynamic objects from the temporary one to the permanent one.
Now let's turn our attention to CDynamicObject, the base class for our dynamic objects. As with ActiveX COM objects, there's one rule you must always abide by when using CDynamicObjects. When you are done with the CDynamicObject, you must call CDynamicObject::Release. Release decrements the object's reference count. If the reference count goes to zero, Release calls CObjectInfo::StoreObject, which we discussed earlier. We need to be really careful. StoreObject will delete the CDynamicObject. This has the same effect as deleting this pointer. Just be careful not to access any of the object's memory after the call to StoreObject—which is something that is easy to overlook. This is why we return zero rather than m_nRefCount:

 ULONG CDynamicObject::Release()
 {
   if (--m_nRefCount > 0)
     return m_nRefCount;
   else
   {
     m_pObjectInfo->StoreObject(TRUE);
     return 0;  
   }
 }

Dynamic Object Pattern

Let's move on to the Dynamic Object design pattern, which provides a generic, cookbook approach to structuring classes and using dynamic objects in any application. The pattern coordinates the actions of eight participants (see Figure 15):
  • CViewBroker (or some other class responsible for managing views)
  • CMultiDocTemplateEx
  • MFC Framework
  • KindOfCViewAndCObserver (a class inheriting from CView and CObserver)
  • CObjectBroker
  • CObjectInfo
  • KindOfCDynamicObjectAndCSubject (a class inheriting from CDynamicObject and CSubject)
  • KindOfCDocument (a class inheriting from CDocument)
Keep in mind that while the pattern is described using the CViewBroker class, the pattern can be used in other contexts as long as some class that is responsible for view management performs the CViewBroker duties described here.
The first part of the pattern handles the actions required to open a view on a dynamic object. This is a significant task since the application has no knowledge of either the view or the dynamic object that will be created. The pattern begins when CViewBroker calls CMultiDocTemplateEx::CreateNewFrame. CreateNewFrame instantiates a new KindOfCViewAndCObserver. Following the call to CreateNewFrame, CViewBroker calls CWnd::SendMessage to send a WM_ OBJECTINFO custom message to the newly created KindOfCViewAndCObserver. A pointer to an ObjectInfoMessage structure is passed along with the message. This structure contains the unique key for the dynamic object associated with the view being opened, along with a pointer to the object broker. The call to SendMessage triggers a call to KindOfCViewAndCObserver::OnObjectInfo. Since a pointer to the object broker was passed with the message, OnObjectInfo can call CObjectBroker::GetObjectInfoByKey and then CObjectInfo::GetObject to obtain a pointer to the dynamic object.
Recall that the call to GetObject will cause the dynamic object to move from the application's compound file into active memory if it isn't already there. Also, remember that a call to GetObject carries with it an obligation to call CDynamicObject::Release when we are finished with the dynamic object. With the pointer to the dynamic object in hand, OnObjectInfo can call CSubject::AttachToSubject to register KindOfCViewAndCObserver as an observer of KindOfCDynamicObjectAndCSubject. Finally, CViewBroker calls CMultiDocTemplate::InitialUpdateFrame, which will result in a call to KindOfCViewAndCObserver::OnInitialUpdate and KindOfCViewAndCObserver::OnUpdate. These functions need to do whatever is necessary to refresh the view for the first time as indicated in the pattern by the call to KindOfCViewAndCObserver::RefreshViewAsRequired. RefreshViewAsRequired is a generic function name that you will probably change to fit your own context.
The second part of the pattern handles the actions required for KindOfCViewAndCObserver to handle graphical user interface (GUI) events. The MFC framework will trigger a call to KindOfCViewAndCObserver::OnGUIEvent as a result of a GUI event such as pressing a button, changing a selection in a list box, or handling a mouse click. OnGUIEvent is another generic function name that will change depending on the context of the particular GUI event. OnGUIEvent calls KindOfCDynamicObjectAndCSubject::ChangeState (another generic function name) to change the state of the dynamic object as required by the GUI event. Then OnGUIEvent calls CDocument::SetModifiedFlag to mark the document as dirty and CDocument::UpdateAllViews to tell all views to update themselves as required.
Now, let's back up and look again at KindOfCDynamicObjectAndCSubject::ChangeState. Note that, after ChangeState performs its designated function, it calls CSubject::NotifyObservers to broadcast a change notification to all registered observers of KindOfCDynamicObjectAndCSubject. There could be a very long list of objects and views registered as observers, and the result could be a very complex cascade of events. Don't worry about all of that—trust the pattern and everything will work. The call to NotifyObservers will cause the CObserver::SubjectChanged function to be called for each registered observer. KindOfCViewAndCObserver::SubjectChanged calls KeepTrackOfChanges (another generic function name) which should keep track of the changes required to refresh the CView during the next CView::OnUpdate call, which results from the call to CDocument::UpdateAllViews discussed previously. KindOfCViewAndCObserver::OnUpdate calls KindOfCViewAndCObserver:: RefreshViewAsRequired to accomplish the necessary changes to the view.
The final part of the pattern concerns the actions required when our KindOfCViewAndCObserver is destructed. We need to call CSubject::DetachFromSubject to unregister the KindOfCViewAndCObserver as an observer of CDynamicObjectAndSubject. Finally we must call CDynamicObject::Release, which decrements the reference count on the dynamic object and dismisses the object from memory back to the compound file if the count reaches zero.
You may be wondering why we decided to send the WM_OBJECTINFO message to the view rather than providing a base class for the view and putting a member function in the base class that communicates the dynamic object information to the view. Since we don't know what kind of view will be used, we didn't want to provide several different base classes—one for each view class in MFC. This approach would require using runtime type information to get the class type and then making a call to the appropriate class, which is a very messy way to do things. So we picked the lesser evil of sending a custom message.
The Dynamic Object pattern probably seems a bit complicated at first glance, but handling multiple views on interdependent objects is not trivial. If you plug into the cookbook design of the Dynamic Object pattern, everything will work. This will become more evident as we move on and look at our three example DLLs. The pattern will be rigorously applied to each one. You will see how extensible the pattern is and how using the pattern becomes a mechanical process that really isn't all that complicated—considering what it accomplishes.

Dynamic Objects and Views

Our first example DLL, OVRotor.dll, demonstrates how a view should interact with a dynamic object and how to handle persistent data for dynamic objects. OVRotor.dll houses a dynamic object class called CRotor, a base class for the dynamic object called CGenericRotor, and an associated view class called CRotorView (see Figures 16 and 17). CRotor knows how to draw several different rotors on a device context. The user's rotor selection is persistent data.
Figure 16 ObjectView Hosting a CRotor Dynamic Object
Figure 16 ObjectView Hosting a CRotor Dynamic Object

Figure 17 CRotor, CGenericRotor, and CrotorView
Figure 17 CRotor, CGenericRotor, and CrotorView

Recall that a function called ClassInfo must be exported by each DLL housing a dynamic object. ClassInfo for OVRotor.dll looks a lot like ClassInfo did for Simple.dll (see Figure 18). Of course, the class ID, class category, and runtime classes are different.
Now, let's begin stepping through our Dynamic Object pattern and looking at the implementation of each step. We'll begin when CViewBroker calls CMultiDocTemplateEx::CreateNewFrame resulting with the instantiation of a new CRotorView. All that happens in the constructor for CRotorView is the addition of constraints for the flexible view behavior in the CNSFlexFormView base class (see Figure 18). Note how easy it is to add the resizing behavior. Well-designed, extensible architectures can let you add a lot of functionality by inserting new classes in the inheritance chain.
Next, CViewBroker will send the WM_OBJECTINFO message with information about the CRotor dynamic object that our view references. Again, the code looks the same as for Simple.dll (see Figure 18). The key difference this time is that CRotorView uses multiple inheritance to inherit from both CNSFlexFormView and CObserver. Inheriting from CObserver will allow us to be informed about changes to the CRotor dynamic object. After we get hold of the CRotor dynamic object through a call to CObjectInfo:: GetObject, we call CSubject::AttachToSubject to register as an observer.
If this is the first view being opened on the CRotor dynamic object, the dynamic object will not be in memory, so GetObject will instantiate a new CRotor and then call CRotor::Serialize to bring in the persistent data (see Figure 18). The Serialize function is the one you are familiar with, but recall that the object broker is in the background, silently making the compound file look like a CArchive.
The last thing CViewBroker does in opening a view on a CRotor is to call CMultiDocTemplate::InitialUpdateFrame. This results in a call to CRotorView::OnInitialUpdate, which results in a call to CRotorView::OnUpdate (see Figure 18). All we do is make sure the rotor selection in the list box is set properly and then call Invalidate to force the selected rotor to be redrawn. The details of drawing the rotor are not important here, but you can check out the source code if you are interested in how it's done.
Now we have a CRotor dynamic object and an associated CRotorView. Next, we want to see what happens on a GUI event. There's a list box on CRotorView that allows the user to select the type of rotor graphic. When the user changes the selection in the list box (see Figure 18), CRotorView:: OnSelchangeRotors gets called by the MFC framework. We follow the pattern exactly. First we call CRotor::SelectRotor (analogous to the ChangeState function name in the pattern) to change the state of our dynamic object. Then we call CDocument::SetModifiedFlag and CDocument:: UpdateAllViews.
CRotor::SelectRotor also follows the pattern exactly (see Figure 18). We change a member variable to indicate the current rotor selection, and then broadcast information about the change to registered observers through a call to CSubject::NotifyObservers.
CRotorView, a registered observer of the CRotor, will receive notification of the rotor change through CObserver:: SubjectChanged (see Figure 18). The pattern says we need to keep track of changes required to our view. For this simple case, it's just a matter of setting the m_bNeedUpdate flag.
Recall that CRotorView::OnSelchangeRotors made a call to CDocument::UpdateAllViews. UpdateAllViews generates a call to CRotorView::OnUpdate. We've already looked at CRotorView::OnUpdate since it was called as part of CRotorView::OnInitialUpdate. We complete the pattern with the destructor for CRotorView (see Figure 18). Again, we follow the pattern exactly.
Now is a good time to go and play with the ObjectView code again (see Figure 16). Click the Add button on the Object Broker window to create an instance of a CRotor object. Click the View button to open a CRotorView on the CRotor object. With a CRotorView window activated, click on the Window | New Window command or hit the toolbar button and open another view on the object. Notice how all windows get updated as you change the selected graphic. Resize a CRotorView window and notice how the controls are intelligently repositioned as a result of using the CNSFlexFormView. If you click the Remove button on the Object Broker window while the Rotor window is open, all the Rotor windows close and the dynamic object is permanently removed. Remember, all of this behavior was added without touching the ObjectView application. Also keep in mind that the CRotor objects are silently shuffling between memory and the application's compound file as required, and persistent object data is getting saved appropriately. The user and developer never notice any of this. In fact, the only thing the user will ever notice is that memory usage is low and saving files is quick—even when working with numerous large objects.

Interdependencies

Our second example DLL, OVRotorDriver.dll, demonstrates how to handle object interdependencies—a common source of problems in many applications. We're going to create a new object that tells the rotor object from the first example to draw itself at timed intervals and specific orientations. In other words, we're going to spin the rotor. OVRotorDriver.dll houses a dynamic object class called CRotorDriver, an associated view class called CRotorDriverView, and a popup dialog called CPropertiesDlg (see Figures 19 and 20). CRotorDriver knows how to reference a CGenericRotor and make it spin on a thread of execution. The references to CGenericRotor and several other parameters are persistent data.
Figure 19  ObjectView Hosting a CRotor Driver Dynamic Object
Figure 19 ObjectView Hosting a CRotor Driver Dynamic Object

Figure 20  CRotorDriver and CrotorDriverView
Figure 20 CRotorDriver and CrotorDriverView

Again, the Dynamic Object pattern makes handling object interdependencies simple and mechanical. CRotorDriver and CRotorDriverView are very similar to CRotor and CRotorView from our first example. The added twist is the reference to CGenericRotor, which makes a CRotorDriver dependent on an instance of a CGenericRotor.
Recall that CObjectInfo::GetObject generates a call to CDynamicObject::Serialize to load persistent data into the object. The interesting thing to note about CRotorDriver::Serialize is the serialization of m_csRotorKey, the key for the referenced CGenericRotor object (see Figure 21).
After calling CDynamicObject::Serialize, CObjectInfo::GetObject calls CDynamicObject::OnSerializedObjectFromArchive, which is overridden in CRotorDriver (see Figure 21). Note that CRotorDriver::OnSerializedObjectFromArchive makes calls to CRotorDriver:: AttachToObjectBroker and CRotorDriver::AttachToRotor. This is also where we launch a new thread of execution to make the rotor spin. We decided to throw threads into the example just for fun. Threads are not important to the main ideas here, so we won't dive into that subject. But you can look at the code that accompanies the article if you are interested in seeing the details.
CRotorDriver::AttachToObjectBroker simply calls CSubject::AttachToSubject to register as an observer of CObjectBroker (see Figure 21). CRotorDriver::AttachToRotor does the real work of turning m_csRotorKey, the key to the referenced object, into m_pRotor, a pointer to a living, breathing CGenericRotor object (see Figure 21). First, AttachToRotor calls CRotorDriver::DetachFromRotor to make sure we unregister as an observer of CGenericRotor and release any CGenericRotor object to which we may have already been attached. We call CObjectBroker:: GetObjectInfoByKey to get the CObjectInfo associated with m_csRotorKey, and then call CObjectInfo::GetObject to get a pointer to the CGenericRotor object. Finally, we call CSubject::AttachToSubject to register as an observer of the CGenericRotor object. It's really that simple to handle each object interdependency. It doesn't matter how numerous or complicated they get. Just be careful and do each one the same way.
CRotorDriver::SubjectChanged is also a little more interesting than before (see Figure 21). One of the nice things about the implementation of SubjectChanged is that it passes us a pointer to the CSubject that has changed. CRotorDriver is watching two subjects: the CGenericRotor object and the CObjectBroker object. Since we're holding a reference to another object, we need to know if a higher authority—the user—wants it deleted. If so, we have to release our reference. We check to see if pSubject is the CObjectBroker; if it is, we check for the OBJECT_BROKER_ ABOUT_TO_REMOVE_OBJECT hint and call CRotorDriver::SetRotorKey with a null string. CRotorDriver:: SetRotorKey sets the value of m_csRotorKey, calls CRotorDriver::AttachToRotor, and notifies any of its observers through a call to CSubject::NotifyObservers (see Figure 21).
In the destructor for CRotorDriver, we need to detach from the CGenericRotor object and CObjectBroker object. This is accomplished through calls to CRotorDriver:: DetachFromRotor and CRotorDriver::DetachFromObjectBroker (see Figure 21).
Run ObjectView again (see Figure 19). Click the Add button on the Object Broker window to create a rotor. Click the Add button again to create a rotor driver. Click the View button to open a view on the rotor driver. Now click the Properties button on the rotor driver and select the rotor from the dropdown list. Finally, hit the Start button and you will see it spinning. To change the appearance of the spinning rotor, open a view on the rotor and select a different rotor type. The Dynamic Object pattern makes everything work. Changes to rotors are smoothly propagated to rotor drivers—and the main application never had to know anything about either one.

Interchangeable Classes

Our last example DLL, OVUserRotor.dll, demonstrates the useful concept of interchangeable classes. Interchangeable classes are a very extensible way to provide hooks for customizing applications at runtime. OVUserRotor.dll houses a dynamic object class called CUserRotor that can be used interchangeably with the CRotor class introduced in the first example (see Figure 22).
Figure 22  CRotorDriver Referencing CuserRotor
Figure 22 CRotorDriver Referencing CuserRotor

Let's drop back to the OVRotor.dll example for a moment. CGenericRotor, a base class for CRotor, contains a single virtual function called DrawRotor—a hook for an interchangeable class. The popup dialog in the OVRotorDriver.dll example, CPropertiesDlg, includes a list of CGenericRotor objects. The list gets loaded by enumerating all the objects contained in the CObjectBroker object and loading only those objects that belong to the class category for CGenericRotor objects (see Figure 23). The CUserRotor dynamic object class in our new DLL is interchangeable with a CRotor class because it inherits from CGenericRotor and its class category matches the CGenericRotor class category.
Generic dynamic object base classes provide hooks for interchanging classes at runtime. Users can create runtime classes to do virtually anything as long as they abide by whatever contracts you set up in the form of virtual functions in your base class. The new classes can have their own views or dialogs, can use the services of other objects in your application, and can serialize their data in the application's compound file.

What About COM, ActiveX, and OLE?

Some of the design concepts we presented resemble COM, ActiveX, and OLE techniques. However, there are two things that separate our approach from other alternatives: the ability to add dynamic objects at runtime that can use the services of other objects living in the application, whether they are COM objects or C++ objects; and the ability to add dynamic views at runtime that are indistinguishable from other views in the application, from either a user or programming perspective.
COM objects like to communicate with other COM objects. A COM object added at runtime will have a tough time using the services of a C++ object living in the application. Dynamic objects allow that to happen. An interesting idea might be to wrap COM objects inside dynamic objects to get the best of both worlds.
ActiveX controls and OLE compound documents with in-place activation allow objects to interact with users, but the mechanism is quite different from a normal MFC CView. This may or may not make sense for your application. Dynamic object views allow interaction with the user without imposing any new user interface constraints and without a lot of additional code.

Conclusion

Good architectures break your code neatly into hierarchical layers that manage interdependence seamlessly—an important consideration for large projects with multiple development teams. You've seen that pushing our concepts to the limit can produce code that lets you compile and link your base application without ever including header files or libraries for any dynamic objects your application will host at runtime.
The concept of dynamic runtime objects and interchangeable classes can reap huge benefits for applications that need to support user extensions. Consider, for example, an economic forecasting application. A user could code a DLL for her own economic model and drop it in at runtime. There are unlimited possibilities for scientific and engineering applications where users want to twiddle with everything.
The code we're providing is just an example of how you can implement the ideas that we have introduced in this article. The various components we've presented can be customized, used alone, or used collectively. The CSubject and CObserver classes can be used by themselves to solve many types of object update problems. The CClassBroker and CObjectBroker can be used by themselves to reduce memory requirements and handle serializing object references. The CViewBroker class can be used as is or modified to fit into your application. And remember that, although we are letting the CClassBroker scan for runtime DLLs, you may choose to load classes into the CClassBroker manually. Mix and match our components to meet your own needs. You can download all of the code for ObjectView and the example runtime DLLs from the link at the top of this article.

From the July 1997 issue of Microsoft Systems Journal. Get it at your local newsstand, or better yet, subscribe.

© 1997 Microsoft Corporation. All rights reserved. Legal Notices.

© 2014 Microsoft Corporation. All rights reserved. Contact Us |Terms of Use |Trademarks |Privacy & Cookies
Microsoft