MVC - 基础

ASP.NET MVC 入门 (Learning ASP.NET MVC)

传统的WebForm发展到如今出现不少的缺陷, 比如为了解决Http的无状态WebForm模式使用了ViewsState来保存客户端和服务端数据,而过量使用则会造成页面臃肿不堪。大量服务端控件的出现使得CSS样式难以控制且不如直接使用html便捷。关于页面生命周期的事件处理代码的底层实现机制非常复杂难以完全掌握、可测试性弱,而ASP.NE MVC框架利用了MVC模式,它的出现很好的解决了WebForm出现的问题。

ASP.NET MVC与ASP.NET Web Form明显的不同点

两者都有HTTP处理程序和HTTP模块(HTTP处理程序用于处理请求,HTTP处理模块可以拦截请求,执行一些其他的逻辑),但有一些明显的不同(只说简单的),这里总结一下

1.ASP.NET WEB FORM使用视图状态造成页面臃肿,而ASP.NET MVC利用的是缓存和会话。

2.ASP.NET WEB FORM的服务器控件要写一大堆代码,而ASP.NET MVC中的HtmlHelper类提供的各种渲染Html的方法,更简洁。

3.ASP.NET WEB FORM的自定义用户控件被ASP.NET MVC的分部视图取代。

4.ASP.NET WEB FORM的视图和逻辑紧耦合,ASP.NET MVC的视图与逻辑是分离的。

ASP.NET MVC的三个主要部分

Model模型:对应于封装在数据库中的数据的.NET类型,程序员需要持续与这些类型所封装的数据进行操作

Views视图:动态生成HTML的模板

Controller控制器:响应客户端请求,使用模型的数据与客户端进行交互

至今的版本特点

MVC - 基础MVC - 基础

ASP.NET MVC框架默认目录结构

MVC - 基础

控制器

控制器类提供了具体的方法针对某一具体的URL请求做出响应和处理,再通过将数据绑定到视图页面后返回给用户。客户端通过URL请求进入路由管道,路由机制会查找由开发人员注册好的路由列表,测试该请求是否有与之匹配的路由,如果有则提取URL中的控制器名称和Action方法名称,再通过DefaultControllerFactory实例化控制器对象,再调用控制器对象的Execute(同步)或BeginExecute(异步)方法执行具体的Action方法。

创建一个MVC项目,命名为ShopMall。

MVC - 基础

Controllers目录存储的是控制器, 右击Controllers目录 - 添加控制器,命名为DefaultController,创建控制器同时会默认为该控制器添加一个Index方法 。

namespace ShopMall.Controllers
{
    // 控制器用于处理客户端请求。
    public class DefaultController : Controller
    {
        // 一个控制器可以包含N个方法,控制器的方法称为Action
        public ActionResult Index()
        {
            ViewData["trace"] = "hello word";
            return View();
        }
    }
}

Views Code

右击Index方法 - 添加视图 - 取消选择使用布局或母版页并点击确定,则会在Views目录下创建一个以控制器名字命名的目录,在该目录下生成了一个以Action 方法命名的的页面,该页面称为视图页面,要创建多个视图则只需要在控制器中添加Action方法,再右击Action方法添加视图即可。

MVC - 基础

打开Viewss目录-Default目录的Index视图,输入如下Razor语法的代码,启动程序,将会看到浏览器看到hello word文本。

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    <div> 
        @ViewData["trace"]
    </div>
</body>
</html>

Views Code

视图

视图负责向用户呈现界面,在Views目录中的视图不能通过向浏览器地址栏输入地址直接访问,它与Action方法关联在一起,一个Action方法关联一个视图。当一个请求发起,到服务端被转化为路由,再由路由调用相应的Action方法处理请求,根据需求,Action方法可能会使用模型对象或其它简单的数据类型,对模型对象或其它简单的数据类型进行处理,接着Action方法会创建对应的视图对象,它调用视图的构造函数时,会将刚才处理好的模型或简单数据作为参数传递进去,视图对象会将这个模型对象转换为适合显示给用户查看的格式。Views目录下的子目录的名称与控制器关联在一起,Views目录的名为Home的子目录关联的就是HomeController,该目录下的那些视图关联的则是HomeController下的Action方法。

渲染视图

public ActionResult Index()
{
    //每个Action方法都靠渲染一个视图返回给用户,通过调用Controller的View方法可以实例化一个视图
    //调用无参的View方法就是实例化一个与当前Action方法所关联的同名视图,这个视图在Views目录中的以当前Action所在的Controller命名的目录之下
    return View();
    //如果需要渲染并返回同一目录的其它视图,则将同一目录的视图名称作为参数传递给View方法,
    return View("Department"); //注意,返回Department视图时是直接返回该视图,它不会进入Department视图所对应的Action方法。
    //注意,VieWData是在当前控制器下的各个视图之间所共享的,也即只有隶属于某个控制器的那些视图才会共享ViewData
}

控制器向视图传递数据

1.使用数据字典对象ViewDataDictionary(ViewsData["key"]= value)

视图具有ViewData属性,是一个字典对象,它是Controller(控制器)和Views(视图)共享的一个属性,在前面的例子中已经使用过这个字典对象, 可以将数据保存在ViewsData中通过键来取出值。它存储的都是object类型,如果你存储的是强类型则需要强转。

ViewData["trace"] = "hello word"; //ViewData作为Controller的成员为其赋值后,可以在视图页面获取它

2.使用ViewBag动态对象(ViewsBag.result=value )

这是Controller(控制器)和Views(视图)共享的一个属性,可直接动态为ViewsBag对象添加属性并赋值,在视图页面存取。ViewsBag因为是动态解析的,所以VS不会提供智能感知功能。

ViewBag.trace = "hello word"; //ViewBag是一个dynamic的动态对象,它作为Controller的成员为其赋值后,可以在视图页面获取它

注意,扩展方法无法识别动态对象,所以把ViewBag对象的属性值(属性也是一个dynamic)传递给Html类的扩展方法是行不通的,除非显示转换或强转。

@Html.TextBox("name",(string)ViewBag.name)

3.使用强类型视图

视图页面具有一个Model的属性,此属性是一个dynamic对象,它与视图的ViewData字典对象关联在一起,而Controller的View方法有7个重载,除了可以向视图构造函数传递一个视图的名称,还可以传递一个强类型的对象,这个对象自动存储在视图的ViewData属性中,你可以在视图页面通过Model属性得到这个对象,此时有两种方式来获取该对象的数据:

方式1:直接遵循dynamic的原则使用Model获取数据

方式2:在视图页面顶部为Model显示指定为其本身的类型(利用@model声明该对象的类型)使视图变成强类型的视图,,这样在视图页面可以直接利用智能感知大纲点出对象的成员以便获取数据。

namespace ShopMall.Controllers
{
    public class Person
    {
        public string Name;
    }     public class DefaultController : Controller
    {
        public ActionResult Index()
        {
            List<Person> personList = new List<Person> {
                new Person{ Name="sam" },
                new Person{ Name="leo" }            
            };
            //向视图传递强类型
            return View(personList);
        }
    }
}

使用方式1获取数据:

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    @foreach (var person in Model)
    {
        <div>@person.Name</div>
    }
</body>

使用方式2获取数据:

<!--在视图页面顶部声明当前视图所要使用的强类型,必须提供强类型所在的命名空间-->
@model IEnumerable<ShopMall.Controllers.Person>
<!--或-->
@using ShopMall.Controllers
@model IEnumerable<Person>
<!--或,在Views目录下的Web.Config中为视图基类注册一些经常使用的命名空间,方便在视图页面必须再声明要使用的类型的命名空间-->
<!--以下是在Views目录的Web.Config中的Pages节点中为视图添加命名空间-->
<pages pageBaseType="System.Web.Mvc.WebViewPage">
    <namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Optimization" />
        <add namespace="System.Web.Routing" />
        <add namespace="MusicStore" />
        <add namespace="ShopMall.Controllers" />
    </namespaces>
</pages>
<!DOCTYPE html> <html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    <div>
        @foreach(Person person in Model)
        {
            <div>@person.Name</div>
        }
    </div>
</body>

模型

模型又称领域模型,是对现实世界的对象、过程和规则的展现,它定义了程序的主体。一个模型实际上就是一个C#对象,它提供了成员来表示现实世界的事物,一般情况下模型都对应了数据库中的表,成员即是表字段。要创建模型可以右击Models目录- 添加类。下面创建了专辑、流派和艺术家的模型,创建完成后先编译一次,接着通过添加带EF操作的基架来生成控制器。注意,Models目录不要存放非模型的类型,因为此目录下的类型都对应着数据库的表结构,如果利用添加带EF操作的基架创建控制器,EF会自动将Models目录下的类型注册到EF数据库上下文对象Dbcontext的DSet<TModel>属性中,如果是首次创建数据库,则EF还会根据模型的结构在数据库创建对应的表。

namespace MusicStore.Models
{
    public class Album
    {
        public int AlbumId { get; set; } //按照默认约定,类名+Id后缀的属性会作为数据库表的主键
        public int GenreId { get; set; }
        public int ArtistId { get; set; }
        public string Title { get; set; }
        public decimal Price { get; set; }
        public string AlbumArtUrl { get; set; }
        //Genre被称为导航属性,因为通过这个属性,你可以导航到与专辑所关联的流派上,这个属性不会成为数据库表的字段,它只为模型提供导航功能
        public virtual Genre Genre { get; set; }
        //Artist被称为导航属性,因为通过这个属性,你可以导航到与专辑所关联的艺术家上,这个属性不会成为数据库表的字段,它只为模型提供导航功能
        public virtual Artist Artist { get; set; }
    }
}
namespace MusicStore.Models
{
    public class Genre
    {
        public int GenreIid { get; set; }
        public  string Name { get; set; }
        public string Description { get; set; }
    }
}
namespace MusicStore.Models
{
    public class Artist
    {
        public virtual int  ArtistId { get; set; }
        public virtual string  Name { get; set; }
    }
}

基架

基架用于自动生成控制器和视图,是提供增删查改的基本代码生成器。右击Controllers目录-添加-控制器,可以看到如下所示的添加基架的选项,如果创建空的控制器则只会自动生成一个控制器和为控制器创建一个Index的Action方法。如果创建带有读写操作的控制器,则会自动生成一个控制器且带有Index、Details、Create、Edit和Delete的Action方法及需要的所有相关视图,如果创建带EF数据操作的控制器,则会自动生成一个控制器且带有Index、Details、Create、Edit和Delete的Action方法及EF数据操作代码和需要的所有相关视图。创建带EF操作的控制器时需要先将Models目录的类型都生成一次才行,否则基架对话框不会显示Models目录下的模型。

MVC - 基础

创建带EF操作的控制器

使用带EF操作的控制器,通过点击+号可以新建一个EF数据操作类,基架会自动创建一个以你指定的名字从EF数据操作上下文派生的类型。

MVC - 基础

这个数据操作上下文在指定的Models目录下被自动创建,上下文对象有一系列的Dbset<T>类型的属性,这种属性的名称是以模型的名称加上s后缀用来表示数据库表对象,T即是模型的类型,可以把模型看成是一条记录,而把Dbset<T>看成是一张表,通过面向对象的方式在Dbset<T>中进行Linq查询。


namespace MusicStore.Models
{
    public class MusicStoreDB : 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 MusicStoreDB() : base("name=MusicStoreDB")
        {
        }         public System.Data.Entity.DbSet<MusicStore.Models.Album> Albums { get; set; }
        public System.Data.Entity.DbSet<MusicStore.Models.Artist> Artists { get; set; }
        public System.Data.Entity.DbSet<MusicStore.Models.Genre> Genres { get; set; }
     //如果后来在Models目录又增加一个Order类,那么只要使用基架指定数据操作上下文,Order类会自动注册在此处,不需要手动输入      public System.Data.Entity.DbSet<MusicStore.Models.Order> Orders{ get; set; }
    }
}

在Web.Config中配置数据库连接字符串

  <connectionStrings>
    <add name="MusicStoreDB" connectionString="Data Source=.; Initial Catalog=MusicStoreDB; Integrated Security=True; MultipleActiveResultSets=True; AttachDbFilename=|DataDirectory|MusicStoreD.mdf"
      providerName="System.Data.SqlClient" />
  </connectionStrings>

运行程序后,EF会自动创建MusicStore数据库(code first模式),在数据库中会自动添加额外的一张表(__MigrationHistory),这张表跟踪模型的状态,如果状态发生改变(比如删除了某个属性或增加了某个属性),EF就会抛出异常,而使用code first方式很可能会在以后频繁修改模型,这样就很容易发生数据库与模型不同步,为此,你需要在初始化数据库时指定应如何创建数据库。为了初始化数据库,需要调用System.Data.Entity.DataBase类的SetInitializer方法,该方法接收四种类型的参数,它们可以指定数据库创建模式:

1.DropCreateDatabaseAlways<DbContext>:无论怎样,删除同名数据库,再重新创建

2.CreateDatabaseIfNotExists<DbContext>:当数据库不存在时,自动创建数据库

3.DropCreateDatabaseIfModelChanges<DbContext>:如果模型发生改变,则先删除同名数据库,再重新创建

4.NullDatabaseInitializer<T>:不做任何操作

你可以利用自定义的从DBContext派生的数据库上下文类型,在其静态构造函数中指定初始化数据库,同时利用以上4种模式指定数据库的创建。也可以在Global文件的app_start事件中初始化数据库,,同时指定数据库创建模式。如果选择第三种方式,那么模型一旦被修改,数据库就会被删除再重建,如果数据库存在数据,数据就会丢失,为此,需要使用数据迁移,参看:数据迁移(Data Migration)

namespace MusicStore.Models
{
    public class MusicStoreDB : 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 MusicStoreDB() : base("name=MusicStoreDB")
        {
        }         //利用静态构造函数,在代码块里使用System.Data.Entity.DataBase类初始化数据库,通过以下三个参数中的任何一个来指定数据库的创建模式
        static MusicStoreDB()
        {
            Database.SetInitializer(new DropCreateDatabaseAlways<MusicStoreDB>()); //无论怎样,删除同名数据库,再重新创建
            Database.SetInitializer(new CreateDatabaseIfNotExists<MusicStoreDB>()); //默认,参数可为null,当数据库不存在时,自动创建数据库   
            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<MusicStoreDB>()); //如果模型发生改变,则先删除同名数据库,再重新创建
        }         public System.Data.Entity.DbSet<MusicStore.Models.Album> Albums { get; set; }
        public System.Data.Entity.DbSet<MusicStore.Models.Artist> Artists { get; set; }
        public System.Data.Entity.DbSet<MusicStore.Models.Genre> Genres { get; set; }
    }
}

向数据库播种数据

通过这种code first模式来创建数据库,你还可以在初始化数据库时向数据库插入一些数据,DropCreateDatabaseAlways<T>、CreateDatabaseIfNotExists<T>、DropCreateDatabaseIfModelChanges<T>都提供了Seed方法,但这个方法受保护,可以采取一个折中的办法,比如创建一个类型,使其从前面三种类型中的任何一个派生,然后调用Seed方法。接着需要将这个类型的实例作为参数传递给Database的SetInitializer方法

namespace MusicStore.Models
{    
    public class MusicStoreDB : 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 MusicStoreDB() : base("name=MusicStoreDB")
        {
        }         //利用静态构造函数,在代码块里使用System.Data.Entity.DataBase类初始化数据库,通过MusicStoreDBInitializer来指定数据库的创建模式
        static MusicStoreDB()
        {
            Database.SetInitializer(new MusicStoreDBInitializer()); 
        }         public System.Data.Entity.DbSet<MusicStore.Models.Album> Albums { get; set; }
        public System.Data.Entity.DbSet<MusicStore.Models.Artist> Artists { get; set; }
        public System.Data.Entity.DbSet<MusicStore.Models.Genre> Genres { get; set; }
    }     public class MusicStoreDBInitializer : DropCreateDatabaseAlways<MusicStoreDB>
    {
        protected override void Seed(MusicStoreDB context)
        {
            context.Artists.AddRange(new List<Artist>
            {
                new Artist{  Name="Madrugada" },
                new Artist{  Name="原声带" },
                new Artist{  Name="Сны моего неба" },
                new Artist{  Name="Mogwai" },
                new Artist{  Name="Talk Talk" },
                new Artist{  Name="July Skies" },
            });             context.Genres.AddRange(new List<Genre>
            {
                new Genre{ Name="Post Rock", Description="后摇滚" },
                new Genre{ Name="OST", Description="电影原声" },
                new Genre{ Name="Japanese opera", Description="戏曲" }
            });             //Album类具有两个导航属性:Artist和Genre
            //也就是说Album表作为外键表引用了两个外键,它们是Artist的id和Genre的id
            //下面插入Album的时候没有指定Artist和Genre的id,因为Artist表和Genre表还没有当前专辑所需要引用的数据
            //此处可以直接new出Artist和Genre,EF会自动先将数据插入Artist和Genre表,接着再自动将这两张表的新记录的id插入到Album表
            context.Albums.AddRange(new List<Album>
            {
                new Album{ Artist=new Artist{  Name="乌仁娜" }, Genre=new Genre{ Name="World", Description="民族" }, Title="Lieder aus dem mongolischen Grasland", Price= },
                new Album{ Artist=new Artist{  Name="群星" }, Genre=new Genre{ Name="Japanese opera", Description="戏曲" }, Title="能劇:石橋", Price= },
            });
            base.Seed(context);
        }
    }
}

Visual Studio自带简易数据库,当创建完模型和EF数据操作上下文之后,运行web程序就会自动创建简易的数据库(不需要安装SQL)

MVC - 基础

模型绑定器

当客户端向服务端提交数据的时候(无论GET、POST),服务端需要调用Request对象N次才能把提交的数据全部取出来,而模型绑定器解决了这个问题,它简化了代码量。模型绑定器与Action方法一起工作,它检查Action方法接收的任何类型的参数,从路由、Url请求、Form表单集合中获取路由数据、查询字符串、表单字段,并将这些数据与Action方法接收的类型进行匹配,如果是强类型则创建一个实例,实例的属性与获取的数据绑定在一起。这样,你可以通过Action方法的参数来获取数据,省去了需要调用N次Request。

传统方式获取提交的数据

//传统方式获取提交的数据
public string Test(FormCollection forms)
{
    return $"{ forms["name"]}{ forms["id"]}" ?? $"{Request["name"]}{ Request["id"]}";
}

隐式调用模型绑定器

隐式调用即不需要显示地把模型绑定方法写出来,只要Action方法带了参数,模型绑定器就会自动对提交的数据进行绑定。

//参数可以是对应表单域中的字段的name
//Url查询字符串的变量名 
//Url路由的非Controller和Action的segment参数变量名(需要在RouteConfig中注册路由模板)
public string Test(string name, string age)
{
    return $"{name}{age}";
}
//参数是强类型时,也同样是利用模型绑定器,无论是GET提交或POST提交,只要提交的数据的变量名能对得上模型的属性名,就能够绑定到模型上
public string Test(Album album)
{
    return $"{album.Title}{album.Price}";
}

显示调用模型绑定器

使用Controller的UpdateModel或TryUpdateModel方法都可以显示调用模型绑定器,显示调用时,不要为Action指定参数,然后在方法内部创建参数变量并将变量作为参数传递给UpdateModel或TryUpdateModel方法。

public string Test( )
{
    Album album = new Album();
    UpdateModel(album); //如果绑定失败会抛出异常,应放入try块
    TryUpdateModel(album);//如果绑定失败会返回false
    return $"{album.Title}{album.Price}";
}

ModelState模型状态字典

模型绑定的状态存储在Controller的ModelState属性中,此属性是一个字典集合,存储模型相关的状态(模型绑定发生错误的信息、成功的信息、客户端提交的更新(即编辑修改)模型的数据)。

//ModelState的属性和方法
IsValid;
//模型实例是否可用 IsReadOnly;
//状态字典集合是否为只读 ModelState["key"].Errors
//根据key取出key对应的错误信息集合
//示例:
string userNameErrorMsg = ModelState["UserName"].Errors[].ErrorMessage; //获取此key对应的错误信息 AddModelError(string key, string errorMsg[Exception ex])
//将模型绑定失败后的错误信息插入模型状态字典中,如果key是空字符,则表示插入模型级别的错误,否则表示插入属性级别的错误 IsValidField(string key)
//测试此key是否有对应的错误信息
//示例:
bool userNameValid = ModelState.IsValidField("UserName"); //测试此key是否有对应的错误信息
public string Test( )
{
    Album album = new Album();
    UpdateModel(album); 
    TryUpdateModel(album);
    if (!ModelState.IsValid) return $"数据绑定失败";
    return $"{album.Title}{album.Price}";
}

过滤模型绑定

1.UpdateModel或TryUpdateModel方法

只有出现在UpdateModel或TryUpdateModel方法的第二个参数数组中的key才会绑定到模型上,其它key会被忽略。

TryUpdateModel(album,new [] { "Title" });//只绑定Title

2.Bind特性(System.Web.Mvc)

为Action方法应用Bind特性可以声明模型需要绑定的字段或不需要绑定的字段。

//Include:绑定的字段白名单,名单中的字段会被绑定
//Exclude:绑定的字段黑名单,名单中的字段不会被绑定
//未被绑定的字段会根据其数据类型设为对应的默认值
[Bind( Include = "Name" )]//只绑定Name
public ActionResult Index(UserLoginInfo user)

Razor语法

Razor视图是以cshtml结尾的文件,传统的aspx页面中<% %>服务端标记被@替代,使用@就是声明这是一段服务端程序。 直接使用@符号作为服务端脚本的起始,@会告诉视图对象,接下来的字符串请当做C#进行处理。

Razor代码块

紧跟在@后的如果是大括号则被视为代码块,代码块里都是C#的代码,它只运算结果,但不输出。

<div>
    @{ string name = "sam"; } //Razor代码块
</div>

Razor代码段

紧跟在@后的如果不是中括号,则表示两种可能:

1.执行流程语句

2.打印变量的值

所以如果@后跟的是流程语句,则表示将执行流程语句

如果@后跟的是变量名,则表示将输出变量值。如果要输出html,则直接写html即可。

//Razor代码段
@if (true)
{
    <div>
        @name //Razor代码段
    </div> 
}

在代码块中输出变量

<div>
    @{ string name = "sam"; @name } //变量name紧跟在@后被视为Razor代码段进行输出
</div>

Javascript使用服务端变量

<script type="text/javascript">
    //将Razor服务端代码用双引号包含起来即可
    var name = "@name";
    alert(name);
    //服务端代码中如果存在字符,字符必须使用双引号而不能使用单引号
    var errorTip = "@ViewData["errorTip"]"; //["errorTip"]使用了双引号,单引号是无效的
    var s = "@{string sss = "hello";}@sss"; //在C#代码中定义了一个sss的变量并输出了该变量
    alert(s);
</script>

HTML辅助方法

视图对象具有Html、Url、Ajax属性,这三个属性都是C#类型,都提供了N多个方法来渲染Html表单元素。视图的Html属性是一个System.Web.Mvc.HtmlHelper<dynamic>,dynamic表示传递给当前视图的模型实例。System.Web.Mvc.HtmlHelper<T>包含了实例方法和扩展方法,智能感知大纲列表中带向下箭头的就是扩展方法。扩展方法也称为辅助方法,它们都定义在System.Web.Mvc.Html命名空间中,此命名空间已经预先在Views目录的WebConfig文件的system.web.webPages.razor配置节中被引入,所以每个视图都可以调用那些扩展方法。

自定义Html扩展方法

using System.Web.Mvc;

namespace WebApplication1.Extetion
{
    public static class HtmlHelperExtention
    {
        public static MvcHtmlString DivBox(this System.Web.Mvc.HtmlHelper htmlHelper)
        {
            return new MvcHtmlString("<div>hello word</div>");
        }
    }
}

在Views目录下的config文件中

<system.web.webPages.razor>
  <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.4.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
  <pages pageBaseType="System.Web.Mvc.WebViewPage">
    <namespaces>
      <add namespace="System.Web.Mvc" />
      <add namespace="System.Web.Mvc.Ajax" />
      <add namespace="System.Web.Mvc.Html" />
      <add namespace="System.Web.Optimization"/>
      <add namespace="System.Web.Routing" />
      <add namespace="WebApplication1" />
      <add namespace="WebApplication1.Extention" />
    </namespaces>
  </pages>
</system.web.webPages.razor>

在视图页面调用

@Html.DivBox()

Html辅助方法的三个行为

1.渲染Html表单元素,具体应该渲染哪种Html元素,是由扩展方法从应用在模型上的某些特性所决定的,扩展方法会自动读取模型上应用的某些特性

2..将模型的数据装入Html标签,视图首次加载时,扩展方法自动从ViewData、ViewBag、View.Model中获取数据,视图非首次加载时,扩展方法从ModelState中获取用户提交的数据。

Html辅助方法的两个参数

Html辅助方法的name参数

所有的辅助方法几乎都提供了一个string类型的名为name的形参,这个参数作用如下:

1.表示渲染出的HTML表单标签的name和id的值(比如input的name和id都以Html辅助方法的第1个参数的字符表示作为值)

2.根据name去获取数据,将数据填充到Html表单元素的Value属性中。数据的获取是由辅助方法将name参数作为key或Property,从ViewData[name]、ViewBag.name、View.Model.name中获取。如果当前视图是用户已经完成编辑后提交到服务端并返回的视图,则辅助方法会从ModelState[name]中获取提交的数据。ModelState[name]的数据就是用户提交用来更新模型的某个属性的新值,不管服务端是否模型绑定成功,提交的数据都会存储在ModelState[name]中,所以,当模型绑定失败(用户输入的数据不合法)时,服务端就原样返回这个View,这样,用户就可以及时在可编辑的表单元素中看到刚才编辑修改后提交的数据了。当模型绑定失败,模型绑定器还会将错误信息插入到ModelState[name].Errors中。

@{ 
  public ActionResult Test()
  {
      ViewBag.Title = "听风的歌";
      return View();
  }
}
@Html.TextBox("Title") //将渲染一个包含name=Title,id=Title的input标签,其value=听风的歌,如果当前视图没有绑定ViewBag或ViewData或View.Model,则只渲染没有value属性的input
@{ 
    public ActionResult Test()
    {
        ViewBag.Album = new Album { Title = "听风的歌", Price =  };
        return View();
    }
}
@Html.TextBox("Album.Price") //将渲染一个包含name=Album_Price,id=Album_Price的input标签,其value=150,如果当前视图没有绑定ViewBag或ViewData或View.Model,则只渲染没有value属性的input,注意,虽然输入框的name不是Price,但提交后,模型绑定器还是可以自动将Name为Album.Price的数据绑定到Action方法所接收的Album模型实例上
@{ 
    public ActionResult Test()
    {
        var album = new Album { Title = "听风的歌", Price =  }; 
        return View(album); //View.Model
    }
}
@Html.TextBox("Price") //将渲染一个包含name=Price,id=Price的input标签,其value=150,如果当前视图没有绑定ViewBag或ViewData或View.Model,则只渲染没有value属性的input

Html辅助方法的HtmlAttribute参数

所有的辅助方法几乎都提供了一个Object类型的名为HtmlAttribute的形参,你可以利用这个对象设置HTML标签的属性和值,对于样式表类名,必须加@符号转义,因为class是C#关键字,如果Html属性名带有-连字符,必须使用下划线代替(输出HTML后会自动还原为连字符),因为C#不能识别连字符的属性名。

示例:
@Html.TextBox("name", "", new { @class = "inputStyle", color = "red", data_validatable = true })

Html辅助方法

传统的表单Html

<form action="/test/index" method="post">
    <input type="text" name="userName" id="userName" /><br /><br />
    <input type="text" name="userPwd" id="userPwd" /><br /><br />

<input type="radio" name="gender" checked="checked" />男&nbsp;&nbsp;
    <input type="radio" name="gender" />女

<input type="checkbox" name="favit" value="1" />音乐
    <input type="checkbox" name="favit" value="2" />图书<br /><br /><br /><br />
    <input type="submit" value="OK" />
</form>

MVC框架的Html辅助方法

如果辅助方法返回的是MvcHtmlString类型,则可以用Razor代码段包裹它,因为可以直接输出,如果不是,则必须用Razor代码块包裹它。

BeginForm()
//创建Post提交的Form起始标签,返回一个MvcForm

EndForm()
//创建Form结束标签,无返回值
//示例:
//一般查询或只读操作最好使用GET请求,方便用户使用浏览器将页面收藏为书签,下次还可以直接访问该页面
@{ Html.BeginForm("Search", "StoreManager", FormMethod.Get,); }
<input type="text" name="Title" />
<input type="submit" value="OK" />
@{ Html.EndForm(); }
//直接使用using语句块可以省略调用EndForm方法
@using (Html.BeginForm("Search", "StoreManager", FormMethod.Get))
{
    <input type="text" name="Title" />
    <input type="submit" value="OK" />
}

TextBox()
//创建文本框标签,返回一个MvcHtmlString

Hidden()
//创建隐藏文本域,返回一个MvcHtmlString

Password()
//创建密码框,返回一个MvcHtmlString

Label()
//创建纯文本标签,返回一个MvcHtmlString
//示例:
@using (Html.BeginForm("Test", "StoreManager", FormMethod.Get))
{
    @Html.Label("male");
    @Html.RadioButton("male", "1");
}
//将生成如下的Html标签,指定的参数是某个Html标签的id,label以这个id为依据以便点击label时可以使某个具有此id的Html获得焦点。比如用Label关联单选按钮、复选按钮和下拉选项
<label for="male">Male</label> //点击label时,单选按钮会被选中,不提供参数则没有for属性,点击label,单选按钮不会选中
<input type="radio" name="male" id="male" value="1" />

TextArea()
//创建多行文本框标签,返回一个MvcHtmlString

RadioButton()
//创建单选按钮标签,返回一个MvcHtmlString
//示例:
@Html.RadioButton("GenreId", "Rock")
@Html.RadioButton("GenreId", "Jazz")
@Html.RadioButton("GenreId", "Word")

CheckBox()
//创建复选按钮标签,,返回一个MvcHtmlString。复选框无论是否选中都会提交到服务端,值都是true或者false。
//示例:
@Html.CheckBox("GenreId", true) Rock
@Html.CheckBox("GenreId", false) Jazz
@Html.CheckBox("GenreId", true) Word

DropDownList()
//创建下拉列表标签,返回一个MvcHtmlString
//示例:
@{
    public ActionResult Test(int AlbumID)
    {
        var album = db.Albums.SingleOrDefault(a => a.AlbumId == AlbumID); 查询某一个专辑
        var genresList = db.Genres.ToList(); 获取所有的流派
        var selectList = new SelectList(genresList, "GenreId", "Name", album.GenreId);
        ViewBag.GenreId = selectList;
        return View(album);
    }
    //SelectList会使用反射,如果不喜欢反射带来的开销,可以直接创建SelectListItem
    public ActionResult Test(int AlbumID)
    {
        var album = db.Albums.SingleOrDefault(a => a.AlbumId == AlbumID);
        var genresList = db.Genres.Select(e => new SelectListItem { Text = e.Name, Value = e.GenreId.ToString(), Selected = e.GenreId == album.GenreId });
        ViewBag.GenreId = genresList;
        return View(album);
    }
}
@Html.DropDownList("GenreId", null, htmlAttributes: null)

ListBox()
//创建多选列表标签,返回一个MvcHtmlString

ValidationSummary(excludePropertyErrors: true)
//如果模型绑定器不能绑定提交的数据(数据格式有误或不能转换为目标C#类型)
//则此方法会渲染一个错误摘要列表(一个用div包裹的ul列表,div的样式类名为:
//validation-summary-errors),参数excludePropertyErrors指示是否不显示属性级别的错误
//此方法要求在服务端为Modelstate模型状态字典调用AddModelError方法指定绑定失败的错误信息
//返回一个MvcHtmlString

ValidationMessage()
//如果模型绑定器不能绑定提交的数据(数据格式有误或不能转换为目标C#类型)
//则此方法会渲染一个错误提示(一个用span包裹的文本提示,span的样式类名为:field-validation-errors)
//此方法要求在服务端为Modelstate模型状态字典调用AddModelError方法指定绑定失败的错误信息,
//ValidationMessage参数1指定表单某个字段的name,参数2是提示的错误信息,参数2可以不指定,
//如果不指定则默认使用服务端的Modelstate的AddModelError方法所指定的错误信息,否则参数2可以覆盖服务端的错误信息
//返回一个MvcHtmlString

Partial(string partialName , ViewDataDictionary data , Model model)
//partialName:分布视图的名称
//data:ViewDataDictionary类型的对象
//model:分布视图使用的模型
//将分布视图渲染到当前视图页面,分布视图可以手动在当前视图的目录中创建 扩展名还是cshtml 只不过里面没有html、head、body标签。
//你可以在分布视图中编写body以下级别的html元素,也可以写Js和服务端脚本。
//除了没有几个主体标签,其它和视图都是一样的。
//可以右击Action - 添加视图 - 选择分布视图
//返回一个MvcHtmlString
//示例:在分部视图编码:
<div style="background:#010067;width:200px;height:100px;padding-top:50px;text-align:center;color:white;">
    <label style="color:red;font-size:20px;font-weight: bolder;vertical-align:middle;">+</label>
    <label style="vertical-align:middle;">这个框里的内容是分布视图TestPartial</label>
</div>
在主页面调用Partial方法加载分部视图
<body>
    <div style="background:#ffd800;width:200px;padding:10px;">
        这是Index视图<br /><br />
        @Html.Partial("TestPartial")
    </div>
</body>

MVC - 基础

RenderPartial()
//与Partial方法有类似的行为
//区别在于Partial是将分布视图作为字符串加入主视图
//而RenderPartial则是将分布式图写入响应输出流
//在性能上RenderPartial要优于前者
//但此方法不返回值,所以使用的时候应在Razor代码块中进行调用

ActionLink()
//创建Html超链接标签,如果不指定controller,则默认使用当前视图所在的目录的名称作为控制器的名称

//如果Action名称是Index,则生成的链接中不会包含index,因为MVC框架认为index是默认的视图,当访问视图所在的控制器时,就会返回index视图
//返回一个MvcHtmlString
//示例:
@Html.ActionLink("主页","index","manager")

RouteLink()
//创建Html超链接标签,与ActionLink类似,只不过方法的参数不同。如果不指定controller,则默认使用当前视图所在的目录的名称作为控制器的名称,返回一个MvcHtmlString
//示例:
@Html.RouteLink("主页", new { action = "index", controller = "manager" })

Action(string actionName , string controllerName , object routeValues)
//调用一个操作(Action) 并以Html形式返回结果,返回一个MvcHtmlString

RenderAction()
//与Action方法有类似的行为
//区别在于Action方法是将服务端的返回结果作为字符串接收
//而RenderAction则是将服务端的返回结果写入响应输出流
//在性能上RenderAction要优于前者,但只有在大量循环迭代时才能分出优劣
//但此方法不返回值,所以使用的时候应在Razor代码块中进行调用

 
Encode(object value)
//将指定的Html标签字符中的标签编码为Html所对应的转义字符,返回一个MvcHtmlString
 
Raw(object value)
//将编码转义过的Html字符转换为Html标签,返回一个MvcHtmlString

AntiForgeryToken( )
//防止跨站请求伪造的加密方法,将自动生成加密cookie和隐藏域,参看:跨站请求伪造

因为CheckBox是针对模型中的bool类型的属性,所以它只能提交true或者false,如果我们希望选中复选框并提交非bool类型的值,比如专辑类型的id,那么可以直接使用<input type="checkBox">,然后在服务端通过数组参数来获取,或者模型的属性是一个数组,用这个属性来创建<input type="checkBox">。

Html模板辅助方法

模板辅助方法会从模型上获取应用在模型上的Html模板特性,以此渲染出由模板特性指定的HTML元素来包裹模型的属性。如果模型上未指定模板,则使用默认的Html模板渲染数据

DisplayName()
//渲染出模型属性的名称或别名,这个名称的文本将不会有任何Html包裹它
//示例:
@Html.DisplayName( "Name" ) //假设模型Person具有一个Name的属性

DisplayNameFor( )
//是DisplayName方法的翻版,具有一样的行为

DisplayNameForModel( )
//渲染出视图所绑定的模型的名称或别名,这个名称的文本将不会有任何Html包裹它
//示例:
Html.DisplayNameForModel() //假设视图绑定了一个模型

Display( )
//渲染出模型属性的值,值包含在div中
//如果模型的属性是一个bool值则渲染出出一个处于禁用状态的复选框,复选框是否选中则取决于bool值是true或者false
//示例:
@{
    public class Person
    {
        public string Name { get; set; }
        public bool IsEmployee { get; set; }
    }

public class MessageController : Controller
    {
        public ActionResult Test( )
        {
            Person p = new Person( );
            p.Name = "sam";
            p.IsEmployee = true;
            return View( p );
        }
    }
}
@Html.Display( "Name" ) //将渲染出:<div>sam</div>
@Html.Display( "IsEmployee" ) //将渲染出:<input type="checkbox" checked="checked" disabled="disabled" name="IsEmployee" id="IsEmployee" />
MVC - 基础

DisplayFor( )
//是Display方法的翻版,具有一样的行为

DisplayForModel( )
//渲染出模型的所有属性的名称或别名和属性的值,属性名称包含在div中,属性的值包含在另一个div中
//如果模型的属性是一个bool值,则该属性的名称包含在div中,,再渲染出一个处于禁用状态的复选框,复选框是否选中则取决于bool值是true或者false
//示例:
@Html.DisplayForModel()

MVC - 基础
//或:
@{
    public class Person
    {
        [DisplayName( "姓名" )]
        public string Name { get; set; }
        [DisplayName( "是否是员工" )]
        public bool IsEmployee { get; set; }
    }
}
@Html.DisplayForModel()
MVC - 基础

Editor( )
//渲染出模型属性的输入框,以便设置属性的值

EditorFor( )
//是Editor方法的翻版,具有一样的行为

EditorForModel( )
//渲染出模型所有属性的输入框,以便用户可编辑属性的值

//总结一下:以Display开头的方法都是只读数据,DisplayName获取名称,Display获取值,DisplayForModel同时获取名称和值
//总结一下:以Editor开头的方法都是可读写的数据,Editor编辑属性的值,EditorForModel编辑模型所有属性的值

Html带For后缀的强类型辅助方法

以上扩展方法几乎都支持For后缀,For后缀的方法不从ViewData或ViewBag中获取数据,它从服务端返回的模型中查找数据(View.Model:视图的Model属性),它的第一个参数是一棵表达式树,表达式树中的lambda表达式的主体部分将作为渲染出的Html元素的name和id,它的第二个参数是Html元素的value。使用For后缀的辅助方法可以通过Lambda表达式接收的类型来点出属性,这样可以增强程序员的编码效率。提供智能感知和编译时检查,而且如果在后来修改了模型的某个属性的名称,那么Visual Studio可以自动修改视图页面中调用的带For后缀的扩展方法中的模型所对应的属性名。

@{
    public ActionResult Test(int AlbumID)
    {
        var album = db.Albums.SingleOrDefault(a => a.AlbumId == AlbumID);
        return View(album);
    }
}
@Html.TextBoxFor(album => album.Title)
@Html.TextBoxFor(album => album.Price)

@{
    public ActionResult Test(int AlbumID)
    {
        var album = db.Albums.SingleOrDefault(a => a.AlbumId == AlbumID);
        var genresList = db.Genres.Select(e => new SelectListItem { Text = e.Name, Value = e.GenreId.ToString(), Selected = e.GenreId == album.GenreId });
        ViewBag.GenreList = genresList;
        return View(album);
    }
}

@foreach (var item in (ViewBag.GenreList as IQueryable<SelectListItem>))
{
    @Html.RadioButtonFor(album => album.GenreId, item.Value)
    @item.Text
    @*CheckBoxFor方法只接收一个能返回bool值的委托,但生成的复选框的name=item.Selected,当提交到服务端进行模型绑定时会因为找不到模型的这个属性而致使模型绑定失败,暂时不知道如何解决*@
    @Html.CheckBoxFor(album => item.Selected, new { name = "GenreId" }) 
    @item.Text
}

@Html.DropDownListFor(album => album.GenreId, ViewBag.GenreList as IEnumerable<SelectListItem>)

DropdownListFor绑定枚举

@Html.DropDownListFor( model => model.City, new SelectList( Enum.GetNames( typeof( Users.Models.Citys ) ), Model != null ? Model.City : Users.Models.Citys.北京 ) )
@Html.DropDownListFor( model => model.City, Enum.GetNames( typeof( Citys ) ).Select( cityName => new SelectListItem { Text=cityName, Value= cityName, Selected = Model != null ? Model.City.ToString() == cityName : cityName=="北京" } ) )

不使用Html辅助方法

直接使用原始的Html代码书写表单也许更容易编码,但表单的各个控件的name无法使用lambda表达式点出来,下面是一个折中的办法。

@{

Func<Expression<Func<User, dynamic>>, string> func = (expro) =>
    {
        string lambdaBody = expro.Body.ToString().Trim("()".ToCharArray());
        return lambdaBody.Substring(lambdaBody.IndexOf(".") + 1);
    };
}

<form action="@Url.Action("index")" method="post">
    <input type="text" name="@func(e=>e.userName)" id="userName" />
    <input type="text" name="@func(e=>e.userPwd)" id="userPwd" />
    <input type="submit" value="OK" />
</form>

URL辅助方法(UrlHelper<T>类的辅助方法)

Action()
//返回超链接的字符串表示 RouteUrl()
//与Action方法有类似的行为,只不过方法的参数不同
@Url.RouteUrl( new { action="Test", controller= "Albums" } ) //print /Albums/Test Content()
//将一个虚拟的、相对的URL转换为应用程序的绝对URL的字符串表示
//示例:
<script type="text/javascript" src="@Url.Content("~/Scripts/jquery-1.10.2.js")"></script>
//从ASP.NET MVC4开始,可以直接写前缀为~号代表根目录了,也即不需要手动调用Content方法
<script type="text/javascript" src="~/Scripts/jquery-1.10.2.js"></script>

Ajax辅助方法(AjaxHelper<T>类的辅助方法)

BeginForm( ActionStringName , AjaxOptions)
//创建Ajax表单,返回一个MvcForm
//ActionStringName:Action名称
//AjaxOptions:AjaxOptions配置对象
//AjaxOptions属性如下
//HttpMethod :可选,Ajax请求的表单提交方式,默认按Post提交
//InsertionMode :可选,一个枚举值。当服务端返回数据时,指定数据呈现在DOM元素里的模式,InsertionMode可能的值如下:
//Replace 使用服务端返回的数据替换掉容器里原来的内容
//InsertAfter 在原来的内容后面插入服务端返回的数据
//InsertBefore 在原来的内容前面插入服务端返回的数据
//UpdateTargetId:指定承载服务端返回的数据的DOM元素的id
//OnSuccess:指定异步请求成功后要执行的Javascript的回调函数
//示例:
@using (Ajax.BeginForm( "SectorManagement", new AjaxOptions { HttpMethod = "Get", InsertionMode = InsertionMode.Replace, UpdateTargetId = "user" } ))
{
    //……
}
ActionLink(string linkName, string ActionName,[object routeValue],AjaxOptions options)
//创建一个发起Ajax请求的超链接,返回一个MvcHtmlString
//示例:
@Ajax.ActionLink( "删除", "Delete", new { id = item.EmpID }, new AjaxOptions { OnSuccess = "AjaxSuccess" } )
//附:Request.IsAjaxRequest //当前请求是否是Ajax请求 

Controller的属性

HttpContext //Http上下文对象
ViewData //数据字典对象
RouteData //路由字典对象
ViewBag //数据动态对象
ModelState //模型字典

Action方法

Action的返回值

返回值可以是整个视图或一个File、Content、JavaScript 等等,这些数据将以一个Html页面为载体返回,所以通常它们都只是用于不刷新页面的表单提交,返回一段js脚本时需要注意,只有ajax提交时,js会成功执行,如果是普通提交,那么即使服务端return了一段js也将是一个空白的文件包含了一行字符串而已,因为没有script标签被加载,所以你看到的只可能是一段js字符串了。

public ActionResult Index( )
{
    return File ( ); // 文件下载
    return Content ( ); // 纯文本
    return Views ( ); // 视图
    return HttpNotFound ( ); // 404错误
    return JavaScript ( ); // 一段可执行的Javascript脚本字符
    return Json ( );  // Json字符,返回被序列化的对象,只需传入对象作为参数,对象会自动被序列化,注意返回给客户端敏感数据时应添加[HttpPost]避免数据劫持
    return Redirect ( );  // 建议使用RedirectToAction、RedirectToRoute、RedirectToRoutePermanent(),这三个方法便于搜索引擎编入索引

几个常用特性

[NonAction]
//表示此方法不是Action方法
[HttpPost]
//该Action只处理POST请求
[ValidateInput( false )]
//不执行表单的有效性或安全性验证,主要用于表单中有类似FCKEditor的文本编辑器,提交时会抛异常。
//对于编辑器的值,使用FormCollection来获取,不要使用Request, 否则就算使用了该特性仍然会抛出异常。

在类库中访问服务端对象

使用HttpContext.Current可获取当前请求中的HttpContext对象,它提供Request、Response、Server、Session、Application等属性。

表单

表单的默认提交方式使Get,提交的表单数据会自动作为URI查询字符串被编码到URI中,除非显示指定提交方式为POST。通常情况下客户端只是发起一个没有报文主体的Http请求,所有的数据都是附加在Url查询字符串中的,而当客户端需要向服务端发送大量数据时,则是通过POST方式将数据放进报文主体中进行发送。Url请求附加的查询字符串被自动编码,它不具有Content-Type,因为Content-Type指的是发送方发送的报文主体的编码方式,所以只有当客户端以POST方式提交数据时才需要指定Content-Type,而Content-Type是由form标签的enctype属性指定的,enctype可能的值如下:

//application/x-www-form-urlencoded 键值对编码,比如name字段的值是sam,则提交的数据为:name:sam,服务端使用索引或key从数组中提取值
//application/json JSON编码,提交的数据是以Json对象的形式提交,比如name字段的值是sam,则提交的数据为:{ name : 'sam' }
//text/html  HTML编码
//text/plain  纯文本编码
//text/xml  XML编码
//image/gif gif编码
//image/jpeg jpg编码
//image/png png编码
//application/xhtml+xml XHTML编码
//application/xml XML编码
//application/atom+xml Atom XML编码
//application/pdf Pdf编码
//application/msword Word编码
//application/octet-stream 二进制流编码(如常见的文件下载)
//encType multipart/form-data 上传文件编码

  

ASP.NET - 学习总目录

上一篇:ReactJS.NET 开发


下一篇:Android -- Android下的NDK开发(一)