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":
- Business-oriented model object (introduced in this chapter)
- 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 ofAlbum
object, as we can naviate from an album to associate artist using this propertyfavoriateAlbum.Artist
Artist
is a foreign key property ofAlbum
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
- adds a
- 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 typeDbSet<T>
, whereT
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:
- User click
Edit
link inIndex
view (of StoreManager page) - Edit link sends an HTTP GET request to server (e.g.
StoreManager/Edit/8
) - MVC return a View for edit an album, and user set values for album
- User click
Save
link inEdit
view - Sends an HTTP POST request to server for change data
- 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:
- accept an Album model object with all user's edit inside
- 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 andAlbum
has aTitle
property, it looks for a parameter named "Title" in request
- e.g. For
- 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 clauseTryUpdateModel
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);
}
}