Главная > ASP.NET > ScriptManager для ASP.NET MVC.

ScriptManager для ASP.NET MVC.

Открываю новый цикл статей по ASP.NET MVC. Все описанные плюшки реализованы в нашей CMS. Начну пожалуй с такой штуки, как ScriptManager. Наверное каждый, кто занимался разработкой на ASP.NET использовал этот удобный функционал для добавления скриптов и файлов-стилей на страницу. ScriptManager широко применяется в классических ASP.NET Webforms и по понятным причинами отсутствует в ASP.NET MVC, я очень активно искал аналоги этого класса для 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.

Реализация очень простая:

  1. создаем несколько extension-methods HtmlHelper`a для загрузки и выгрузки скриптов, чтобы было удобно вызывать их на вьюхе;
  2. оборачиваем каждый скрипт на каждой вьюхе в соответствующий метод вместо непосредственного вызова скрипта с помощью HTML-тэгов в коде страницы;
  3. в самом низу страницы-контейнера вызываем экстеншн-метод для выгрузки скриптов.

Скрипты, как уже говорилось, будем загружать в некоторый 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>, а у нас они грузятся внизу страницы, поэтому, проверяя страницу валидатором мы увидим следующее:

Validation error
Validation message

Для устранения этой проблемы можно попробовать перехватить ответ от сервера (Response), найти в нем некоторую метку, например [SCRIPTS] (ее предварительно нужно установить в секции <head>), сформировать строку со скриптами и css-файлами из нашего Dictionary и заменить метку этой строкой. В таком случае, метод LoadClientScripts(this HtmlHelper helper) нам уже будет не нужен.

Жду комментариев :).

  1. Артём
    21 декабря 2011 в 13:13 | #1

    к сожалению это не решение, а костыль((

  2. 21 декабря 2011 в 17:51 | #2

    Почему же ? С цссками — да, согласен, но проблема только с валидатором разметки, в случае со скриптами все работает отлично.

  3. 9 января 2012 в 12:52 | #3

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

  4. 26 декабря 2013 в 14:09 | #4

    В каком порядке выгружаются скрипты LoadClientScripts ?
    Иногда бы хотелось задать порядок.

  5. Александр
    4 ноября 2015 в 09:19 | #5

    Or how about including the ScriptManager itself, as the sole inhabitant of a solitary, once-per-page

  1. 11 августа 2012 в 10:20 | #1
  2. 31 июля 2015 в 21:58 | #2