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 CustomOptionControl
s 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);
//}
}