📜 ⬆️ ⬇️

Navigation AJAX sites: Extender Control for ajaxtoolkit: TabContainer

This post will be of interest primarily to ASP.NET-developers who master the “advanced” functionality of AjaxControlToolkit, in particular, the extension of the standard TabControl, which provides client tabs (“tabs”) on the page.

However, since the task essentially boils down to client programming, the general principles will be useful not only for ASP.NET developers, so I think it’s a place on the Web Development blog.

Problem definition: when using TabControl, ensure that the currently selected tab matches the contents of the browser address bar. I.e,
  1. so that when you switch between tabs, the address changes accordingly,
  2. it was possible at any time to copy the link, and after opening it after, get on the same tab from which the link was copied,
  3. ensure the correct operation of the browser's Back and Forward buttons for navigating tabs.
I decided to deal with AJAX Extender Controls and implement this useful thing in the form of Extender control.
Actually, the task falls into two:
  1. fundamentally implement the above behavior and
  2. form a solution in the form of an Extender, like standard AjaxToolKit extenders.

The implementation of synchronization with the address bar

An approach

Obviously, you need to synchronize in two directions:The second point raises questions. After all, when changing the URL
document.location = "..." ;
There will be a page reload, which doesn’t get us at all. A short googling indicates that the only part of the url that is not transmitted to the server and does not reload the page is the anchor, that is, what comes after the '#' in the address, for example, http://site.com/user/vasya/profile / #contacts . And it can be changed without causing a reboot, like this:
document.location.hash = "myTabName" ;
Thus, the current tab will be displayed on the anchor, for example, like this:

The principal solution to the problem

Let there be such a tabbed control:
< 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 >
We first write the prototype of the solution (without making it into classes, etc.), just to make sure that the approach works. First, we must ensure that the url is rewritten in response to a tab switch. The TabContainer client has an OnClientActiveTabChanged client event for this. Specifying the name of the handler function in it, we get what we wanted. Here is the function:
var tabNames = [ 'contacts' , 'password' , 'subscribe' ];
function onTabChanged (sender, args) {
document .location.hash = tabNames [sender.get_activeTabIndex ()];
}
Here, sender is a TabContainer client object that has a get_activeTabIndex method that returns the number of the currently selected tab. And tabNames is an array of tab names displayed in the URL.
')
Now you need to switch to the tab specified in the address bar:
var lastSetTab = null ;
function setTabFromUrl ()
{
// get the tab name from the URL
var tabFromUrl = window.location.hash.replace ( '#' , '' );
// find the TabContainer client object
var tbcMenu = $ find ( '<% = tbcProfile.ClientID%>' );
// if the object has already been initialized and the tab in the URL has changed since the last check
if (tbcMenu! = null && tabFromUrl! = lastSetTab)
{ // then remember the last switch
lastSetTab = tabFromUrl;
// look for which index of the required tab
for ( var i = 0; i <tabNames.length; i ++)
{
if (tabFromUrl == tabNames [i])
{ // if found, then
// temporarily disable reverse synchronization with URL
tbcMenu.supressTabChanged = true ;
// try to make the selected tab active
try {tbcMenu.set_activeTabIndex (i); }
// if the object is not fully initialized, an exception may be
catch (e) {lastSetTab = null ; } // then simply cancel the memorization of the switch - as if nothing had happened
// enable reverse synchronization with URL
tbcMenu.supressTabChanged = false ;
break ;
}
}
}

// run the same method in a short time interval - thus tracking the “Back” and “Forward” buttons.
setTimeout (setTabFromUrl, 200);
}

And it remains only to call setTabFromUrl in the page load handler. I must say that the approach worked. (UPD: adjusted for IE )
And now you can arrange everything in a beautiful and reusable solution through the extender.

TabContainer Extender Development - UrlFriendlyTabExtender

Formulation of the problem

It should look like this: To the "normal" ad
< 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 >
Ad is added:
< ext: UrlFriendlyTabExtender runat = "server" TargetControlID = "tbcProfile" TabNames = "contacts, password, subscribe" />
And after that, all of the above should work.

Implementation

Create a library project using the AJAX Extender Control Library template. We have 3 files in the project: with server C # code, client JS script and with resources (empty). Rename the classes to meet our requirements: the server class will be called UrlFriendlyTabExtender, and the client class will be called UrlFriendlyTabClientBehavior, the resource file will be named the same as the client script. I cite the code with comments:
class 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>
/// Ensures that using TabControl matches the currently selected tab with the contents of the browser’s address bar.
/// I.e,
/// - so that when switching between tabs the address is changed accordingly,
/// - it was possible at any time to copy the link, and after opening it after, get on the same tab,
/// where the link was copied from
/// - to ensure correct operation of the browser's Back and Forward buttons for navigating tabs.
/// </ summary>
[ // indicate that our extender is for TabContainer
TargetControlType ( typeof (TabContainer))
]
public class UrlFriendlyTabExtender: ExtenderControl
{
protected override void OnPreRender ( EventArgs e)
{
base .OnPreRender (e);

// sign the TabContainer on its client event of changing the current tab
// assign the "static" method of the client class UrlFriendlyTabClientBehavior.onTabChanged to the handler
((TabContainer) Parent.FindControl (TargetControlID)). OnClientActiveTabChanged =
"Utils.Web.Extenders.UrlFriendlyTabClientBehavior.onTabChanged" ;

// we describe how to initialize an array of tabs names - the gene script of this array
// and replace the UrlFriendlyTabClientBehavior.initTabNames method with this code.
Page .ClientScript.RegisterStartupScript ( Page .GetType (), ClientID, string .Format ( @ "
Utils.Web.Extenders.UrlFriendlyTabClientBehavior.prototype.initTabNames = function ()
{{
this._tabNames = [{0}];
}}
" , ScriptTabNamesAsJavaScriptArray ()), true );
}

/// <summary>
/// A list of words separated by spaces, commas or semicolons
/// which represent the reference names (anchors) for the tabs of our panel.
/// Names go in the order of the tabs.
/// <example> TabNames = "main, photos, news" </ example>
/// </ summary>
[Bindable ( true ), Category ( "Behavior" )]
public string TabNames
{
get
{
if ((ViewState [ "TabNames" ] as string )! = null )
return ( string ) ViewState [ "TabNames" ];
else
return string .Empty;
}
set
{
ViewState [ "TabNames" ] = value ;
}
}

// standard overload for 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>
/// From the string with delimiters generates a script JS-array (without parentheses).
/// </ 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);
}
}
}
Class UrlFriendlyTabClientBehavior (JavaScript)
Type.registerNamespace ( "Utils.Web.Extenders" );

// short method to create an associated delegate
// if it causes questions, go here: http://habrahabr.ru/blogs/javascript/31647/
function $ delegate ($ this , method)
{
return function ()
{
return method.apply ($ this , arguments);
};
};

// constructor of the UrlFriendlyTabClientBehavior class
// param: element - the DOM element that our TabContainer is wearing
Utils.Web.Extenders.UrlFriendlyTabClientBehavior = function (element)
{
// base class constructor
Utils.Web.Extenders.UrlFriendlyTabClientBehavior.initializeBase ( this , [element]);
// description and initialization of the fields of our class
this ._lastSetTab = null ;
this ._tabNames = [];
};

// static method - handler of the client event of the change of the selected tab
Utils.Web.Extenders.UrlFriendlyTabClientBehavior.onTabChanged = function (sender, args)
{
// passed to the TabContainer client object
var tbcMenu = sender;
// from it we get a DOM object (get_element ()), and from it - an object of our class UrlFriendlyTabClientBehavior
var extender = tbcMenu.get_element (). UrlFriendlyTabClientBehavior;
// if it is not our code that caused tab switching
if (! tbcMenu.supressTabChanged)
{ // then rewrite the anchor in the address bar, getting the name of the anchor using the UrlFriendlyTabClientBehavior.getTabName method (see below)
document .location.hash = extender.getTabName (tbcMenu.get_activeTabIndex ());
}
};

// methods of the UrlFriendlyTabClientBehavior class
Utils.Web.Extenders.UrlFriendlyTabClientBehavior.prototype =
{
initialize: function ()
{
Utils.Web.Extenders.UrlFriendlyTabClientBehavior.callBaseMethod ( this , 'initialize' );
// load the tab names array into the _tabNames field
this .initTabNames ();
// try to set the one specified in the URL
this .setTabFromUrl ();
},
// set the current tab as specified in the URL
setTabFromUrl: function ()
{
// get its name from the URL
var tabFromUrl = window.location.hash.replace ( '#' , '' );
// find the TabContainer client object
var tbcMenu = $ find ( this .get_element (). id);
// if the object has already been initialized and the tab in the URL has changed since the last check
if (tbcMenu! = null && tabFromUrl! = this ._lastSetTab)
{ // then remember the last switch
this ._lastSetTab = tabFromUrl;
// look for which index of the required tab
for ( var i = 0; i < this ._tabNames.length; i ++)
{
if (tabFromUrl == this ._tabNames [i])
{ // if found, then
// temporarily disable reverse synchronization with URL
tbcMenu.supressTabChanged = true ;
// try to make the selected tab active
try {tbcMenu.set_activeTabIndex (i); }
// if the object is not fully initialized, an exception may be
catch (e) { this ._lastSetTab = null ; } // then simply cancel the memorization of the switch - as if nothing had happened
// enable reverse synchronization with URL
tbcMenu.supressTabChanged = false ;
break ;
}
}
}

// run the same method in a short time interval - thus tracking the “Back” and “Forward” buttons.
window.setTimeout ($ delegate ( this , this .setTabFromUrl), 200);
},
// get the name of the index tab
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 ();



Actually, it's done. Now you can add AJAX navigation behavior for TabContainer ;-)

Online demo

Download:
Extender source code (Visual Studio 2008 project, C #)
Minimalistic site example of the use of extender
Library with Extender class compiled in Release mode

Stuck comfortable turned out, use :). It will, by the way, be used in two interesting projects that will soon see the light on Habré.

UPD: IE7 support

As mbrodin pointed out, the solution did not solve all the tasks in its original form, namely: in IE, although the address was updated, there were no records in the navigation history, and the “Forward” and “Back” buttons remained inactive.

To solve this problem, you need to manually explain to the dull IE that it is required to add an entry to the navigation history. I found the approach in the HistoryKeeper library, its essence: add an invisible iframe with content to the page, which we update every time we want to get an entry in the navigation history. The new version of the component now works with IE 7, as with the native. Thanks for the feedback ;-)

Cross post from a gendix blog

Source: https://habr.com/ru/post/38495/


All Articles