views:

2895

answers:

2

Hi All,

I'm setting up a User Control driven by a XML configuration. It is easier to explain by example. Take a look at the following configuration snippet:

<node>
  <text lbl="Text:"/>
  <checkbox lbl="Check me:" checked="true"/>
</node>

What I'm trying to achieve to translate that snippet into a single text box and a checkbox control. Of course, had the snippet contained more nodes more controls would be generated automatically.

Give the iterative nature of the task, I have chosen to use Repeater. Within it I have placed two (well more, see bellow) Controls, one CheckBox and one Editbox. In order to choose which control get activate, I used an inline switch command, checking the name of the current configuration node.

Sadly, that doesn't work. The problem lies in the fact that the switch is being run during rendering time, long after data binding had happened. That alone would not be a problem, was not for the fact that a configuration node might offer the needed info to data bind. Consider what would happen if the check box control will try to bind to the text node in the snippet above, desperately looking for it's "checked" attribute.

Any ideas how to make this possible?

Thanks, Boaz

Here is my current code:

Here is my code (which runs on a more complex syntax than the one above):

<asp:Repeater ID="settingRepeater" runat="server">
        <ItemTemplate>
           <% 
              switch (((XmlNode)Page.GetDataItem()).LocalName)
              {
                 case "text":
           %>
           <asp:Label ID="settingsLabel" CssClass="editlabel" Text='<%# XPath("@lbl") %>' runat="server" />
           <asp:TextBox ID="settingsLabelText" Text='<%# SettingsNode.SelectSingleNode(XPath("@xpath").ToString()).InnerText %>'
              runat="server" AutoPostBack="true"  Columns='<%#  XmlUtils.OptReadInt((XmlNode)Page.GetDataItem(),"@width",20) %>'
              />
           <% break;
                 case "checkbox":
           %>
           <asp:CheckBox ID="settingsCheckBox" Text='<%# XPath("@lbl") %>' runat="server"
                         Checked='<%# ((XmlElement)SettingsNode.SelectSingleNode(XPath("@xpath").ToString())).HasAttribute(XPath("@att").ToString()) %>'
            />
          <% break;
              } %>
           &nbsp;&nbsp;
        </ItemTemplate>
     </asp:Repeater>
+1  A: 

You need something that looks more like this:

<ItemTemplate>
    <%# GetContent(Page.GetDataItem()) %>
</ItemTemplate>

And then have all your controls generated in the code-behind.

Keltex
+1  A: 

One weekend later, here is what I came with as a solution. My main goal was to find something that will both work and allow you to keep specifying the exact content of the Item Template in markup. Doing things from code would work but can still be cumbersome.

The code should be straight forward to follow, but the gist of the matter lies in two parts.

The first is the usage of the Repeater item created event to filter out unwanted parts of the template.

The second is to store decisions made in ViewState in order to recreate the page during post back. The later is crucial as you'll notice that I used the Item.DataItem . During post backs, control recreation happens much earlier in the page life cycle. When the ItemCreate fires, the DataItem is null.

Here is my solution:

Control markup

 <asp:Repeater ID="settingRepeater" runat="server" 
            onitemcreated="settingRepeater_ItemCreated" 
          >
        <ItemTemplate>
             <asp:PlaceHolder  ID="text" runat="server">
                  <asp:Label ID="settingsLabel" CssClass="editlabel" Text='<%# XPath("@lbl") %>' runat="server" />
                  <asp:TextBox ID="settingsLabelText"  runat="server"
                      Text='<%# SettingsNode.SelectSingleNode(XPath("@xpath").ToString()).InnerText %>'
                     Columns='<%#  XmlUtils.OptReadInt((XmlNode)Page.GetDataItem(),"@width",20) %>'

                   />

            </asp:PlaceHolder>
            <asp:PlaceHolder ID="att_adder" runat="server">
               <asp:CheckBox ID="settingsAttAdder" Text='<%# XPath("@lbl") %>' runat="server"
                             Checked='<%# ((XmlElement)SettingsNode.SelectSingleNode(XPath("@xpath").ToString())).HasAttribute(XPath("@att").ToString()) %>'
                />
            </asp:PlaceHolder>
      </ItemTemplate>
  </asp:Repeater>

Note: for extra ease I added the PlaceHolder control to group things and make the decision of which controls to remove easier.

Code behind

The code bellow is built on the notion that every repeater item is of a type. The type is extracted from the configuration xml. In my specific scenario, I could make that type to a single control by means of ID. This could be easily modified if needed.

 protected List<string> repeaterItemTypes
   {
      get
      {
         List<string> ret = (List<string>)ViewState["repeaterItemTypes"];
         if (ret == null)
         {
            ret = new List<string>();
            ViewState["repeaterItemTypes"] = ret;
         }
         return ret;
      }
   }

   protected void settingRepeater_ItemCreated(object sender, RepeaterItemEventArgs e)
   {
      string type;
      if (e.Item.DataItem != null)
      {
         // data binding mode..
         type = ((XmlNode)e.Item.DataItem).LocalName;
         int i = e.Item.ItemIndex;
         if (i == repeaterItemTypes.Count)
            repeaterItemTypes.Add(type);
         else
            repeaterItemTypes.Insert(e.Item.ItemIndex, type);
      }
      else
      {
         // restoring from ViewState
         type = repeaterItemTypes[e.Item.ItemIndex];
      }

      for (int i = e.Item.Controls.Count - 1; i >= 0; i--)
      {
         if (e.Item.Controls[i].ID != type) e.Item.Controls.RemoveAt(i);
      }
   }
Boaz