суббота, 12 апреля 2008 г.

Microsoft MVC Framework - роутинг

Правила
В основе роутинга в MS MVC Framework лежит следующий принцип - "мы не обращаемся к файлу, мы обращаемся к классу контроллера, и делаем это через паттерны". Что здесь имеется в виду? А то, что мы действительно не вызываем никакие файлы, т.е. мы пишем в адресной строке не mysite/products/details.aspx?productid=6, а вместо этого указываем "дружественный" URL - mysite/products/details/6. Второй момент - это, конечно, никакой не URL. Т.е. слэши здесь играют роль разделения не папок (как в настоящем URL), а частей (элементов) паттерна. Суть в том, что запрос на сервере разбирается, и каждая его часть (отделенная слэшем) интерпретируется так, как вы задали в паттерне.

Паттерн (или маршрут) это последовательность элементов-заполнителей (placeholders), в соответствие с которыми производится разбор строки запроса. Паттерн может содержать не только некоторые элементы-переменные, но и константные значения. Каждый элемент заключается в фигурные скобки { и }. Элементы отделяются друг от друга слэшем или точкой. Все, что не заключено в фигурные скобки, воспринимается как константа. Типичный пример паттерна:

{controller}/{action}/{id}

А вот строка, соответствующая этому паттерну:

Products/Details/55

Для того, чтобы приложение знало о том, как обрабатывать запрос, паттерн надо зарегистрировать. Лучше всего его регистрировать в событии Application_Start в файле Global.asax. Если вы хотите, чтобы регистрация паттерна была доступна и в проекте, предназначенном для тестирования, то метод регистрации должен быть статическим и принимать в качестве параметра RouteCollection.
Например, регистрация маршрута может выглядеть так:

protected void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}

public static void RegisterRoutes(RouteCollection routes)
{
routes.Add(new Route
(
"Category/{action}/{categoryName}"
, new CategoryRouteHandler()
));
}


Какие запросы могут подходить под этот паттерн? Например, localhost/Category/View/Yachts. А вот запрос localhost/Category/View не подходит, поскольку во-первых, в строке не хватает третьего элемента, а во-вторых, потому, что паттерн не предусматривает дефолтного значения для него (об этом чуть ниже).
Следует учесть, что в процессе регистрации все паттерны складываются в коллекцию, из которой извлекаются в порядке очереди. Это значит, что размещать паттерны надо по степени убывания специализации, как кэтчи в блоке try-catch, и дефолтный паттерн должен идти последним.

Теперь посмотрим, как задаются дефолтные значения для элемента (они подставляются в случае отсутствия элемента в строке запроса):


void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}

public static void RegisterRoutes(RouteCollection routes)
{
routes.Add(new Route
(
"Category/{action}/{categoryName}"
new CategoryRouteHandler()
)
{
Defaults = new RouteValueDictionary
{{"categoryName", "food"}, {"action", "view"}}
}
);
}


Как видим, все достаточно просто - если отсутствует имя категории, подставляем food, если нет action, подставляем view.

Если мы не знаем заранее точно, сколько элементов может содержать строка запроса, но хотим все же как-то обрабатывать запрос, то можно записать паттерн следующим образом:
"Category/{action}/{*titles}" . Звездочка указывает на то, что начиная с этого элемента число последующих элементов может быть любым. Но при этом все они будут трактоваться как третий элемент. Например, для строки запроса localhost/Category/View/Maxi/55 Category - это Category, action - это View, а titles - это Maxi/55. Что делать с таким параметром, мы уже решаем сами.

На элементы запроса можно накладывать ограничения (констрейнты), которые представляют собой обыкновенные регулярные выражения. Если задано такое ограничение, то сервер будет обязательно проверять, подходит ли элемент строки под это выражение. Вот пример, взятый с того же Quickstart:

void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}

public static void RegisterRoutes(RouteCollection routes)
{
routes.Add(new Route
(
"{locale}/{year}"
, new ReportRouteHandler()
)
{
Constraints = new RouteValueDictionary
{{"locale", "{a-z}{2}-{A-Z}{2}"},{"year", @"\d{4}"}}
});
}

Мы видим, что элемент year должен содержать 4 цифры, только в этом случае он будет распознан.

Итак, главное, что надо уяснить с роутингом - это то, что строка запроса всегда разбивается ровно на три логические части - controller, action и id. Если вам нужно передать контроллеру (вернее, его методу) параметров больше одного, то вы просто удлиняете запрос, как будто добавляете еще одну папку, а уже в методе разбиваете параметр на нужное количество.

IIS 6 и II7 - замечания по использованию
Необходимо учитывать момент, связанный с тем, какую именно версию IIS вы используете. Пока мы тестируем наше приложение с помощью сервера, встроенного в студию, все идет хорошо. Но как только мы запускаем приложение через IIS 6, то все перестает работать - адреса попросту не находятся.


Кстати, если при попытке отладки через сервер у вас начнут вылезать странные ошибки вроде Failed to access IIS metabase или Unable to start debugging on the web server. The web server is not configured correctly., просто снесите asp.net 2 и установите его заново - aspnet_regiis.exe -u, потом aspnet_regiis.exe -i .


Да, речь идет только об IIS6. Потому что IIS7 уже умеет работать с приложениями, основанными на MVC Frm, и понимает запросы без расширений файлов, а шестой еще нет. Поэтому надо ему помочь. Для этого надо в код регистрации паттерна включить версию для IIS6. Но для начала в конфиге пропишем явно, на какой версии IIS запускаем наше приложение. Поскольку этот параметр может быть изменен (например, после деплоймента на рабочий сервер), заносим этот момент в наш список TODO. Вот примерный код:

<add key="IISVersion" value="6" />

Теперь в код Global.asax добавляем код, с помощью которого сначала производим выбор версии IIS:

protected void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}
public static void RegisterRoutes(RouteCollection routes)
{
int iisVersion = Convert.ToInt32(ConfigurationManager.AppSettings["IISVersion"], System.Globalization.CultureInfo.InvariantCulture);

if (iisVersion >= 7)
{
RegisterRoutesForIIS6(routes);
}
else
{
RegisterRoutesForIIS7(routes);
}
}


Затем мы в соответствующих методах регистрируем наши паттерны. Код в методах отличается только наличием расширения .mvc для IIS6. Мне не очень нравится подобное дублирование, но ничего лучше мне придумать не удалось (если придумаете, напишите мне, а я расскажу здесь о вашей идее - с сохранением всех копирайтов, разумеется). Итак, получился примерно такой код:


private static void RegisterRoutesForIIS7(ICollection routes)
{
routes.Add(new Route("{controler}/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
});
...
}
private static void RegisterRoutesForIIS6(ICollection routes)
{
routes.Add(new Route("{controler}.mvc/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
});
...
}

Неудобство использования IIS6 проявляется и в том, что нормальные адреса (как предполагалось, без расширений) все равно не получаются. Более того, скажу честно, мне так и не удалось заставить всю эту кухню работать под IIS6. На встроенном в студию сервере - пожалуйста, а под IIS6 - ни в какую. Однако не будем терять надежды. Во-первых, может быть, это всего лишь баг, и его в релизе исправят, а во-вторых, может, к тому времени на серверах отомрут IISы-6...

Хозяйке на заметку

С контроллерами мы будем разбираться позже, сейчас же буквально два слова о том, каким образом MVC Frm узнает о том, что и кому передавать. В основе всего лежит объект System.Web.Routing.UrlRoutingModule, который реализует ничто иное, как интерфейс IHttpModule. Http-модуль характеризуется тем, что вставивается в конвейер обработки запросов. Данный модуль перехватывает запросы и интерпретирует их по-своему. Этот модуль, как полагается, зарегистрирован в файле конфига:

<httpModules>
<add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<httpModules>


А Http-хандлером является MvcHandler, он решает, какой контроллер вызывать и, соответственно, какой метод. Надо знать, что если в строке запроса указан контроллер, например, Products, то реально сам контроллер (это вообще класс) должен называться ProductController. Имя метода же должно точно соответствовать элементу action запроса.

Полезные ссылки:
Kigg - первый стартер-кит, построенный на MS MVC Framework (клон Digg'a)
Еще кое-что о роутинге

Куда дальше?
В следующей части я рассмотрю архитектуру приложения.

Комментариев нет: