Wednesday, December 28, 2016

MVC Part Three: Creating Controller and Views from Scratch.

Overview


Part one | Part Two

For this section, I want to add a view of the Reviews that includes not the ids of the Reviewer and the Book title, but the reviewers names and the actual title of the book being reviewed. To do this I am going to add a class to the model. I will make a new controller from scratch and create my own views.

Adding a Class to the Model


First, right click on the folder Models and select Add/Class. I name it "CompleteReview."

new model class

We will need sets and gets for all the fields we want to see. We will also need the ReviewerKey and BookKey to use for other methods.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace BookReviewMVCApp.Models
{
    public class CompleteReview
    {
        public int ReviewKey { get; set; }
        public string BookTitle { get; set; }
        public int BookKey { get; set; }
        public int ReviewerKey { get; set; }
        public string ReviewerLastName { get; set; }
        public System.DateTime ReviewDate { get; set; }
        public string ReviewTitle { get; set; }
        public int ReviewRating { get; set; }
        public string ReviewText { get; set; }

    }
}

Adding a controller


Add a controller by right clicking on the Controller folder and choosing ADD/ Controller. We Want to add an Empty MVC controller. I named it "ReviewController."

Add empty Controller

The first thing to add to the empty controller is a using statement that adds the model

using BookReviewMVCApp.Models;

We are going to go through this step by step. First we will add the Index. It is a little more complicated than the one that was made for us, because we are not going to return the class directly from the ADO Entity Model. We are going to use our CompleteReview class instead. But we are going to populate it from the model. This means writing a query and then looping through the results and writing them to the class fields. We will write our classes to a list and then pass the list to the view. Here is the code for the whole Index method:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using BookReviewMVCApp.Models; //add for models


namespace BookReviewMVCApp.Controllers
{
    public class ReviewController : Controller
    {
        //reference to Book Review entities
        private BookReviewDbEntities db = new BookReviewDbEntities();
        // GET: Review
        public ActionResult Index()
        {
            //use the entity model to get values
            var rev = from r in db.Reviews
                      select new
                      {
                          r.ReviewKey,
                          r.Book.BookTitle,
                          r.ReviewerKey,
                          r.Reviewer.ReviewerLastName,
                          r.ReviewTitle,
                          r.BookKey,
                          r.ReviewDate,
                          r.ReviewRating,
                          r.ReviewText
                      };
            List<CompleteReview> reviews = new List<CompleteReview>();
            //loop through our results and assign them
            //to our model class. Add the model class
            //to a list
            foreach(var r in rev)
            {
                CompleteReview cr = new CompleteReview();
                cr.ReviewKey = r.ReviewKey;
                cr.BookTitle = r.BookTitle;
                cr.BookKey = r.BookKey;
                cr.ReviewerLastName = r.ReviewerLastName;
                cr.ReviewerKey = r.ReviewerKey;
                cr.ReviewTitle = r.ReviewTitle;
                cr.ReviewDate = r.ReviewDate;
                cr.ReviewRating = r.ReviewRating;
                cr.ReviewText = r.ReviewText;
                reviews.Add(cr);
            }
            //return the list of CompleteReviews
            return View(reviews);
        }
    }
}

Now we will create the View. There should already be a folder called "Reviews" under views. If not, create it. Then right click on it and choose Add /View. We want an Empty view. We will name it Index.

new Empty view

The pattern for this view is basically the same as for the autogenerated one. First we make a reference to the model. Then we set up the table and the headings. Lastly we loop through the model writing the fields to table cells. Here is the code for the view.

@model IEnumerable<BookReviewMVCApp.Models.CompleteReview>
@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.ReviewTitle)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.ReviewerLastName)
        </th>
       
        <th>
            @Html.DisplayNameFor(model => model.ReviewDate)
        </th>

        <th>
            @Html.DisplayNameFor(model => model.BookTitle)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.ReviewRating)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.ReviewText)
        </th>
    </tr>

    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.ReviewTitle)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReviewerLastName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReviewDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.BookTitle)
            </td>

            <td>
                @Html.DisplayFor(modelItem => item.ReviewRating)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReviewText)
            </td>
            
        </tr>
    }

</table>

When we run the Index, we get this:

Review Index running

Details and Delete


Next we will do the details view, since it is relatively easy, though only relatively, as you will see. We can just pass the ReviewKey to the method since, although we are using our class CompleteReview, the key is the same as the key for the Review entity. We can retrieve the review and then map it to our CompleteReview class. Note the dot notation. We can get from Review to Book to BookTitle, and the Same with Reviewer.ReviewerLastName. We return our instance of the CompleteReview.

 public ActionResult Details(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Review review = db.Reviews.Find(id);
            CompleteReview rev = new CompleteReview();
            rev.ReviewKey = review.ReviewKey;
            rev.ReviewTitle = review.ReviewTitle;
            rev.BookTitle = review.Book.BookTitle;
            rev.BookKey = review.BookKey;
            rev.ReviewerLastName = review.Reviewer.ReviewerLastName;
            rev.ReviewerKey = review.ReviewerKey;
            rev.ReviewDate = review.ReviewDate;
            rev.ReviewRating = review.ReviewRating;
            rev.ReviewText = review.ReviewText;
            if (review == null)
            {
                return HttpNotFound();
            }
            return View(rev);
        }
    }

Now let's create the view. Right click on the Review folder under Views and ADD/ View, Again we want an empty MVC5 view. Name it Details. We want to do pretty much as in the other Details, setting up a Data List <dl> with its Data Titles <dt> and data details <dd>

 @model BookReviewMVCApp.Models.CompleteReview

@{
    ViewBag.Title = "Details";
}

<h2>Details</h2>

<div>
    <h4>Review</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.ReviewTitle)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.ReviewTitle)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.ReviewerLastName)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.ReviewerLastName)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.BookTitle)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.BookTitle)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.ReviewDate)
        </dt>

         <dd>
            @Html.DisplayFor(model => model.ReviewDate)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.ReviewRating)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.ReviewRating)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.ReviewText)
        </dt>
        <dd>
            @Html.DisplayFor(model=> model.ReviewText)
        </dd>
        

    </dl>
</div>
<p>
   
    @Html.ActionLink("Back to List", "Index")
</p>

@{
    ViewBag.Title = "Details";
}

<h2>Details</h2>

We also want to add a link to the index page. To do this I added an extra <th> </th> tags at the top so that the number of cells in the header row stays the same as the number of cells in the detail rows. Then at the end of table rows in the loop I add

<td>
   @Html.ActionLink("Details", "Details", new { id = item.ReviewKey }) 
</td>

Now if you run from the Index, you will see the Details button on the end of the row.

Details Added

Now if you click the details button, you will see a details page for our class.

I should remove that last <h2> tag to get rid of the extra header at the bottom.

We can add the first part of the delete by just copying the Detail method and pasting it from the ReviewerController into our ReviewController. Just change "Reviewer" to "Review" and "Reviewers" to "Reviews."

 public ActionResult Delete(int? id)
        {
            
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Review review = db.Reviews.Find(id);
            CompleteReview rev = new CompleteReview();
            rev.ReviewKey = review.ReviewKey;
            rev.ReviewTitle = review.ReviewTitle;
            rev.BookTitle = review.Book.BookTitle;
            rev.BookKey = review.BookKey;
            rev.ReviewerLastName = review.Reviewer.ReviewerLastName;
            rev.ReviewerKey = review.ReviewerKey;
            rev.ReviewDate = review.ReviewDate;
            rev.ReviewRating = review.ReviewRating;
            rev.ReviewText = review.ReviewText;
            if (review == null)
            {
                return HttpNotFound();
            }
            return View(rev);

            
        }

We should also need to create a view for it. You can copy the data list from the details view. I copied the delete action at the end from the Reviewers Delete view.

@model BookReviewMVCApp.Models.CompleteReview
@{
    ViewBag.Title = "Delete";
}

<h2>Delete</h2>
<p>Are you sure that you want to Delete this record?</p>
<div>
<dl class="dl-horizontal">
    <dt>
        @Html.DisplayNameFor(model => model.ReviewTitle)
    </dt>

    <dd>
        @Html.DisplayFor(model => model.ReviewTitle)
    </dd>
    <dt>
        @Html.DisplayNameFor(model => model.ReviewerLastName)
    </dt>

    <dd>
        @Html.DisplayFor(model => model.ReviewerLastName)
    </dd>

    <dt>
        @Html.DisplayNameFor(model => model.BookTitle)
    </dt>

    <dd>
        @Html.DisplayFor(model => model.BookTitle)
    </dd>
    <dt>
        @Html.DisplayNameFor(model => model.ReviewDate)
    </dt>

    <dd>
        @Html.DisplayFor(model => model.ReviewDate)
    </dd>

    <dt>
        @Html.DisplayNameFor(model => model.ReviewRating)
    </dt>
    <dd>
        @Html.DisplayFor(model => model.ReviewRating)
    </dd>

    <dt>
        @Html.DisplayNameFor(model => model.ReviewText)
    </dt>
    <dd>
        @Html.DisplayFor(model => model.ReviewText)
    </dd>


</dl>
</div>
<p>
@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-actions no-color">
        <input type="submit" value="Delete" class="btn btn-default" /> |
        @Html.ActionLink("Back to List", "Index")
    </div>
}
</p>

For the DeleteConfirmed method, we can just change the class from "Reviewer" to "Review."

 public ActionResult DeleteConfirmed(int id)
        {
            Review review = db.Reviews.Find(id);
            db.Reviews.Remove(review);
            db.SaveChanges();
            return RedirectToAction("Index");
        }

Create


I kind of punted for the Create and basically went for the defaults. Instead of using CompleteReview as a data class, I just used the Review in the Entities classes. That means instead of entering the title of the book I need to enter the key for the book. The same is true of the reviewer. It is, I believe, possible to add drop down lists to the view that would let a user select among titles. If we had our login set up, the Reviewer key could be retrieved from a session variable. But for now, I am just going with the defaults. Here is the code for the two Create methods in our controller.

  public ActionResult Create()
        {
            return View();
        }

        
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create([Bind(Include = "ReviewTitle,ReviewerDate,ReviewerKey,BookKey, ReviewRating, ReviewText")] Review review)
        {
            if (ModelState.IsValid)
            {
                db.Reviews.Add(review);
                db.SaveChanges();
                return RedirectToAction("Index");
                
            }

            return View(review);
        }
    }

The first method just returns the empty form. The second takes the data in the form, assigns it to a new Review class, adds the Review class to the collection of Reviews and saves the changes.

Here is the code for the view. I modeled it closely after the view that was created for us with the Reviewer controller.

  @model BookReviewMVCApp.Models.Review

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>


@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Review</h4>
        <hr />

        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.ReviewTitle, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.ReviewTitle, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.ReviewTitle, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.ReviewerKey, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.ReviewerKey, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.ReviewerKey, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.ReviewDate, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.ReviewDate, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.ReviewDate, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.BookKey, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.BookKey, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.BookKey, "", new { @class = "text-danger" })
            </div>
        </div>



        <div class="form-group">
            @Html.LabelFor(model => model.ReviewRating, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.ReviewRating, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.ReviewRating, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.ReviewText, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.ReviewText, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.ReviewText, "", new { @class = "text-danger" })
            </div>
        </div>



        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

I also added the ActionLink to the top of the index page. Here is the form when you click "Create."

new Create form

Here is the form with some new data.

create with Data

And, After clicking Create, here is the Index with the new record.

Index

Edit


For the Edit, I did something similar to the Details, in that I passed a CompleteReview class and mapped it to the underlying Review class. Here is the controller code for the Edit.

 public ActionResult Edit(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Review review = db.Reviews.Find(id);
            CompleteReview rev = new CompleteReview();
            rev.ReviewKey = review.ReviewKey;
            rev.ReviewTitle = review.ReviewTitle;
            rev.BookTitle = review.Book.BookTitle;
            rev.BookKey = review.BookKey;
            rev.ReviewerLastName = review.Reviewer.ReviewerLastName;
            rev.ReviewerKey = review.ReviewerKey;
            rev.ReviewDate = review.ReviewDate;
            rev.ReviewRating = review.ReviewRating;
            rev.ReviewText = review.ReviewText;

            if (review == null)
            {
                return HttpNotFound();
            }
            return View(rev);
        }

        // 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 = "ReviewKey,ReviewTitle,ReviewDate,ReviewerKey,BookKey,ReviewRating,ReviewText")] CompleteReview review)
        {
            if (ModelState.IsValid)
            {
               
                Review rev = db.Reviews.Find(review.ReviewKey);
                rev.ReviewTitle= review.ReviewTitle;
                rev.ReviewDate = review.ReviewDate;
                rev.ReviewerKey = review.ReviewerKey;
                rev.BookKey = review.BookKey;
                rev.ReviewRating = review.ReviewRating;
                rev.ReviewText = review.ReviewText;

                db.Entry(rev).State = System.Data.Entity.EntityState.Modified;
                db.SaveChanges();
                return RedirectToAction("Index");
            }
            return View(review);
        }

Here is the Edit view:

@model BookReviewMVCApp.Models.CompleteReview

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>


@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Reviewer</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        @Html.HiddenFor(model => model.ReviewKey)

        <div class="form-group">
            @Html.LabelFor(model => model.ReviewTitle, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.ReviewTitle, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.ReviewTitle, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.ReviewDate, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.ReviewDate, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.ReviewDate, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.BookKey, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.BookKey, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.BookKey, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.ReviewerKey, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.ReviewerKey, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.ReviewerKey, "", new { @class = "text-danger" })
            </div>
        </div>
     


        <div class="form-group">
            @Html.LabelFor(model => model.ReviewRating, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.ReviewRating, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.ReviewRating, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.ReviewText, htmlAttributes: new { @class = "control-label col-md-2" })
           <div class="col-md-10">
            @Html.EditorFor(model => model.ReviewText, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.ReviewText, "", new { @class = "text-danger" })
          </div>
       </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
     </div>
    
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Here are screen shots of the edit at work:

before Edit edit after edit

Final Notes


The last task for our new controller and views is to add the link to the Review on the main page. I also want to note that the Delete does not fully function. It shows the record to delete but does not actually delete it. An issue to be worked on later.

All the code for these three tutorials is on Github at https://github.com/spconger/BookReviewMVC

No comments:

Post a Comment