В 2001 г., когда Microsoft ввела Microsoft .NET Framework, а вместе с ней и новую технологию, названную ASP.NET, веб-разработчики стали применять ее для создания сайтов, используя инфраструктуру на основе форм. Эта инфраструктура, известная как Web Forms, выдержала проверку временем и постоянно изменялась и совершенствовалась для поддержки развивающейся веб-среды. Создание веб-приложения в те годы было простым выбором: в диалоге New Project появлялось четыре вида проектов ASP.NET (рис. 1). Большинство из нас игнорировали проекты ASP.NET Mobile Web Site и Web Control Library и создавали лишь проекты ASP.NET Web Application. Если вам были нужны веб-сервисы, вы добавляли к существующему веб-сайту SOAP-сервис в виде файла .asmx.
Рис. 1. Исходные варианты новых проектов ASP.NET в Visual C#
В начале 2009-го появление Model-View-Controller (MVC) кардинально изменило ландшафт ASP.NET. Услышав, что больше не понадобится обрабатывать состояние представления (viewstate), жизненный цикл событий страницы или события обратной передачи (postback), разработчики потянулись к этой новой инфраструктуре. Я был одним из них, заинтригованный потенциалом этой более полно тестируемой веб-технологией. Мы должны были находить способы выбивать из наших руководителей и бухгалтерий бюджеты на перевод приложений под MVC. Все замечательно работало с MVC в течение нескольких лет, а потом Web вдруг немного переменилась. ASP.NET вновь требовала совершенствования.
В 2012-ом Microsoft предоставила две новые инфраструктуры, добавив их в арсенал ASP.NET: Web API и SignalR. Обе эти инфраструктуры привносят в среду нечто специфическое, и каждая из них по-своему уникальна.
- Web API позволяет разработчикам доставлять в стиле MVC контент, предназначенный для машинной интерпретации. UI нет, и транзакции выполняются по аналогии с тем, как это делается в RESTful. После согласования типов контента Web API может автоматически форматировать контент как JSON или XML на основе HTTP-заголовков, передаваемых конечной точке Web API.
- SignalR — новая модель доставки «Web в реальном времени» от Microsoft. Эта технология открывает клиент-серверный коммуникационный канал, обеспечивающий моментальное взаимодействие сервера с клиентом. Модель доставки контента в SignalR переворачивает наши обычные ожидания, так как для взаимодействия с контентом сервер вызывает клиента.
Рассмотрим преимущества и недостатки Web Forms и MVC и сравним их с таковыми для Web API и MVC (табл. 1).
Табл. 1. Преимущества каждой инфраструктуры компонентов ASP.NET
Инфраструктура | Продуктивность труда | Контроль | UI | В реальном времени |
Web Forms | • | | • | |
MVC | | • | • | |
Web API | • | • | | |
SignalR | | | | • |
Под продуктивностью труда подразумеваются средства, ускоряющие разработку и поставку решения. Контроль — это мера вашего влияния на биты, передаваемые по сети вашим пользователям. UI обозначает возможность использования данной инфраструктуры для создания полноценного UI. Наконец, в столбце «В реальном времени» отмечается, насколько хорошо данная инфраструктура справляется со своевременной доставкой контента, который можно было бы принимать как немедленное обновление.
Теперь в 2013-ом, когда я открываю свою копию Visual Studio и пытаюсь начать проект ASP.NET, на экране появляются диалоги, показанные на рис. 2 и 3.
Рис. 2. Новый веб-проект в Visual Studio 2012
Рис. 3. Новый диалог Project Template в Visual Studio 2012
Глядя на эти окна, возникают некоторые непростые вопросы. С какого проекта я должен начать? Какой шаблон лучше всего подходит для моего решения? А что будет, если я захочу включить некоторые компоненты каждого шаблоны? Можно ли создать мобильное приложение с использованием некоторых серверных элементов управления и Web API?
Надо ли следовать только одному подходу?
Если кратко ответить на этот вопрос, то, нет, вы не обязаны выбирать только одну из этих инфраструктур при создании какого-либо веб-приложения. Существуют методики, позволяющие использовать Web Forms совместно с MVC, и в противоположность содержимому представленных диалогов Web API и SignalR можно легко добавлять как некую функциональность в любое веб-приложение. Вспомните, что весь контент ASP.NET проходит рендеринг через наборы HttpHandlers и HttpModules. Если вы ссылаетесь на правильные обработчики и модули, то можете создавать решение с применением любой из этих инфраструктур.
Это основа концепции «One ASP.NET»: не выбирайте только одну из этих инфраструктур — создавайте свое решение, используя те части каждой из них, которые лучше всего подходят вашему приложению. У вас есть выбор, и не ограничивайте себя чем-то одним.
Чтобы вы могли увидеть это на практике, я соберу небольшое веб-приложение, которое будет иметь унифицированную разметку, экран поиска и экран для создания списка товаров. Экран поиска будет создан на основе Web Forms и Web API и будет показывать обновления от SignalR в реальном времени. Экран для создания будет генерироваться автоматически MVC-шаблонами. Я также значительно улучшу внешний вид веб-форм, используя стороннюю библиотеку элементов управления — Telerik RadControls for ASP.NET AJAX. Пробная версия этих элементов управления доступна по ссылке bit.ly/15o2Oab.
Подготовка проекта-примера и общая разметка
Для начала я должен создать проект, используя диалог, показанный на рис. 2. Хотя я мог бы выбрать пустой шаблон или шаблон приложения Web Forms, самое правильное решение — выбрать приложение MVC. Начинать с MVC-проекта лучше всего, так как вы получаете весь инструментарий Visual Studio, который поможет вам в конфигурировании моделей, представлений и контроллеров, а также возможность добавлять объекты Web Forms в любое место файловой структуры проекта. Есть возможность добавить инструментарий MVC обратно в существующее веб-приложение изменением некоторого XML-контента в файле .csproj. Этот процесс можно автоматизировать, установив NuGet-пакет AddMvc3ToWebForms.
Чтобы сконфигурировать элементы управления Telerik для использования в этом проекте, мне нужно кое-что изменить в Web.config для добавления HttpHandlers и HttpModules, которые обычно настраиваются в стандартном проекте Telerik RadControls. Сначала я добавлю пару строк для определения «скина» Telerik AJAX-элементов управления:
<add key="Telerik.Skin" value="WebBlue" />
</appSettings>
Затем добавлю префиксный тег Telerik:
<add tagPrefix="telerik" namespace="Telerik.Web.UI" assembly="Telerik.Web.UI" />
</controls>
Я внесу минимальные изменения в раздел HttpHandlers файла Web.config, необходимые для элементов управления Telerik:
<add path="Telerik.Web.UI.WebResource.axd" type="Telerik.Web.UI.WebResource"
verb="*" validate="false" />
</httpHandlers>
И наконец, я дополню handlers в Web.config строками, нужными элементам управления Telerik:
<system.WebServer>
<validation validateIntegratedModeConfiguration="false" />
<handlers>
<remove name="Telerik_Web_UI_WebResource_axd" />
<add name="Telerik_Web_UI_WebResource_axd"
path="Telerik.Web.UI.WebResource.axd"
type="Telerik.Web.UI.WebResource" verb="*" preCondition="integratedMode" />
Теперь надо создать страницу разметки для этого проекта, поэтому я создам страницу site.master (Web Forms) в папке Views | Shared. В разметку этого сайта я хочу добавить стандартный логотип и меню для всех страниц. Изображение логотипа я просто перетаскиваю в свою разметку. Затем, чтобы добавить каскадное меню в разметку, я перетаскиваю RadMenu из окна элементов управления в дизайнере — чуть ниже изображения. В рабочей области дизайнера можно быстро построить меню, щелкнув правой кнопкой мыши элемент управления меню и выбрав Edit Items, чтобы получить окно, как на рис. 4.
Рис. 4. Окно настройки Telerik RadMenu
Я хочу сконцентрироваться на двух элементах меню под Products: Search и New. Для каждого из этих элементов я задал свойство NavigateUrl и текст:
<telerik:RadMenuItem Text="Products">
<Items>
<telerik:RadMenuItem Text="Search" NavigateUrl="~/Product/Search" />
<telerik:RadMenuItem Text="New" NavigateUrl="~/Product/New" />
</Items>
</telerik:RadMenuItem>
Сконфигурировав меню, я сталкиваюсь с одной проблемой там, где определил свою разметку, используя Web Forms, и должен разместить MVC-контент. Задача не тривиальная, но вполне решаемая.
Наведение моста через границы: настраиваем MVC на использование эталонной страницы Web Forms
Как и большинство, я предпочитаю не усложнять вещи. Я хочу, чтобы определенная мной для этого проекта разметка совместно использовалась Web Forms и MVC. Для этого существует четко документированная методика Мэтта Холи (Matt Hawley), которая демонстрирует, как использовать эталонную страницу (master page) Web Forms с MVC-представлениями на основе Razor (bit.ly/ehVY3H). Я намерен задействовать эту методику в данном проекте. Что построить мост, я сконфигурирую простое представление Web Forms с именем RazorView.aspx, которое ссылается на эталонную страницу:
<%@ Page Language="C#" AutoEventWireup="true"
MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
<%@ Import Namespace="System.Web.Mvc" %>
<asp:Content id="bodyContent" runat="server"
ContentPlaceHolderID="body">
<% Html.RenderPartial((string)ViewBag._ViewName); %>
</asp:Content>
Чтобы MVC-контроллеры использовали это представление и позволяли выполнять свои представления на основе Razor, мне нужно расширить каждый контроллер. Это необходимо для корректного распределения контента представлений. Для этого применяется метод расширения, который перенаправляет модель, ViewData и TempData через RazorView.aspx, как показано на рис. 5.
Рис. 5. Метод расширения RazorView для перенаправления MVC-представлений через эталонную страницу Web Forms
public static ViewResult RazorView(this Controller controller,
string viewName = null, object model = null)
{
if (model != null)
controller.ViewData.Model = model;
controller.ViewBag._ViewName = !string.IsNullOrEmpty(viewName)
? viewName
: controller.RouteData.GetRequiredString("action");
return new ViewResult
{
ViewName = "RazorView",
ViewData = controller.ViewData,
TempData = controller.TempData
};
}
Создав этот метод, я могу легко направлять все MVC-операции через эталонную страницу. Следующий шаг — настройка ProductsController для того, чтобы можно было создавать список товаров.
MVC и экран для создания списка товаров
MVC-часть этого решения следует вполне стандартному в MVC подходу. Я определил простую объектную модель BoardGame в папке Models своего проекта (рис. 6).
Рис. 6. Объект BoardGame
public class BoardGame
{
public int Id { get; set; }
public string Name { get; set; }
[DisplayFormat(DataFormatString="$0.00")]
public decimal Price { get; set; }
[Display(Name="Number of items in stock"), Range(0,10000)]
public int NumInStock { get; set; }
}
Затем, используя стандартный инструментарий MVC в Visual Studio, я создаю пустой ProductController. Я добавлю папку Views | Product, щелкну правой кнопкой мыши папку Product и выберу View из меню Add. Это представление будет поддерживать создание новых настольных игр (board games), поэтому я создам его с параметрами, показанными на рис. 7.
Рис. 7. Создание представления New
Благодаря инструментарию и шаблонам MVC мне не требуется ничего менять. Созданное представление имеет метки и средства проверки и может использовать мою эталонную страницу. На рис. 8 показано, как определить операцию New в ProductController.
Рис. 8. Направление ProductController через RazorView
public ActionResult New()
{
return this.RazorView();
}
[HttpPost]
public ActionResult New(BoardGame newGame)
{
if (!ModelState.IsValid)
{
return this.RazorView();
}
newGame.Id = _Products.Count + 1;
_Products.Add(newGame);
return Redirect("~/Product/Search");
}
Этот синтаксис должен быть знаком MVC-разработчикам, так как единственное изменение здесь — возврат RazorView вместо View. Объект _Products является статическим набором только для чтения и содержит вымышленные товары (игры), которые определяются в этом контроллере (вместо использования в этом примере базы данных):
public static readonly List<BoardGame> _Products =
new List<BoardGame>()
{
new BoardGame() {Id=1, Name="Chess", Price=9.99M},
new BoardGame() {Id=2, Name="Checkers", Price=7.99M},
new BoardGame() {Id=3, Name="Battleship", Price=8.99M},
new BoardGame() {Id=4, Name="Backgammon", Price= 12.99M}
};
Конфигурирование страницы поиска, основанной на Web Forms
Я хочу, чтобы мои пользователи могли обращаться к странице поиска товаров с URL, не похожей на Web Forms URL и дружественной к поиску. С выпуском ASP.NET 2012.2 это делается довольно легко. Просто откройте файл App_Start/RouteConfig.cs и вызовите EnableFriendlyUrls, чтобы активизировать необходимую функциональность:
public static void RegisterRoutes(
RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.EnableFriendlyUrls();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action =
"Index", id = UrlParameter.Optional }
);
}
Далее надо сконфигурировать страницу поиска, на которой показывается сетка с текущими товарами и их запасы на складе. Я создам папку Product в своем проекте и добавлю в него новую веб-форму Search.aspx. В этом файле я удалю всю разметку, кроме директивы @Page, и в качестве MasterPageFile укажу ранее определенный файл Site.Master. Для отображения результатов я выбрал Telerik RadGrid, чтобы быстро настраивать и отображать данные результата поиска:
<%@ Page Language="C#" AutoEventWireup="true"
CodeBehind="Search.aspx.cs"
Inherits="MvcApplication1.Product.Search"
MasterPageFile="~/Views/Shared/Site.Master" %>
<asp:Content runat="server" id="main" ContentPlaceHolderID="body">
<telerik:RadGrid ID="searchProducts" runat="server" width="500"
AllowFilteringByColumn="True" CellSpacing="0" GridLines="None"
AllowSorting="True">
Я хочу, чтобы определенная мной разметка совместно использовалась Web Forms и MVC.
Сетка будет автоматически визуализировать столбцы, связанные с ней на серверной стороне, и обеспечивать возможности сортировки и фильтрации. Однако я хотел бы делать это динамичнее. Мне нужно управлять данными на клиентской стороне. В этой модели данные можно было бы передавать и связывать безо всякого серверного кода Web Forms. Для этого я воспользуюсь Web API, который будет доставлять данные и позволит выполнять операции над ними.
Добавление в смесь Web API
Чтобы добавить контроллер Web API с именем ProductController в папку api в свой проект, я воспользуюсь стандартным меню Project | Add New. Это поможет сохранить четкое различие между MVC- и API-контроллерами. Данный API-контроллер будет делать только одно: доставлять данные для сетки в формате JSON и поддерживать OData-запросы. Чтобы реализовать это в Web API, я напишу единственный метод Get и дополню его атрибутом Queryable:
[Queryable]
public IQueryable<dynamic> Get(ODataQueryOptions options)
{
return Controllers.ProductController._Products.Select(b => new
{
Id = b.Id,
Name = b.Name,
NumInStock = b.NumInStock,
Price = b.Price.ToString("$0.00")
}).AsQueryable();
}
Этот код возвращает мой набор объектов BoardGame в статический список с минимальным форматированием. Так как метод дополнен атрибутом [Queryable] и возвращает опрашиваемый (queryable) набор, инфраструктура Web API будет автоматически обрабатывать OData-команды фильтрации и сортировки. Этот метод также надо сконфигурировать с входным параметром ODataQueryOptions, чтобы обрабатывать данные фильтра, передаваемые сеткой.
Чтобы настроить сетку в Search.aspx на использование этого нового API, надо добавить в разметку страницы кое-какие параметры. В данном элементе управления «сетка» я определяю связывание с данными на клиенте с элементом ClientSettings и параметр DataBinding. В параметре DataBinding перечисляются местонахождение API, тип формата ответа и имя контроллера для запроса, а также формат OData-запроса. Задав эти настройки и указав определение столбцов в сетке, я могу запустить проект и увидеть сетку, связанную с данными в списке данных _Products, как показано на рис. 9.
Рис. 9. Полный исходный код форматирования сетки
<telerik:RadGrid ID="searchProducts" runat="server" width="500"
AllowFilteringByColumn="True" CellSpacing="0" GridLines="None"
AllowSorting="True" AutoGenerateColumns="false"
>
<ClientSettings AllowColumnsReorder="True"
ReorderColumnsOnClient="True"
ClientEvents-OnGridCreated="GridCreated">
<Scrolling AllowScroll="True" UseStaticHeaders="True"></Scrolling>
<DataBinding Location="/api" ResponseType="JSON">
<DataService TableName="Product" Type="OData" />
</DataBinding>
</ClientSettings>
<MasterTableView ClientDataKeyNames="Id" DataKeyNames="Id">
<Columns>
<telerik:GridBoundColumn DataField="Id" HeaderStyle-Width="0"
ItemStyle-Width="0"></telerik:GridBoundColumn>
<telerik:GridBoundColumn DataField="Name" HeaderText="Name"
HeaderStyle-Width="150" ItemStyle-Width="150">
</telerik:GridBoundColumn>
<telerik:GridBoundColumn ItemStyle-CssClass="gridPrice"
DataField="Price"
HeaderText="Price" ItemStyle-HorizontalAlign="Right">
</telerik:GridBoundColumn>
<telerik:GridBoundColumn DataField="NumInStock"
ItemStyle-CssClass="numInStock"
HeaderText="# in Stock"></telerik:GridBoundColumn>
</Columns>
</MasterTableView>
</telerik:RadGrid>
Активация сетки данными реального времени
Последняя часть головоломки — возможность отображения в реальном времени изменений складских запасов по мере поставки и получения товаров. Для передачи обновлений и представления новых значений в сетке поиска я добавлю концентратор (hub) SignalR. Чтобы добавить SignalR в проект, требуется выдать следующие две NuGet-команды:
Install-Package -pre Microsoft.AspNet.SignalR.SystemWeb
Install-Package -pre Microsoft.AspNet.SignalR.JS
Эти команды установят серверные компоненты ASP.NET для хостинга внутри веб-сервера IIS и сделают клиентские библиотеки JavaScript доступными Web Forms.
Серверный компонент SignalR называется Hub, и я определю свой, добавив класс StockHub в папку Hubs веб-проекта. StockHub должен наследовать от класса Microsoft.AspNet.SignalR.Hub. Я определил статический System.Timers.Timer, чтобы приложение могло имитировать изменение складских запасов. Для такой имитации через каждые 2 секунды (когда срабатывает событие Elapsed таймера) я присваиваю случайным образом новое значение уровню запаса на складе случайно выбранному товару. Как только задан новый уровень, я уведомляю все подключенные клиенты, выполняя на клиенте метод setNewStockLevel (рис. 10).
Рис. 10. Серверный компонент SignalR Hub
public class StockHub : Hub
{
public static readonly Timer _Timer = new Timer();
private static readonly Random _Rdm = new Random();
static StockHub()
{
_Timer.Interval = 2000;
_Timer.Elapsed += _Timer_Elapsed;
_Timer.Start();
}
static void _Timer_Elapsed(object sender, ElapsedEventArgs e)
{
var products = ProductController._Products;
var p = products.Skip(_Rdm.Next(0, products.Count())).First();
var newStockLevel = p.NumInStock +
_Rdm.Next(-1 * p.NumInStock, 100);
p.NumInStock = newStockLevel;
var hub = GlobalHost.ConnectionManager.GetHubContext<StockHub>();
hub.Clients.All.setNewStockLevel(p.Id, newStockLevel);
}
}
Чтобы данные этого концентратора были доступны с сервера, надо добавить строку в RouteConfig, указывающую наличие концентратора. Вызвав routes.MapHubs в методе RegisterRoutes из RouteConfig, я завершаю конфигурирование SignalR на серверной стороне.
Затем требуется сделать так, что сетка прослушивала соответствующие события от сервера. Для этого я добавляю несколько JavaScript-ссылок на клиентскую библиотеку SignalR, установленную NuGet, и на код, сгенерированный командой MapHubs. Код на рис. 11 подключает сервис SignalR, который предоставляет клиенту метод setNewStockLevel.
Рис. 11. Клиентский код SignalR для активации сетки
<script src="/Scripts/jquery.signalR-1.0.0-rc2.min.js"></script>
<script src="/signalr/hubs"></script>
<script type="text/javascript">
var grid;
$().ready(function() {
var stockWatcher = $.connection.stockHub;
stockWatcher.client.setNewStockLevel = function(id, newValue) {
var row = GetRow(id);
var orgColor = row.css("background-color");
row.find(".numInStock").animate({
backgroundColor: "#FFEFD5"
}, 1000, "swing", function () {
row.find(".numInStock").html(newValue).animate({
backgroundColor: orgColor
}, 1000)
});
};
$.connection.hub.start();
})
</script>
В jQuery-обработчике события ready я устанавливаю ссылку stockWatcher на StockHub по синтаксису $.connection.stockHub. Затем определяю метод setNewStockLevel в клиентском свойстве stockWatcher. Этот метод использует некоторые другие вспомогательные JavaScript-методы для прохождения по сетке, поиска строки с соответствующим товаром и изменения уровня его запаса на складе; при этом применяется цветовая анимация, предоставляемая jQuery UI, как показано на рис. 12.
Рис. 12. Интерфейс поиска для сетки, генерируемый Web API и поддерживаемый SignalR
Заключение
Я продемонстрировал, как создать проект ASP.NET MVC и добавить разметку Web Forms, сторонние AJAX-элементы управления и обеспечить направление к ним со стороны Web Forms. Я сгенерировал UI с помощью инструментария MVC и сделал контент активным средствами Web API и SignalR. В этом проекте были задействованы средства всех четырех инфраструктур ASP.NET для обеспечения связующего интерфейса, использующего лучшие качества каждого компонента. Вы можете сделать то же самое. Не выбирайте только одну инфраструктуру ASP.NET для своего следующего проекта. Вместо этого попробуйте выбрать их все.