提供 CRUD(创建、读取、更新和删除)数据窗体输入支持

Microsoft

下载 PDF

这是免费的 “NerdDinner”应用程序教程 的步骤 5,介绍如何使用 ASP.NET MVC 1 生成小型但完整的 Web 应用程序。

步骤 5 演示如何通过启用对编辑、创建和删除 Dinners 的支持来进一步利用 DinnersController 类。

如果你使用的是 ASP.NET MVC 3,我们建议你遵循入门与 MVC 3MVC 音乐应用商店教程。

NerdDinner 步骤 5:创建、更新、删除窗体方案

我们引入了控制器和视图,并介绍了如何使用它们实现现场晚宴的列表/详细信息体验。 下一步是进一步学习 DinnersController 类,并启用对编辑、创建和删除 Dinners 的支持。

DinnersController 处理的 URL

我们之前向 DinnersController 添加了操作方法,这些方法实现了对两个 URL 的支持: /Dinners/Dinners/Details/[id]

URL 动词 用途
/晚餐/ GET 显示即将举行的晚餐的 HTML 列表。
/Dinners/Details/[id] GET 显示有关特定晚餐的详细信息。

现在,我们将添加操作方法以实现三个附加 URL: /Dinners/Edit/[id]/Dinners/Create/Dinners/Delete/[id]。 这些 URL 将支持编辑现有 Dinners、创建新的 Dinners 和删除 Dinners。

我们将支持 HTTP GET 和 HTTP POST 谓词与这些新 URL 的交互。 对这些 URL 的 HTTP GET 请求将显示数据的初始 HTML 视图 (在“编辑”中填充了 Dinner 数据的窗体,如果为“创建”,则显示空白窗体,在“删除”) 时会显示删除确认屏幕。 对这些 URL 的 HTTP POST 请求将保存/更新/删除 DinnerRepository (中的 Dinner 数据,并从那里保存到数据库) 。

URL 动词 用途
/Dinners/Edit/[id] GET 显示填充有 Dinner 数据的可编辑 HTML 表单。
POST 将特定 Dinner 的窗体更改保存到数据库。
/Dinners/Create GET 显示允许用户定义新 Dinners 的空 HTML 窗体。
POST 创建新的 Dinner 并将其保存在数据库中。
/Dinners/Delete/[id] GET 显示删除确认屏幕。
POST 从数据库中删除指定的 dinner。

编辑支持

让我们从实现“编辑”方案开始。

HTTP-GET 编辑操作方法

我们将首先实现编辑操作方法的 HTTP“GET”行为。 请求 /Dinners/Edit/[id] URL 时,将调用此方法。 我们的实现如下所示:

//
// GET: /Dinners/Edit/2

public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);
    
    return View(dinner);
}

上面的代码使用 DinnerRepository 检索 Dinner 对象。 然后,它使用 Dinner 对象呈现视图模板。 由于我们尚未将模板名称显式传递给 View () 帮助程序方法,因此它将使用基于约定的默认路径来解析视图模板:/Views/Dinners/Edit.aspx。

现在,让我们创建此视图模板。 我们将通过在 Edit 方法中右键单击并选择“添加视图”上下文菜单命令来执行此操作:

创建视图模板以在 Visual Studio 中添加视图的屏幕截图。

在“添加视图”对话框中,我们将指示我们要将 Dinner 对象作为其模型传递给视图模板,并选择自动搭建“编辑”模板的基架:

添加视图以自动搭建编辑模板基架的屏幕截图。

单击“添加”按钮时,Visual Studio 将在“\Views\Dinners”目录中添加新的“Edit.aspx”视图模板文件。 它还会在代码编辑器中打开新的“Edit.aspx”视图模板 - 填充了初始的“Edit”基架实现,如下所示:

代码编辑器中新“编辑视图”模板的屏幕截图。

让我们对生成的默认“编辑”基架进行一些更改,并更新编辑视图模板,使其具有以下内容 (这将删除一些我们不希望公开) 的属性:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Edit: <%=Html.Encode(Model.Title)%>
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Edit Dinner</h2>

    <%=Html.ValidationSummary("Please correct the errors and try again.") %>  
    
    <% using (Html.BeginForm()) { %>

        <fieldset>
            <p>
                <label for="Title">Dinner Title:</label>
                <%=Html.TextBox("Title") %>
                <%=Html.ValidationMessage("Title", "*") %>
            </p>
            <p>
                <label for="EventDate">EventDate:</label>
                <%=Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate))%>
                <%=Html.ValidationMessage("EventDate", "*") %>
            </p>
            <p>
                <label for="Description">Description:</label>
                <%=Html.TextArea("Description") %>
                <%=Html.ValidationMessage("Description", "*")%>
            </p>
            <p>
                <label for="Address">Address:</label>
                <%=Html.TextBox("Address") %>
                <%=Html.ValidationMessage("Address", "*") %>
            </p>
            <p>
                <label for="Country">Country:</label>
                <%=Html.TextBox("Country") %>               
                <%=Html.ValidationMessage("Country", "*") %>
            </p>
            <p>
                <label for="ContactPhone">ContactPhone #:</label>
                <%=Html.TextBox("ContactPhone") %>
                <%=Html.ValidationMessage("ContactPhone", "*") %>
            </p>
            <p>
                <label for="Latitude">Latitude:</label>
                <%=Html.TextBox("Latitude") %>
                <%=Html.ValidationMessage("Latitude", "*") %>
            </p>
            <p>
                <label for="Longitude">Longitude:</label>
                <%=Html.TextBox("Longitude") %>
                <%=Html.ValidationMessage("Longitude", "*") %>
            </p>
            <p>
                <input type="submit" value="Save"/>
            </p>
        </fieldset>
        
    <% } %>
    
</asp:Content>

运行应用程序并请求 “/Dinners/Edit/1” URL 时,将看到以下页面:

“我的 M V C 应用程序”页的屏幕截图。

视图生成的 HTML 标记如下所示。 它是标准 HTML - 带有表单<元素,在按下“保存”<输入 type=“submit”/> 按钮时,该表单>元素对 /Dinners/Edit/1 URL 执行 HTTP POST。 已为每个可编辑属性输出 HTML <输入 type=“text”/> 元素:

生成的 HT M L 标记的屏幕截图。

Html.BeginForm () 和 Html.TextBox () Html 帮助程序方法

我们的“编辑.aspx”视图模板使用多个“Html 帮助程序”方法:Html.ValidationSummary () 、Html.BeginForm () 、Html.TextBox () 和 Html.ValidationMessage () 。 除了为我们生成 HTML 标记外,这些帮助程序方法还提供内置的错误处理和验证支持。

Html.BeginForm () 帮助程序方法

Html.BeginForm () 帮助程序方法是在标记中输出 HTML <表单> 元素的内容。 在 Edit.aspx 视图模板中,你会注意到,在使用此方法时,我们将应用 C# “using” 语句。 左大括号表示窗体内容的开头<,右大括号表示 /form> 元素的<>结尾:

<% using (Html.BeginForm()) { %>

   <fieldset>
   
      <!-- Fields Omitted for Brevity -->
   
      <p>
         <input type="submit" value="Save"/>
      </p>
   </fieldset>
   
<% } %>

或者,如果发现“using”语句方法对于此类方案不自然,则可以使用 Html.BeginForm () 和 Html.EndForm () 组合 () 执行相同的操作:

<% Html.BeginForm();  %>

   <fieldset>
   
      <!-- Fields Omitted for Brevity -->
   
      <p>
          <input type="submit" value="Save"/>
      </p>
   </fieldset>
   
<% Html.EndForm(); %>

在不带任何参数的情况下调用 Html.BeginForm () 将导致它将执行 HTTP-POST 的表单元素输出到当前请求的 URL。 这就是编辑视图生成 <表单 action=“/Dinners/Edit/1” method=“post”> 元素的原因。 如果要发布到其他 URL,也可以将显式参数传递给 Html.BeginForm () 。

Html.TextBox () 帮助程序方法

Edit.aspx 视图使用 Html.TextBox () 帮助程序方法输出 <input type=“text”/> 元素:

<%= Html.TextBox("Title") %>

上述 Html.TextBox () 方法采用单个参数 - 用于指定要输出的输入 type=“text”/> 元素的 <id/name 属性,以及要从中填充文本框值的 model 属性。 例如,传递给编辑视图的 Dinner 对象具有“.NET Futures”的“Title”属性值,因此 Html.TextBox (“Title”) 方法调用输出: <input id=“Title” name=“Title” type=“text” value=“.NET Futures” />

或者,可以使用第一个 Html.TextBox () 参数来指定元素的 ID/名称,然后显式传入值以用作第二个参数:

<%= Html.TextBox("Title", Model.Title)%>

通常,我们需要对输出的值执行自定义格式设置。 内置于 .NET 中的 String.Format () 静态方法对于这些方案很有用。 我们的 Edit.aspx 视图模板使用此模板设置 EventDate 值的格式 (,该值的类型为 DateTime) ,以便它不会显示时间的秒数:

<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>

Html.TextBox () 的第三个参数可用于输出其他 HTML 属性。 下面的代码片段演示如何在输入 type=“text”/> 元素上<呈现其他 size=“30” 属性和 class=“mycssclass”属性。 请注意如何使用“@”字符转义类属性的名称,因为“class”是 C# 中的保留关键字 (keyword) :

<%= Html.TextBox("Title", Model.Title, new { size=30, @class="myclass" } )%>

实现 HTTP-POST 编辑操作方法

我们现在已实现 Edit 操作方法的 HTTP-GET 版本。 当用户请求 /Dinners/Edit/1 URL 时,他们会收到如下所示的 HTML 页面:

用户请求编辑晚餐时的 H T M L 输出的屏幕截图。

按“保存”按钮会导致表单发布到 /Dinners/Edit/1 URL,并使用 HTTP POST 谓词提交 HTML <输入> 表单值。 现在,让我们实现编辑操作方法的 HTTP POST 行为 ,该方法将处理保存 Dinner。

首先,我们将向 DinnersController 添加重载的“Edit”操作方法,该方法具有“AcceptVerbs”属性,指示它处理 HTTP POST 方案:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {
   ...
}

将 [AcceptVerbs] 属性应用于重载的操作方法时,ASP.NET MVC 根据传入的 HTTP 谓词自动处理对相应操作方法的调度请求。 对 /Dinners/Edit/[id] URL 的 HTTP POST 请求将转到上述 Edit 方法,而对 /Dinners/Edit/[id] URL 的所有其他 HTTP 谓词请求将转到我们实现 (的第一个 [AcceptVerbs] Edit 方法,该方法没有属性) 。

侧主题:为什么要通过 HTTP 谓词来区分?
你可能会问 -为什么我们使用单个 URL 并通过 HTTP 谓词区分其行为? 为什么不只使用两个单独的 URL 来处理加载和保存编辑更改? 例如:/Dinners/Edit/[id] 显示初始窗体和 /Dinners/Save/[id] 处理表单帖子以保存它? 发布两个单独的 URL 的缺点是,如果我们发布到 /Dinners/Save/2,然后由于输入错误而需要重新显示 HTML 表单,最终用户最终会在浏览器的地址栏中 (/Dinners/Save/2 URL,因为这是表单发布到) 的 URL。 如果最终用户将此重新显示的页面书签添加到其浏览器收藏夹列表,或复制/粘贴该 URL 并将其通过电子邮件发送给好友,他们最终会保存一个在将来 (不起作用的 URL,因为该 URL 依赖于) 的帖子值。 通过公开单个 URL (,例如:/Dinners/Edit/[id]) 并通过 HTTP 谓词区分其处理方式,最终用户可以安全地为编辑页面添加书签和/或将 URL 发送给其他人。

检索表单 Post 值

有多种方法可以在 HTTP POST“编辑”方法中访问已发布的表单参数。 一个简单的方法是仅使用 Controller 基类上的 Request 属性来访问表单集合并直接检索已发布的值:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    // Retrieve existing dinner
    Dinner dinner = dinnerRepository.GetDinner(id);

    // Update dinner with form posted values
    dinner.Title = Request.Form["Title"];
    dinner.Description = Request.Form["Description"];
    dinner.EventDate = DateTime.Parse(Request.Form["EventDate"]);
    dinner.Address = Request.Form["Address"];
    dinner.Country = Request.Form["Country"];
    dinner.ContactPhone = Request.Form["ContactPhone"];

    // Persist changes back to database
    dinnerRepository.Save();

    // Perform HTTP redirect to details page for the saved Dinner
    return RedirectToAction("Details", new { id = dinner.DinnerID });
}

不过,上述方法有点冗长,尤其是在我们添加错误处理逻辑后。

对于此方案,更好的方法是利用控制器基类上的内置 UpdateModel () 帮助程序方法。 它支持使用传入窗体参数更新我们传递的对象的属性。 它使用反射来确定对象上的属性名称,然后根据客户端提交的输入值自动转换并向其分配值。

可以使用 UpdateModel () 方法通过以下代码简化 HTTP-POST 编辑操作:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    UpdateModel(dinner);

    dinnerRepository.Save();

    return RedirectToAction("Details", new { id = dinner.DinnerID });
}

现在可以访问 /Dinners/Edit/1 URL,并更改 Dinner 的标题:

“编辑晚餐”页的屏幕截图。

单击“保存”按钮时,将执行“编辑”操作的表单发布,更新后的值将保留在数据库中。 然后,我们将重定向到 Dinner (的详细信息 URL,该 URL 将在) 显示新保存的值:

Dinner 详细信息 URL 的屏幕截图。

处理编辑错误

我们当前的 HTTP-POST 实现工作正常 - 出现错误时除外。

当用户在编辑表单时出错时,我们需要确保重新显示窗体,并显示一条信息性错误消息,引导他们修复它。 这包括最终用户发布不正确的输入 (的情况,例如:) 格式错误的日期字符串,以及输入格式有效但违反业务规则的情况。 发生错误时,表单应保留用户最初输入的输入数据,以便他们不必手动重新填充更改。 此过程应根据需要重复多次,直到表单成功完成。

ASP.NET MVC 包括一些不错的内置功能,使错误处理和表单重新显示变得简单。 若要在操作中查看这些功能,请使用以下代码更新 Edit 操作方法:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {

        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {

        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }
}

上面的代码类似于我们以前的实现 ,只不过我们现在在工作周围包装 try/catch 错误处理块。 如果在调用 UpdateModel () 时发生异常,或者当我们尝试保存 DinnerRepository (如果尝试保存的 Dinner 对象由于模型) 中的规则冲突而无效,则会引发异常,则会执行 catch 错误处理块。 在其中,我们循环访问 Dinner 对象中存在的任何规则冲突,并将其添加到 ModelState 对象 (我们将在稍后) 对此进行讨论。 然后重新显示视图。

若要查看此工作,请重新运行应用程序,编辑 Dinner,并将其更改为具有空 Title、EventDate 为“BOGUS”,并使用具有美国国家/地区值的英国电话号码。 按下“保存”按钮时,HTTP POST 编辑方法将无法保存 Dinner (,因为) 存在错误,并会重新显示窗体:

使用 H T T P O S T Edit 方法时由于错误而重新显示窗体的屏幕截图。

我们的应用程序具有不错的错误体验。 输入无效的文本元素以红色突出显示,并且向最终用户显示有关它们的验证错误消息。 表单还会保留用户最初输入的输入数据,这样他们就不必重新填充任何内容。

你可能会问,这是怎么发生的? “标题”、“EventDate”和“ContactPhone”文本框如何以红色突出显示自身,并知道输出最初输入的用户值? 错误消息是如何显示在顶部的列表中的? 好消息是,这并非由 magic 实现,而是因为我们使用了一些内置 ASP.NET MVC 功能,这些功能使输入验证和错误处理方案变得简单。

了解 ModelState 和验证 HTML 帮助程序方法

控制器类具有“ModelState”属性集合,该集合提供一种方法来指示将模型对象传递给视图时存在错误。 ModelState 集合中的错误条目标识问题 (模型属性的名称,例如:“Title”、“EventDate”或“ContactPhone”) ,并允许 (指定用户友好的错误消息,例如:“Title is required”) 。

UpdateModel () 帮助程序方法在尝试将表单值分配给模型对象的属性时遇到错误时,会自动填充 ModelState 集合。 例如,Dinner 对象的 EventDate 属性的类型为 DateTime。 当 UpdateModel () 方法无法在上述方案中为其分配字符串值“BOGUS”时,UpdateModel () 方法向 ModelState 集合添加了一个条目,指示该属性发生了赋值错误。

开发人员还可以编写代码以将错误条目显式添加到 ModelState 集合中,就像我们在“catch”错误处理块中所做的那样,该块根据 Dinner 对象中的活动规则冲突使用条目填充 ModelState 集合:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
    
        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }
}

Html 帮助程序与 ModelState 集成

HTML 帮助程序方法(如 Html.TextBox () )在呈现输出时检查 ModelState 集合。 如果项存在错误,则呈现用户输入的值和 CSS 错误类。

例如,在“编辑”视图中,我们使用 Html.TextBox () 帮助程序方法来呈现 Dinner 对象的 EventDate:

<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>

在错误场景中呈现视图时,Html.TextBox () 方法检查 ModelState 集合,以查看是否存在与 Dinner 对象的“EventDate”属性关联的任何错误。 当它确定存在错误时,它将提交的用户输入 (“BOGUS”) 呈现为值,并将 css error 类添加到 <它生成的 input type=“textbox”/> 标记中:

<input class="input-validation-error"id="EventDate" name="EventDate" type="text" value="BOGUS"/>

可以根据需要自定义 css error 类的外观。 默认 CSS 错误类“input-validation-error”在 \content\site.css 样式表中定义,如下所示:

.input-validation-error
{
    border: 1px solid #ff0000;
    background-color: #ffeeee;
}

此 CSS 规则导致无效输入元素突出显示的原因,如下所示:

突出显示的无效输入元素的屏幕截图。

Html.ValidationMessage () 帮助程序方法

Html.ValidationMessage () 帮助程序方法可用于输出与特定模型属性关联的 ModelState 错误消息:

<%= Html.ValidationMessage("EventDate")%>

上面的代码输出: <span class=“field-validation-error”> 值 'BOGUS' 无效</span>

Html.ValidationMessage () 帮助程序方法还支持第二个参数,该参数允许开发人员替代显示的错误消息:

<%= Html.ValidationMessage("EventDate","*") %>

当 EventDate 属性存在错误时,上述代码输出: <span class=“field-validation-error”>*</span> ,而不是默认错误文本。

Html.ValidationSummary () 帮助程序方法

Html.ValidationSummary () 帮助程序方法可用于呈现摘要错误消息,并附带 <ModelState 集合中所有详细错误消息的 ul><li/></ul> 列表:

ModelState 集合中所有详细错误消息列表的屏幕截图。

Html.ValidationSummary () 帮助程序方法采用可选的字符串参数 ,该参数定义要在详细错误列表上方显示的摘要错误消息:

<%= Html.ValidationSummary("Please correct the errors and try again.") %>

可以选择使用 CSS 来替代错误列表的外观。

使用 AddRuleViolations 帮助程序方法

我们最初的 HTTP-POST Edit 实现在其 catch 块中使用 foreach 语句循环访问 Dinner 对象的规则冲突,并将其添加到控制器的 ModelState 集合:

catch {
        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }

我们可以通过将“ControllerHelpers”类添加到 NerdDinner 项目,使此代码更简洁,并在其中实现一个“AddRuleViolations”扩展方法,该方法将帮助程序方法添加到 ASP.NET MVC ModelStateDictionary 类。 此扩展方法可以封装使用 RuleViolation 错误列表填充 ModelStateDictionary 所需的逻辑:

public static class ControllerHelpers {

   public static void AddRuleViolations(this ModelStateDictionary modelState, IEnumerable<RuleViolation> errors) {
   
       foreach (RuleViolation issue in errors) {
           modelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
       }
   }
}

然后,我们可以更新 HTTP-POST Edit 操作方法,以使用此扩展方法使用晚餐规则冲突填充 ModelState 集合。

完成 Edit 操作方法实现

下面的代码实现了编辑方案所需的所有控制器逻辑:

//
// GET: /Dinners/Edit/2

public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);
    
    return View(dinner);
}

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
    
        ModelState.AddRuleViolations(dinner.GetRuleViolations());

        return View(dinner);
    }
}

Edit 实现的一个好事是,控制器类和视图模板都不需要知道 Dinner 模型正在强制执行的特定验证或业务规则。 我们可以在将来向模型添加其他规则,并且无需对控制器或视图进行任何代码更改即可支持它们。 这使我们能够灵活地在未来通过最少的代码更改轻松改进应用程序要求。

创建支持

我们已完成实现 DinnersController 类的“编辑”行为。 现在,让我们继续对它实现“创建”支持 , 这将使用户能够添加新的 Dinners。

HTTP-GET Create 操作方法

首先,我们将实现 create 操作方法的 HTTP“GET”行为。 当有人访问 /Dinners/Create URL 时,将调用此方法。 实现如下所示:

//
// GET: /Dinners/Create

public ActionResult Create() {

    Dinner dinner = new Dinner() {
        EventDate = DateTime.Now.AddDays(7)
    };

    return View(dinner);
}

上面的代码创建一个新的 Dinner 对象,并将其 EventDate 属性指定为将来的一周。 然后,它呈现基于新 Dinner 对象的视图。 由于我们尚未将名称显式传递给 View () 帮助程序方法,因此它将使用基于约定的默认路径来解析视图模板:/Views/Dinners/Create.aspx。

现在,让我们创建此视图模板。 为此,可以在 Create 操作方法中右键单击并选择“添加视图”上下文菜单命令。 在“添加视图”对话框中,我们将指示我们要将 Dinner 对象传递给视图模板,并选择自动搭建“创建”模板的基架:

添加视图以创建视图模板的屏幕截图。

单击“添加”按钮时,Visual Studio 会将基于基架的新“Create.aspx”视图保存到“\Views\Dinners”目录,并在 IDE 中打开它:

用于编辑代码的 ID 的屏幕截图。

让我们对为我们生成的默认“create”基架文件进行一些更改,并将其修改为如下所示:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
     Host a Dinner
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Host a Dinner</h2>

    <%=Html.ValidationSummary("Please correct the errors and try again.") %>
 
    <% using (Html.BeginForm()) {%>
  
        <fieldset>
            <p>
                <label for="Title">Title:</label>
                <%= Html.TextBox("Title") %>
                <%= Html.ValidationMessage("Title", "*") %>
            </p>
            <p>
                <label for="EventDate">EventDate:</label>
                <%=Html.TextBox("EventDate") %>
                <%=Html.ValidationMessage("EventDate", "*") %>
            </p>
            <p>
                <label for="Description">Description:</label>
                <%=Html.TextArea("Description") %>
                <%=Html.ValidationMessage("Description", "*") %>
            </p>
            <p>
                <label for="Address">Address:</label>
                <%=Html.TextBox("Address") %>
                <%=Html.ValidationMessage("Address", "*") %>
            </p>
            <p>
                <label for="Country">Country:</label>
                <%=Html.TextBox("Country") %>
                <%=Html.ValidationMessage("Country", "*") %>
            </p>
            <p>
                <label for="ContactPhone">ContactPhone:</label>
                <%=Html.TextBox("ContactPhone") %>
                <%=Html.ValidationMessage("ContactPhone", "*") %>
            </p>            
            <p>
                <label for="Latitude">Latitude:</label>
                <%=Html.TextBox("Latitude") %>
                <%=Html.ValidationMessage("Latitude", "*") %>
            </p>
            <p>
                <label for="Longitude">Longitude:</label>
                <%=Html.TextBox("Longitude") %>
                <%=Html.ValidationMessage("Longitude", "*") %>
            </p>
            <p>
                <input type="submit" value="Save"/>
            </p>
        </fieldset>
    <% } 
%>
</asp:Content>

现在,当我们运行应用程序并在浏览器中访问 “/Dinners/Create” URL 时,它将从创建操作实现中呈现如下所示的 UI:

运行应用程序并访问 Dinners URL 时创建操作实现的屏幕截图。

实现 HTTP-POST Create 操作方法

我们已实现 Create 操作方法的 HTTP-GET 版本。 当用户单击“保存”按钮时,它会对 /Dinners/Create URL 执行表单发布,并使用 HTTP POST 谓词提交 HTML <输入> 表单值。

现在,让我们实现创建操作方法的 HTTP POST 行为。 首先,我们将向 DinnersController 添加重载的“Create”操作方法,该方法具有“AcceptVerbs”属性,指示它处理 HTTP POST 方案:

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create() {
    ...
}

可通过多种方式在启用了 HTTP-POST 的“创建”方法中访问已发布的表单参数。

一种方法是创建新的 Dinner 对象,然后使用 UpdateModel () 帮助程序方法 (,就像使用编辑操作) 一样,使用已发布的表单值填充它。 然后,我们可以将其添加到 DinnerRepository,将其保存到数据库,并将用户重定向到详细信息操作,以使用以下代码显示新创建的 Dinner:

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create() {

    Dinner dinner = new Dinner();

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Add(dinner);
        dinnerRepository.Save();

        return RedirectToAction("Details", new {id=dinner.DinnerID});
    }
    catch {
    
        ModelState.AddRuleViolations(dinner.GetRuleViolations());

        return View(dinner);
    }
}

或者,我们可以使用一种方法,其中 Create () 操作方法将 Dinner 对象作为方法参数。 ASP.NET MVC 将自动为我们实例化一个新的 Dinner 对象,使用表单输入填充其属性,并将其传递给操作方法:

//
//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Dinner dinner) {

    if (ModelState.IsValid) {

        try {
            dinner.HostedBy = "SomeUser";

            dinnerRepository.Add(dinner);
            dinnerRepository.Save();

            return RedirectToAction("Details", new {id = dinner.DinnerID });
        }
        catch {        
            ModelState.AddRuleViolations(dinner.GetRuleViolations());
        }
    }
    
    return View(dinner);
}

上述操作方法通过检查 ModelState.IsValid 属性来验证 Dinner 对象是否已成功填充表单 post 值。 如果 (输入转换问题,则返回 false:EventDate 属性) 的“BOGUS”字符串,如果存在任何问题,我们的操作方法将重新显示窗体。

如果输入值有效,则操作方法会尝试添加新的 Dinner 并将其保存到 DinnerRepository。 它将此工作包装在 try/catch 块中,如果存在任何业务规则冲突 (会导致 dinnerRepository.Save () 方法引发异常) ,它将重新显示表单。

若要查看操作中的此错误处理行为,我们可以请求 /Dinners/Create URL 并填写有关新 Dinner 的详细信息。 不正确的输入或值将导致重新显示创建窗体,并突出显示错误,如下所示:

重新显示的窗体的屏幕截图,其中突出显示了错误。

请注意,创建表单遵循与编辑表单完全相同的验证和业务规则。 这是因为我们的验证和业务规则是在模型中定义的,并且未嵌入到应用程序的 UI 或控制器中。 这意味着我们以后可以在单个位置更改/改进验证或业务规则,并在整个应用程序中应用这些规则。 我们无需更改 Edit 或 Create 操作方法中的任何代码即可自动遵循任何新规则或修改现有规则。

当我们修复输入值并再次单击“保存”按钮时,我们对 DinnerRepository 的添加将成功,并将新的 Dinner 添加到数据库。 然后,我们将重定向到 /Dinners/Details/[id] URL - 我们将在此处看到有关新创建的 Dinner 的详细信息:

新创建的晚餐的屏幕截图。

删除支持

现在,让我们向 DinnersController 添加“删除”支持。

HTTP-GET 删除操作方法

首先,我们将实现删除操作方法的 HTTP GET 行为。 当有人访问 /Dinners/Delete/[id] URL 时,将调用此方法。 下面是实现:

//
// HTTP GET: /Dinners/Delete/1

public ActionResult Delete(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (dinner == null)
         return View("NotFound");
    else
        return View(dinner);
}

操作方法尝试检索要删除的 Dinner。 如果 Dinner 存在,它将基于 Dinner 对象呈现视图。 如果对象不存在 (或已被删除) 它返回一个视图,该视图呈现我们之前为“Details”操作方法创建的“NotFound”视图模板。

可以通过在 Delete 操作方法中右键单击并选择“添加视图”上下文菜单命令来创建“删除”视图模板。 在“添加视图”对话框中,我们将指示我们将 Dinner 对象作为其模型传递给视图模板,并选择创建一个空模板:

将“删除视图”模板创建为空模板的屏幕截图。

单击“添加”按钮时,Visual Studio 将在“\Views\Dinners”目录中为我们添加新的“Delete.aspx”视图模板文件。 我们将向模板添加一些 HTML 和代码,以实现删除确认屏幕,如下所示:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Delete Confirmation:  <%=Html.Encode(Model.Title) %>
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>
        Delete Confirmation
    </h2>

    <div>
        <p>Please confirm you want to cancel the dinner titled: 
           <i> <%=Html.Encode(Model.Title) %>? </i> 
        </p>
    </div>
    
    <% using (Html.BeginForm()) {  %>
        <input name="confirmButton" type="submit" value="Delete" />        
    <% } %>
     
</asp:Content>

上面的代码显示要删除的 Dinner 的标题,并输出一个 <表单元素,如果最终用户单击其中的“删除”按钮,该表单> 元素对 /Dinners/Delete/[id] URL 执行 POST。

当我们运行应用程序并访问有效 Dinner 对象的 “/Dinners/Delete/[id]” URL 时,它会呈现如下所示的 UI:

H T P G E T Delete 操作方法中晚餐删除确认 U I 的屏幕截图。

附带主题:我们为什么要执行 POST?
你可能会问 - 为什么我们在 <“删除”确认屏幕中创建表单> ? 为什么不只使用标准超链接链接到执行实际删除操作的操作方法? 这是因为我们希望小心翼翼地防范 Web 爬网程序和搜索引擎发现我们的 URL,并在它们跟踪链接时无意中导致数据被删除。 基于 HTTP-GET 的 URL 被视为“安全”,可供其访问/爬网,并且不应遵循 HTTP-POST。 一个很好的规则是确保始终将破坏性或数据修改操作置于 HTTP-POST 请求后面。

实现 HTTP-POST 删除操作方法

现在,我们实现了删除操作方法的 HTTP-GET 版本,该方法会显示删除确认屏幕。 当最终用户单击“删除”按钮时,它将对 /Dinners/Dinner/[id] URL 执行表单发布。

现在,让我们使用以下代码实现 delete 操作方法的 HTTP“POST”行为:

// 
// HTTP POST: /Dinners/Delete/1

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Delete(int id, string confirmButton) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (dinner == null)
        return View("NotFound");

    dinnerRepository.Delete(dinner);
    dinnerRepository.Save();

    return View("Deleted");
}

Delete 操作方法的 HTTP-POST 版本尝试检索要删除的 dinner 对象。 如果它找不到它 (因为它已被删除) 它呈现我们的“NotFound”模板。 如果找到 Dinner,则会将其从 DinnerRepository 中删除。 然后,它呈现“已删除”模板。

若要实现“已删除”模板,我们将右键单击操作方法并选择“添加视图”上下文菜单。 我们将将视图命名为“Deleted”,将其命名为空模板 (,而不采用强类型模型对象) 。 然后,我们将向其添加一些 HTML 内容:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Dinner Deleted
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Dinner Deleted</h2>

    <div>
        <p>Your dinner was successfully deleted.</p>
    </div>
    
    <div>
        <p><a href="/dinners">Click for Upcoming Dinners</a></p>
    </div>
    
</asp:Content>

现在,当我们运行应用程序并访问有效 Dinner 对象的 “/Dinners/Delete/[id]” URL 时,它将呈现晚餐删除确认屏幕,如下所示:

H T P O S T Delete 操作方法中“晚餐删除确认”屏幕的屏幕截图。

单击“删除”按钮时,它将对 /Dinners/Delete/[id] URL 执行 HTTP-POST,这将从数据库中删除 Dinner,并显示“已删除”视图模板:

已删除视图模板的屏幕截图。

模型绑定安全性

我们讨论了使用 ASP.NET MVC 内置模型绑定功能的两种不同方法。 第一个使用 UpdateModel () 方法更新现有模型对象的属性,第二个使用 ASP.NET MVC 支持将模型对象作为操作方法参数传入。 这两种技术都非常强大且非常有用。

这种权力也带来了责任。 接受任何用户输入时,始终对安全性持偏执态度很重要,在将对象绑定到表单输入时也是如此。 应注意始终对用户输入的任何值进行 HTML 编码,以避免 HTML 和 JavaScript 注入攻击,并注意 SQL 注入攻击 (注意:我们将为应用程序使用 LINQ to SQL,该应用程序会自动对参数进行编码,以防止这些类型的攻击) 。 不应仅依赖客户端验证,并始终使用服务器端验证来防范试图向你发送虚假值的黑客。

使用 ASP.NET MVC 的绑定功能时,要确保考虑的另一个安全项是绑定对象的范围。 具体而言,你需要确保你了解允许绑定的属性的安全含义,并确保只允许那些真正应由最终用户更新的属性。

默认情况下,UpdateModel () 方法将尝试更新模型对象上与传入表单参数值匹配的所有属性。 同样,默认情况下,作为操作方法参数传递的对象也可以通过表单参数设置其所有属性。

按使用情况锁定绑定

可以通过提供可更新的属性的显式“包含列表”,根据使用情况锁定绑定策略。 为此,可将额外的字符串数组参数传递给 UpdateModel () 方法,如下所示:

string[] allowedProperties = new[]{ "Title","Description", 
                                    "ContactPhone", "Address",
                                    "EventDate", "Latitude", 
                                    "Longitude"};
                                    
UpdateModel(dinner, allowedProperties);

作为操作方法参数传递的对象还支持 [Bind] 属性,该属性允许指定允许的属性的“包含列表”,如下所示:

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create( [Bind(Include="Title,Address")] Dinner dinner ) {
    ...
}

基于类型锁定绑定

还可以按类型锁定绑定规则。 这允许你指定绑定规则一次,然后让它们应用于所有方案, (包括 UpdateModel 和操作方法参数方案,) 跨所有控制器和操作方法。

可以通过将 [Bind] 属性添加到类型中或在应用程序的 Global.asax 文件中注册它来自定义每类型绑定规则, (对于没有类型) 拥有该类型的方案很有用。 然后,可以使用 Bind 属性的 Include 和 Exclude 属性来控制哪些属性可绑定特定类或接口。

我们将对 NerdDinner 应用程序中的 Dinner 类使用此技术,并向其添加 [Bind] 属性,以将可绑定属性列表限制为以下内容:

[Bind(Include="Title,Description,EventDate,Address,Country,ContactPhone,Latitude,Longitude")]
public partial class Dinner {
   ...
}

请注意,我们不允许通过绑定操作 RSVP 集合,也不允许通过绑定设置 DinnerID 或 HostedBy 属性。 出于安全原因,我们将只使用操作方法中的显式代码来操作这些特定属性。

CRUD Wrap-Up

ASP.NET MVC 包含许多有助于实现表单过帐方案的内置功能。 我们在 DinnerRepository 的基础上使用了各种这些功能来提供 CRUD UI 支持。

我们将使用以模型为中心的方法来实现应用程序。 这意味着,我们所有的验证和业务规则逻辑都在模型层中定义,而不是在控制器或视图中定义。 我们的 Controller 类和视图模板都不知道 Dinner 模型类正在强制执行的特定业务规则。

这将保持应用程序体系结构干净,并使其更易于测试。 我们可以在将来向模型层添加其他业务规则,无需对控制器或视图 进行任何代码更改 即可获得支持。 这将为我们提供极大的敏捷性,以在将来改进和更改应用程序。

我们的 DinnersController 现在支持 Dinner 列表/详细信息,以及创建、编辑和删除支持。 类的完整代码可在下面找到:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    //
    // GET: /Dinners/

    public ActionResult Index() {

        var dinners = dinnerRepository.FindUpcomingDinners().ToList();
        return View(dinners);
    }

    //
    // GET: /Dinners/Details/2

    public ActionResult Details(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");
        else
            return View(dinner);
    }

    //
    // GET: /Dinners/Edit/2

    public ActionResult Edit(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);
        return View(dinner);
    }

    //
    // POST: /Dinners/Edit/2

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(int id, FormCollection formValues) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        try {
            UpdateModel(dinner);

            dinnerRepository.Save();

            return RedirectToAction("Details", new { id= dinner.DinnerID });
        }
        catch {
            ModelState.AddRuleViolations(dinner.GetRuleViolations());

            return View(dinner);
        }
    }

    //
    // GET: /Dinners/Create

    public ActionResult Create() {

        Dinner dinner = new Dinner() {
            EventDate = DateTime.Now.AddDays(7)
        };
        return View(dinner);
    }

    //
    // POST: /Dinners/Create

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Create(Dinner dinner) {

        if (ModelState.IsValid) {

            try {
                dinner.HostedBy = "SomeUser";

                dinnerRepository.Add(dinner);
                dinnerRepository.Save();

                return RedirectToAction("Details", new{id=dinner.DinnerID});
            }
            catch {
                ModelState.AddRuleViolations(dinner.GetRuleViolations());
            }
        }

        return View(dinner);
    }

    //
    // HTTP GET: /Dinners/Delete/1

    public ActionResult Delete(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");
        else
            return View(dinner);
    }

    // 
    // HTTP POST: /Dinners/Delete/1

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Delete(int id, string confirmButton) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");

        dinnerRepository.Delete(dinner);
        dinnerRepository.Save();

        return View("Deleted");
    }
}

下一步

现在,在 DinnersController 类中,我们有了基本的 CRUD (创建、读取、更新和删除) 支持实现。

现在,让我们看看如何使用 ViewData 和 ViewModel 类在表单上启用更丰富的 UI。