ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

Загрузка изображений

Напоследок мы усложним функциональность SportsStore, добавив возможность для администратора загружать изображения товаров и сохранять их в базе данных, чтобы отображать их в каталоге товаров.

Расширяем базу данных

Откройте окно Visual Studio Database Explorer и перейдите к таблице Products в базе, которую мы создали в главе 7. Название соединения для передачи данных может быть изменено на EFDbContext – это то название, которое мы назначили соединению в файле Web.config в главе 7. Visual Studio может быть немного непоследовательной, когда переименовывает соединения, так что возможно, что вы увидите оригинальное название, которое отображалось при создании соединения.

Щелкните правой кнопкой мыши в таблице и выберите Open Table Definition из контекстного меню. Добавьте определения для двух новых столбцов, которые показаны на рисунке 11-4. Убедитесь, что вы правильно установили данные столбцов, и не забудьте отметить флажком опцию Allow Nulls для них обоих.

Рисунок 11-4: Добавляем новые столбцы в таблицу Products

Нажмите кнопку Update, после чего Visual Studio определит, какие операторы SQL нужно отправить в базу данных для ее обновления, и отобразит их в диалоговом окне Preview Database Updates, как показано на рисунке 11-5.

Рисунок 11-5: Предварительный просмотр обновлений базы данных

Нажмите кнопку Update Database для создания новых столбцов в базе данных.

Расширяем доменную модель

Нам нужно добавить два новых поля в класс Products проекта SportsStore.Domain, которые соответствуют столбцам, добавленным в базу данных. Дополнения выделены жирным шрифтом в листинге 11-9.

Листинг 11-9: Добавляем свойства в класс Products
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; }

		public byte[] ImageData { get; set; }

		[HiddenInput(DisplayValue = false)]
		public string ImageMimeType { get; set; }
	}
}

Мы не хотим, чтобы какое-либо из этих новых свойств было видимым при визуализизации редактора. Для этого мы используем атрибут HiddenInput в свойстве ImageMimeType. Нам не нужно ничего делать со свойством ImageData, потому что платформа не визуализирует редактор для массивов байтов. Она делает это только для "простых" типов, таких как int, string, DateTime и так далее.

Внимание!

Убедитесь в том, что имена свойств, которые вы добавляете в класс Product, в точности соответствуют названиям новых столбцов в базе данных.

Создаем элементы пользовательского интерфейса для загрузки

Далее нам нужно добавить поддержку загрузки файлов. Для этого понадобиться создать пользовательский интерфейс, который будет использоваться администратором для загрузки изображений. Измените представление Views/Admin/Edit.cshtml в соответствии с листингом 11-10 (дополнения выделены жирным шрифтом).

Листинг 11-10: Добавляем поддержку изображений
@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", 
	FormMethod.Post, new { enctype = "multipart/form-data" })) {
	@Html.EditorForModel()
	<div class="editor-label">Image</div>
	<div class="editor-field">
		@if (Model.ImageData == null) {
			@:None
		}
		else {
			<img width="150" height="150" src="@Url.Action("GetImage", "Product", new { Model.ProductID })" />
		}
		<div>Upload new image:
			<input type="file" name="Image" /></div>
	</div>
	<input type="submit" value="Save" />
	@Html.ActionLink("Cancel and return to List", "Index")
}

Вы, возможно, уже знаете, что браузеры будут загружать файлы должным образом только тогда, когда атрибут enctype в HTML-элементе form содержит значение multipart/form-data. Другими словами, для успешной загрузки элемент form должен выглядеть следующим образом:

<form action="/Admin/Edit" enctype="multipart/form-data" method="post">
...
</form>

Без атрибута enctype браузер будет передавать только имя файла, а не его содержимое, что для нас совершенно бесполезно. Чтобы гарантировать наличие атрибута enctype, мы должны использовать перегруженный вспомогательный метод Html.BeginForm, который позволяет указать HTML-атрибуты, например:

@using (Html.BeginForm("Edit", "Admin", FormMethod.Post,
	new { enctype = "multipart/form-data" })) {

Обратите внимание, что если свойство ImageData отображаемого объекта Product не содержит null, мы добавляем элемент img и устанавливаем в качестве его источника результат вызова метода действия GetImage контроллера Product. Скоро мы это реализуем.

Сохраняем изображения в базе данных

Нам нужно расширить версию POST метода действия Edit класса AdminController так, чтобы мы могли принимать загруженное изображение и сохранять его в базе данных. Необходимые изменения показаны в листинге 11-11.

Листинг 11-11: Обработка изображения в классе AdminController
[HttpPost]
public ActionResult Edit(Product product, HttpPostedFileBase image)
{
	if (ModelState.IsValid)
	{
		if (image != null)
		{
			product.ImageMimeType = image.ContentType;
			product.ImageData = new byte[image.ContentLength];
			image.InputStream.Read(product.ImageData, 0, image.ContentLength);
		}
		repository.SaveProduct(product);
		TempData["message"] = string.Format("{0} has been saved", product.Name);
		return RedirectToAction("Index");
	}
	else
	{
		// there is something wrong with the data values
		return View(product);
	}
}

Мы добавили в метод Edit новый параметр, с помощью которого MVC Framework передает нам данные загруженного файла. Мы проверяем значение этого параметра, и если он не содержит null, то копируем данные и тип MIME из параметра в объект Product, чтобы сохранить их в базе данных.

Примечание

Понадобится обновить модульные тесты, чтобы отразить новый параметр в листинге 11-11. Передача параметра со значением null позволит тестам скомпилироваться.

Мы также должны обновить класс EFProductRepository в проекте SportsStore.Domain и гарантировать, что значения, присвоенные свойствам ImageData и ImageMimeType, сохраняются в базе данных. В листинге 11-12 показаны необходимые изменения в методе SaveProduct.

Листинг 11-12: Гарантируем, что данные изображений сохраняются в базе данных
public void SaveProduct(Product product)
{
	if (product.ProductID == 0)
	{
		context.Products.Add(product);
	}
	else
	{
		Product dbEntry = context.Products.Find(product.ProductID);
		if (dbEntry != null)
		{
			dbEntry.Name = product.Name;
			dbEntry.Description = product.Description;
			dbEntry.Price = product.Price;
			dbEntry.Category = product.Category;
			dbEntry.ImageData = product.ImageData;
			dbEntry.ImageMimeType = product.ImageMimeType;
		}
	}
	context.SaveChanges();
}

Реализуем метод действия GetImage

В листинге 11-10 мы добавили элемент img, содержание которого было получено с помощью метода действия GetImage. Теперь мы его реализуем, чтобы можно было отображать изображения, содержащиеся в базе данных. В листинге 11-13 показан метод, который мы добавили в класс ProductController.

Листинг 11-13: Метод действия GetImage
public FileContentResult GetImage(int productId)
{
	Product prod = repository.Products.FirstOrDefault(p => p.ProductID == productId);
	if (prod != null)
	{
		return File(prod.ImageData, prod.ImageMimeType);
	}
	else
	{
		return null;
	}
}

Этот метод пытается найти товар, который соответствует указанному в параметре ID. Он возвращает класс FileContentResult, когда мы хотим вернуть файл браузеру клиента, и экземпляры создаются с помощью метода File базового класса контроллера. Мы обсудим различные типы результатов, которые можно возвращать из методов действий, в главе 15.

Модульный тест: получение изображений

Мы хотим убедиться, что метод GetImage возвращает из хранилища правильный тип MIME, и гарантировать, что данные не возвращаются, если мы запрашиваем несуществующий ID товара. Вот тестовые методы, которые мы добавили в новый файл тестов под названием ImageTests.cs:

using Microsoft.VisualStudio.TestTools.UnitTesting;
	using Moq;
	using SportsStore.Domain.Abstract;
	using SportsStore.Domain.Entities;
	using SportsStore.WebUI.Controllers;
	using System.Linq;
	using System.Web.Mvc;
	namespace SportsStore.UnitTests
	{
		[TestClass]
		public class ImageTests
		{
			[TestMethod]
			public void Can_Retrieve_Image_Data()
			{
				// Arrange - create a Product with image data
				Product prod = new Product
				{
					ProductID = 2,
					Name = "Test",
					ImageData = new byte[] { },
					ImageMimeType = "image/png"
				};
				// 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"},
					prod,
					new Product {ProductID = 3, Name = "P3"}
				}.AsQueryable());
				// Arrange - create the controller
				ProductController target = new ProductController(mock.Object);
				// Act - call the GetImage action method
				ActionResult result = target.GetImage(2);
				// Assert
				Assert.IsNotNull(result);
				Assert.IsInstanceOfType(result, typeof(FileResult));
				Assert.AreEqual(prod.ImageMimeType, ((FileResult)result).ContentType);
			}
			[TestMethod]
			public void Cannot_Retrieve_Image_Data_For_Invalid_ID()
			{
				// 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"},
					new Product {ProductID = 2, Name = "P2"}
				}.AsQueryable());
				// Arrange - create the controller
				ProductController target = new ProductController(mock.Object);
				// Act - call the GetImage action method
				ActionResult result = target.GetImage(100);
				// Assert
				Assert.IsNull(result);
			}
		}
	}

Когда у нас есть действительный ID товара, тест гарантирует то, что мы получаем результат FileResult из метода действия и тип содержимого соответствует типу имитированных данных. Класс FileResult не дает нам доступ к двоичному содержимому файла, так что придется довольствоваться не вполне совершенным тестом. Когда мы запрашиваем недействительный ID товара, мы просто проверяем, что результат будет содержать null.

Теперь администратор может загружать изображения для товаров. Вы можете попробовать сделать это сами, запустив приложение, перейдя по ссылке в /Admin/Index и отредактировав один из товаров. Пример показан на рисунке 11-6.

Рисунок 11-6: Добавляем изображение к списку товаров

Выводим изображения товаров

Теперь нам остается только визуализировать изображения рядом с описаниями товаров в каталоге. Отредактируйте представление Views/Shared/ProductSummary.cshtml и внесите в него изменения, выделенные жирным шрифтом в листинге 11-14.

Листинг 11-14: Отображаем изображения в каталоге товаров
@model SportsStore.Domain.Entities.Product
<div class="item">
	@if (Model.ImageData != null) {
		<div style="float: left; margin-right: 20px">
			<img width="75" height="75" src="@Url.Action("GetImage", "Product", new { Model.ProductID })" />
		</div>
	}
	<h3>@Model.Name</h3>
	@Model.Description
	<div class="item">
		@using (Html.BeginForm("AddToCart", "Cart")) {
			@Html.HiddenFor(x => x.ProductID)
			@Html.Hidden("returnUrl", Request.Url.PathAndQuery)
			<input type="submit" value="+ Add to cart" />
		}
	</div>
	<h4>@Model.Price.ToString("c")</h4>
</div>

Когда эти изменения применены, пользователи при просмотре каталога будут видеть изображения как часть описания товара, как показано на рисунке 11-7.

Рисунок 11-7: Выводим изображения товаров
или RSS канал: Что новенького на smarly.net