Static mode DropdownlistImage
Dynamic mode DropdownlistImage with dynamic databinding
Introduction
It's very difficult to find a free and compatible (with all browsers) dropdownlist that can contain image and text. However, I looked at the jQuery open source components and saw that jQuery developers have done an excellent work with their several widgets that they have developed. jQuery is a powerful JavaScript framework with very beautiful and optimized widgets.
I found an example of their dropdownlist image in a simple HTML page, and I decided to try to create a scalable HTML control that encapsulates this dropdownlist. The URL of the simple example is http://marghoobsuleman.com/jquery-image-dropdown.
Using the Code
The base component is a very simple basic HtmlSelect
(System.Web.UI.Htmlcontrols
namespace). I extended this class to add the specific behaviour.
At the beginning, I added the resources of the component in the assembly (JavaScript files, CSS file, and the arrow image of my extended HtmlSelect
class). I added these resources on the override of the PreRender
method, and I added the references in the AssemblyInfo.cs file. Here is the Microsoft recommendation for that. All these resources must be "embedded resources".
The following code shows you a special case: how to display a picture embedded in an assembly and call it in CSS code. In the beginning, the arrow image of the HtmlSelect
is referenced in dd.css like this:
.dd .ddTitle span.arrow
{
float:right;
display:inline-block;
width:16px;
height:16px;
cursor:pointer;
background-image:url('arrow_d.gif');
}
ASP.NET is not able to display arrow_d.gif with this declaration, because the URL of the picture is not arrow_d.gif. If you want to display this picture, you must follow three steps:
Step 1: You must get the image URL with the GetWebResourceUrl
method of the ClientScript
object like this:
string urlImageArrowCombo = Page.ClientScript.GetWebResourceUrl(this.GetType(),
"AssemblyName.dd_arrow.gif");
(In this example, the picture is at the root of the assembly). If the picture was in an assembly directory called "toto", "AssemblyName.dd_arrow.gif" would become "AssemblyName.toto.dd_arrow.gif".
Step 2: Extract the CSS code of the CSS file and put it in the server side to inject the image URL in the style:
string urlImageArrowCombo =
Page.ClientScript.GetWebResourceUrl(this.GetType(), "ComboImg.dd_arrow.gif");
StringBuilder strStyleArrow = new StringBuilder();
strStyleArrow.Append(" .dd .ddTitle span.arrow { ")
.Append(" float:right; ")
.Append(" display:inline-block; ")
.Append(" width:16px; ")
.Append(" height:16px; ")
.Append(" cursor:pointer; ")
.Append(" background-image:url('" + urlImageArrowCombo + "');")
.Append(" } ");
HtmlGenericControl styleArrow = new HtmlGenericControl("style");
styleArrow.Attributes.Add("type", "text/css");
jqdd.Controls.Add(
new LiteralControl(strStyleArrow.ToString())
);
Page.Header.Controls.Add(styleArrow);
Last step: If there are lots of HtmlSelect
images in the same page, the style that you add in the header of the page must be shared by all HtmlSelect
image controls. So, the test Page.Items.Contains("styleAlreadyPut"))
is necessary, to add the style once. The OnPreRender
event is to add resources like I just explained. Here is the code:
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
if (!this.DesignMode)
{
ScriptManager sm = ScriptManager.GetCurrent(Page);
if (sm == null)
throw new HttpException("A ScriptManager control " +
"must exist on the current page.");
string cssdd = Page.ClientScript.GetWebResourceUrl(GetType(),
"ComboImg.dd.css");
string scriptJQuery = Page.ClientScript.GetWebResourceUrl(this.GetType(),
"ComboImg.jquery-1.3.2.min.js");
string scriptJQuerydd = Page.ClientScript.GetWebResourceUrl(this.GetType(),
"ComboImg.jquery.dd.js");
HtmlLink lnk = new HtmlLink();
lnk.Href = cssdd;
lnk.Attributes["rel"] = "stylesheet";
lnk.Attributes["type"] = "text/css";
HtmlGenericControl jq = new HtmlGenericControl("script");
jq.Attributes.Add("language", "JavaScript");
jq.Attributes.Add("type", "text/javascript");
jq.Attributes.Add("src", scriptJQuery);
jq.Attributes.Add("alt", "scriptJQuery");
HtmlGenericControl jqdd = new HtmlGenericControl("script");
jqdd.Attributes.Add("language", "JavaScript");
jqdd.Attributes.Add("type", "text/javascript");
jqdd.Attributes.Add("src", scriptJQuerydd);
jqdd.Attributes.Add("alt", "scriptJQuerydd");
Page.Header.Controls.Add(jq);
Page.Header.Controls.Add(jqdd);
Page.Header.Controls.Add(lnk);
if (!Page.Items.Contains("styleAlreadyPut"))
{
string urlImageArrowCombo = Page.ClientScript.GetWebResourceUrl
(this.GetType(), "ComboImg.dd_arrow.gif");
StringBuilder strStyleArrow = new StringBuilder();
strStyleArrow.Append(" .dd .ddTitle span.arrow { ")
.Append(" float:right; ")
.Append(" display:inline-block; ")
.Append(" width:16px; ")
.Append(" height:16px; ")
.Append(" cursor:pointer; ")
.Append(" background-image:url('" + urlImageArrowCombo + "');")
.Append(" } ");
HtmlGenericControl styleArrow = new HtmlGenericControl("style");
styleArrow.Attributes.Add("type", "text/css");
jqdd.Controls.Add(
new LiteralControl(strStyleArrow.ToString())
);
Page.Header.Controls.Add(styleArrow);
Page.Items.Add("styleAlreadyPut", "styleAlreadyPut");
}
}
}
I overrode the OnDataBinding
method because the base OnDataBinding
of the HtmlSelect
does a specific treatment only if DataValueField
and DataTextField
are defined. (Watch the source code of OnDataBinding
with Reflector (it's your best friend :-)).)
In our case, we don't use these properties because we don't need them and we need to create a ListItem
with the values of the bound DataTable
. The final point to see is the currentValue
property which allows to save the currently selected value before the databinding, to retrieve it afterwards.
protected override void OnDataBinding(EventArgs e)
{
IEnumerable data = base.GetData();
currentValue = Value;
if (data != null)
{
DataTable source = ((DataView)data).Table;
base.Items.Clear();
ICollection is2 = data as ICollection;
if (is2 != null)
{
this.Items.Capacity = is2.Count;
}
foreach (DataRow dr in source.Rows)
{
string value = FormatString(dr[0]);
string imgPath = FormatString(dr[1]);
string text = FormatString(dr[2]);
ListItem it = new ListItem();
it.Enabled = true;
it.Text = text;
it.Value = value;
it.Selected = (value.Equals(currentValue));
base.Items.Add(it);
}
}
Value = currentValue;
this.ViewState["_!DataBound"] = true;
this.RequiresDataBinding = false;
}
Using Reflector, I looked at the implementation of the RenderChildren
method of the HtmlSelect
class. In my case, I made two distinctions:
- Without data-binding: call the
RenderChildren
base class. - With data-binding: override the
RenderChildren
implementation to generate option tags with the correct format.
The correct format is "item text".
The conclusion is that if you want to databind this combo, you must provides a DataTable
with three columns:
- item value
- image path
- the text of the item
Here is the RenderChildren
implementation:
protected override void RenderChildren(HtmlTextWriter writer)
{
if (DataSource != null)
{
if (DataSource.GetType() == typeof(DataTable))
{
DataTable source = (DataTable)DataSource;
if (source.Rows.Count == 0)
{
base.RenderChildren(writer);
return;
}
foreach (DataRow dr in source.Rows)
{
string value = FormatString(dr[0]);
string imgPath = FormatString(dr[1]);
string text = FormatString(dr[2]);
bool selected = currentValue.Equals(value);
if (selected)
writer.Write("" + text + "");
else
{
writer.Write("" + text + "");
}
writer.WriteLine();
}
}
}
else
{
base.RenderChildren(writer);
}
}
RenderControl
adds a script which allows the initialization of the dropdownlist with the powerful jQuery framework. To initialize the dropdownlist, you must execute the following jQuery statements: $(document).ready(function() { $('#dropdownlistId').msDropDown(); }
. But, ASP.NET will encounter JavaScript errors during the loading phase if you execute only these statements.
Arnold Matusz is a jQuery ASP.NET developer, and his solution to use JQuery with ASP.NET is available here. The solution is simple: execute this script at the end of the Request AJAX event.
public override void RenderControl(HtmlTextWriter writer)
{
base.RenderControl(writer);
StringBuilder scriptInit = new StringBuilder();
scriptInit.Append(" <script type="\"text/javascript\""> ")
.Append(" Sys.WebForms.PageRequestManager.getInstance()" +
".add_endRequest(EndRequestHandler); ")
.Append(" function EndRequestHandler(sender, args) { ")
.Append(" if (args.get_error() == undefined) { ")
.Append(" setImageComBo_" + this.ID + "(); ")
.Append(" } ")
.Append(" } ")
.Append(" function setImageComBo_"+this.ID+"() { ")
.Append(" $(document).ready(function() { ")
.Append(" $('#"+this.ID +"').msDropDown(); ")
.Append(" }); ")
.Append(" } ")
.Append(" setImageComBo_" + this.ID + "(); ")
.Append("</script>");
writer.Write(scriptInit.ToString());
}
Points of Interest
I learned to encapsulate a jQuery component within ASP.NET. I invite you to try developing more jQuery ASP.NET web controls; they are very useful and beautiful...
I look forward to your suggestions.
History
- 28th September, 2009: Initial post
- 29th September, 2009: Updated source code