The data model is a set of data structures that lay the base for the businnes logic of an application. In typical object-oriented application, the data model is built of client-side classes and collections. The data model typically is somehow stored into a Database Management System, however how the data is exactly stored is not a concern of MVC.
The view is a user interface element (typically a window) that presents the data to the user and allows the user to change the data. It is a typical situation to have several views active at the same time (for example in the Multiple-Document Interface).
The controller is a connection point between the model and views: views register in the controller and reports changes in the model to them.
Having model and views separated seems correct from the perspective of object-oriented design but at the same time we face a serious problem: how do views communicate? or more precisely how the changes made to the model should be at once reported to all views?
MVC gives you the the answer to this question.
public class Point { private int x; public int X { get { return x; } set { x = value; } } private int y; public int Y { get { return y; } set { y = value; } } private Point() { } public Point( int x, int y ) { this.x = x; this.y = y; } }Now we need a model with storage for points inside. Node that the internal dynamic collection is not directly exposed. Instead, we expose it as a more restrictive type of collection.
public class Model { private List<Point> points; public Point[] Points { set { points = new List<Point>( value ); } get { return points.ToArray(); } } public void AddPoint( int y ) { points.Add( new Point( points.Count, y ) ); } public Model() { points = new List<Point>(); } public void Initialize( int Points, int MaxValue ) { for ( int p = 0; p < Points; p++ ) AddPoint( (int)( MaxValue / 2 + Math.Sin( 2 * Math.PI * p / Points ) * MaxValue / 2 ) ); } }We also need a default instance for the model and here we can use a singleton pattern.
public class Model { #region Singleton static Model instance; public static Model Instance { get { if ( instance == null ) instance = new Model(); return instance; } } #endregion ... }
We start by defining how changes in the model will be exposed. Since our model focuses on points, we only need to inform anyone that one of points is changed. However, in real-life applications, when the model is much more complicated, the amount of details you put into the notification is limited only by your imagination.
public class ModelChangeEventArgs { public int PointIndex; private ModelChangeEventArgs() {} public ModelChangeEventArgs( int PointIndex ) { this.PointIndex = PointIndex; } }Views are described in an uniform way
public interface IView { void ModelChange( object sender, ModelChangeEventArgs e ); }and to be the view, a form must only implement the interface
public partial class PointGraphics : Form, IView { ...What should happen when the notification about the model beeing changed is received by a view? Well, it depends on the view itself and the notification. In our example the nofitication says: "hello, one of points is changed" and the grid view will update only one cell
public partial class PointView : Form, IView { public PointView() { InitializeComponent(); pointBindingSource.DataSource = Model.Instance.Points; } #region IView Members public void ModelChange( object sender, ModelChangeEventArgs e ) { if ( e.PointIndex >= 0 && e.PointIndex < Consts.POINTS ) { DataGridViewCell cell = dataGridView1.Rows[e.PointIndex].Cells[1]; cell.Value = Model.Instance.Points[e.PointIndex].Y; dataGridView1.InvalidateCell( cell ); } } #endregionwhile the graphical view will be just completely rebuilt:
public partial class PointGraphics : Form, IView { public PointGraphics() { InitializeComponent(); this.SetStyle( ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true ); } private void PointGraphics_Paint( object sender, PaintEventArgs e ) { double xstep = (double)this.ClientRectangle.Width / Model.Instance.Points.Length; double ystep = (double)this.ClientRectangle.Height / Consts.MAXH; for ( int p = 0; p < Model.Instance.Points.Length - 1; p++ ) { using ( Pen pn = new Pen( Color.Red, 2 ) ) e.Graphics.DrawRectangle( pn, new Rectangle( (int)( p * xstep ), (int)( Model.Instance.Points[p].Y * ystep ), 3, 3 ) ); using ( Pen pn = new Pen( Color.Blue, 1 ) ) e.Graphics.DrawLine( pn, new PointF( (int)( p * xstep ), (int)( Model.Instance.Points[p].Y * ystep ) ), new PointF( (int)( ( p + 1 ) * xstep ), (int)( Model.Instance.Points[p + 1].Y * ystep ) ) ); } } #region IView Members public void ModelChange( object sender, ModelChangeEventArgs e ) { Refresh(); } #endregion
public class Controller { #region Singleton private static Controller instance; public static Controller Instance { get { if ( instance == null ) instance = new Controller(); return instance; } } #endregion public delegate void ModelChangeDelegate( object sender, ModelChangeEventArgs e ); public event ModelChangeDelegate ModelChangeEvent; public void RegisterView( IView view ) { this.ModelChangeEvent += new ModelChangeDelegate( view.ModelChange ); } public void UnregisterView( IView view ) { this.ModelChangeEvent -= new ModelChangeDelegate( view.ModelChange ); } public void RaiseModelChange( object sender, ModelChangeEventArgs e ) { if ( ModelChangeEvent != null ) ModelChangeEvent( sender, e ); } }
private void PointView_Load( object sender, EventArgs e ) { Controller.Instance.RegisterView( this ); } private void PointView_FormClosed( object sender, FormClosedEventArgs e ) { Controller.Instance.UnregisterView( this ); }As for the second issue, in cause of any user interaction raises the model change event. In a grid view we monitor changes in the DataGridView:
private void dataGridView1_CellValueChanged( object sender, DataGridViewCellEventArgs e ) { Controller.Instance.RaiseModelChange( this, new ModelChangeEventArgs( e.RowIndex ) ); }In a graphical view we allow the user to select points with mouse and move them vertically:
private void PointGraphics_MouseMove( object sender, MouseEventArgs e ) { if ( MovedPoint != null ) { MovedPoint.Y += (int)( ( e.Y - LastY ) / ( (double)ClientRectangle.Height / Consts.MAXH ) ); LastY = e.Y; Controller.Instance.RaiseModelChange( this, new ModelChangeEventArgs( MovedPoint.X ) ); } }