精通MVC3摘译(n)-模型绑定

简介:

Model binding在HTTP请求中,通过浏览器发送的数据创建.NET对象的过程。每次我们定义一个带参数的action方法(参数对象由model binding创建),已经应用了model binding处理。这里我们要展示model binding系统的工作方式,同时为高级应用演示一些自定义的技巧。

理解model binding

设想有如下定义的action方法:

using System;

using System.Web.Mvc;

using MvcApp.Models;

namespace MvcApp.Controllers {

public class HomeController : Controller {

public ViewResult Person(int id) {

// get a person record from the repository

Person myPerson = null; //...retrieval logic goes here...

return View(myPerson);

}

    }

}

我们的action方法在HomeController类中定义,Visual Studio为我们创建的默认路由可以让我们调用action方法。这是默认代码:

routes.MapRoute(

"Default", // Route name

"{controller}/{action}/{id}", // URL with parameters

new { controller = "Home", action = "Index", id = UrlParameter.Optional }

);

当我们接受一个URL请求,比如/Home/Person/23,MVC Framework将映射request的细节,传递正确的值或者对象到我们的action方法。

负责调用action方法的组件,负责在调用action方法前,获取值给参数。默认的action invoker——ControllerActionInvoker,依靠model binder。 model binder由IModelBinder接口定义,如下,IModelBinder接口:

namespace System.Web.Mvc {

public interface IModelBinder {

object BindModel(ControllerContext controllerContext,

ModelBindingContext bindingContext);

}

}

在MVC应用程序中,可以有多个 model binder,每一个 binder可以绑定一个或多个model类型。当action invoker 需要调用action方法时,它会查看model定义的参数,俄日每一个参数类型查找对应的model binder。在上例中,action invoker 将发现action方法有1个int参数,所以它将定位binder去负责绑定int值,同时调用它的BindModel方法。如果没有要操作int的binder,那么就会使用默认的binder。

model binder负责生成合适的action方法参数值,这意味着转换一些request数据的元素,比如form或者query string值。但是MVC Framework不会对如何获得数据做任何限制。我们后面将会展示一些自定义binder的例子,展示你一些ModelBindingContext类的细节,ModelBindingContext会传递给IModelBinder.BindMode方法。

使用默认的 Model Binder

尽管应用程序可以有多个binder,大多数还是仅仅依靠内建的binder类—— DefaultModelBinder。这个binder在action invoker 找不到绑定类型的自定义binder时使用。

默认的,这个model binder为了数据匹配要绑定的参数名,搜寻4个地方,如下所列,DefaultModelBinder搜寻参数数据的顺序:

7060aabaf87543a78ff1c6194ed80ecc

以上位置按顺序查询.比如,如果action方法入上面的例子, For example, in the case of the action method shown in

Listing 17-1, DefaultModelBinder 类检查我们的action方法,看到有一个参数命名是id,那么它会按如下顺序查找值。

1. Request.Form["id"]

2. RouteData.Values["id"]

3. Request.QueryString["id"]

4. Request.Files["id"]

注意:还有一个数据源,在接受JSON数据时候使用。我们会在后面详述。

查询值一旦找到,就停止查询,在我们的例子中,搜索了form数据和路由数据的值,但是由于路由片段带有id,在第2步就被找到,将不会在查找query string和upload file。

注意,你能看到action方法参数的名字的重要性了,参数名字和请求数据项的名字必须匹配,这样的话DefaultModelBinder类才可以找到并使用数据。

绑定简单类型

当处理简单参数类型时,DefaultModelBinder通过使用 System.ComponentModel.TypeDescriptor 尝试转换从请求数据中获得的string值为参数的类型。

如果值不能被转换,比如我们提供的apple作为参数值,但是真正需要的是一个int值,那么DefaultModelBinder将不会绑定到这个model。如果要避免这个问题,我们就需要修改参数。我们可以使用可空类型,如下:

public ViewResult RegisterPerson(int? id) {

如果使用这种方法,id参数的值如果没有匹配,或者数据转换失败的话,会设为null。或者,我们可以在没有有效数据的时候,给参数一个默认值,如下:

public ViewResult RegisterPerson(int id = 23) {

绑定复杂类型

当action方法参数是复杂类型时,换句话就是不能通过TypeConverter类转换的类型,那么DefaultModelBinder 累使用反射来获得公共属性的集合,然后按顺序绑定他们中的每一个。入下面的例子,展示了Person类,我们使用此类来演示model binding。

public class Person {

[HiddenInput(DisplayValue=false)]

public int PersonId { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

[DataType(DataType.Date)]

public DateTime BirthDate { get; set; }

public Address HomeAddress { get; set; }

public bool IsApproved { get; set; }

public Role Role { get; set; }

}

默认的model binder检查此类的属性,看看是否是简单类型。如果是,就在request中查询有相同名字的属性作为数据项,也就是说,FirstName属性会使得binder去查询FirstName数据项。

如果这个属性是另一个复杂类型,那么处理过程会为这个新类型重复此过程。公共属性的集合获得后,binder试着为它们找找值。不同点在于这些属性的名字是嵌套的。比如:Person类的HomeAddress 属性是Address类型的,Address类型如下所示:

public class Address {

public string Line1 { get; set; }

public string Line2 { get; set; }

public string City { get; set; }

public string PostalCode { get; set; }

public string Country { get; set; }

}

当为Line1属性查询值时,model binder为HomeAddress.Line1查询值,换句话说,model对象中属性的名字和属性类型中属性的名字组合了。

创建EASY-TO-BIND HTML

创建遵循命名格式的HTML最简单的方法就是使用templated view helper。在前面已经介绍过了。当我们在一view model是Person类的view中调用 @Html.EditorFor(m => m.FirstName)时,得到如下HTML:

<input class="text-box single-line" id="FirstName" name="FirstName"

type="text" value="Joe" />

当我们调用@Html.EditorFor(m =&gt; m.HomeAddress.Line1),那么得到的是如下代码:

<input class="text-box single-line" id="HomeAddress_Line1" name="HomeAddress.Line1"

type="text" value="123 North Street" />

你可以看到HTML的name属性被自动的设置为model binder查询的值。我们也可以手动创建HTML,但是对于此类工作,我们喜欢templated helpers 的便捷性。

设定自定义前缀

我们可以为默认的model binder设定自定义前缀,如果我们在发送给客户端的HTML中,包含额外的model对象,那么这就很有用了。下面是一个例子,增加额外的view model对象数据到响应:

@using MvcApp.Models;

@model MvcApp.Models.Person

@{

Person myPerson = new Person() {

FirstName = "Jane", LastName = "Doe"

};

}

@using (Html.BeginForm()) {

@Html.EditorFor(m =&gt; myPerson)

@Html.EditorForModel()

<input type="submit" value="Submit" />

}

为了在view中创建和植入的Person对象,我们在view中使用EditorFor方法生成HTML,虽然这会很容易的通过ViewBag传递到view。输入的拉姆达表达式是model对象,(由m表示)但是我们忽略这个,同时返回第二个person对象作为目标呈现。我们也可以调用EditorForModel方法,这样发送给用户的HTML包含2个person对象的数据。

当我面呈现这样对象时,templated view helper在HTML元素的name属性上使用前缀。这就在主view model上分隔了数据。前缀从变量名中提取,myPerson。比如,下面是由View呈现的HTML,FirstName属性。

<input class="text-box single-line" id="myPerson_FirstName" name="myPerson.FirstName"

type="text" value="Jane" />

这个元素的name特性的值,通过对属性名和变量名加前缀创建——myPerson.FirstName。当查询数据的时候,model binder期待这个方法同时使用action方法参数的名字作为可能的前缀。如果我们的表单提交的action方法已经有如下签名:

第一个参数对象将被绑定到未加前缀的数据,第二个会绑定到有参数名作为前缀的数据,即myPerson.FirstName,myPerson.LastName等等。

如果我们不想以这种方法束缚我们的参数名字,那么我们可以通过Bind特性使用自定义前缀。如下代码所示,使用Bind特性知道自定义数据前缀:

public ActionResult Register(Person firstPerson,

[Bind(Prefix="myPerson")] Person secondPerson)

我们对myPerson设置了前缀属性。这意味着默认的 model binder将为数据项使用myPerson作为前缀,即使参数名字是secondPerson。

有选择的绑定属性

设想,Person类的IsApproved属性非常敏感。使用之前介绍的技术,我们可以阻止这属性在HTMLModel中呈现,但是对于恶意的用户,恶意简单的zaiURL后增加 ?IsAdmin=true,然后在提交表单。如果这样做了,model binder将很愉快的在绑定过程使用这个数据。

幸运的是,我们可以使用Bind特性在绑定过程中包括或者排除model属性。要知道仅仅某些属性可以包括,我需要设置Include特性的值,如下代码:

public ActionResult Register([Bind(Include="FirstName, LastName")] Person person) {

上述代码指定了仅有FirstName和LastName属性包括在绑定过程中,Person属性的其他值则被护绿。或者,我们可以指定这些属性为excluded,如下代码:

public ActionResult Register([Bind(Exclude="IsApproved, Role")] Person person) {

上述代码告诉midel binder在绑定过程包含所有的person类的属性,除了IsApproved和Role.

当我们这样使用绑定特性的时候,仅仅应用在一个单一的action方法上,如果我们要应用这个策略到所有的controller的所有的action方法,那么我们在model类上使用Bind特性,如下代码,在Model类上使用Bind特性。

[Bind(Exclude="IsApproved")]

public class Person {

[HiddenInput(DisplayValue=false)]

public int PersonId { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

[DataType(DataType.Date)]

public DateTime BirthDate { get; set; }

public Address HomeAddress { get; set; }

public bool IsApproved { get; set; }

public Role Role { get; set; }

}

绑定到数组和集合

认的model binder能优雅的处理含有相同名字的多个数据项。比如,如下的view,view中的HTML元素有相同的name:

@{

ViewBag.Title = "Movies";

}

Enter your three favorite movies:

@using (Html.BeginForm()) {

@Html.TextBox("movies")

@Html.TextBox("movies")

@Html.TextBox("movies")

<input type=submit />

}

我们使用 Html.TextBox方法创建了3个input元素,name属性都是movies。

<input id="movies" name="movies" type="text" value="" />

<input id="movies" name="movies" type="text" value="" />

<input id="movies" name="movies" type="text" value="" />

我们可以使用如下的action方法接收用户输入的值,代码如下:

[HttpPost]

public ViewResult Movies(List<string> movies) {

...

model binder将查找所有的用户提供的值,在List<string>中,传递他们到Movies action方法。这个binder非常聪明,支持不同的参数类型。我们可以选择接收数据为stringp[]或者甚至是IList<string>。

绑定到用户类型的集合

处理多值绑定的方法非常的优雅,但是如果我们想要它能支持自定义类型,那么我们必须以某种格式处理HTML,下面的代码展示了我们对Person对象数组该如何做:

@model List<MvcApp.Models.Person>

@for (int i = 0; i < Model.Count; i++) {

&lt;h4>Person Number: @i</h4>

@:First Name: @Html.EditorFor(m =&gt; m[i].FirstName)

@:Last Name: @Html.EditorFor(m =&gt; m[i].LastName)

}

这个templated helper生成HTML,在每个属性上用集合中对象的索引号加前缀,如下:

...

<h4>Person Number: 0</h4>

First Name: <input class="text-box single-line" name="[0].FirstName" type="text" value="Joe" />

Last Name: <input class="text-box single-line" name="[0].LastName" type="text" value="Smith" />

<h4>Person Number: 1</h4>

First Name: <input class="text-box single-line" name="[1].FirstName" type="text" value="Jane" />

Last Name: <input class="text-box single-line" name="[1].LastName" type="text" value="Doe" />

...

要绑定这种数据,我们只需要定义带有view model类型集合参数的action方法。如下代码

[HttpPost]

public ViewResult Register(List<Person> people) {

...

因为我们绑定到一个集合,默认的model binder将为加过前缀的Person类的属性查询值。当然,我们不必使用templated helpers老生成HTML。我们可以在view中显式的做,入下面例子演示的那样

<h4>First Person</h4>

First Name: @Html.TextBox("[0].FirstName")

Last Name: @Html.TextBox("[0].LastName")

<h4>Second Person</h4>

First Name: @Html.TextBox("[1].FirstName")

Last Name: @Html.TextBox("[1].LastName")

只要我们确保index值正确的生成,model binder将能找到并且绑定我们所定义的全部元素。

绑定到无索引的集合

另一种定义集合项的方法是使用任意的字符串作为key。这对客户端使用javascript动态增加删除控件很有用而且不需要担心维护索引顺序。

使用这种方法,我们需要定义一个hidden input元素,命名为index,为item指定key,如下面的代码所示:

<h4>First Person</h4>

<input type="hidden" name="index" value="firstPerson"/>

First Name: @Html.TextBox("[firstPerson].FirstName")

Last Name: @Html.TextBox("[firstPerson].LastName")

<h4>Second Person</h4>

<input type="hidden" name="index" value="secondPerson"/>

First Name: @Html.TextBox("[secondPerson].FirstName")

Last Name: @Html.TextBox("[secondPerson].LastName")

我们为input元素的name属性加前缀,以此匹配hidden 索引元素的值。model binder发现index,并且在处理期间,使用它关联数据值。

绑定到字典

默认的model binder能绑定到一个字典,但我们必须遵循一个非常特殊的命名序列。

<h4>First Person</h4>

<input type="hidden" name="[0].key" value="firstPerson"/>

First Name: @Html.TextBox("[0].value.FirstName")

Last Name: @Html.TextBox("[0].value.LastName")

<h4>Second Person</h4>

<input type="hidden" name="[1].key" value="secondPerson"/>

First Name: @Html.TextBox("[1].value.FirstName")

Last Name: @Html.TextBox("[1].value.LastName")

我们绑定到一个字典类Dictionary<string, Person>或者IDictionary<string, Person>,这个字典包含Person对象,我们可以使用如下的action方法接受数据

[HttpPost]

public ViewResult Register(IDictionary<string, Person> people) {

手动调用Model Binding

当一个action方法定义了参数,模型绑定过程就会自动执行,但是我们也能够直接控制绑定过程。对于初始化model对象,数据值在那里获得,怎样处理数据转换错误,我们有了更多的控制能力。

下例演示了action方法手动调用绑定过程。

[HttpPost]

public ActionResult RegisterMember() {

Person myPerson = new Person();

UpdateModel(myPerson);

return View(myPerson);

}

Update方法接受我们之前创建的model对象作为参数,尝试使用标准的绑定过程为它的公共属性获得值,一个手动调用model binding的理由就是能在model对象中支持依赖注入(dependency injection (DI))。下面的例子增加了一个依赖注入到model对象创建。

[HttpPost]

public ActionResult RegisterMember() {

Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));

UpdateModel(myPerson);

return View(myPerson);

}

如演示的那样,这不是仅有的方法引入DI到binding过程。我们在后面还会展示其他方法。

限制对指定数据源的绑定

当我们收到调用绑定过程,我们可以限制绑定过程在但一个的数据源上。默认的,binder查询4个地方,表单数据,路由数据,query string,和任何上传的文件。

下面代码演示了如何限制绑定在表单数据上。

[HttpPost]

public ActionResult RegisterMember() {

Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));

UpdateModel(myPerson, new FormValueProvider(ControllerContext));

return View(myPerson);

}

这个版本的UpdateModel方法接收IValueProvider接口的实现,使之在绑定过程中是仅有的数据源。四个默认数据位置的每一个都有IValueProvider 实现,如下表:ed9cd034ae54435db549ea2bacc47fd5

限制数据源的最常用的方法是仅在form值中查询,我们可以使用一个简洁的绑定方法,意味着不需要创建FormValueProvider的实例。如下演示:

[HttpPost]

public ActionResult RegisterMember(FormCollection formData) {

Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));

UpdateModel(myPerson, formData);

return View(myPerson);

}

FormCollection类实现了IValueProvider接口,如果我们定义了action方法带有这种类型的参数。那么model binder将提供我们一个可以直接传给UpdateModel方法的参数。

通过对UpdateModel方法的重载,允许我们指定一个前缀来查询,也允许我们指定哪个model属性应该被包含在绑定过程中。

处理绑定错误

用户不可避免的会传递不能绑定到model属性的值,比如无效的日期,或者数字文本。当我们显式的调用model binding时,我们对任何诸如此类的错误负责。mdel binder通过抛出InvalidOperationException来表示绑定错误。错误细节可以通过ModelState查看。当使用UpdateModel方法时,我们必须准备捕获异常,使用ModelState将错误信息显示给用户,比如如下代码:

[HttpPost]

public ActionResult RegisterMember(FormCollection formData) {

Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));

try {

UpdateModel(myPerson, formData);

}

catch (InvalidOperationException ex) {

//...provide UI feedback based on ModelState

}

return View(myPerson);

}

另一种方法,我们可以使用TryUpdateModel 方法,如果绑定过程成功,该方法返回true,否则返回false。如下代码:

[HttpPost]

public ActionResult RegisterMember(FormCollection formData) {

Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));

if (TryUpdateModel(myPerson, formData)) {

//...proceed as normal

} else {

//...provide UI feedback based on ModelState

}

}

如果你不喜欢捕获或者处理异常,那么就使用TryUpdateModel 而不是UpdateModel 。在model bingding过程中,是没用任何差别的。

当model binding被自动的调用,绑定错误并不会由异常发信号,我们必须通过ModelState.IsValid属性,自己检查结果。

使用Model Binding接收文件上传

要接收上传的文件,我们要做的就是定义一个action 方法,带有HttpPostedFileBase 类型参数。如下代码演示:

public ActionResult Upload(HttpPostedFileBase file) {

// Save the file to disk on the server

string filename = "myfileName"; // ... pick a filename

file.SaveAs(filename);

// ... or work with the data directly

byte[] uploadedBytes = new byte[file.ContentLength];

file.InputStream.Read(uploadedBytes, 0, file.ContentLength);

// Now do something with uploadedBytes

}

我们已经以指定的格式创建了HTML表单,允许用户上传文件。演示代码如下:

@{

ViewBag.Title = "Upload";

}

<form action="@Url.Action("Upload")" method="post" enctype="multipart/form-data">

Upload a photo: <input type="file" name="photo" />

<input type="submit" />

</form>

关键点是将enctype属性设置为multipart/form-data.如果我们不这样做,浏览器会传送文件名字,而不是文件本身。(这是浏览器工作的方式,而不是MVC Framework的特性)

上述代码,我们使用HTML呈现了form元素,我们也可以使用 Html.BeginForm方法生成这个元素,但是只有通过重载方法才能满足4个参数,所以我们觉得使用纯HTML更加可读。

自定义Model Binding系统

我们已经展示了默认的model binding过程。和你想的一样,还有一些不同的方法使我们自定义binding system。接下来我们会展示几个例子。

创建一个自定义Value Provider

通过自定义Value provider,我们可以把我们自己的数据源加到model binding过程中。Value provider实现了IValueProvider接口,如下面的代码,IValueProvider接口

namespace System.Web.Mvc {

using System;

public interface IValueProvider {

bool ContainsPrefix(string prefix);

ValueProviderResult GetValue(string key);

}

}

ContainsPrefix方法有model binder调用,它决定了value provider是否可以根据提供的前缀处理数据。GetValue方法返回一个有给定的数据key的值,或者provider没有合适的数据的话,则返回null。下列代码展示了绑定了类型为timestamp的CurrentTime属性的provider的值。下面的代码只是一个演示:

public class CurrentTimeValueProvider :IValueProvider {

public bool ContainsPrefix(string prefix) {

    return string.Compare("CurrentTime", prefix, true) == 0;

}

public ValueProviderResult GetValue(string key) {

return ContainsPrefix(key) ? new ValueProviderResult(DateTime.Now, null, CultureInfo.InvariantCulture): null;

}

}

我们想只响应对CurrentTime的请求。当我们得到这样的请求,我们返回DateTime.Now值。对所有的其他请求,则返回null。意味着我们不能提供其他数据。

我们必须返回数据作为 ValueProviderResult类,此类有3个构造参数,第一个是我们想要和请求key关联的数据项。第二个参数用来追踪model binding错误的,不会应用于我们的例子中。最后一个参数是和value相关的cultrue信息。我们设定为InvariantCulture。

要注册我们 value provider ,我们需要创建一个工厂类,创建provider的实例。此类继承与抽象类ValueProviderFactory类。下面显示了一个针对CurrentTimeValueProvider的工厂类。

public class CurrentTimeValueProviderFactory : ValueProviderFactory {

public override IValueProvider GetValueProvider(ControllerContext controllerContext) {

return new CurrentTimeValueProvider();

}

}

当model binder想获得值时,GetValueProvider方法会被调用。我们的实现创建和返回了CurrentTimeValueProvider的实例。

最后一步是注册工厂类,在Global.asax中的Application Start方法中。如下代码:

protected void Application Start() {

AreaRegistration.RegisterAllAreas();

ValueProviderFactories.Factories.Insert(0, new CurrentTimeValueProviderFactory());

RegisterGlobalFilters(GlobalFilters.Filters);

RegisterRoutes(RouteTable.Routes);

}

我们通过增加实例到静态的ValueProviderFactories.Factories集合中注册我们的工厂类、我们之前解释过。model binder按照顺序查遍历value providers。如果我们想让自定义provider优先于内建的那些,那么我们必须使用Insert方法,使我们的工厂在集合的首位,如上述代码所示。如果我们想要我们的provider作为后备,当其他provider不支持数据值的时候才启用,那么需要使用Add方法将工厂类追加到集合。就像这样:

ValueProviderFactories.Factories.Add(new CurrentTimeValueProviderFactory());

我们可以通过定义一个含有DateTime类型参数的action方法,测试我们的provider。如下:

public ActionResult Clock(DateTime currentTime) {

    return Content("The time is " + currentTime.ToLongTimeString());

}

因为我们的value provider是model binder请求数据中的第一个,我们可以提供绑定到这个参数的值

使用ModelBinder特性

The final way of registering a custom model binder is to apply the ModelBinder attribute to the model

class, as shown in Listing 17-36.

最后一个注册自定义model binder的方法是应用ModelBinder特性到model类,如下代码:

[ModelBinder(typeof(PersonModelBinder))]

public class Person {

[HiddenInput(DisplayValue=false)]

public int PersonId { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

[DataType(DataType.Date)]

public DateTime BirthDate { get; set; }

public Address HomeAddress { get; set; }

public bool IsApproved { get; set; }

public Role Role { get; set; }

}

ModelBinder特性中的参数是model binder的类型,此类型在绑定这种类型对象时使用。我们指定我们的自定义PersonModelBinder类。我们趋向于实现IModelBinderProvider接口来处理需求,这让人感觉和MVC Framework设计的其余部分更一致,或者在这种情况下使用 [ModelBinder] 。然而,既然所有的这些技术都导致相同的行为,因此我们不必介意使用哪种方式。
















本文转自cnn23711151CTO博客,原文链接:http://blog.51cto.com/cnn237111/832069,如需转载请自行联系原作者



相关文章
|
8月前
|
存储 设计模式 前端开发
QTChart实现柱状图的mvc模型
QTChart实现柱状图的mvc模型
94 1
|
13天前
|
前端开发 Java PHP
信息系统架构模型(1) MVC
信息系统架构模型(1) MVC
17 0
|
2月前
|
设计模式 前端开发 数据处理
MVC架构中,控制器和模型之间是如何交互的
MVC架构中,控制器和模型之间是如何交互的
12 0
|
2月前
|
存储 设计模式 前端开发
请解释 Web 应用程序的 MVC(模型-视图-控制器)架构。
【2月更文挑战第26天】【2月更文挑战第89篇】请解释 Web 应用程序的 MVC(模型-视图-控制器)架构。
|
5月前
|
前端开发 JavaScript Java
让你了解什么是spring MVC模型数据(附大量代码)
让你了解什么是spring MVC模型数据(附大量代码)
45 0
|
8月前
|
前端开发
MVC模型
MVC模型
34 0
|
10月前
|
开发框架 前端开发 安全
ASP.NET Core MVC 从入门到精通之Html辅助标签补充及模型校验基础
ASP.NET Core MVC 从入门到精通之Html辅助标签补充及模型校验基础
94 0
|
12月前
|
前端开发 网络协议 Java
02.【基础】sdk和runtime区别及让你一睹为快使用CLI在CentOS上快速搭建Console,WebApi,MVC三大应用模型
02.【基础】sdk和runtime区别及让你一睹为快使用CLI在CentOS上快速搭建Console,WebApi,MVC三大应用模型
185 0
|
设计模式 存储 前端开发
Python:设计模式之模型-视图-控制器-MVC复合模式
Python:设计模式之模型-视图-控制器-MVC复合模式
75 0