Using Model-View-Controller with Sitemagic CMS
Sitemagic CMS is all about best practices to ensure a robust and flexible architecture, as well as code of high quality that is easy to maintain and extend. Object Oriented Programming (OOP) and Separation of Concerns (SoC) is in the heart of our framework, and naturally Model-View-Controller is well supported.
Before digging deeper into implementation details, let's first establish a common sense of what MVC is all about. If you do not care about the theory behind MVC, you may
skip to the implementation example.
Model-View-Controller (MVC)
MVC is separation of concerns - in this case separation between Model (data and business logic) and View (the user interface). MVC was invented as a way of separating responsibilities in Graphical User Interfaces (GUI) back in late 1970 at Xerox Parc. But it was not until 1988 that MVC was expressed as a general concept. So MVC has been around for a very long time.
MVC is one of the most quoted patterns of UI design, but at the same time one of the most misunderstood design patterns as well - almost no framework actually implements the pattern as it was intended.
Patterns of Enterprise Application Architecture by Martin Fowler explains the pattern quite well.
In MVC the Controller is responsible for handling events triggered in the View (e.g. a button click), receive changes to data, and update the Model appropriately. The View is solely responsible for loading data from the Model and display it. The Model contains data and business logic. When the Model is changed, it fires an event which allow listeners to respond to these changes - the View subscribe to this event through the Observer Pattern and updates itself when necessary.
The use of Observer Pattern was especially useful for Desktop Applications which allowed for data to be displayed in a variety of ways using multiple Views - e.g sorted or filtered, visualized differently using charts or tables, and so on. Changing data in one window would cause all Views to update to reflect the changes, much like we see in spreadsheets where all charts are updated simultaneously when data is changed. However, such capabilities are rarely required in Web Applications, and if they are, they are usually implemented using AJAX, since Web Applications are very different from Desktop Applications. With Web Applications most processing is done on the server, and the result is then served back to the browser where it basically remains static until a new request is sent to the server. Desktop Applications keeps running on the same computer, which greately simplifies the process of synchronizing data between Views. Therefore, many Web Applications implement MVC without this feature, or using a different approach such as AJAX.
Model-View-Presenter (MVP)
The Model-View-Presenter is a popular MVC derivate which is often confused with actual MVC. MVP comes in two variants; Passive View and Supervising Controller. The difference is mainly how the View gets its data - with Supervisning Controller the View itself is responsible for data binding, while the Passive View depends on the Presenter to synchronize data between the Model and View. The Passive View has the obvious benefit of having absolutely no coupling between the View and the Model - they are completely unaware of each other. This allows for greater testability and code reuse. Again quite often Web Applications implement both variants without the Observer part, or uses AJAX to ensure a current View.
Theory and practice
Most people don't know the difference between MVC, MVP Passive View, MVP Supervisning Controller, and other similar patterns. Therefore you will find that theory and practice does not always go hand in hand. Think of MVC as a concept for separating the View from the Model. How it is actually implemented usually differs from developer to developer, and most frameworks, including Sitemagic CMS, allows the developer to implement this separation in a variety of ways.
Below is an example of how Model-View-Controller (no, actually it's Model-View-Presenter with Passive View) could be implemented without the Observer Pattern.
Model - In this case our Model is simply an instance of
SMDataSource, but could also be a complete Domain Object Model.
View - Our View is an instance of
SMTemplate, allowing us to define our presentation in pure HTML and CSS - truely a Passive View.
Presenter - Our Presenter is responsible for binding GUI controls to the Passive View and synchronize data between the Model and View.
Business logic is related to the Model, and if such is required, the SMDataSource instance should be wrapped in a class along with the relevant business logic. In this case we have no business logic.
The View in this example is nothing but pure HTML. Placeholders define the location of data or GUI controls:
Passive View (Person.html)
<table>
<tr>
<td><b>Enter name</b></td>
<td>{[Name]}</td>
</tr>
<tr>
<td><b>Enter age</b></td>
<td>{[Age]}</td>
</tr>
<tr>
<td><b>Enter gender</b></td>
<td>{[Gender]}</td>
</tr>
<tr>
<td></td>
<td>{[SaveButton]}</td>
</tr>
</table>
The Presenter creates GUI controls, loads data from the Model into the GUI controls, and binds them to the View.
Presenter class (SMTestPersonPresenter)
class SMTestPersonPresenter
{
public function Render()
{
// Get Model
$ds = new SMDataSource("SMTestPersons");
// Create GUI Controls
$txtName = new SMInput("SMTestName", SMInputType::$Text);
$txtAge = new SMInput("SMTestAge", SMInputType::$Text);
$txtGender = new SMInput("SMTestGender", SMInputType::$Text);
$cmdSave = new SMLinkButton("SMTestSave");
$cmdSave->SetTitle("Save data");
// Check whether Save button was clicked - if so, save data to Model.
// In this example we work with just one entry.
if ($cmdSave->PerformedPostBack() === true)
{
$data = new SMKeyValueCollection();
$data["Name"] = $txtName->GetValue();
$data["Age"] = $txtAge->GetValue();
$data["Gender"] = $txtGender->GetValue();
if ($ds->Count() === 0) // Create new
{
$ds->Insert($data);
}
else // Update existing
{
// NOTICE: No update condition specified!
// Would update all entries if there were more than one!
// Usually this would look something like:
// $ds->Update($data, "id = '83772'");
$ds->Update($data);
}
}
// Load data from Model into GUI Controls
$persons = $ds->Select("Name, Age, Gender");
if (count($persons) === 1)
{
$txtName->SetValue($persons[0]["Name"]);
$txtAge->SetValue($persons[0]["Age"]);
$txtGender->SetValue($persons[0]["Gender"]);
}
// Insert GUI controls into Passive View
$tpl = new SMTemplate("extensions/SMTest/Person.html");
$tpl->ReplacePlaceholder(new SMKeyValue("Name", $txtName->Render()));
$tpl->ReplacePlaceholder(new SMKeyValue("Age", $txtAge->Render()));
$tpl->ReplacePlaceholder(new SMKeyValue("Gender", $txtGender->Render()));
$tpl->ReplacePlaceholder(new SMKeyValue("SaveButton", $cmdSave->Render()));
// Return View
return $tpl->GetContent();
}
}
To run this, simply create an instance of the Presenter in the Extension Controller like shown below.
class SMTest extends SMExtension
{
public function Render()
{
$pp = new SMTestPersonPresenter();
return $pp->Render();
}
}
As you can imagine, having multiple Views is now super easy. The same View can also be used by different Presenters - e.g. one responsible for editing (it binds GUI controls to the View), another for displaying data (it binds data directly to the View).
Soon to be even easier
We are currently working on something we call a Data-View-Mediator which reduces the amount of code when using an instance of SMDataSource as the Model. Here is an example of how it allows us to almost automatically synchronize data between the View and Model. Be aware that this approach does not allow custom business logic within the Model itself.
Presenter class
class SMTestPersonPresenter
{
public function Render()
{
// Create Model, View, and Mediator
$ds = new SMDataSource("SMTestPersons");
$tpl = new SMTemplate("extensions/SMTest/Person.html");
$dvm = new SMDataViewMediator($ds, $tpl);
// Create GUI Controls
$txtName = new SMInput("SMTestName", SMInputType::$Text);
$txtAge = new SMInput("SMTestAge", SMInputType::$Text);
$txtGender = new SMInput("SMTestGender", SMInputType::$Text);
$cmdSave = new SMLinkButton("SMTestSave");
$cmdSave->SetTitle("Save data");
// Bind GUI controls to View using Mediator.
$dvm->BindControl("Name", $txtName);
$dvm->BindControl("Age", $txtAge);
$dvm->BindControl("Gender", $txtGender);
$dvm->BindControl("SaveButton", $cmdSave);
// Load person into View
$dvm->Load(); // No WHERE statement (condition) - loads first item
// Save data if Save button was clicked
if ($cmdSave->PerformedPostBack() === true)
$dvm->Save(); // Updates existing if found, otherwise creates new
// Render View
return $dvm->Present();
}
}
Pretty impressive - this is about 40% less code, and it's now even easier to maintain and extend.
NOTICE: This example will NOT work, as we are not yet ready with the Data-View-Mediator. Contact us on the
Facebook page if you want access to the developer preview.