The Vision Column - Een ASP.NET Control raadsel
The Vision Column wordt verzorgd door ontwikkelaars van
The Vision Web (tegenwoordig Ordina).
Door Michiel van Otegem
12 juli 2005
Soms ben je ergens mee bezig en heb je ineens zoiets van "Wat is hier nou in
godsnaam aan de hand?" Zo ook iets waar ik recentelijk mee bezig was.
Onlangs was ik bezig met een Tab-control voor ASP.NET. Het idee was simpel: ik
heb een control en daarin kunnen tabs gedefinieerd worden waarbij er altijd één
zichtbaar is. Bij de implementatie maakte ik gebruik van een Panel-control voor
iedere tab. So far, so good… totdat ik controls in de Panels zette.
Als ik veranderde van tab, dan werd de ViewState van de controls op andere tabs
niet bewaard. De oplossing was uiteindelijk eenvoudig doch subtiel, maar daar
kwam ik pas achter nadat ik van alles geprobeerd had. Hierdoor vond ik wel iets
uit dat op z'n minst opmerkelijk te noemen is.
Om te weten wat er mis ging, wilde ik graag zien wat er gebeurde bij het opslaan
van de ViewState. Ik maakte een TestBox-control die overerft van TextBox, zodat
ik overrides kon maken en daarin breakpoints kon zetten, als volgt:
public class TestBox : System.Web.UI.WebControls.TextBox {}
De definitie hierboven geeft een control die 100% gelijk is aan de TextBox.
Althans, dat zou je denken. Maar… als ik de TestBox in een Panel zette, werd de
ViewState daarvan wel opgeslagen, terwijl dat van een TextBox in dezelfde Panel
niet zo was. What the ....?!?! Dit was voor mij een dermate groot raadsel dat ik
besloot de jongens in Redmond even lastig te vallen. Het antwoord:
"Ja, dat klopt. Van onze eigen controls weten we precies wanneer er wel/geen
ViewState nodig is (mits alles goed geprogrammeerd is), dus slaan we die echt
alleen op als dat noodzakelijk is. Van controls die geërfd zijn, weten we niet
zeker dat de ViewState nog hetzelfde werkt, dus wordt voor de zekerheid de
ViewState daarvan wel opgeslagen."
Met andere woorden er staat ergens gewoon de volgende pseudo-code:
if(control.IsInherited) control.SaveViewState();
Het nut van deze informatie is natuurlijk beperkt, maar het is wel eens leuk om
te weten waarom sommige dingen anders werken dan je zou verwachten.
Maar wat was nu de oorzaak van het niet werken van mijn control? Het moment
waarop ik instelde welke tab zichtbaar moet zijn: ik deed dit in eerste instantie
in de Render methode van de control. Dan is de ViewState echter al opgeslagen,
en weet ASP.NET niet dat bepaalde controls onzichtbaar gaan worden en dat daarvan
de ViewState opgeslagen moet worden. Op het moment dat ik de zichtbaar/onzichtbaar
logica verplaatste naar het RaisePostbackEvent (dat uitgevoerd wordt als je op een
van de tabs klikt), dat vóór het opslaan van de ViewState plaatsvindt, waren mijn
problemen verleden tijd…
Toch wel een domme fout voor iemand die de volgorde van de events in controls
van voor naar achter en weer terug kent. Ik troost mij met de gedachte dat
iedereen wel eens iets over het hoofd ziet dat voor de hand ligt. Ter referentie
staat hieronder nog de code die niet werkt.
De control
1: using System;
2: using System.ComponentModel;
3: using System.Web.UI;
4: using System.Web.UI.WebControls;
5:
6: namespace Dev.WebControls
7: {
8: [ParseChildren(false)]
9: [ControlBuilder(typeof(TestControlBuilder))]
10: public class TestControl : WebControl, IPostBackEventHandler
11: {
12: protected int SelectedPanelIndex
13: {
14: get
15: {
16: object o = ViewState["SelectedPanelIndex"];
17: if(o == null) return 0;
18: return (int)o;
19: }
20: set
21: {
22: ViewState["SelectedPanelIndex"]= value;
23: }
24: }
25:
26: public override ControlCollection Controls
27: {
28: get
29: {
30: this.EnsureChildControls();
31: return base.Controls;
32: }
33: }
34:
35: protected override void
RenderContents(System.Web.UI.HtmlTextWriter writer)
36: {
37: for(int i = 0; i < this.Controls.Count; i++)
38: {
39: Panel panel = (Panel)this.Controls[i];
40: writer.AddAttribute(HtmlTextWriterAttribute.Href,
Page.GetPostBackClientHyperlink(this, i.ToString()));
41: writer.RenderBeginTag(HtmlTextWriterTag.A);
42: writer.Write(panel.ID);
43: writer.RenderEndTag();
44:
45: //Dit is dus waar het fout gaat.
46: //Deze code moet voor het opslaan van de ViewState.
47: if(this.SelectedPanelIndex != i)
48: {
49: panel.Visible = false;
50: }
51:
52: writer.WriteFullBeginTag("br");
53: }
54: writer.WriteFullBeginTag("hr");
55: base.RenderContents(writer);
56:
57: foreach(Control control in this.Controls)
58: {
59: if(control is Panel) control.Visible = true;
60: }
61: }
62:
63: #region IPostBackEventHandler Members
64:
65: public void RaisePostBackEvent(string eventArgument)
66: {
67: this.SelectedPanelIndex = Convert.ToInt32(eventArgument);
68: }
69:
70: #endregion
71: }
72:
73: public class TestControlBuilder : ControlBuilder
74: {
75: public override bool AllowWhitespaceLiterals()
76: {
77: return false;
78: }
79:
80: public override Type GetChildControlType(string tagName,
System.Collections.IDictionary attribs)
81: {
82: if(string.Compare(tagName, "asp:Panel", true) == 0)
83: {
84: return typeof(Panel);
85: }
86: else
87: {
88: throw new ApplicationException("Only panels
are allowed as child controls.");
89: }
90: }
91: }
92: }
De pagina
1: <%@ Register TagPrefix="cc1" Namespace="Dev.WebControls"
Assembly="Dev.WebControls" %>
2: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
3: <html>
4: <body>
5: <form id="Form1" method="post" runat="server">
6: <cc1:TestControl id="TestControl1" runat="server">
7: <asp:panel id="Panel1" runat="server">
8: <cc1:TestBox ID="TestBox1" Runat="server">123</cc1:TabBox>
9: <asp:TextBox ID="Textbox1" Runat="server">456</asp:TextBox>
10: </asp:panel>
11: <asp:panel id="Panel2" runat="server">
12: <asp:TextBox ID="Textbox2" Runat="server">789</asp:TextBox>
13: </asp:panel>
14: </cc1:TestControl>
15: </form>
16: </body>
17: </html>
|