Document/View Manager

Let's first take stock of the requirements for a document/view manager, and write a little testcase.The document/view manager will have to take care of the mapping between documents and views. Every document can have more than one view, but a view can only show one document.

A guideline during implementation is that the document/view manager should not make GUI calls directly. There are two reasons for this: it is easier to write good testcases if there is no GUI involved, and denying the document/view manager access to the GUI forces us to place all the actual GUI code in one place, namely the application or controller object.

Here's the testcase:

Example 15-1. A testcase for a document manager

import unittest
from docmanager import *
from qt import *

class TestViewManager(QObject):

    def activeWindow(self):
        return None

    def width(self):
        return 100

    def height(self):
        return 100

class TestParent(QObject):

    def queryCloseDocument(self, document):
        return QMessageBox.Yes

    def querySaveDocument(self, document):
        return QMessageBox.No

    def queryDiscardDocument(self, document):
        return QMessageBox.Yes

    def queryFileName (self, document =None):
        return "fileName"

class TestDocument(QObject):

    def modified(self):
        return TRUE

    def save(self):
        pass

    def close(self):
        pass

    def title(self):
        return "title"

    def pathName(self):
        return "pathname"

    def setPathName(self, pathname):
        pass

class TestView(QObject):

    def __init__(self, parent, document, *args):
        QObject.__init__(self, parent)
        self._document = document

    def show(self): pass

    def showMaximized(self): pass

    def setCaption(self, caption): pass

    def resize(self, x, y): pass

    def close(self, destroy):
        return TRUE
    

The purpose of this testcase is to test the documentmanager. An interesting side effect is that the development of the testcase necessitates the development of fake versions of the other necessary components. Creating these fake components for the view, document and application makes clear which functions they must support.

The TestViewManager class is an interesting object. It will manage the different views (windows, tabs, or splitpanes) for the application. As such, it will become the visual counterpart of the DocManager class.

The TestParent represents the application itself — that is, the central class that manages all QActions, menus, toolbars and so on. As you can see, we need four methods in the application, to ask the user whether she wants to close, save or discard the document, and what the filename should be. By not calling QMessageBox or QFileDialog directly, we again get a stronger separation of GUI and application logic. But life is messy, and a complete separation is not attainable.

This is most apparent in the TestView class. Here we need to create stubs for a number of functions that are part of QWidget, such as setCaption().

The TestDocument class also shows a clear interface: but more than that, it is also clearly meant for file-oriented applications. A database application would in all likelihood not concern itself with obscurities like pathnames. On the other hand, with a database application it is even more important to allow more than one view on more than one document at a time — if we simply equate document with query.

class DocManagerTestCase(unittest.TestCase):

    def setUp(self):
        self.parent = TestParent()
        self.viewManager = TestViewManager()

    def checkInstantiate(self):
        try:
            docManager = DocManager(self.parent, self.viewManager)
        except Exception, e:
            self.fail("Could not instantiate docmanager: " + str(e))

    def checkCreateDocument(self):
        docManager = DocManager(self.parent, self.viewManager)
        numberOfDocs = docManager.numberOfDocuments() + 1
        numberOfViews = docManager.numberOfViews() + 1
        try:
            document = docManager.createDocument(TestDocument, TestView)
        except Exception, e:
            self.fail("Could not add a new document: " + str(e))

        assert document, "No document created"
        assert numberOfDocs == docManager.numberOfDocuments(),\
               "No document added"
        assert numberOfViews == docManager.numberOfViews(), \
               "No view added"
        assert docManager.views(document),\
               "Document does not have a view"
            
    def checkAddView(self):
        docManager = DocManager(self.parent, self.viewManager)
        document = docManager.createDocument(TestDocument, TestView)
        numberOfDocs = docManager.numberOfDocuments()
        numberOfViews = docManager.numberOfViews() + 1
        numberOfDocViews = len(docManager.views(document)) +1
        
        try:
            view = docManager.addView(document, TestView)
        except DocManagerError, e:
            self.fail(e)
        except Exception, e:
            self.fail("Could not add a view to a document " + str(e))
            
        assert view is not None,\
               "No view created"
        assert numberOfDocs == docManager.numberOfDocuments(),\
               "Document added"
        assert numberOfViews == docManager.numberOfViews(), \
               "No view added"
        assert numberOfDocViews == len(docManager.views(document)), \
               "No view added to document"

        view = None
        document = TestDocument()
        try:
            view = docManager.addView(document, TestView)
            fail("Should not have been able to add a view " +
                 "to an unmanaged document")
        except DocManagerError, e:
            pass
        assert view == None,\
               "View created"

    def checkCloseView(self):
        docManager = DocManager(self.parent, self.viewManager)
        document = docManager.createDocument(TestDocument, TestView)
        view = docManager.addView(document, TestView)
        numberOfViews = docManager.numberOfViews()
        docManager.closeView(view)
        assert numberOfViews > docManager.numberOfViews(), \
               "No view removed: was %i, is %i" % (docManager.numberOfViews(),
                                                   numberOfViews)
        
    def doNotCheckCloseDocument(self):
        docManager = DocManager(self.parent, self.viewManager)
        document = docManager.createDocument(TestDocument, TestView)
        docManager.closeDocument(document)
        assert docManager.numberOfDocuments() == 0,\
               "docManager still manages a document"
        
def suite():
    testSuite=unittest.makeSuite(DocManagerTestCase, "check")
    return testSuite

def main():
    runner = unittest.TextTestRunner()
    runner.run(suite())

if __name__=="__main__":
    main()
     

A look at the testcases shows how the documentmanager is intended to be used. When a document is created, one view is automatically created. More views can be added to a document. Views can be removed, and when the last view is removed, the document is closed. Creating documents and views is the job of the documentmanager; this is why we pass the classes of the view and document to the manager, and not to complete objects.

As I said, life is messy, and if you look at the last test, you will see one bit of unavoidable mess. During the implementation of the document manager it became clear that in order to ‘catch' events (such as closing the application or window with the close button in the title bar) it was necessary to install an event filter in every view. This meant that the original implementation of closeDocument(), which called closeView(), had to be changed to one where closeDocument() called view.close() — which fires the event filter, which fires the closeView(). This, however, is only possible if you use actual QWidget-derived objects; it cannot be done with the fakes we created for the test. This means that the checkCloseDocument() needs to be renamed doNotCheckCloseDocument()(It is a convention to prefix tests that don't work with doNot)—the test will never work.