Основы. Часть 8 - Области

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

Как правило блок администрирования располагается в каталоге Admin или Administrator, куда вход закрыт паролем. На текущем этапе не будем вдаваться в подробности авторизации в ASP.NET MVC. Поэтому пока просто сделаем так, чтобы управления каталогом была доступно по адресу http://<имя сервера>/Admin/.

Решение "в лоб" и почему так делать не стоит

В качестве решения можно создать Контроллер Admin и переложить все функции на него. Вроде бы логично, но к чему это приведёт? Давайте представим, сколько может быть задачи у системы управления веб-приложением? Легко понять, что в реальном проекте такой Контроллер будет содержать очень большое число Действий. В результате, такой подход усложнит дальнейшее сопровождение кода.

Также необходимо отметить, что созданный в этом случае класс AdminController объединит в себе функции многих Контроллеров, у которых были четко определенные задачи. По сути, получится "мастер на все руки", что является плохой практикой при проектировании и разработке приложений. При этом от слова Admin в тексте ссылки необходима только группировка функциональности по её общему смыслу.

Поэтому, для решения подобных задач в ASP.NET MVC была введена поддержка областей (Area).

Использование областей (Area) в ASP.NET MVC 3

Идея областей очень простая – позволить разделить большой проект на несколько логических частей, которые могут взаимодействовать друг с другом. При этом они вправе содержать собственные Контроллеры и Представления, а также расширять Модель своими классами.

Кроме того, таким образом можно разделить большой проект между несколькими разработчиками или группами. Например, на сайте могут быть разделы форум и блог со ссылками http://<имя сервера>/Forum/ и http://<имя сервера>/Blog/ соответственно. При этом они могут использовать общую Модель для идентификации посетителя сайта, но при этом иметь и собственные элементы бизнес-логики.

Рассмотрим создание области на конкретном примере.

Создание области

Создание области ASP.NET MVCДля создания новой области выберем в контекстном меню проекта, открывающемся в Solution Explorer / Navigation Explorer, пункт "Add > Area ..." и в появившемся диалоге "Add Area" введем имя Admin.

Помощник ASP.NET MVC 3 добавит в проект папку Areas, в которой будут размещены все дополнительные области. При этом главной или областью по умолчанию будем называть корневой каталог веб-приложения.

Структура области ASP.NET MVCВ папке Areas размещена подпапка только что созданной области Admin. Внутри последней создана структура, аналогичная структуре самого проекта: папки Controllers, Models и Views. Таким образом, область может использовать как классы проекта, так и добавлять свои специфические объекты.

Создание Контроллеров и Представлений осуществляется также с использованием помощника ASP.NET MVC или заготовок, для которых область указывается в качестве значения параметра -Area. И как еще один вариант, можно добавлять классы и файлы вручную.

Стоит обратить внимание на файл <имя области>AreaRegistration.cs (в данном случае это AdminAreaRegistration.cs). В нем расположена информация для регистрации области в ядре ASP.NET MVC 3. Взглянем на него чуть подробнее:

namespace BookCatalog.Areas.Admin
{
    using System.Web.Mvc;

    public class AdminAreaRegistration : AreaRegistration
    {
        public override string AreaName
        {
            get { return "Admin"; }
        }

        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.MapRoute(
                "Admin_default",
                "Admin/{controller}/{action}/{id}",
                new {
                    controller = "Catalog",
                    action = "Index",
                    id = UrlParameter.Optional
                }
            );
        }
    }
}

Класс AdminAreaRegistration является наследником AreaRegistration. Его свойство AreaName содержит имя области, которое будет использоваться внутри ASP.NET MVC. Метод RegisterArea() предназначен для выполнения необходимых действий при её регистрации. В данном случае определяется маршрут, по которому будет доступна данная область. Выделенная в исходном коде строка добавлена для указания Контроллера по умолчанию.

Все области проекта регистрируется в Application_Start() при помощи AreaRegistration.RegisterAllAreas().

Рассмотрим необходимые действия для переноса Контроллеров и Представлений из главной в только что созданную область. При этом Модель затронута не будет.

Переносим функциональность

Для переноса функциональности в область Admin просто скопируем в неё все Контроллеры и их Представления. Сделать это можно просто перетащив их мышкой в Solution Explorer / Navigation Explorer. Затем исправим пространства имен с BookCatalog.Controllers на соответствующее новому месту расположения – BookCatalog.Areas.Admin.Controllers.

Создадим новый шаблон разметки _Layout.cshtml в папке Areas/Admin/Views/Shared. Его содержимое можно скопировать с существующего варианта в главной области. Исключение составит только новая ссылка для возврата к главной странице:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/modernizr-1.7.min.js")" type="text/javascript"></script>
</head>
<body>
    <div class="menu">
        @Html.ActionLink("Catalog", "Index", "Catalog")&nbsp;|&nbsp;
        @Html.ActionLink("Publishers", "Index", "Publishers")&nbsp;|&nbsp;
        @Html.ActionLink("Languages", "Index", "Languages")&nbsp;|&nbsp;
        @Html.ActionLink("Tags", "Index", "Tags")&nbsp;|&nbsp;
        @Html.ActionLink("Back", string.Empty, string.Empty, new { area = "" }, null)
    </div>
    @RenderBody()
</body>
</html>

Ссылки, создаваемые с помощью ActionLink(), осуществляют навигацию в границах текущей области. Чтобы выйти за её пределы необходимо явно указать это в параметрах метода. В выделенной строке осуществляется переход к главной странице веб-приложения. При этом будут задействованы Контроллер и Действие по умолчанию, определенные для главной области.

Следующим шагом является создание файла _ViewStart.cshtml с указанием Представлениям в Areas/Admin/Views использовать новый шаблон разметки:

@{
    Layout = "~/Areas/Admin/Views/Shared/_Layout.cshtml";
}

Добавляем функции в главную область

Новый Контроллер

Поскольку в главной области не осталось Контроллеров, добавим в неё один для отображения списка книг. С этой целью создадим класс CatalogController в папке Controllers со следующим кодом:

namespace BookCatalog.Controllers
{
    using System.Web.Mvc;
    using BookCatalog.Models.Repositories;

    public class CatalogController : Controller
    {
        private readonly IBookDetailsRepository _bookdetailsRepository;

        // If you are using Dependency Injection, you can delete the following constructor
        public CatalogController()
            : this(new BookDetailsRepository())
        {
        }

        public CatalogController(IBookDetailsRepository bookdetailsRepository)
        {
            this._bookdetailsRepository = bookdetailsRepository;
        }

        // GET: /Catalog/
        public ViewResult Index()
        {
            return this.View(
                this._bookdetailsRepository.AllIncluding(
                    bookdetails => bookdetails.Language,
                    bookdetails => bookdetails.Publisher,
                    bookdetails => bookdetails.Tags));
        }
    }
}

Здесь определено только одно Действие Index, ответственное за вывод списка. Обратите внимание, что разные области могут содержать Контроллеры с одинаковыми именами.

Представление для отображения каталога книг

Теперь добавим Представление для нового Контроллера: в папке View создадим подпапку Catalog и разместим в ней файл Index.cshtml:

@model IEnumerable<BookCatalog.Models.BookDetails>
@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<table>
    <tr>
        <th>Title</th>
        <th>Author</th>
        <th>PublishedAt</th>
        <th>Url</th>
        <th>Description</th>
        <th>Tags</th>
        <th>Rating</th>
        <th>IsFree</th>
        <th>Language</th>
        <th>Publisher</th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>@item.Title</td>
        <td>@item.Author</td>
        <td>@String.Format("{0:D}", item.PublishedAt)</td>
        <td>@item.Url</td>
        <td>@item.Description</td>
        <td>@string.Join(", ", item.Tags.Select(tag => tag.Text).ToArray())</td>
        <td>@item.Rating</td>
        <td>@item.IsFree</td>
        <td>@(item.Language == null ? "None" : item.Language.Name)</td>
        <td>@(item.Publisher == null ? "None" : item.Publisher.Title)</td>
    </tr>
}

</table>

В отличие от Представления из области Admin, здесь удалён вывод ссылок на создание, редактирование и удавление записей о книгах. Кроме того, отсутствует столбец IsVisible, который в дальнейшем и будет отвечать за отображение записи в данном списке.

Изменяем файл разметки

Последнее изменение в главной области – файл разметки. Там также больше не нужны ссылки на различные таблицы. Вместо них поставим только одну ссылку – на область администрирования:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/modernizr-1.7.min.js")" type="text/javascript"></script>
</head>
<body>
    <div class="menu">
        @Html.ActionLink("Adminisrtrator", string.Empty, string.Empty, new { area = "Admin" }, null)
    </div>
    @RenderBody()
</body>
</html>

Здесь также явно указана область, к которой осуществляется переход. При этом будут использованы Контроллер и Действие по умолчанию.

Одинаковые имена Контроллеров

Если сейчас запустить веб-приложение на выполнение, то результатом будет сообщение об ошибке. Дело в том, что ядро ASP.NET MVC 3 видит два класса с одинаковыми именами CatalogController. Оба могут быть Контроллером Catalog, указанным как вариант по умолчанию.

Чтобы разрешить эту ситуацию, необходимо явно указать пространство имен при настройке маршрутов. Для этого в файл Global.asax внесем соответствующее изменение:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        "Default", // Route name
        "{controller}/{action}/{id}", // URL with parameters
        new {
            controller = "Catalog",
            action = "Index",
            id = UrlParameter.Optional
        }, // Parameter defaults
        new string[] { "BookCatalog.Controllers" }
    );
}

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


Исходный код проекта (C#, Visual Studio 2010): mvc3-in-depth-basics-08.zip

Комментарии (28) -

Александр 04.07.2011 23:22:49

Андрей, ошибочка есть в коде контроллера CatalogConrtoller в методе

public ViewResult Index()
        {
            return this.View(
                this._bookdetailsRepository.AllIncluding(
                    bookdetails => bookdetails.Language, bookdetails =>   bookdetails.Publisher, bookdetails => bookdetails.Tags));
        }
чтобы представление работало нужно прочесть еще и св-во bookdetails.Tags

@ Александр: Вы правы. С другой стороны меня заинтересовало не сколько, почему данный кусок кода не был создан заготовкой (этот метод - копия того, что был сгенерирован в прошлых частях). Более интересно почему данная ошибка не проявляется при использовании SQL Server Compact (стоило подключить SQL Server и она себя сразу проявила). Если будет время - погляжу в чем причина. Спасибо.

Спасибо за статьи. Изучаю Asp.Net по ним Smile

Виталий 20.09.2011 1:24:33

Доброго времени суток Андрей.
Вот начал изучать asp.net mvc и наткнулся на вашу статью, что очень оказалось кстати. Но вот пробую сделать то что тут сделано, но когда тыкаешь на ссылку Languages в проекте, вылезает ошибка:
"Ошибка сервера в приложении '/'.
--------------------------------------------------------------------------------

Не удалось найти данный ресурс.
Описание: HTTP 404. Возможно, искомый ресурс (или один из зависимых от него компонентов) удален, получил другое имя или временно недоступен.  Просмотрите следующий URL-адрес и проверьте, что он введен правильно.

Запрошенный URL: /Admin/Languages


--------------------------------------------------------------------------------
Информация о версии: Платформа Microsoft .NET Framework, версия:4.0.30319; ASP.NET, версия:4.0.30319.237 "

Виталий 20.09.2011 2:16:50

Вопрос снят, я сам ошибся((

Paul Stark 11.10.2011 16:05:56

Сталкнулся с таким моментом: пока в методе RegisterArea() не указал контроллер, администраторская область была недоступна.


public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { controller="Home", action = "Index", id = UrlParameter.Optional }
);
}


Только у меня используется HomeController. И это кстати еще интересный момент. Я так понимая, здесь не нужно указывать пространство имен, т.к. админская область не видит общую.

Не совсем понял. Этот ведь код создается автоматически.

@ Andrey :

С другой стороны меня заинтересовало не сколько, почему данный кусок кода не был создан заготовкой (этот метод - копия того, что был сгенерирован в прошлых частях). Более интересно почему данная ошибка не проявляется при использовании SQL Server Compact (стоило подключить SQL Server и она себя сразу проявила). Если будет время - погляжу в чем причина. Спасибо.
Удалось выяснить в чем причина?

@ Michal: Нет, т.к. не удалось воспроизвести в дальнейшем.

@ Andrey:
Заметил следующее:
данная ошибка не проявляется при использовании SQL Server Compact
верно
стоило подключить SQL Server и она себя сразу проявила)
да, и вот что пишется

Существует назначенный этой команде Command открытый DataReader, который требуется предварительно закрыть.

Однако, если в connectionString добавить
MultipleActiveResultSets=true
, она исчезнет. Или использовать Include(), что мне кажется, правильнее.
почему данный кусок кода не был создан заготовкой - Похоже, это баг Scaffold'a, использующего репозиторий.

Можно ли создать еще одну область (например, Main), перенести в нее всю логику из главной области, и сделать область Main отображаемой по умолчанию? Я пробовал сделать по аналогии с примером в статье, но при запуске приложения получаю ошибку

Не удалось найти данный ресурс.
Описание: HTTP 404. Возможно, искомый ресурс (или один из зависимых от него компонентов) удален, получил другое имя или временно недоступен.  Просмотрите следующий URL-адрес и проверьте, что он введен правильно.

Запрошенный URL: /

@ Drongo: Можно, главное правильно указать MapRoute() до неё как путь по умолчанию.

Андрей, спасибо за ответ. Я пробовал указывать маршрут в файле Global.asax следующим образом:

            

routes.MapRoute(
                "Default",
                "Main/{controller}/{action}/{id}",
                new { controller = "Catalog", action = "Index", id = UrlParameter.Optional }
                , new string[] { "BookCatalog.Areas.Admin.Controllers.CatalogController" }
            );


И в результате получаю ошибку, которую указал в предыдущем посте. Не подскажете, в чем может быть проблема, куда стоит "копать"?

В MVC похоже есть баг, т.к. в этом случае View ищутся в основной части сайта. Поэтому необходимо добавить указание Area и внести сам Area в список.

routes.MapRoute(
    "Default",
    "{controller}/{action}/{id}",
    new { area = "Admin", controller = "Home", action = "Index", id = UrlParameter.Optional },
    new string[] { "MvcApplication1.Areas.Admin.Controllers" }
).DataTokens.Add("area", "Admin");

@ Andrey:

Спасибо, все заработало.
Отдельное спасибо за статьи Smile

Спасибо огромное за Ваши труды;)

@ Саша: Пожалуйста.

Робин 29.05.2012 2:52:28

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

@ Робин: В итоге вы разделяете сайт на части. Например, общую и административную. Каждая имеет свои Контроллеры и Представления, а также может иметь свои Модели.По сути вы логически разбиваете сайт на части. Например, как еще добавить www.site.com/Admin/? Через контроллер Admin? Представьте сколько у него будет Действий и зависимостей от других классов. А через области это сделать проще и удобнее.

Дамир Гарипов 18.12.2012 19:06:58

Кто подскажет почему такая конструкция не работает

return this.View(

                  this._bookdetailsRepository.AllIncluding(
                      bookdetails => bookdetails.Language,
                      bookdetails => bookdetails.Publisher,
                      bookdetails => bookdetails.Tags));


(кусок приведен из вашего примера у меня другая база, но алгоритм тот же). Модель базы данных создана на основе существующей путем POCO Generated. При попытке чтения зависимостей выскакивает ошибка:
Существует назначенный этой команде Command открытый DataReader, который требуется предварительно закрыть
В общем не может прочесть внедренные зависимости. Почему так происходит? вроде все сделал правильно.

@ Дамир Гарипов: Первое о чем подумал, а контекст не закрыт(существует) на момент чтения зависимостей?

Дамир Гарипов 19.12.2012 0:44:34

@ Andrey:

да не должен быть закрыт. Ведь при чтении зависимости как раз и выдается ошибка, что мол соединение заблокировано и не может использоваться для другого DataReader

Дамир Гарипов 19.12.2012 1:11:52

вот код unit-теста

[TestMethod]

        public void Responsible()
        {
            //Организация

            IResponsibleRepository repository = new ResponsibleRepositoryTest();

            //Действие

            IQueryable<Responsible> responsibles = repository.AllIncluding(p => p.Person);

            //Утверждение

            foreach (var res in responsibles)
            {
                Assert.IsTrue(res.Guid.ToString().ToUpper() ==
                      "E8B77F0B-0447-E211-A703-005056C00008");

                Assert.IsTrue(res.Person.FullName == "Иванов Петр Сидорович");

            }


свйство res.Guid читает нормально, а вот при чтении зависимости Person выходит ошибка. И что интересно, когда я ставлю брейкпоинт на foreach и навожу мышкой на responsible, то в "представлении результатов" все прекрасно считывается и все значения заполняются

@ Дамир Гарипов: Вот это и смущает. Я бы смотрел как работает класс ResponsibleRepositoryTest и его метод AllIncluding(). Особенно учитывая что он возвращает IQueryable (что мне очень не нравится в дизайне классов).

Возможно он создает контекст именно на период запроса, после чего он живет еще какое-то время до сборки мусора. Поэтому с breakpoint срабатывает, а без не - exception.

Дамир Гарипов 19.12.2012 11:05:23


using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using sc.Infrastructure;
using sc.Models;

namespace sc.Tests
{
    class ResponsibleRepositoryTest : IResponsibleRepository
    {
            private readonly ScContext _context;

            public ResponsibleRepositoryTest()
            {
                var builder = new SqlConnectionStringBuilder
                {
                    DataSource = @".\sqlexpress",
                    InitialCatalog = "ic",
                    IntegratedSecurity = true,
                    UserID = "user",
                    Password = "password"
                    //,MultipleActiveResultSets = true
                };

                _context = new ScContext(builder.ToString());
            }

            public IQueryable<Responsible> AllIncluding(params Expression<Func<Responsible, object>>[] includeProperties)
            {
                IQueryable<Responsible> query = _context.Responsibles;

                foreach (var includeProperty in includeProperties)
                {
                    query.Include(includeProperty);
                }

                //query.Include("Person");

                return query;
            }

            public void Dispose()
            {
                _context.Dispose();
            }

    }
}

@ Дамир Гарипов: Не сразу заметил, но замените IQueryable на IEnumerable. Или в тестовом методе до foreach (var res in responsibles) поставьте что-то вроде  var results = responsibles.ToList(); и сам foreach уже по results делайте.

Проблема в том, что IQueryable - это сам запрос, который выполняется отложено. Предполагаю, что он пытается прочитать БД через один контекст дважды (отсюда и исключение), а в случае с точкой останова, загружает весь список и использует уже его. Точнее не скажу - код проверять некогда, а IQueryable, из-за его сути, я никогда не использовал для передачи данных (поэтому и не сталкивался).

Дамир Гарипов 19.12.2012 17:54:22

Спасибо!!! Помогло)))

Артем 03.12.2014 18:48:40

День добрый Андрей.
Статья конечно же не про роли а облостя, но затрагивает разграничение между админом и остальными. То что вы показали все предельно ясно НО!
Не понять как программа определяет Админ и Остальных.

В интернете примеры с Атрибутами Roles = "Administrators" которые можно присваивать контроллерам или функциям контроллера

В вашем же примере нет ясности на счет этого.

Не могли бы вы дать короткий комментарий или дописать статью что было бы намного лучше для начинающих.

ОГРОМНОЕ спасибо за проделанную вами работу.
Буду одним из постоянных читателей вашего блога.

Добавить комментарий