Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

ASP_NET_MVC_4_Framework_s_primerami_na_C_dlya_p

.pdf
Скачиваний:
25
Добавлен:
19.03.2016
Размер:
17.66 Mб
Скачать

свойство называется Products, тип - Product. Для репрезентации строк в таблице Products мы хотим использовать тип модели Product.

Мы должны сообщить Entity Framework, как подключаться к базе данных, и для этого мы добавляем строку подключения к базе данных в файл Web.config в проекте SportsStore.WebUI, которая имеет то же имя, что и класс контекста, как показано в листинге 7-12.

Совет

Обратите внимание, что здесь мы перешли на другой проект. Мы определяем модель и логику хранения данных в проекте SportsStore.Domain, но информацию о подключении к базе данных помещаем в файле Web.config в проекте

SportsStore.WebUI.

Листинг 7-12: Добавляем подключение к базе данных

<connectionStrings>

<add name="EFDbContext"

connectionString="Data Source=(localdb)\v11.0;Initial Catalog=SportsStore;Integrated Security=True"

providerName="System.Data.SqlClient"/>

</connectionStrings>

В разделе connectionsStrings файла Web.config будет еще один элемент add, который Visual Studio создает по умолчанию. Вы можете либо игнорировать его, либо, как мы, удалить.

Создаем хранилище Product

Теперь у нас есть все, что нужно, чтобы реализовать реальный класс IProductRepository. Добавьте класс под названием EFProductRepository в папку Concrete проекта SportsStore.Domain.

Измените файл класса так, чтобы он соответствовал листингу 7-13.

Листинг 7-13: EFProductRepostory.cs

using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System.Linq;

namespace SportsStore.Domain.Concrete

{

public class EFProductRepository : IProductRepository

{

private EFDbContext context = new EFDbContext(); public IQueryable<Product> Products

{

get { return context.Products; }

}

}

}

Это наш класс хранилища. Он реализует интерфейс IProductRepository и использует экземпляр EFDbContext, чтобы извлекать данные из базы с помощью Entity Framework. Мы продемонстрируем приемы работы с Entity Framework (и насколько они простые), когда будем добавлять в хранилище новые функции.

171

Наконец мы заменим привязку Ninject к нашему имитированному хранилищу на привязку к реальному хранилищу. Отредактируйте класс NinjectControllerFactory в проекте SportsStore.WebUI так, чтобы метод AddBindings соответствовал листингу 7-14.

Листинг 7-14: Добавляем привязку к реальному хранилищу

using System;

using System.Web.Mvc; using System.Web.Routing; using Ninject;

using SportsStore.Domain.Entities; using SportsStore.Domain.Abstract; using System.Collections.Generic; using System.Linq;

using Moq;

using SportsStore.Domain.Concrete;

namespace SportsStore.WebUI.Infrastructure

{

public class NinjectControllerFactory : DefaultControllerFactory

{

private IKernel ninjectKernel; public NinjectControllerFactory()

{

ninjectKernel = new StandardKernel(); AddBindings();

}

protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)

{

return controllerType == null ? null

: (IController)ninjectKernel.Get(controllerType);

}

private void AddBindings()

{

ninjectKernel.Bind<IProductRepository>().To<EFProductRepository>();

}

}

}

Новая привязка выделена жирным шрифтом. Она сообщает Ninject, что мы хотим создавать экземпляры класса EFProductRepository для обслуживания запросов к интерфейсу IProductRepository. Теперь остается только запустить приложение еще раз. Результаты показаны на рисунке 7-13, и вы можете видеть, что сейчас наш список содержит данные о товарах, которые мы занесли в базу данных.

Рисунок 7-13: Результат реализации реального хранилища

172

Этот подход использования Entity Framework для представления базы данных SQL Server как серии объектов модели - очень простой, и он позволяет нам оставаться сфокусированными на MVC Framework. Конечно, мы пропускаем много деталей касательно того, как работает Entity Framework, и огромное количество доступных опций конфигурации. Нам действительно нравится Entity Framework, и мы рекомендуем вам потратить немного времени и познакомиться с ним более подробно. Начать можно с сайта Microsoft для Entity Framework: http://msdn.microsoft.com/data/ef.

Добавление нумерации страниц

Как вы можете видеть на рисунке 7-13, все товары из базы данных отображаются на одной странице. В этом разделе мы добавим поддержку нумерации страниц, чтобы на одной странице отображать определенное количество товаров, и чтобы пользователь мог бы посматривать каталог, переходя с одной страницы на другую. Для этого мы добавим в метод List контроллера Product параметр, показанный в листинге 7-15.

Листинг 7-15: Добавляем поддержку нумерации страниц в метод List контроллера Product

using System;

using System.Collections.Generic; using System.Linq;

using System.Web; using System.Web.Mvc;

using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities;

namespace SportsStore.WebUI.Controllers

{

public class ProductController : Controller

{

private IProductRepository repository; public int PageSize = 4;

public ProductController(IProductRepository productRepository)

{

this.repository = productRepository;

}

public ViewResult List(int page = 1)

{

return View(repository.Products

.OrderBy(p => p.ProductID)

.Skip((page - 1) * PageSize)

.Take(PageSize));

}

}

}

Дополнения в классе контроллера выделены жирным шрифтом. Поле PageSize указывает, что мы хотим видеть четыре товара на странице. В дальнейшем мы вернемся к этому механизму и заменим его на более действенный. Мы добавили дополнительный параметр в метод List. Это означает, что при вызове метода без параметра (List ()) наш вызов обрабатывается так, как если бы мы указали значение параметра (List (1)). В результате, если мы не указываем номер страницы, мы получим первую страницу. LINQ делает нумерацию страниц очень простой. В методе List мы получаем объекты Product из хранилища, упорядочиваем их по первичному ключу, пропускаем товары, которые идут до начала нашей страницы, и берем количество товаров, указанное в поле PageSize.

173

Модульный тест: нумерация страниц

Чтобы протестировать функцию нумерации страниц, мы можем создать имитированное хранилище, внедрить его в конструктор класса ProductController, а затем вызвать метод List и запросить конкретную страницу. Далее мы можем сравнить те объекты Product, которые получили, с теми, которые ожидали получить из тестовых данных в имитированной реализации. Подробно создание модульных тестов описано в главе 6. Вот тест, который мы создали для этой цели:

using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq;

using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using SportsStore.WebUI.Controllers; using System.Collections.Generic; using System.Linq;

namespace SportsStore.UnitTests

{

[TestClass]

public class UnitTest1

{

[TestMethod]

public void Can_Paginate()

{

// Arrange

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"}, new Product {ProductID = 3, Name = "P3"}, new Product {ProductID = 4, Name = "P4"}, new Product {ProductID = 5, Name = "P5"}

}.AsQueryable());

ProductController controller = new ProductController(mock.Object);

controller.PageSize = 3;

// Act

IEnumerable<Product> result = (IEnumerable<Product>)controller.List(2).Model;

// Assert

Product[] prodArray = result.ToArray(); Assert.IsTrue(prodArray.Length == 2); Assert.AreEqual(prodArray[0].Name, "P4"); Assert.AreEqual(prodArray[1].Name, "P5");

}

}

}

Обратите внимание, как легко можно получить данные, возвращаемые из метода контроллера. Мы вызываем свойство Model в результате, чтобы получить последовательность IEnumerable<Product>, которую мы генерировали в методе List. Затем мы можем проверить, те ли это данные, которые мы хотим получить. В этом случае мы преобразовали последовательность в массив, и проверили длину и значение отдельных объектов.

174

Отображаем ссылки на страницы

Если вы запустите приложение, то увидите, что на странице отображаются только четыре элемента. Если вы хотите просмотреть другую страницу, можно добавить параметры строки запроса в конец URL, например:

http://localhost:23081/?page=2

Вам нужно будет изменить порт в URL, чтобы он соответствовал порту вашего рабочего сервера ASP.NET. С помощью этих строк запросов мы можем просмотреть весь каталог товаров.

Конечно, этот способ известен только нам. Клиенты не знают, что можно использовать параметры строк запросов, и, даже если знают, мы все равно уверены, что они не собираются просматривать каталог таким образом. Нам нужно отображать ссылки на страницы под каждым списком товаров, чтобы клиенты могли переходить с одной страницы на другую. Для этого мы реализуем многоразовый вспомогательный метод HTML, похожий на методы Html.TextBoxFor и Html.BeginForm, с которыми мы работали в главе 2. Этот вспомогательный метод будет генерировать разметку HTML для необходимых нам навигационных ссылок.

Добавляем модель представления

Для поддержки вспомогательного метода HTML, мы будем передавать в представление информацию о количестве доступных страниц, текущей странице и общем количестве товаров в хранилище. Самый простой способ это сделать - создать модель представления, о которой мы кратко упоминали в главе 3. Добавьте класс под названием PagingInfo, показанный в листинге 7-16, в папку в Models в

проекте SportsStore.WebUI.

Листинг 7-16: Класс модели представления PagingInfo

using System;

namespace SportsStore.WebUI.Models

{

public class PagingInfo

{

public int TotalItems { get; set; } public int ItemsPerPage { get; set; } public int CurrentPage { get; set; }

public int TotalPages

{

get { return (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage); }

}

}

}

Модель представления не является частью доменной модели. Это просто удобный класс для передачи данных между представлением и контроллером. Чтобы подчеркнуть это, мы помещаем этот класс в проект SportsStore.WebUI, чтобы держать его отдельно от классов доменной модели.

Добавляем вспомогательный метод HTML

Теперь, когда у нас есть модель представления, мы реализуем вспомогательный метод HTML, который назовем PageLinks. Создайте в проекте SportsStore.WebUI новую папку под названием HtmlHelpers, и добавьте новый статический класс под названием PagingHelpers. Содержимое файла класса показано в листинге 7-17.

175

Листинг 7-17: Класс PagingHelpers

using System; using System.Text;

using System.Web.Mvc;

using SportsStore.WebUI.Models;

namespace SportsStore.WebUI.HtmlHelpers

{

public static class PagingHelpers

{

public static MvcHtmlString PageLinks( this HtmlHelper html,

PagingInfo pagingInfo, Func<int, string> pageUrl)

{

StringBuilder result = new StringBuilder();

for (int i = 1; i <= pagingInfo.TotalPages; i++)

{

TagBuilder tag = new TagBuilder("a"); // Construct an <a> tag tag.MergeAttribute("href", pageUrl(i));

tag.InnerHtml = i.ToString(); if (i == pagingInfo.CurrentPage)

tag.AddCssClass("selected");

result.Append(tag.ToString());

}

return MvcHtmlString.Create(result.ToString());

}

}

}

Метод расширения PageLinks генерирует HTML для набора ссылок на страницы, используя информацию, предоставленную в объекте PagingInfo. Параметр Func предоставляет возможность передачи делегата, который будет использоваться для генерации ссылок на другие страницы.

Модульный тест: создание ссылок на страницы

Для проверки вспомогательного метода PageLinks мы вызываем метод с тестовыми данными и сравниваем результаты с ожидаемым HTML. Метод модульного теста выглядит следующим образом:

using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq;

using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using SportsStore.WebUI.Controllers; using SportsStore.WebUI.Models; using System;

using System.Collections.Generic; using System.Linq;

using System.Web.Mvc;

using SportsStore.WebUI.HtmlHelpers;

namespace SportsStore.UnitTests

{

[TestClass]

public class UnitTest1

{

[TestMethod]

public void Can_Paginate()

{

176

// ...statements removed for brevity...

}

[TestMethod]

public void Can_Generate_Page_Links()

{

//Arrange - define an HTML helper - we need to do this

//in order to apply the extension method

HtmlHelper myHelper = null;

//Arrange - create PagingInfo data PagingInfo pagingInfo = new PagingInfo

{

CurrentPage = 2, TotalItems = 28, ItemsPerPage = 10

};

//Arrange - set up the delegate using a lambda expression Func<int, string> pageUrlDelegate = i => "Page" + i;

//Act

MvcHtmlString result = myHelper.PageLinks(pagingInfo, pageUrlDelegate);

// Assert

Assert.AreEqual(result.ToString(), @"<a href=""Page1"">1</a>"

+@"<a class=""selected"" href=""Page2"">2</a>"

+@"<a href=""Page3"">3</a>");

}

}

}

Этот тест проверяет вывод вспомогательного метода, используя значение литеральной строки, которая содержит двойные кавычки. С# прекрасно работает с такими строками до тех пор, пока мы ставим перед строкой @ и используем два набора двойных кавычек ("") вместо одного набора. Мы должны также помнить, что нельзя разбивать литеральную строку на отдельные строки, если только строка, с которой мы сравниваем, не разбита аналогично. Так, например, литерал, который мы используем в тестовом методе, занял две строки из-за небольшой ширины страницы. Мы не добавили символ новой строки; если бы мы это сделали, тест завершился бы неудачей.

Метод расширения доступен для использования только тогда, когда содержащее его пространство имен находится в области видимости. В файле кода это делается с помощью оператора using; в представлении Razor мы должны добавить запись конфигурации в файл Web.config, или оператор @using в само представление. Это может привести к путанице, но в проекте MVC Razor есть два файла Web.config: основной, который находится в корневой директории проекта приложения, и специальный для представлений, который находится в папке Views. Данное изменение мы должны провести в файле Views/Web.config, и оно показано в листинге 7-18.

Листинг 7-18: Добавляем пространство имен вспомогательного метода HTML в файл

Views/Web.config

<system.web.webPages.razor>

<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />

<pages pageBaseType="System.Web.Mvc.WebViewPage"> <namespaces>

<add namespace="System.Web.Mvc" /> <add namespace="System.Web.Mvc.Ajax" /> <add namespace="System.Web.Mvc.Html" />

<add namespace="System.Web.Optimization"/> <add namespace="System.Web.Routing" />

177

<add namespace="SportsStore.WebUI.HtmlHelpers"/>

</namespaces>

</pages>

</system.web.webPages.razor>

Каждое пространство имен, к которому нам потребуется обратиться в представлении Razor, нужно объявить либо таким образом, либо в самом представлении с помощью оператора @using.

Добавляем данные модели представления

Мы еще не вполне готовы использовать наш вспомогательный метод HTML. Нам осталось предоставить экземпляр класса модели представления PagingInfo в представление. Это можно было бы сделать, используя ViewBag, но лучше мы объединим все данные, которые мы собираемся отправить из контроллера в представление, в один класс модели представления. Для этого добавьте новый класс под названием ProductsListViewModel в папку Models проекта SportsStore.WebUI.

Содержание этого класса показано в листинге 7-19.

Листинг 7-19: Класс ProductsListViewModel

using System.Collections.Generic; using SportsStore.Domain.Entities;

namespace SportsStore.WebUI.Models

{

public class ProductsListViewModel

{

public IEnumerable<Product> Products { get; set; } public PagingInfo PagingInfo { get; set; }

}

}

Теперь мы можем обновить метод List в классе ProductController так, чтобы он начал использовать класс ProductsListViewModel и предоставлять представлению сведения о товарах, которые нужно отображать на странице, и информацию о нумерации страниц, как показано в листинге 7-20.

Листинг 7-20: Обновляем метод List

using System;

using System.Collections.Generic; using System.Linq;

using System.Web; using System.Web.Mvc;

using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using SportsStore.WebUI.Models;

namespace SportsStore.WebUI.Controllers

{

public class ProductController : Controller

{

private IProductRepository repository; public int PageSize = 4;

public ProductController(IProductRepository productRepository)

{

this.repository = productRepository;

}

public ViewResult List(int page = 1)

{

178

ProductsListViewModel model = new ProductsListViewModel

{

Products = repository.Products

.OrderBy(p => p.ProductID)

.Skip((page - 1) * PageSize)

.Take(PageSize), PagingInfo = new PagingInfo

{

CurrentPage = page, ItemsPerPage = PageSize,

TotalItems = repository.Products.Count()

}

};

return View(model);

}

}

}

Эти изменения передают объект ProductsListViewModel в качестве данных модели в представление.

Модульный тест: данные модели представления о нумерации страниц

Нам нужно убедиться, что контроллер отправляет в представление правильную информацию о нумерации страниц. Чтобы проверить это, мы добавили в наш проект с тестами следующий модульный тест:

[TestMethod]

public void Can_Send_Pagination_View_Model()

{

// Arrange

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"}, new Product {ProductID = 3, Name = "P3"}, new Product {ProductID = 4, Name = "P4"}, new Product {ProductID = 5, Name = "P5"}

}.AsQueryable());

// Arrange

ProductController controller = new ProductController(mock.Object); controller.PageSize = 3;

// Act

ProductsListViewModel result = (ProductsListViewModel)controller.List(2).Model;

// Assert

PagingInfo pageInfo = result.PagingInfo; Assert.AreEqual(pageInfo.CurrentPage, 2); Assert.AreEqual(pageInfo.ItemsPerPage, 3); Assert.AreEqual(pageInfo.TotalItems, 5); Assert.AreEqual(pageInfo.TotalPages, 2);

}

Нам также нужно изменить предыдущий модульный тест для нумерации страниц, который содержится в методе Can_Paginate. Он полагается на то, что метод действия List возвращает ViewResult, у которого свойство Model представляет собой последовательность объектов Product, но мы заключили данные в другой тип модели представления. Вот обновленный тест:

[TestMethod]

public void Can_Paginate()

179

{

//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"}, new Product {ProductID = 3, Name = "P3"}, new Product {ProductID = 4, Name = "P4"}, new Product {ProductID = 5, Name = "P5"}

}.AsQueryable());

//create a controller and make the page size 3 items ProductController controller = new ProductController(mock.Object); controller.PageSize = 3;

//Action

ProductsListViewModel result = (ProductsListViewModel)controller.List(2).Model;

// Assert

Product[] prodArray = result.Products.ToArray(); Assert.IsTrue(prodArray.Length == 2); Assert.AreEqual(prodArray[0].Name, "P4"); Assert.AreEqual(prodArray[1].Name, "P5");

}

Обычно мы бы создали общий метод настройки, учитывая степень дублирования между этими двумя тестовыми методами. Однако, так как мы выносим модульные тесты в отдельные блоки, такие как этот, мы будем держать весь код отдельно, чтобы вы могли видеть каждый тест целиком.

На данный момент представление ожидает последовательность объектов Product, так что нам необходимо обновить List.cshtml, как показано в листинге 7-21, чтобы работать с новым типом модели представления.

Листинг 7-21: Обновляем представление List.cshtml

@model SportsStore.WebUI.Models.ProductsListViewModel

@{

ViewBag.Title = "Products";

}

@foreach (var p in Model.Products)

{

<div class="item"> <h3>@p.Name</h3> @p.Description <h4>@p.Price.ToString("c")</h4>

</div>

}

Мы изменили директиву @model, чтобы сообщить Razor, что сейчас мы работаем с другим типом данных. Нам также нужно было обновить цикл foreach так, чтобы источником данных стало свойство Products данных модели.

Отображаем ссылки на страницы

Теперь у нас есть все, чтобы добавить ссылки на страницы представления List. Мы создали модель представления, которая содержит информацию о нумерации страниц, обновили контроллер так, чтобы эта информация передавалась в представление, и изменили директиву @model так, чтобы она

180

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]