Атрибуты ListItems в DropDownList теряются при обратной передаче?

Коллега показал мне это:

У него есть DropDownList и кнопка на веб-странице. Вот код позади:

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;
    }

При загрузке страницы отображаются всплывающие подсказки элементов, но при первой обратной передаче атрибуты теряются. Почему это так, и есть ли обходные пути?


person David Hodgson    schedule 21.08.2009    source источник
comment
Вероятно, также должен отображаться ваш код .aspx.   -  person madcolor    schedule 21.08.2009


Ответы (10)


У меня была та же проблема, и я хотел внести свой вклад этот ресурс, в котором автор создал унаследованный потребитель ListItem для сохранения атрибутов во ViewState. Надеюсь, это сэкономит кому-то время, которое я потратил впустую, пока не наткнулся на него.

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];
            }
        }
    }
}
person Laramie    schedule 23.06.2010
comment
почему так загадочно? если это предназначено для наследования от ListItem, то это не работает - person Ted; 27.01.2014
comment
Вы должны наследовать класс от DropDownList, а затем использовать его, как описано ниже gleapman;) - person Markus Szumovski; 25.03.2014
comment
Решение включает в себя создание нового элемента управления, который мне не нравится. Есть способ сделать это без каких-либо подклассов. - person ; 06.01.2015

Спасибо, Ларами. Как раз то, что я искал. Прекрасно сохраняет свои свойства.

Чтобы расширить, ниже приведен файл класса, который я создал с использованием кода Ларами для создания раскрывающегося списка в VS2008. Создайте класс в папке App_Code. После создания класса используйте эту строку на странице aspx, чтобы зарегистрировать его:

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

Затем вы можете поместить элемент управления в свою веб-форму с помощью этого

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

Ну вот и класс...

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];
                }
            }
        }
    }
  }
}
person gleapman    schedule 10.05.2011
comment
Возможно, вам придется добавить сборку в тег ссылки, даже если она находится в той же сборке... Я думаю, это зависит от того, является ли это проектом веб-приложения или веб-сайтом. Для веб-приложения с именем MyWebApplication это будет выглядеть следующим образом: ‹%@ Register Assembly=MyWebApplication TagPrefix=aspNewControls Namespace=NewControls%› - person Markus Szumovski; 25.03.2014
comment
Я пробовал ваше решение, но если я использую ваш унаследованный элемент управления, он почему-то недоступен в коде позади. Я имею в виду, что если я попытаюсь ddlWhatever.Items, он выдаст нулевое исключение из ddlWhatever Есть идеи, почему? - person m3div0; 05.11.2014
comment
@david: Это не сработает, если вы создадите UserControl и попытаетесь унаследовать DropDownList. - person Gabriel GM; 13.05.2015
comment
У меня отлично работал для ListBox. Теперь я могу использовать настраиваемые атрибуты, такие как данные-данные, для правильного отображения моих элементов управления с помощью плагинов jQuery, таких как selectize при обратной передаче. - person abney317; 20.03.2018
comment
Спасибо, этот ответ решает проблему, но есть ли какие-либо обновления для лучшего решения? - person Leo; 23.02.2020

Простое решение — добавить атрибуты всплывающей подсказки в событие pre-render раскрывающегося списка. Любые изменения состояния должны выполняться на событии pre-render.

образец кода :

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");
        }
person ram    schedule 25.10.2012

Если вы хотите загружать элементы списка только при первой загрузке страницы, вам нужно включить ViewState, чтобы элемент управления мог сериализовать свое состояние там и перезагрузить его, когда страница отправляется обратно.

Есть несколько мест, где можно включить ViewState — проверьте узел <pages/> в файле web.config, а также в директиве <%@ page %> в верхней части самого файла aspx для свойства EnableViewState. Этот параметр должен быть true для работы ViewState.

Если вы не хотите использовать ViewState, просто удалите if (!IsPostBack) { ... } вокруг кода, добавляющего ListItems, и элементы будут воссоздаваться при каждой обратной передаче.

Редактировать: приношу свои извинения - я неправильно понял ваш вопрос. Вы правы в том, что атрибуты не сохраняются после обратной передачи, поскольку они не сериализуются в ViewState. Вы должны повторно добавлять эти атрибуты при каждой обратной передаче.

person Andrew Hare    schedule 21.08.2009

Одно простое решение: вызовите функцию загрузки раскрывающегося списка в событии щелчка, когда вы запрашиваете обратную публикацию.

person Subhash    schedule 12.07.2011
comment
Не забудьте сохранить раскрывающийся список.SelectedIndex перед перезагрузкой раскрывающегося списка, чтобы впоследствии можно было восстановить выбор пользователя. - person Patrick; 12.07.2016

Типичные решения этой проблемы включают создание новых элементов управления, которые не вполне осуществимы в обычных условиях. У этой проблемы есть простое, но тривиальное решение.

Проблема в том, что ListItem теряет свои атрибуты при обратной передаче. Однако сам список никогда не теряет никаких пользовательских атрибутов. Таким образом, можно воспользоваться этим простым, но эффективным способом.

Шаги:

  1. Сериализуйте свои атрибуты, используя код в ответе выше (https://stackoverflow.com/a/3099755/3624833)

  2. Сохраните его в пользовательском атрибуте ListControl (раскрывающийся список, контрольный список и т. д.).

  3. При обратной отправке прочитайте пользовательский атрибут из ListControl, а затем десериализуйте его обратно как атрибуты.

Вот код, который я использовал для (де) сериализации атрибутов (мне нужно было отслеживать, какие элементы списка изначально отображались как выбранные при извлечении из бэкэнда, а затем сохранять или удалять строки в соответствии с изменениями, внесенными пользователь в пользовательском интерфейсе):

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);

(выше «Пользователи» — это элемент управления CheckboxList).

При отправке обратно (в моем случае событие нажатия кнопки «Отправить») я использую приведенный ниже код, чтобы получить то же самое и сохранить их в словаре для последующей обработки:

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: у меня есть библиотечные функции, которые выполняют обработку ошибок и преобразование данных, опуская то же самое здесь для краткости).

person Community    schedule 06.01.2015

Вот код решения VB.Net, предложенный Ларами и усовершенствованный gleapman.

Обновление: Код, который я разместил ниже, на самом деле предназначен для элемента управления ListBox. Просто измените наследование на DropDownList и переименуйте класс.

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
person MPaul    schedule 11.03.2016
comment
использовал это успешно, но нужно было исправить одну ошибку, чтобы заставить его работать правильно. В двух вложенных циклах в LoadViewState я переместил приращение i в первый цикл, но перед вторым циклом, и я также инициализировал i равным 0 перед первым циклом. - person rdans; 15.11.2016
comment
@MPaul Поскольку здесь обычно считается невежливым изменять чужой код, не хотите ли вы внести исправление, на которое указал rdans, или вы хотите, чтобы я сделал это за вас? - person Andrew Morton; 10.05.2018

@Sujay Вы можете добавить текст, разделенный точкой с запятой, в атрибут значения раскрывающегося списка (например, стиль csv) и использовать String.Split(';'), чтобы получить 2 «значения» из одного значения, как обходной путь, чтобы уйти без необходимости создавать новый пользовательский элемент управления. Особенно, если у вас всего несколько дополнительных атрибутов, и если он не слишком длинный. Вы также можете использовать значение JSON в атрибуте значения раскрывающегося списка, а затем проанализировать оттуда все, что вам нужно.

person APetersen786    schedule 13.04.2017

Простое решение без ViewState, создание нового серверного элемента управления или чего-то сложного:

Создание:

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);
}

Обновление:

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;    
    }
}

Пример:

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));
        }
    }
}
person Andrei Shostik    schedule 11.11.2015
comment
С помощью этого решения вы делаете запросы к базе данных при каждой обратной передаче. Лучше использовать ViewState. - person nZeus; 22.07.2016

Мне удалось добиться этого с помощью переменных сеанса, в моем случае мой список не будет содержать много элементов, поэтому он работает довольно хорошо, вот как я это сделал:

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();
        }
    }
}

Когда страница загружается в первый раз, список заполняется, и я добавляю атрибут изображения, который теряется после обратной передачи :( поэтому, когда я добавляю элементы с его атрибутами, я создаю одну переменную сеанса "attr" плюс номер взятого элемента из цикла "for" (это будет как attr0, attr1, attr2 и т.д...) и в них я сохраняю значение атрибута (путь к изображению в моем случае), когда происходит постбэк (внутри " else") я просто зацикливаю список и добавляю атрибут, взятый из переменной Session, используя "int" цикла "for", который совпадает с тем, когда страница была загружена (это потому, что на этой странице я не добавляю элементы в список просто выберите, чтобы они всегда имели один и тот же индекс) и атрибуты снова установлены, я надеюсь, что это поможет кому-то в будущем, привет!

person JCO9    schedule 18.07.2016