Админ панель: список товаров

141 Исходный код проекта

В этой статье мы продолжим построение приложения GameStore и предоставим администратору сайта способ управления каталогом товаров. По соглашению при управлении коллекциями элементов пользователю предоставляются два типа страниц: страница списка и страница редактирования.

Структура CRUD для админ панели

Все эти страницы позволяют пользователю создавать, читать, обновлять и удалять (create, read, update, delete - CRUD) элементы в коллекции. Совместно такие действия называются операциями CRUD.

Разработчики нуждаются в реализации операций CRUD настолько часто, что среда Visual Studio пытается помочь в этом, предлагая сгенерировать контроллеры MVC, которые имеют методы действий для операций CRUD, и поддерживающие их шаблоны представлений. Однако, как и со всеми шаблонами Visual Studio, намного лучше изучить использование средств MVC Framework напрямую.

Создание контроллера CRUD

Для поддержки средств администрирования в приложении GameStore мы создадим новый контроллер. В окне Solution Explorer щелкните правой кнопкой мыши на папке Controllers проекта GameStore.WebUI и выберите в контекстном меню пункт Add --> Controller (Добавить --> Контроллер). В диалоговом окне Add Scaffold (Добавление шаблона) укажите вариант MVC 5 Controller - Empty (Контроллер MVC 5 - Пустой) и щелкните на кнопке Add. В диалоговом окне Add Controller введите для имени класса контроллера AdminController и щелкните на кнопке Add, чтобы создать файл Controllers/AdminController.cs.

Приведите содержимое этого файла в соответствие с кодом ниже:

using System.Web.Mvc;
using GameStore.Domain.Abstract;

namespace GameStore.WebUI.Controllers
{
    public class AdminController : Controller
    {
        IGameRepository repository;

        public AdminController (IGameRepository repo)
        {
            repository = repo;
        }

        public ViewResult Index()
        {
            return View(repository.Games);
        }
	}
}

В конструкторе контроллера объявлена зависимость от интерфейса IGameRepository, которую Ninject будет распознавать при создании экземпляров. В самом контроллере определен единственный метод действия Index(), который вызывает метод View(), чтобы выбрать стандартное представление для действия, передавая ему в качестве модели представления набор товаров из базы данных.

Модульное тестирование: метод действия Index()

Нас интересует поведение метода действия Index() в контроллере Admin, которое заключается в корректном возврате объектов Game, находящихся в хранилище. Это можно протестировать путем создания имитированной реализации хранилища и сравнения тестовых данных с данными, возвращаемыми методом действия. Ниже приведен код модульного теста, помещенный в новый файл модульных тестов по имени AdminTests.cs внутри проекта GameStore.UnitTests.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using GameStore.Domain.Abstract;
using GameStore.Domain.Entities;
using GameStore.WebUI.Controllers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;

namespace GameStore.UnitTests
{
    [TestClass]
    public class AdminTests
    {
        [TestMethod]
        public void Index_Contains_All_Games()
        {
            // Организация - создание имитированного хранилища данных
            Mock<IGameRepository> mock = new Mock<IGameRepository>();
            mock.Setup(m => m.Games).Returns(new List<Game>
            {
                new Game { GameId = 1, Name = "Игра1"},
                new Game { GameId = 2, Name = "Игра2"},
                new Game { GameId = 3, Name = "Игра3"},
                new Game { GameId = 4, Name = "Игра4"},
                new Game { GameId = 5, Name = "Игра5"}
            });

            // Организация - создание контроллера
            AdminController controller = new AdminController(mock.Object);

            // Действие
            List<Game> result = ((IEnumerable<Game>)controller.Index().
                ViewData.Model).ToList();

            // Утверждение
            Assert.AreEqual(result.Count(), 5);
            Assert.AreEqual("Игра1", result[0].Name);
            Assert.AreEqual("Игра2", result[1].Name);
            Assert.AreEqual("Игра3", result[2].Name);
        }
    }
}

Создание новой компоновки

Мы создадим новую компоновку для использования с административными представлениями GameStore. Это будет простая компоновка, которая предоставит единственную точку, где можно будет применять изменения ко всем административным представлениям.

Создайте новую компоновку, щелкнув правой кнопкой мыши на папке Views/Shared в проекте GameStore.WebUI и выбрав в контекстном меню пункт Add --> MVC 5 Layout Page (Razor) (Добавить --> Страница компоновки MVC 5 (Razor)). Укажите в качестве имени _AdminLayout.cshtml (обратите внимание на начальный символ подчеркивания) и щелкните на кнопке ОК, чтобы создать файл Views/Shared/_AdminLayout.cshtml. Приведите содержимое нового файла представления в соответствие с кодом ниже:

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <link href="~/Content/bootstrap.css" rel="stylesheet" />
    <link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
    <link href="~/Content/ErrorStyles.css" rel="stylesheet" />
    <title></title>
</head>
<body>
    <div>
        @RenderBody()
    </div>
</body>
</html>

Как объяснялось ранее, по соглашению имя компоновки начинается с символа подчеркивания "_". Механизм визуализации Razor также используется другой технологией Microsoft под названием WebMatrix, в которой символ подчеркивания применяется для предотвращения обслуживания страниц компоновки браузерами. В MVC подобного рода защита не нужна, однако упомянутое соглашение об именовании представлений все равно соблюдается во всех приложениях MVC.

В компоновку добавлен вызов метода RenderBody(), поэтому содержимое представления, для которого используется компоновка, будет вставлено в ответ, предназначенный серверу. Были также добавлены элементы <link> для файлов Bootstrap и файлов CSS, содержащих стили, которые созданы в целях подсветки полей, вызвавших ошибки проверки достоверности.

Реализация представления списка

Теперь, имея новую компоновку, можно добавить в проект представление для метода действия Index() контроллера Admin. Хотя мы не являемся поклонниками средств формирования шаблонов Visual Studio, мы собираемся создать представление для метода Index() с применением системы формирования шаблонов, чтобы продемонстрировать ее работу. Один лишь факт, что нам не нравится предварительно заготовленный код, вовсе не означает, что вы не должны им пользоваться.

Щелкните правой кнопкой мыши на папке Views/Admin в проекте GameStore.WebUI и выберите в контекстном меню пункт Add --> View (Добавить --> Представление). Введите в поле View Name (Имя представления) строку Index, выберите в списке Template (Шаблон) вариант List (обычно здесь выбирался один из вариантов Empty), укажите Game в списке Model Class (Класс модели), отметьте флажок Use a layout page (Использовать страницу компоновки) и выберите файл _AdminLayout.cshtml из папки Views/Shared. Все конфигурационные настройки показаны на рисунке ниже:

Конфигурирование шаблонного представления

При использовании формирования шаблонов вида List среда Visual Studio предполагает, что вы работаете с последовательностью IEnumerable типа модели представления, поэтому можете просто выбрать в списке форму класса в единственном (а не множественном) числе.

Щелкните на кнопке Add, чтобы создать новое представление, которое будет иметь содержимое, показанное в примере ниже:

@model IEnumerable<GameStore.Domain.Entities.Game>

@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_AdminLayout.cshtml";
}

<h2>Index</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Name)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Description)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Category)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Price)
        </th>
        <th></th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Name)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Description)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Category)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Price)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.GameId }) |
            @Html.ActionLink("Details", "Details", new { id=item.GameId }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.GameId })
        </td>
    </tr>
}

</table>

Среда Visual Studio выясняет тип объекта модели представления и генерирует элементы HTML-таблицы, которые соответствуют свойствам, определенным данным типом модели. Чтобы посмотреть, как это представление визуализируется, необходимо запустить приложение и перейти на URL вида /Admin/Index. Результат показан на рисунке ниже:

Визуализация шаблонного представления вида List

Шаблонное представление является разумной попыткой настройки удобной отправной точки. Мы имеем колонки для каждого свойства класса Game и ссылки на операции CRUD, связанные с методами действий в контроллере Admin (хотя, поскольку этот контроллер создавался без формирования шаблонов, методы действий не существуют).

Средство формирования шаблонов достаточно интеллектуально, но генерируемые им представления являются обыденными и настолько общими, что становятся бесполезными в проекте любого уровня сложности. Принятый нами подход предполагает создание вначале пустых контроллеров, представлений и компоновок и постепенное добавление необходимой функциональности.

Вернувшись к такому подходу, отредактируйте разметку в файле Index.cshtml:

@model IEnumerable<GameStore.Domain.Entities.Game>

@{
    ViewBag.Title = "Админ панель: список товаров";
    Layout = "~/Views/Shared/_AdminLayout.cshtml";
}

<div class="panel panel-default">
    <div class="panel-heading">
        <h3>Список игр</h3>
    </div>
    <div class="panel-body">
        <table class="table table-striped table-condensed table-bordered">
            <tr>
                <th class="text-right">ID</th>
                <th>Название</th>
                <th class="text-right">Цена</th>
                <th class="text-center">Действия</th>
            </tr>
            @foreach (var item in Model)
            {
                <tr>
                    <td class="text-right">@item.GameId</td>
                    <td>@Html.ActionLink(item.Name, "Edit", new { item.GameId })</td>
                    <td class="text-right">@item.Price.ToString("# руб")</td>
                    <td class="text-center">
                        @using (Html.BeginForm("Delete", "Admin"))
                        {
                            @Html.Hidden("GameId", item.GameId)
                            <input type="submit"
                                   class="btn btn-default btn-xs"
                                   value="Удалить" />
                        }
                    </td>
                </tr>
            }
        </table>
    </div>
    <div class="panel-footer">
        @Html.ActionLink("Добавить игру", "Create", null,
            new { @class = "btn btn-default" })
    </div>
</div>

Это представление отображает информацию в более компактной форме, опуская некоторые свойства класса Game и применяя для стилизации библиотеку Bootstrap. Результат визуализации этого представления показан на рисунке:

Визуализация модифицированного представления списка товаров

Теперь мы имеем неплохо выглядящую страницу списка. Администратор может видеть товары в каталоге, к тому же имеются ссылки или кнопки для добавления, удаления и просмотра элементов. В следующих статьях мы добавим функциональность для поддержки всех этих действий.

Пройди тесты
Лучший чат для C# программистов