ScriptManager для ASP.NET MVC.
Итак, в чем же сложность реализации ScriptManager в MVC ? Рассмотрим небольшой пример. Пусть у нас есть MasterLayout-вьюха (MasterPage в терминологии вебформ), есть некоторая View (контент-страница), которая использует MasterLayout, обе вьюхи содержат некоторый HTML-код, в том числе PartialView`s или вызовы других Action`s для отрисовки каких-либо модулей (UserControls). Каждое частичное представление (PartialView) или Action (который возвращает PartialView) используют некоторые Javascript-скрипты. Далее, пусть у нас есть модуль Module1, который использует скрипт module1-script.js. Пусть этот модуль загружается на странице N-раз, таким образом, ссылка на module1-script.js будет присутствовать на странице тоже N-раз — это дубликация ссылки, повторные вызовы и возможные проблемы с работой скрипта. Несмотря на то, что загруженные скрипты кэшируются браузером и не загружаются каждый раз заново, мы все же хотим, чтобы ссылка на этот скрипт была отрисована в HTML-коде лишь один раз.
Проблема создания ScriptManager в ASP.NET MVC связана с тем, что в ASP.NET MVC отсутствует (к счастью) событийная модель подобная классическим вебформам, поэтому каждая страница отрисовывается сверху вниз без всяких событий вроде Init, Load, Render, причем неважно, View это или PartialView — все независимо загрузится и отрисуется в свою очередь последовательно, а следовательно, мы никогда не сможем получить список скриптов, которые требуются для каждой отрисованной вьюхи в конкретном запросе перед началом отрисовки сформированной итоговой страницы, мы сможем сделать это только в самом конце. Т.е. для создания ScriptManager мы могли бы создать некоторый Dictionary, в который при каждой загрузке вьюхи грузить необходимые ей скрипты, а потом выгружать их все внизу страницы-контейнера, но такой вариант в некоторых случаях не работает, потому что иногда возникают ситуации, когда отрисованный HTML-код требует необходимый ему скрипт сразу, но не видит его, поскольку он находится внизу страницы, в результате скрипт не работает. Тогда мы должны грузить скрипт до отрисовки модуля, который его использует. Догадались о каких ситуациях идет речь ? Это ajax-загрузки PartialView`s.
Реализация очень простая:
- создаем несколько extension-methods HtmlHelper`a для загрузки и выгрузки скриптов, чтобы было удобно вызывать их на вьюхе;
- оборачиваем каждый скрипт на каждой вьюхе в соответствующий метод вместо непосредственного вызова скрипта с помощью HTML-тэгов в коде страницы;
- в самом низу страницы-контейнера вызываем экстеншн-метод для выгрузки скриптов.
Скрипты, как уже говорилось, будем загружать в некоторый Dictionary. Я буду использовать HttpContext.Current.Items.
public static void AddClientScriptBlock(this HtmlHelper helper, String key, Func<object, HelperResult> scriptBlock) { AddFileLink(helper, key, scriptBlock(null).ToString(), "{0}"); } public static void AddClientScript(this HtmlHelper helper, String filePath) { AddFileLink(helper, filePath, filePath, Constants.Web.ScriptTemplate); } public static void AddClientCss(this HtmlHelper helper, String filePath) { AddFileLink(helper, filePath, filePath, Constants.Web.CssTemplate); } private static void AddFileLink(HtmlHelper helper, String key, String filePath, String fileTemplate) { var url = new UrlHelper(helper.ViewContext.RequestContext, helper.RouteCollection); var script = String.Format(fileTemplate, url.Content(filePath)); var loadedScripts = HttpContext.Current.Items["client-script-list"] as Dictionary<String, String> ?? new Dictionary<String, String>(); if (loadedScripts.ContainsKey(key)) return; loadedScripts.Add(key, script); HttpContext.Current.Items["client-script-list"] = loadedScripts; if (helper.ViewContext.RequestContext.HttpContext.Request.IsAjaxRequest()) { helper.ViewContext.Writer.WriteLine(script); } }
Последнее условие вспомогательного метода AddFileLink нуждается в комментарии. Это условие требуется как раз на тот случай, когда загруженной вьюхе требуется скрипт именно в месте ее загрузки, т.е. в случае аяксового запроса.
Первый метод предназначен для загрузки «чистого» javascript:
@{ Html.AddClientScriptBlock("VerticalLeft.cshtml", @<text> <script type="text/javascript"> $(document).ready(function () { var menuLinks = $(".vertical-left-menu a"); var defaultPadding = menuLinks.css("padding-left"); menuLinks.mouseenter(function () { $(this).animate({ paddingLeft: "+=15px" }, "fast"); }); menuLinks.mouseleave(function () { $(this).animate({ paddingLeft: defaultPadding }, "fast"); }); }); </script> </text>); }
Второй — для загрузки ссылок на файлы скриптов:
@{ Html.AddClientScript("~/Plugins/lightbox204/js/jquery.lightbox-0.5.js"); } @{ Html.AddClientScript("~/Scripts/jquery.effects.core.js"); } @{ Html.AddClientScript("~/Scripts/jquery.effects.slide.js"); }
Третий — для загрузки ссылок на файлы стилей css:
@{ Html.AddClientCss("~/Plugins/lightbox204/css/jquery.lightbox-0.5.css"); }
Еще нам нужен метод для выгрузки скриптов в нижнюю часть страницы-контейнера, тут тоже все очень просто, считаю, что комментарии излишни:
public static void LoadClientScripts(this HtmlHelper helper) { var scripts = HttpContext.Current.Items["client-script-list"] as Dictionary<String, String>; if (scripts == null) return; foreach (var script in scripts) { helper.ViewContext.Writer.WriteLine(script.Value); } }
ScriptManager готов. Пример работы можете посмотреть на нашем оригинальном сайте http://webferia.ru. У такой реализации имеется существенный для SEO недостаток. Файлы css-стилей должны всегда грузиться в секции <head>, а у нас они грузятся внизу страницы, поэтому, проверяя страницу валидатором мы увидим следующее:


Для устранения этой проблемы можно попробовать перехватить ответ от сервера (Response), найти в нем некоторую метку, например [SCRIPTS] (ее предварительно нужно установить в секции <head>), сформировать строку со скриптами и css-файлами из нашего Dictionary и заменить метку этой строкой. В таком случае, метод LoadClientScripts(this HtmlHelper helper) нам уже будет не нужен.
Жду комментариев :).
к сожалению это не решение, а костыль((
Почему же ? С цссками — да, согласен, но проблема только с валидатором разметки, в случае со скриптами все работает отлично.
Недостатки данного решения устранены в новой версии скрипт-менеджера, которую покажу позже в новой публикации. Собственно идея этого решения обозначена в последнем абзаце статьи.
В каком порядке выгружаются скрипты LoadClientScripts ?
Иногда бы хотелось задать порядок.
Or how about including the ScriptManager itself, as the sole inhabitant of a solitary, once-per-page