Пример перезаписи URL


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

http://www.mysite.ru/termin.aspx?id=1

Наша цель - добиться того, чтобы строка запроса выглядела так:

http://www.mysite.ru/termin/1.aspx


Такой адрес не только выглядит лучше и запоминается лучше - такая страница будет лучше индексироваться поисковиками, потому что будет восприниматься ими именно как отдельная страница.
С чего начать? Откроем код файла Global.asax и найдем там обработчик события BeginRequest. Вот как выглядит метод:

protected void Application_BeginRequest(Object sender, EventArgs e) { }

Если не нашли, то просто добавьте туда этот метод. Привязка данного события происходит автоматически.
Для перезаписи URL используется метод RewritePath класса HttpContext. Ссылку на экземпляр класса HttpContext, соответствующего текущему запросу можно получить из свойства Context, класса HttpApplication, от которого наследован наш класс Global. Текущую строку запроса можно получить из свойства Request.

protected void Application_BeginRequest(Object sender, EventArgs e) { //сначала получим текущий относительный путь //Request.Url.AbsolutePath включает путь к приложению, а это нам не нужно string url = Request.Url.AbsolutePath.Remove(0, Request.ApplicationPath.Length).TrimStart('/'); //проверяем, что строка то, что нам нужно //(потом мы будем использовать регулярные выражения, но для начала и это сойдет) if(url.StartsWith("termin/")) { //получим код string id = url.Remove(0, "termin/".Length); //собственно перезапись Context.RewritePath("~/termin.aspx","","id="+id); } }

Вот и все. Замечу, что предыдущий адрес (с параметром в строке запроса) тоже будет работать.

Теперь небольшое улучшение. Оно скорее относится к проектированию, чем к программированию. Согласитесь, что числовое название страницы не намного лучше строки параметров в строке запроса.
Я рекомендую переписать страницу, чтобы она использовала текстовый ключ, а не цифровой код, как обычно делают. Причем этот текстовый ключ должен соответствовать термину. Это является дополнительным преимуществом: во-первых, такую страницу легче запомнить, а во-вторых - при ранжировании результатов поиска эта страница будет выше, если запрос содержал данное слово.
И так изменим нашу страницу, чтобы она "откликалась" на подобные URL:

http://www.mysite.ru/termin.aspx?id=book

или

http://www.mysite.ru/termin/book.aspx


Выглядет намного лучше, правда?

Тут надо помнить одну важную деталь. Оба адреса открывают одну и ту же страницу, но находятся в разных папках! Следует иметь это ввиду, при указании ссылок на другие страницы, указании путей к изображениями, CSS-файлам. Самый надежный способ использовать метод ResolveUrl() и указывать путь от корня приложения (используюя тильду "~").

<img src='<%=ResolveUrl("~/")%>images/myimage.gif' />

или

<img src='<%=ResolveUrl("~/images/myimage.gif")%>' />

Теперь все хорошо. Картинки видны, CSS подгрузился, ссылки все работают корректно. Но это пока нам не понадобилось использовать на странице форму с атрибутом runat="server". Все дело в том, что по умалчанию элемент HtmlForm самостоятельно устанавливает значение атрибута action в текущий путь и не разрешает его никак изменить(попросту игнорирует устанваливаемые нами значения). При это он ничего не "знает" об первоначальном адресе, а видит только адрес после перезаписи, в который и устанваливает значение атрибута action. В результате получается, что на странице http://www.mysite.ru/termin/book.aspx мы имеем форму, атрибут action, которой равен http://www.mysite.ru/termin.aspx?id=book.
Данная проблема решается нормально только одним способом - наследованием от HtmlForm и переопределением поведения. При этом необходимо в методе перезаписи сохранить первоначальный адрес.

protected void Application_BeginRequest(Object sender, EventArgs e) { //сначала получим текущий относительный путь //Request.Url.AbsolutePath включает путь к приложению, а это нам не нужно string url = Request.Url.AbsolutePath.Remove(0, Request.ApplicationPath.Length).TrimStart('/'); //проверяем, что строка то, что нам нужно if(url.StartsWith("termin/")) { //получим код string id = url.Remove(0, "termin/".Length); //сохраним оригинальный адрес  Context.Items["OriginalPath"] = Request.Url.AbsolutePath; //собственно перезапись Context.RewritePath("~/termin.aspx","","id="+id); } }

Далее сама форма:

public class Form : System.Web.UI.HtmlControls.HtmlForm { protected override void RenderAttributes(System.Web.UI.HtmlTextWriter writer) { //получаем значение первоначального пути из Context.Items string action = (string)Context.Items["OriginalPath"]; //если значения нет, то вызываем метод базового класса и выходим if (action == null) { base.RenderAttributes(writer); return; } string s = string.Empty; StringWriter w = new StringWriter(); try { base.RenderAttributes(new HtmlTextWriter(w)); s = w.ToString(); } finally { if (w != null) w.Close(); } //заменяем неправильный путь с помощью регулярного выражения s = Regex.Replace(s, "action=\\\"[^\\\"]+\\\"", "action=\"" + action + "\""); writer.Write(s); } }

Еще один момент, который стоит учитывать - строка запроса (QueryString). Дело в том, что при вызове метода Context.RewritePath( filePath, pathInfo, queryString) исходня строка запроса теряется. Есть два способа сохранить исходню строку запроса:

1. Использовать метод Context.RewritePath(filePath) с одним параметром, а для передачи нужных значений использовать Context.Items

... //сохраним код Context.Items["TerminID"] = id; //перезапись Context.RewritePath("~/termin.aspx"); ...

2. Просто добавить новые параметры к существующим. Данный метод опасен тем, что имена исходных параметров могу совпасть с новыми.

... //проверяем наличие строки запроса и если есть, то заменяем вопрос (?) на амперсанд (&) string curQuery = Request.Url.Query.Length < 2 ? "" : Request.Url.Query.Replace("?", "&"); //перезапись Context.RewritePath("~/termin.aspx","","id="+id+curQuery); ...
 
Вышеизложенное решение подходит для использования в конкретном приложении. При необходимости правила замены можно добавлять для каждого нового случая.
Теперь попробуем "смастерить" решение, которое можно повторно использовать.
Для начала разберемся с местом. Есть более удобный способ подключения обработчика события BeginRequest, чем Global.asax. Для этого существует интерфейс IHttpModule и секция httpModules файла Web.config. Но об этом я напишу в следующий раз.
Продолжение следует...

На главную