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.

2007-10-02

ID-optimering, fortsättning

Jag skrev i en tidigare bloggpost om hur man enkelt kan optimera sina sidor genom att välja korta ID:n på sina Asp.Net-kontroller. Det här är en fortsättning.

ID på kontroller som saknar ID

Vissa kontroller saknar ID:n. De får då ett ID av typen ctl00, ctl01, ctl02 och så vidare. Det kan därför vara god idé att tilldela dessa kontroller ett ID som är kortare.

ID på MasterPage

På sajter där man använder MasterPages sidor så visar det sig att alla ID:n börjar med ctl00. Med största sannolikhet så kommer den delen från själva MasterPage-kontrollen, eftersom de flesta inte bryr sig om att sätta något ID på den.


För att sätta ID på en masterpage får man gå in i dess konstruktor och där sätta ID. Om masterpage-kontrollen heter DefaultMasterPage.master, så ska följande kod läggas till i dess code behind:

    public DefaultMasterPage()
{
ID = "M";
}



Den förändringen slår i genom på alla kontroller på alla sidor (som utnyttjar DefaultMasterPage).

2007-10-01

Använd korta ID på tunga sidor

I projektet jag jobbar i nu har vi en rätt tung sida: den är komplex till sin struktur, mycket nästlade kontroller och mycket databindning.

Eftersom vi när vi byggde sidan koncentrerade oss på funktionalitet framför att få den snabb blev vi nu, såklart, tvungna att optimera sidan.

Jag har precis påbörjat arbetet. En första sak jag gjorde var att byta ut alla ID:n. Från saker som RepeaterAllRooms och ContainerValidators till RR och CV.

ID:n "ärvs"

Ju högre upp en kontroll är i hierarkin desto viktigare är det att få ID:et kort, eftersom föräldrars ID används för att skapa en kontrolls unika ID. Så alla rader i kontrollen RepeaterAllRooms får ID i stil med RepeaterAllRooms_RoomRow1, RepeaterAllRooms_RoomRow2, etc. Dessutom ligger ju de i sin tur i en container som ligger i en container som ligger i en container som…

Längst ner i hierarkin har vi input-fält. Varje fält fick ID:n i stil med:
ctl00_ctl00_ContentPlaceHolder1_tabContentPlaceHolder_RateAllotmentEditView1_
RateAllotmentControl_MultiTemplatePeriods_I0_RepeaterRooms_I0_RepeatedColumns_
ctl01_ColumnData_colPercentage_txt

Eftersom det är ett input-fält får den dessutom lika långt Name:
ctl00$ctl00$ContentPlaceHolder1$tabContentPlaceHolder$RateAllotmentEditView1$
RateAllotmentControl$MultiTemplatePeriods$I0$RepeaterRooms$I0$RepeatedColumns$
ctl01$ColumnData$colPercentage$txt

Med 100 fält blir sedan snabbt rätt tung. Inte nog med det; ViewState blir tungt också eftersom ID:n används även där.

36% html-kod försvann

Före förändringen var HTML-koden för sidan på runt 338 kB. Efter kortare-ID-bytet var sidan nere i 216 kB. Rätt stor förändring utan särskilt mycket jobb! 

Nästa steg blir bl.a. att se över ViewState.

Förkorta, förkorta, förkorta

Så se till att använda korta ID:n. Kör du med MasterPages, byt ut ContentPlaceHolder1 mot t.ex. CPH eftersom ALLA kontroller under det kommer att innehålla strängen ContentPlaceHolder1. På vår sida gav bara det en minskning med 14 kB. Kontroller som i sin tur innehåller många andra kontroller, som t.ex. en databunden Repeater, är goda kandidater för förkortning.

Att förkorta ID måste man ju såklart väga mot att få läsbar kod.