Friday, June 13, 2008

Databinding the value property of an AJAX Autocomplete Extender in a ListView - .NET 3.5, C#

OK. New problem. Suffice it to say I like to use databinding if at all possible, but if you want to do anything tricky you need to be creative. The problem I've run across this time is this. I've got a ListView that is using an AJAX Autocomplete Extender in the Insert template field. The extender is set to update the text of a textbox, but what if I want to use the value element of the extender too? In my example I want users to be able to type in an employee name, and then when they select the employee name they are looking for, the employee ID gets stored to the DB too. Here's what the template field looks like:

<InsertItemTemplate>
<ajax:AutoCompleteExtender ID="aceEmployeeName" runat="server" TargetControlID="txtEmployeeName" ServiceMethod="GetCompletionList"
ServicePath="Default.aspx" FirstRowSelected="true" OnClientItemSelected ="EmployeeNameSelected" CompletionListCssClass="autocomplete_completionListElement"
MinimumPrefixLength="4" CompletionInterval="1000" EnableCaching="true" CompletionSetCount="12">
</ajax:AutoCompleteExtender>
Employee Name:
<asp:TextBox ID="txtEmployeeName" runat="server" Text='<%# Bind("EmpName") %>' />
<asp:HiddenField ID="hfEmpID" runat="server" OnPreRender="hfEmpID_PreRender" Value='<%# Bind("EmpID") %>'/>
</InsertItemTemplate>

Here's the ServiceMethod in the code behind that feeds the autocomplete extender (obviously this is rather hard-coded for now):

[WebMethod]
[System.Web.Script.Services.ScriptMethod]
//used by the ajax auto complete control
public static string[] GetCompletionList(string prefixText, int count)
{
List items = new List(3);
items.Add(AjaxControlToolkit.AutoCompleteExtender.
CreateAutoCompleteItem("Name1234", "1234"));
items.Add(AjaxControlToolkit.AutoCompleteExtender.
CreateAutoCompleteItem("Name2345", "2345"));
items.Add(AjaxControlToolkit.AutoCompleteExtender.
CreateAutoCompleteItem("Name3456", "3456"));

return items.ToArray();
}

But, how do I get the selected value of the autocomplete extender stored into the databound hidden field? It's tricky, but it doesn't take a lot of code. I'm going to use client side javascript. First of all I need to create a hidden field outside of the ListView to store the ID of the hidden field within the ListView. The reason I need to do this is because I have no idea what the ID is going to be at run time since I can have multiple rows in the listview, and the server names them dynamically at run time.

<asp:HiddenField ID="hfInsertHiddenFieldID" runat="server" />

Then I tell the hidden field within the list view to store its name there on PreRender:

<asp:HiddenField ID="hfEmpID" runat="server" OnPreRender="hfEmpID_PreRender" Value='<%# Bind("EmpID") %>'/>

Then, the codebehind for this event looks like this:

protected void hfEmpID_PreRender(object sender, EventArgs e)
{
//store the name of the databound hidden field on the client side
HiddenField hf = sender as HiddenField;
string strHiddenFieldClientID = hf.ClientID;
hfInsertHiddenFieldID.Value = strHiddenFieldClientID;
}

OK, so far so good. Now all that's left to do is to tell the autocomplete extender to run a piece of javascript on the client side when a new name is selected (you may have already noticed this in the first snippet above).

OnClientItemSelected ="EmployeeNameSelected"

Now, in the javascript I can find out the name of the hiddenfield in the ListView that is bound to the EmployeeID and change its value based on the selected value of the autocomplete extender:

function EmployeeNameSelected(source, eventArgs)
{
//find out the name of the databound hidden field and then store the selected value from the autocomplete extender
var hf = document.getElementById("ctl00_body_hfInsertHiddenFieldID");
var strHiddenFieldID = hf.getAttribute("value");
var targetElement = document.getElementById(strHiddenFieldID);
targetElement.value = eventArgs.get_value();
}

That's it! (I think) Simple? Not really, but now I can keep the page databound and my project still has 0 stored procedures :)

Thursday, February 7, 2008

OutOfRangeAwareDropDownList - .NET 2.0, C#

Another big problem I've come across (and I know I'm not the only one) has to do with using databinding on DropDownLists.

There is a common issue that crops up. Say the items in the listbox get out of sync with records in the backend datastore. For example the value "foo" is in a data record, but "foo" doesn't exist in the DropDownList. When you pull up the record using databinding (SelectedValue='<%# Bind("FooColumn") %>') you'll get an error like "'lstBadFooList' has a SelectedValue which is invalid because it does not exist in the list of items. Parameter name: value".

I've come up with a solution for this! It's a custom control that I've named, appropriately enough, OutOfRangeAwareDropDownList. What it does is check to see if it's throwing this error, and if it is, it just decides to bind to nothing. Not much to it, but it saves a lot of headaches.

Here's the code:

using System;
using System.Collections;
using System.Web.UI.WebControls;

//this dropdown control overrides the databinding event
//it is used to overcome an issue where an element originally put into the record
//no longer exists in the dropdown list

namespace kevnls.controls
{
public class OutOfRangeAwareDropDownList : DropDownList
{
protected override void PerformDataBinding(IEnumerable dataSource)
{
try
{
base.PerformDataBinding(dataSource);
}
catch (ArgumentOutOfRangeException)
{
base.SelectedIndex = -1;
}
}
}
}

All you have to do is compile this into a .dll and then use it in your project. The absolute easiest way to use it in Visual Studio 2005 is to right-click in your toolbox and select "Choose Items..." and find the .dll, and a new control will show up in your toolbox. Then you can just drag it into your .aspx page like any other control and Visual Studio will take care of registering the assembly and importing the reference. Simple as that.

Get unbound DropDownList value in GridView - .NET 2.0, C#

I've been looking for a solution to a problem when using one dropdownlist within a gridview to populate another dropdownlist.

The issue is that the databinding is broken on the second dropdownlist once you do this, so you can't use SelectedValue='<%# Bind("....") %>' or else you'll get an error when you go to do an update to your database. ("Databinding methods such as Eval(), XPath(), and Bind() can only be used in the context of a databound control. ").

The solution I've come up with is to create a hidden field that can hold the value of the selection from the second dropdownlist, then use a client-side javascript to find and store the value from this dropdownlist into the hidden field. The control is server-side so when I do my database update I can simply find the hiddenfield.value.

I've attached the relevant code below. The only trick is to do a little server-side/client-side tie by passing the javascript the ClientIDs of the controls that are involved (since they'll be named dynamically).

Here's the relevant code snippets:

***javascript***

<script type="text/javascript">
function jobTitleToHiddenField(strListClientID, strHiddenFieldClientID)
{
var strJobTitle = document.getElementById(strListClientID).value;
var hiddenField = document.getElementById(strHiddenFieldClientID);
hiddenField.value = strJobTitle;
}
</script>

***hidden textbox***

<asp:HiddenField ID="hfJobTitle" runat="server" />

***dropdown in template field***

<EditItemTemplate>
<asp:DropDownList ID="lstJobTitles" runat="server" DataSourceID="dsJobTitles"
DataTextField="Title" DataValueField="Title" OnPreRender="lstJobTitles_PreRender" ></asp:DropDownList>
</EditItemTemplate>

***code behind***

protected void lstJobTitles_PreRender(object sender, EventArgs e)
{
DropDownList ddlist = sender as DropDownList;
string strListClientID = ddlist.ClientID;
string strHiddenFieldClientID = hfJobTitle.ClientID;
ddlist.Attributes.Add("onchange", "jobTitleToHiddenField(’" + strListClientID + "’, ‘" + strHiddenFieldClientID + "’);");
}

then you can get the value in the code behind anywhere you need it with…

string strTitle = hfJobTitle.Value;