Overview
Model View Control (MVC) is a basic design pattern in programming. It is all about separating functionality. The Model consists of classes that model data. The data can come from files, or from databases, or can even be generated in the data classes themselves. The View consists of the classes that are all about input and output, forms for data entry, or pages for display. The Controller consists of classes whose job is to get the data from the model to the view or from the view to the model.
Ideally, the model is ignorant of the view and the view is ignorant of the model. Only the controller is aware of the the other two elements.
Microsoft's MVC isn't quite a pure version of the pattern. The model must be registered with the view, so, in fact, the view and the controller are both aware of the model; only the model is really ignorant of the the other two.
For this part I am going to explore the elements as given by the wizards and tools. In a second part, I will examine how to customize those elements. In a third part, I will create a controller and views from scratch.
I am going to use the BookReviewDB on SQL Server as my model. The SQL to create the database is available on
https://github.com/spconger/DatabaseSchema/blob/master/BookReviewDB.sql
Change the view to "RAW," copy the code and paste it into a new query in the SQL Server Management Studio. Run it to create the database. It will generate a few errors because of logins that don't exist. You can ignore these.
All the code for all three parts is available at https://github.com/spconger/BookReviewMVC
Starting a ASP.Net MVC app
For this example, I am using Visual Studio 2015.
You start by creating a new Project.You select "Web" and "ASP.Net Web Application." Give the project a name. I named it "BookReviewMVCApp."
Click OK
The next screen lets you choose what kind of web application. Choose "MVC." Uncheck the checkbox "Host in Cloud." You can also set up the type of Authentication here, but we won't.
Click OK.
The first thing to notice is that you get a start page with lot's of links to helpful tutorials and content on MVC.
You also get a fully formed CSS style sheet based on Bootstrap. That is a mixed blessing. I think it is often harder to redo and undo the CSS they give you, than it would be to make it from scratch.
The program will run as it is, though it doesn't do a lot.
You get a default home page.
The Menus work and you also get templates for "About" and "Contact"
You also get forms for Register and Login but these are not currently Active.
The other thing you should notice is the hierarchy of folders in the Solution Explorer.
It is probably worth taking a few moment to explore these.
Properties has an Assembly class. Best to leave this alone.
If your expand the references you will see a list of all the libraries the app is referencing. Quite a number really.
Currently there is nothing in the App_Data folder.
The App_Start folder contains configuration classes for all the pre-built objects.
The Content folder contains the Bootstrap CSS and the Site Css files.
The Controller folder contains the pre-built controller classes.
The Fonts folder contains fonts.
The Models folder contains the model classes for the pre-built objects
The Scripts folder contains the Bootstrap and JQuery JavaScript libraries.
The Views folder contains the pre-built views.
There are also some additional configuration files and the startup.cs which starts the application.
Adding a Model
Make sure you have stopped your application.
There are many ways to create a model, but we are going to add a model using ADO Data Entities, and using an existing database. It is possible to write classes in C# and have them used to create a new database, but we want to use one we already have.
Right click on the Models folder. Select "Add/ ADO Data Entities Model." (if it is not in the List, select New and then select ADO Data Entities Model from the list.)
This starts the ADO Data Entity Model Wizard. First name the Model
Click OK.
We want "EF Designer from Database."
Click Next.
On the "Choose Data Connection" click the "New Connection" button.
Choose "Sql Server." If you have made SQL server Connections before you may not see this dialog.
The next screen gives you your connection properties. My Server for this will be SqlExpress. If you are using a full version default installation you can write "localhost." Otherwise, use the name of your server. I am leaving it at Windows Authentication, and I have chosen the database BookReviewDB.
Click OK and then Next.
Now we need to choose the tables and other elements for our Data Entities model. I am going to choose all the tables and the two stored procedures "usp_Reviewerlogin" and "usp_NewReviewer."
Click Finish. You will get a couple of warning dialogs:
I would suggest NOT clicking the checkbox "Do not show this Message Again," because the warning message is a good indicator that everything is going as planned. Its absence could mean that the wizard has failed to complete properly. Click OK as many times as needed.
You should end up with a diagram that looks something like this:
Now you should go to the Top level menu "Build" and build the project. Otherwise the model might not register when we create the controller.
Adding a Contoller
Right click on the Controller folder and select "ADD Controller."
Choose "MVC 5 Controller with Views Using Entity Framework."
Click Add.
Add the class we are going to use for this controller: Reviewer (BookReviewMVCApp.Models), and add the model: BookReviewDbEntities (BookReviewMVCApp.Models).
Everything is now functional, though maybe not as we would like it. To get to the Reviewers Index page just run the program and add "/Reviewers" to the URL. This gives a list of all the reviewers in the database. In our case that's not too many, but it could be a problem if we had thousands of reviewers. Also we really don't really want to show the password plain or hashed or the ReviewerKeyCode.
The "Create View," "Edit" and "Details" also work, but with similar problems. We don't really want to edit passwords and hashes.
Next, let's look at the code it generated for us. This will help when we want to customize it.
The Controller
here is the code that was generated for the controller:
using System; using System.Collections.Generic; using System.Data; using System.Data.Entity; using System.Linq; using System.Net; using System.Web; using System.Web.Mvc; using BookReviewMVCApp.Models; namespace BookReviewMVCApp.Controllers { public class ReviewersController : Controller { private BookReviewDbEntities db = new BookReviewDbEntities(); // GET: Reviewers public ActionResult Index() { return View(db.Reviewers.ToList()); } // GET: Reviewers/Details/5 public ActionResult Details(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Reviewer reviewer = db.Reviewers.Find(id); if (reviewer == null) { return HttpNotFound(); } return View(reviewer); } // GET: Reviewers/Create public ActionResult Create() { return View(); } // POST: Reviewers/Create // To protect from overposting attacks, please enable the specific properties you want to bind to, for // more details see http://go.microsoft.com/fwlink/?LinkId=317598. [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "ReviewerKey,ReviewerUserName,ReviewerFirstName,ReviewerLastName,ReviewerEmail,ReviewerKeyCode,ReviewPlainPassword,ReviewerHashedPass,ReviewerDateEntered")] Reviewer reviewer) { if (ModelState.IsValid) { db.Reviewers.Add(reviewer); db.SaveChanges(); return RedirectToAction("Index"); } return View(reviewer); } // GET: Reviewers/Edit/5 public ActionResult Edit(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Reviewer reviewer = db.Reviewers.Find(id); if (reviewer == null) { return HttpNotFound(); } return View(reviewer); } // POST: Reviewers/Edit/5 // To protect from overposting attacks, please enable the specific properties you want to bind to, for // more details see http://go.microsoft.com/fwlink/?LinkId=317598. [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit([Bind(Include = "ReviewerKey,ReviewerUserName,ReviewerFirstName,ReviewerLastName,ReviewerEmail,ReviewerKeyCode,ReviewPlainPassword,ReviewerHashedPass,ReviewerDateEntered")] Reviewer reviewer) { if (ModelState.IsValid) { db.Entry(reviewer).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } return View(reviewer); } // GET: Reviewers/Delete/5 public ActionResult Delete(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Reviewer reviewer = db.Reviewers.Find(id); if (reviewer == null) { return HttpNotFound(); } return View(reviewer); } // POST: Reviewers/Delete/5 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public ActionResult DeleteConfirmed(int id) { Reviewer reviewer = db.Reviewers.Find(id); db.Reviewers.Remove(reviewer); db.SaveChanges(); return RedirectToAction("Index"); } protected override void Dispose(bool disposing) { if (disposing) { db.Dispose(); } base.Dispose(disposing); } } }
Let's Walk through it Line by Line.
First every controller class inherits from "Controller." Next a class level variable "db" is declared which connects the controller class the the Entity model. The first method is named "Index." It is important to note that the names of the methods match the views. Note other methods named "Edit", "Details," "Create," "Delete." Each method returns a type of "ActionResult." The "Index" method returns the list of reviewers from the Data Entity (db) collection Reviewers. Notice that the method returns a View and the content of the view is in the parenthesis.
public ActionResult Index() { return View(db.Reviewers.ToList()); }
There is no selection here. It just returns everything in the table. That is something we will want to change in part two of this tutorial.
The second method returns the details of a single record based on the Id of the record. The "int?" is a function of generics to determine what kind of int it is. Most of the method is error trapping if the particular record cannot be found.
public ActionResult Details(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Reviewer reviewer = db.Reviewers.Find(id); if (reviewer == null) { return HttpNotFound(); } return View(reviewer); }
The meat of the method is in the line.
Reviewer reviewer = db.Reviewers.Find(id);
It finds a record and assigns it to the object reviewer. This object is what is returned to the view.
The next method is Create. It is used to add a new Reviewer.
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "ReviewerKey,ReviewerUserName,ReviewerFirstName,ReviewerLastName,ReviewerEmail,ReviewerKeyCode,ReviewPlainPassword,ReviewerHashedPass,ReviewerDateEntered")] Reviewer reviewer) { if (ModelState.IsValid) { db.Reviewers.Add(reviewer); db.SaveChanges(); return RedirectToAction("Index"); } return View(reviewer); }
The [HttpPost] and [ValidateAntiForgeryToken] are compiler directives. The first tells the compiler to pass the record by Post. The default is Get. The second offers some protection against cross site request forgeries.
The actual method starts with the ActionResult as return type, Create as the name and then in the parameters uses [Bind(Include) to create a new instance of the class Reviewer. If you scroll across you can see all the fields that are included in that class. Inside the method, it tests to see if the Model state is valid. If it is, it adds the reviewer to the collection of Reviewers (found in the ADO Data Entities) and then saves the changes to the database and redirects the user back to the Index page. If it is not valid it still returns the reviewer.
The Edit method is overloaded. The first one takes an int? id as a parameter and is essentially the same as Details method. The overload of the method takes a Reviewer object constructed just as in the Create Method, but has a couple of changes:
public ActionResult Edit([Bind(Include = "ReviewerKey,ReviewerUserName,ReviewerFirstName,ReviewerLastName,ReviewerEmail,ReviewerKeyCode,ReviewPlainPassword,ReviewerHashedPass,ReviewerDateEntered")] Reviewer reviewer) { if (ModelState.IsValid) { db.Entry(reviewer).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } return View(reviewer); }
Instead of adding the Reviewer to the Collection Reviewers, because the Reviewer already exists, the code makes the state of the record as changed and saves the changes.
There are two delete methods, the first just finds the record by id. The second DeleteConfirm actually removes the record from database.
public ActionResult Delete(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Reviewer reviewer = db.Reviewers.Find(id); if (reviewer == null) { return HttpNotFound(); } return View(reviewer); } // POST: Reviewers/Delete/5 [HttpPost, ActionName("Delete")] ValidateAntiForgeryToken] public ActionResult DeleteConfirmed(int id) { Reviewer reviewer = db.Reviewers.Find(id); db.Reviewers.Remove(reviewer); db.SaveChanges(); return RedirectToAction("Index"); }
Finally, there is a Dispose method. The function of the dispose method is to clean up the class and make sure all is removed from memory when the class goes out of scope.
The View
Let's take a look at the views for this controller. You can find it under Views\Reviewer. You will notice that there is a view for each separate function. Let's look at the Index view first.
@model IEnumerable<BookReviewMVCApp.Models.Reviewer> @{ ViewBag.Title = "Index"; } <h2>Index</h2> <p> @Html.ActionLink("Create New", "Create") </p> <table class="table"> <tr> <th> @Html.DisplayNameFor(model => model.ReviewerUserName) </th> <th> @Html.DisplayNameFor(model => model.ReviewerFirstName) </th> <th> @Html.DisplayNameFor(model => model.ReviewerLastName) </th> <th> @Html.DisplayNameFor(model => model.ReviewerEmail) </th> <th> @Html.DisplayNameFor(model => model.ReviewerKeyCode) </th> <th> @Html.DisplayNameFor(model => model.ReviewPlainPassword) </th> <th> @Html.DisplayNameFor(model => model.ReviewerHashedPass) </th> <th> @Html.DisplayNameFor(model => model.ReviewerDateEntered) </th> <th></th> </tr> @foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item.ReviewerUserName) </td> <td> @Html.DisplayFor(modelItem => item.ReviewerFirstName) </td> <td> @Html.DisplayFor(modelItem => item.ReviewerLastName) </td> <td> @Html.DisplayFor(modelItem => item.ReviewerEmail) </td> <td> @Html.DisplayFor(modelItem => item.ReviewerKeyCode) </td> <td> @Html.DisplayFor(modelItem => item.ReviewPlainPassword) </td> <td> @Html.DisplayFor(modelItem => item.ReviewerHashedPass) </td> <td> @Html.DisplayFor(modelItem => item.ReviewerDateEntered) </td> <td> @Html.ActionLink("Edit", "Edit", new { id=item.ReviewerKey }) | @Html.ActionLink("Details", "Details", new { id=item.ReviewerKey }) | @Html.ActionLink("Delete", "Delete", new { id=item.ReviewerKey }) </td> </tr> } </table>
A few things to note: the @ notation designates a C# scripting language called Razor. It is meant to make scripting in the HTML easier. At the top of the page is the reference to the model. The IEnumberable is an interface that is required if a thing is to be "listable" or "sortable." The @Html.ActionLink is like a button that that will call the Create method in the controller. The "Create New" is the text that will show on the page. Most of the page consists of laying out a table for display, first the headers and then a loop for each of the rows in the data set. The Razor syntax here is fairly clear. We want to display the item of the Reviewer class specified. It ends with more Html.ActionLinks calling the Details view, Edit and Delete.
You might be wondering where the rest of the HTML is. There is not HTML declaration, no html, head or body tags. The rest of the HTML resides in the Shared folder, mostly in the _Layout.cshtml file. The index view is an insert into that layout.
Here is the _Layout.cshtml file.
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> @Styles.Render("~/Content/css") @Scripts.Render("~/bundles/modernizr") </head> <body> <div class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" }) </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li>@Html.ActionLink("Home", "Index", "Home")</li> <li>@Html.ActionLink("About", "About", "Home")</li> <li>@Html.ActionLink("Contact", "Contact", "Home")</li> </ul> @Html.Partial("_LoginPartial") </div> </div> </div> <div class="container body-content"> @RenderBody() <hr /> <footer> <p>© @DateTime.Now.Year - My ASP.NET Application</p> </footer> </div> @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/bundles/bootstrap") @RenderSection("scripts", required: false) </body> </html>
In the next part of the tutorial we will look at this in more detail also edit it, to customize our application at least a little.
We should also look at the other views that were created. Let's look first at the Create View. We will only look at a couple of details. It can be a bit overwhelming to look at it all together, but much of it is just a repetition of the same elements for each field. At the top we have the reference to the model, the View bag name of the view, a header and the include for the beginning of the form:
@model BookReviewMVCApp.Models.Reviewer @{ ViewBag.Title = "Create"; } <h2>Create</h2> @using (Html.BeginForm()) { @Html.AntiForgeryToken()
Let's look at just the first form element:
<div class="form-group"> @Html.LabelFor(model => model.ReviewerUserName, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.ReviewerUserName, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.ReviewerUserName, "", new { @class = "text-danger" }) </div> </div>
First it gets the label for the model field and applies a CSS class to it. Next, in the div, it adds and "EditorFor" which is essentially a text box for the user to enter the value. Below that is a validation control which validates the text box. All form group elements follow this exact pattern. At the bottom of the form is a submit button and an Actionlink that will take the user back to the Index page.
The Edit view looks just like the Create. The Delete is also easy to understand though it uses dt and dd tags instead of table tags.
In the next blog, I will modify the controller and views to better reflect our use of the database.
No comments:
Post a Comment