ListItems attributes in a DropDownList are lost on postback?
Asked Answered
P

11

75

A coworker showed me this:

He has a DropDownList and a button on a web page. Here's the code behind:

protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            ListItem item = new ListItem("1");
            item.Attributes.Add("title", "A");

            ListItem item2 = new ListItem("2");
            item2.Attributes.Add("title", "B");

            DropDownList1.Items.AddRange(new[] {item, item2});
            string s = DropDownList1.Items[0].Attributes["title"];
        }
    }

    protected void Button1_Click(object sender, EventArgs e)
    {
        DropDownList1.Visible = !DropDownList1.Visible;
    }

On the page load, the items' tooltips are showing, but on the first postback, the attributes are lost. Why is this the case, and are there any workarounds?

Pantsuit answered 21/8, 2009 at 18:8 Comment(1)
Pobably should show your .aspx code as well.Augend
H
74

I had the same problem and wanted to contribute this resource where the author created an inherited ListItem Consumer to persist attributes to ViewState. Hopefully it will save someone the time I wasted until I stumbled on it.

protected override object SaveViewState()
{
    // create object array for Item count + 1
    object[] allStates = new object[this.Items.Count + 1];

    // the +1 is to hold the base info
    object baseState = base.SaveViewState();
    allStates[0] = baseState;

    Int32 i = 1;
    // now loop through and save each Style attribute for the List
    foreach (ListItem li in this.Items)
    {
        Int32 j = 0;
        string[][] attributes = new string[li.Attributes.Count][];
        foreach (string attribute in li.Attributes.Keys)
        {
            attributes[j++] = new string[] {attribute, li.Attributes[attribute]};
        }
        allStates[i++] = attributes;
    }
    return allStates;
}

protected override void LoadViewState(object savedState)
{
    if (savedState != null)
    {
        object[] myState = (object[])savedState;

        // restore base first
        if (myState[0] != null)
            base.LoadViewState(myState[0]);

        Int32 i = 1;
        foreach (ListItem li in this.Items)
        {
            // loop through and restore each style attribute
            foreach (string[] attribute in (string[][])myState[i++])
            {
                li.Attributes[attribute[0]] = attribute[1];
            }
        }
    }
}
Hardiman answered 23/6, 2010 at 7:35 Comment(3)
why so cryptic? if this is meant to inherit from a ListItem then it doesn't workNaiad
You have to inherit a class from DropDownList and then use this, just as gleapman explained below ;)Acquainted
The solution involves creating a new control that I don't like. There is a way to do this without any subclassing.Safier
E
42

Thanks, Laramie. Just what I was looking for. It keeps the attributes perfectly.

To expand, below is a class file I created using Laramie's code to create a dropdownlist in VS2008. Create the class in the App_Code folder. After you create the class, use this line on the aspx page to register it:

<%@ Register TagPrefix="aspNewControls" Namespace="NewControls"%>

You can then put the control on your webform with this

<aspNewControls:NewDropDownList ID="ddlWhatever" runat="server">
                                                </aspNewControls:NewDropDownList>

Ok, here's the class...

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Security.Permissions;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace NewControls
{
  [DefaultProperty("Text")]
  [ToolboxData("<{0}:ServerControl1 runat=server></{0}:ServerControl1>")]
  public class NewDropDownList : DropDownList
  {
    [Bindable(true)]
    [Category("Appearance")]
    [DefaultValue("")]
    [Localizable(true)]

    protected override object SaveViewState()
    {
        // create object array for Item count + 1
        object[] allStates = new object[this.Items.Count + 1];

        // the +1 is to hold the base info
        object baseState = base.SaveViewState();
        allStates[0] = baseState;

        Int32 i = 1;
        // now loop through and save each Style attribute for the List
        foreach (ListItem li in this.Items)
        {
            Int32 j = 0;
            string[][] attributes = new string[li.Attributes.Count][];
            foreach (string attribute in li.Attributes.Keys)
            {
                attributes[j++] = new string[] { attribute, li.Attributes[attribute] };
            }
            allStates[i++] = attributes;
        }
        return allStates;
    }

    protected override void LoadViewState(object savedState)
    {
        if (savedState != null)
        {
            object[] myState = (object[])savedState;

            // restore base first
            if (myState[0] != null)
                base.LoadViewState(myState[0]);

            Int32 i = 1;
            foreach (ListItem li in this.Items)
            {
                // loop through and restore each style attribute
                foreach (string[] attribute in (string[][])myState[i++])
                {
                    li.Attributes[attribute[0]] = attribute[1];
                }
            }
        }
    }
  }
}
Ebby answered 10/5, 2011 at 21:58 Comment(5)
Could be that you'll have to add the Assembly to the Reference-Tag, even if it's in the same Assembly... I think it depends if it's a Web Application Project or a Website. This would, for a Web Application named "MyWebApplication", then read: <%@ Register Assembly="MyWebApplication" TagPrefix="aspNewControls" Namespace="NewControls"%>Acquainted
I tried your sollution, but if I use your inherrited control, it is somehow inaccessible in the code behind. I mean if I try ddlWhatever.Items it throws null exception from the ddlWhatever Any idea why?Adagietto
@david : It doesn't work if you create a UserControl and try to inherit the DropDownList.Exegetics
Worked great for me for ListBox. Now I can use custom attributes like data-data to properly render out my controls through jQuery plugins like selectize on postbackFungiform
Thanks, this answer solve the problem but are there any update for better solution?Kepler
L
14

Simple solution is to add the tooltip attributes in the pre-render event of the dropdown. Any changes to the state should be done at pre-render event.

sample code :

protected void drpBrand_PreRender(object sender, EventArgs e)
        {
            foreach (ListItem _listItem in drpBrand.Items)
            {
                _listItem.Attributes.Add("title", _listItem.Text);
            }
            drpBrand.Attributes.Add("onmouseover", "this.title=this.options[this.selectedIndex].title");
        }
Les answered 25/10, 2012 at 15:43 Comment(1)
Using a different event does not change anything in this case, the actual change here is the removal of IsPostBack and modifying the existing items. This answer does not solve the underlying issue, so if the attributes come from a data source then it must be queried again.Yan
P
8

If you only want to load the listitems on the first load of the page then you will need to enable ViewState so that the control can serialize its state there and reload it when the page posts back.

There are several places where ViewState can be enabled - check the <pages/> node in the web.config and also in the <%@ page %> directive at the top of the aspx file itself for the EnableViewState property. This setting will need to be true for ViewState to work.

If you don't want to use ViewState, simply remove the if (!IsPostBack) { ... } from around the code that adds the ListItems and the items will be recreated on each postback.

Edit: I apologize - I misread your question. You are correct that the attributes do no survive postback as they are not serialized in ViewState. You must re-add those attributes on each postback.

Paulpaula answered 21/8, 2009 at 18:12 Comment(0)
S
6

One simple solution- Call your drop down loading function on the click event where you request for post back.

Seriema answered 12/7, 2011 at 12:59 Comment(1)
Don't forget to store the dropdown.SelectedIndex before you reload the dropdown so you can restore the user's selection afterward.Ewing
R
3

Here's the VB.Net code of the solution proposed by Laramie and refined by gleapman.

Update: The code I posted below is actually for the ListBox control. Just change the inheritance to DropDownList and rename the class.

Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Security.Permissions
Imports System.Linq
Imports System.Text
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls

Namespace CustomControls

<DefaultProperty("Text")> _
<ToolboxData("<{0}:ServerControl1 runat=server></{0}:ServerControl1>")>
Public Class PersistentListBox
    Inherits ListBox

    <Bindable(True)> _
    <Category("Appearance")> _
    <DefaultValue("")> _
    <Localizable(True)> _
    Protected Overrides Function SaveViewState() As Object
        ' Create object array for Item count + 1
        Dim allStates As Object() = New Object(Me.Items.Count + 1) {}

        ' The +1 is to hold the base info
        Dim baseState As Object = MyBase.SaveViewState()
        allStates(0) = baseState

        Dim i As Int32 = 1
        ' Now loop through and save each attribute for the List
        For Each li As ListItem In Me.Items
            Dim j As Int32 = 0
            Dim attributes As String()() = New String(li.Attributes.Count - 1)() {}
            For Each attribute As String In li.Attributes.Keys
                attributes(j) = New String() {attribute, li.Attributes(attribute)}
                j += 1
            Next
            allStates(i) = attributes
            i += 1
        Next


        Return allStates
    End Function

    Protected Overrides Sub LoadViewState(savedState As Object)
        If savedState IsNot Nothing Then
            Dim myState As Object() = DirectCast(savedState, Object())

            ' Restore base first
            If myState(0) IsNot Nothing Then
                MyBase.LoadViewState(myState(0))
            End If

            Dim i As Int32 = 0
            For Each li As ListItem In Me.Items
                ' Loop through and restore each attribute 
                ' NOTE: Ignore the first item as that is the base state and is represented by a Triplet struct
                i += 1
                For Each attribute As String() In DirectCast(myState(i), String()())
                    li.Attributes(attribute(0)) = attribute(1)
                Next
            Next
        End If
    End Sub
End Class
End Namespace
Ramonitaramos answered 11/3, 2016 at 21:58 Comment(2)
used this successfully but needed to make one bug fix to get it to work right. In the two nested loops within the LoadViewState I moved the i increment to within the first loop but before the second loop and I also initialised i to 0 before the first loopJehanna
@Ramonitaramos As it is generally considered impolite here to alter someone else's code, would you like to make the correction that rdans pointed out or would you like me to do it for you?Figueroa
S
2

Typical solutions to this problem involves creating new controls that are not quite feasible in normal circumstances. There is a simple yet trivial solution to this problem.

The issue is that the ListItem loses its attributes on postback. However, the List itself never loses any custom attributes. One can take advantage of this in a simple yet effective manner thus.

Steps:

  1. Serialize your attributes using the code in the answer above (https://mcmap.net/q/268682/-listitems-attributes-in-a-dropdownlist-are-lost-on-postback)

  2. Store it to a custom attribute of the ListControl (dropdownlist, checklistbox, whatever).

  3. On post back, read back the custom attribute from the ListControl and then deserialize it back as attributes.

Here is the code I used to (de)serialize attributes (What I needed to do was to keep track of what items of the list were originally rendered as selected when retrieved from the backend and then save or delete rows as per the changes made by the user on the UI):

string[] selections = new string[Users.Items.Count];
for(int i = 0; i < Users.Items.Count; i++)
{
    selections[i] = string.Format("{0};{1}", Users.Items[i].Value, Users.Items[i].Selected);
}
Users.Attributes["data-item-previous-states"] = string.Join("|", selections);

(above, "Users" is a CheckboxList control).

On post back (in my case a Submit button Click event), I use the below code to retrieve the same and store them into a Dictionary for post processing:

Dictionary<Guid, bool> previousStates = new Dictionary<Guid, bool>();
string[] state = Users.Attributes["data-item-previous-states"].Split(new char[] {'|'}, StringSplitOptions.RemoveEmptyEntries);
foreach(string obj in state)
{
    string[] kv = obj.Split(new char[] { ';' }, StringSplitOptions.None);
    previousStates.Add(kv[0], kv[1]);
}

(PS: I have a library funcs that perform error handling and data conversions, omitting the same here for brevity).

Safier answered 6/1, 2015 at 3:53 Comment(0)
B
1

Simple solution without ViewState, creating new server control or smth complex:

Creating:

public void AddItemList(DropDownList list, string text, string value, string group = null, string type = null)
{
    var item = new ListItem(text, value);

    if (!string.IsNullOrEmpty(group))
    {
        if (string.IsNullOrEmpty(type)) type = "group";
        item.Attributes["data-" + type] = group;
    }

    list.Items.Add(item);
}

Updating:

public void ChangeItemList(DropDownList list, string eq, string group = null, string type = null)
{
    var listItem = list.Items.Cast<ListItem>().First(item => item.Value == eq);

    if (!string.IsNullOrEmpty(group))
    {
        if (string.IsNullOrEmpty(type)) type = "group";
        listItem.Attributes["data-" + type] = group;    
    }
}

Example:

protected void Page_Load(object sender, EventArgs e)
{
    if (!Page.IsPostBack)
    {
        using (var context = new WOContext())
        {
            context.Report_Types.ToList().ForEach(types => AddItemList(DropDownList1, types.Name, types.ID.ToString(), types.ReportBaseTypes.Name));
            DropDownList1.DataBind();
        }
    }
    else
    {
        using (var context = new WOContext())
        {
            context.Report_Types.ToList().ForEach(types => ChangeItemList(DropDownList1, types.ID.ToString(), types.ReportBaseTypes.Name));
        }
    }
}
Bravissimo answered 11/11, 2015 at 19:5 Comment(1)
With this solution you do requests to the database on every post-back. It's better to use ViewState.Coble
M
1

@Sujay You could add a semi-colon separated text into the dropdown's value attribute (like csv style), and use String.Split(';') to get 2 "values" out of the one value, as a workaround to get away with not having to create anew user control. Especially if you only have few extra attributes, and if it is not too long. You could also use a JSON value into the dropdown's value attribute and then parse out whatever you need from there.

Metagalaxy answered 13/4, 2017 at 8:58 Comment(0)
R
0
    //In the same block where the ddl is loaded (assuming the dataview is retrieved whether postback or not), search for the listitem and re-apply the attribute
    if(IsPostBack)
    foreach (DataRow dr in dvFacility.Table.Rows)
{                        
   //search the listitem 
   ListItem li = ddl_FacilityFilter.Items.FindByValue(dr["FACILITY_CD"].ToString());
    if (li!=null)
 {
  li.Attributes.Add("Title", dr["Facility_Description"].ToString());    
 }                  
} //end for each  
Ringster answered 12/7, 2013 at 15:0 Comment(0)
F
0

I managed to achieve that using Session Variables, in my case my list is not going to contain many elements so it works pretty well, this is how I did it:

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        string[] elems;//Array with values to add to the list
        for (int q = 0; q < elems.Length; q++)
        {
            ListItem li = new ListItem() { Value = "text", Text = "text" };
            li.Attributes["data-image"] = elems[q];
            myList.Items.Add(li);
            HttpContext.Current.Session.Add("attr" + q, elems[q]);
        }
    }
    else
    {
        for (int o = 0; o < webmenu.Items.Count; o++) 
        {
            myList.Items[o].Attributes["data-image"] = HttpContext.Current.Session["attr" + o].ToString();
        }
    }
}

When the Page is loaded first time the list is populated and I add an Image attribute which is lost after postback :( so at the time I add the elements with its attributes I create one Session variable "attr" plus the number of the element taken from the "for" cycle (it will be like attr0, attr1, attr2, etc...) and in them I save the value of the attribute (a path to an image in my case), when postback occurs (inside the "else") I just loop the list and add the attribute taken from the Session variable using the "int" of the "for" loop that is the same as when the page was loaded (this is because in this page I do not add elements to the list just selecting so they have always the same index) and the attributes are set again, I hope this helps someone in the future, greetings!

Flank answered 18/7, 2016 at 15:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.