<!-- Download Links -->
This document is designed to look at some of the problems associated with multiple views on a common data set, such as the Document/View architecture found in the MFC class libraries. This is more a "proof-of-concept" study than a full fledged application or tool, used to study different approaches to this topic.
C# and the .NET framework does not provide built-in Document/View style architecture that so many C++/MFC programmers know and love. Instead, they have left it to the developer to implement his/her own scheme for manipulating and managing multiple views on a single or common data set, which will have to be reproduced over and over for each new project requiring this type of capability.
I don't know how others are resolving this type of issue, because after hours and hours of searching the internet for C# and .NET coding sites and newsgroups, I have found nothing approaching this topic and newsgroup messages to numerous groups have gone without response. Is that because programmers consider this so obvious and beneath explanation? Is it because nobody has gotten around to doing it yet? I don't know, so I decided to look into it. Hopefully this article will find usefulness among the developer community.
Into The Abyss
Ok, let's get to the issue at hand. You have an application that defines and maintains a certain data set. The application allows for multiple views on the data set. MFC provides an extremely easy and painless way for this using the
DocumentTemplate class on the main application. It is possible to define multiple templates for multiple view types as well as multiple document types. The default MFC windows messaging will then handle all the "dirty work" and "bookkeeping" for you. In fact, the code to instantiate looks something like the following:
pDocTemplate = new CMultiDocTemplate(
Without any further intervention from the developer, the MFC framework will handle the MDI needs of the application, in this case multiple
CMyView instances on a single
CMyDoc instance. The question of the hour, however, is "what is it really doing and how can I provide this capability in my C# application?"
The document template is basically the container the application uses to store and manage the different documents the application is suppose to handle. The document template creates a managed list of all document instances that match the template. The document itself contains a managed list of views attached to it, and the views contain a reference back to the document whose data it is reflecting. When the document has been modified, the document calls a method that iterates over all views attached to it and calls a method that basically informs each view to examine and redisplay the data contained within the document
The 'I's have it
What we need to do is to provide similar capabilities with our C# classes. This intent here is not to encapsulate the full capabilities provided by MFC, but rather to lay some groundwork and get started. With that in mind, what are we looking for in this demonstration? Well, the document will need a means to store a list of views who desire to reflect it's information, and some member methods to manipulate and manage these views. The views will need a member method to respond to the documents update. Additionally, a means to get and set the views document would be handy as well.
So now to approach the code and start putting this stuff in, what are our options? We could provide a base class and force our data set and view classes to derive from them. This gives us reusability and reliability, which is good, but could it restrict us in some ways? Perhaps, especially since C# does not accommodate multiple inheritance. I think that the implementation of these methods are simple enough to not cause any problems, but what if the user has some advanced, specialized, or 3rd party class that they really want to use? If the developer does not have access to the source of the class s/he what's to use, and, perhaps the code for this data set/view scheme is in a library and the source is not accessible either, then what?
Perhaps, then we'll take a look at using the C# Interface. This doesn't really provide the reusability we would ideally want, but does provide greater flexibility without forcing a base class on someone. Besides, I wanted to play with Interfaces a bit and it's my article. :) The interfaces might look something like this.
public interface IDocument
void AddView ( IView view );
void CloseView ( IView view );
void CloseAllViews ( );
void UpdateAllViews ( );
public interface IView
void AddDocument ( IDocument doc );
IDocument GetDocument ( );
void UpdateData ( );
When used, the compiler will force the user to implement the methods defined with the interface. The implementation is fairly straightforward. Let's define a data set class, let's call it
DataSet, that implements the
public class DataSet : IDocument
The first thing we need to do is to define a managed view list on the
DataSet class. Fortunately the rich set of classes provided by C# and the .NET framework has just the ticket in the
System.Collections.ArrayList class it provides.
private System.Collections.ArrayList viewList;
CloseView methods are very straight forward, they simply insert or remove a view class from the managed view list on the
DataSet like so.
public void AddView (IView view)
if ( !this.viewList.Contains (view) )
public void CloseView (IView view)
UpdateAllViews are also fairly straightforward. They must cycle thru all the views attached to the data set.
public void CloseAllViews ()
foreach (ViewForm view in this.viewList )
public void UpdateAllViews ()
foreach (IView view in this.viewList )
Believe it or not, we are already headed for trouble. Nobody's ever accused me of being the sharpest knife in the drawer, so perhaps you have already figured out one of the more significant problems we are about to face, and will be outlined in just a few minutes. By looking at the code, you've probably already guessed the name of the view class, so let's look at that for a moment.
ViewForm class implements the
IView interface. The methods, again, are fairly straightforward and look something like:
public void AddDocument ( IDocument doc )
if (this.m_DataSet != null && this.m_DataSet != doc)
System.WinForms.MessageBox.Show ("Error: Attempting to attach a data set to a form "
+ "which already contains a data set." );
this.m_DataSet = doc;
public IDocument GetDocument ( )
public void UpdateData ( )
System.Console.WriteLine ("Update the view: " + this.Text );
In addition to the implementation of the Interface methods, the constructor methods of the
ViewForm classes need some modification as well. The
DataSet needs to know the main frame/window that will contain the data set and it's MDI views. Likewise, the
ViewForm constructor needs to know the parent window/frame as well as the data set to which it is suppose to display.
public DataSet( System.WinForms.Form parentForm )
public ViewForm(IDocument doc, System.WinForms.Form parentForm )
The nice thing about Interfaces, they act like base classes in that you can use then to define method parameters and variables. Any class implementing that interface can be used or assigned to that variable or parameter. This allows code to be written in a slightly more generic way, like the constructors and the list iterations earlier.
Again, keep in mind that this is more a "proof-of-concept" that a full blown application, there are several things which should also be addressed which I will not be handling in the scope of this article. Additional bookkeeping like handling the Frame Close event and disposing the data set if all views on it are closed separately are left as an exercise to the reader.
Comfortable with things so far? Easy right? So what's the big deal? Let's screw things up a bit., suppose the requirements of the application require an additional view, a different presentation of the same data set, something like a tree view. An example might be a painting package that uses layers. The
FormViews discussed earlier might represent the graphical representation of the piece of work, displaying the content of each separate layer or the total composite image. The tree view might then present each layer as a node with the graphical elements within each layer presented as the leaves of the node. What, then, are the typical characteristics of a tree view?
- It generally does not get disposed when the document closes.
- There is rarely, if ever, multiples of the same type of tree views for different projects, the same tree view will be reset with the new data to be displayed.
- Tear-off type docking (beyond the scope of this article)
This doesn't enumerate all the typical characteristics of a tree view, just enough to see that this is quite a different kind of view on the world than what we've done so far, but we still want it to use the
IView interface because it needs to know what the data set is and when the data set has changed and the view needs to be updated.
The tree view implementation will be pretty much like the
ViewForm implementation, implementing the interface methods and containing the constructor modification., the problems begin in the data set. The data sets
UpdateAllViews method uses the
IView as the base which implements the interfaces expected method to trigger an update by the view, this isn't a problem, in fact, this is a nice feature of using the Intrerface method. The problem lies in the
CloseAllViews method. This method makes two erroneous assumptions, first that all views attached to the data set will be of a
WinForm.Form base (unlikely but possible), and the second that all views attached to the data set want to be closed when the data set closes. This leaves us basically two options, we can either add an interface method to close the view so that all views that implement the
IView interface have this capability available to act on and override as needed. The second option is to specialize the data sets method to screen and alter behavior depending on the view. Because I wanted to see how to check on the runtime class of an object, this is the approach I took, the proper approach would be to add the new interface method.
Fortunately checking and comparing the runtime class of an object is easily done in C#. Each object contains a method called
GetType and C# provides an operation called
typeof which will create a new
Type instance based on the class type passed in. This makes the class type comparison easy.
if ( view.GetType() != typeof(TreeForm) )
Adding this comparison to the
CloseAllViews method, as well as changing the iteration to use the
IView interface class and then cast the view to the
WinForm.Form (still an erroneous assumption) to close and dispose of the view will complete the modifications and give us the behavior we are looking for. See the 'Interface' project for a full source sample of this "proof-of-concept" project.
It gets the job done, but is there not a better way?
A Better Mouse Trap
In addition to the Interface concept, C# also provides Delegates and Events which look promising. Perhaps they can be used to provide similar behavior, let's have a look.
First, let's look back and figure out what it is we want to accomplish. Taking a look at the Data Set, what methods are on it, and what is the task or objective for it?
|AddView ||Add a view to the DataSet's managed view list. ||This is necessary so the data set can communicate to the appropriate views when needed in the future. ||
|CloseView ||Remove a view from the DataSet's managed View list. ||This is necessary as a bookkeeping item on the data set, so that the managed list contains only valid views.|
|CloseAllViews ||Remove and Close all views associated to this DataSet. ||This is needed as a bookkeeping item on the data set, so that when the document is closed all views depending on this document can clean up appropriately.|
|UpdateAllViews ||Trigger the views to redraw their data ||Indicate to all views in the DataSet's managed view list, that they should update their display of information.|
There are three things that stand out to me, the need of a list of views that need to be managed, a method to register a view, and a method to remove a single view. What if there were better ways? After all, why should the data set have to worry about what views are looking at its information? Why should the data set care that a view is closing?
What is really necessary, from the data set's point of view, to communicate to the rest of the system? In a nutshell and for our purposes, the data set needs to communicate that it has been changed and that it is about to be destroyed. What if the data set could define an event for each of these two situations and let whomever 'listen' for the event?
C# has two provisions which are perfect for this, Delegates and Events. Delegates are effectively method prototypes, it provides the fingerprint of the method, but does not provide any functionality. Events are a means of an object to say "Something significant happened, for anyone who cares." Instead of pushing messages into the system message queue, the C# method works off of a "publish and subscribe" philosophy. For those who don't know what this means it's really quite simple. One object basically publishes things it wants to talk about, other objects subscribe to the things the first object publishes. Radio, television, and newsgroups are all prime examples of this "publish and subscribe" philosophy, they are constantly "publishing" or broadcasting their information for everyone to see, but only those who "subscribe" or tune-in to the broadcast actually recieve the information.
Hopefully by now you've caught on to where I'm heading. What if, instead of the data set having to manage the views who "register", the data set simply defines a Delegate/Event combination to inform anyone who cares when it is closing and when it has been updated. The code would look something like:
public delegate void Closing ( Object obj, EventArgs e );
public delegate void Update ( Object obj, EventArgs e );
public event Closing onCloseData;
public event Update onUpdateData;
The data set now need only implement two methods to trigger the two defined events, which is very straight forward and might look something like:
public void CloseData ()
if ( onCloseData != null)
onCloseData (this, null);
public void UpdateData ()
if ( onUpdateData != null )
onUpdateData (this, null);
It is now the responsibility of the view class to implement a method that has the same fingerprint as the delegate defined in the data set, for our example we will define them as follows:
public void UpdateData (object obj, System.EventArgs e )
System.Console.WriteLine ("Update the view: " + this.Text );
public void CloseView ( Object obj, System.EventArgs e )
System.Console.WriteLine ("Close the view: " + this.Text );
With the delegates defined, we need to "subscribe" or "listen" to the data sets messages. The class constructor is the perfect place for this and might look something like
doc.onCloseData += new DataSet.Closing (this.CloseView);
doc.onUpdateData += new DataSet.Update (this.UpdateData);
Looking at the
TreeForm class, which forced us to make new considerations and change code in the Interface example, the steps to implement the Delegate/Event scheme are identical to the
ViewForm class. So now the
TreeForm and the
ViewForm class instances behave how we want them to and they both respond to the data set when it changes and when it is destroyed.
This approach, to me, seems much cleaner. Hardly any of the bookkeeping headaches the Interface scheme seems to need, no need to be concerned about what view classes are looking at and representing it's data, for that matter it doesn't even care if it is a view class. Absolutely any class, who cares enough, can listen in on what the data set has to say. This is a powerful concept.
Are these the only ways to handle multiple views on a common data set? Probably not. You know what your data set looks like, you know the approaches that are familiar to the people on your team, you have to decide what approach makes the most sense to you and your situation. One of the benefits of the Interface approach, with some level of effort and study, it would be entirely possible to implement the features set as provided in MFC, something familiar. Is that wise? Only you can decide that. Having seen the power and flexibility that the Delegate/Event scheme offers, it is my opinion that the effort spent defining and understanding what communication is needed will reap dividends in code readability and usability.