1 chap4_models
jason.zhu edited this page 2021-06-23 00:19:17 +10:00

Chapter 4: Models

Topic of this topic: (May not be summarized)

  • How to model the Music Store
  • What it means to scaffold
  • How to edit an album
  • All about model binding

MVC design pattern has 2 "model":

  1. Business-oriented model object (introduced in this chapter)
  2. View-specific model object (For creating strong typed view)

This chap focus on business-oriented model object (Represent domain the app focus on) that use it to:

  • Send information to db
  • Perform business calculation
  • Render in a view

Modeling the Music Store

Before creating logic around model, first create model in /Models

/Models/Album.cs

public class Album
{
    public virtual int      AlbumId     { get; set; }
    public virtual int      GenreId     { get; set; }
    public virtual int      ArtistId    { get; set; }
    public virtual string   Title       { get; set; }
    public virtual decimal  Price       { get; set; }
    public virtual string   AlbumArtUrl { get; set; }
    public virtual Genre    Genre       { get; set; }
    public virtual Artist   Artist      { get; set; }
}

/Models/Artist.cs

public class Artist
{
    public virtual int     ArtistId { get; set; }
    public virtual string  Name     { get; set; }
}

/Models/Genre.cs

public class Genre
{
    public virtual int      GenreId     { get; set; }
    public virtual string   Name        { get; set; }
    public virtual string   Description { get; set; }
    public virtual List<Album> Albums   { get; set; }
}
  • Artist is a navigational property of Album object, as we can naviate from an album to associate artist using this property favoriateAlbum.Artist
  • Artist is a foreign key property of Album object, as we maintain artist and album in separate table in db. Each artist can maintain an association with multiple albums using this foreign key property

Scaffolding a Store Manager

After creating 3 models, we can use them to scaffold a controller named StoreManager

What is Scaffolding?

Scaffolding in ASP.NET MVC can generate the boilerplate code you need for create, read, update, and delete (CRUD) functionality in an application. The scaffolding templates can examine the type definition for a model (such as the Album class you've created), and then generate a controller and the controller's associated views. The scaffolding knows how to name controllers, how to name views, what code needs to go in each component, and also knows where to place all these pieces in the project for the application to work.

Scaffolding Options

In VS2019, right click on /Controllers directory and add a controller will spawn a scaffold dialog. VS2019 provide 3 options:

  • Empty Controller:
    • adds a Controller-derived class with specified name.
    • add an Index action
    • No views created
  • Controller with Empty Read/Write Actions:
    • Adds controller class with specified name.
    • add Index, Details, Create, Edit, Delete actions. Actions are not empty, but are not functional.
  • Controller with Read/Write Actions and Views, Using Entity Framework
    • Add controller with specified name.
    • Add Index, Details, Create, Edit, Delete actions, codes to retreieve info from db
    • with corresponding views

Scaffolding and Entity Framework (EF)

EF = an Object-Relational Mapping (ORM) framework, understand how to store/retrieve .NET objects in relational db, given LINQ query

2 style of development using EF:

  • Code First: first writing C# classes and later EF figures out how to create db and store data info in SQL Server
    • Preferred, for new project and during development
  • Database First: Create/Define database first using database schema or VS designer, then write code to interact with defined db
    • For developing on existing db

Models objects defined above are virtual, as it provide hook for EF to look in C# class.

DBContext

How to use EF to interact with DB?

  • Create class drive from System.Data.Entity.DbContext (EF's gateway to db)
  • Derived class public class MusicStoreDB : DbContext will have properties of type DbSet<T>, where T is the object type that want to persist (e.g. Album, Artist)

e.g. of using DBContext: retreive all albums in alphabetical order using LINQ

var db = new MusicStoreDB();
var allAlbums = from album in db.Albums
                orderby album.Title ascending
                select album;

Executing the Scaffolding Template

Using Scaffolding dialog, by choosing correct Model class (i.e. Album (MvcMusicStore.Models)), a set of files will be generated by scaffolding template

Using Database Initializers

EF will connect to SQL server, and connect to database. (either re-create one, or keep existing one depending on app)

2 initializer strategy:

  • Re-create db everytime an app starts (DropCreateDatabaseAlways)
  • Re-create db only when it detects a change in model (DropCreateDatabaseIfModelChanges)

Choose them by edit Global.asax.cs

using System.Data.Entity;
protected void Application_Start()
{
    Database.SetInitialization(new DropCreateDatabaseAlways<MusicStoreDB>()); // For re-create db everytime
    // Database.SetInitialization(new DropCreateDatabaseIfModelChanges<MusicStoreDB>()); // For re-create db only if model changed

    AreaRegistration.RegisterAllAreas();
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}

...???... (Not completed summarized)

Editing an Album

User editing album means:

  1. User click Edit link in Index view (of StoreManager page)
  2. Edit link sends an HTTP GET request to server (e.g. StoreManager/Edit/8)
  3. MVC return a View for edit an album, and user set values for album
  4. User click Save link in Edit view
  5. Sends an HTTP POST request to server for change data
  6. MVC receive POST request and change db

Building a Resource to Edit an Album

Default (scaffolded) action method Edit in StoreManager controller

       // GET: StoreManager/Edit/5
        public ActionResult Edit(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            
            // Extract album model using LINQ, so it will be passed to View
            Album album = db.Albums.Find(id);
            if (album == null)
            {
                return HttpNotFound();
            }

            // Scaffolding generated this, so it can pass data (All artists, and all genreid) to scaffolded view to create dropdown
            ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
            ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
            return View(album);
        }

Models and View Models Redux

Scaffolding generated code pass data using ViewBag or ViewData. Sometime, if you want to include comprehensive information about several models, then View-specific Model (ViewModel) is better.

e.g. AlbumEditViewModel contain both album info and artists info

namespace MvcMusicStore.Models
{
    public class AlbumEditViewModel
    {
        public Album AlbumToEdit { get; set; }
        public System.Web.Mvc.SelectList Genres { get; set; }
        public System.Web.Mvc.SelectList Artists { get; set; }
    }
}

Using this specific ViewModel, Edit action will instantiate AlbumEditViewModel, set object's properties, and pass view model to the view.

Responding to Edit POST Request

User hit send button, will create HTTP POST request, which will be accepted by followinig action

        // POST: StoreManager/Edit/5
        // To protect from overposting attacks, enable the specific properties you want to bind to, for 
        // more details see https://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit([Bind(Include = "AlbumId,GenreId,ArtistId,Title,Price,AlbumArtUrl")] Album album)
        // Accept user's album model as parameter
        {
            if (ModelState.IsValid)
            {   // Happy Path
                // Accept an Album model object from user, save the object into database
                db.Entry(album).State = EntityState.Modified;
                db.SaveChanges();
                return RedirectToAction("Index");
            }

            // Sad Path: rebuild/render edit view
            ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
            ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
            return View(album);
        }

Responsibility of the [HTTPPost] Edit action is:

  1. accept an Album model object with all user's edit inside
  2. save object into database

Two paths:

  • Happy Path: accept user's entry and update database, then return to Index page of StoreManagerController
  • Sad Path: Re-render the page and ask user to re-enter album info

Model Binding

When implementing HTTP POST action method (e.g. Edit), how to extract data?

  • Method 1: pull values directly from request
  • Method 2: If view have named each form input to match with an property, we can use generic code to push values around based on name convention. (i.e. Model binding feature of ASP.NET MVC)

Method 1 e.g. pull values directly from request in MVC3 (Using Web Form) or check Pass Parameter or Query String In ASP.NET MVC

[HttpPost]
public ActionResult Edit()
{
  var album = new Album();
  album.Title = Request.Form["Title"];
  album.Price = Decimal.Parse(Request.QueryString("Price"));
  ...
}

the DefaultModelBinder

Method 2 e.g. instead of digging form values out of request, scaffolded Edit action takes an Album object as a parameter

[HttpPost]
public ActionResult Edit(Album album)
{
  // ...
}
  • MVC runtime uses a model binder to build parameter when controller action method has a parameter.
  • There can be multiple model binders registered in MVC runtime. The default is DefaultModelBinder
  • Following nameing convention (like scaffold), default model binder can automatically convert and move values from request into an album object
    • e.g. For Album object in parameter, default model binder inspects the album (parameter) and finds all album properties avialable for binding. When model binder sees and Album has a Title property, it looks for a parameter named "Title" in request
  • Model binder can look at route data, query string, form collection, and custom value providers (e.g. cache)
  • Model binder work on all HTTP requests like GET, POST, etc. (i.e. parse data from parameter to action)
  • Routing engine find values in URL e.g. id=8 in /StoreManager/Edit/8. But it's model binder that convert and moves value from route data inito parameter

e.g. Model binding for HTTP GET, feed primitive parameter into action

public ActionResult Edit(int id)
{
  // ...
}

Model Binding Security

Pay attention to "over-posting" attack in Chapter 7.

Explicit Model Binding

Model binding work implicitly when there is an action parameter. But we can invoke model binding using UpdateModel & TryUpdaetModel methods in controller

  • UpdateModel will throw exception if model is invalid or model binding has problem, so need try clause
  • TryUpdateModel won't throw exception, it return a boolean value, true if model binding succeeded and model is valid, false if sth went wrong.
[HttpPost]
public ActionResult Edit()
{
  var album = new Album();
  try
  {
    UpdateModel(album); // Use explicit model binding inistead of action parameter, UpdateModel will through exception if model is invalid or model binding has problem
    db.Entry(album).State = EntityState.Modified;
    db.SaveChanges();
    return RedirectToAction("Index");
  }
  catch
  {
    ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
    ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
    return View(album);
  }
}
[HttpPost]
public ActionResult Edit()
{
  var album = new Album();
  if (TryUpdateModel(album))
  {
    db.Entry(album).State = EntityState.Modified;
    db.SaveChanges();
    return RedirectToAction("Index");
  }
  else
  {
    ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
    ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
    return View(album);
  }
}

Model State, byproduct of model binding

  • Every value model binder moves into a model, it records an entry in model state.
  • Model state can be checked anytime after model binding occurs to see if model binding succeeded
  • Any errors occurred during model binding, the ModelState will contains name of properties that caused failure, attempted value, and error message.

e.g. check ModelState

[HttpPost]
public ActionResult Edit()
{
  var album = new Album();

  TryUpdateModel(album);
  if (ModelState.IsValid) // Check model state after update
  {
    db.Entry(album).State = EntityState.Modified;
    db.SaveChanges();
    return RedirectToAction("Index");
  }
  else
  {
    ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
    ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
    return View(album);
  }
}