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
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
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 double
s 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.
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* ) {
}
};
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:
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);
}
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:
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:
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!