2007-10-09

Event som inte höjs för dynamiskt skapa kontroller

En kollega hade problem med att ett Click-event för en dynamiskt skapad kontroll inte höjdes vid vissa postbacks. Oftast brukar problem med att event inte höjs för en dynamiskt skapad kontroll hänga ihop med att dess ID ändras mellan postbacks. Så även denna gång.

Sidan och problemet

Förenklat sett, består sidan av en lista med saker och en dropdownlist som används som val för att filtrera listan. Initialt visar listan upp alla saker. I slutet på varje rad i listan finns en knapp (en ImageButton). Saker ska hända när man klickar på knappen genom en eventhanterare på knappens Click-event,

När man visar sidan för första gången visar listan alla saker och ett klick på en av knapparna leder till att eventet höjs.

När man väljer något i dropdownlisten så autopostbackar sidan och listan populeras om. Klickar man nu på någon knapp görs en postback men eventet höjs aldrig. Sidan returneras. Ett klick på en knapp nu fungerar; eventet höjs.

Så direkt efter att man gjort ett val slutar eventet att höjas, för att vi nästa försök fungera igen.

Hur listan byggs upp

Listan byggs upp i en metod MyCreateControls() som anropas i Page_Load. I metoden hämtas en lista med saker att visa. För varje sak i listan laddas en UserControl, lite värden sätts på UserControl-instansen och den läggs till en PlaceHolders ControlCollection via propertyn Controls.

Pseudokod för MyCreateControls

MyCreateControls()
PlaceHolder1.Controls.Clear()
DataList=GetData()
ForEach(item in DataList)
{
ctrl=MyCreateControl()
ctrl.data=item.data
PlaceHolder1.Controls.Add(ctrl)
}



När man väljer ett item i dropdownlistan anropas MyCreateControls. PlaceHoldern rensas på alla kontroller med Controls.Clear() (PostBackEvents höjs efter OnLoad som redan hunnit fylla på listan enligt de gamla kriterierna) och därefter fylls PlaceHoldern på som beroende på valet i dropdownlistan kommer att använda en annan lista av saker.


UniqueID


Varje kontroll, och då menar jag Control, har en property UniqueID med ett id som är unikt för just den kontrollen. Detta id består av två delar: den första delen är UniqueID för den NamingContainer som kontrollen ligger i; den andra delen är kontrollens ID. Så för en kontroll med ID="MyChildControl" som ligger i en NamingContainer-kontroll med UniqueID="MyParent" är UniqueId="MyParent:MyChildControl". Separatorn kan också vara "$".


Om kontrollen saknar id används en löpnummerserie i stil med "ctl00", "ctl01" och vi får då UniqueId="MyParent:ctl00". Skapandet av ID om kontrollen saknar ett görs i Control.GenerateAutomaticID() som, för varje gång den behöver skapa ett unikt id, räknar upp ett värde på kontrollens NamingControl. Så lägger vi till tre kontroller som saknar ID till en NamingControl kommer här värdet att räknas upp från 0 till 1 till 2 till 3 (den sista kontrollen får ID="ctl02"). Detta värde nollställs eller räknas aldrig ner så tar vi sen bort dessa tre kontroller och lägger dit en ny, som också saknar ID kommer den att få ID="ctl03" och värdet har räknats upp till 4.


När vi ändå håller på: ClientId är UniqueId med "_" som separator istället för ":".  Så vi får då: "MyParent_MyChildControl"


UniqueID och PostBacks


När en kontroll, t.ex. en ImageButton (som skrivs ut som en <input type="image">-HTML-kontroll) postar tillbaka värden görs det under dess UniqueID. Så om man klickar på en ImageButton som har UniqueID="MyParent:ImageButton1" kommer den att skicka tillbaka den x- och y-koordinat man klickade på under "MyParent:ImageButton1.x" och "MyParent:ImageButton1.y". Det här gör att Asp.Net vet till vilken kontroll varje postat värde hör till.


Problemet denna gång


Tillbaka till det faktiska problement. UserControlen som lades i PlaceHoldern fick inget ID satt. Det gjorde att varje UserControl fick UniqueID i stil med: "xx:PlaceHolder1:ctl00", "xx:PlaceHolder1:ctl01" osv.


Antag att vi via Page_Load lagt till tio kontroller (den sista kontrollen får ID="ctl09"). Om man valt ett nytt värde i dropdownen kommer MyCreateControls() att anropas igen och dessa tio kontroller tas då bort ur Controls. Istället kommer till exempel tre nya kontroller att läggas till. Den första nya kontrollen blir dock inte "ctl00" utan "ctl10", eftersom det är så Control.GenerateAutomaticID fungerar: den fortsätter på löpnummerserien. Så vi har då de tre kontrollerna "ctl10", "ctl11" och "ctl12".


Vad händer då när man klickar på den första ImageButton-kontrollen som ligger i den första UserControlen? Den har UniqueID="xx:PlaceHolder1:ctl10:ImageButton1", så x- och y-koordinaten skickas i "xx:PlaceHolder1:ctl10:ImageButton1.x" och "xx:PlaceHolder1:ctl10:ImageButton1.y".


Men Page_Load som bygger upp de tre kontrollerna på nytt kommer denna gång att skapa "ctl00", "ctl01" och "ctl02" (eftersom räknaren alltid börjar på noll och det är de första kontrollerna vi lägger till). Gissa vad som händer då med våra x- och y-värden.


Absolut ingenting. Det finns ingen ImageButton1 som ligger i en kontroll med UniqueID="xx:PlaceHolder1:ctl10". Och eftersom inga x- och y-värden ändras på någon kontroll kommer inte någon ImageButton att höja sitt Click-event. Sidan skickas tillbaka.


Men, klickar man på knappen denna gång så skickas "xx:PlaceHolder1:ctl00:ImageButton1.x" och "xx:PlaceHolder1:ctl01:ImageButton1.y" och den här gången finns det en ImageButton1 som ligger i en NamingContainer med UniqueID="xx:PlaceHolder1:ctl00" vilket gör att värdena hamnar rätt. I och med att dessa värden sätts så kommer ImageButton1 att höja sitt Click-event.


Lösningen


Den enkla lösningen är att själv ange ett ID med en löpnummerserie, t.ex. C0, C1 osv.)  som nollställs i i början av MyCreateControls(). Det här säkerställer att första kontrollen alltid får id=C0, och därmed kommer PostBack-värdena att alltid hamna rätt.

MyCreateControls()
PlaceHolder1.Controls.Clear()
DataList=GetData()
Counter=0
ForEach(item in DataList)
{
ctrl=MyCreateControl()
ctrl.data=item.data
ctrl.ID="C"+Counter
PlaceHolder1.Controls.Add(ctrl)
Counter=Counter+1
}



Avslutningsvis


Om du har problem med att event höjs ibland och ibland inte, ta dig en titt på kontrollernas ID:n. Ändrar de sig mellan postbacks så har du troligen orsaken där.

Inga kommentarer: