Главная страница   /   9.1. Использование модели связывания данных (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

9.1. Использование модели связывания данных

MVC Framework использует систему под названием модель связывания данных для создания объектов C# из HTTP-запросов и передачи их в качестве значений параметров в методы действий. Таким образом, например, MVC обрабатывает формы. Платформа смотрит на параметры целевого метода действия и использует механизм связывания данных модели, чтобы получить значения входных элементов формы и преобразовать их в одноименный тип параметра.

Механизмы связывания могут создавать типы C# из любых данных, доступных в запросе. Это является одной из центральных возможностей MVC Framework. Мы создадим пользовательский механизм связывания, чтобы улучшить наш класс CartController.

Нам нравится использовать состояние сеанса в контроллере Cart для хранения и управления объектами Cart, но нам не нравится то, как мы должны это делать. Это не соответствует всей остальной модели приложения, которая основана на параметрах методов действий. Мы не сможем должным образом протестировать класс CartController, если только не создадим имитацию параметра Session базового класса, а это означает, что нужно будет создавать имитацию класса Controller и много других вещей, с которыми мы бы предпочли не иметь дела.

Чтобы решить эту проблему, мы создадим пользовательский механизм связывания, который будет получать объект Cart, содержащийся в данных сессии. Тогда MVC Framework сможет создавать объекты Cart и передавать их в качестве параметров методов действий в наш класс CartController. Связывание данных – очень мощная и гибкая возможность. Мы рассмотрим ее подробнее в главе 22, но этот пример отлично подходит, чтобы с ней познакомиться.

Создаем пользовательский механизм связывания данных

Мы создаем пользовательский механизм связывания путем реализации интерфейса IModelBinder. Создайте новую папку под названием Binders в проекте SportsStore.WebUI, а в ней - класс CartModelBinder. Определение класса CartModelBinder показано в листинге 9-1.

Листинг 9-1: Класс CartModelBinder
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;
		}
	}
}

Интерфейс IModelBinder определяет один метод: BindModel. Мы передаем в него два параметра для того, чтобы сделать возможным создание объекта доменной модели. ControllerContext обеспечивает доступ ко всей информации, которой располагает класс контроллера и которая включает в себя детали запроса клиента. ModelBindingContext предоставляет информацию об объекте создаваемой модели, и некоторые инструменты, которые облегчат процесс связывания. Мы вернемся к этому классу в главе 22.

Класс ControllerContext интересует нас больше всего. У него есть свойство HttpContext, у которого, в свою очередь, есть свойство Session, которое позволяет получать и устанавливать данные сессии. Прочитав значение ключа из данных сессии, мы получаем объект Cart или, если его еще не существует, создаем новый.

Мы должны сообщить MVC Framework, что она может использовать класс CartModelBinder для создания экземпляров объекта Cart. Мы делаем это в методе Application_Start файла Global.asax, как показано в листинге 9-2.

Листинг 9-2: Регистрируем класс CartModelBinder
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Binders;
using SportsStore.WebUI.Infrastructure;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace SportsStore.WebUI
{
	public class MvcApplication : System.Web.HttpApplication
	{
		protected void Application_Start()
		{
			AreaRegistration.RegisterAllAreas();
			WebApiConfig.Register(GlobalConfiguration.Configuration);
			FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
			RouteConfig.RegisterRoutes(RouteTable.Routes);
			BundleConfig.RegisterBundles(BundleTable.Bundles);

			ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());
			ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());
		}
	}
}

Теперь мы можем обновить класс CartController, чтобы удалить метод GetCart и задействовать на наш механизм связывания, который MVC Framework будет применять автоматически. Изменения показаны в листинге 9-3.

Листинг 9-3: Используем механизм связывания в CartController
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

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

		public CartController(IProductRepository repo)
		{
			repository = repo;
		}

		public ViewResult Index(Cart cart, string returnUrl)
		{
			return View(new CartIndexViewModel
			{
				Cart = cart,
				ReturnUrl = returnUrl
			});
		}

		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 });
		}
	}
}

Мы удалили метод GetCart и добавили параметр Cart в каждый метод действия. Когда MVC Framework получает запрос, который требует, скажем, вызвать метод AddToCart, она будет сначала смотреть на параметры для метода действия. Она рассмотрит список доступных механизмов связывания и попытается найти тот, который сможет создать экземпляры каждого типа параметра. Наш пользовательский механизм связывания должен будет создать объект Cart, что он и сделает, используя состояние сеанса. Между обращениями к нашему механизму связывания и механизму по умолчанию MVC Framework может создать набор параметров, которые необходимы для вызова метода действия, позволяя нам реорганизовать контроллер так, чтобы в нем не осталось сведений о том, как создаются объекты Cart при получении запросов.

Использование подобного механизма связывания дает нам несколько преимуществ. Первое заключается в том, что мы отделили логику для создания объектов Cart от контроллера, что позволит нам изменять способ сохранения этих объектов без необходимости изменять контроллер. Вторым преимуществом является то, что любой класс контроллера, который работает с объектами Cart, может просто объявить их как параметры метода действия и воспользоваться пользовательским механизмом связывания. Третье и, на наш взгляд, самое главное преимущество состоит в том, что теперь мы сможем тестировать контроллер Cart, не создавая имитаций встроенных возможностей ASP.NET.

Модульный тест: контроллер Cart

Мы можем протестировать класс CartController, создавая объекты Cart и передавая их в методы действия. Мы хотим проверить три аспекта этого контроллера:

  • Метод AddToCart должен добавить выбранный товар в корзину покупателя.
  • После добавления товара в корзину он должен перенаправить нас в представление Index.
  • URL, по которому пользователь сможет вернуться в каталог, должен быть корректно передан в метод действия Index.

Вот модульные тесты, которые мы добавили в файл CartTests.cs проекта SportsStore.UnitTests:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SportsStore.Domain.Entities;
using System.Linq;
using Moq;
using SportsStore.Domain.Abstract;
using SportsStore.WebUI.Controllers;
using System.Web.Mvc;
using SportsStore.WebUI.Models;
namespace SportsStore.UnitTests
{
	[TestClass]
	public class CartTests
	{
		//...existing test methods omitted for brevity...
		[TestMethod]
		public void Can_Add_To_Cart()
		{
			// Arrange - create the mock repository
			Mock<IProductRepository> mock = new Mock<IProductRepository>();
			mock.Setup(m => m.Products).Returns(new Product[] {
				new Product {ProductID = 1, Name = "P1", Category = "Apples"},
			}.AsQueryable());

			// Arrange - create a Cart
			Cart cart = new Cart();

			// Arrange - create the controller
			CartController target = new CartController(mock.Object);

			// Act - add a product to the cart
			target.AddToCart(cart, 1, null);

			// Assert
			Assert.AreEqual(cart.Lines.Count(), 1);
			Assert.AreEqual(cart.Lines.ToArray()[0].Product.ProductID, 1);
		}

		[TestMethod]
		public void Adding_Product_To_Cart_Goes_To_Cart_Screen()
		{
			// Arrange - create the mock repository
			Mock<IProductRepository> mock = new Mock<IProductRepository>();
			mock.Setup(m => m.Products).Returns(new Product[] {
				new Product {ProductID = 1, Name = "P1", Category = "Apples"},
			}.AsQueryable());

			// Arrange - create a Cart
			Cart cart = new Cart();

			// Arrange - create the controller
			CartController target = new CartController(mock.Object);

			// Act - add a product to the cart
			RedirectToRouteResult result = target.AddToCart(cart, 2, "myUrl");

			// Assert
			Assert.AreEqual(result.RouteValues["action"], "Index");
			Assert.AreEqual(result.RouteValues["returnUrl"], "myUrl");
		}

		[TestMethod]
		public void Can_View_Cart_Contents()
		{
			// Arrange - create a Cart
			Cart cart = new Cart();

			// Arrange - create the controller
			CartController target = new CartController(null);

			// Act - call the Index action method
			CartIndexViewModel result = (CartIndexViewModel)target.Index(cart, "myUrl").ViewData.Model;

			// Assert
			Assert.AreSame(result.Cart, cart);
			Assert.AreEqual(result.ReturnUrl, "myUrl");
		}
	}
}