Создание специального механизма визуализации

118

В предыдущих статьях было показано, что методы действий контроллеров могут возвращать объекты ActionResult. Как вы узнали, наиболее часто используемым результатом действия является ViewResult, который приводит к визуализации представления и возврату его клиенту. Вы уже видели применение представлений во многих примерах, поэтому должны приблизительно понимать, что они делают. В этой и последующих статьях будет внесена необходимая конкретика.

Мы собираемся впасть в крайность и создать специальный механизм визуализации. В большинстве проектов делать это не придется, поскольку инфраструктура ASP.NET MVC Framework содержит встроенный механизм визуализации Razor. Этот механизм использовался во всех примерах, приведенных ранее.

Ценность создания специального механизма визуализации заключается в демонстрации функционирования конвейера обработки запросов и завершении описания работы ASP.NET MVC Framework. Это также предполагает понимание того, насколько большую свободу имеют механизмы визуализации при трансляции ViewResult в ответ, предназначенный клиенту. Механизмы визуализации реализуют интерфейс IViewEngine, который показан в примере ниже:

namespace System.Web.Mvc
{
    public interface IViewEngine
    {
        ViewEngineResult FindPartialView(ControllerContext ControllerContext, 
            string partialViewName, bool useCache);
        ViewEngineResult FindView(ControllerContext ControllerContext, 
            string viewName, string masterName, bool useCache);
        void ReleaseView(ControllerContext ControllerContext, IView view);
    }
}

Роль механизма визуализации заключается в трансляции запросов к представлениям в объекты ViewEngineResult. Первые два метода в интерфейсе - FindPartialView() и FindView() - принимают параметры, которые описывают запрос и обрабатывающий его контроллер (объект ControllerContext), имя представления и его компоновку, а также признак того, разрешено ли механизму визуализации повторно использовать предыдущий результат из кеша. Эти методы вызываются во время обработки ViewResult. Последний метод - ReleaseView() - вызывается, когда представление больше не требуется.

Поддержка механизмов визуализации в MVC Framework реализована классом ControllerActionInvoker, который является встроенной реализацией интерфейса IActionInvoker. В случае реализации собственного активатора действий или фабрики контроллеров непосредственно из интерфейсов IActionInvoker или IControllerFactory автоматического доступа к средству механизмов визуализации не будет.

Класс ViewEngineResult позволяет механизму визуализации отвечать инфраструктуре MVC Framework, когда запрошено представление. Код класса ViewEngineResult приведен в примере ниже:

using System.Collections.Generic;

namespace System.Web.Mvc
{
    public class ViewEngineResult
    {
        public ViewEngineResult(IEnumerable<string> searchedLocations)
        {
            if (searchedLocations == null)
            {
                throw new ArgumentNullException("searchedLocations");
            }

            SearchedLocations = searchedLocations;
        }

        public ViewEngineResult(IView view, IViewEngine viewEngine)
        {
            if (view == null)
            {
                throw new ArgumentNullException("view");
            }
            if (viewEngine == null)
            {
                throw new ArgumentNullException("viewEngine");
            }

            View = view;
            ViewEngine = viewEngine;
        }

        public IEnumerable<string> SearchedLocations { get; private set; }
        public IView View { get; private set; }
        public IViewEngine ViewEngine { get; private set; }
    }
}

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

public ViewEngineResult(IView view, IViewEngine viewEngine)

В качестве параметров этому конструктору передается реализация интерфейса IView и объект механизма визуализации (так что метод ReleaseView() может быть вызван позже). Если механизм визуализации не может предоставить представление для запроса, то применяется следующий конструктор:

public ViewEngineResult(IEnumerable<string> searchedLocations)

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

Если вы считаете класс ViewEngineResult несколько неудобным в использовании, то вы не одиноки со своим мнением. Выражение результатов с применением нескольких версий конструктора класса - странный подход, который совершенно не согласуется с остальными проектными решениями, положенными в основу инфраструктуры MVC Framework.

Последним строительным блоком в системе механизма визуализации является интерфейс IView, который показан в примере ниже:

using System.IO;

namespace System.Web.Mvc
{
    public interface IView
    {
        void Render(ViewContext viewContext, TextWriter writer);
    }
}

Реализация IView передается конструктору класса ViewEngineResult, экземпляр которого возвращается из методов механизма визуализации. Инфраструктура MVC Framework затем вызывает метод Render(). Параметр ViewContext сообщает информацию о запросе из клиента и вывод из метода действия. Параметр TextWriter предназначен для записи вывода на стороне клиента.

В классе ViewContext определены свойства, которые предоставляют доступ к информации о запросе и деталях о том, как он обрабатывался MVC Framework до сих пор. Наиболее полезные свойства этого класса описаны в таблице ниже:

Полезные свойства класса ViewContext
Свойство Описание
Controller

Возвращает реализацию интерфейса IController, которая обработала текущий запрос

RequestContext

Возвращает детали текущего запроса

RouteData

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

TempData

Возвращает временные данные, связанные с запросом

View

Возвращает реализацию интерфейса IView, которая будет обрабатывать запрос. Очевидно, что это будет текущий класс, если создается специальная реализация представления

ViewBag

Возвращает экземпляр типа object, представляющий объект ViewBag

ViewData

Возвращает словарь данных модели представления, который также содержит данные ViewBag и метаданные для модели. Подробности приведены в таблице ниже

Самым интересным свойством из всех перечисленных является ViewData, которое возвращает объект ViewDataDictionary. В классе ViewDataDictionary определено несколько полезных свойств, предоставляющих доступ к модели представления, ViewBag и метаданным модели представления. Наиболее полезные свойства классе ViewDataDictionary описаны в таблице:

Полезные свойства класса ViewDataDictionary
Свойство Описание
Keys

Возвращает коллекцию значений ключей для данных в словаре, которые могут использоваться для доступа к свойствам ViewBag

Model

Возвращает объект модели представления для запроса

ModelMetadata

Возвращает объект ModelMetadata, который может применяться для рефлексии типа модели

ModelState

Возвращает информацию о состоянии модели

Как упоминалось ранее, увидеть, как все работает, т.е. каким образом функционируют вместе IViewEngine, IView и ViewEngineResult, проще всего путем создания механизма визуализации. Мы построим простой механизм визуализации, возвращающий одну разновидность представления. Это представление будет визуализировать результат, который содержит информацию о запросе и данные представления, сгенерированные методом действия. Такой подход позволяет продемонстрировать способ функционирования механизмов визуализации, не отвлекаясь на разбор шаблонов представлений.

Подготовка проекта для примера

Для этой статьи мы создали новый проект MVC по имени Views с использованием шаблона Empty (Пустой) и отметкой флажка MVC в разделе Add folders and core references for (Добавить папки и основные ссылки для). В проект добавлен контроллер Home, код которого показан в примере ниже:

using System;
using System.Web.Mvc;

namespace Views.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = "Hello World";
            ViewBag.Time = DateTime.Now.ToShortTimeString();
            return View("DebugData");
        }

        public ActionResult List()
        {
            return View();
        }
    }
}

В этом проекте представления не создаются, поскольку вместо работы с Razor будет реализован специальный механизм визуализации.

Создание специальной реализации IView

Начнем с создания реализации интерфейса IView. Для этого в пример проекта добавляется папка Infrastructure, внутри которой создается файл класса по имени DebugDataView.cs с содержимым, показанным в примере ниже:

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

namespace Views.Infrastructure
{
    public class DebugDataView : IView
    {
        public void Render(ViewContext viewContext, TextWriter writer)
        {
            Write(writer, "---Данные маршрутизации---");
            foreach (string key in viewContext.RouteData.Values.Keys)
            {
                Write(writer, "Ключ: {0}, Значение: {1}",
                    key, viewContext.RouteData.Values[key]);
            }

            Write(writer, "---Данные представления---");
            foreach (string key in viewContext.ViewData.Keys)
            {
                Write(writer, "Ключ: {0}, Значение: {1}", key,
                    viewContext.ViewData[key]);
            }
        }

        private void Write(TextWriter writer, string template, params object[] values)
        {
            writer.Write(string.Format(template, values) + "<p/>");
        }
    }
}

В этом представлении демонстрируется применение двух параметров метода Render(): мы получаем значения из ViewContext и записываем ответ клиенту с помощью TextWriter. Сначала выводятся данные маршрутизации, а затем данные ViewBag.

Средство данных представления является наследием ранних версий MVC Framework, выпущенных до появления в языке C# поддержки динамических объектов. Данные представления - это менее гибкий предшественник данных ViewBag и они больше напрямую не используются за исключением случаев, когда создаются специальные реализации IView и необходимо обеспечить простой доступ к свойствам, определенным в объекте ViewBag.

Создание реализации IViewEngine

Вспомните, что механизм визуализации предназначен для построения объекта ViewEngineResult, который содержит либо IView, либо список мест для поиска подходящего представления. Теперь, когда есть рабочая реализация IView, можно создать механизм визуализации. Для этого в папку Infrastructure добавляется новый файл класса по имени DebugDataViewEngine.cs, содержимое которого приведено в примере ниже:

using System.Web.Mvc;

namespace Views.Infrastructure
{
    public class DebugDataViewEngine : IViewEngine
    {

        public ViewEngineResult FindView(ControllerContext controllerContext,
                string viewName, string masterName, bool useCache)
        {

            if (viewName == "DebugData")
            {
                return new ViewEngineResult(new DebugDataView(), this);
            }
            else
            {
                return new ViewEngineResult(
                    new string[] { "Нет представления (Debug Data View Engine)" });
            }
        }

        public ViewEngineResult FindPartialView(ControllerContext controllerContext,
                string partialViewName, bool useCache)
        {

            return new ViewEngineResult(new string[] { "Нет представления (Debug Data View Engine)" });
        }

        public void ReleaseView(ControllerContext controllerContext, IView view)
        {
            // Нет реализации
        }
    }
}

Мы планируем поддерживать только одно представление под названием DebugData. Когда поступит запрос для этого представления, мы возвратим новый экземпляр нашей реализации IView, примерно так:

return new ViewEngineResult(new DebugDataView(), this);

В случае реализации более серьезного механизма визуализации мы могли бы искать шаблоны, принимая во внимание компоновку и настройки кеширования. В данной ситуации наш простой пример требует создания только нового экземпляра класса DebugDataView. Если получен запрос к представлению, отличному от DebugData, то возвращается ViewEngineResult следующего вида:

return new ViewEngineResult(
    new string[] { "Нет представления (Debug Data View Engine)" })

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

Специальный механизм визуализации не поддерживает частичные представления, так что из метода FindPartialView() возвращается результат, указывающий на отсутствие представления. Метод ReleaseView() не был реализован, поскольку нет никаких ресурсов, которые понадобилось бы освобождать в реализации IView, для чего обычно служит этот метод.

Регистрация специального механизма визуализации

Механизмы визуализации регистрируются в методе Application_Start() внутри файла Global.asax, как показано ниже:

using System.Web.Mvc;
using System.Web.Routing;
using Views.Infrastructure;

namespace Views
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);

            ViewEngines.Engines.Add(new DebugDataViewEngine());
        }
    }
}

Статическая коллекция ViewEngine.Engines содержит набор механизмов визуализации, установленных в приложении. Инфраструктура MVC Framework поддерживает идею наличия множества механизмов, установленных в единственном приложении. Во время обработки ViewResult активатор действий получает набор установленных механизмов визуализации и по очереди вызывает их методы FindView().

Активатор действий останавливает вызов методов FindView() сразу после получения объекта ViewEngineResult, который содержит реализацию IView. Это значит, что порядок добавления механизмов в коллекцию ViewEngines.Engines важен, если два или более механизма способны обработать запрос, адресованный к тому же самому имени представления. Если хотите, чтобы ваше представление получило приоритет, добавьте его в начало коллекции:

ViewEngines.Engines.Insert(0, new DebugDataViewEngine());

Тестирование механизма визуализации

Теперь можно протестировать специальный механизм визуализации. Когда приложение запускается, браузер автоматически переходит на корневой URL для проекта, который будет отображен на действие Index контроллера Home. Этот метод действия использует метод View() для возврата объекта ViewResult, который указывает представление DebugData. Результат можно видеть на рисунке ниже:

Использование специального механизма визуализации

Это результат вызова нашего метода FindView() для представления, которое мы можем обработать. Если перейти на URL вида /Home/List, инфраструктура MVC Framework вызовет метод действия List(), который обращается к методу View() для запроса его стандартного представления, но данное представление не является поддерживаемым. Результат показан на рисунке:

Запрашивание неподдерживаемого представления

Сообщение отображается как одно из местоположений, в которых производился поиск представления. Обратите внимание, что представления Razor и ASPX также присутствуют в списке; причина в том, что эти механизмы визуализации по-прежнему используются. Если необходимо обеспечить применение только специального механизма визуализации, то перед его регистрацией в файле Global.asax понадобится вызвать метод Clear():

using System.Web.Mvc;
using System.Web.Routing;
using Views.Infrastructure;

namespace Views
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);

            ViewEngines.Engines.Clear();
            ViewEngines.Engines.Add(new DebugDataViewEngine());
        }
    }
}

Перезапустив приложение и перейдя на /Home/List, можно заметить, что использоваться будет только специальный механизм визуализации:

Использование в примере приложения только специального механизма визуализации
Пройди тесты
Лучший чат для C# программистов