模型(Model)– ASP.NET MVC 4 系列

       爲 MVC Music Store 建模

       在 Models 目錄中爲專輯、藝術家、流派建模:程序員

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 Gener Genre { get; set; }
    public virtual Artist Artist { get; set; }
}
public class Artist
{
    public virtual int ArtistId { get; set; }
    public virtual string Name { get; set; }
}
public class Gener
{
    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 和 ArtistId 兩個屬性來管理與之相關的藝術家。這裏的 Artist 屬性稱爲 導航屬性(navigational property),對於一個專輯,能夠經過點操做符來找到與之相關的藝術家web

       ArtistId 屬性爲外鍵屬性(foreign key property),知道一點數據庫知識的話,就會知道藝術家和專輯會被保存在兩個不一樣的表中,而且一個藝術家可能與多個專輯有關聯數據庫

       一個專輯也會有一個相關的流派,一種流派也會對應一個相關專輯的列表。app

 

爲商店管理器構造基架

       新建的 ASP.NET MVC 4 項目會自動包含對實體框架的引用。EF 是一個對象關係映射框架,它不但知道如何在關係型數據庫中保存 .NET 對象,並且還能夠利用 LINQ 查詢語句檢索保存在關係型數據庫中的 .NET 對象。框架

       還記得模型對象中的全部屬性都是虛擬的嗎?虛擬屬性不是必需的,可是它們給 EF 提供一個指向純 C# 類集的鉤子(hook),併爲 EF 啓用了一些特性,如高效的修改跟蹤機制。EF 須要知道模型屬性值的修改時刻,由於它要在這一刻生成並執行一個 SQL UPDATE 語句,使這些改變和數據庫保持一致。ide

       當使用 EF 的代碼優先方法時,須要從 EF 的 DbContext 類派生出的一個類來訪問數據庫。該派生類通常具備多個 DbSet<T> 類型的屬性。性能

 

       右擊 Controller 文件夾,添加控制器,ASP.NET MVC 中的基架能夠爲應用程序的 CRUD 功能生成所需的樣板代碼,選擇新建數據上下文:網站

image

image

 

       基架會在 Models 文件夾中添加 MusicStoreDBContext.cs 文件,此類繼承了實體框架的 DbContext 類。儘管只告知了基架 Album 類,可是它看到了相關的模型並把它們也包含在了上下文中this

public class MusicStoreDBContext : DbContext
{
    // You can add custom code to this file. Changes will not be overwritten.
    // 
    // If you want Entity Framework to drop and regenerate your database
    // automatically whenever you change your model schema, please use data migrations.
    // For more information refer to the documentation:
    // http://msdn.microsoft.com/en-us/data/jj591621.aspx
 
    public MusicStoreDBContext() : base("name=MusicStoreDBContext")
    {
    }
 
    public System.Data.Entity.DbSet<MvcMusicStore.Models.Album> Albums { get; set; }
 
    public System.Data.Entity.DbSet<MvcMusicStore.Models.Artist> Artists { get; set; }
 
    public System.Data.Entity.DbSet<MvcMusicStore.Models.Genre> Genres { get; set; }
 
}

 

       選擇的基架模板也會生成 StoreManagerController 類,並擁有選擇和編輯專輯信息所需的全部代碼。spa

public class StoreManagerController : Controller
{
    private MusicStoreDBContext db = new MusicStoreDBContext();
 
    // GET: /StoreManager/
    public ActionResult Index()
    {
        var albums = db.Albums.Include(a => a.Artist).Include(a => a.Genre);
        return View(albums.ToList());
    }
 
    // more later ...

       Include 方法的調用告知實體框架在加載一個專輯的相關流派和藝術家信息時採用預加載策略(盡其所能使用查詢語句加載全部數據)。

       實體框架另外一種策略是延遲加載(默認)。在這種狀況下,EF 在 LINQ 查詢中只加載主要對象(專輯)的數據,而不填充 Genre 和 Artist 屬性:

var albums = db.Albums;

       延遲加載根據須要來加載相關數據,也就是說,只有當須要 Album 的 Genre 和 Artist 屬性時,EF 纔會經過向數據庫發送一個額外的查詢來加載這些數據。然而不巧的是,當處理專輯信息時,延遲加載策略會強制框架爲列表中的每個專輯向數據庫發送一個額外的查詢。對於含有 100 個專輯的列表,若是要加載全部的藝術家數據,延遲加載則總共須要進行 101 個查詢,其中,第一個查詢是用在 全部 Album 查詢上,另外 100 個查詢是延遲加載策略用來查詢藝術家數據形成的。這就是典型的 「N + 1」問題。延遲加載在帶來便利的同時可能也要付出潛在的性能損失代價

 

       基架運行完成,新的視圖集也出如今了 Views 目錄下。視圖的模型是 Album 對象的枚舉序列。

@model IEnumerable<MvcMusicStore.Models.Album>
 
@{
    ViewBag.Title = "Index";
}
 
<h2>Index</h2>
 
<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Artist.Name)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Genre.Name)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Title)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Price)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.AlbumArtUrl)
        </th>
        <th></th>
    </tr>
 
    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Artist.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.AlbumArtUrl)
            </td>
            <td>
                @Html.ActionLink("Edit", "Edit", new { id = item.AlbumId }) |
                @Html.ActionLink("Details", "Details", new { id = item.AlbumId }) |
                @Html.ActionLink("Delete", "Delete", new { id = item.AlbumId })
            </td>
        </tr>
    }
</table>

       注意,基架是如何選擇全部「重要的」字段顯示給用戶的。換句話說,視圖的表中沒有顯示任何外鍵屬性的值(由於它們對用戶是無心義的),但顯示了相關的藝術家姓名和流派名稱。

image

 

用實體框架建立數據庫

       EF 的代碼優先方法會盡量的使用約定而非配置。若是在運行時不配置一個具體的數據庫鏈接,EF 將按照約定建立一個鏈接。EF 會嘗試鏈接 SQL Server Express 的本地實例。

       建議在生產環境下仍是手動配置數據庫鏈接!在 web.config 文件中添加一個鏈接字符串,該字符串名稱必須與數據上下文類的名稱一致!

<connectionStrings>
  <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\aspnet-MvcMusicStore-20151006055357.mdf;Initial Catalog=aspnet-MvcMusicStore-20151006055357;Integrated Security=True"
    providerName="System.Data.SqlClient" />
  <add name="MusicStoreDBContext" connectionString="Data Source=.; 
    Initial Catalog=MusicStoreDB; Integrated Security=True; MultipleActiveResultSets=True;"
    providerName="System.Data.SqlClient" />
</connectionStrings>

       路由到 Index 操做上,此時會發現本機安裝的 SQL Server 數據庫已建立了新的 DataBase:

image

 

使用數據庫初始化器

       保持數據庫和模型變化同步的一個簡單方法是容許實體框架從新建立一個現有的數據庫。能夠告知 EF 在程序每次啓動時從新建立數據庫或者僅當檢測到模型變化時重建數據庫。當調用 EF 的 Database 類中的靜態方法 SetInitializer 時,能夠選擇這 2 種策略中的任意一個。

       當使用 SetInitializer 方法時,須要向其傳遞一個 IDatabaseInitializer 對象,而框架中有 2 個此對象:DropCreateDatabaseAlways 和 DropCreateDatabaseIfModelChanges。這兩個初始化器都須要一個泛型類型的參數,而且這個參數必須是 DbContext 的派生類。

       假如要在應用程序每次啓動時都從新建立音樂商店的數據庫,那麼在 global.asax.cs 內部,可在應用程序啓動過程當中設置一個初始化器:

protected void Application_Start()
{
    System.Data.Entity.Database.SetInitializer(
        new System.Data.Entity.DropCreateDatabaseAlways<MvcMusicStore.Models.MusicStoreDBContext>() );
 
    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
}

       如今可能極想知道爲何有人想在每次應用程序從新啓動時都要重建數據庫,儘管模型改變了,可是難道不想保留其中的數據嗎?

       這些都是很合理的問題。必須記住,代碼優先方法(如數據庫的初始化器)的特徵是爲應用程序生命週期早期階段的迭代和快速變化提供便利的。一旦發佈一個實際網站而且採用真實的客戶數據,就不能在每次改變模型時從新建立數據庫了。

 

播種數據庫

       假設每次應用程序都會重建數據庫,然而,想讓新建的數據庫中帶有一些流派、藝術家甚至一些專輯,以便在開發應用程序時沒必要輸入數據就可使其進入可用狀態。這樣的情形下,能夠建立一個 DropCreateDatabaseAlways 的派生類並重寫其中的 Seed 方法:

public class MusicStoreDbInitializer : DropCreateDatabaseAlways<MusicStoreDBContext>
{
    protected override void Seed(MusicStoreDBContext context)
    {
        context.Artists.Add(new Artist { Name = "Al Di Meola" });
        context.Genres.Add(new Genre { Name = "Jazz" });
        context.Albums.Add(new Album
        {
            Artist = new Artist { Name = "Rush" },
            Genre = new Genre { Name = "Rock" },
            Price = 9.99m,
            Title = "Caravan"
        });
        base.Seed(context);
    }
}
public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        System.Data.Entity.Database.SetInitializer(
            new MvcMusicStore.Models.MusicStoreDbInitializer());
 
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
    }
}

       此時,每次應用程序重啓以後,在數據庫中都會有 2 種流派,2 個藝術家以及 1 個專輯。

image

image

       看起來彷佛作了不少的工做,可是一旦知道基架可以作的工做,那麼實際的工做量是很是小的,僅須要 3 步:

  1. 實現模型類
  2. 爲控制器和視圖構建基架
  3. 選擇數據庫初始化策略

 

編輯專輯

       點擊 Edit 連接,默認的 MVC 路由規則將 HTTP GET 請求傳遞到相應的操做上:

// GET: /StoreManager/Edit/5
public ActionResult Edit(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Album album = db.Albums.Find(id);
    if (album == null)
    {
        return HttpNotFound();
    }
    ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
    ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
    return View(album);
}

       Find 方法接受的參數是表的主鍵值,如上圖中的 id,真正的表的主鍵 AlbumId 值在 Index 視圖中被賦予了超連接的 id 屬性上:

image

       HttpNotFound() 返回一個 404 響應碼的錯誤。

       編輯頁面以下:

image

       編輯頁面提供給用戶下拉菜單以選擇流派和藝術家:

<div class="col-md-10">
    @Html.DropDownList("GenreId", string.Empty)
    @Html.ValidationMessageFor(model => model.GenreId)
</div>

       控制器是這樣提供數據給視圖以檢索下拉菜單的:

// 參數列表:數據源、valueField、textField、選中項
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);

 

模型和視圖模型終極版

       有 2 種解決方案。基架生成代碼將額外的信息傳遞到 ViewBag 結構中,在模型較爲簡單和緊湊的狀況下,這是一種合理且便於實現的方式。而一些程序員更喜歡經過一個強類型的模型對象獲得全部的模型數據。

       強類型模型的擁護者會選擇第 2 種方案,這個模型可能須要這樣定義:

namespace MvcMusicStore.ViewModel
{
    public class AlbumEditViewModel
    {
        public Album AlbumToEdit { get; set; }
        public SelectList Genres { get; set; }
        public SelectList Artists { get; set; }
    }
}

 

響應編輯時的 POST 請求

       方法名稱仍爲 Edit,不一樣的是它有一個 HttpPost 特性,它接受一個 Album 對象,並保存至數據庫。

// POST: /StoreManager/Edit/5
// 爲了防止「過多發佈」攻擊,請啓用要綁定到的特定屬性,有關 
// 詳細信息,請參閱 http://go.microsoft.com/fwlink/?LinkId=317598。
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include="AlbumId,GenreId,ArtistId,Title,Price,AlbumArtUrl")] Album album)
{
    if (ModelState.IsValid)
    {
        db.Entry(album).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
    ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
    return View(album);
}

 

模型綁定

       Edit 視圖認真的命名了每個表單元素的 name 名稱,例如 Price 值的 input 元素的名稱是 Price 等。

       當操做帶有參數時,MVC 運行環境會使用一個默認的模型綁定器(DefaultModelBinder)來構造這個參數。固然,也能夠爲不一樣的模型註冊多個模型綁定器。在本例中,默認的模型綁定器檢查 Album 類,並查找能用於綁定的全部 Album 屬性。換句話說,當模型綁定器看到 Album 類中具備 Title 屬性時,它就在請求中查找名爲「Title」的參數。注意,這裏說的是請求中,而不是表單集合中。

       模型綁定器使用稱爲值提供器(value provide)的組件在請求的不一樣區域中查找參數值。模型綁定器能夠查看路由數據、查詢字符串、表單集合。另外,也能夠添加自定義的值提供器。

       模型綁定器有點像搜救犬,運行時告知模型綁定器想要知道某個參數值或複合對象的一些屬性值,而後,模型綁定器開始導出查找這些參數。

 

顯式模型綁定

       當操做中有參數時,模型綁定器會隱式的工做。但也可使用控制器中的 UpdateModel 和 TryUpdateModel 方法顯式的調用模型綁定:

[HttpPost]
public ActionResult Edit()
{
    var album = new Album();
    try
    {
        UpdateModel(album);
        db.Entry(album).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    catch 
    {
        ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
        ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
        return View(album);               
    }
}

 

       TryUpdateModel(album) 不會拋出異常,所以也可使用 if else 結構替換上述的 try catch 結構:

if (TryUpdateModel(album))
{
    // Do something...
}
else
{
    // Do something...
}

 

       模型綁定的副產品就是模型狀態。模型綁定器移進模型中的每個值在模型狀態中都有相應的一條記錄,並在綁定後,能夠查看模型狀態以肯定綁定是否成功:

TryUpdateModel(album);
if (ModelState.IsValid)
{
    // Do something...
}
else
{
    // Do something...
}

       若是在模型綁定過程當中出現錯誤,那麼模型狀態將會包含致使綁定失敗的屬性名、嘗試的值、以及錯誤消息。

相關文章
相關標籤/搜索