söndag 21 juli 2013

Tutorial: Using the MVC design pattern in Visual C#


This is a small tutorial to explain about the MVC (Model View Controller) design pattern, and to show how it can be implemented in Visual C#. The example also makes use of the Observer pattern and the Publish/Subscribe pattern.

The application we will implement is a simple calculator. Actually it won't be a very useful calculator - to keep the tutorial simple the calculator will only have two functions: addition and subtraction. How's that for a calculator? Well, this will be enough to illustrate the usage of the design patterns.

First let's say a few words about MVC and how to design our application. (If you already know well enough about the MVC pattern, you can skip this section and go directly to "1. Start Visual Studio..." further down below.)

MVC stands for Model View Controller and it is a way to separate the user interface (UI) logic from the business logic. Taking a calculator as an example, the UI consists of the buttons and the number display, while the business logic corresponds to the internal handling of numbers in the calculator: addition, subtraction, multiplication, division, and so on.

Why do we want to separate the UI and the business logic from each other? Well, the idea is that the code will be easier to maintain by doing so.

  • If the UI needs to be extended in the future, it is easier to do so if the UI is not entangled with business logic.
  • We could also write a completely new UI, without having to rewrite any of the business logic, or perhaps let the user choose between different UIs, without having to duplicate the business logic for each one of them.
  • If the UI and business logic and very clearly separated, we could even have them running on different computers if we want to. A server could handle the business logic and a client could handle the UI.
  • Also, having the UI and business logic separate means development work can be parallellized more efficiently: a UI developer can work with the UI while a business logic developer works with the business logic, and they will not be stepping in each others files.

In the MVC pattern, an application is split into three parts:

  • (M)odel: this is where the data representations and business logic is at. The model keeps track of which state the application is in, and has logic for transitioning between states
  • (V)iew: this is the UI, i.e. this is what the user sees and interacts with. Here might be buttons, text boxes, labels, and so on.
  • (C)ontroller: this is responsible for taking user actions and pushing them to the Model so that the Model can do its work and calculate a new state.

So, in order to have the UI decoupled from the business logic, we don't want any strong references between the View and the Model. It should be the Controller who is responsible for pushing the user actions to the Model. But then again it is actually the View that is wired by the OS to receive user input. (Considering Visual C#, the View is typically the window form and thus it is the receiver of all user input events for the application.) How can we make the Controller aware of the user input, so that it can notify the Model about it? Here it is fitting to use the Observer pattern. With the Observer pattern, one entity can notify another entity - the Observer - when something interesting happens. To implement this, we will let the View offer a registration function, where the Controller can register itself. Then when interesting things happen in the View, the View will call the registered entities to notify them about the user actions. This way the Controller can be kept up to date with what the user does, and notify the Model so that the Model can do its work.

Now let's say the Model has done some work - for example it has calculated the result of adding two numbers that the user has chosen. We don't want the result to just stay inside the Model - it should of course be shown to the user. It is the View who has all the UI elements and so it is finally the View that should show the result to the user. How do we pass the calculation result from the Model to the View, without using strong references between them? Here we can use the Publish/Subscribe pattern. In this pattern, there is an additional entity that offers others to both publish, and subscribe to, data of different types. It is the Publish/Subscribe entity that keeps track of who subscribes to what, and who publishes what, i.e. a publisher and a subscriber do not have direct references to each other, they just post and receive data independently. Nice! Applying this to the calculator example, let's have the Model publishing its results to the Pub/Sub entity, and the View subscribing to such results so that it can update the UI when necessary.

Now let's move on to creating our calculator application.

1. Start Visual Studio and select New Project -> Visual C# -> Windows Application. I named the app MVCexample.

2. Let's design the UI. I added:
* buttons for numbers 0 to 9, named button0, button1, and so on
* two buttons for adding and subtracting, named buttonAdd and buttonSubtract
* one button for clearing, named buttonClear
* one button for calculating results, named buttonEquals
* one text box for showing the results, named textBox



3. Let's create an event class to use when we implement the Observer pattern. Events like this will be passed from the View to the Controller to let the Controller know which user actions are happening. To create the event class, I selected menu Project -> Add Class, and named the new file Event.cs. And here is the code it should contain:


using System;
using System.Collections.Generic;
using System.Text;
 
namespace MVCexample
{
  public enum EventType
  {
    eventTypeNumberClick,
    eventTypeAddClick,
    eventTypeSubtractClick,
    eventTypeClearClick,
    eventTypeEqualsClick
  }
 
  public class Event
  {
    private EventType mEventType;
    private int mData = 0;
 
    public Event(EventType eventType)
    {
      mEventType = eventType;
    }
 
    public Event(EventType eventType, int data)
    {
      mEventType = eventType;
      mData = data;
    }
 
    public EventType getEventType()
    {
      return mEventType;
    }
 
    public int getData()
    {
      return mData;
    }
  }
}

4. We'll also create an interface that the UI event listener (i.e. the Controller) can implement to receive UI notifications: go to menu Project -> Add Class, choose name IEventListener.cs. This interface will be very small: it will just declare one function, receivedEvent, which will be called whenever something interesting happens in the UI. The function will take one parameter of the Event class type, which we just wrote.


namespace MVCexample
{
  public interface IEventListener
  {
    void receivedEvent(Event ev);
  }
}

5. Now we'll add UI event handling code to Form1.cs to handle the user's button click events. Also we'll add code to Form1.cs to let someone (the Controller) register in order to receive UI event updates. The button click handlers should call the registered listeners.


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
 
namespace MVCexample
{
  public partial class Form1 : Form
  {
    List<IEventListener> mNumberClickListeners = new List<IEventListener>();
    List<IEventListener> mAddClickListeners = new List<IEventListener>();
    List<IEventListener> mSubtractClickListeners = new List<IEventListener>();
    List<IEventListener> mClearClickListeners = new List<IEventListener>();
    List<IEventListener> mEqualsClickListeners = new List<IEventListener>();
 
    public Form1()
    {
      InitializeComponent();
    }
 
    public void registerListener(IEventListener listener, Event ev)
    {
      switch (ev.getEventType())
      {
        case EventType.eventTypeNumberClick:
          mNumberClickListeners.Add(listener);
          break;
        case EventType.eventTypeAddClick:
          mAddClickListeners.Add(listener);
          break;
        case EventType.eventTypeSubtractClick:
          mSubtractClickListeners.Add(listener);
          break;
        case EventType.eventTypeClearClick:
          mClearClickListeners.Add(listener);
          break;
        case EventType.eventTypeEqualsClick:
          mEqualsClickListeners.Add(listener);
          break;
        default:
          Debug.WriteLine("Tried to register listener for unknown event type " + ev.getEventType());
          break;
      }
    }
 
    private void button1_Click(object sender, EventArgs e)
    {
      for (int i = 0; i < mNumberClickListeners.Count; i++)
      {
        Event ev = new Event(EventType.eventTypeNumberClick, 1);
        mNumberClickListeners[i].receivedEvent(ev);
      }
    }
 
    private void button2_Click(object sender, EventArgs e)
    {
      for (int i = 0; i < mNumberClickListeners.Count; i++)
      {
        Event ev = new Event(EventType.eventTypeNumberClick, 2);
        mNumberClickListeners[i].receivedEvent(ev);
      }
    }
 
    private void button3_Click(object sender, EventArgs e)
    {
      for (int i = 0; i < mNumberClickListeners.Count; i++)
      {
        Event ev = new Event(EventType.eventTypeNumberClick, 3);
        mNumberClickListeners[i].receivedEvent(ev);
      }
    }
 
    private void button4_Click(object sender, EventArgs e)
    {
      for (int i = 0; i < mNumberClickListeners.Count; i++)
      {
        Event ev = new Event(EventType.eventTypeNumberClick, 4);
        mNumberClickListeners[i].receivedEvent(ev);
      }
    }
 
    private void button5_Click(object sender, EventArgs e)
    {
      for (int i = 0; i < mNumberClickListeners.Count; i++)
      {
        Event ev = new Event(EventType.eventTypeNumberClick, 5);
        mNumberClickListeners[i].receivedEvent(ev);
      }
    }
 
    private void button6_Click(object sender, EventArgs e)
    {
      for (int i = 0; i < mNumberClickListeners.Count; i++)
      {
        Event ev = new Event(EventType.eventTypeNumberClick, 6);
        mNumberClickListeners[i].receivedEvent(ev);
      }
    }
 
    private void button7_Click(object sender, EventArgs e)
    {
      for (int i = 0; i < mNumberClickListeners.Count; i++)
      {
        Event ev = new Event(EventType.eventTypeNumberClick, 7);
        mNumberClickListeners[i].receivedEvent(ev);
      }
    }
 
    private void button8_Click(object sender, EventArgs e)
    {
      for (int i = 0; i < mNumberClickListeners.Count; i++)
      {
        Event ev = new Event(EventType.eventTypeNumberClick, 8);
        mNumberClickListeners[i].receivedEvent(ev);
      }
    }
 
    private void button9_Click(object sender, EventArgs e)
    {
      for (int i = 0; i < mNumberClickListeners.Count; i++)
      {
        Event ev = new Event(EventType.eventTypeNumberClick, 9);
        mNumberClickListeners[i].receivedEvent(ev);
      }
    }
 
    private void button0_Click(object sender, EventArgs e)
    {
      for (int i = 0; i < mNumberClickListeners.Count; i++)
      {
        Event ev = new Event(EventType.eventTypeNumberClick, 0);
        mNumberClickListeners[i].receivedEvent(ev);
      }
    }
 
    private void buttonClear_Click(object sender, EventArgs e)
    {
      for (int i = 0; i < mClearClickListeners.Count; i++)
      {
        Event ev = new Event(EventType.eventTypeClearClick);
        mClearClickListeners[i].receivedEvent(ev);
      }
    }
 
    private void buttonAdd_Click(object sender, EventArgs e)
    {
      for (int i = 0; i < mAddClickListeners.Count; i++)
      {
        Event ev = new Event(EventType.eventTypeAddClick);
        mAddClickListeners[i].receivedEvent(ev);
      }
    }
 
    private void buttonSubtract_Click(object sender, EventArgs e)
    {
      for (int i = 0; i < mSubtractClickListeners.Count; i++)
      {
        Event ev = new Event(EventType.eventTypeSubtractClick);
        mSubtractClickListeners[i].receivedEvent(ev);
      }
    }
 
    private void buttonEquals_Click(object sender, EventArgs e)
    {
      for (int i = 0; i < mEqualsClickListeners.Count; i++)
      {
        Event ev = new Event(EventType.eventTypeEqualsClick);
        mEqualsClickListeners[i].receivedEvent(ev);
      }
    }
  }
}

(If you think it looks bad to have explicit functions for all the number buttons like this, I guess you could create the buttons programatically instead and put them in an array. That way you could reuse one click event handler for all the number buttons.)

Please do not forget that you have to map all the buttons' click events to the click functions you just wrote. You do this from Properties, choosing each button and assigning the Click event to the corresponding click function.

6. Now let's create the Controller: from Project -> Add Class, I just named the new file Controller.cs.
In the Controller, we want to call the View's registration function so that we can receive the button events. Let's also add a private reference to a Model, and a function for setting the model. We will call this function later to associate the Controller with the Model. And we should implement the IEventListener interface to act on those button events. When we receive button events we will call the Model using our private reference. For now I have commented out all the references to the Model, we'll activate those later.


using System;
using System.Diagnostics;
 
namespace MVCexample
{
  public class Controller : IEventListener
  {
       // private Model mModel = null;
 
    public Controller(Form1 viewForm)
    {
      Event myNumberClickEvent = new Event(EventType.eventTypeNumberClick);
      viewForm.registerListener(this, myNumberClickEvent);
 
      Event myAddEvent = new Event(EventType.eventTypeAddClick);
      viewForm.registerListener(this, myAddEvent);
 
      Event mySubtractEvent = new Event(EventType.eventTypeSubtractClick);
      viewForm.registerListener(this, mySubtractEvent);
 
      Event myClearEvent = new Event(EventType.eventTypeClearClick);
      viewForm.registerListener(this, myClearEvent);
 
      Event myEqualsEvent = new Event(EventType.eventTypeEqualsClick);
      viewForm.registerListener(this, myEqualsEvent);
    }
 
    /* public void setModel(Model model)
    {
      mModel = model;
    }*/
 
    public void receivedEvent(Event ev)
    {
      switch (ev.getEventType())
      {
        case EventType.eventTypeNumberClick:
          numberClick(ev.getData());
          break;
        case EventType.eventTypeAddClick:
          addClick();
          break;
        case EventType.eventTypeSubtractClick:
          subtractClick();
          break;
        case EventType.eventTypeClearClick:
          clearClick();
          break;
        case EventType.eventTypeEqualsClick:
          equalsClick();
          break;
        default:
          Debug.WriteLine("Received unknown event: " + ev.getEventType());
          break;
      }
    }
 
    private void numberClick(int number)
    {
      Debug.WriteLine("You clicked number " + number);
      if (mModel != null)
      {
        // mModel.pushNumber(number);
      }
    }
 
    private void addClick()
    {
      Debug.WriteLine("You clicked add");
      // mModel.add();
    }
 
    private void subtractClick()
    {
      Debug.WriteLine("You clicked subtract");
      // mModel.subtract();
    }
 
    private void clearClick()
    {
      Debug.WriteLine("You clicked clear");
      // mModel.clear();
    }
 
    private void equalsClick()
    {
      Debug.WriteLine("You clicked equals");
      // mModel.equals();
    }
  }
}

7. Now let's create the Model! This is where all the cool and complicated calculations of the application will be going on. Only in our case they're not so cool and complicated after all: we'll only have functions for adding and subtracting numbers. Hm. (Let's make a game another time! Then we would put all the cool physics calculations and collision detection routines here.)

In general, if the application can have states, the state implementation should typically be here in the Model. Our simple calculator will have three different possible states: Idle, Adding, and Subtracting. We'll also keep a private member for the "current number" of the calculator. And we'll have functionality for modifying this value (pushNumber), and for temporarily storing a value, as well as adding, subtracting, clearing, and calculating results.

To create the Model, I selected menu Project -> Add Class, and I named the new file Model.cs. Here is the code it should contain:


using System;
using System.Collections.Generic;
using System.Text;
 
namespace MVCexample
{
  public class Model
  {
    private enum State
    {
      IDLE,
      ADDING,
      SUBTRACTING
    }
 
    private int mCurrentNumber = 0;
    private int mStoredNumber = 0;
 
    private bool mClearOnNextNumber = false;
 
    private State mCurrentState = State.IDLE;
 
    public void pushNumber(int number)
    {
      if (mClearOnNextNumber == true)
      {
        mCurrentNumber = 0;
        mClearOnNextNumber = false;
      }
 
      mCurrentNumber *= 10;
      mCurrentNumber += number;
    }
 
    public void add()
    {
      mCurrentState = State.ADDING;
      mStoredNumber = mCurrentNumber;
      mClearOnNextNumber = true;
    }
 
    public void subtract()
    {
      mCurrentState = State.SUBTRACTING;
      mStoredNumber = mCurrentNumber;
      mClearOnNextNumber = true;
    }
 
    public void clear()
    {
      mCurrentNumber = 0;
    }
 
    public void equals()
    {
      if (mCurrentState == State.ADDING)
      {
        mCurrentNumber = mStoredNumber + mCurrentNumber;
      }
      else if (mCurrentState == State.SUBTRACTING)
      {
        mCurrentNumber = mStoredNumber - mCurrentNumber;
      }
      mCurrentState = State.IDLE;
    }
  }
}

Now that we have the Model in place, we can go back to the Controller and activate all the references to the Model. Open up Controller.cs again and turn on the code that we commented out before, i.e:

 
private Model mModel = null;
 
    public void setModel(Model model)
    {
      mModel = model;
    }
 
    private void numberClick(int number)
    {
      Debug.WriteLine("You clicked number " + number);
      if (mModel != null)
      {
        mModel.pushNumber(number);
      }
    }
 
    private void addClick()
    {
      Debug.WriteLine("You clicked add");
      mModel.add();
    }
 
    private void subtractClick()
    {
      Debug.WriteLine("You clicked subtract");
      mModel.subtract();
    }
 
    private void clearClick()
    {
      Debug.WriteLine("You clicked clear");
      mModel.clear();
    }
 
    private void equalsClick()
    {
      Debug.WriteLine("You clicked equals");
      mModel.equals();
    }

By now we have most of the functionality in place:

  • We have a UI.
  • We are taking care of the user's button click events.
  • We can call the Model's calculator functionality to add or subtract numbers.

What we are lacking is UI updates when the calculator updates its result. For this, we will use the Publish/Subscribe pattern: the Model will publish its results, and the View will subscribe to such updates and update its UI accordingly. For this mechanism to work, we will create one interface and one class, in steps 8 and 9 below.

8. Create a new interface that the subscriber will implement, and which contains the different things we can subscribe to. In our example we will just have one thing we can subscribe to: calculator number updates. We'll name this type NUMBER_UPDATED.
Go to menu Project -> Add Class, choose name ISubscription.cs.

 
namespace MVCexample
{
  public enum SubscriptionType
  {
    NUMBER_UPDATED
  }
 
  public interface ISubscription
  {
    void numberUpdated(int number);
  }
}

9. Create a new class that will handling publishing and subscriptions: menu Project -> Add Class, choose name PubSub.cs. The class should have one function that the publisher calls to publish data, and one function the subscriber calls to register subscriptions of a certain type. In our simple example, NUMBER_UPDATED will be published whenever the Model updates its number representing the calculator's value to be displayed. Here is the code of the file:


using System;
using System.Collections.Generic;
using System.Text;
 
namespace MVCexample
{
  public class PubSub
  {
    private List<ISubscription> mSubscribers = new List<ISubscription>();
 
    public void subscribe(ISubscription subscriber, SubscriptionType type)
    {
      if (type == SubscriptionType.NUMBER_UPDATED)
      {
        mSubscribers.Add(subscriber);
      }
    }
 
    public void publish(SubscriptionType type, int data)
    {
      if (type == SubscriptionType.NUMBER_UPDATED)
      {
        for (int i = 0; i < mSubscribers.Count; i++)
        {
          mSubscribers[i].numberUpdated(data);
        }
      }
    }
  }
}

Now we need to make use of the PubSub class. It's time to add code for actually starting a subscription, and for publishing data.

10. Let's start with the publishing: modify Model.cs by adding a private reference to a PubSub object, and adding a function to set the PubSub object, and adding code to call the PubSub object's publishing function whenever the Model has updated its value.

 
private PubSub mPubSub = null;
 
    public void setPubSub(PubSub pubSub)
    {
      mPubSub = pubSub;
    }
 
    public void pushNumber(int number)
    {
      if (mClearOnNextNumber == true)
      {
        mCurrentNumber = 0;
        mClearOnNextNumber = false;
      }
 
      mCurrentNumber *= 10;
      mCurrentNumber += number;
 
      if (mPubSub != null)
      {
        mPubSub.publish(SubscriptionType.NUMBER_UPDATED, mCurrentNumber);
      }
    }
 
    public void clear()
    {
      mCurrentNumber = 0;
      if (mPubSub != null)
      {
        mPubSub.publish(SubscriptionType.NUMBER_UPDATED, mCurrentNumber);
      }
    }
 
    public void equals()
    {
      if (mCurrentState == State.ADDING)
      {
        mCurrentNumber = mStoredNumber + mCurrentNumber;
      }
      else if (mCurrentState == State.SUBTRACTING)
      {
        mCurrentNumber = mStoredNumber - mCurrentNumber;
      }
 
      if (mPubSub != null)
      {
        mPubSub.publish(SubscriptionType.NUMBER_UPDATED, mCurrentNumber);
      }
 
      mCurrentState = State.IDLE;
    }

11. Now add subscription code to the View. We will let Form1.cs implement the ISubscription interface and update the textbox in the UI when something gets published. Open Form1.cs and add this in it:


public partial class Form1 : Form, ISubscription
 
    private PubSub mPubSub = null;
 
    public void setPubSub(PubSub pubSub)
    {
      mPubSub = pubSub;
      mPubSub.subscribe(this, SubscriptionType.NUMBER_UPDATED);
    }
 
    public void numberUpdated(int number)
    {
      textBox.Text = "" + number;
    }

12. Now we need to wire some things together. Open Program.cs (which should have been autogenerated when you created the project), and modify it to look like this:


using System;
using System.Collections.Generic;
using System.Windows.Forms;
 
namespace MVCexample
{
  static class Program
  {
    [STAThread]
    static void Main()
    {
      Application.EnableVisualStyles();
      Application.SetCompatibleTextRenderingDefault(false);
      PubSub myPubSub = new PubSub();
      Form1 myForm = new Form1();
      myForm.setPubSub(myPubSub);
      Controller myController = new Controller(myForm);
      Model myModel = new Model();
      myModel.setPubSub(myPubSub);
      myController.setModel(myModel);
      Application.Run(myForm);
    }
  }
}

To recap what we are doing here, we create a PubSub object, and we attach it to both the View and the Model, so that they can subscribe and publish independently of each other. We create the Controller and make it aware of the View, so that the Controller can register and listen to UI events using the Observer pattern. We also make the Controller aware of the Model, because the Controller needs a reference to the Model in order to give user data to it.

And that's it, we're done! Now you can run the project and play around with the calculator.

In case you get compile errors, make sure you are using the right namespace in all of your files.

And in case you have a problem that nothing happens when you click a button, it's likely you have forgotten to map the buttons' click events with the corresponding click functions. (See "Please do not forget..." above in point 5.)


Note that the UI is unaware of the Model, and the Model has no direct references to the View either. (MVC mission accomplished!) So if you would want to make a more fancy UI, you would only have to update the View (Form1.cs) without messing around with the internals of the calculator.

And in case you would want to run the Model and the View on separate computers (for example running the Model on a server), you could modify the PubSub implementation so that it passes subscription messages asynchronously over the network instead, for example using TCP or UDP.

If you'd want to use fewer classes, I guess you could skip using the event listener and instead reuse the PubSub object for sharing UI events with the Controller from the View.

One important shortcoming with this calculator (other than that it only handles plus and minus, ehum...) is that it uses a signed int for storing its value. This means it can't handle numbers larger than 2^31 minus 1. If you'd want to implement a real calculator you should use a larger datatype.

If you want to get hold of the files for this project, I have uploaded them here: https://github.com/mattiaserlo/MVCexample

Inga kommentarer: