- Why?
Several problems can arise when applications contain a mixture of data access code, business logic code, and presentation code. Such applications are hard to maintain, because interdependencies between all of the components cause strong ripple effects whenever a change is made anywhere. High level of coupling makes classes difficult or impossible to reuse because they depend on so many other classes. Adding new data views often requires reimplementing or cutting and pasting business logic code, which then requires maintenance in multiple places. Data access code has the same problem, being cut and pasted among business logic methods.
The Model-View-Controller design pattern solves these problems by decoupling data access, business logic, and data presentation and user interaction.
- Model
In MVC the model is the code that performs a certain task. It is built with no necessary concern for how it will "look and feel" when presented to the user. It has a purely functional interface, meaning that it has a set of public functions that can be used to achieve all of its functionality. Some of the functions are "query" methods that permit a UI to get information about the current state of the model. Others methods permit to modify the state. In our case the model is a set of business C++ objects with public get-/set- methods and a notification system.
- View
The view (Grid) renders the contents of a model. It accesses data (C++ objects) of the model and specifies how that data should be presented. It is the view's responsibility to maintain consistency in its presentation when the model changes.
- Controller
In a classic MVC pattern, the view does not directly change the model's state (as in, field values managed by the model). Instead, it fires events to the controller. The controller contains the logic to decide how the model state changes and informs the model of the appropriate state change with a direct function call. The model, upon processing the state change, fires an event which is received by the view(s). The view adjusts the state of the UI to reflect the state change of the model.
Our library provides a thread-safe binding to the data model (C++ objects). Therefore, views have a direct communication with the business model. If the view changes (edit in place or other interactions), the field's value in the model is automatically updated, and if the field's value in the model changes, the view (grid) is automatically updated. The controller never intervenes. However, in our model the controller is intended to provide custom formatting/parsing values of the model and strings, displayed in grids rather than state management.
- How does it implemented?
1. For example let's describe a simple data model by the following C++ class:
class CShare
{
public:
CString GetName() const;
double GetPrice() const;
private:
CString m_Name;
double m_Price;
};
2. Now, we will describe mapping that implements data binding mechanism and permits to call Get- & Set- methods of C++ object by identifiers.
Note, that in declarations we use values like 1, 2, etc. These values are identifiers of fields. They should be used in columns of the grid. Below we will give a better solution for these identifiers. The third parameter is a pointer to the Get- function that will be called when the grid needs for the data.
3. Create columns with the declared above identifiers and add to the header
class CViewDemo : public CView
{
...
protected:
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
...
private:
Dapfor::GUI::CGrid m_Grid;
};
int CViewDemo::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
m_Grid.Create(0, 0, WS_VISIBLE|WS_CHILD, CRect(), this, 1000);
Dapfor::GUI::CHeader* header = new Dapfor::GUI::CHeader(true);
header->Add(new Dapfor::GUI::CColumn(1, _T("Name"), 70));
header->Add(new Dapfor::GUI::CColumn(2, _T("Price"), 60));
m_Grid.SetHeader(header);
...
}
4. Add some C++ data objects to the grid.
CShare* share = new CShare(...)
m_Grid.Add(share);
grid2.Add(share);
grid3.Add(share);
...
Now, each grid will handle the share's position according to the sorting, hierarchical and filtering rules.
5. Now, let's explain how to fire events to the grids. Our share has a field m_Price. Let's add a Set- function that changes this field. The share, being derived from the Dapfor::Common::CDataObject has an embedded thread-safe notification mechanism. To send a notification you can just call the NotifyUpdate() method with the field identifier.
class CShare : public Dapfor::Common::CDataObject
{
...
void SetPrice(double price);
};
...
void CShare::SetPrice(double price)
{
m_Price = price;
NotifyUpdate(2);
}
When the grid is notified, it automatically updates the text in cell, changes row position according to the sorting and filtering rules, updates the timestamp of the last update and highlights the corresponding cell. Each grid does all these operations automatically in a thread-safe way without the deadlock risk.
6. Edit in place? It is very easy. We have almost everything we need. We should only add a SetPrice() method to the mapping and switch on edit in place in the column.
As you see, this powerful methodology separates the core business model functionality from the presentation and grealy simplifies the application.
7. Optimizations: Numeric values are not the best way to determine field identifiers. Instead, we adwise you to use enumerations. This enables you to detect and fix potential errors at compile time! See the following example:
class CShare : public Dapfor::Common::CDataObject
{
public:
enum Fields
{
FidName,
FidPrice,
};
...
};
DF_BEGIN_FIELD_MAP(CShare)
DF_MFC_STRING_ID(FidName, _T("Name"), &CShare::GetName, 0, 0)
DF_DOUBLE_ID (FidPrice, _T("Price"), &CShare::GetPrice, 0, 0)
DF_END_FIELD_MAP()
...
void CShare::SetPrice(double price)
{
m_Price = price;
NotifyUpdate(FidPrice);
}
int CViewDemo::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
...
header->Add(new Dapfor::GUI::CColumn(CShare::FidName, _T("Name"), 70));
header->Add(new Dapfor::GUI::CColumn(CShare::FidPrice, _T("Price"), 60, Dapfor::GUI::CColumn::Auto, true));
}
- Conclusion:
The MVC pattern is very powerful. The examples we provided here are somewhat trivial. In a complicated UI, where an application may have multiple grids, changes in one grid affects the state of another grid, and this technique becomes very useful.