Этот пост будет интересен прежде всего ASP.NET-разработчикам, которые осваивают «продвинутый» функционал AjaxControlToolkit, в частности, расширение стандартного TabControl — контрола, обеспечивающего клиентские вкладки («табы») на странице.
Впрочем, так как задача по сути сводится к клиентскому программированию, общие принципы окажутся полезными не только ASP.NET-разработчикам, поэтому, думаю, место ей в блоге «Веб-разработка».
Постановка проблемы: обеспечить при использовании TabControl соответствие текущей выбранной вкладки содержимому адресной строки браузера. То есть,
Собственно, задача распадается на две:
Теперь нужно добиться перехода на вкладку, указанную в адресной строке:
И теперь можно оформить всё в красивое и повторно используемое решение через расширитель.
Собственно, готово. Теперь можно одной строкой добавлять поведение AJAX-навигации для TabContainer ;-)
Online-демо
Скачать:
Исходный код расширителя (проект Visual Studio 2008, C#)
Минималистичный сайт-пример применения расширителя
Библиотека с классом расширителя, собранная в режиме Release
Штука удобная получилась, используйте :). Она будет, кстати, использована в двух интересных проектах, которые уже очень скоро увидят свет на хабре.
Для решения этой проблемы нужно вручную разъяснить непонятливому IE, что от него требуется добавить запись в историю навигации. Подход я нашёл в библиотеке HistoryKeeper, вот его суть: добавляем к странице невидимый iframe с содержимым, которое и обновляем каждый раз, когда хотим получить запись в истории навигации. Новая версия компонента теперь работает с IE 7, как с родным. Спасибо за обратную связь ;-)
Кросс-пост с блога компании gendix
Впрочем, так как задача по сути сводится к клиентскому программированию, общие принципы окажутся полезными не только ASP.NET-разработчикам, поэтому, думаю, место ей в блоге «Веб-разработка».
Постановка проблемы: обеспечить при использовании TabControl соответствие текущей выбранной вкладки содержимому адресной строки браузера. То есть,
- чтобы при переходе между вкладками соответствующим образом изменялся адрес,
- можно было в любой момент скопировать ссылку, и открыв её после, попасть на ту же вкладку, откуда была скопирована ссылка,
- обеспечить корректную работоспособность кнопок «Назад» и «Вперёд» браузера для навигации по вкладкам.
Собственно, задача распадается на две:
- принципиально реализовать указанное выше поведение и
- оформить решение в виде расширителя (Extender), подобно стандартным расширителям AjaxToolKit.
Реализация синхронизации с адресной строкой
Подход
Очевидно, что необходимо синхронизовать в две стороны:- при открывании страницы показать ту вкладку, которая указана в URL (где именно указывать эту информацию — об этом ниже), и
- при переключении вкладок — изменять URL.
document.location = "...";будет происходить перезагрузка страницы, что нас никак не устравает. Недолгое гугление показывает, что единственная часть url, которая не передаётся на сервер и не перезагружает страницу — это якорь, то есть то, что идёт после '#' в адресе, например, http://site.com/user/vasya/profile/#contacts. И его можно менять, не вызывая перезагрузку, вот так:
document.location.hash = «myTabName»;Таким образом, текущая вкладка будет отображаться на якорь, например, вот так:
- /user/profile/#contacts
- /user/profile/#password
- /user/profile/#subscribe
Принципиальное решение проблемы
Пусть есть вот такой контрол с вкладками:<ajax:TabContainer ID=«tbcProfile» runat=«server»Напишем сперва прототип решения (не оформляя его в классы и т.п.), чтобы просто убедиться, что подход сработает. Во-первых, мы должны обеспечить перезапись url в ответ на переключение вкладки. Для этого есть у контрола TabContainer клиентское событие OnClientActiveTabChanged. Указав в нём имя функции-обработчика, мы получим то, что хотели. Вот эта функция:
ActiveTabIndex=«0»>
<ajax:TabPanel ID=«tabContacts» runat=«server»>
<ContentTemplate>
...
ContentTemplate>
ajax:TabPanel>
<ajax:TabPanel ID=«tabPassword» runat=«server»>
<ContentTemplate>
...
ContentTemplate>
ajax:TabPanel>
<ajax:TabPanel ID=«tabSubscribe» runat=«server»>
<ContentTemplate>
...
ContentTemplate>
ajax:TabPanel>
ajax:TabContainer>
var tabNames = ['contacts', 'password', 'subscribe'];
function onTabChanged(sender, args) {
document.location.hash = tabNames[sender.get_activeTabIndex()];
}Здесь sender — клиентский объект TabContainer, имеющий метод get_activeTabIndex, возвращающий номер текущей выбранной вкладки.А tabNames — массив имён вкладок, отображаемых в URL.
Теперь нужно добиться перехода на вкладку, указанную в адресной строке:
var lastSetTab = null;И осталось только вызвать setTabFromUrl в обработчике загрузки страницы. Сразу скажу, что подход сработал. (UPD: с корректировкой для IE)
function setTabFromUrl()
{
// получаем имя вкладки из URL
var tabFromUrl = window.location.hash.replace('#', '');
// находим клиентский объект TabContainer
var tbcMenu = $find('<%= tbcProfile.ClientID %>');
// если объект уже инициализирован и вкладка в URL изменилась с момента последней проверки
if (tbcMenu != null && tabFromUrl != lastSetTab)
{ // то запоминаем последнее переключение
lastSetTab = tabFromUrl;
// ищем, какой индекс у требуемой вкладки
for (var i = 0; i < tabNames.length; i++)
{
if (tabFromUrl == tabNames[i])
{ // если нашли, то
// временно отключаем обратную синхронизацию с URL
tbcMenu.supressTabChanged = true;
// пытаемся сделать активной выбранную вкладку
try { tbcMenu.set_activeTabIndex(i); }
// если объект ещё не до конца проинициализирован, может быть исключение
catch (e) { lastSetTab = null; } // тогда просто отменяем запоминание переключения — словно ничего и не было
// включаем обратную синхронизацию с URL
tbcMenu.supressTabChanged = false;
break;
}
}
}
// запускаем этот же метод через небольшой интервал времени — отслеживая таким образом кнопки «Назад» и «Вперёд».
setTimeout(setTabFromUrl, 200);
}
И теперь можно оформить всё в красивое и повторно используемое решение через расширитель.
Разработка расширителя для TabContainer — «UrlFriendlyTabExtender»
Постановка задачи
Выглядеть это должно так: К «обычному» объявлению<ajax:TabContainer ID=«tbcProfile» runat=«server»добавляется объявление:
ActiveTabIndex=«0»>
<ajax:TabPanel ID=«tabContacts» runat=«server»>
<ContentTemplate>
...
ContentTemplate>
ajax:TabPanel>
<ajax:TabPanel ID=«tabPassword» runat=«server»>
<ContentTemplate>
...
ContentTemplate>
ajax:TabPanel>
<ajax:TabPanel ID=«tabSubscribe» runat=«server»>
<ContentTemplate>
...
ContentTemplate>
ajax:TabPanel>
ajax:TabContainer>
<ext:UrlFriendlyTabExtender runat=«server» TargetControlID=«tbcProfile» TabNames=«contacts, password, subscribe»/>И после этого всё вышеописанное должно заработать.
Реализация
Создаём проект библиотеки по шаблону AJAX Extender Control Library. Мы имеем в проекте 3 файла: с серверным C#-кодом, клиентским JS-скриптом и с ресурсами (пустой). Переименуем классы для соответствия нашим требованиям: серверный класс будет называться UrlFriendlyTabExtender, а клиентский — UrlFriendlyTabClientBehavior, файл с ресурсами — с тем же именем, что и клиентский скрипт. Привожу код с комментариями:класс UrlFriendlyTabExtender (C#)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.UI;
using AjaxControlToolkit;
using System.Text;
using System.ComponentModel;
namespace Utils.Web.Extenders
{
/// <summary>
/// Обеспечивает при использовании TabControl соответствие текущей выбранной вкладки содержимому адресной строки браузера.
/// То есть,
/// - чтобы при переходе между вкладками соответствующим образом изменялся адрес,
/// - можно было в любой момент скопировать ссылку, и открыв её после, попасть на ту же вкладку,
/// откуда была скопирована ссылка,
/// - обеспечить корректную работоспособность кнопок «Назад» и «Вперёд» браузера для навигации по вкладкам.
/// </summary>
[ // указываем, что наш расширитель именно для TabContainer
TargetControlType(typeof(TabContainer))
]
public class UrlFriendlyTabExtender: ExtenderControl
{
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
// подписываем TabContainer на его клиентское событие изменения текущей вкладки
// обработчиком назначаем «статический» метод клиентского класса UrlFriendlyTabClientBehavior.onTabChanged
((TabContainer)Parent.FindControl(TargetControlID)).OnClientActiveTabChanged =
«Utils.Web.Extenders.UrlFriendlyTabClientBehavior.onTabChanged»;
// описываем, как инициализировать массив имён вкладок — генерим скрипт этого массива
// и замещаем метод UrlFriendlyTabClientBehavior.initTabNames этим кодом.
Page.ClientScript.RegisterStartupScript(Page.GetType(), ClientID, string.Format(@"
Utils.Web.Extenders.UrlFriendlyTabClientBehavior.prototype.initTabNames = function()
{{
this._tabNames = [{0}];
}}
", ScriptTabNamesAsJavaScriptArray()), true);
}
/// <summary>
/// Список слов, разделённых пробелами, запятыми или точками с запятой,
/// которые представляют ссылочные имена (якори) для вкладок нашей панели.
/// Имена идут в порядке, соответствующем порядку вкладок.
/// <example>TabNames=«main, photos, news»</example>
/// </summary>
[Bindable(true), Category(«Behaviour»)]
public string TabNames
{
get
{
if ((ViewState[«TabNames»] as string) != null)
return (string)ViewState[«TabNames»];
else
return string.Empty;
}
set
{
ViewState[«TabNames»] = value;
}
}
// стандартные перегрузки для Extender
protected override IEnumerable<ScriptDescriptor>
GetScriptDescriptors(System.Web.UI.Control targetControl)
{
yield return new ScriptBehaviorDescriptor(«Utils.Web.Extenders.UrlFriendlyTabClientBehavior», targetControl.ClientID);
}
protected override IEnumerable<ScriptReference>
GetScriptReferences()
{
yield return new ScriptReference(«Utils.Web.Extenders.UrlFriendlyTabClientBehavior.js», this.GetType().Assembly.FullName);
}
/// <summary>
/// Из строки с разделителями генерирует скрипт JS-массива (без скобок).
/// </summary>
/// <returns></returns>
private string ScriptTabNamesAsJavaScriptArray()
{
var tabsMapDeclaration = new StringBuilder();
IEnumerable<string> tabNames = GetTabNames(TabNames);
int tabNamesCount = tabNames.Count();
int tabNamesIndex = 0;
foreach (string tabName in tabNames)
{
if (tabNamesIndex < (tabNamesCount — 1))
tabsMapDeclaration.AppendFormat("'{0}', ", tabName);
else
tabsMapDeclaration.AppendFormat("'{0}'", tabName);
tabNamesIndex++;
}
return tabsMapDeclaration.ToString();
}
private IEnumerable<string> GetTabNames(string tabNamesAggregated)
{
return tabNamesAggregated.Split(new[] { ' ', ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
}
}
}
Класс UrlFriendlyTabClientBehavior (JavaScript)
Type.registerNamespace(«Utils.Web.Extenders»);
// короткий метод для создания привязанного делегата
// если он вызывает вопросы, вам сюда: http://habrahabr.ru/blogs/javascript/31647/
function $delegate($this, method)
{
return function()
{
return method.apply($this, arguments);
};
};
// конструктор класса UrlFriendlyTabClientBehavior
// param: element — DOM-элемент, на который «надет» наш TabContainer
Utils.Web.Extenders.UrlFriendlyTabClientBehavior = function(element)
{
// конструктор базового класса
Utils.Web.Extenders.UrlFriendlyTabClientBehavior.initializeBase(this, [element]);
// описание и инициализация полей нашего класса
this._lastSetTab = null;
this._tabNames = [];
};
// статический метод — обработчик клиентского события изменения выбранной вкладки
Utils.Web.Extenders.UrlFriendlyTabClientBehavior.onTabChanged = function(sender, args)
{
// передаётся клиентский объект TabContainer
var tbcMenu = sender;
// по нему получаем DOM-объект (get_element()), а из него — объект нашего класса UrlFriendlyTabClientBehavior
var extender = tbcMenu.get_element().UrlFriendlyTabClientBehavior;
// если это не наш код вызвал переключение вкладки
if (!tbcMenu.supressTabChanged)
{ // то переписываем якорь в адресной строке, получая название якоря методом UrlFriendlyTabClientBehavior.getTabName (см. ниже)
document.location.hash = extender.getTabName(tbcMenu.get_activeTabIndex());
}
};
// методы класса UrlFriendlyTabClientBehavior
Utils.Web.Extenders.UrlFriendlyTabClientBehavior.prototype =
{
initialize: function()
{
Utils.Web.Extenders.UrlFriendlyTabClientBehavior.callBaseMethod(this, 'initialize');
// загружаем в поле _tabNames массив имён вкладок
this.initTabNames();
// пытаемся установить ту, что указана в URL
this.setTabFromUrl();
},
// установить текущей ту вкладку, что указана в URL
setTabFromUrl: function()
{
// получаем её имя из URL
var tabFromUrl = window.location.hash.replace('#', '');
// находим клиентский объект TabContainer
var tbcMenu = $find(this.get_element().id);
// если объект уже инициализирован и вкладка в URL изменилась с момента последней проверки
if (tbcMenu != null && tabFromUrl != this._lastSetTab)
{ // то запоминаем последнее переключение
this._lastSetTab = tabFromUrl;
// ищем, какой индекс у требуемой вкладки
for (var i = 0; i < this._tabNames.length; i++)
{
if (tabFromUrl == this._tabNames[i])
{ // если нашли, то
// временно отключаем обратную синхронизацию с URL
tbcMenu.supressTabChanged = true;
// пытаемся сделать активной выбранную вкладку
try { tbcMenu.set_activeTabIndex(i); }
// если объект ещё не до конца проинициализирован, может быть исключение
catch (e) { this._lastSetTab = null; } // тогда просто отменяем запоминание переключения — словно ничего и не было
// включаем обратную синхронизацию с URL
tbcMenu.supressTabChanged = false;
break;
}
}
}
// запускаем этот же метод через небольшой интервал времени — отслеживая таким образом кнопки «Назад» и «Вперёд».
window.setTimeout($delegate(this, this.setTabFromUrl), 200);
},
// получить имя index-ой вкладки
getTabName: function(index)
{
return this._tabNames[index];
},
dispose: function()
{
Utils.Web.Extenders.UrlFriendlyTabClientBehavior.callBaseMethod(this, 'dispose');
}
};
Utils.Web.Extenders.UrlFriendlyTabClientBehavior.registerClass('Utils.Web.Extenders.UrlFriendlyTabClientBehavior', Sys.UI.Behavior);
if (typeof (Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
Собственно, готово. Теперь можно одной строкой добавлять поведение AJAX-навигации для TabContainer ;-)
Online-демо
Скачать:
Исходный код расширителя (проект Visual Studio 2008, C#)
Минималистичный сайт-пример применения расширителя
Библиотека с классом расширителя, собранная в режиме Release
Штука удобная получилась, используйте :). Она будет, кстати, использована в двух интересных проектах, которые уже очень скоро увидят свет на хабре.
UPD: поддержка IE7
Как указал mbrodin, решение в первоначальном виде решало не все поставленные задачи, а именно: в IE, хотя адрес и обновлялся, записей в истории навигации не появлялось, и кнопки «Вперёд» и «Назад» оставались неактивными.Для решения этой проблемы нужно вручную разъяснить непонятливому IE, что от него требуется добавить запись в историю навигации. Подход я нашёл в библиотеке HistoryKeeper, вот его суть: добавляем к странице невидимый iframe с содержимым, которое и обновляем каждый раз, когда хотим получить запись в истории навигации. Новая версия компонента теперь работает с IE 7, как с родным. Спасибо за обратную связь ;-)
Кросс-пост с блога компании gendix