Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Demystifying and Simplifying MVC Frameworks

5.00/5 (19 votes)
25 Jul 2017MIT10 min read 30.2K  
MVC as a design pattern has been strongly defined by the Gang of Four, but in recent years it's been hijacked as a marketing term more than a design pattern. This article aims to simplify and define what the MVC design pattern is and present a simple MVC framework that you can reuse in your project

Introduction

The Model-View-Controller (MVC) design pattern has been around a long time.  For years it's been the gold standard for creating great applications that can scale among technologies, implementations and even theologies.  In the '90s Microsoft based their MFC (Microsoft Foundation Classes) application framework on the MVC design pattern; abstracting the GUI from the logic and data access was a novel new concept that turned out to be a great competitive advantage for devs and Microsoft alike.

Background

With the introduction of VB6 and then .Net, it became more of a wild-wild-west coding mentatility for businesses (developers in these disciplines were a lot cheaper than C++ MFC guys).  Classic ASP with VB6 COM objects became a thing, and was replaced by ASP.Net WebForms.  ASP.Net WebForms followed the same event model as VB6 desktop apps, making it very approachable to the developers that cut their teeth building Client-Server apps in the business world.  It seemed the world was moving away from MVC and more towards a web-centric, front-end-heavy world that harkened back to the days of the Mainframe.

Then Microsoft set the world on its ear and released ASP.Net MVC.  Woah.  Talk about coming full-circle!  This was actually a fairly predictable move for Microsoft.  The .Net development environment was rapidly maturing and developers were hungry to leave ASP.Net WebForms and it's overhead behind.  Many had starting making their own MVC frameworks (such as my daytime alter-ego) as they studied true Object-Oriented Programming (OOP) principals.  One thing was certain, MVC as a design pattern was coming back to the fore-front.

Since the introduction of ASP.Net MVC, the Javascript kids have created their own MVC frameworks such as AngularJS.  While this is a noble endeavor in theory, applications written in Javascript as Single-Page-Applications (SPA) vary wildly in their performance as they are subject to the vagueries of browsers, computers, tablets and phones.  Big business' intranets aren't affected as much as public sites as they typically have much more control of the client running the Javascript.  

About SimpleMVC

Having years of experience writing MVC applications of all types (ASP.Net, WinForms, WPF, UWP, Android, Xamarin), I've developed a lot of reusable concepts (see the article I linked to my alter-ego) that help me quickly and easily get an MVC-based application up and running in minutes.  I thought real hard about it and decided it was time to create an MVC library that can be reused by anyone desiring to build an easily implemented and tested application.

Did I just mention testing?  Yes, yes I did.  See, one of the greatest attributes of MVC is that the layers have no knowledge of the other layers inner workings outside what their public contracts expose.  This is accomplished through the appropriate usage of interfaces to abstract the individual layers and isolate the relevant parts.  MVC is true OOP, it presents Encapsulation (Models & Controllers), Polymorphism (Views and Models), and Inheritance (Controllers).  When you learn MVC, you learn OOP in the progress.

SimpleMVC is really 10+ years in the making.  I've created MVC frameworks that were simpler, and MVC frameworks that were more complicated.  SimpleMVC is a balance between the two, much in line with Einstein's ascertation that things should be simple, but not simpler than they are.

Having said all of that, let's talk about what SimpleMVC is and what it isn't.

SimpleMVC is:

  • Extensible
  • Simple
  • Reusable
  • Robust
  • Loosely Coupled
  • Tightly Cohesive
  • Standards-Based

SimpleMVC is not:

  • Brittle
  • Procedural
  • A marketing tool

SimpleMVC as a framework exposes some very useful public interfaces and base classes:

This is all you need to build a great application utilizing the MVC architecture.  

Using the code

Follow these steps:

  1. Create your models by extending ModelBase.  Model base allows the models to expose basic functionality for CRUD operations on the model.  If you are using Entity Framework, or whatever DAL provider you desire, you would simply implement the access code in the methods you extend from ModelBase.  The controllers will know nothing about the details inside the model; all the controller has to do is request data actions and process the results.  To further enhance reusability of your application, you should use an adapter pattern to provide a framework for your models to utilize to access data.
  2. Extend the ISimpleView interface with interfaces that express the user interfaces of your application.  Each single group of functionality deserves it's own interface.  If you are creating a WPF app, you would create an interface for each page, as well as an interface for reusable controls.  With an ASP.net Webforms application, the page would get an interface and any user controls would get their own interfaces.
  3. Extend the SimpleControllerBase.  Here you have some important decisions to make.  In Microsoft's ASP.Net framework, you have a controller-per-model paradigm.  I don't believe this is an accurate breakdown of an application.  I suggest grouping together business logic for related functionality.  Example, if you have a Student who registers for classes and takes tests.  In Microsoft's paradigm, you would have three models and three controllers.  In the world of SimpleMVC, it is recommended that you would have three models but only one Controller.  The Controller is the glue between the models and the views.   Models and views mean nothing without business logic, and very rarely is business logic neatly encapsulated to a single model's data.
  4. Create mock UI objects that implement the  abstracted views created in step 1.
  5. Create mock DAL object that implement the CRUD functionality needed to manipulate the model persistence.
  6. Create Unit Tests that excercise the business rules in the controllers through the events and callbacks of the mock views.
  7. Make it all go green.  (this means make your tests succeed).

Imagine we are tasked with creating a small WinForms application to manage the registration of students into a school.  This WinForms app will be run by an administrator or two.  The app will talk to a SQL Server database on the school's network.  All the app has to do is create, update, print and delete students.

Next, imagine we are tasked with creating a small Asp.Net WebForms application to allow students to register for classes from anywhere.  The student data will be retrived from the school's SQL Server database on the school's network.

Finally, imagine we are tasked with creating a web service that professors will use to post grades to.  They may be using Excel or some other application to keep up with the student's tests and assignments, but ultimately the grades have to go to one place, regardless of where they come from.

Here we have three distinctly different applications, but they all work on the same problem domain: a student.

We know all the data will be stored in the school's SQL Server.  We know the logic for interacting with the data is consistent between the three applications.  We know what data to use to interact with the controller from the views.  

Step 1, the Models

We know we will need to store some information about the students such as their DOB, first and last name and a picture of the student.  The student will be identified with a StudentID number.  Also, the student will take classes, which we must define as well.  A class will have a name, a schedule, and a professor.  Finally, classes will have grades which will have a date, percent score and name.  All of this goes in a SQL Server, so we'll create a DB diagram to build the models from.

Now, even though this is good third normal form relational data, it doesn't blend well with the application for a couple of reasons:

  1. Cross-walk tables are very inconvient to work with.  It's much simpler to represent the aggregation with a List to the other side of the many-to-many relationship.
  2. Even though the Grade table is technically a cross-walk, it's scope is first to the Class as the input will be coming from the Professor, not the Student.  As such, the only link to the Grade will be through the Class object as a Dictionary<Student, List<Grade>>.

This leaves us creating three Models as such:

public class StudentModel : ModelBase
{
    public string StudentID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public byte[] Picture { get; set; }
    
    public List<Class> Classes { get; set; }

    // Boilerplate CRUD methods
}

public class ClassModel : ModelBase
{
    public Guid ClassID { get; set; }
    public string ClassName { get; set; }
    public string Professor { get; set; }
    public DateTime Time { get; set; }
    public bool IsMonday { get; set; }
    public bool IsTuesday { get; set; }
    public bool IsWednesday { get; set; }
    public bool IsThursday { get; set; }
    public bool IsFriday { get; set; }

    public List<StudentModel> Students { get; set; }
    public List<StudentModel, List<GradeModel>> Grades { get; set; }

  // Boilerplate CRUD methods
}

public class GradeModel : ModelBase
{
    public Guid GradeID { get; set; }
    public string GradeName { get; set; }
    public decimal GradeValue { get; set; }

    public StudentModel Student { get; set; }
    public ClassModel Class { get; set; }

  // Boilerplate CRUD methods
}

 

Step 2, the Views

We are now ready to define our views.  First, the WinForms app--we begin by mocking up the form in your favorite tool.

This Window finds a couple of interesting elements.  First, there's a search box to find a student, and a grid to display those results.  This is a master view and will be set up as it's own ISimpleView implmentation called IStudentMasterView.

IStudentMasterView needs a property to hold the search criteria, a collection of student models, an event to tell the controller to perform the search, and a callback for the controller to notify the view of the search results.

public interface IStudentMasterView : ISimpleView
{
    string Criteria { get; }
    List<StudentModel> StudentsResult { get; set; }

    event EventHandler PerformSearch;

    void Databind();
}

The bottom half of the Window is a details section.  As the user selects the user from the grid, it will populate the form with their date of birth, first name, last name and picture.  Alternatively, the user can click the "New" button to initialize the child form for a new student.  On the child form we'll need properties for the Student ID, First name, Last Name, DOB and picture.  Next we need events to initialize a new student and save a student.  Finally, we need a callback to notify the view that the save is complete in the event that the save is asynchronous (as it should be).

public interface IStudentDetailView : ISimpleView
{
    string StudentID { get; set; }
    string FirstName { get; set; }
    string LastName { get; set; }
    DateTime DOB { get; set; }

    event EventHandler LoadStudent;
    event EventHandler SaveStudent;

    void StudentLoaded();
    void StudentSaved();
}

Finally, you'll create your WinForms window in Visual Studio and create two user controls for the master and detail.  The master user control will implement IStudentMasterView and the detail user control will implement IStudentDetailView.

Wire up the appropriate properties to the accessor properties of the windows controls, bind the grid to the list of student models, and wire up the buttons to fire the events.  Before you know it, you'll have a complete user interface to meet the requirements for the student registration app!

Step 3, The Controller

So far we've designed the Models and Views for interacting with the database and users respectively.  Now it's time to apply some logic to complete the interactions between the two.  There are two schools of thought when it comes to Controllers and business logic.  On one side you'll hear that all business rules should be encapsulated into separate classes and assemblies so that logic can be shared accross applications and architectures.  On the other side, the MVC pattern is considered to be so generic that the Controllers ARE the business rules and simply provide the public interface to those rules; and new applications should use the MVC pattern anyways to leveral both the Controllers and the Models, even if the views are completely different.  Personally, I like the second position as it maintains strong cohesion between the logic, models and UI without forcing any tight coupling.  Also, it makes for more robust applications because changes to business logic does not have to be implemented in multiple places.  If the BL code was separated from the Controller and you had a Controller in another project consuming the same library, changes to that BL code may require code changes to both Controllers, forcing a QA cycle for both applications.

In our sample application we have defined the views for the Student.  In the future there will be new views for registering for classes and posting grades, both with separate implementation technologies.  For now we'll start our Controller with the event handlers for the two view interfaces.

public class StudentController : SimpleControllerBase
{
    public override bool Initialize()
    {
        var result = true;

        foreach(var view in Views.Values)
        {
            if(view is IStudentHeaderView)
            {
                var shv = view as IStudentHeaderView;

                shv.PerformSearch += ShvOnPerformSearch;

                result &= true;
            }
            else if(view is StudentDetailView)
            {
                var sdv = view as IStudentDetailView;

                sdv.PerformSearch += SdvOnLoadStudent;
                sdv.SaveStudent += SdvOnSaveStudent;

                result &= true;
            }
            else
            {
                result = false;
            }
        }

        return result;
    }

    public async void ShvOnPerformSearch(object sender, EventArgs e)
    {
        var view = sender as IStudentHeaderView;

        if(view != null)
        {
            var models = await StudentModel.SearchAsync(
                new SearchCriteria<StudentModel> { 
                    SearchCriteriaTypes = SearchCriteriaTypes.Contains,
                    CriteriaValues = new List<object> { view.Criteria }
                });

            view.StudentsResult = models;

            view.Databind();
        }
    }

    public async void SdvOnLoadStudent(object sender, EventArgs e)
    {
        var view = sender as IStudentDetailView;

        if(view != null)
        {
            // Marshal the data to the View.
            // We don't have an overload of LoadAsync that accepts a string
            var model = await StudentModel.LoadSearchAsync(
                new SearchCriteria<StudentModel {
                    SearchCriteriaTypes = SearchCriteriaTypes.IsEquals,
                    CriteriaValues = new List<object> { view.StudentID }
            }).FirstOrDefault();

            if(model != null)
            {
                view.StudentID = model.StudentID;
                view.FirstName = model.FirstName;
                view.LastName = model.LastName;
                view.DateOfBirth = model.DateOfBirth;
                view.Picture = model.Picture;

                view.Classes = model.Classes;

                view.StudentLoaded();
            }
        }
    }

    public async void SdvOnSaveStudent(object sender, EventArgs e)
    {
        var view = sender as IStudentDetailView;

        if(view != null)
        {
            // Business rules here!
            if(view.DateOfBirth >= DateTime.Today) 
                throw new ApplicationException("Invalid Date of Birth.");


            // Marshal the data from the View.
            // We don't have an overload of LoadAsync that accepts a string
            var model = await StudentModel.LoadSearchAsync(
            new SearchCriteria<StudentModel {
                SearchCriteriaTypes = SearchCriteriaTypes.IsEquals,
                CriteriaValues = new List<object> { view.StudentID }
            }).FirstOrDefault();

           if(model == null)
           {
               model = new StudentModel();
           }

           model.StudentID = view.StudentID;
           model.FirstName = view.FirstName;
           model.LastName = view.LastName;
           model.DateOfBirth = view.DateOfBirth;
           model.Picture = view.Picture;

           model.Classes = view.Classes;

           if(await Model.SaveAsync() == 1)
           {
              view.StudentSaved();
           }
          
       }
    }
}

Believe it or not, that's all we need.  

A few notes:

  • The event handlers are marked async so that they can be used in a responsive UI situation.  WPF, UWP and even WinForm application benefit greatly from this.
  • The display logic goes in the implementation of the view (display logic versus business rules is the topic for another day) which maintains the loose-coupling.
  • The Models expose a mini-search engine, the details of which are implemented in the adapter classes (which is another topic for another day).

Over at GitHub, the SimpleMVC repository contains a UnitTest project.  Clone the repository, study it and run the tests.  The project site is at http://simplemvc.gatewayprogramming.school.

Conclusion

People are very passionate about programming in general, and debates about how to implement the MVC pattern have raged since the day the GoF introduced it.  This article is but just another stake in the ground.  I greatly encourage you to learn more about the MVC pattern before blindly following a single methodology for implementing it.

License

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