views:

756

answers:

3

I have a UserControl ('AddressInfoGroup') within an updatepanel that dynamically loads user controls ('AddressInfo') via the loadControl method. I have two other such controls on the same page, both of which function correctly.

When the AddressInfoGroup control renders an AddressInfo control to the page, the text boxes in AddressInfo have id's unchanged from the ascx markup. These id's should have been dynamically generated by the .NET clientID process. Because of this, when a second AddressInfo control is added to AddressInfoGroup, I get a runtime exception "An entry with the same key already exists". The two working controls produce proper clientID's and consequently don't return this error.

I should also mention that the AddressInfoGroup control has a few buttons which are correctly rendered with clientID so it seems to be an issue with the AddressInfo control itself. Any suggestions at all would be really helpful!

AddressInfoGroup markup

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="AddressInfoGroup.ascx.cs" Inherits="MFRI.Controls.Contact.AddressInfoGroup" %>

<h2>Address information</h2>
<div class="formGroup">
    <asp:PlaceHolder ID="plc_infoCtrls" runat="server" />
    <asp:Button id="btn_addAddress" CssClass="btn_add" Text="v" runat="server" OnClick="addAddress_click" UseSubmitBehavior="false" CausesValidation="false"  />
    <asp:Button id="btn_removeAddress" CssClass="btn_remove" Text="^" runat="server" OnClick="removeAddress_click" UseSubmitBehavior="false" CausesValidation="false" />
</div>

AddressInfoGroup codebehind

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using MFRI.Common.Contact;
using MFRI.Common;

namespace MFRI.Controls.Contact
{
    public partial class AddressInfoGroup : System.Web.UI.UserControl
    {
        private int _maxNumberOfControls = 3;
        private int _minNumberOfControls = 1;
        private List<Address> _controlListToBind = null;

        public int NumberOfControls
        {
            get { return SafeConvert.ToInt(ViewState["NumberOfControls"] ?? 0); }
            set { ViewState["NumberOfControls"] = value; }
        }
        public bool Editable
        {
            get { return (bool)(ViewState["InfoControlsEditable"] ?? false); }
            set { ViewState["InfoControlsEditable"] = value; }
        }

        protected void Page_Load(object sender, EventArgs e)
        {
            if (_controlListToBind != null && _controlListToBind.Count > 0)
            {
                NumberOfControls = _controlListToBind.Count;
                foreach (Address address in _controlListToBind)
                {
                    AddressInfo addressInfo = (AddressInfo)LoadControl("AddressInfo.ascx");
                    addressInfo.InitControl(Editable, address);
                    plc_infoCtrls.Controls.Add(addressInfo);
                }
            }
            else
            {
                if (NumberOfControls <= 0)
                    NumberOfControls = 1;
                for (int i = 0; i < NumberOfControls; i++)
                {
                    AddressInfo addressInfo = (AddressInfo)LoadControl("AddressInfo.ascx");
                    addressInfo.InitControl(Editable, null);
                    plc_infoCtrls.Controls.Add(addressInfo);
                }
            }
            RefreshButtons();
        }

        public void BindAddressList(List<Address> addressList)
        {
            _controlListToBind = addressList;
        }

        public List<Address> GetAddressList()
        {
            List<Address> returnList = new List<Address>();
            foreach (AddressInfo addressInfo in plc_infoCtrls.Controls.OfType<AddressInfo>())
            {
                returnList.Add(addressInfo.GetAddress());
            }
            return returnList;
        }

        private void RefreshButtons()
        {
            btn_addAddress.Enabled = false;
            btn_removeAddress.Enabled = false;
            if (Editable)
            {
                if (NumberOfControls > _minNumberOfControls)
                    btn_removeAddress.Enabled = true;
                if (NumberOfControls < _maxNumberOfControls)
                    btn_addAddress.Enabled = true;
            }
        }

        protected void addAddress_click(object sender, EventArgs e)
        {
            if (NumberOfControls < _maxNumberOfControls)
            {
                AddressInfo addressInfo = (AddressInfo)LoadControl("AddressInfo.ascx");
                addressInfo.InitControl(Editable, null);
                plc_infoCtrls.Controls.Add(addressInfo);
                NumberOfControls++;
            }
            RefreshButtons();
        }

        protected void removeAddress_click(object sender, EventArgs e)
        {
            if (_minNumberOfControls < NumberOfControls)
            {
                plc_infoCtrls.Controls.RemoveAt(plc_infoCtrls.Controls.Count - 1);
                NumberOfControls--;
            }
            RefreshButtons();
        }
    }
}

AddressInfo markup

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="AddressInfo.ascx.cs" Inherits="MFRI.Controls.Contact.AddressInfo" %>

<div>
    <asp:Label ID="lbl_line1" AssociatedControlID="txt_line1" runat="server">Line 1:</asp:Label><asp:TextBox ID="txt_line1" runat="server"></asp:TextBox>
    <asp:RequiredFieldValidator id="val_line1" runat="server" ErrorMessage="Please include a street address" ControlToValidate="txt_line1" Display="Dynamic">*</asp:RequiredFieldValidator>
</div>
<div>
    <asp:Label ID="lbl_line2" AssociatedControlID="txt_line2" runat="server">Line 2:</asp:Label><asp:TextBox ID="txt_line2" runat="server"></asp:TextBox>
</div>
<div>
    <asp:Label ID="lbl_zip" AssociatedControlID="txt_zip" runat="server">Zip code:</asp:Label><asp:TextBox ID="txt_zip" runat="server"></asp:TextBox>
    <asp:RequiredFieldValidator id="val_zip" runat="server" ErrorMessage="Please include a zip code" ControlToValidate="txt_zip" Display="Dynamic">*</asp:RequiredFieldValidator>
    <asp:RegularExpressionValidator id="regVal_zip" ControlToValidate="txt_zip" ErrorMessage="Zip code must be made up of integers in the format xxxxx" ValidationExpression="^\d{5}$" Runat="server" Display="Dynamic">*</asp:RegularExpressionValidator>
</div>
<div>
    <asp:Label ID="lbl_city" AssociatedControlID="txt_city" runat="server">City:</asp:Label><asp:TextBox ID="txt_city" runat="server" CssClass="disabled"></asp:TextBox>
</div>
<div>
    <asp:Label ID="lbl_state" AssociatedControlID="ddl_state" runat="server">State:</asp:Label><asp:DropDownList ID="ddl_state" runat="server" Enabled="false"></asp:DropDownList>
</div>
<div>
    <asp:Label ID="lbl_edit" AssociatedControlID="chk_edit" runat="server">Edit City/State:</asp:Label><asp:CheckBox ID="chk_edit" runat="server" />
</div>

AddressInfo codebehind

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using MFRI.Common.Contact;
using System.Text;
using System.Globalization;
using System.Threading;
using MFRI.Common;

namespace MFRI.Controls.Contact
{
    public partial class AddressInfo : System.Web.UI.UserControl
    {
        private bool _editable = false;
        private bool _allowStateCityEdit = false;

        protected bool AllowStateCityEdit{
            set { _allowStateCityEdit = value; }
        }

        protected ClientScriptManager clientScriptManager
        {
            get { return Page.ClientScript; }
        }

        protected void Page_Load(object sender, EventArgs e)
        {
            txt_city.Attributes.Add("readonly", "readonly");
            RegisterEditCityStateScript();
            if (_editable)
                RegisterZipCodeScript();
        }

        public void InitControl(bool editable, Address address)
        {
            InitStateDropDown();

            if (!_allowStateCityEdit)
            {
                lbl_edit.Visible = false;
                chk_edit.Visible = false;
            }

            _editable = editable;
            if (!_editable)
            {
                txt_line1.Enabled = false;
                val_line1.Enabled = false;

                txt_line2.Enabled = false;
                txt_city.Enabled = false;

                txt_zip.Enabled = false;
                val_zip.Enabled = false;
                regVal_zip.Enabled = false;

                ddl_state.Enabled = false;
                chk_edit.Enabled = false;
            }
            else
            {
                txt_zip.Attributes.Add("onblur", "GetCityState('" + txt_city.ClientID + "','" + ddl_state.ClientID + "','" + txt_zip.ClientID + "');");
                chk_edit.Attributes.Add("onClick", "ToggleCityState('" + txt_city.ClientID + "','" + ddl_state.ClientID + "','" + txt_zip.ClientID + "')");
            }

            if (address != null)
            {
                txt_line1.Text = address.Line1;
                txt_line2.Text = address.Line2;
                txt_city.Text = address.City;
                txt_zip.Text = SafeConvert.ToString(address.Zip);
                ddl_state.SelectedValue = SafeConvert.ToString((int)address.State);
            }
        }

        private void RegisterZipCodeScript(){
            if (!clientScriptManager.IsClientScriptBlockRegistered(this.GetType(), "zipCodeScript"))
            {
                StringBuilder script = new StringBuilder();
                script.Append("function GetCityState(txtCityId,ddlStateId,txtZipId) {\n");
                script.Append("  var textZip = document.getElementById(txtZipId);\n");
                script.Append("  var zipCode = parseFloat(textZip.value);\n");
                script.Append("  if(isNaN(zipCode)){\n");
                script.Append("    var txtCity = document.getElementById(txtCityId);\n");
                script.Append("    var ddlState = document.getElementById(ddlStateId);\n");
                script.Append("    txtCity.value = '';\n");
                script.Append("    ddlState.selectedIndex = 0;\n");
                script.Append("  }\n");
                script.Append("  else{\n");
                script.Append("    MFRI.Controls.Contact.ZipCodeService.GetCityState(zipCode, txtCityId, ddlStateId, SucceededCallback);\n");
                script.Append("  }\n");
                script.Append("}\n");
                script.Append("function SucceededCallback(result) {\n");
                script.Append("  var txtCity = document.getElementById(result[0]);\n");
                script.Append("  txtCity.value = result[2];\n");
                script.Append("  var ddlState = document.getElementById(result[1]);\n");
                script.Append("  var stateId = result[3];\n");
                script.Append("  for(i=0; i<ddlState.options.length; i++){\n");
                script.Append("    if(ddlState.options[i].value == stateId){\n");
                script.Append("      ddlState.selectedIndex = i;\n");
                script.Append("    }\n");
                script.Append("  }\n");
                script.Append("}\n");
                clientScriptManager.RegisterClientScriptBlock(this.GetType(), "zipCodeScript", script.ToString(), true);
            }
        }

        private void RegisterEditCityStateScript()
        {
            if (!clientScriptManager.IsClientScriptBlockRegistered(this.GetType(), "editCityState"))
            {
                StringBuilder script = new StringBuilder();
                script.Append("function ToggleCityState(txtCityId, ddlStateId, txtZipId) {\n");
                script.Append("  var txtCity = document.getElementById(txtCityId);\n");
                script.Append("  var ddlState = document.getElementById(ddlStateId);\n");
                script.Append("  if(ddlState.disabled == true){\n");
                script.Append("    txtCity.removeAttribute('readonly');\n");
                script.Append("    ddlState.disabled = false;\n");
                script.Append("  }\n");
                script.Append("  else{\n");
                script.Append("    txtCity.setAttribute('readonly', 'readonly');\n");
                script.Append("    ddlState.disabled = true;\n");
                script.Append("    GetCityState(txtCityId,ddlStateId,txtZipId); \n");
                script.Append("  }\n");
                script.Append("}\n");
                clientScriptManager.RegisterClientScriptBlock(this.GetType(), "editCityState", script.ToString(), true);
            }
        }

        private void InitStateDropDown(){
            CultureInfo cultureInfo = Thread.CurrentThread.CurrentCulture;
            TextInfo textInfo = cultureInfo.TextInfo;

            ddl_state.Items.Add(new ListItem("Select a state","-1"));
            foreach (StateType state in Enum.GetValues(typeof(StateType))) 
            {
                string displayName = state.ToString().ToLower();
                displayName = displayName.Replace('_', ' ');
                ddl_state.Items.Add(new ListItem(textInfo.ToTitleCase(displayName), state.GetHashCode().ToString()));
            }
        }

        public Address GetAddress()
        {
            Address address = new Address();
            address.Line1 = txt_line1.Text;
            address.Line2 = txt_line2.Text;
            address.State = (StateType)Enum.ToObject(typeof(StateType), SafeConvert.ToInt(ddl_state.SelectedValue));
            address.City = txt_city.Text;
            address.Zip = SafeConvert.ToInt(txt_zip.Text);
            return address;
        }
    }
}
A: 

Have you thought about using a ListView control to dynamically create and display your Address controls on the fly? You would have one instance of the control hard-coded on the page and then in your ListView's ItemTemplate you would put in your Address control and databind it to the data in your ListView's DataSource property.

Payton Byrd
ListView could possibly be used instead of the solution I have created, but that doesn't really address the problem I am having. My user control groups function correctly, one of them is just not producing client id's.
caltrop
A: 

Have you tried explicitly setting the ID value of the dynamically added AddressInfo controls? For instance, in the Page_Load of the AddressInfoGroup:

protected void Page_Load(object sender, EventArgs e)
{
    if (_controlListToBind != null && _controlListToBind.Count > 0)
    {
        NumberOfControls = _controlListToBind.Count;
        int index = 0;
        foreach (Address address in _controlListToBind)
        {
            AddressInfo addressInfo = (AddressInfo)LoadControl("AddressInfo.ascx");
            addressInfo.ID = String.Format("AddressInfo{0}", index++);
            addressInfo.InitControl(Editable, address);
            plc_infoCtrls.Controls.Add(addressInfo);
        }
    }
    else
    {
        if (NumberOfControls <= 0)
            NumberOfControls = 1;
        for (int i = 0; i < NumberOfControls; i++)
        {
            AddressInfo addressInfo = (AddressInfo)LoadControl("AddressInfo.ascx");
            addressInfo.ID = String.Format("AddressInfo{0}", i);
            addressInfo.InitControl(Editable, null);
            plc_infoCtrls.Controls.Add(addressInfo);
        }
    }
    RefreshButtons();
}
300 baud
+1  A: 

I figured it out finally.

Each addressInfo Control is created on the fly by addressInfoGroup. After instantiation, addressInfoGroup calls the 'initControl' method of the newly created addressInfo control. Within that init function I retrieve a few clientID values:

txt_zip.Attributes.Add("onblur", "GetCityState('" + txt_city.ClientID + "','" + ddl_state.ClientID + "','" + txt_zip.ClientID + "');");
chk_edit.Attributes.Add("onClick", "ToggleCityState('" + txt_city.ClientID + "','" + ddl_state.ClientID + "','" + txt_zip.ClientID + "')");

Because of my reference to the clientID's, all of the controls within addressInfo retain the markup Id's. If I comment those lines out, .NET generates the proper Id's. This has something to do with WHEN these lines are called. When I moved the lines into addressInfo's postback, everything worked as expected.

So end result: If a parent control tells a child control to reference a clientID, all of the child controls clientID's seem to malfunction.

caltrop