Recently I’ve found an interesting bug in the LookupField (Microsoft.SharePoint.WebControls.LookupField) from SharePoint 2007. LookupField doesn’t save selected value after an “idle postback”. By “idle postback” I mean any postback, which doesn’t lead to an item saving. For example, you have changed a list item and click on Save-button, but some validation fails and the page is just reloaded with an appropriate error message.
Below are depicted some steps to reproduce the bug.
This bug reveals itself only when the amount of items in the lookup-list is more than 20. It’s related with the fact, that LookupField renders itself as usual DropDownList if items amount <= 20, and as TextBox with the dynamically appeared Html-select when the items amount > 20. We can see this difference in the following piece of CreateChildControls() from Reflector:
protected override void CreateChildControls()
{
// some code skipped
this.Controls.Clear();
if (((this.DataSource != null) &&
(((this.DataSource.Count > 20) && !base.InDesign) && SPUtility.IsIE55Up(this.Page.Request))) &&
!SPUtility.IsAccessibilityMode(this.Page.Request))
{
// rendering as TextBox
this.m_tbx = new TextBox();
this.m_tbx.Attributes.Add("choices", this.Choices);
this.m_tbx.Attributes.Add("match", "");
this.m_tbx.Attributes.Add("onkeydown", "HandleKey()");
this.m_tbx.Attributes.Add("onkeypress", "HandleChar()");
this.m_tbx.Attributes.Add("onfocusout", "HandleLoseFocus()");
this.m_tbx.Attributes.Add("onchange", "HandleChange()");
this.m_tbx.Attributes.Add("class", "ms-lookuptypeintextbox");
this.m_tbx.Attributes.Add("title", field.Title);
this.m_tbx.TabIndex = this.TabIndex;
this.m_tbx.Attributes["optHid"] = this.HiddenFieldName;
Literal child = new Literal();
child.Text = "<span style=\"vertical-align:middle\">";
Literal literal2 = new Literal();
literal2.Text = "</span>";
this.Controls.Add(child);
this.Controls.Add(this.m_tbx);
this.m_tbx.Attributes.Add("opt", "_Select");
this.m_dropImage = new Image();
this.m_dropImage.ImageUrl = "/_layouts/images/dropdown.gif";
this.m_dropImage.Attributes.Add("alt", SPResource.GetString("LookupWordWheelDropdownAlt", new object[0]));
this.m_dropImage.Attributes.Add("style", "vertical-align:middle;");
this.Controls.Add(this.m_dropImage);
this.Controls.Add(literal2);
}
else
{
// rendering as DropDownList
this.m_dropList = new DropDownList();
this.m_dropList.ID = "Lookup";
this.m_dropList.TabIndex = this.TabIndex;
this.m_dropList.DataSource = this.DataSource;
this.m_dropList.DataValueField = "ValueField";
this.m_dropList.DataTextField = "TextField";
this.m_dropList.ToolTip = SPHttpUtility.NoEncode(field.Title);
this.m_dropList.DataBind();
this.Controls.Add(this.m_dropList);
}
// some code skipped
this.SetFieldControlValue(this.ItemFieldValue);
}
Another interesting point is a method SetFieldControlValue (in above shown code it’s invoked at the end of CreateChildControls()). SetFieldControlValue registers a hidden html-field that contains the identifier of the option selected by user. When Sharepoint save all changes to SPListItem, it uses the identifier from this hidden field (to be more exact, SharePoint deals with property Value, which, in turn, gets value from the hidden field). That is why it’s very important to have the right value in the hidden field. Let’s take a look at SetFieldControlValue:
private void SetFieldControlValue(object value)
{
if ((this.m_value != value) || !this.m_hasValueSet)
{
this.Clear();
this.m_value = value;
this.m_hasValueSet = true;
if (this.DataSource != null) // here m_selectedValueIndex will be initialized with the index of the option currently stored in SPListItem
{
// some code skipped
if (this.m_tbx != null)
{
DataRowView view = null;
if (this.m_selectedValueIndex >= 0)
{
view = this.m_dataSource[this.m_selectedValueIndex];
this.m_tbx.Text = view["TextField"] as string;
}
if (this.Page != null)
{
string str = "0";
if (this.m_selectedValueIndex < 0) // (***) here is the problem
{
if (this.Page.IsPostBack)
{
// extract the option picked by user
str = this.Context.Request.Form[this.HiddenFieldName];
if (string.IsNullOrEmpty(str))
{
str = "0";
}
}
}
else
{
// extract the option stored in SPListItem, because m_selectedValueIndex still contains the old value
view = this.m_dataSource[this.m_selectedValueIndex];
str = ((int)view["ValueField"]).ToString(CultureInfo.InvariantCulture);
}
// register the hidden field with, in some cases, wrong value
this.Page.ClientScript.RegisterHiddenField(this.HiddenFieldName, str);
}
}
}
}
}
Let’s examine this.m_selectedValueIndex. DataSource contains available options to choose. In turn, m_selectedValueIndex contains the index of the option currently stored in SPListItem, the index inside DataSource. Note that m_selectedValueIndex doesn’t by no means reflect the option picked by user on the page, but it reflects the option currently stored in SPListItem.
I marked with (***) the code line where we face the problem. While postback, SetFieldControlValue doesn’t extract from hidden html-field the option picked by user (Context.Request.Form[this.HiddenFieldName]), if some valid option has been already stored in SPListItem before (i.e. if this.m_selectedValueIndex >= 0). In other words, LookupField ignores the option selected by user and populates the next hidden html-field with old value. As the result, during the next successful postback, the old option will be again stored in SPListItem.
Now how to fix this bug. I’ve implemented a descendant of LookupField, which allows to avoid above described problem.
public class FixedLookupField : LookupField
{
protected object _selectedValue = null;
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
// preserve picked option (property Value gets the selected option from hidden html-field)
if (Page.IsPostBack)
_selectedValue = Value;
}
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
try
{
if (Page.IsPostBack && _selectedValue != null && IsTextBox())
{
// register a javascript, which overrides value contained in hidden html-field with the right one
string hiddenFieldName = GetHiddenFieldName();
string startupScript = string.Format("document.getElementById('{0}').value = {1};", hiddenFieldName, _selectedValue.ToString());
string startupScriptKey = "FixedLookupField_" + hiddenFieldName;
if (!Page.ClientScript.IsStartupScriptRegistered(startupScriptKey))
Page.ClientScript.RegisterStartupScript(this.GetType(), startupScriptKey, startupScript, true);
}
}
catch (Exception ex)
{
}
}
// allows to detect what way of rendering we have (DropDownList or TextBox with javascript tricks)
protected bool IsTextBox()
{
Type baseType = this.GetType().BaseType;
FieldInfo fldInfo = baseType.GetField("m_tbx", BindingFlags.Instance | BindingFlags.NonPublic);
object tb = fldInfo.GetValue(this);
return tb != null;
}
// returns the ID of hidden html-field how it will be on the page
protected string GetHiddenFieldName()
{
Type baseType = this.GetType().BaseType;
PropertyInfo propInfo = baseType.GetProperty("HiddenFieldName", BindingFlags.Instance | BindingFlags.NonPublic);
return (string)propInfo.GetValue(this, null);
}
}
Related posts: