Главная > ASP.NET, SEO > ScriptManager для ASP.NET MVC. Второй вариант.

ScriptManager для ASP.NET MVC. Второй вариант.

В продолжении предыдущего поста выкладываю стопроцентно-"трушный" вариант ScriptManager`а для ASP.NET MVC, с которым ваш сайт пройдет HTML-валидацию. Как я уже говорил, общая идея заключается в том, что нам нужно перехватить ответ сервера и изменить его перед отправкой. Перехватить Response нам поможет HttpModule.

Следует отметить, что данное решение требуется тем, кто по каким-либо причинам не использует сторонние ява-скрипт и цсс комбайнеры вроде Cassette`а или включенного в поставку ASP.NET MVC 4 Bundling`а (все это — отдельная тема для разговора).

Итак, приступим. Общая идея такова: на HTML-странице должна быть специальная текстовая метка (или две, если вам требуется выводить скрипты и стили в разные области страницы), на место которой перед отправкой ответа от сервера будет записана информация (HTML-код), содержащая ссылки на скрипты и стили.

Сначала делаем эти метки:

        public static void RenderJs(this HtmlHelper helper)
        {
            helper.ViewContext.Writer.Write(Constants.Web.ScriptsZoneMark);
        }      
        
        public static void RenderCss(this HtmlHelper helper)
        {
            helper.ViewContext.Writer.Write(Constants.Web.CssZoneMark);
        }

Это обычные Extension methods стандартного HtmlHelper`а. Далее, вставляем метки в шаблон страницы:

<head>
    ...
    @{ Html.RenderCss(); }
    @{ Html.RenderJs(); }
    ...
</head>

Таким образом, наши стили и скрипты будут выведены внутри head. Теперь посмотрим PartialView`s, а именно то, как в них регистрировать скрипты и стили, чтобы они попали в head родительского шаблона. Пишем так:

...
@{ Html.RegisterCss("~/Scripts/lightbox204/css/jquery.lightbox-0.5.css"); }
@{ Html.RegisterJs(SettingsManager.Instance.JqueryScriptPath); }
@{ Html.RegisterJsBlock("some-key", @<text>
<script type="text/javascript">
    $(document).ready(function () {
       alert("hello");
    });
</script>
</text>); }
...

И C#-код (чтобы было понятно новичкам, комментарии написал на русском):

/// <summary>
/// Добавляет ссылку на css-файл.
/// </summary>
/// <param name="helper">HtmlHelper</param>
/// <param name="filePath">Путь до css-файла</param>
public static void RegisterCss(this HtmlHelper helper, String filePath)
{
	AddFileLink(helper, filePath, filePath, ContentType.Css, Constants.Web.CssTemplate);
}

/// <summary>
/// Добавляет ссылку на js-файл.
/// </summary>
/// <param name="helper">HtmlHelper</param>
/// <param name="filePath">Путь до js-файла</param>
public static void RegisterJs(this HtmlHelper helper, String filePath)
{
	AddFileLink(helper, filePath, filePath, ContentType.Js, Constants.Web.ScriptTemplate);
}

/// <summary>
/// Регистрирует js-блок.
/// </summary>
/// <param name="helper">HtmlHelper</param>
/// <param name="key">Ключ для скрипта</param>
/// <param name="scriptBlock">Текст js-блока</param>
public static void RegisterJsBlock(this HtmlHelper helper, String key, Func<Object, HelperResult> scriptBlock)
{
	AddFileLink(helper, key, scriptBlock(null).ToString(), ContentType.Js, "{0}");
}

private static void AddFileLink(HtmlHelper helper, String key, String filePath, ContentType type, String fileTemplate)
{
	var url = new UrlHelper(helper.ViewContext.RequestContext, helper.RouteCollection);
	var script = String.Format(fileTemplate, url.Content(filePath));
	var loadedItems = HttpContext.Current.Items[type] as Dictionary<String, String> ?? new Dictionary<String, String>();
	if (!loadedItems.ContainsKey(key))
	{
		loadedItems.Add(key, script);
		HttpContext.Current.Items[type] = loadedItems;
	}
}

private enum ContentType
{
	Js,
	Css
}

Мы загрузили в коллекцию HttpContext.Current.Items необходимые нам скрипты и стили. Теперь, собственно, остался сам HttpModule, который сделает работу по вставке Html в указанную нами метку на странице. Листинг тоже с комментариями на русском:

public class ScriptCssRenderingModule : IHttpModule
    {
        public void Init(HttpApplication application)
        {
            // Подписка на событие, на котором будем переписывать Response
            application.PostRequestHandlerExecute += OnPostRequestHandlerExecute;
        }

        public void Dispose()
        {
        }

        private static void OnPostRequestHandlerExecute(Object sender, EventArgs e)
        {
            // Проверяем, что Request имеет нужный нам тип и меняем Response, используя Filter (это стандартный подход)
            var httpContext = new HttpContextWrapper(HttpContext.Current);
            if (httpContext.Response.ContentType == "text/html" || httpContext.Response.ContentType == "application/xhtml+xml")
            {
                httpContext.Response.Filter = new ResponseFilter(httpContext.Request, httpContext.Response);
            }
        }

        private class ResponseFilter : MemoryStream
        {            
            private readonly Stream outputStream;            
            private readonly HttpRequestBase request;
            private readonly HttpResponseBase response;
            private readonly StringBuilder outputHtml = new StringBuilder();
            private bool isRewrited;

            public ResponseFilter(HttpRequestBase httpRequestWrapper, HttpResponseBase httpResponseWrapper)
            {
                request = httpRequestWrapper;
                response = httpResponseWrapper;                
                outputStream = response.Filter;
            }

            public override void Write(Byte[] buffer, Int32 offset, Int32 count)
            {
                outputHtml.Append(response.Output.Encoding.GetString(buffer, offset, count));
            }

            public override void Close()
            {
                if (!isRewrited)
                {
                    RewriteOutput();
                    isRewrited = true;
                }

                base.Close();
            }

            private void RewriteOutput()
            {
                if (request.IsAjaxRequest())
                {
                    outputHtml
                        .Append(HtmlCustomHelper.LoadCss())
                        .Append(HtmlCustomHelper.LoadJs());
                }
                else
                {
                    outputHtml
                        .Replace(Constants.Web.CssZoneMark, HtmlCustomHelper.LoadCss())
                        .Replace(Constants.Web.ScriptsZoneMark, HtmlCustomHelper.LoadJs());
                }

                var outputHtmlBytes = response.Output.Encoding.GetBytes(outputHtml.ToString());
                outputStream.Write(outputHtmlBytes, 0, outputHtmlBytes.Length);
            }
        }
    }

Еще несколько недостающих методов из HtmlCustomHelper:

public static String LoadJs()
{
	return LoadClientItems(ContentType.Js);
}

public static String LoadCss()
{
	return LoadClientItems(ContentType.Css);
}                

private static String LoadClientItems(ContentType itemType)
{
	var scripts = HttpContext.Current.Items[itemType] as Dictionary<String, String>;
	if (scripts == null)
		return String.Empty;

	var stringBuilder = new StringBuilder();
	foreach (var script in scripts)
	{
		stringBuilder.AppendLine(script.Value);
	}

	return stringBuilder.ToString();
}

Осталось только зарегистрировать наш модуль в Web.config:

<httpModules>
      <add name="ScriptCssRenderingModule" type="Tortuga.Web.HttpModules.ScriptCssRenderingModule"/>
</httpModules>

Вот вроде бы и все, с таким ScriptManager`ом ваш сайт пройдет Html-валидацию.