All entries for Friday 09 September 2005
September 09, 2005
A current job I'm doing has exposed the need for a half-decent document-view architecture in .NET. Back in my (brief) MFC days it was all about CDocument and CView objects! However in .NET (even 2.0) we are without any infrastructure to build on.
The only proper documented attempt I found online was Chris Sells series Creating Document-Centric Applications in Windows Forms Although Chris does give a rather excellent explanation and full implementation there were a few reasons why I can't use it. First off the "document" aspect is an actual Component, that's great for drag-n-drop style development, but I already have a code base that I simply wish to "act-as" a document. Also Chris's code is a bit too complete, in such a way that it limits my options in terms of implementation of my application. Finally, Chris's code does not easily support multiple document types or multiple views of a single document. All that said, I still thank Chris for that excellent series of articles that certainly got me thinking!
So what is my approach then?
- Interfaces – Loose Coupling – Flexibilty*
My framework revolves around 4 simple interfaces:
IDocument defines what a document object should be able to do:
Public Interface IDocument
ReadOnly Property IsDirty() As Boolean
ReadOnly Property IsNew() As Boolean
Function Save() As IDocument
Function SaveAs(ByVal context As Object) As IDocument
- Track if it is dirty (has un-saved changes) and if it is new (never saved).
- Save itself and save it self to a given context. Note that context will usually be a filename, but there is no reason that a certain document type wouldn't save to a database or other location.
A view should provide some manner of interaction with a document. Typically, this will be a Form or UserControl.
Public Interface IView
Property Document() As IDocument
Event Closing As EventHandler(Of System.ComponentModel.CancelEventArgs)
Event Closed As EventHandler
It needs a reference to its document object.
The EndEdit() method will called when saving to allow the view to commit any pending edits to the document.
The Closing event will be listened to by the infrastructure to know when a view has been closed. When the last view of a document it is closing it can ask the user to save changes, etc.
Originally I did not have a Closed event and put the code to remove a document from the application in the Closing handler. However, due to way in which .NET MDI Parent forms raise Closing on all child forms, then raise all the Closed, instead of Closing followed by Closed for each child, I had to require the this additional event. Despite the added implementation detail, it does make more sense in some ways,
Public Interface IDocumentFactory
Function NewDocument() As IDocument
Function OpenDocument(ByVal context As Object) As IDocument
Function CreateView(ByVal document As IDocument) As IView
The purpose of a document factory is to allow the framework to open new and existing documents, and to create a view of a document. You wonder why we don’t just create document objects ourselves, but the reason for this interface will become clearer once we delve into the document-view manager class. By abstracting the idea of a document factory we can do clever things like put a factory class into a DLL separate from the document. We can enumerate these factory assemblies without having to load all the associated business logic and document code into our working set. When an application supports a large number of different document types this reduction in memory allocation will certain improve start-up times. Additional meta-data can be added to the factory implementations to describe the documents they can create. This meta-data could be used to build runtime UI, such as “New Document” and “Open Document” dialog boxes.
A factory implementation could even return different document types and views depending on some value passed into the constructor, for example. This would not be possible if the creation logic was in the actual document.
The following interface is the real core of the framework. So it is a little big, but bear with me!
Public Interface IDocumentViewManager
Event ActiveViewChanged As EventHandler
Event ViewListChanged As EventHandler(Of ViewListChangedEventArgs)
Event PromptToSaveChanges As EventHandler(Of PromptToSaveChangesEventArgs)
Event ShowSaveAsUI As EventHandler(Of ShowSaveAsUIEventArgs)
Property ActiveView() As IView
ReadOnly Property Documents() As ReadOnlyCollection(Of IDocument)
ReadOnly Property Views(ByVal document As IDocument) As ReadOnlyCollection(Of IView)
Function NewDocument(ByVal documentFactory As IDocumentFactory) As IDocument
Function OpenDocument(ByVal documentFactory As IDocumentFactory, ByVal context As Object) As IDocument
Function SaveDocument(ByVal document As IDocument) As IDocument
Function SaveDocumentAs(ByVal document As IDocument) As IDocument
Sub CreateView(ByVal document As IDocument, ByVal info As IDocumentFactory)
Sub AddView(ByVal document As IDocument, ByVal view As IView)
Before diving into the member-by-member descriptions, I should discuss the overall objective a document-view manager. UIs change and get re-done over time, so it is important to remove the document-view management from any actual UI code. Yes, we could stuff a document list into a MDI form, but that will never be a maintainable solution over time. At the same time I did not want to totally prevent that option. My framework includes an implementation of IDocumentViewManager, but there is nothing to stop someone making a form implement it directly.
A document-view manager basically has a list of documents currently loaded and a list of open views for each document. If provides a central location to create new documents, open existing ones and save them. It can create the default view for a document and also allows a view created externally to be added to a document.
The manager also keeps track of views being closed so it can prompt to save changes if a document is dirty.
First the events:
- ActiveViewChanged should be raised when, as expected, the active IView in the application has changed. This will allow other UI to update accordingly.
- ViewListChanged should be raised when an IView is added/deleted to/from a document.
- PromptToSaveChanges should ask the user if they want to save changes to a given document. The event arguments contain both the document and a “response” property in which to store the selected action (Yes, No or Cancel).
- ShowSaveAsUI should display some way for the user to enter the context under which to save the document. For example, show a SaveFileDialog. The event arguments still allow the setting of a “Cancel” property to abort the save operation. There is also a property, Context, to save the filename or other save location.
The three properties are fairly self explanatory. ActiveView gets or sets the currently active IView object in the UI. Documents returns a ReadOnlyCollection, enforcing that all changes must be made through the interfaces methods, not directly on the collection. Views will return a ReadOnlyCollection of views for a given document.
The NewDocument and OpenDocument functions use a document factory to actually create or open a document. Once a document object is created it is added to the manager’s list of documents.
The save functions should examine the state of the document (via IsDirty and IsNew) and raise the necessary events to prompt the user and get the save context. In addition, EndEnd() should be invoked on each IView for the document.
… I have run out of time at the moment, but will continue this discussion at the next convenient point in time…