Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Handling Multiple Views on a Common Data Set

0.00/5 (No votes)
20 May 2001 1  
This atricle looks at some problems and solutions for using multiple views on a single data set in the C# and .NET framework.

Abstract

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.

Introduction

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:

// Register the application's document templates.  Document templates

//  serve as the connection between documents, frame windows and views.


CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
    IDR_MYDOCTYPE,
    RUNTIME_CLASS(CMyDoc),
    RUNTIME_CLASS(CMDIFrame),
    RUNTIME_CLASS(CMyView));

AddDocTemplate(pDocTemplate);

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 IDocument interface.

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.

// the data set's managed view list.  This list is needed so that the

// data set knows who to communicate stuff to.

private System.Collections.ArrayList viewList;

The AddView and CloseView methods are very straight forward, they simply insert or remove a view class from the managed view list on the DataSet like so.

// method needed to add a view to the managed view list

public void AddView (IView view)
{
    // If the view is now already being managed by the data set

    if ( !this.viewList.Contains (view) )
    {
        // then add this view to the list of all views managed by the data set

        this.viewList.Add (view);
    }
}

 // method to remove a view from the data set managed views list

 public void CloseView (IView view)
 {
    this.viewList.Remove (view);
 }

The CloseAllViews and UpdateAllViews are also fairly straightforward. They must cycle thru all the views attached to the data set.

// method to remove and close all views attached to this data set

public void CloseAllViews ()
{
    // cycle thru each view

    foreach (ViewForm view in this.viewList )
    {
        // otherwise, close the view

        view.Close ();
        // and dispose of it

        view.Dispose ();
    }

    this.viewList.Clear ();
}



// method to trigger an update on all views attached to the managed view list

public void UpdateAllViews ()
{
    // cycle thru each view

    foreach (IView view in this.viewList )
    {
        // trigger the update data method, which all views implementing the

        // IView interface will define.

        view.UpdateData ();
    }
}

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.

The ViewForm class implements the IView interface. The methods, again, are fairly straightforward and look something like:

// method to attach the document reference to this view

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." );
            return;
    }

    this.m_DataSet = doc;
}

// method to return the data set associated to this view.

public IDocument GetDocument ( )
{
    return this.m_DataSet;
}

// this method is what gets called by the data set's managed view list when the data set has changed

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 DataSet and 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.

// constructor for the DataSet class

public DataSet( System.WinForms.Form parentForm )

// constructor for the ViewForm class

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?

  1. It generally does not get disposed when the document closes.
  2. 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.
  3. 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:

// define two delegate for the close and update events

public delegate void Closing ( Object obj, EventArgs e );
public delegate void Update  ( Object obj, EventArgs e );

// define the close and update data events.

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:

// method to trigger the data set destruction and 

// notify those who care

public void CloseData ()
{
    if ( onCloseData != null)
    {
        // instead of passing null into the second

        // parameter, add information which the "listeners"

        // might find useful

        onCloseData (this, null);
    }
}

// method to trigger the update data event on the data set.

// This method will inform all those who care that data has

// changed.

public void UpdateData ()
{
    if ( onUpdateData != null )
    {
        // instead of passing null into the second

        // parameter, add information which the "listeners"

        // might find useful

        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:

// method used to act as the data set's update data event

// listener this method it what get's called when the data

// set triggers an Update event.

public void UpdateData (object obj, System.EventArgs e )
{
    System.Console.WriteLine ("Update the view: " + this.Text );
}

// method used to act as teh data set's close data event

// listener this method is what get's calles when the

// dataset triggers a Closing event.

public void CloseView ( Object obj, System.EventArgs e )
{
    System.Console.WriteLine ("Close the view: " + this.Text );
    this.Close ();
    this.Dispose ();
}

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


// set the event listeners

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.

Conclusion

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here