|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionLike Evyatar Ben-Shitrit says in the introduction to his article The ScrollableListBox Custom Control for ASP.NET 2.0, I too started out searching for an ASP.NET At first I thought I just wanted a BackgroundI started out searching and immediately found lintom's article Finally a Horizontal Scroll Bar List Box in ASP.NET! Through that article I learned about the strategy of wrapping the ListBox inside a What I can bring to this series of improvements upon a horizontally-scrollable
A Comprehensive SolutionThe first thing we need to do is create a new server control that extends Microsoft's using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace DanLudwig.Controls.Web
{
public class ListBox : System.Web.UI.WebControls.ListBox,
System.Web.UI.IScriptControl
{
// all the server code we need will go right here
}
}
As for configurable properties, our protected virtual ScriptManager ScriptManager
{
get { return ScriptManager.GetCurrent(Page); }
}
public virtual bool HorizontalScrollEnabled
{
set { this.ViewState["HorizontalScrollEnabled"] = value; }
get
{
object output = this.ViewState["HorizontalScrollEnabled"];
if (output == null)
output = false;
return (bool)output;
}
}
public virtual int ScrollTop
{
set { this.ViewState["ScrollTop"] = value; }
get
{
object output = this.ViewState["ScrollTop"];
if (output == null)
output = 0;
return (int)output;
}
}
public virtual int ScrollLeft
{
set { this.ViewState["ScrollLeft"] = value; }
get
{
object output = this.ViewState["ScrollLeft"];
if (output == null)
output = 0;
return (int)output;
}
}
At this point, most other AJAX Extensions primer articles I've read discuss how to implement the
//
// 1.)
// Register the client control's namespace
//
Type.registerNamespace('DanLudwig.Controls.Client');
// 2.)
// Define the client control's class
//
DanLudwig.Controls.Client.ListBox = function(element)
{
// initialize base (Sys.UI.Control)
DanLudwig.Controls.Client.ListBox.initializeBase(this, [element]);
// declare fields for use by properties
this._horizontalScrollEnabled = null;
this._scrollTop = null;
this._scrollLeft = null;
}
// 3.)
// Define the class prototype
//
DanLudwig.Controls.Client.ListBox.prototype =
{
// 3a)
// Override / implement the initialize method
//
initialize : function()
{
// call base initialize
DanLudwig.Controls.Client.ListBox.callBaseMethod(this, 'initialize');
// more code will go here later
}
,
// 3b)
// Override / implement the dispose method
//
dispose : function()
{
// more code will go here later
// call base dispose
DanLudwig.Controls.Client.ListBox.callBaseMethod(this, 'dispose');
}
,
// 3c)
// Define the event handlers (this will be done later)
//
,
// 3d)
// Define the property get and set methods.
//
set_horizontalScrollEnabled : function(value)
{
if (this._horizontalScrollEnabled !== value)
{
this._horizontalScrollEnabled = value;
this.raisePropertyChanged('_horizontalScrollEnabled');
}
}
,
get_horizontalScrollEnabled : function()
{
return this._horizontalScrollEnabled;
}
,
set_scrollTop : function(value)
{
if (this._scrollTop !== value)
{
this._scrollTop = value;
this.raisePropertyChanged('_scrollTop');
}
}
,
get_scrollTop : function()
{
return this._scrollTop;
}
,
set_scrollLeft : function(value)
{
if (this._scrollLeft !== value)
{
this._scrollLeft = value;
this.raisePropertyChanged('_scrollLeft');
}
}
,
get_scrollLeft : function()
{
return this._scrollLeft;
}
} // end prototype declaration
// 4.)
// Optionally enable JSON Serialization
//
DanLudwig.Controls.Client.ListBox.descriptor =
{
properties: [
{name: '_horizontalScrollEnabled', type: Boolean}
,{name: '_scrollTop', type: Number }
,{name: '_scrollLeft', type: Number }
]
}
// 5.)
// Register the client control as a type that inherits from Sys.UI.Control.
//
DanLudwig.Controls.Client.ListBox.registerClass(
'DanLudwig.Controls.Client.ListBox', Sys.UI.Control);
// 6.)
// Notify the ScriptManager that this script is loaded.
//
if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
This file doesn't give us any real functionality yet, but it defines the necessary parts so that the client control (the client JavaScript code specific only to our protected virtual IEnumerable<ScriptReference> GetScriptReferences()
{
ScriptReference reference = new ScriptReference();
// use this line when debugging the control in a web site project
reference.Path = ResolveClientUrl("ListBox.js");
// use these lines when the control is released with an embedded
// js resource
//reference.Assembly = "DanLudwig.Controls.AspAjax.ListBox";
//reference.Name = "DanLudwig.Controls.Client.ListBox.js";
return new ScriptReference[] { reference };
}
IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
{
return GetScriptReferences();
}
protected virtual IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
ScriptControlDescriptor descriptor = new ScriptControlDescriptor(
"DanLudwig.Controls.Client.ListBox", this.ClientID);
descriptor.AddProperty("horizontalScrollEnabled",
this.HorizontalScrollEnabled);
descriptor.AddProperty("scrollTop", this.ScrollTop);
descriptor.AddProperty("scrollLeft", this.ScrollLeft);
return new ScriptDescriptor[] { descriptor };
}
IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors()
{
return GetScriptDescriptors();
}
protected override void OnPreRender(EventArgs e)
{
if (!this.DesignMode)
{
if (ScriptManager == null)
throw new HttpException(
"A ScriptManager control must exist on the current page.");
ScriptManager.RegisterScriptControl(this);
}
base.OnPreRender(e);
}
protected override void Render(HtmlTextWriter writer)
{
if (!this.DesignMode)
ScriptManager.RegisterScriptDescriptors(this);
// more code will go here later
base.Render(writer);
// more code will go here later
}
This should get rid of those pesky compiler errors telling you to implement the interface you declared for the class earlier. If you're following along with our own web site project, the code discussed so far should compile and render fine at this point (though there isn't any real functionality yet). The server control will set properties on the client control, but we still have to put them to use. Be warned though, from here forward, testing the control in an ASPX page may produce funky results until we reach the end of the article. Render a DIV around the ListBox... or notSo far we've done the common things you'll need to do every time you create a new AJAX-enabled server control with client events. The client events are what will be specific to your control. In the case of our Another thing we should do while we're rendering is pass certain characteristics of the public const string ContainerClientIdSuffix = "__LISTBOXHSCROLLCONTAINER";
public const string ScrollStateClientIdSuffix = "__LISTBOXHSCROLLSTATE";
protected override void Render(HtmlTextWriter writer)
{
if (!this.DesignMode)
ScriptManager.RegisterScriptDescriptors(this);
if (this.HorizontalScrollEnabled)
{
// wrap this control in a DIV container
this.AddContainerAttributesToRender(writer);
writer.RenderBeginTag(HtmlTextWriterTag.Div);
}
base.Render(writer);
if (this.HorizontalScrollEnabled)
{
// add a hidden field to store client scroll state
// and close the container
this.AddScrollStateAttributesToRender(writer);
writer.RenderBeginTag(HtmlTextWriterTag.Input);
writer.RenderEndTag();
writer.RenderEndTag();
}
}
protected virtual void AddContainerAttributesToRender(HtmlTextWriter writer)
{
// when horizontal scrolling is enabled, width and border styles
// should be delegated to the container
if (HorizontalScrollEnabled)
{
// add required container attributes
writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID
+ ContainerClientIdSuffix);
writer.AddStyleAttribute(HtmlTextWriterStyle.Overflow, "auto");
writer.AddStyleAttribute(HtmlTextWriterStyle.Width,
this.Width.ToString());
// add optional container attributes
Color borderColor = this.BorderColor;
if (!borderColor.Equals(Color.Empty))
writer.AddStyleAttribute(HtmlTextWriterStyle.BorderColor,
string.Format("#{0}", borderColor.ToArgb().ToString("x")
.Substring(2)));
BorderStyle borderStyle = this.BorderStyle;
if (!borderStyle.Equals(BorderStyle.NotSet))
writer.AddStyleAttribute(HtmlTextWriterStyle.BorderStyle,
borderStyle.ToString());
Unit borderWidth = this.BorderWidth;
if (!borderWidth.Equals(Unit.Empty))
writer.AddStyleAttribute(HtmlTextWriterStyle.BorderWidth,
borderWidth.ToString());
// move style declarations from the Style attribute into the DIV
// container.
foreach (string key in this.Style.Keys)
{
writer.AddStyleAttribute(key, this.Style[key]);
}
this.Style.Remove(HtmlTextWriterStyle.Width);
this.Style.Remove("width");
this.Style.Remove(HtmlTextWriterStyle.BorderWidth);
this.Style.Remove(HtmlTextWriterStyle.BorderStyle);
this.Style.Remove(HtmlTextWriterStyle.BorderColor);
this.Style.Remove("border");
}
}
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
if (HorizontalScrollEnabled)
{
// take advantage of the base method by clearing the properties
// we don't want rendered, adding the attributes, then restoring
// the properties to their original values. BTW, why is there no
// writer method to remove attributes? Or did I miss it???
Unit originalWidth = this.Width;
Unit originalBorderWidth = this.BorderWidth;
BorderStyle originalBorderStyle = this.BorderStyle;
Color originalBorderColor = this.BorderColor;
this.Width = Unit.Empty;
this.BorderWidth = Unit.Empty;
this.BorderStyle = BorderStyle.NotSet;
this.BorderColor = Color.Empty;
base.AddAttributesToRender(writer);
// get rid of default firefox border
writer.AddStyleAttribute("border", "0px none");
this.Width = originalWidth;
this.BorderWidth = originalBorderWidth;
this.BorderStyle = originalBorderStyle;
this.BorderColor = originalBorderColor;
}
else
{
base.AddAttributesToRender(writer);
}
}
protected virtual void AddScrollStateAttributesToRender(
HtmlTextWriter writer)
{
// the hidden field should have an id, name, type, and default value
string fieldId = this.ClientID + ScrollStateClientIdSuffix;
writer.AddAttribute(HtmlTextWriterAttribute.Id, fieldId);
writer.AddAttribute(HtmlTextWriterAttribute.Name, fieldId);
writer.AddAttribute(HtmlTextWriterAttribute.Type, "hidden");
writer.AddAttribute(HtmlTextWriterAttribute.Value, string.Format(
"{0}:{1}", this.ScrollTop, this.ScrollLeft));
}
This is almost all of the code we need in the server control. The only thing that's missing is what the server control does when it receives new scroll position information from the client during a postback. As it turns out, all we need to do is extract the data passed by our hidden field and apply its values to our respective public static readonly char[] ClientStateSeparator = new char[] { ':' };
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
// update the server control's scroll position state
string scrollStateClientId = this.ClientID + ScrollStateClientIdSuffix;
object state = Page.Request.Form[scrollStateClientId];
if (state != null)
{
//the state will be formatted with the pattern "scrollTop:scrollLeft"
string[] scrollState = state.ToString().Split(
ClientStateSeparator,2);
int scrollTop = 0;
if (scrollState[0] != null && int.TryParse(scrollState[0],
out scrollTop))
this.ScrollTop = scrollTop;
int scrollLeft = 0;
if (scrollState[1] != null && int.TryParse(scrollState[1],
out scrollLeft))
this.ScrollLeft = scrollLeft;
}
}
Handle Client Events to Add ListBox Functionality... or notThe rest of the code we need to write is all in the ListBox.js file. All of the user interaction with the In order to handle these events, we first have to add some code to our overridden // 3.)
// Define the class prototype
//
DanLudwig.Controls.Client.ListBox.prototype =
{
// 3a)
// Override / implement the initialize method
//
initialize : function()
{
// call base initialize
DanLudwig.Controls.Client.ListBox.callBaseMethod(this, 'initialize');
// only create event handlers and delegates if horizontal
// scrolling is enabled
if (this.get_horizontalScrollEnabled())
{
// create event delegates
this._onkeydownHandler = Function.createDelegate(
this, this._onKeyDown);
this._onchangeHandler = Function.createDelegate(
this, this._onChange);
this._onkeydownContainerHandler = Function.createDelegate(
this, this._onContainerKeyDown);
this._onscrollContainerHandler = Function.createDelegate(
this, this._onContainerScroll);
// add event handlers for the ListBox
$addHandlers(this.get_element(),
{
'keydown' : this._onKeyDown
,'change' : this._onChange
}, this);
// add event handlers for the ListBox's DIV container
$addHandlers(this.get_elementContainer(),
{
'keydown' : this._onContainerKeyDown
,'scroll' : this._onContainerScroll
}, this);
}
// when horizontal scrolling is enabled, initialize the control
if (this.get_horizontalScrollEnabled())
{
var listBox = this.get_element();
var container = this.get_elementContainer();
// before changing the listbox's size, store the original
// size in the container.
container.originalListBoxSize = listBox.size;
if (this.get_element().options.length > this.get_element().size)
{
// change the listbox's size to eliminate internal scrolling
listBox.size = listBox.options.length;
// set the height of the container based on the
// original listbox's size
// (add 2 pixels of padding to prevent clipping)
container.style.height
= ((container.scrollHeight / listBox.size)
* (container.originalListBoxSize)) + 2 + 'px';
}
// set the scroll position based on server state
container.scrollTop = this.get_scrollTop();
container.scrollLeft = this.get_scrollLeft();
// if the ListBox is too narrow, expand it to fill the DIV
// container
if (container.scrollWidth <= parseInt(container.style.width))
{
listBox.style.width = '100%';
listBox.style.height = container.scrollHeight + 'px';
container.style.height
= ((container.scrollHeight / listBox.size)
* (container.originalListBoxSize)) + 'px';
// there is a known bug in some FF versions that renders
// 'XX' in empty selects. To overcome this issue, you could
// add an empty item to empty ListBoxes
//if (listBox.options.length < 1)
//{
// listBox.options[0] = new Option('','');
//}
}
}
}
,
// 3b)
// Override / implement the dispose method
//
dispose : function()
{
// clear event handlers from the ListBox
$clearHandlers(this.get_element());
// can only clear event handlers from the DIV container if it exists
if (this.get_elementContainer() != null)
{
$clearHandlers(this.get_elementContainer());
}
// call base dispose
DanLudwig.Controls.Client.ListBox.callBaseMethod(this, 'dispose');
}
,
// helper method for retrieving the ListBox's DIV container
get_elementContainer : function()
{
// only return the container if horizontal scrolling is enabled.
if (this.get_horizontalScrollEnabled()
&& this.get_element().parentNode != null)
{
return this.get_element().parentNode;
}
else
{
return null;
}
}
,
In the // 3c)
// Define the event handlers
//
_onKeyDown : function(e)
{
if (this.get_element() && !this.get_element().disabled)
{
// cancel bubble to prevent listbox from re-scrolling
// back to the top
event.cancelBubble = true;
return true;
}
}
,
_onChange : function(e)
{
if (this.get_element() && !this.get_element().disabled)
{
// update the scroll position when the user changes the
// item selection
updateListBoxScrollPosition(this.get_elementContainer(),
this.get_element(), null);
event.cancelBubble = true;
return true;
}
}
,
// when keypresses are received by the container
// (not bubbled up from the listbox),
// they should be passed to the listbox.
_onContainerKeyDown : function(e)
{
// setting focus on the listbox scrolls back to the top
this.get_element().focus();
// re-position the container scollbars after the focus()
// method scrolled to the top
setTimeout("updateListBoxScrollPosition("
+ "document.getElementById('"
+ this.get_elementContainer().id
+ "'), document.getElementById('"
+ this.get_element().id
+ "'), "
+ this.get_elementContainer().scrollTop
+ ")", "5");
}
,
_onContainerScroll : function(e)
{
// when the container is scrolled, update this control
this.set_scrollState();
}
,
// set this property when the DIV container is scrolled
set_scrollState : function()
{
// first of all, make sure the scroll properties are set
this.set_scrollTop(this.get_elementContainer().scrollTop);
this.set_scrollLeft(this.get_elementContainer().scrollLeft);
// server control expects the state to be in the format
// "scrollTop:scrollLeft"
var stateValue = this.get_scrollTop() + ':' + this.get_scrollLeft();
// save the state data in the hidden field
this.get_elementState().value = stateValue;
this.raisePropertyChanged('_scrollState');
}
,
get_elementState : function()
{
// function locates and returns the hidden form field which
// stores the scroll state data.
if (this.get_horizontalScrollEnabled()
&& this.get_element().parentNode != null)
{
// the second child node in the DIV container with a valid
// VALUE property is the hidden field.
// must find the hidden field this way because IE and FF have
// differing childNodes collections
var childNodeIndex = -1;
for (i = 0; i<this.get_elementContainer().childNodes.length; i++)
{
if (this.get_elementContainer().childNodes[i].value != null)
{
childNodeIndex++;
if (childNodeIndex > 0)
{
childNodeIndex = i;
break;
}
}
}
return this.get_elementContainer().childNodes[childNodeIndex];
}
else
{
return null;
}
}
,
If you've been trying to get these functions to work before coming this far in the article, you're probably pretty frustrated. There's one more client-side function we need to add, and it's probably the most important of all. Again, thanks to zivros for the final piece that ties everything together and actually updates the // 5.)
// Register the client control as a type that inherits from Sys.UI.Control.
//
DanLudwig.Controls.Client.ListBox.registerClass(
'DanLudwig.Controls.Client.ListBox', Sys.UI.Control);
// static function called by the client control(s)
function updateListBoxScrollPosition(container, listBox, realScrollTop)
{
// realScrollTop defaults to zero when it is not set
if (realScrollTop == null)
realScrollTop = container.scrollTop;
// determine the size of a single item in the ListBox
var scrollStepHeight = container.scrollHeight / listBox.size;
//find out what are the visible top & bottom items in the ListBox
var minVisibleIdx = Math.round(realScrollTop / scrollStepHeight);
var maxVisibleIdx = minVisibleIdx + container.originalListBoxSize - 2;
// handle the case where a user is scrolling down...
if (listBox.selectedIndex >= maxVisibleIdx)
{
container.scrollTop
= (listBox.selectedIndex - container.originalListBoxSize + 2)
* scrollStepHeight;
}
// handle the case where a user is scrolling up...
else if (listBox.selectedIndex < minVisibleIdx)
{
container.scrollTop = listBox.selectedIndex * scrollStepHeight;
}
// in all other cases, set the vertical scroll to the realScrollTop
// parameter.
else
{
container.scrollTop = realScrollTop;
}
}
// 6.)
// Notify the ScriptManager that this script is loaded.
//
if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
VoilaThere is a lot of overlapping code here, so if you're copying and pasting it straight from this article into your own files, you may be running into the dreaded "DanLudwig is undefined" client error message. That error message, like so many others, can be misleading. I assure you, I am very well defined. However, the slightest syntax error in the ListBox.js file will cause the DanLudwig namespace to not be successfully registered. Sometimes it's because there aren't the required commas separating the different parts of the prototype declaration, and other times it's because of more obscure errors in the client script file. If you're having trouble copying and pasting, use the source files provided along with this article (but remember to change the reference in the Perfectionism is a DiseaseNow we have a fairly intuitive Another shortcoming of this control can be demonstrated by viewing it in IE7 and FireFox 1.5. In FireFox, when the mouse is positioned over the You might also have noticed that our Points of InterestThis is only the second code article I've ever written. The first, now quite obsolete More Scrolling Function in Flash 5, I wrote way back in 2000. Some of the same mathematics needed to calculate the scroll position is similar, but even I find it odd that the only two articles I've ever written have to do with this rather mundane, yet UI-critical necessity. Perhaps this Exercises for YouI know this control isn't perfect, though I do think it's highly reusable when packaged correctly, and we're going to make it even better in the next article. Here are some considerations for you to tackle if you find this control falling short of your requirements:
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||