Berichten met tag ASP.NET
JavaScript wrappen in herbruikbare jQuery plugins
0In mijn vorige blog Gebruikersergernis: verspringende scrollbars is een oplossing in JavaScript uitgewerkt. jQuery is een veelgebruikte JavaScript library die gebruik maakt van een plugin architectuur. Daarmee kun je uitbreidingen realiseren. Plugins maken het mogelijk om functionaliteit en programmatuur eenvoudig herbruikbaar te maken. De JavaScript-oplossing om scrollbarposities te onthouden, kan in een jQuery plugin verwerkt worden om de herbruikbaarheid en het onderhoud van de functionaliteit te verbeteren. Ook zijn er nieuwe mogelijkheden door gebruik te maken van jQuery. Zo is het eenvoudig om configuraties en extra functionaliteit toe te voegen en deze netjes te bundelen binnen de namespace van de plugin.
jQuery plugins
jQuery heeft een standaardarchitectuur voor het schrijven van plugins. Andere plugins en libraries, zoals jQuery UI, maken hier eveneens gebruik van. Een plugin heeft een eigen namespace waarin de methodes van de plugin ook binnen dezelfde namespace blijven. Dit voorkomt onduidelijkheden en conflicten met andere plugins en jQuery functionaliteiten. Het framework van de plugin architectuur ziet er als volgt uit:
(function($){
var methods = {
init : function(options) {
scrollpositionSave : function() {
scrollpositionRestoreAll : function() {
scrollpositionClearAll : function() {
};
//Plugin namespace 'scrollPosition' within jQuery
$.fn.scrollPosition = function(method) {
// Method calling logic
if methods[method]) {
return methods[ method ].apply( this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || ! method) {
return methods.init.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exist on jQuery.scrollPosision');
}
};
})(jQuery);
Methodes en events koppelen
De scrollbarmethodes scrollpositionSave(), scrollpositionRestoreAll() en scrollpositionClearAll() worden verwerkt in de plugin architectuur. De werking en benaming van deze methodes zal iets wijzigen door de nieuwe plugin structuur. Ook het koppelen van het scroll event van de HTML DOM-elementen wordt naar de plugin verplaatst. Het koppelen van het event met scrollpositionSave() ziet er in de plugin structuur als volgt uit:
var methods = {
remember: function () {
var obj = $(this);
obj.each(function () {
$(this).scroll(function () {
var element = this;
if (elementCanBeIdentified(element) && elementHasScrolled(element)) {
setCookie(element.id, element.scrollTop);
}
});
});
$(this).scrollPosition("restore");
}
};
Vervolgens wordt een ‘remember’-methode gemaakt die voor ieder object in de jQuery selectie, logica koppelt aan de scrollmethode. Hierin wordt, net zoals in de oude situatie, de schuifbalkpositie in een cookie opgeslagen. De overige methodes worden op dezelfde manier verwerkt in de ‘methods’ array variabele.
Elementen selecteren
Wat verder opvalt is de context van geselecteerde DOM-elementen waarin de plugin zich bevindt. Dit gebeurt op de gebruikelijke jQuery selectie-manier. De gewenste elementen worden op de pagina geselecteerd en doorgegeven aan de plugin:
$().ready(function () {
$('.RememberScrollPosition').scrollPosition('remember');
});
Met $(‘.RememberScrollPosition’) worden DOM-elementen geselecteerd welke ‘RememberScrollPosititon’ als waarde hebben voor het class attribuut. Vervolgens wordt op deze selectie met .scrollPosition(‘remember’) de jQuery plugin ‘scrollPosition’ gestart en en de methode ‘remember’ aangeroepen. Hierdoor is het mogelijk om in de plugin met .each de geselecteerde elementen te doorlopen en per element uit te voeren. Alle bewerkingen in de methodes gebeuren op basis van de geselecteerde elementen. Het is dus niet meer nodig om alle cookies te doorlopen.
Configureren
In jQuery plugins is het gebruikelijk om de werking instelbaar te maken zodat de plugin breed inzetbaar is. Het wordt bijvoorbeeld mogelijk conflicten met naamgevingen van cookies te voorkomen door eenvoudig een prefix toe te voegen aan de cookienaam, aangevuld met een identificatie van de pagina waarop het element zich bevindt. Wanneer een element met hetzelfde ID zich op een andere pagina bevindt zal de scrollpositie niet worden ingesteld. In de oude situatie was dit nog wel het geval. Ook zullen we, in de vorm van een namespace, gebruik maken van een koppelkarakter tussen de cookie prefix, de paginanaam en het ID van het DOM-element. Verder zullen we debug/log informatie geven voor ontwikkelaars. Deze instellingen en hun standaardwaarden nemen we als volgt op in de jQuery-plugin in de ‘settings’ variabele:
(function ($) {
// Default/initial settings for this jQuery plugin.
var settings = {
cookiePrefix: "scrollpos",
page: "default",
concatChar: "::",
debug: false
};
var methods = {
//...
})(jQuery);
Deze plugin instellingen kunnen we meegeven in de aanroep van de methode:
$().ready(function () {
$('.RememberScrollPosition').scrollPosition('remember', { page: "Default.aspx", debug: true });
});
In de plugin methodes moeten de parameters in de methodeaanroep dan enkel nog verwerkt worden in de standaardwaarden van de jQuery-plugin:
var methods = {
remember: function (options) {
options = $.extend(settings, options);
//...
Private methods
Debug informatie en andere logica die enkel binnen de plugin beschikbaar hoeft te zijn, houden we binnen de namespace van de plugin en publiceren we niet met behulp van de methods-constructie. Het verwerken van debug functionaliteit en het toevoegen van de cookie namespace ziet er dan als volgt uit:
(function ($) {
// Default/initial settings for this jQuery plugin.
var settings = {
cookiePrefix: "scrollpos",
page: "default",
concatChar: "::",
debug: false
};
// Public methods
var methods = {
remember: function (options) {
_debug("Calling jQuery.scrollPosition.remember");
options = $.extend(settings, options);
var obj = $(this);
obj.each(function () {
$(this).scroll(function () {
var element = this;
if (_elementCanBeIdentified(element)) {
if (_elementHasScrolled(element)) {
var elementStorageKey = _getElementStoragePropertyName(element);
var elementScrollPosition = element.scrollTop;
_setCookie(elementStorageKey, elementScrollPosition);
_debug("Remembered scroll position in cookie: " + _getElementStorageProperty(element));
} else {
$(this).scrollPosition("forget", options);
}
}
});
});
$(this).scrollPosition("restore", options);
}
};
$.fn.scrollPosition = function (method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exist on jQuery.scrollPosition');
}
};
// Private methods
function _debug(logMessage) {
if (settings._debug == false) {
return;
}
if (window.console != undefined) {
console.log(logMessage);
} else {
//alert(logMessage);
}
}
function _getElementStorageProperty(element) {
return _getElementStoragePropertyName(element) + "=" + element.scrollTop;
};
function _getElementStoragePropertyName(element) {
return _getElementStoragePropertyPrefix() + element.id;
};
function _getElementStoragePropertyPrefix() {
return settings.cookiePrefix + settings.concatChar + settings.page + settings.concatChar;
};
function _setCookie(key, value) {
document.cookie = key + "=" + value;
}
function _elementCanBeIdentified(element) {
try {
return document.getElementById(element.id) && element.id.length > 0;
}
catch (e) {
return false;
}
}
function _elementHasScrolled(element) {
try {
return element.scrollTop > 0;
}
catch (e) {
return null;
}
}
})(jQuery);
De debug logging informatie – die de werking van de programmatuur inzichtelijker maakt voor ontwikkelaars – ziet er als volgt uit:
Conclusies
Zoals je kunt zien is het vrij eenvoudig om bestaande JavaScript-functionaliteit in een jQuery plugin te wrappen. De voordelen zijn direct duidelijk; de code is volgens een duidelijke, vaste en (ook voor andere ontwikkelaars) herkenbare structuur uitgewerkt, hetgeen de onderhoudbaarheid en kwaliteit van de code ten goede komt. Ook kunnen we de standaard functionaliteit van jQuery eenvoudig gebruiken in de plugins. Door gebruik te maken van de plugin hebben we eenvoudig uitbreidingen kunnen realiseren. Deze functionaliteit is in meerdere situaties te gebruiken.
» Download hier het demo project (Visual Studio 2010)
- Visual Studio 2010
- ASP.NET 4.0 Web Application
- jQuery 1.4.1 (standaard beschikbaar in ASP.NET 4.0 Web Application)
De volledige jQuery plugin ziet er als volgt uit:
(function ($) {
// Default/initial settings for this jQuery plugin.
var settings = {
cookiePrefix: "scrollpos",
page: "default",
concatChar: "::",
debug: false
};
// Public methods
var methods = {
// Initialize plugin
init: function (options) {
},
// Store scrollbar position on each scroll of the selected elements
// and restores known scrollbar positions (useful when script is fired, e.g. on request/postback).
// Uses cookie storage.
remember: function (options) {
_debug("Calling jQuery.scrollPosition.remember");
options = $.extend(settings, options);
var obj = $(this);
obj.each(function () {
$(this).scroll(function () {
var element = this;
if (_elementCanBeIdentified(element)) {
if (_elementHasScrolled(element)) {
var elementStorageKey = _getElementStoragePropertyName(element);
var elementScrollPosition = element.scrollTop;
_setCookie(elementStorageKey, elementScrollPosition);
_debug("Remembered scroll position in cookie: " + _getElementStorageProperty(element));
} else {
$(this).scrollPosition("forget", options);
}
}
});
});
$(this).scrollPosition("restore", options);
},
// Restores known scrollbar positions. Uses cookie storage.
restore: function (options) {
_debug("Calling jQuery.scrollPosition.restore");
options = $.extend(settings, options);
var obj = $(this);
obj.each(function () {
var element = this;
var cookies = _getCookiesAsArray();
try {
var elementStorageKey = _getElementStoragePropertyName(element);
var elementScrollPosition = cookies[elementStorageKey];
_elementSetScrollPosition(element.id, elementScrollPosition);
_debug("Restored scroll position: " + element.id + "=" + elementScrollPosition);
}
catch (e) {
}
});
},
// Forget known scrollbar positions. Expire cookie storage.
// Note: does not decouple the scroll event which may store the scroll
// position again if 'remember' is called on the element!
forget: function (options) {
_debug("Calling jQuery.scrollPosition.forget");
options = $.extend(settings, options);
var obj = $(this);
obj.each(function () {
var element = this;
try {
var elementStorageKey = _getElementStoragePropertyName(element);
_clearCookie(elementStorageKey);
_debug("Cleared scroll position: " + element.id);
}
catch (e) {
}
});
}
};
// Private methods
function _debug(logMessage) {
if (settings.debug == false) {
return;
}
if (window.console != undefined) {
console.log(logMessage);
} else {
//alert(logMessage);
}
}
function _getElementStorageProperty(element) {
return _getElementStoragePropertyName(element) + "=" + element.scrollTop;
};
function _getElementStoragePropertyName(element) {
return _getElementStoragePropertyPrefix() + element.id;
};
function _getElementStoragePropertyPrefix() {
return settings.cookiePrefix + settings.concatChar + settings.page + settings.concatChar;
};
function _getCookiesAsArray() {
var result = {};
var cookieContent = document.cookie;
if (cookieContent.length > 0) {
var cookies = cookieContent.split(";");
for (var i = 0; i < cookies.length; i++) {
var keyValuePair = cookies[i].split("=");
try {
var key = keyValuePair[0].replace(" ", "");
var value = keyValuePair[1];
result[key] = value;
}
catch (e) {
}
}
}
return result;
}
function _setCookie(key, value) {
document.cookie = key + "=" + value;
}
function _clearCookie(key) {
try {
var date = new Date();
document.cookie = key + "=0;expires=" + date.toGMTString() + ";";
return true;
}
catch (e) {
return false;
}
}
function _elementCanBeIdentified(element) {
try {
return document.getElementById(element.id) && element.id.length > 0;
}
catch (e) {
return false;
}
}
function _elementHasScrolled(element) {
try {
return element.scrollTop > 0;
}
catch (e) {
return null;
}
}
function _elementSetScrollPosition(elementId, position) {
try {
document.getElementById(elementId).scrollTop = position;
return true;
}
catch (e) {
return false;
}
}
//Prevent jQuery namespacing cluttering by using the suggested jQuery plugin architecture.
//http://docs.jquery.com/Plugins/Authoring#Plugin_Methods
$.fn.scrollPosition = function (method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exist on jQuery.scrollPosition');
}
};
})(jQuery);
Gebruikersergernis: verspringende scrollbars
0Gebruikersergernissen zitten vaak in een klein hoekje. Eén daarvan is dat de positie van de schuifbalk verloren gaat wanneer je een webpagina ververst, bijvoorbeeld bij het aanvinken van een selectiebox of een keuzeveld. In deze blogpost laat ik zie hoe je met behulp van JavaScript hiervoor eenvoudig een oplossing kunt realiseren.
Waarom dit probleem?
Soms kun je het verversen van (delen van) webpagina’s niet vermijden. Ontwikkelaars houden hier meestal rekening mee en proberen requests bij dit soort gebruikersacties te vermijden of zoveel mogelijk te beperken. Soms kan het echter niet anders. Er kunnen bijvoorbeeld afhankelijkheden zitten tussen de verschillende invoervelden en andere formulierelementen. Zie onderstaand voorbeeld:
In het voorbeeld zijn twee containers te zien met een scrollbar. Om functionele redenen worden bij iedere selectie van een item de gegevens verstuurd waarmee iets gedaan moet worden. Voorbeelden daarvan zijn het tonen van informatie over de huidige selectie of het bijwerken van de waarden van een ander afhankelijk formulierelement. Microsoft ASP.NET verstuurt deze data met ‘Postbacks’. Andere implementaties, in bijvoorbeeld PHP, versturen direct een page request terwijl AJAX de inhoud van de formulierelementen vervangt. Ongeacht de gebruikte technologie zal bij het vervangen van de inhoud de scrollbar naar boven verspringen, naar de beginpositie. Dit is niet gebruiksvriendelijk.
Oplossingsrichting
Dit verspringen van de scrollbar is eenvoudig te verhelpen. Functioneel werkt het als volgt. Sla de positie van de balk op zodra deze wijzigt. Wanneer je de inhoud ververst kun je de positie weer terugzetten. De technische werking van deze oplossingsrichting bevat tevens een aantal ontwerpbeslissingen. Wanneer de pagina laadt zul je het scroll event moeten koppelen aan de logica die de positie van de scrollbar opslaat. Het opslaan van de posities moet op een manier gebeuren die tussen pagina requests niet verloren gaat. Dit kan door middel van hidden input values die met het formulier meegestuurd worden. Nog beter is om cookies te gebruiken als opslagmechanisme. Ook als elementen met een scrollbar zich buiten een formulier bevinden, wordt zo de waarde opgeslagen. Om de functionaliteit herbruikbaar te maken voor gebruik in de toekomst, kun je specificeren van welke elementen de scrollbarpositie opgeslagen moet worden. Dit los je op door de class ‘RememberScrollPosition’ aan de betreffende HTML DOM-elementen toe te kennen.
Technische oplossing
In dit voorbeeld kiezen we voor een client side oplossing in JavaScript en gaan we uit van een ASP.NET oplossing die requests/postbacks uitvoert bij iedere selectie van een item. De HTML-code en ASPX-code ziet er als volgt uit:
<div id="scrolbaarElement1" class="RememberScrollPosition"
style="width: 100px; height: 150px; overflow: auto; border: 1px solid #000;">
<asp:CheckBoxList ID="CheckBoxList1" runat="server" AutoPostBack="True">
<asp:ListItem>Item 1</asp:ListItem>
<asp:ListItem>Item 2</asp:ListItem>
<asp:ListItem>Item n</asp:ListItem>
…
</asp:CheckBoxList>
</div>
Allereerst is er logica nodig. Met de class ‘RememberScrollPosition’ kun je die op het scroll event van DOM-elementen uitvoeren. Wanneer jQuery beschikbaar is, kun je dit heel eenvoudig oplossen:
$(document).ready(function () {
scrollpositionRestoreAll();
$(".RememberScrollPosition").each(function () {
$(this).scroll(function () {
scrollpositionSave(this);
});
});
});
Bovenstaande code komt in de <head> sectie van de pagina en wordt uitgevoerd bij het laden van de pagina. Eerst roep je scrollpositionRestoreAll() aan om eerder opgeslagen scrollbalkposities te herstellen. Vervolgens selecteer je met behulp van jQuery alle DOM-elementen met de class ‘RememberScrollPosition’. Op het scroll event van ieder element roep je vervolgens scrollpositionSave() aan. Vanzelfsprekend bevat dit de logica om de positie op te slaan in een cookie. Dat gaat als volgt:
function scrollpositionSave(element) {
if (elementCanBeIdentified(element) && elementHasScrolled(element)) {
setCookie(element.id, element.scrollTop);
}
}
function elementCanBeIdentified(element) {
return document.getElementById(element.id) && element.id.length > 0;
}
function elementHasScrolled(element) {
return element.scrollTop > 0;
}
De DOM-elementen waarvan je de scrollpositie opslaat, moeten een ID-attribuut hebben zodat het element uniek is en herkend kan worden. Anders is niet duidelijk om welk element op de pagina het gaat. De eigenschap ‘scrollTop’ kun je in JavaScript gebruiken om de positie van de schuifbalk op te halen. De positie wordt alleen opgeslagen wanneer deze niet in de beginpositie staat. Zo beperk je de opslag van de posities tot wat strikt noodzakelijk is.
Nu duidelijk is hoe het opslagmechanisme werkt, kun je de posities van de schuifbalk implementeren. Dit gebeurt door middel van het uitlezen van het cookie. Voor iedere waarde die in het cookie staat, wordt het element en de positie uitgelezen én toegepast wanneer het element op de pagina is gevonden.
function scrollpositionRestoreAll() {
var cookies = getCookiesAsArray();
for (var cookieKey in cookies) {
if (elementExists(cookieKey)) {
var elementId = cookieKey;
var elementScrollPosition = cookies[elementId];
elementSetScrollPosition(elementId, elementScrollPosition);
}
}
}
In bovenstaande code worden hulpfuncties gebruikt. Die zijn in aparte functies gezet om ze in andere delen van de programmatuur opnieuw te kunnen gebruiken.
function getCookiesAsArray() {
var result = { };
var cookieContent = document.cookie;
if (cookieContent.length > 0) {
var cookies = cookieContent.split(";");
for (var i = 0; i < cookies.length; i++) {
var keyValuePair = cookies[i].split("=");
try {
var key = keyValuePair[0].replace(" ", "");
var value = keyValuePair[1];
result[key] = value;
}
catch (e) {
}
}
}
return result;
}
function elementExists(elementId) {
return document.getElementById(elementId);
}
function elementSetScrollPosition(elementId, position) {
document.getElementById(elementId).scrollTop = position;
}
In sommige situaties is het nodig om de eerder opgeslagen schuifbalkposities te verwijderen. Dit gaat op dezelfde manier als scrollpositionsRestoreAll(). De actie echter is niet het herstellen van de schuifbalkpositie, maar het verlopen van de cookie. De browser verwijdert in dat geval de waarde.
function scrollpositionsClearAll() {
var cookies = getCookiesAsArray();
for (var cookieKey in cookies) {
if (elementExists(cookieKey)) {
clearCookie(cookieKey);
}
}
}
Bovenstaande JavaScript-methodes kun je opslaan in een apart .js bestand. De functionaliteit is daarmee herbruikbaar op andere pagina’s.
Eventueel kun je scrollpositionsClearAll() gebruiken wanneer de pagina voor het eerst wordt aangeroepen. De gebruiker start dan iedere keer met een herkenbare beginsituatie. Dit kan in de codebehind van de ASPX-1pagina:
C#:
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
Page.ClientScript.RegisterClientScriptBlock(
this.GetType(), "Body_Load_ClearScrollPositionsScript",
"scrollpositionsClearAll()", true);
}
}
VB:
Protected Overrides Sub OnLoad(ByVal e As EventArgs)
MyBase.OnLoad(e)
If Not Me.IsPostBack() Then
Page.ClientScript.RegisterClientScriptBlock(Me.GetType(), "Body_Load_ClearScrollPositionsScript", "scrollpositionsClearAll()", True)
End If
End Sub
Conclusies
Met enkele eenvoudige JavaScript-methodes heb je zo een oplossing gerealiseerd waarbij de scrollposities van elementen worden onthouden tussen de postbacks en het verversen van de pagina. De functionaliteit kan hergebruikt worden. Het zou echter handig zijn als we dit in een jQuery plugin kunnen wrappen. Dat zorgt er niet alleen voor dat het eenvoudiger wordt om deze functionaliteit opnieuw te gebruiken, maar onderhoudt ook de programmatuur beter. In een volgende blogpost zal ik dit verder uitwerken.
» Download hier het demo project (Visual Studio 2010)
- Visual Studio 2010
- ASP.NET 4.0 Web Application
- jQuery 1.4.1 (standaard beschikbaar in ASP.NET 4.0 Web Application)


