Model-View-Controller Approach (MVC) with Wisej.NET

What is MVC?

MVC is a way of separating an application into three interconnected parts: the data storage (Model), user interface (View), and connection between the view and the model (Controller).

The Model represents the application's data. It provides an interface for storing, accessing and updating the data. The View represents the user interface, displaying data from the model to the user and accepting input. The Controller acts as an intermediary between the View and Model, handling user input and updating the Model and View as needed.

MVC enables a clear separation of concerns which improves code organization, enhances maintainability, and facilitates code reusability. MVC's testability enables focused unit testing of individual components, leading to more robust and bug-free applications. Moreover, the flexibility of having multiple views for the same data supports the development of scalable and adaptable applications across different platforms or devices.

Creating a Wisej.NET application with MVC Architecture

You can find the full code of the sample project here: https://github.com/iceteagroup/wisej-architecture-examples

The Model

Simple Example Model

A simple example of a model would look like this:

public class StudentModel
{
    public int Id { get; set; }
    public string Email { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

In this example, the model represents a StudentModel object with its properties Id, Email, Name and Age.

Model with validation logic

The model is responsible for all validation logic that enforces business rules on the data. For example, if you wanted to check that the Email property had the correct format of local-part@domain, the proper place to put that logic would be in the model. Likewise, if you wanted to check that the Id property was between 1 and 999, that logic would belong in the model.

Here is the StudentModel, updated with data annotations to check that the Id and Email are formatted correctly. It also contains a method, ValidateData(), that determines if a given model contains valid data.

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace WisejMVC
{
	public class StudentModel
	{
		[Range(1, 999, ErrorMessage = "Id must be between 1 and 999")]
		public int Id { get; set; }

		[EmailAddress(ErrorMessage = "Invalid Email Address")]
		public string Email { get; set; }

		public string Name { get; set; }
		public int Age { get; set; }
		
		static public string ValidateData(StudentModel model)
		{
			string message = "";
			ValidationContext context = new ValidationContext(model, null, null);
			List<ValidationResult> validationResults = new List<ValidationResult>();
			bool valid = Validator.TryValidateObject(model, context, validationResults, true);
			if (!valid)
			{
				foreach (ValidationResult validationResult in
				validationResults)
				{
					message += validationResult.ErrorMessage + " \n";
				}
			}
			else
			{
				message += "The data is valid!";
			}
			return message;

		}
	}
}

Let's break down what this code does.

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

System.Collections.Generic is needed for the List<ValidationResult> that is created later in the code. The System.ComponentModel.DataAnnotations namespace provides code for validating data.

[Range(1, 999, ErrorMessage = "Id must be between 1 and 999")]
public int Id { get; set; }

[EmailAddress(ErrorMessage = "Invalid Email Address")]
public string Email { get; set; }

Notice that there is a new line of code in brackets above the declaration of the Id property. This is a validation attribute that comes from the System.ComponentModel.DataAnnotations namespace. In this case, it indicates that the Id should be between 1 and 999, inclusive, and it provides an error message that can be shown if the Id is not valid. Likewise, the line of code [EmailAddress(ErrorMessage = "Invalid Email Address")] indicates that the Email property must be a valid e-mail, and gives the error message to show if the email is not set to a valid e-mail. Note that these attributes do not prevent you from making an "invalid" instance of StudentModel. You can write this line of code, and it will compile and run:

StudentModel model = new StudentModel() { Name = "John", Id = 999999, Age = 30, Email = "bademail" };

So how can we avoid creating an invalid StudentModel? Simple: we define a function called ValidateData() which can determine if a given instance of StudentModel contains valid data. Let's look at the function in more detail:

string message = ""; Defines a string variable to hold the error message, or lack therof. This variable will be returned at the end of the function.

ValidationContext context = new ValidationContext(model, null, null); The ValidationContext class contains information about the object being validated, the validation attributes that are applied to the object's properties, and the current validation state. It provides a way to get the values of the object's properties. In this line of code, we create an instance of the ValidationContext class, passing in the object being validated (model) and null values for the service provider and items. List<ValidationResult> validationResults = new List<ValidationResult>(); The ValidationResult class represents the result of a validation operation. It contains an error message and a reference to the property that failed validation. If the validation was successful, the ValidationResult object will not contain any error messages. In this line of code, we create a list called validationResults that can contain ValidationResult objects. We will use this list to store a ValidationResult object for each property that fails validation.

bool valid = Validator.TryValidateObject(model, context, validationResults, true);

We send 4 parameters to the Validator.TryValidateObject() function: model (the object to be validated), context (a ValidationContext object), validationResults (a list of ValidationResult objects- it will be used to store any failed validations) and true (indicates that all properties will be validated) The function returns true if model is valid, false if model is not valid, and we store this in the variable valid.

You can read more about this function in the documentation:

if (!valid)
{
	foreach (ValidationResult validationResult in
	validationResults)
	{
		message += validationResult.ErrorMessage + " \n";
	}
}
else
{
	message += "The data is valid!";
}

If valid is false, meaning that at least one property of model was not valid, the code loops through the validationResult list and appends each error message to message. If valid is true, meaning that all properties of model were valid, message is set to "The data is valid!".

return message; returns the contents of the variable message, so that they can be used outside of this function.

Model interaction with database

The model is also responsible for retrieving data from and storing data in the database.

The GetStudents() function accesses the database, and returns a list of the items in the database.

// returns a list of StudentModel objects from the database
public static List<StudentModel> GetStudents()
{
        string jsonDatabaseFilePath = "database.json";
	
	// Read the JSON file content
        string json = File.ReadAllText(jsonDatabaseFilePath);
        
        // Deserialize the JSON into a List<StudentModel>
        List<StudentModel> students = JsonConvert.DeserializeObject<List<StudentModel>>(json);
        
        return students;
}	

The AddStudent() function adds the StudentModel to the database. Note the use of the keyword this. Each instane of the StudentModel class can call AddStudent() to add itself to the database.

// Adds the model to the database. Returns a string which contains an error message if the model is invalid.
public string AddStudent()
{
        string validMessage = "Added new student to the database.";
        // ValidateData returns a string with the validation errors. Returns validMessage if the data is valid.
        string message = StudentModel.ValidateData(this, validMessage);
        // if the data in the model is valid, add the student to the database
        if (message == validMessage)
        {
                string jsonDatabaseFilePath = "database.json";
                //add the data to the database
                UpdateJsonFile(jsonDatabaseFilePath, this);
        }
	// If the data is not valid, add a bit to the message informing the user that the student was not added to the database.
	else
	{
	message += " New student was not added to database.";
	}
	return message;
}

In this sample, the "database" is simply a JSON file. If the database was a SQL server database, you would still have the GetStudents() and AddStudent() functions. You would connect to the database via a connection string instead of via the filename of the JSON file.

The View

The View is a Wisej page that displays the list of students in a DataGridView and has an "Add Student" button. Generally, the view will be edited by using drag-and-drop to add controls to the designer. An example of a View would look like this, when seen in the designer:

Designer.cs

When the user adds or edits controls in the designer, code is produced in the Designer.cs file (In this case- Page1.Designer.cs) Let's look at the code for the View:

namespace WisejMVC
{
	partial class Page1
	{
		/// <summary>
		/// Required designer variable.
		/// </summary>
		private System.ComponentModel.IContainer components = null;

		/// <summary>
		/// Clean up any resources being used.
		/// </summary>
		/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
		protected override void Dispose(bool disposing)
		{
			if (disposing && (components != null))
			{
				components.Dispose();
			}
			base.Dispose(disposing);
		}

		#region Wisej Designer generated code

		/// <summary>
		/// Required method for Designer support - do not modify
		/// the contents of this method with the code editor.
		/// </summary>
		private void InitializeComponent()
		{
			this.dataGridView1 = new Wisej.Web.DataGridView();
			this.button1 = new Wisej.Web.Button();
			this.label1 = new Wisej.Web.Label();
			this.txtId = new Wisej.Web.TextBox();
			this.txtEmail = new Wisej.Web.TextBox();
			this.txtName = new Wisej.Web.TextBox();
			this.txtAge = new Wisej.Web.TextBox();
			((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
			this.SuspendLayout();
			// 
			// dataGridView1
			// 
			this.dataGridView1.Location = new System.Drawing.Point(102, 70);
			this.dataGridView1.Name = "dataGridView1";
			this.dataGridView1.Size = new System.Drawing.Size(598, 276);
			this.dataGridView1.TabIndex = 0;
			// 
			// button1
			// 
			this.button1.Location = new System.Drawing.Point(740, 298);
			this.button1.Name = "button1";
			this.button1.Size = new System.Drawing.Size(146, 37);
			this.button1.TabIndex = 1;
			this.button1.Text = "Add Student";
			this.button1.Click += new System.EventHandler(this.button1_Click);
			// 
			// label1
			// 
			this.label1.AutoSize = true;
			this.label1.Location = new System.Drawing.Point(324, 25);
			this.label1.Name = "label1";
			this.label1.Size = new System.Drawing.Size(129, 18);
			this.label1.TabIndex = 2;
			this.label1.Text = "Student Management";
			// 
			// txtId
			// 
			this.txtId.LabelText = "Id";
			this.txtId.Location = new System.Drawing.Point(740, 53);
			this.txtId.Name = "txtId";
			this.txtId.Size = new System.Drawing.Size(149, 53);
			this.txtId.TabIndex = 3;
			// 
			// txtEmail
			// 
			this.txtEmail.LabelText = "Email";
			this.txtEmail.Location = new System.Drawing.Point(740, 112);
			this.txtEmail.Name = "txtEmail";
			this.txtEmail.Size = new System.Drawing.Size(146, 53);
			this.txtEmail.TabIndex = 4;
			// 
			// txtName
			// 
			this.txtName.LabelText = "Name";
			this.txtName.Location = new System.Drawing.Point(740, 171);
			this.txtName.Name = "txtName";
			this.txtName.Size = new System.Drawing.Size(146, 53);
			this.txtName.TabIndex = 5;
			// 
			// txtAge
			// 
			this.txtAge.LabelText = "Age";
			this.txtAge.Location = new System.Drawing.Point(740, 230);
			this.txtAge.Name = "txtAge";
			this.txtAge.Size = new System.Drawing.Size(149, 53);
			this.txtAge.TabIndex = 6;
			// 
			// Page1
			// 
			this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 19F);
			this.AutoScaleMode = Wisej.Web.AutoScaleMode.Font;
			this.Controls.Add(this.txtAge);
			this.Controls.Add(this.txtName);
			this.Controls.Add(this.txtEmail);
			this.Controls.Add(this.txtId);
			this.Controls.Add(this.label1);
			this.Controls.Add(this.button1);
			this.Controls.Add(this.dataGridView1);
			this.Name = "Page1";
			this.Size = new System.Drawing.Size(1296, 430);
			this.Load += new System.EventHandler(this.Page1_Load);
			((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
			this.ResumeLayout(false);
			this.PerformLayout();

		}

		#endregion

		private Wisej.Web.DataGridView dataGridView1;
		private Wisej.Web.Button button1;
		private Wisej.Web.Label label1;
		private Wisej.Web.TextBox txtId;
		private Wisej.Web.TextBox txtEmail;
		private Wisej.Web.TextBox txtName;
		private Wisej.Web.TextBox txtAge;
	}
}

Look between the tags #region Wisej Designer generated code and #endregion to find the code that was generated by the designer. You will notice a pattern- there are 3 commented lines, followed by a series of lines of code that set the properties of a Wisej control.

Button code

For example, here is how the button was set up: private Wisej.Web.Button button1; The variable button1 is declared this.button1 = new Wisej.Web.Button(); A new button object is created

//			
// button1
// 
this.button1.Location = new System.Drawing.Point(740, 298);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(146, 37);
this.button1.TabIndex = 1;
this.button1.Text = "Add Student";
this.button1.Click += new System.EventHandler(this.button1_Click);

The button's properties- Location, Name, Size, TabIndex and Text are set. this.button1.Click += new System.EventHandler(this.button1_Click); assigns an event handler function (this.button1_Click) to the "Click" event of the button1 control. Thus, whenever the Click event fires, the function button1_Click() will be called. button1_Click()can be found in Page1.cs - it is part of the controller.

The Controller

Page1.cs serves as the controller in this sample.

The "Controller" is merely a bridge between the Model and the View.

If you have to format data to later display it in your View, that should happen in the Controller. For example, if your model stored a string as "Example_Text_Here", and you wanted the View to display it as "Example Text Here", it would be the controller's job to replace the underscores with spaces. In this sample, our data is pretty simple, so we don't format it.

public partial class Page1 : Page
	{
		List<StudentModel> studentdata = new List<StudentModel>();
		public Page1()
		{
			InitializeComponent();
		}

		/// Load data from the database on initial page load.
		private void Page1_Load(object sender, System.EventArgs e)
		{
			studentdata = StudentModel.GetStudents();
			dataGridView1.DataSource = studentdata;
		}

		/// Add a new student to the database using the values from the text fields.
		private void button1_Click(object sender, System.EventArgs e)
		{
			//check to make sure that all the text fields have text (ie they are not the empty string)
			if (txtId.Text != "" && txtEmail.Text != "" && txtName.Text != "" && txtAge.Text != "")
			{
				//read the data from the text fields in the view
				int id = Int32.Parse(txtId.Text);
				string email = txtEmail.Text;
				string name = txtName.Text;
				int age = Int32.Parse(txtAge.Text);

				// create a StudentModel based on the data in the text fields.
				StudentModel model = new StudentModel() { Name = name, Id = id, Age = age, Email = email };
				// Attempt to add the student to the database- show a sucess or failure message.
				string message = model.AddStudent();
				AlertBox.Show(message);

				//clear the textboxes
				txtId.Text = "";
				txtEmail.Text = "";
				txtName.Text = "";
				txtAge.Text = "";

				//Get the data from the database and show it in the view so that the new student is seen
				studentdata = StudentModel.GetStudents();
				dataGridView1.DataSource = studentdata;
			}
			else //if at least one of the text fields is blank
			{
				AlertBox.Show("Please enter values for all fields.");
			}
		}
	}

Adding a new student to the database

The button1_Click() event handler contains code for adding a new student to the database. The user enters data in the textboxes, and then clicks this button. A new student is added to the database, based on the values that were entered in the textboxes in the view. This is an excellent example of how the controller connects the View to the Model+database. Let's break it down:

We first check to make sure that all the textboxes have text. This ensures that a value will be sent in for each field of the model.

if (txtId.Text != "" && txtEmail.Text != "" && txtName.Text != "" && txtAge.Text != "")

We then read the data from each textbox, and store it in a variable. Note that the view contains some very generic data validation in order to make this code run without errors. For example, the user is only able to enter numbers in the "id" textbox- they cannot enter words. Validation that is specific to the model- ie checking that the id is between 1 and 999- is done by the model.

int id = Int32.Parse(txtId.Text);
string email = txtEmail.Text;
string name = txtName.Text;
int age = Int32.Parse(txtAge.Text);

Next, a StudentModel is created based on the data from the text fields. The AddStudent() method is called, which checks that the data meets the validation requirements in the student model. (In this case, the requirements are that the id is between 1 and 999, the id is unique, and that the email address is formatted correctly. A message is shown- it either indicates that the data was sucessfully added to the database, or it gives an explanation of why the data was invalid.

// create a StudentModel based on the data in the text fields.
StudentModel model = new StudentModel() { Name = name, Id = id, Age = age, Email = email };
// Attempt to add the student to the database- show a sucess or failure message.
string message = model.AddStudent();
AlertBox.Show(message);

Finally, the textboxes are cleared.

//clear the textboxes
txtId.Text = "";
txtEmail.Text = "";
txtName.Text = "";
txtAge.Text = "";

Synchronizing the View and the database

We see this code snippet twice- it runs when the page (the View) is initially loaded, and after a new student is added to the database. This code ensures that the View and the database are in sync- the same information that the user sees in the view is what is stored in the database.

studentdata = StudentModel.GetStudents();
dataGridView1.DataSource = studentdata;

Switching Views

In the context of a Wisej.NET application, the View is generally a Page or a Form. The controller is responsible for determining which view will be shown. There can be multiple views for each controller. An application can also have multiple controllers- in this case, each controller would most likely have its own view. Note that this sample only has one view and one controller.

The code-behind is the code that connects the View to the Controller. With a Wisej.NET project, the code-behind would be located in Page1.cs, and you would have a separate Controller class (ie Controller.cs). Ideally, the code-behind is as small as possible- it's job is to call Controller class functions at the appropriate times (ie in event handlers, when switching views). This sample does not have a code-behind because Page1.cs is the controller.

Summary

Model -Has all validation logic that enforces business rules on the data -Responsible for retrieving data from and storing data in the database. View -Can be created using drag-and-drop controls in the Wisej designer -The User Interface of the application -In Wisej.NET applications, generally a Page or a Form Controller -Connects the Model and the View -Contains event handlers -Formats data from the model before it is displayed in the View -Responsible for syncing the View and the database

Last updated

Logo

© Ice Tea Group LLC, All Rights Reserved.