MFC Grid manual

Model-View-Controller (MVC) patterns and MFC grid.

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.

//file Share.h

class CShare : public Dapfor::Common::CDataObject
{
    ...
    DF_DECLARE_FIELD_MAP();
};



//file Share.cpp

DF_BEGIN_FIELD_MAP(CShare)
    DF_MFC_STRING_ID(1, _T("Name"),  &CShare::GetName,  0, 0)
    DF_DOUBLE_ID    (2, _T("Price"), &CShare::GetPrice, 0, 0)
DF_END_FIELD_MAP()

CString CShare::GetName() const
{
    return m_Name;
}

double CShare::GetPrice() const
{
    return m_Price;
}
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

//file ViewDemo.h

class CViewDemo : public CView
{
...
protected:
    afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
...
private:
    Dapfor::GUI::CGrid m_Grid;
};


//file ViewDemo.cpp

int CViewDemo::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CView::OnCreate(lpCreateStruct) == -1)
        return -1;

    //Initialize the grid
    m_Grid.Create(0, 0, WS_VISIBLE|WS_CHILD, CRect(), this, 1000);

    //Create a header.
    Dapfor::GUI::CHeader* header = new Dapfor::GUI::CHeader(true);
    
    //Add columns with the identifiers, described above. 
    header->Add(new Dapfor::GUI::CColumn(1,   _T("Name"),  70));
    header->Add(new Dapfor::GUI::CColumn(2,   _T("Price"), 60));

    //Set the header
    m_Grid.SetHeader(header);
    
    ...
}

4. Add some C++ data objects to the grid.

    //Create our share object
    CShare* share = new CShare(...)

    //Add the object to the grid. 
    m_Grid.Add(share);

    //If there are other grids, we can add the same share in the same way...
    //The share in these grids may look in different ways(columns, formats, colors etc.)
    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.

//file Share.h

class CShare : public Dapfor::Common::CDataObject
{
    ...
    void SetPrice(double price);
};


//file Share.cpp

...
void CShare::SetPrice(double price)
{
    m_Price = price;
    
    //Fire event. 
    //Note that this event can be used not only by grids, but also by the model.
    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.

//file Share.cpp

DF_BEGIN_FIELD_MAP(CShare)
    ...
    DF_DOUBLE_ID    (2, _T("Price"), &CShare::GetPrice, &CShare::SetPrice, 0)
DF_END_FIELD_MAP()



//file ViewDemo.cpp

int CViewDemo::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
...
    //Switch on edit in place for the column 'Price'
    header->Add(new Dapfor::GUI::CColumn(2, _T("Price"), 60, Dapfor::GUI::CColumn::Auto, true));
}

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:

//file Share.h

class CShare : public Dapfor::Common::CDataObject
{
public:
    enum Fields
    {
        FidName, 
        FidPrice,
    };
    ...
};

//file Share.cpp

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;
    
    //Fire event. 
    //Note that this event can be used not only by grids, but also by the model.
    NotifyUpdate(FidPrice);
}


//file ViewDemo.cpp

int CViewDemo::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    ... 
    
    //Add columns with the identifiers, described above. 
    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.