2009-08-21 16 views
61

Un collega mi ha mostrato questo:Gli attributi ListItems in una DropDownList vanno persi nel postback?

Ha una DropDownList e un pulsante su una pagina web. Ecco il codice dietro:

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

Al caricamento della pagina, i suggerimenti le voci stanno mostrando, ma al primo postback, gli attributi vengono persi. Perché è così e ci sono soluzioni alternative?

+0

Dovresti mostrare anche il tuo codice .aspx. – madcolor

risposta

66

Ho avuto lo stesso problema e volevo contribuire con la risorsa this in cui l'autore ha creato un Consumer ListItem ereditato per mantenere gli attributi su ViewState. Spero che salvi qualcuno quando ho perso fino a quando non sono inciampato su di esso.

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]; 
      } 
     } 
    } 
} 
+8

Come posso usare questo codice? –

+0

perché così criptico? se questo è inteso per ereditare da un ListItem allora non funziona – Ted

+2

Devi ereditare una classe da DropDownList e quindi usarla, proprio come spiegato da gleapman di seguito;) –

8

Se si desidera caricare solo i listitems sul primo caricamento della pagina, è necessario abilitare ViewState in modo che il controllo possa serializzarne lo stato e ricaricarlo quando la pagina viene reinserita.

Non ci sono molti posti dove ViewState può essere abilitato - controllare il nodo <pages/> nel web.config ed anche nella direttiva <%@ page %> nella parte superiore del file stesso aspx per la proprietà EnableViewState. Questa impostazione dovrà essere true affinché ViewState funzioni.

Se non si desidera utilizzare ViewState, rimuovere semplicemente if (!IsPostBack) { ... } dal codice che aggiunge ListItems e gli elementi verranno ricreati su ogni postback.

Modifica: Mi scuso - Ho letto male la domanda. Hai ragione nel ritenere che gli attributi non sopravvivano al postback in quanto non sono serializzati in ViewState. È necessario aggiungere nuovamente tali attributi su ciascun postback.

+0

Non so perché, ma non funziona anche con viewstate! – TheVillageIdiot

31

Grazie, Laramie. Proprio quello che stavo cercando. Mantiene perfettamente gli attributi.

Per espandere, di seguito è un file di classe che ho creato utilizzando il codice di Laramie per creare un elenco a discesa in VS2008. Creare la classe nella cartella App_Code. Dopo aver creato la classe, utilizzare questa linea sulla pagina aspx per registrarlo:

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

È possibile poi mettere il controllo sul vostro WebForm con questo

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

Ok, ecco la classe di ...

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]; 
       } 
      } 
     } 
    } 
    } 
} 
+0

Potrebbe essere che dovrai aggiungere l'Assembly al Tag di Riferimento, anche se è nello stesso Assembly ... Penso che dipenda se si tratta di un Progetto di Applicazione Web o di un Sito Web. Ciò, per un'applicazione Web denominata "MyWebApplication", leggerà: <% @ Register Assembly = "MyWebApplication" TagPrefix = "aspNewControls" Namespace = "NewControls"%> –

+1

Ho provato la tua sollution, ma se utilizzo il controllo ereditato , è in qualche modo inaccessibile nel codice sottostante. Voglio dire, se provo 'ddlWhatever.Items' getta un'eccezione null da' ddlWhatever' Qualche idea perch? – david

+0

@david: non funziona se si crea un 'UserControl' e si prova ad ereditare' DropDownList'. –

6

Una soluzione semplice: chiama la funzione di caricamento a discesa sull'evento click in cui richiedi il postback.

+0

Non dimenticare di memorizzare dropdown.SelectedIndex prima di ricaricare il menu a discesa in modo da poter ripristinare la selezione dell'utente in seguito. – Patrick

8

La soluzione semplice consiste nell'aggiungere gli attributi tooltip nell'evento pre-render dell'elenco a discesa. Eventuali modifiche allo stato devono essere eseguite all'evento pre-render.

codice di esempio:

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

soluzioni tipiche a questo problema consiste nel creare nuovi controlli che non sono abbastanza fattibile in circostanze normali.C'è una soluzione semplice ma banale a questo problema.

Il problema è che il ListItem perde i suoi attributi sul postback. Tuttavia, l'Elenco stesso non perde mai attributi personalizzati. Uno può approfittare di questo in un modo semplice ma efficace così.

Passi:

  1. serializzare i tuoi attributi utilizzando il codice nella risposta precedente (https://stackoverflow.com/a/3099755/3624833)

  2. Conservarlo per un attributo personalizzato della ListControl (DropDownList, CheckListBox, a prescindere).

  3. Sul retro del post, leggere l'attributo personalizzato da ListControl e quindi deserializzare come attributi.

Ecco il codice che ho usato per (de) serializzare attributi (cosa che dovevo fare era quello di tenere traccia di quali elementi della lista sono stati originariamente reso come selezionati quando viene recuperato dal backend e poi salvare o cancellare righe come per le modifiche apportate dall'utente sull'interfaccia utente):

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

(sopra, "utenti" è un controllo CheckboxList).

Su palo (nel mio caso un pulsante Invia evento Click), utilizzare il codice sottostante per recuperare lo stesso e memorizzarli in un dizionario per la post-elaborazione:

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: ho un funzioni di libreria che eseguono la gestione degli errori e le conversioni dei dati, omettendo lo stesso qui per brevità).

0

soluzione semplice senza ViewState, creando nuovo controllo server o complesso dovrebbe occupare:

Creazione:

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

Aggiornamento:

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

Esempio:

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

Con questa soluzione si fanno richieste al database su ogni post-back. È meglio usare ViewState. – nZeus

1

Qui' s il codice VB.Net della soluzione proposta da Laramie e perfezionata da gleapman.

Aggiornamento: Il codice che ho postato di seguito è in realtà per il controllo ListBox. Basta modificare l'ereditarietà in DropDownList e rinominare la classe.

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 = 1 
      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 
       For Each attribute As String() In DirectCast(myState(i), String()()) 
        li.Attributes(attribute(0)) = attribute(1) 
        i += 1 
       Next 
      Next 
     End If 
    End Sub 
End Class 
End Namespace 
+0

usato con successo ma necessario per fare una correzione di bug per farlo funzionare correttamente. Nei due cicli annidati all'interno di LoadViewState ho spostato l'incremento i all'interno del primo ciclo ma prima del secondo ciclo e ho anche inizializzato da I a 0 prima del primo ciclo – rdans

0

sono riuscito a ottenere che l'utilizzo di variabili di sessione, nel mio caso la mia lista non sta per contenere molti elementi in modo che funziona abbastanza bene, questo è come ho fatto:

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

Quando il La pagina viene caricata la prima volta che viene compilata la lista e aggiungo un attributo Image che viene perso dopo il postback :(così nel momento in cui aggiungo gli elementi con i relativi attributi creo una variabile Session "attr" più il numero dell'elemento preso dal "for" cycle (sarà come attr0, attr1, attr2, ecc ...) e in essi salverò il valore dell'attributo (un percorso per un'immagine nel mio caso), quando si verifica il postback (all'interno del "else") faccio semplicemente un loop dell'elenco e aggiungo l'attributo preso dalla variabile Session usando il comando "int" "del ciclo" for "che è lo stesso di quando è stata caricata la pagina (questo perché in questa pagina non aggiungo elementi all'elenco solo selezionando in modo da avere sempre lo stesso indice) e gli attributi sono impostati di nuovo, I spero che questo aiuti qualcuno in futuro, saluti!

1

@Sujay È possibile aggiungere un testo separato da punto e virgola all'attributo del valore a discesa (come stile csv) e utilizzare String.Split (';') per ottenere 2 "valori" da un valore, come una soluzione alternativa per farla franca senza dover creare un nuovo controllo utente. Soprattutto se hai solo pochi attributi in più e se non è troppo lungo. Puoi anche utilizzare un valore JSON nell'attributo del valore del menu a discesa e quindi analizzare tutto ciò che ti serve da lì.