CREATING MULTIPLE HTML FORMS IN A PAGE

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 195 - 204)

Using the Html.BeginForm helper in each product listing means that every Add to cart button is rendered in its own separate HTML form element. This may be surprising if you’ve been developing with ASP.NET Web Forms, which imposes a limit of one form per page. ASP.NET MVC doesn’t limit the number of forms per page, and you can have as many as you need.

There is no technical requirement for us to create a form for each button. However, since each form will postback to the same controller method, but with a different set of parameter values, it is a nice and simple way to deal with the button presses.

Implementing the Cart Controller

We need to create a controller to handle the Add to cart button presses. Create a new controller called CartController and edit the content so that it matches Listing 8-16.

16. Listing 8-16. Creating the Cart Controller using System.Linq;

using System.Web.Mvc;

using SportsStore.Domain.Abstract;

using SportsStore.Domain.Entities;

namespace SportsStore.WebUI.Controllers { public class CartController : Controller { private IProductRepository repository;

public CartController(IProductRepository repo) { repository = repo;

}

public RedirectToRouteResult AddToCart(int productId, string returnUrl) { Product product = repository.Products

.FirstOrDefault(p => p.ProductID == productId);

if (product != null) {

GetCart().AddItem(product, 1);

}

return RedirectToAction("Index", new { returnUrl });

}

public RedirectToRouteResult RemoveFromCart(int productId, string returnUrl) { Product product = repository.Products

.FirstOrDefault(p => p.ProductID == productId);

if (product != null) {

GetCart().RemoveLine(product);

}

return RedirectToAction("Index", new { returnUrl });

}

private Cart GetCart() {

Cart cart = (Cart)Session["Cart"];

if (cart == null) { cart = new Cart();

Session["Cart"] = cart;

}

return cart;

} } }

There are a few points to note about this controller. The first is that we use the ASP.NET session state feature to store and retrieve Cart objects. This is the purpose of the GetCart method. ASP.NET has a nice session feature that uses cookies or URL rewriting to associate requests from a user together, to form a single browsing session. A related feature is session state, which allows us to associate data with a session. This is an ideal fit for our Cart class. We want each user to have his own cart, and we want the cart to be persistent between requests. Data associated with a session is deleted when a session expires (typically because a user hasn’t made a request for a while), which means that we don’t need to manage the storage or life cycle of the Cart objects. To add an object to the session state, we set the value for a key on the Session object, like this:

Session["Cart"] = cart;

To retrieve an object again, we simply read the same key, like this:

Cart cart = (Cart)Session["Cart"];

Tip Session state objects are stored in the memory of the ASP.NET server by default, but you can configure a range of different storage approaches, including using a SQL database.

For the AddToCart and RemoveFromCart methods, we have used parameter names that match the input elements in the HTML forms we created in the ProductSummary.cshtml view. This allows the MVC Framework to associate incoming form POST variables with those parameters, meaning we don’t need to process the form ourselves.

Displaying the Contents of the Cart

The final point to note about the Cart controller is that both the AddToCart and RemoveFromCart methods call the RedirectToAction method. This has the effect of sending an HTTP redirect instruction to the client browser, asking the browser to request a new URL. In this case, we have asked the browser to request a URL that will call the Index action method of the Cart controller.

We are going to implement the Index method and use it to display the contents of the Cart. If you refer back to Figure 8-8, you’ll see that this is our workflow when the user clicks the Add to cart button.

We need to pass two pieces of information to the view that will display the contents of the cart: the Cart object and the URL to display if the user clicks the Continue shopping button. We will create a simple view model class for this purpose. Create a new class called CartIndexViewModel in the Models folder of the SportsStore.WebUI project.

The contents of this class are shown in Listing 8-17.

17. Listing 8-17. The CartIndexViewModel Class using SportsStore.Domain.Entities;

namespace SportsStore.WebUI.Models { public class CartIndexViewModel { public Cart Cart { get; set; } public string ReturnUrl { get; set; } }

}

Now that we have the view model, we can implement the Index action method in the Cart controller class, as shown in Listing 8-18.

18. Listing 8-18. The Index Action Method public ViewResult Index(string returnUrl) { return View(new CartIndexViewModel { Cart = GetCart(),

ReturnUrl = returnUrl });

}

The last step is to display the contents of the cart is to create the new view. Right-click the Index method and select Add View from the pop-up menu. Set the name of the view to Index, check the option to create a strongly typed view, and select CartIndexViewModel as the model class, as shown in Figure 8-9.

Figure 8-9. Adding the Index view

We want the contents of the cart to be displayed consistently with the rest of the application pages, so ensure that the option to use a layout is checked, and leave the text box empty so that we use the default _Layout.cshtml file.

Click Add to create the view and edit the contents so that they match Listing 8-19.

19. Listing 8-19. The Index View

@model SportsStore.WebUI.Models.CartIndexViewModel

@{

ViewBag.Title = "Sports Store: Your Cart";

}

<h2>Your cart</h2>

<table width="90%" align="center">

<thead><tr>

<th align="center">Quantity</th>

<th align="left">Item</th>

<th align="right">Price</th>

<th align="right">Subtotal</th>

</tr></thead>

<tbody>

@foreach(var line in Model.Cart.Lines) { <tr>

<td align="center">@line.Quantity</td>

<td align="left">@line.Product.Name</td>

<td align="right">@line.Product.Price.ToString("c")</td>

<td align="right">@((line.Quantity * line.Product.Price).ToString("c"))</td>

</tr>

} </tbody>

<tfoot><tr>

<td colspan="3" align="right">Total:</td>

<td align="right">

@Model.Cart.ComputeTotalValue().ToString("c") </td>

</tr></tfoot>

</table>

<p align="center" class="actionButtons">

<a href="@Model.ReturnUrl">Continue shopping</a>

</p>

The view looks more complicated than it is. It just enumerates the lines in the cart and adds rows for each of them to an HTML table, along with the total cost per line and the total cost for the cart. The final step is to add some more CSS. Add the styles shown in Listing 8-20 to the Site.css file.

20. Listing 8-20. CSS for Displaying the Contents of the Cart H2 { margin-top: 0.3em }

TFOOT TD { border-top: 1px dotted gray; font-weight: bold; } .actionButtons A, INPUT.actionButtons {

font: .8em Arial; color: White; margin: .5em;

text-decoration: none; padding: .15em 1.5em .2em 1.5em;

background-color: #353535; border: 1px solid black;

}

We now have the basic functions of the shopping cart in place. When we click the Add to cart button, the appropriate product is added to our cart and a summary of the cart is displayed, as shown in Figure 8-10. We can click the Continue shopping button and return to the product page we came from—all very nice and slick.

Figure 8-10. Displaying the contents of the shopping cart

We have more work to do. We need to allow users to remove items from a cart and also to complete their purchase. We will implement these features later in this chapter. Next, we are going to revisit the design of the Cart controller and make some changes.

Using Model Binding

The MVC Framework uses a system called model binding to create C# objects from HTTP requests in order to pass them as parameter values to action methods. This is how MVC processes forms, for example. The framework looks at the parameters of the action method that has been targeted, and uses a model binder to get the values of the form input elements and convert them to the type of the parameter with the same name.

Model binders can create C# types from any information that is available in the request. This is one of the central features of the MVC Framework. We are going to create a custom model binder to improve our CartController class.

We like using the session state feature in the Cart controller to store and manage our Cart objects, but we really don’t like the way we have to go about it. It doesn’t fit the rest of our application model, which is based around action method parameters. We can’t properly unit test the CartController class unless we mock the Session parameter of the base class, and that means mocking the Controller class and a whole bunch of other stuff we would rather not deal with.

To solve this problem, we are going to create a custom model binder that obtains the Cart object contained in the session data. The MVC Framework will then be able to create Cart objects and pass them as parameters to the action methods in our CartController class. The model binding feature is very powerful and flexible. We go into a lot more depth about this feature in Chapter 17, but this is a nice example to get us started.

Creating a Custom Model Binder

We create a custom model binder by implementing the IModelBinder interface. Create a new folder in the SportsStore.WebUI project called Binders and create the CartModelBinder class inside that folder. Listing 8-21 shows the implementation of this class.

21. Listing 8-21. The CartModelBinder Class using System;

using System.Web.Mvc;

using SportsStore.Domain.Entities;

namespace SportsStore.WebUI.Binders { public class CartModelBinder : IModelBinder { private const string sessionKey = "Cart";

public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {

// get the Cart from the session

Cart cart = (Cart)controllerContext.HttpContext.Session[sessionKey];

// create the Cart if there wasn't one in the session data if (cart == null) {

cart = new Cart();

controllerContext.HttpContext.Session[sessionKey] = cart;

}

// return the cart return cart;

} } }

The IModelBinder interface defines one method: BindModel. The two parameters are provided to make creating the domain model object possible. The ControllerContext provides access to all of the information that the controller class has, which includes details of the request from the client. The ModelBindingContext gives you information about the model object you are being asked to build and tools for making it easier. We’ll come back to this class in Chapter 17.

For our purposes, the ControllerContext class is the one we’re interested in. It has the HttpContext property, which in turn has a Session property that lets us get and set session data. We obtain the Cart by reading a key value from the session data, and create a Cart if there isn’t one there already.

We need to tell the MVC Framework that it can use our CartModelBinder class to create instances of Cart. We do this in the Application_Start method of Global.asax, as shown in Listing 8-22.

22. Listing 8-22. Registering the CartModelBinder Class protected void Application_Start() {

AreaRegistration.RegisterAllAreas();

RegisterGlobalFilters(GlobalFilters.Filters);

RegisterRoutes(RouteTable.Routes);

ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());

ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());

}

We can now update the CartController class to remove the GetCart method and rely on our model binder.

Listing 8-23 shows the changes.

23. Listing 8-23. Relying on the Model Binder in CartController using System.Linq;

using System.Web.Mvc;

using SportsStore.Domain.Abstract;

using SportsStore.Domain.Entities;

using SportsStore.WebUI.Models;

namespace SportsStore.WebUI.Controllers { public class CartController : Controller { private IProductRepository repository;

public CartController(IProductRepository repo) { repository = repo;

}

public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl) { Product product = repository.Products

.FirstOrDefault(p => p.ProductID == productId);

if (product != null) { cart.AddItem(product, 1);

}

return RedirectToAction("Index", new { returnUrl });

}

public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl) {

Product product = repository.Products

.FirstOrDefault(p => p.ProductID == productId);

if (product != null) {

cart.RemoveLine(product);

}

return RedirectToAction("Index", new { returnUrl });

}

public ViewResult Index(Cart cart, string returnUrl) { return View(new CartIndexViewModel {

Cart = cart,

ReturnUrl = returnUrl });

} } }

We have removed the GetCart method and added a Cart parameter to each of the action methods.

When the MVC Framework receives a request that requires, say, the AddToCart method to be invoked, it begins by looking at the parameters for the action method. It looks at the list of binders available and tries to find one that can create instances of each parameter type. Our custom binder is asked to create a Cart object, and it does so by working with the session state feature. Between our binder and the default binder, the MVC Framework is able to create the set of parameters required to call the action method. And so it does, allowing us to refactor the controller so that it has no view as to how Cart objects are created when requests are received.

There are a few benefits to using a custom model binder like this. The first is that we have separated the logic used to create a Cart from that of the controller, which allows us to change the way we store Cart objects without needing to change the controller. The second benefit is that any controller class that works with Cart objects can simply declare them as action method parameters and take advantage of the custom model binder. The third benefit, and the one we think is most important, is that we can now unit test the Cart controller without needing to mock a lot of ASP.NET plumbing.

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 195 - 204)

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

(603 trang)