This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


February 1996

Microsoft Systems Journal Homepage

C++ Q&A

Paul DiLascia is a freelance software consultant specializing in training and software development in C++ and Windows. He is the author of Windows++: Writing Reusable Code in C++ (Addison-Wesley, 1992).

Click to open or copy the VIEW3 project files.

QI am developing a Windows® 3.1 MDI application with Visual C++™ and MFC. Each document can have three views: a text view, a contour plot view, and a 3D surface plot view of data from a manufacturing platform. The view is selected by the user when defining a document. The type of view is then stored in the document when it's saved. When the document is restored, how can I determine the correct view to invoke based on the saved view type? The way the document/view architecture is defined, this looksimpossible.

Roger Vaught

AThe easiest and most straightforward way to implement multiple views is to store an ID in your view class that identifies which "mode" the view is in, and act accordingly.

 enum { VIEW_TEXT, VIEW_CONTOUR, VIEW_3D };

void CMyView::OnDraw(CDC* pDC)
{
switch (m_nMode) {
case VIEW_TEXT:
OnDrawText(pDC);
break;
case VIEW_CONTOUR:
OnDrawContour(pDC);
break;
case VIEW_3D:
OnDraw3D(pDC);
break;
default:
ASSERT(FALSE);
}
}

This approach works well if the differences among your views are limited to a few places, such as drawing. But if each view has different commands and different behavior in a number of places, the switch (or if) statements will grow quickly tiresome, not to mention inelegant. Whenever you have a class with member functions that look like the fragment above, you really want several classes. After all, that's what C++ and virtual functions are for.

 class CViewText : public CView {
virtual void OnDraw(CDC* pDC); . . . };
class CViewContour : public CView {
virtual void OnDraw(CDC* pDC); . . . };
class CView3D : public CView {
virtual void OnDraw(CDC* pDC); . . . };

With these classes defined, when you or the framework calls pView->OnDraw, the right OnDraw function is invoked for whichever class pView really points to. That's what virtual functions do; it's their whole role in life. The only question is, how and when do you create the appropriate view?

The problem is that MFC assumes that there is one and only one view class associated with each kind of document. This is reflected in the definition of CDocTemplate, which stores three pointers for the document, frame, and view classes.

 class CDocTemplate : public CCmdTarget {

               .
               .
               .
  CRuntimeClass* m_pDocClass;   // for new docs
  CRuntimeClass* m_pFrameClass; // for new frames
CRuntimeClass* m_pViewClass; // for new views
};

(There are other classes too, for OLE frames and such, but you needn't worry about those here.) These pointers are initialized when you create the document template, usually in your app's InitInstance function.

 AddDocTemplate(new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CMyDoc),
RUNTIME_CLASS(CMyMainFrame),
RUNTIME_CLASS(CMyView)));

You only get to specify one view class. Not two, not three, not seventeen. One. When MFC decides it's time to create a new frame on the fly-such as whenever the user opens a document in a MDI app-MFC uses the CRuntimeClass pointers in CDocTemplate to create the objects it needs.

 // Create new doc 
// (in CDocTemplate::CreateNewDocument)
pDocument =
(CDocument*)m_pDocClass->CreateObject(); // Create new frame
// (in CDocTemplate::CreateNewFrame)
CFrameWnd* pFrame =
(CFrameWnd*)m_pFrameClass->CreateObject();

This is why views, frames, and docs must be declared with DECLARE_DYNCREATE (or DECLARE_SERIAL): so MFC can create them through its run-time class mechanism, CRuntimeClass::CreateObject. The view is created in similar fashion, but indirectly. The doc template creates the frame, and the frame in turn creates the view in its OnCreate handler. The frame knows which view class to create because CDocTemplate passes m_pViewClass inside an intermediate struct, CCreateContext. Since there's just one m_pViewClass pointer, you can have only one kind of view.

At least, that's how MFC's doc/view UI model is supposed to work. In practice, you can always do whatever you want-if you're sneaky enough. But before you go violating the nice pretty model, it's a good idea to think about what you're doing. Usually, whenever you bump into some sort of fundamental limitation, it's a sure sign that something deep is going on. What are the UI implications of having more than one view per document?

A number of questions arise. How do users select which view they want? If you save the view type in the document, does that mean changing views counts as modifying the document? If not, the change may not get saved. And what happens if there are two views open on the same document? Most MDI apps have a "New Window" command that lets users create another window (frame/view) on the current document. AppWizard even generates this command for you. Unless you explicitly remove it, users can create multiple views on the same doc. If there are two views on a document-say one is contour and one is 3D-which one do you save? The active one? Both? If both, should you also save the positions of the windows? If you save the positions and types of all the views, you're moving toward a workspace UI model, like the one Visual C++ 4.0 uses. In this model, there's a file called the "project" or "workspace" that references other files. The project provides a place to store the positions of all the document windows and other supra-document information.

My point is just that whether you go for a full-blown workspace UI model or not, you enter a different world as soon as you allow more than one view per document, so you need to sit down and explore all the implications and design your user interface carefully, before you write any code. This is one situation where adding what seems like an innocent feature in fact opens a whole can of worms.

That said, I implemented a simple multiview app, VIEW3, that implements three different views on a document (see Figures 1 and 2). VIEW3 documents store a single text string, which VIEW3 lets users view either as normal ASCII text, binary zeroes and ones, or as "binary stripes": the ones and zeroes are converted to black and white vertical stripes that resemble a bar code. Three commands, View As ASCII, View As Binary, and View As Binary Stripes, let users change the view. VIEW3 stores the type of whichever view was active when the document was last saved, and restores it when that document is opened.

Figure 1 Three, three, three views in one!

There are several different ways you could implement a program like VIEW3. I already outlined the type-code-and-switch-statements approach. You could also implement the three views as three different child window classes contained within a single parent CView class. The view would handle some operations itself, and delegate others (such as drawing) to the child windows. You could even keep all the windows alive at the same time, but only show whichever one the user selects. The main advantage with this approach is that from MFC's perspective, there's still only one view class, so you don't have to delve into MFC architecture to make it work. The drawback is that it's a bit cumbersome. You have to create and destroy the child windows, you have to size them whenever the view gets an OnSize message, you have to route commands to the current active child window, and you may have to handle messages like as OnActivate and OnSetFocus that MFC normally takes care of invisibly.

The only really satisfying approach, the one I chose for VIEW3, is to implement three different view classes. I mean, if you have three different views of your document, you should be able to implement them that way, right? The challenge is figuring out how to fake MFC into using multiple view classes when it really wants to use just one. It's a little hairy, so now might be a good time for a caffeine supplement.

In VIEW3, there are two places where the view can change: when the user invokes one of the View As commands and when the user opens a new document. You might guess from my opening discussion that all you have to do to make VIEW3 fly is change CDocTemplate::m_pViewClass.JustinterceptOnFileOpen,setpDocTemplate->m_pViewClass to whichever view class you want, and then let CWinApp::OnFileOpen continue on its merry way. In fact, this will work fine. There's just one problem: you don't know which view class to use until the document is loaded-which happens in CWinApp::OnFileOpen. By then, it's too late-MFC has already created the frame and view! Moreover, setting m_pViewClass won't help for the View As commands, since these commands don't involve creating a new frame or view, at least not as far as MFC knows.

Youmightbetemptedtowriteacommandhandlerlikeso.

 void CMyFrame::OnViewAs3D()
{
CView* pView = GetActiveView();
delete pView;
pView = new CView3D;
SetActiveView(pView);
}

This is basically the right idea, but there are a few details to sweat before it'll fly. First of all, you have to do something with the window (HWND) associated with the view. And you have to be careful about destroying any object that MFC stores a pointer to. I originally tried destroying the view in my view's OnInitialUpdate function. Ignoring for a moment the fact that I was deleting the very object through which I was being called, effectively doing a "delete this" inside a member function (a stunt that's always socially unacceptable but not necessarily fatal, and even MFC does it sometimes), my code failed because it was called from some function deep in MFC that had a pView local variable pointing to the view I'd just deleted.

 // (pseudo-code in MFC)
pView = pFrame->GetActiveView();
pView->OnInitialUpdate(); // calls me to delete pView
pView->Activate(); // Oops!

Needless to say, things weren't very copacetic when my function returned and MFC tried to activate my deleted view! Sigh.

The only safe place to destroy the view is in some class outside it. Since the document template is the official coordinating entity for documents, frames, and views, it's the natural choice. For VIEW3, I derived a new doc template class, CDynViewDocTemplate, that implements "dynamic views" in a totally generic fashion. You should be able to simply plop DYNTEMPL.CPP and DYNTEMPL.H directly into your app and use CDynViewDocTemplate without altering it.

To use CDynViewDocTemplate, the first thing you have to do is create a table that tells it what view classes you have. VIEW3.CPP does it like this:

 static const DYNAMICVIEWINFO MyViewClasses[] = {
{ RUNTIME_CLASS(CView1), "ASCII" },
{ RUNTIME_CLASS(CView2), "Binary" },
{ RUNTIME_CLASS(CView3), "Binary Stripes" },
{ NULL, NULL }
};

DYNAMICVIEWINFO is defined in DYNTEMPL.H. It associates a display name with each run-time class. You can put as many views as you like in the table, but the order of the entries must match the order of View As menu command IDs and the last entry must contain NULLs. You pass this table when you create your document template in your app's InitInstance function.

 // Create "dynamic" document template. 
AddDocTemplate(new CDynViewDocTemplate(IDR_VIEW3TYPE,
RUNTIME_CLASS(CView3Doc),
RUNTIME_CLASS(CChildFrame),
MyViewClasses)); // array of view classes

This looks a lot like creating a normal CMultiDocTemplate, which is a good sign that the design is reasonable. The difference is that instead of specifying a single view class, you give a table of classes. CDynViewDocTemplate uses your table to swap views when the user invokes one of the View As commands. CDynViewDocTemplate uses ON_COMMAND_EX_RANGE and ON_UPDATE_COMMAND_UI_RANGE to handle commands in the range from ID_VIEW_AS_BEGIN to ID_VIEW_AS_END. (I used the _EXversionof ON_COMMAND_RANGE so other command targets in the app can handle the View As commands too.)

 BEGIN_MESSAGE_MAP(CDynViewDocTemplate, CDocTemplate)
ON_UPDATE_COMMAND_UI_RANGE
(ID_VIEW_AS_BEGIN, ID_VIEW_AS_END, OnUpdateViewAs)
ON_COMMAND_EX_RANGE
(ID_VIEW_AS_BEGIN, ID_VIEW_AS_END, OnViewAs)
END_MESSAGE_MAP()

You must define ID_VIEW_AS_BEGIN and ID_VIEW_AS_END in your RESOURCE.H file. I could have let the IDs be passed at run time, but I'd have had to override CDocTemplate::OnCmdMsg, or else handle the entire range of IDs from 0 to 0xFFFFFFFF and then examine the actual ID to see if it was mine-but both require writing several lines of code and cause a slight performance penalty, since every command would go through my handler, even ones I'm not interested in. It seemed better and not so terribly odious to simply hardcode a couple of #define symbols and make programmers use them. MFC's message map macro approach, where everything must be known at compile time, makes life easy for code generators but it's not particularly conducive to writing reusable code and class libraries. Such is life.

The OnViewAs command handler in DYNTEMPL.CPP converts the command ID to a zero-based offset from ID_VIEW_AS_BEGIN, and passes this view ID to a helper function, ReplaceView, which uses it as an index into the view class table to find out which view to use. If the requested view class differs from the current one, ReplaceView changes the view by destroying the old one and creating a new one.

 // Tell MFC not to delete my document
// when I destroy the last view.
CDocument* pDoc = pView->GetDocument();
BOOL bSaveAutoDelete = pDoc->m_bAutoDelete;
pDoc->m_bAutoDelete = FALSE; // Destroy old view and create new one,
// preserving the window (HWND)
pFrame->SetActiveView(NULL);
HWND hwnd = pView->Detach();
delete pView;
pView = (CView*)pViewClass->CreateObject();
pView->Attach(hwnd);
pDoc->AddView(pView);
pFrame->SetActiveView(pView); // Restore doc and do initial update
pDoc->m_bAutoDelete = bSaveAutoDelete;
pFrame->InitialUpdateFrame(pDoc, bMakeVisible);

The above may look a little hairy at first, but it's not really so bad. The basic idea is to reuse the window (HWND) instead of destroying it and creating a new one. This has the advantage of preserving the window size, style flags, and any other information stored in the window itself, and it's faster. Instead of using the normal MFC allocate/Create sequence for creating windows, I simply Detach the HWND from the old view, and reAttach it to the new one. Easy. Of course, if you're going to create a view in unorthodox ways, you've got to hook it up manually to its document and frame. CDocument::AddView and CFrameWnd::SetActiveView are the functions that do it. The only trick is that you have to set pDoc->m_bAutoDelete to FALSE before destroying the original view, because otherwise MFC will delete the document when you destroy the old view. (I found out the hard way.) Once the new view is installed, ReplaceView calls InitialUpdateFrame to do everything MFC would normally do when a new frame/view is created-like update the title and send WM_INITIALUPDATE to the view.

Of course, the decision to recycle the window (HWND) is justthat:adecision.Youcoulddoitdifferently.Forexample, you might use a different (Windows) window class for each view. In that case, you'd call DestroyWindow instead of Detach/delete to destroy the old view; and new/Create or even CFrameWnd::CreateView to create the new view, instead of new/Attach. DYNTEMPL.CPP actually contains both implementations. To select the non-window-recycling implementation, just #define DONT_RECYCLE_HWND.

OK, so much for the View As commands. To change the view when a document is opened, CDynViewDocTemplate overrides the virtual function CDocTemplate::InitialUpdateFrame. MFC calls this function after a new frame and view are created and the document has been loaded-but before the frame is displayed. (The CDynViewDocTemplate constructor initializes m_pViewClass to the first view class in your table.) It's the perfect time to change the view without the user noticing. The implementation is trivial, it calls the same ReplaceView function that OnViewAs calls. The only problem is: how does CDynViewDocTemplate get the ID of the view to use? I couldn't just write pDoc->GetViewID, because GetViewID is not a member of CDocument.

Being a firm believer in reusability, I wanted to make CDynViewDocTemplate as self-contained and generic as possible, so anyone could use it to implement apps that support multiple views per doc. The obvious thing to do is derive a new class, CDynViewDoc, with a pure virtual function, GetViewID, and make programmers derive their doc from that. But it seems excessive to derive a whole new class just for one virtual function-and what if you want to derive from COleDocument instead of CDocument? So I took an unorthodox approach. I introduced a new function, CDynViewDocTemplate::GetViewID(CDocument*). Instead of

 pDoc->GetViewID();

I use

 GetViewID(pDoc);

But that only begs the question: how do I implement CDynViewDocTemplate::GetViewID? I don't! I left it out of DYNTEMPL.CPP. In other words, someone else (ahem) has to implement it. Why not? There's no law that says all the functions for a class or library have to be implemented in the same file, or even implemented at all! GetViewID is like a "callback" that you supply at compile time instead of run time! In VIEW3, CDynViewDocTemplate::GetViewID is implemented in DOC.CPP:

 // (in VIEW3\Doc.cpp)
int CDynViewDocTemplate::GetViewID(CDocument *pDoc)
{
ASSERT_KINDOF(CView3Doc, pDoc);
return ((CView3Doc*)pDoc)->GetViewID();
}

It just returns the ID stored in a data member m_nViewID. Another app could implement CDynViewDocTemplate:: GetViewID differently. It doesn't matter how you implement it, as long as you return a valid index into the original view class table you supplied when you created the CDynViewDocTemplate.

The only other interesting thing in CDynViewDocTemplate is a command update handler that sets the radio buttons in the View As menu (see Figure 1). You can read the code yourself to see how it works; it's not rocket science.

The rest of VIEW3 is pretty simple. CView3Doc::Serialize saves the ID of whichever view was active when the user saved the document, and restores it on loading. VIEW3 has three different view classes, CView1, CView2 and CView3, each with a different OnDraw function that implements the view. The views all derive from a common CBaseView that implements a command to modify the document's content. Just for fun, I wrote my own CChildFrame::OnUpdateFrameTitle to include the name of the view type in the caption, as in Figure 1.

VIEW3 compiles with MFC 4.0 or later, but it would only require a few modifications to work in earlier versions. Now, if only I knew how bar codes really work, maybe I could sell come copies of VIEW3 and make a buck.

Have a question about programming in C or C++? You can mail it directly to C/C++ Q&A, Microsoft Systems Journal, 825 Eighth Avenue, 18th Floor, New York, New York 10019, or send it to MSJ (re: C/C++ Q&A) via:


Internet:

Paul DiLascia
72400,2702

Eric Maffei
ericm@microsoft.com

From the February 1996 issue of Microsoft Systems Journal.