Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

MVC in the Visual Component Framework

3.88/5 (20 votes)
17 Jun 2008BSD15 min read 1   388  
An introduction to the Model-View-Controller Pattern in the Visual Component Framework.

Image 1

Introduction

This will be the first in a four part series of articles that will explain how to use the built-in Model-View-Controller features of the Visual Component Framework, and its DocumentManager classes that provide a high level set of functionality similar to MFC's Doc/View classes. During the course of the articles, we'll discuss the basic design of MVC in the framework, how to make a simple MVC app with a Model, and a very simple Controller. I'll discuss how to build your UI using the VCF's Visual Form File format and how to hook up event handlers (or callbacks) in the C++ code to the UI components defined in the layout file. Then, we'll walk through making a complete MVC based app, that starts with drawing simple shapes, and ends with a simple drawing app that can draw shapes, allows you to open or save with different file formats, supports cut/copy/paste, undo and redo, drag and drop for both files and shape objects that you select, and that can be dropped on external applications, shell integration, and finally has support in the UI for updating the state of various UI elements like menu items and toolbar buttons.

Intro to Model-View-Controller

Model-View-Controller (MVC) is a pattern that's been discussed all over the place. It originated with the Smalltalk group at Xerox PARC, run by Alan Kay, in the early 1970's. It's basically a design pattern whose goal is to cleanly separate the code that deals with the application's data (or Model) from its user interface (or View) and the code that responds to the user's actions. There are a variety of implementations out there, some better than others, some easier to use, some more difficult. Java's Swing framework is one example of MVC (though some might argue that it's a bit overdone). MFC also has a primitive sort of MVC though it's possible to argue that it's not very well done.

Typically, the Model is designed to hold only data. You interact with the model to change its data. The Model usually has one or more Views, with each View being responsible for displaying some or all of the Model's data. If the Model is changed, then there should be some mechanism by which Views are notified so they can update themselves. The Controller acts as a kind of "referee" between the Model and the View(s). For example, if the user were to click on the View, then the Controller would decide how to interpret the action (the mouse click) and make any changes. Perhaps, if the program were a drawing program, the click might result in adding a new shape. If this were the case, then the Controller would need to tell the Model to add a new shape. This would result in a modification to the Model, which would in turn notify its View(s) and the UI would be updated and redrawn.

Given a general overview, let's take a look at how the VCF implements the specifics of this. The VCF has two primary classes that define the Model and the View. In addition, there is the Control class that implements the View interface as well as allowing you to set a custom View. All of the various UI control classes, such as the TextControl, ListControl, TreeControl, and so on, use Models to store their data, so MVC is not just a throwaway in the framework, it's built-in and heavily used.

MVC classes in the VCF

Image 2

Model Basics

The Model class is an abstract class that other more specific Model classes derive from. It provides the basic implementation for managing and connecting Views to the Model. It also provides a general purpose notification system for anyone interested in changes to the Model by using a specific delegate variable named ModelChanged. The Model's Views are updated by the updateAllViews() function that iterates through all the registered Views of a Model and calls the updateView() function for each View.

ModelChanged delegate

Image 3

Data Access

The Model class itself does not store or define how data is stored. It does provide some basic methods to setting and/or retrieving data, as well as determining if the Model has any data at all, and a method to clear it, or empty it, of all data.

You can access data generically using the Model's getValue() or getValueAsString() methods. These take an optional key that's a variant that can be used to help retrieve the specific value. For example, if you had a Model that represented a bunch of text, then your implementation of getValue might just ignore the key parameter and simply return all the text. If your Model was a list of some sort, then the key might be interpreted as an index and you'd return the value at the specified index in the list. These are not meant to be the only way to provide access to the Model's data, but rather an optional way that you provide in the case where it's not possible to call a more specific method of a Model derived class.

Likewise, it's possible to set data generically through the Model's setValue() method. Like the getValue() method, this takes a variant to store the data and an optional variant to specify the key. Again, this is not considered as the only way to set data on a Model, but rather an optional method.

Data Storage and Variants

If you look at the Model class methods getValue() and setValue(), and if you look at some of the other Model implementations like ListModel or TreeModel, you'll see that the VariantData class is used to represent a piece of data. This makes it easy to pass around a variety of different data types transparently, but it does not mean that you have to implement the Model storage using a VariantData. For example, if you had a collection of floating point values, you might decide to implement the ListModel and store the data in an array of doubles using std::vector<double>.

Views

While the Model has a number of methods associated with it, the View class is relatively simple. Its main function is to render or draw the Model. This is done in the View's paintView() method which gets called by the framework. A GraphicsContext is passed in, and you can draw whatever you need to.

The View also has a pointer to the Model it's associated with and a pointer to a Control instance that it's associated with. Beyond that, it's up to the application specific design of a particular View to store anything else.

Controls and Views

A Control derives from the View class as well as contains a pointer to a View. This means that, by default, any Control can be added to a Model. By default, the Control's paintView() method is just going to call the code in the Control's paint() method. By allowing the Control the possibility of an external View, you can customize the display of a Control without using inheritance. The default painting behavior for a Control is to check to see if it's View instance is non NULL; if it is, then it calls the View instance's paintView() method, otherwise it does its own internal painting.

Where's the Controller?

So far, I haven't mentioned a specific class for the Controller. That's because there isn't one! The Controller tends to be extremely application specific, and there's not enough common elements to justify a class. Some possible options are to use the existing Application class and put your Controller logic there, or create a brand new class that becomes your Controller. The important thing is what the class does and how it interacts with the Model and View that qualify it as a Controller.

Before you get Started

Before we get too far into this, please make sure that you've installed the latest version of the VCF (version 0-9-8 or better). And, you may want to have a glance at some of the articles here on CodeProject to get a feel for how things work. Some useful articles:

A Simple Example

For our first step, let's create a simple application that displays the information in the ubiquitous Person class.

Image 4

First, let's create our Model. We'll call it a Person and define like so:

class Person : public Model {
public:
    virtual bool isEmpty() {
        return attributes.empty();
    }
protected:
    Dictionary attributes;
};

This is obviously pretty bare. Instead of having separate fields for each member variable, like a name, address, etc., we are going to be lazy and use the framework's Dictionary class, which is not much more than a wrapper around std::map<string,variantdata>, with a few utility methods thrown in for good measure. This lets us write something like:

attributes["Age"] = 38;
attributes["Name"] = "Bob";

When you implement your Model class, you need to implement the isEmpty() method; this allows the framework to tell if your Model has any data in it. In our case, we just return whether or not the attributes member variable is empty().

Now, let's add support for getting or setting data by implementing the generic getValue() and setValue() methods:

class Person : public Model {
public:
    virtual bool isEmpty() {
        return attributes.empty();
    }
    
    virtual VariantData getValue( const VariantData& key=VariantData::null() )     {
        String strKey = key;
        return attributes[ strKey ];
    }

    virtual void setValue( const VariantData& value, 
            const VariantData& key=VariantData::null() )  {
        String strKey = key;
        attributes[ strKey ] = value;
        ModelEvent e( this, Model::MODEL_CHANGED );
        changed( &e );
    }
protected:
    Dictionary attributes;
};

Note our setValue() implementation. We declare a ModelEvent, pass in the event's source (the Model), set its event type to Model::MODEL_CHANGED, and then call the Model's changed() method. This does two things for us: it makes sure to invoke the ModelChanged delegate with the event instance, and it calls the Model's updateAllViews() method. It's a convenience method, so don't forget to do this yourself.

If we wanted to get really fancy, we could add a specific delegate to our class for greater granularity; for example, we could have a NameChanged delegate, but for now, we're just going to keep things simple.

Obviously, we don't want to have to remember what the various attribute keys are, so let's add some methods to make it easy to get and set the various attributes of the Person class.

class Person : public Model {
public:
    virtual bool isEmpty() {
        return attributes.empty();
    }

    virtual VariantData getValue( const VariantData& key=VariantData::null() )     {
        String strKey = key;
        return attributes[ strKey ];
    }

    virtual void setValue( const VariantData& value, 
            const VariantData& key=VariantData::null() )  {
        String strKey = key;
        attributes[ strKey ] = value;
        ModelEvent e( this, Model::MODEL_CHANGED );
        changed( &e );
    }

    uint32 getAge() const {
        return attributes["Age"];
    }

    String getFirstName() const {
        return attributes["FirstName"];
    }

    String getLastName() const {
        return attributes["LastName"];
    }

    String getZIPCode() const {
        return attributes["ZIPCode"];
    }

    String getAddress() const {
        return attributes["Address"];
    }

    String getState() const {
        return attributes["State"];
    }

    String getCountry() const {
        return attributes["Country"];
    }

    String getPhoneNumber() const {
        return attributes["PhoneNumber"];
    }

    void setAge( const uint32& val ) {
        attributes["Age"] = val;
        ModelEvent e( this, Model::MODEL_CHANGED );
        changed( &e );
    }

    void setFirstName( const String& val ) {
        attributes["FirstName"] = val;
        ModelEvent e( this, Model::MODEL_CHANGED );
        changed( &e );
    }

    void setLastName( const String& val ) {
        attributes["LastName"] = val;
        ModelEvent e( this, Model::MODEL_CHANGED );
        changed( &e );
    }

    void setZIPCode( const String& val ) {
        attributes["ZIPCode"] = val;
        ModelEvent e( this, Model::MODEL_CHANGED );
        changed( &e );
    }

    void setAddress( const String& val ) {
        attributes["Address"] = val;
        ModelEvent e( this, Model::MODEL_CHANGED );
        changed( &e );
    }

    void setState( const String& val ) {
        attributes["State"] = val;
        ModelEvent e( this, Model::MODEL_CHANGED );
        changed( &e );
    }

    void setCountry( const String& val ) {
        attributes["Country"] = val;
        ModelEvent e( this, Model::MODEL_CHANGED );
        changed( &e );
    }

    void setPhoneNumber( const String& val ) {
        attributes["PhoneNumber"] = val;
        ModelEvent e( this, Model::MODEL_CHANGED );
        changed( &e );
    }

protected:
    Dictionary attributes;
};

So, we can now easily get and set values on the Person class. Note that there's no real reason to use the Dictionary object to store the values other than laziness. It makes for smaller code when implementing the generic getValue() and setValue() methods and isEmpty(), but it's not as efficient as using the more traditional individual member variables for each attribute.

At this point, we have a complete, if simplistic, Model implementation.

Now that we've defined a Model, let's set up a user interface. We're going to build the UIs for all these examples using the VCF's Visual File Format (VFF), which is basically an extension of the format that Borland's Delphi has used for years. It's easy to parse, it's not XML, and, possibly more importantly, it's extremely easy to read and edit by hand. For a more complete definition of it, see these links:

Basically, the VCF loads up a VFF file for each UI class that it attempts to create, using its resource loading logic to load the data in memory. Resources can be stored in a more traditional resource script (.RC) and compiled into the final executable, or they can exist as external files. For the purposes of these articles, to make things easy, we'll use resources as external files. The actual file name must be the same as that of the class with the ".vff" extension on the end. Our window class is named "ModelDataWindow", so our file will be stored as "ModelDataWindow.vff", and it will be in the Resources/ directory, at the same level where the executable is.

We'll start with the following to define our main window:

object ModelDataWindow  : VCF::Window
    top = 200
    left = 200
    height = 110pt
    width = 240pt
    caption = 'ModelData Window'    

    minHeight = 110pt
    maxHeight = 110pt    
endd

A few points really quickly:

  • As you can see, you define an object with a name (this becomes the name of the component), specify its C++ class, and then write out the properties that you want to define.
  • The VFF allows you to specify coordinate values in pixels (the default), points, inches, or centimeters. Points, inches, and centimeters all use the screen's current DPI to determine the actual pixel value.
  • Note the use of minHeight/maxHeight - by setting these values, we can constrain the size of the main window. Since we've set them to the same value, we will not be able to change the height of the window. We will be able to change the resize width.

We're going to have a simple UI that displays some values, in a tabular interface. To make this easy, we're going to use a special container class that will do all the work of positioning the controls.

object ModelDataWindow  : VCF::Window
    //rest commented out
    object hlContainer : VCF::HorizontalLayoutContainer
        numberOfColumns = 2
        maxRowHeight = 35
        rowSpacerHeight = 10
        widths[0] = 80
        widths[1] = 80

        tweenWidths[0] = 10
    end

    object hlContainer2 : VCF::HorizontalLayoutContainer
        numberOfColumns = 2
        maxRowHeight = 35
        rowSpacerHeight = 10
        widths[0] = 80
        widths[1] = 80

        tweenWidths[0] = 10
    end
end

This defines two containers that we can use and reference later on. Most of the values just specify the various dimensions of the container.

We'll split the UI in two: the left side will be editable, and the right side will just show the current values, and will be updated whenever changes are made to the Model.

object ModelDataWindow  : VCF::Window
    //rest commented out
    object hlContainer : VCF::HorizontalLayoutContainer
        //rest commented out
    end

    object hlContainer2 : VCF::HorizontalLayoutContainer
        //rest commented out
    end
    
    object pnl1 : VCF::Panel 
        border = null
        alignment = AlignLeft
        width = 120pt
        container = @hlContainer        
    end
    

    object pnl1 : VCF::Splitter 
        alignment = AlignLeft
    end

    object pnl2 : VCF::Panel 
        container = @hlContainer2
        border = null
        alignment = AlignClient
    end    
end

Now, we have the basics of our UI laid out. Let's add the rest of the controls, specifically the labels, text control, and the button that we'll use. The labels will be for names of the Person attributes that we will display and some of the read-only attribute values. The text control will hold the LastName attribute which can be edited. The button will be used to increment the person's age by 1 year each time it's clicked.

object ModelDataWindow  : VCF::Window
    //commented out
    object pnl1 : VCF::Panel 
        border = null
        alignment = AlignLeft
        width = 120pt
        container = @hlContainer

        object lbl1 : VCF::Label
            caption = 'First Name'        
        end

        object edt1 : VCF::Label
            
        end


        object lbl2 : VCF::Label
            caption = 'Last Name'        
        end

        object edt2 : VCF::TextControl
            
        end

        object lbl3 : VCF::Label
            caption = 'Age'        
        end

        object edt3 : VCF::TextControl
            readonly = true
            enabled = false            
        end



        object lbl4 : VCF::Label
            caption = 'Modify Age'        
        end

        object btn1 : VCF::CommandButton
            caption = 'Click Me'

            delegates
                ButtonClicked = [ModelDataApp@ModelDataApp::clickMe]
            end
        end
    end
    

    object pnl1 : VCF::Splitter 
        alignment = AlignLeft
    end

    object pnl2 : VCF::Panel 
        container = @hlContainer2
        border = null
        alignment = AlignClient

        object lbl1a : VCF::Label
            caption = 'First Name'        
        end

        object valLbl1 : VCF::Label
            
        end


        object lbl2a : VCF::Label
            caption = 'Last Name'        
        end

        object valLbl2 : VCF::Label
            
        end

        object lbl3a : VCF::Label
            caption = 'Age'        
        end

        object valLbl3 : VCF::Label
            
        end
    end    
end

Now that we have the basic UI defined, let's put some code in place to load it up:

class ModelDataWindow : public Window {
public:
    ModelDataWindow() {}
    virtual ~ModelDataWindow(){};
};

_class_rtti_(ModelDataWindow, "VCF::Window", "ModelDataWindow")
_class_rtti_end_


class ModelDataApp : public Application {
public:

    ModelDataApp( int argc, char** argv ) : Application(argc, argv) {
        addCallback( new ClassProcedure1<Event*,ModelDataApp>(this, 
                                          &ModelDataApp::clickMe), 
                      "ModelDataApp::clickMe" );
    }

    virtual bool initRunningApplication(){
        bool result = Application::initRunningApplication();
        REGISTER_CLASSINFO_EXTERNAL(ModelDataWindow);

        Window* mainWindow = 
              Frame::createWindow( classid(ModelDataWindow) );
        setMainWindow(mainWindow);
        mainWindow->show();
        
        return result;
    }

    void clickMe( Event* ) {
        //omitted
    }    
};

The first thing to note is that we need to add support for the VCF's advanced Reflection/RTTI features, and that's where the _class_rtti_ and REGISTER_CLASSINFO_EXTERNAL macros come into play. You need to define this so that the framework can load up your window class dynamically. This happens when the Frame::createWindow static function is called - it loads up a window, based on its RTTI class instance, and automatically loads up the corresponding VFF resource for you.

The next thing is that we add a callback to our application, in its constructor. That way, it's ready to be used, and when we load our window's VFF, we can reference it as a callback to use to add to the button's ButtonClicked delegate.

When we run it, we get something like this:

Image 5

Obviously, not very useful yet, as there's no data! Our plan here is to create the model, and set its initial properties, in the ModelDataWindow VFF file. You don't have to do things, but for the purpose of this first step, it's an easy way to work with it. In order to be able to do this, we need to add RTTI support, just like we did with the main window, only we will also be adding support for the various properties that we want to expose. To do that, we define something like this:

_class_rtti_(Person, "VCF::Model", "Person")
_property_( uint32, "age", getAge, setAge, "" );
_property_( String, "firstName", getFirstName, setFirstName, "" );
_property_( String, "lastName", getLastName, setLastName, "" );
_property_( String, "zipCode", getZIPCode, setZIPCode, "" );
_property_( String, "address", getAddress, setAddress, "" );
_property_( String, "state", getState, setState, "" );
_property_( String, "country", getCountry, setCountry, "" );
_property_( String, "phoneNumber", getPhoneNumber, setPhoneNumber, "" );
_class_rtti_end_

This means that we can reference the age (or any of the other properties that we have declared here) of the person in the VFF by using the property name "age". We register this in the application by adding the code:

virtual bool initRunningApplication(){
    bool result = Application::initRunningApplication();
    REGISTER_CLASSINFO_EXTERNAL(ModelDataWindow);
    REGISTER_CLASSINFO_EXTERNAL(Person);
    //rest omitted
}

Now, we can make some changes to our UI and do the following:

  • We'll create a new Person object. This will be our Model.
  • We'll set the first name, last name, and age of our Person instance.
  • We'll set the model property of some of our controls, and we'll set the control's model key so that it knows how to retrieve data from the Model using the Model::getValue() function.

First, let's create the Person object:

object ModelDataWindow  : VCF::Window
    //rest omitted
    object joeBobSnake :  Person
        firstName = 'Joe Bob'
        lastName = 'Snake'
        age = 24
    end
    //rest omitted
end

We've set its attributes, using the firstName, lastName, and age properties.

Next, let's modify our controls:

object ModelDataWindow  : VCF::Window
    //rest omitted
    object joeBobSnake :  Person
        firstName = 'Joe Bob'
        lastName = 'Snake'
        age = 24
    end

    object pnl1 : VCF::Panel 
        //rest omitted

        object edt1 : VCF::Label
            model = @joeBobSnake
            modelKey = 'FirstName'
        end

        //rest omitted
        
        object edt2 : VCF::TextControl
            model = @joeBobSnake
            modelKey = 'LastName'
        end

        //rest omitted
        
        object edt3 : VCF::TextControl
            readonly = true
            enabled = false
            model = @joeBobSnake
            modelKey = 'Age'
        end
    end    
    //rest omitted
end

Note the two properties that we set "model" and "modelKey". The model property is pretty self-explanatory, we're setting the Model of the specific control. Remember that a control instance is a View, so this calls the setViewModel which, in turn, causes the View to be added to the Model. The use of the "@joeBobSnake" means that we're referring to the component instance named "joeBobSnake". The "modelKey" property sets the key used when the call to Model::getValue() is made by the control. This is used by some controls when they need to display some data, for example, by the label control when it needs to display its caption. Note that the keys being assigned have the same name that we used for accessing our attributes in our Person class.

The first text control (edt2) will allow us to modify the last name of the Person instance. When the control determines that its text is being changed, it tries to modify the Model, using the Model key that's been assigned to it. In our case, whatever text we type in will be be assigned to the Person Model using the Model::setValue with a key of "LastName".

Now, let's add some code to modify the person's age, and we'll be done!

class ModelDataApp : public Application {
public:
    //rest omitted

    void clickMe( Event* ) {
        Person* person = (Person*)findComponent( "joeBobSnake", true );

        person->setAge( person->getAge() + 1 );
    }    
};

Note that we have already defined a method for this earlier. This just retrieves the component, and increments its age attribute. What we end up with is something like this:

Image 6

Notes on Building the Examples

You'll need to have the most recent version of the VCF installed (at least 0-9-8 or better). The current example has an exe that was built with Visual C++ 6. If you'd like to build with a different version of Visual C++, then you'll need to build them manually, and make sure to build the static framework libraries, as opposed to dynamic.

If you're unsure how to build the framework proper, please see this: Building the VCF, it explains the basics of build the framework using various IDEs and tool-chains.

Conclusion

At this point, we've gone through the basics of creating a Model class, creating a user interface, and linking the two together. Our "Controller", if you will, is both the text control, and the application class. The text control, which is also a View, takes care of converting key input and updating the Model. This, in turn, updates any other Views attached to the Model. The other Controller is the application which gets notified when a UI element is clicked on, and then modifies the Model by incrementing the age. Obviously, this is a somewhat simplistic implementation, so our next step will be to enhance this functionality.

Questions about the framework are welcome, and you can post them either here, or in the forums. If you have suggestions on how to make any of this better, I'd love to hear them!

License

This article, along with any associated source code and files, is licensed under The BSD License