Download demo project - 157 Kb
Download source files - 7 Kb
Introduction
I don't know how about you but I was literally pumped when I learned about WTL. I'm not about to suggest that people should just dump MFC, everybody has its own considerations when it comes to choosing a tool. What's really important it became easier to do without it.
As some of you might know by now one of the missing things in WTL is document-view architecture, also known as DVA. Now, quite a few people have criticized DVA. And while I don't find it particularly flexible or even generic enough to play such an important role in MFC, that should not undermine the idea of separating the visual representation from the rest of the logic. So I figured I'll take my shot at it. Only mine of course will be different. Here is how:
First and most important we'll start with something even more generic than separating the UI from the application. Then we'll build that separation on top of it.
The idea isn't new, it has been known under different names for some time now - publish-subscribe, event sources and event listeners, connection points, delegates... All of those are forms of the same programming paradigm: Object A does something and object B reacts on it. You could say there is a connection between them two. Which brings us to the name - Connectable Objects. Ok, the name isn't mine either. The name, as well as the model was pretty much borrowed from COM. And since that's the case, I figured I'm not going to confuse people with newly invented terms, and will stick with familiar ones. So we have connection points, sink objects, the whole crew. Here is a link to a more detailed description of the concept.
We'll start with explaining what were the priorities when designing the library:
- Strong typing
That means no more eventCode
, notificationCode
, LPARAM
and so on, just straight calls.
- Minimal, impact on your existing design
Tried to keep it down with "to inherit a bananas get the whole gorilla" approach.
- Ease of use and understanding
The goal here is to make it so seamless as it almost was part of the language. (I sure wish it was as it is one of my favorite techniques.)
- Have it efficient. If an object has to notify somebody about its event we don't want this to slow it down significantly.
- It's also more of a ATL-like code taking advantage of things like RTTI and multiple inheritance, the lack or the avoidance of which, in my opinion, constrained at the time the design of MFC. It's not really important but I thought I'd mention it.
What we don't have is thread marshaling, so if your sink object is observing objects running in different threads you'll have to take care of it yourself.
Sorry, that's the price we had to pay for strong typing and efficiency. You can however implement your own marshaling when you need it, this way you don't bloat the majority of applications that simply don't use multiple threads.
I'm also planning to look into a generic solution for this.
A sample code
The lecture is getting a little boring so let's dig right in and examine some code.
How do you get two objects connected anyway?
We need three classes:
- The subject (the object firing the events, the event source)
- The observer (or sink object, the object which gets notified about events the first object fires)
- The sink interface (the interface observers must implement in order to allow the subject to talk to them)
That's what it will look like:
class CSinkInterface {
virtual void onEvent1(int p1, LPCTSTR p2) = 0;
....
};
class CObserver : public CSinkInterface {
COSinkOwner m_sinkOwner;
virtual void onEvent1(int p1, LPCTSTR p2) {
...
}
};
class CSubject : public COConnectionPointContainer {
...
CO_BEGIN_CONNECTIONPOINTS(CSubject)
CO_CONNECTIONPOINT(CSinkInterface)
CO_END_CONNECTIONPOINTS()
void method1()
{
...
CO_LOOP(CSinkInterface)
eventSink->onEvent1(p1, p2);
CO_ENDLOOP
}
};
And finally how do we connect them?
CSubject subject;
CObserver observer;
COConnectionPointImpl<CSinkInterface> *cp;
if(subject.coFindConnectionPoint(&cp))
cp->Advise(&observer, &observer.m_sinkOwner);
I intentionally used unrelated names (CSubject
and CObserver
) to make a clear separation between application and the library. I also decided to use the prefix "CO" instead of "C" in class names to avoid collisions with COM, and the prefix "co" in overridable methods to avoid collisions with your methods.
A brief description
So that's the simplest example. Almost every name is known from COM connection points. The only new term here is COSinkOwner
. This is the object responsible for breaking the connection when the sink object goes down. On another end, if the event source dies the connection is broken by an object hidden under CO_CONNECTIONPOINT
.
We could also derive CObserver
from COSinkOwner
and this way we could get rid of m_sinkOwner. That would also buy us the possibility to override a couple of methods (coOnAdvised
and coOnUnadvised
to be exact) and get notified when we get connected to another object. By the way, CSubject
can overwrite coOnConnection
and coOnDisconnection
to do the same thing.
The ability to separate the sink owner and the sink object is very important. For example we can get a form to be a sink owner for its controls. It also can help us with using a technique resembling Java' inner classes to solve name collisions under multiple inheritance.
Just remember one thing - the sink owner should either die before the sink object does or you should take care of calling Unadvise
yourself. Or else... right, Dr' Watson takes over the conversation.
I think it's needless to mention that one object can expose many connection points, so can it connect to many sources. Just as it is with COM connection points.
Q&A
- So since we have so many similarities why not just use COM?
- Because not every class is meant to be a COM object and not every sink interface needs to be declared in a type library. I think the idea of connectable objects is very valuable by itself since it can help making components more loosely coupled and hence more reusable.
- What about Document-View?
- There are many things that can be written using connectable objects.
Since UI separation is the most obvious, the most widely used, part two of this article will focus exclusively on that. I think the topic is important and complex enough to deserve a dedicated article.
One thing I can probably guess, it's very likely that it will be different from MFC doc-view.
- How to contact me
- If you have any questions or suggestions feel free to ICQ me at 11411966 (just type "co" in the authorization request, since I disabled receiving messages from people which are not in my list), or just e-mail me.
- Updates
- Follow this link.