asp.net core系列 40 Web 应用MVC 介绍与详细示例

一. MVC介绍

MVC架构模式有助于实现关注点分离。视图和控制器均依赖于模型。 但是,模型既不依赖于视图,也不依赖于控制器。 这是分离的一个关键优势。 这种分离允许模型独立于可视化展示进行构建和测试。ASP.NET Core MVC 包括以下功能:

路由、模型绑定、模型验证、依赖关系注入、筛选器、区域、Web API、可测试性、Razor 视图引擎、强类型视图、标记帮助程序、 视图组件。

(1) 路由

ASP.NET Core MVC 建立在 ASP.NET Core 的路由之上,是一个功能强大的 URL 映射组件,可用于生成具有易于理解和可搜索 URL 的应用程序。关于路由知识,请查看asp.net core 系列第5,6章。

(2) 模型绑定(Model)

ASP.NET Core MVC 模型绑定将客户端请求数据(窗体值(form)、路由数据、查询字符串参数、HTTP 头)转换到控制器(Controller)可以处理的对象中。 因此,控制器逻辑不必找出传入的请求数据;它只需具备作为其Action方法的参数的数据。下面的LoginViewModel就是一个模型类。

public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
(3) 模型验证

ASP.NET Core MVC 通过使用数据注释验证属性。 验证属性在值发送到服务端前,在客户端上进行检查。并在调用控制器action前在服务端上进行检查。

using System.ComponentModel.DataAnnotations;
public class LoginViewModel
{
    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [Display(Name = "Remember me?")]
    public bool RememberMe { get; set; }
}

//服务端控制器action验证
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
    //验证模型 
    if (ModelState.IsValid)
    {
      // work with the model
    }
    return View(model);
}
(4) 依赖注入

依赖关系注入除了在控制器上通过构造函数请求所需服务,还可以使用@inject 指令,应用在视图文件上。下面是视图页面上通过依赖注入获取服务对象。

@inject SomeService ServiceName
<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ServiceName.GetTitle</title>
</head>
<body>
    <h1>@ServiceName.GetTitle</h1>
</body>
</html>
(5) 筛选器

筛选器帮助开发者封装,横切关注点,例如异常处理或授权。筛选器允许action方法运行自定义预处理和后处理逻辑,并且可以配置为在给定请求的执行管道内的特定点上运行。筛选器可以作为属性应用于控制器或Action(也可以全局运行)。例如MVC 授权筛选器。

 [Authorize]
    public class AccountController : Controller
(6) 区域

区域用在大型Web开发上, 是功能分组的方法。区域是应用程序内的一个 MVC 结构。 例如,具有多个业务单位(如结账、计费、搜索等)的电子商务应用。每个单位都有自己的逻辑组件视图、控制器和模型。

(7) Web API

除了作为生成网站的强大平台,ASP.NET Core MVC 还对生成 Web API 提供强大的支持。 可以生成可连接大量客户端(包括浏览器和移动设备)的服务,前面章节有讲过。

(8) 可测试性

框架对界面和依赖项注入的使用非常适用于单元测试,并且该框架还包括使得集成测试快速轻松的功能(例如 TestHost 和实体框架的 InMemory 提供程序)

(9) Razor 视图引擎

ASP.NET Core MVC 视图使用 Razor 视图引擎呈现视图。 Razor 是一种紧凑、富有表现力且流畅的模板标记语言,用于使用嵌入式 C# 代码定义视图。 Razor 用于在服务器上动态生成 Web 内容。 可以完全混合服务器代码与客户端内容和代码。例如下面嵌入 C#代码,循环输出5组li标记。

<ul>
  @for (int i = 0; i < 5; i++) {
    <li>List item @i</li>
  }
</ul>
(10) 强类型视图

可以基于模型强类型化 MVC 中的 Razor 视图。 控制器可以将强类型化的模型传递给视图,使视图具备类型检查和 IntelliSense 支持。例如,以下视图呈现类型为 IEnumerable<Product> 的模型:

@model IEnumerable<Product>
<ul>
    @foreach (Product p in Model)
    {
        <li>@p.Name</li>
    }
</ul>
(11) 标记帮助程序

标记帮助程序使服务器端代码可以在 Razor 文件中参与创建和呈现 HTML 元素。 例如,内置 LinkTagHelper 可以用来创建指向 AccountsController控制器中 Login的方法链接

 <p>
         Thank you for confirming your email.
        Please <a asp-controller="Account" asp-action="Login">Click here to Log in</a>.
    </p>
(12) 视图组件

通过视图组件可以包装呈现逻辑并在整个应用程序中重用它。 这些组件类似于分部视图,但具有关联逻辑。

二. 完整示例介绍(项目StudyMVCDemo)

2.1 安装EF数据提供程序

这里使用内存数据库Microsoft.EntityFrameworkCore.InMemory,Entity Framework Core 和内存数据库一起使用, 这对测试非常有用。

PM> Install-Package Microsoft.EntityFrameworkCore.InMemory
2.2 新建数据模型类(POCO )和EF上下文类
public class MvcMovieContext : DbContext
    {
        public MvcMovieContext(DbContextOptions options)
            : base(options)
        {
        }
        public DbSet<Movie> Movie { get; set; }
    }
public class Movie
    {
        public int Id { get; set; }
        public string Title { get; set; }

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }
2.3 初始化数据
public static void Main(string[] args)
        {
            var host = CreateWebHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;

                try
                {
                    //var context = services.GetRequiredService<MvcMovieContext>();
                    //程序运行时,使用EF迁移生成数据,用在关系型数据库
                    //context.Database.Migrate();

                    SeedData.Initialize(services);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred seeding the DB.");
                }
            }
            host.Run();
        }
public static class SeedData
    {
        /// <summary>
        /// 初始化数据
        /// </summary>
        /// <param name="serviceProvider"></param>
        public static  void Initialize(IServiceProvider serviceProvider)
        {
            using (var context = new MvcMovieContext(
                serviceProvider.GetRequiredService<DbContextOptions<MvcMovieContext>>()))
            {
                // 如果有数据返回
                if (context.Movie.Any())
                {
                    return;   // DB has been seeded
                }

                context.Movie.AddRange(
                    new Movie
                    {
                        Title = "When Harry Met Sally",
                        ReleaseDate = DateTime.Parse("1989-2-12"),
                        Genre = "Romantic Comedy",
                        Price = 7.99M
                    },

                    new Movie
                    {
                        Title = "Ghostbusters ",
                        ReleaseDate = DateTime.Parse("1984-3-13"),
                        Genre = "Comedy",
                        Price = 8.99M
                    },

                    new Movie
                    {
                        Title = "Ghostbusters 2",
                        ReleaseDate = DateTime.Parse("1986-2-23"),
                        Genre = "Comedy",
                        Price = 9.99M
                    },

                    new Movie
                    {
                        Title = "Rio Bravo",
                        ReleaseDate = DateTime.Parse("1959-4-15"),
                        Genre = "Western",
                        Price = 3.99M
                    }
                );
                context.SaveChanges();
            }
        }
    }
2.4 添加控制器类(MoviesController)
public class MoviesController : Controller
    {

        private readonly  MvcMovieContext _MvcMovieContext;

        public MoviesController(MvcMovieContext MvcMovieContext)
        {
            this._MvcMovieContext = MvcMovieContext;
        }
    }
2.5 列表页Movies/index.cshtml
// GET: /<controller>/
        public IActionResult Index()
        {
            var movies = _MvcMovieContext.Movie.ToList();
            return View(movies);
        }
@model IEnumerable<StudyMVCDemo.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

启动程序,在浏览器中输入http://localhost:18084/Movies,如下图所示:


上图中菜单布局是在 Views/Shared/_Layout.cshtml 文件中实现的,该_Layout.cshtml页中@RenderBody()是视图页面的占位符。

Views/_ViewStart.cshtml 文件将 Views/Shared/_Layout.cshtml 文件引入到每个视图中。 可以使用 Layout属性设置不同的布局视图,或将它设置为 null,这样将不会使用任何布局文件。后面详细了解布局。

2.6 详细页Movies/ Details.cshtml
/// <summary>
        /// 详细页 
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task<IActionResult> Details(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var movie = await _MvcMovieContext.Movie
                .FirstOrDefaultAsync(m => m.Id == id);
            if (movie == null)
            {
                return NotFound();
            }
            return View(movie);
        }
@model StudyMVCDemo.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>

启动程序,从列表页的超连接Details点击进入,如下图所示:


2.7 编辑页Movies/ Edit.cshtml

对于编辑页有二个action, 一个是Get用来提取数据填充到表单,一个是Post用来提交修改的表单数据。
(1) post中的Bind特性是对需要的属性进行更新。

(2) ValidateAntiForgeryToken特性用于防止请求伪造, 生成的隐藏的 XSRF 标记 Input name="__RequestVerificationToken"。用在Post提交的比如修改和删除功能等。

(3) 模型验证asp-validation-for是指表单Post到服务器之前,客户端验证会检查字段上的任何验证规则。 如果有任何验证错误,则将显示错误消息,并且不会Post表单,内部是输入标记帮助程序使用 DataAnnotations 特性,并在客户端上生成 jQuery 验证所需的 HTML 特性。

public async Task<IActionResult> Edit(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var movie = await _MvcMovieContext.Movie.FindAsync(id);
            if (movie == null)
            {
                return NotFound();
            }
            return View(movie);
        }
        
       [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price")] Movie movie)
        {
            if (id != movie.Id)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                try
                {
                    _MvcMovieContext.Update(movie);
                    await _MvcMovieContext.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException)
                {
                    throw;
                }
                return RedirectToAction("Index");
            }
            return View(movie);
        }
@model StudyMVCDemo.Models.Movie

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ReleaseDate" class="control-label"></label>
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

启动程序,从列表页的Edit点击进入,如下图所示:


2.8 删除
// 删除没有对应的页面,从列表页的Delete点击进入,下面是删除的关键代码
public async Task<IActionResult> DeleteConfirmed(int id)
{
    var movie = await _context.Movie.FindAsync(id);
    _context.Movie.Remove(movie);
    await _context.SaveChangesAsync();
    return RedirectToAction(nameof(Index));
}

参考文献

MVC教程

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容