UNIT TEST: EDIT SUBMISSIONS

Một phần của tài liệu Giáo trình lập trình ASP.NET Apress pro ASP NET MVC3 framework pre release (Trang 244 - 252)

For the POST-processing Edit action method, we need to make sure that valid updates to the Product object that the model binder has created are passed to the product repository to be saved.

We also want to check that invalid updates—where a model error exists—are not passed to the repository. Here are the test methods:

[TestMethod]

public void Can_Save_Valid_Changes() {

// Arrange - create mock repository

Mock<IProductRepository> mock = new Mock<IProductRepository>();

// Arrange - create the controller

AdminController target = new AdminController(mock.Object);

// Arrange - create a product

Product product = new Product {Name = "Test"};

// Act - try to save the product

ActionResult result = target.Edit(product);

// Assert - check that the repository was called mock.Verify(m => m.SaveProduct(product));

// Assert - check the method result type

20

Assert.IsNotInstanceOfType(result, typeof(ViewResult));

}

[TestMethod]

public void Cannot_Save_Invalid_Changes() { // Arrange - create mock repository

Mock<IProductRepository> mock = new Mock<IProductRepository>();

// Arrange - create the controller

AdminController target = new AdminController(mock.Object);

// Arrange - create a product

Product product = new Product { Name = "Test" };

// Arrange - add an error to the model state target.ModelState.AddModelError("error", "error");

// Act - try to save the product

ActionResult result = target.Edit(product);

// Assert - check that the repository was not called

mock.Verify(m => m.SaveProduct(It.IsAny<Product>()), Times.Never());

// Assert - check the method result type

Assert.IsInstanceOfType(result, typeof(ViewResult));

}

Displaying a Confirmation Message

We are going to deal with the message we stored using TempData in the _AdminLayout.cshtml layout file. By handling the message in the template, we can create messages in any view that uses the template, without needing to create additional Razor blocks. Listing 9-15 shows the change to the file.

15. Listing 9-15. Handling the ViewBag Message in the Layout

<!DOCTYPE html>

<html>

<head>

<title>@ViewBag.Title</title>

<link href="@Url.Content("~/Content/Admin.css")" rel="stylesheet" type="text/css" />

</head>

<body>

<div>

@if (TempData["message"] != null) {

<div class="Message">@TempData["message"]</div>

}

@RenderBody() </div>

</body>

</html>

Tip The benefit of dealing with the message in the template like this is that users will see it displayed on whatever page is rendered after they have saved a change. At the moment, we return them to the list of products, but we could change the workflow to render some other view, and the users will still see the message (as long as the next view also uses the same layout).

We how have all the elements we need to test editing products. Run the application, navigate to the Admin/Index URL, and make some edits. Click the Save button. You will be returned to the list view, and the TempData message will be displayed, as shown in Figure 9-12.

Figure 9-12. Editing a product and seeing the TempData message

The message will disappear if you reload the product list screen, because TempData is deleted when it is read.

That is very convenient, since we don’t want old messages hanging around.

Adding Model Validation

As is always the case, we need to add validation rules to our model entity. At the moment, the administrator could enter negative prices or blank descriptions, and SportsStore would happily store that data in the database. Listing 9- 16 shows how we have applied data annotations attributes to the Product class, just as we did for the ShippingDetails class in the previous chapter.

16. Listing 9-16. Applying Validation Attributes to the Product Class using System.ComponentModel.DataAnnotations;

using System.Web.Mvc;

namespace SportsStore.Domain.Entities { public class Product {

[HiddenInput(DisplayValue=false)]

public int ProductID { get; set; }

[Required(ErrorMessage = "Please enter a product name")]

public string Name { get; set; }

[Required(ErrorMessage = "Please enter a description")]

[DataType(DataType.MultilineText)]

public string Description { get; set; } [Required]

[Range(0.01, double.MaxValue, ErrorMessage = "Please enter a positive price")]

public decimal Price { get; set; }

[Required(ErrorMessage = "Please specify a category")]

public string Category { get; set; } }

}

Note We have reached the point with the Product class where there are more attributes than properties.

Don’t worry if you feel that the attributes make the class unreadable. You can move the attributes into a different class and tell MVC where to find them. We’ll show you how to do this in Chapter 16.

When we used the Html.EditorForModel helper method to create the form elements to edit a Product, the MVC Framework added all the markup and CSS needed to display validation errors inline. Figure 9-13 shows how this appears when you edit a product and enter data that breaks the validation rules we applied in Listing 9-16.

Figure 9-13. Data validation when editing products

Enabling Client-Side Validation

At present, our data validation is applied only when the administrator submits edits to the server. Most web users expect immediate feedback if there are problems with the data they have entered. This is why web developers often want to perform client-side validation, where the data is checked in the browser using JavaScript. The MVC Framework can perform client-side validation based on the data annotations we applied to the domain model class.

This feature is enabled by default, but it hasn’t been working because we have not added links to the required JavaScript libraries. The simplest place to add these links is in the _AdminLayout.cshtml file, so that client validation can work on any page that uses this layout. Listing 9-17 shows the changes to the layout. The MVC client-side validation feature is based on the jQuery JavaScript library, which can be deduced from the name of the script files.

17. Listing 9-17. Importing JavaScript Files for Client-Side Validation

<!DOCTYPE html>

<html>

<head>

<title>@ViewBag.Title</title>

<link href="@Url.Content("~/Content/Admin.css")" rel="stylesheet" type="text/css" />

<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")"

type="text/javascript"></script>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")"

type="text/javascript"></script>

<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")"

type="text/javascript"></script>

</head>

<body>

<div>

@if (TempData["message"] != null) {

<div class="Message">@TempData["message"]</div>

}

@RenderBody() </div>

</body>

</html>

With these additions, client-side validation will work for our administration views. The appearance of error messages to the user is the same, because the CSS classes that are used by the server validation are also used by the client-side validation. but the response is immediate and doesn’t require a request to be sent to the server.

In most situations, client-side validation is a useful feature, but if, for some reason, you don’t want to validate at the client, you need to use the following statements:

HtmlHelper.ClientValidationEnabled = false;

HtmlHelper.UnobtrusiveJavaScriptEnabled = false;

If you put these statements in a view or in a controller, then client-side validation is disabled only for the current action. You can disable client-side validation for the entire application by using those statements in the

Application_Start method of Global.asax or by adding values to the Web.config file, like this:

<configuration>

<appSettings>

<add key="ClientValidationEnabled" value="false"/>

<add key="UnobtrusiveJavaScriptEnabled" value="false"/>

</appSettings>

</configuration>

Creating New Products

Next, we will implement the Create action method, which is the one specified in the Add a new product link in the product list page. This will allow the administrator to add new items to the product catalog. Adding the ability to create new products will require only one small addition and one small change to our application. This is a great example of the power and flexibility of a well-thought-out MVC application.

First, add the Create method, shown in Listing 9-18, to the AdminController class.

18. Listing 9-18. Adding the Create Action Method to the Admin Controller public ViewResult Create() {

return View("Edit", new Product());

}

The Create method doesn’t render its default view. Instead, it specifies that the Edit view should be used. It is perfectly acceptable for one action method to use a view that is usually associated with another view. In this case, we inject a new Product object as the view model so that the Edit view is populated with empty fields.

This leads us to the modification. We would usually expect a form to postback to the action that rendered it, and this is what the Html.BeginForm assumes by default when it generates an HTML form. However, this doesn’t work for our Create method, because we want the form to be posted back to the Edit action so that we can save the newly created product data. To fix this, we can use an overloaded version of the Html.BeginForm helper method to specify that the target of the form generated in the Edit view is the Edit action method of the Admin controller, as shown in Listing 9-19.

19. Listing 9-19. Explicitly Specifying an Action Method and Controller for a Form

@model SportsStore.Domain.Entities.Product

@{

ViewBag.Title = "Admin: Edit " + @Model.Name;

Layout = "~/Views/Shared/_AdminLayout.cshtml";

}

<h1>Edit @Model.Name</h1>

@using (Html.BeginForm("Edit", "Admin")) { @Html.EditorForModel()

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

@Html.ActionLink("Cancel and return to List", "Index") }

Now the form will always be posted to the Edit action, regardless of which action rendered it. We can create products by clicking the Add a new product link and filling in the details, as shown in Figure 9-14.

Figure 9-14. Adding a new product to the catalog

Deleting Products

Adding support for deleting items is fairly simple. First, we add a new method to the IProductRepository interface, as shown in Listing 9-20.

20. Listing 9-20. Adding a Method to Delete Products using System.Linq;

using SportsStore.Domain.Entities;

namespace SportsStore.Domain.Abstract { public interface IProductRepository {

IQueryable<Product> Products { get; } void SaveProduct(Product product);

void DeleteProduct(Product product);

} }

Next, we implement this method in our Entity Framework repository class, EFProductRepository, as shown in Listing 9-21.

21. Listing 9-21. Implementing Deletion Support in the Entity Framework Repository Class ...

public void DeleteProduct(Product product) { context.Products.Remove(product);

context.SaveChanges();

} ...

The final step is to implement a Delete action method in the Admin controller. This action method should support only POST requests, because deleting objects is not an idempotent operation. As we’ll explain in Chapter 11, browsers and caches are free to make GET requests without the user’s explicit consent, so we must be careful to avoid making changes as a consequence of GET requests. Listing 9-22 shows the new action method.

22. Listing 9-22. The Delete Action Method [HttpPost]

public ActionResult Delete(int productId) {

Product prod = repository.Products.FirstOrDefault(p => p.ProductID == productId);

if (prod != null) {

repository.DeleteProduct(prod);

TempData["message"] = string.Format("{0} was deleted", prod.Name);

}

return RedirectToAction("Index");

}

Một phần của tài liệu Giáo trình lập trình ASP.NET Apress pro ASP NET MVC3 framework pre release (Trang 244 - 252)

Tải bản đầy đủ (PDF)

(603 trang)