In this final part of the Movie Inventory ASP.NET MVC Web Application tutorial I show you a simple version of the frontend part and the usage of formerly written UnitOfWork in order to manipulate the database easily.
This last project of the solution will be the user interface, the MovieInventory.UI MVC web application. This time I created it without any authentication.
When the project is ready, add Entity Framework, DTO and BLL as reference then go to NuGet Package Manager and update all the packages. After updating, you have Bootstrap 4. Now you should add log4net package (by Apache Software Foundation) too. This is a very useful and easy-to-use logger package.
The first thing we have to do is to modify the root Web.config file. Let’s add the SQL connection string just before appSettings into a new tag called connectionStrings.
You should also add log4net as new section at the beginning, then a new log4net tag. This tag will contain every configuration for log4net. You can check my Web.config:
Now add a new abstract base controller to the Controllers folder (MVC 5 Controller – Empty). Later all the controllers must be inherited from this one, do not forget!
using log4net; using MovieInventory.BLL; using MovieInventory.BLL.UnitOfWork; using System.Web.Mvc; namespace MovieInventory.UI.Controllers { public abstract class BaseController : Controller { protected readonly ILog Logger; protected readonly IUnitOfWork UnitOfWork; protected BaseController() { Logger = LogManager.GetLogger(GetType().Name); UnitOfWork = Manager.GetUnitOfWork(); } protected override void OnException(ExceptionContext filterContext) { base.OnException(filterContext); Logger.Error(filterContext.Exception.Message, filterContext.Exception); if (filterContext.Exception.InnerException != null) { Logger.Error(filterContext.Exception.InnerException.Message, filterContext.Exception.InnerException); } } } }
Now create a new empty controller, called ErrorPageController.cs and don’t forget the inheritance:
namespace MovieInventory.UI.Controllers { public class ErrorPageController : BaseController { public ActionResult Error(int statusCode, Exception exception) { Response.StatusCode = statusCode; ViewBag.StatusCode = statusCode + " Error"; ViewData.Add("exception", exception.Message); return View(); } } }
This action needs a View, called Error.cshtml in Views/Shared folder. It exists in that folder, so modify the lines to this:
Now open Global.asax.cs file and add a public method called Application_Error. The name is important, because it is a callback method and .NET will know that it should be called if it exists on this name. Also add a new static property for log4net and call its configurator at the beginning of the Application_Start method:
namespace MovieInventory.UI { public class MvcApplication : System.Web.HttpApplication { public static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); protected void Application_Start() { XmlConfigurator.Configure(); AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); } public void Application_Error(object sender, EventArgs e) { Exception ex = Server.GetLastError(); Server.ClearError(); var routeData = new RouteData(); routeData.Values.Add("controller", "ErrorPage"); routeData.Values.Add("action", "Error"); routeData.Values.Add("exception", ex); if (ex.GetType() == typeof(HttpException)) { routeData.Values.Add("statusCode", ((HttpException)ex).GetHttpCode()); } else { routeData.Values.Add("statusCode", 500); } Response.TrySkipIisCustomErrors = true; IController controller = new ErrorPageController(); controller.Execute(new RequestContext(new HttpContextWrapper(Context), routeData)); Response.End(); } } }
Now open BundleConfig file in App_Start folder and add 2 lines at the end of the only method. This could be useful if you decide to add all your script and style files as bundles and don’t want to declare them in _Layout.cshtml file line by line.
public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/bundles/jquery").Include( "~/Scripts/jquery-{version}.js")); bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include( "~/Scripts/jquery.validate*")); bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( "~/Scripts/modernizr-*")); bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include( "~/Scripts/bootstrap.js", "~/Scripts/respond.js")); bundles.Add(new StyleBundle("~/Content/css").Include( "~/Content/bootstrap.css", "~/Content/site.css")); bundles.IgnoreList.Clear(); BundleTable.EnableOptimizations = true; } }
Just for fun let’s create some useful extension methods. Create a folder called Helpers and add a class ExtensionMethods.cs
This class and all its methods must be static ones:
namespace MovieInventory.UI.Helpers { public static class ExtensionMethods { private static readonly ILog Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); public static string GetDbEntityValidationException(this DbEntityValidationException exception) { string message = ""; foreach (DbEntityValidationResult validationErrors in exception.EntityValidationErrors) { foreach (DbValidationError validationError in validationErrors.ValidationErrors) { string followingMessage = $"{validationError.PropertyName}: {validationError.ErrorMessage}"; Logger.Error(followingMessage); message += followingMessage + " "; } } return message; } public static string ToUpperFirstLetter(this string input) { if (!string.IsNullOrEmpty(input.Trim())) { return input.First().ToString().ToUpper() + input.Substring(1); } throw new ArgumentException($"{nameof(input)} cannot be empty!"); } } }
Now if you start the project it should open the starting page and should create a folder called Log in your project directory. This Log folder now contains a file called Inventory.log
This is what we set in Web.config file before.
We should change the starting Layout page to our ones. Let’s call it _MyLayout.cshtml. Also we have to tell the site that this new layout file should be opened when starting. You can do this by editing _ViewStart.cshtml file.
We have to move the script rendering line from the bottom to the top. It should be there, it’s a know issue when using bootstrap, so everytime you create such site like this, do not forget to move it. A bit more complex site may crash and debugging may take days… I know, believe me… 🙂
Here I create the menu part that is responsive if you resize your browser, you’ll see. I also made the footer sticky, so it is always on the bottom where it should be…
I didn’t make any modification on Index.cshtml, it’s your turn. 🙂
Now create all the 4 controllers in Controllers folder: CategoriesController, FormatsController, InventoryController, MoviesController.
Try to always create empty controllers, because Visual Studio may create controller that suits to Entity Framework, but it doesn’t know you would like to use UnitOfWork… 🙁
All our newly created controllers have the following actions:
- Index
- Details
- Create
- Create (HttpPost)
- Edit
- Edit (HttpPost)
- Delete
- DeleteConfirmed (HttpPost)
The actions without HttpPost are HttpGet actions, of course. HttpGet actions only visualize the model and don’t make any work in the unit.
Basically we always return a View() from each action: either we pass a list to the view, or just show the cshtml file as it is or make UnitOfWork then return with the model (a successful HttpPost operation will call the HttpGet action of Index)
The structure of the CategoriesController and FormatsController are the same and simple.
CategoriesController:
namespace MovieInventory.UI.Controllers { public class CategoriesController : BaseController { public ActionResult Index() { return View(UnitOfWork.Category.GetAll()); } public ActionResult Details(int id) { Category category = UnitOfWork.Category.GetById(id); if (category == null) { return HttpNotFound(); } return View(category); } public ActionResult Create() { return View(); } [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Category category) { if (ModelState.IsValid) { UnitOfWork.Category.Insert(category); return RedirectToAction("Index"); } return View(category); } public ActionResult Edit(int id) { Category category = UnitOfWork.Category.GetById(id); if (category == null) { return HttpNotFound(); } return View(category); } [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit(Category category) { if (ModelState.IsValid) { UnitOfWork.Category.Update(category); return RedirectToAction("Index"); } return View(category); } public ActionResult Delete(int id) { Category category = UnitOfWork.Category.GetById(id); if (category == null) { return HttpNotFound(); } return View(category); } [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public ActionResult DeleteConfirmed(int id) { UnitOfWork.Category.Delete(id); return RedirectToAction("Index"); } } }
FormatsController:
namespace MovieInventory.UI.Controllers { public class FormatsController : BaseController { public ActionResult Index() { return View(UnitOfWork.Format.GetAll()); } public ActionResult Details(int id) { Format format = UnitOfWork.Format.GetById(id); if (format == null) { return HttpNotFound(); } return View(format); } public ActionResult Create() { return View(); } [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Format format) { if (ModelState.IsValid) { UnitOfWork.Format.Insert(format); return RedirectToAction("Index"); } return View(format); } public ActionResult Edit(int id) { Format format = UnitOfWork.Format.GetById(id); if (format == null) { return HttpNotFound(); } return View(format); } [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit(Format format) { if (ModelState.IsValid) { UnitOfWork.Format.Update(format); return RedirectToAction("Index"); } return View(format); } public ActionResult Delete(int id) { Format format = UnitOfWork.Format.GetById(id); if (format == null) { return HttpNotFound(); } return View(format); } [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public ActionResult DeleteConfirmed(int id) { UnitOfWork.Format.Delete(id); return RedirectToAction("Index"); } } }
In the other controllers, let’s now skip the Index action and take a look at the Create (HttpPost) action:
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Inventory inventory) { if (!ModelState.IsValid) { string errorMessage = ""; foreach (ModelState modelState in ViewData.ModelState.Values) { foreach (ModelError error in modelState.Errors) { errorMessage += error.ErrorMessage + " "; } } Logger.Error($"Inventory create: {errorMessage}"); ViewBag.MovieList = UnitOfWork.Movie.Query(); ViewBag.FormatList = UnitOfWork.Format.GetAllVisible(); return View(inventory); } Logger.Info($"Inventory create: {inventory.Id}"); UnitOfWork.Inventory.Insert(inventory); return RedirectToAction("Index"); }
Here you can see that we get all the error messages if the model is not valid and put it into the log file. If the model is valid, then we write a simple info into the log and do the UnitOfWork operation. That’s the difference only. MoviesController and InventoryController have logging.
You may recognized the Index() method of Inventory and Movie. What is happening here:
solving a known issue. When you want to show tables with relations, it is not enough to return with the Query() result. First you have to make a new list with anonymous objects, then create strongly typed list of it… You can try by removing the Select(… parts: when seeing the Movie page, you won’t get the category of the movie.
We pass all the lists in ViewBag to the View from the Controller. This is one of the possible solution of passing data from back to the front. You can also use ViewData or TempData.
Here’s a comparsion table from C# Corner:
ViewData | ViewBag | TempData |
It is Key-Value Dictionary collection | It is a type object | It is Key-Value Dictionary collection |
ViewData is a dictionary object and it is property of ControllerBase class | ViewBag is Dynamic property of ControllerBase class. | TempData is a dictionary object and it is property of controllerBase class. |
ViewData is Faster than ViewBag | ViewBag is slower than ViewData | NA |
ViewData is introduced in MVC 1.0 and available in MVC 1.0 and above | ViewBag is introduced in MVC 3.0 and available in MVC 3.0 and above | TempData is also introduced in MVC1.0 and available in MVC 1.0 and above. |
ViewData also works with .net framework 3.5 and above | ViewBag only works with .net framework 4.0 and above | TempData also works with .net framework 3.5 and above |
Type Conversion code is required while enumerating | In depth, ViewBag is used dynamic, so there is no need to type conversion while enumerating. | Type Conversion code is required while enumerating |
Its value becomes null if redirection has occurred. | Same as ViewData | TempData is used to pass data between two consecutive requests. |
It lies only during the current request. | Same as ViewData | TempData only works during the current and subsequent request |
I used 2 different solutions in Index.cshtml files: Category and Format files are generated using table elements (tr, th, td), while Inventory and Movie is generated with div-s. Just showing the difference and usability.
I won’t copy all the codes here, because it would be too much. I’ve uploaded the full solution here:
Movie Inventory (399 downloads)Before you start it in you visual studio do the followings:
- use Visual Studio 2017
- you will need .NET Framework 4.5 installed (or newer)
- at the beginning of the Web.config file replace the SQL server line with yours
- after opened the solution file, right click on the solution, click on Manage NuGet Packages… and repair them. Now your Visual Studio will download all the missing packages
If you have any question or requests according to this project, don’t hesitate to write me.
Have a good coding!