Retrieving ascx control value from a callback registered on master page. Control added to Repeater's PlaceHolder during OnItemCreated

StackOverflow https://stackoverflow.com/questions/17439423

Question

This is my best attempt to simplify the code to ask the question well. Hopefully it helps.

The short: I need to get the value of a dynamically created Control whose path is loaded from the database and added to a Repeater that contains a PlaceHolder. The value needs to be retrieved from a function on the child page that is called from the master page.

The long: I have a master page that has a lot of settings on it, and an area where a child page can add its own configuration options. Let's say the master page is as follows:

<%@ Master Language="C#" MasterPageFile="~/MainTemplate.master" CodeBehind="ConfigureChoices.master.cs" Inherits="Demo.ConfigureChoices"
AutoEventWireup="true"  %>


<asp:Content ID="Content1" ContentPlaceHolderID="RenderArea" runat="Server">

    <asp:Panel runat="server" ID="PanelConfiguration">
        <asp:TextBox ID="TextBoxForSomething" runat="Server"/>
        <asp:DropDownList ID="AnotherConfigurableThing" runat="server" OnSelectedIndexChanged="DropDownConfiguration_Click" AutoPostBack="true">
            <asp:ListItem Text="Option 1" Selected="True" Value="1"></asp:ListItem>
            <asp:ListItem Text="Option 2"  Value="2"></asp:ListItem>
            <asp:ListItem Text="Option 3"  Value="3"></asp:ListItem>
        </asp:DropDownList>
        <!--etc-->

        <asp:ContentPlaceHolder ID="CustomSettings" runat="server">
        </asp:ContentPlaceHolder>

        <asp:Button ID="ButtonSubmit" runat="Server" Text="Submit" OnClick="ButtonSubmit_Click" />
    </asp:Panel>

</asp:Content>

In codebehind, I need to persist the settings to the database, including custom settings from the user page. The child pages need some of the data created from the master page in order to persist its data. To accomplish this, I have an event that gets populated on child page load and called prior to redirect. It looks like this:

public delegate void BeforeSubmitEventHandler(int configInfoID);
public event BeforeSubmitEventHandler BeforeSubmit;

protected void ButtonSubmit_Click(object sender, EventArgs e)
{
    ConfigInfo config = new ConfigInfo;
    config.EnteredText = TextBoxForSomething.Text;
    config.SelectedValue = AnotherConfigurableThing.SelectedValue;

    int configID = AddToDatabase(config);

    if (BeforeSubmit != null)
        BeforeSubmit(configID);
    Response.Redirect("RedirectURL.aspx");
}

The custom section of the user page has a Repeater, a DropDownList, and an "Add" Button. The Repeater has the name of the option, a short description, a delete image, and a PlaceHolder for loading custom controls from the database. More on that after the code:

<%@ Page Title="" Language="C#" MasterPageFile="~/ConfigureChoices.master" ValidateRequest="false"
    AutoEventWireup="true" Inherits="Demo.CustomChoicePage1" Codebehind="CustomChoicePage1.aspx.cs" 
    MaintainScrollPositionOnPostback="true" %>

<asp:Content ID="MyContent" ContentPlaceHolderID="CustomSettings" runat="server">
    <asp:Repeater ID="RepeaterSelectedOptions" OnItemCreated="OnOptionAdded" runat="server">
        <HeaderTemplate>
            <table id="SelectedOptionsTable">
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Description</th>
                        <th>Remove</th>
                    </tr>
                </thead>

                <tbody>
        </HeaderTemplate>

        <ItemTemplate>
                    <tr>
                        <td>
                            <%# Server.HtmlEncode(Eval("Name").ToString()) %>
                        </td>
                        <td>
                            <%# Server.HtmlEncode(Eval("Description").ToString()) %>
                        </td>
                        <td>
                            <asp:ImageButton ImageUrl="delete.png" ID="ImgDeleteOption" runat="server" OnCommand="DeleteOption_Click" 
                                CommandArgument='<%# Eval("OptionID") %>' />
                        </td>
                    </tr>
                    <asp:PlaceHolder runat="server" ID="optionConfiguration" />
        </ItemTemplate>

        <FooterTemplate>
                </tbody>
            </table>
        </FooterTemplate>
    </asp:Repeater>

    <br />

    <asp:DropDownList ID="DropDownListAvailableOptions" runat="server" />
    <asp:Button ID="ButtonAddOption" runat="server" Text="Add Option" OnCommand="AddOption_Click" />

</asp:Content>

In codebehind, the Repeater is populated the first time on Page_Load using the following code (combination of C# and pseudocode to shorten this already-long question):

protected void Page_Load(object sender, EventArgs e)
{
    ((ConfigureChoices)Master).BeforeSubmit += OnSubmit;
    if (!Page.IsPostBack)
    {
        RefreshOptions();
    }
}

protected void RefreshOptions()
{
    List<Option> fullList = GetOptionsFromDB();
    List<Option> availableList = new List<Option>();
    List<Option> selectedList = new List<Option>();
    List<int> selectedOptions = GetSelectedOptions();

    // Logic here to set the available/selected Lists

    DropDownListAvailableOptions.DataSource = availableList;
    DropDownListAvailableOptions.DataBind();

    RepeaterSelectedOptions.DataSource = selectedList;
    RepeaterSelectedOptions.DataBind();
}

public List<short> GetSelectedOptions()
{
    List<int> selectedOptions = this.ViewState["SelectedOptions"];
    if (selectedOptions == null)
    {
        selectedOptions = new List<int>();
        foreach (Option option in GetOptionsFromDB())
        {
            selectedOptions.Add(option.OptionID);
        }
    }
    return selectedOptions;
}

If the add or remove buttons are clicked, the following methods are used:

public void AddOption_Click(object sender, CommandEventArgs e)
{
    List<int> selectedOptions = GetSelectedOptions();

    selectedOptions.Add(Convert.ToInt32(DropDownListAvailableOptions.SelectedValue));
    this.ViewState["SelectedOptions"] = selectedTests;

    RefreshOptions();
}

public void DeleteOption_Click(object sender, CommandEventArgs e)
{
    List<int> selectedOptions = GetSelectedOptions();

    selectedOptions.Remove(Convert.ToInt32(e.CommandArgument));
    this.ViewState["SelectedOptions"] = selectedOptions;

    RefreshOptions();
}

Finally, the meat of where I think the issue might be, and some explanation of what I've tried. When an option is added to the control, a different table is queried to see if there's an additional ascx that must be loaded into the placeholder. This happens in the method pointed to by OnItemCreated in the Repeater:

protected void OnOptionAdded(Object sender, RepeaterItemEventArgs e)
{
    if (e.Item == null || e.Item.DataItem == null)
        return;

    short optionID = ((Option)e.Item.DataItem).OptionID;
    OptionControl optionControl = GetControlForOptionFromDB(optionID);

    if (optionControl == null)
        return;

    CustomOptionControl control = (CustomOptionControl)this.LoadControl(optionControl.Path);
    control.ID = "CustomControl" + optionID.ToString();

    TableRow tableRow = new TableRow();
    tableRow.ID = "CustomControlTR" + optionID.ToString();
    tableRow.CssClass = "TestConfiguration";

    TableCell tableCell = new TableCell();
    tableCell.ID = "CustomControlTC" + optionID.ToString();
    tableCell.ColumnSpan = 3;

    e.Item.FindControl("optionConfiguration").Controls.Add(tableRow);
    tableRow.Cells.Add(tableCell);
    tableCell.Controls.Add(control);
}

So all of the above "works" in that I see the control on the page, the lists work correctly, and stuff like that. When I click the "Submit" button, I see the configuration (for the sake of this example, let's just say it's a single checkbox) in the Request form variable. However, setting a breakpoint in my callback method on the child page, the CustomOptionControl does not appear to be in the RepeaterSelectedOptions. Only the Option is present.

I have tried at least the following, and more (but I honestly can't recall every step I've tried):

  • adding a call to RefreshOptions to an overridden LoadViewState after the call to load the base
  • doing my initial Repeater binding in Page_Init instead of Page_Load
  • different orders of adding the table row, cell, and custom controls to each other and the main page

How should I be structuring this page and its necessary databinding events so that I can make something like the commented lines in the following code work? When I break at the start of the method and look through the RepeaterOptions.Controls, the CustomOptionControls are gone.

protected void OnSubmit(int configID)
{
    //List<CustomOptionControl> optionsToInsert = find CustomOptionControls in RepeaterOptions (probably an iterative search through the Controls);
    //foreach (CustomOptionControl control in optionsToInsert)
    //{
    //    control.AddOptionToDatabase(configID);
    //}
}
Was it helpful?

Solution

I'm not sure what changed, maybe it was taking the break to rubber duck debug using all of the above. I've gone back and tried tweaking some of the things I had before (order of insertion, which call to use, etc) to see if they make a difference, but either way, the Control is now being persisted in the ViewState properly with the above code. It is available on postback from the master page call so long as the following is added (bullet #1 of what I tried before):

    protected override void LoadViewState(object savedState)
    {
        base.LoadViewState(savedState);
        RefreshOptions();
    }

Earlier, savedState was only showing the List<int> added to it to maintain selected options. At some point in tweaking and debugging, I saw that the controls I created were now in the viewstate, and adding a RefreshOptions call worked. This means on postback for add/remove there are two calls to RefreshOptions, but I can either work around that or ignore it, since behavior is still correct. Thanks for looking!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top