I'm following the Microsoft Contoso University Tutorial to learn MVC.
So far I've created 3 Models
And I have 2 Controllers
I can view a list of students, details about the students including what courses and what grades they have in the courses, edit student details, and add new students.
At this point, I wanted to learn more about routing since I had been using the default Map Route
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
I have a database of students that all have unique last names. I'm aware that doing this is not normally recommended since last names are not unique, but it works for this small learning project. So what I wanted to achieve was to be able to type in /students/{action}/{lastname}
and view details about the student. so I created a new action method in my student controller.
public ActionResult Grab(string studentName)
{
if (studentName == null)
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
var student = db.Students.FirstOrDefault(x => x.LastName.ToLower() == studentName.ToLower());
if (student == null)
return HttpNotFound();
return View(student);
}
And I added a new route in my RouteConfig.cs file
//Custom route
routes.MapRoute(
name: null,
url: "{controller}/{action}/{studentName}",
defaults: new { controller = "Student", action = "Grab", id = UrlParameter.Optional }
);
And finally I created a new view called Grab that shows details about the student.
Now when I type in /student/grab/Norman
it gives me the details of the student with the last name Norman
This is great. But now I have a problem. When I try to use some of my original URLs like /Student/Details/1
they no longer work. I get a 400 error.
The first thing I did was move my default route above the custom route I made
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
//Custom route
routes.MapRoute(
name: null,
url: "{controller}/{action}/{studentName}",
defaults: new { controller = "Student", action = "Grab", id = UrlParameter.Optional }
);
And this fixes that problem but causes a 400 error on my previously working grab route. How can I use both of these at the same time without getting a 400 error?
Here is my full RouteConfig.cs file
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace ContosoUniversity
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
//Custom route
routes.MapRoute(
name: null,
url: "{controller}/{action}/{studentName}",
defaults: new { controller = "Student", action = "Grab", id = UrlParameter.Optional }
);
}
}
}
Here is my entire StudentController.cs file
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 ContosoUniversity.DAL;
using ContosoUniversity.Models;
namespace ContosoUniversity.Controllers
{
public class StudentController : Controller
{
private SchoolContext db = new SchoolContext();
// GET: Student
public ActionResult Index()
{
return View(db.Students.ToList());
}
// GET: Student/Details/5
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
public ActionResult Grab(string studentName)
{
if (studentName == null)
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
var student = db.Students.FirstOrDefault(x => x.LastName.ToLower() == studentName.ToLower());
if (student == null)
return HttpNotFound();
return View(student);
}
// GET: Student/Create
public ActionResult Create()
{
return View();
}
// POST: Student/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 = "LastName, FirstMidName, EnrollmentDate")]Student student)
{
try
{
if (ModelState.IsValid)
{
db.Students.Add(student);
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
}
return View(student);
}
// GET: Student/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
// POST: Student/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, ActionName("Edit")]
[ValidateAntiForgeryToken]
public ActionResult EditPost(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var studentToUpdate = db.Students.Find(id);
if (TryUpdateModel(studentToUpdate, "",
new string[] { "LastName", "FirstMidName", "EnrollmentDate" }))
{
try
{
db.SaveChanges();
return RedirectToAction("Index");
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
}
return View(studentToUpdate);
}
// GET: Student/Delete/5
public ActionResult Delete(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
// POST: Student/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
Student student = db.Students.Find(id);
db.Students.Remove(student);
db.SaveChanges();
return RedirectToAction("Index");
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
}
}
I'll provide any other details needed. Again I want to be able to type in http://localhost:49706/Student/Details/1
or http://localhost:49706/Student/Grab/Alexander
and get the same exact details since Alexander is has the studentID of 1.
You want to differentiate your custom route from the default route url
public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//Custom route
routes.MapRoute(
name: "Students",
url: "Student/{action}/{id}",
defaults: new { controller = "Student", action = "Grab", id = UrlParameter.Optional }
);
//General default route
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Action parameters should also match the route template parameters
// GET: Student/Details/5
public ActionResult Details(int? id) {
if (id == null) {
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var student = db.Students.Find(id);
if (student == null) {
return HttpNotFound();
}
return View(student);
}
public ActionResult Grab(string id) {
if (id == null)
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
var student = db.Students.FirstOrDefault(x => x.LastName.ToLower() == id.ToLower());
if (student == null)
return HttpNotFound();
return View(student);
}
This would now allow the following to be match correctly
Student/Details/1
Student/Grab/Alexander
provided student with last name Alexander has the studentId of 1