Rendering child controls of custom composite control inside a Repeater - c#

I have a custom control derived from the CompositeControl class, which is defined as:
[ToolboxData("<{0}:ContractControl runat=server></{0}:ContractControl>")]
public class ContractControl : CompositeControl
{
private int contractID = 0;
private ContractTileControl tileControl = null;
private ContractDetailControl detailControl = null;
private HtmlGenericControl contractMainDiv = null;
public int ContractID
{
get { return this.contractID; }
set { this.contractID = value; }
}
public ContractTileControl TileControl
{
get { return this.tileControl; }
set { this.tileControl = value; }
}
public ContractDetailControl DetailControl
{
get { return this.detailControl; }
set { this.detailControl = value; }
}
public ContractControl()
{
this.contractMainDiv = new HtmlGenericControl("div");
this.contractMainDiv.ID = "contractMainDiv";
this.contractMainDiv.Attributes.Add("class", "contractMain");
}
#region protected override void OnPreRender(EventArgs e)
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
CreateChildControls();
}
#endregion
#region protected override void CreateChildControls()
protected override void CreateChildControls()
{
if (tileControl != null)
{
this.contractMainDiv.Controls.Add(tileControl);
}
if (detailControl != null)
{
this.contractMainDiv.Controls.Add(detailControl);
}
this.Controls.Add(contractMainDiv);
}
#endregion
}
When I add a number of them to a placeholder control they render fine, but when I try to bind the same ones to a Repeater, the child composite controls tileControl and detailControl do not render, only the contractMainDiv does.
The repeater is defined as:
<asp:Repeater ID="myRepeater" runat="server" EnableTheming="true">
<HeaderTemplate>
<table border="0" cellpadding="0" cellspacing="0">
</HeaderTemplate>
<ItemTemplate>
<tr><td><easit:ContractControl ID="contractControl" runat="server" />
</td></tr>
</ItemTemplate>
<FooterTemplate>
</table>
</FooterTemplate>
</asp:Repeater>
And I bind to it by first generating a List<ContractControl> and then calling:
List<ContractControl> controls = new List<ContractControl>();
//Generate custom controls for each element in input dictionary
//Create TileControl and DetailControl
//Create ContractControl and add it to collection
ContractControl contractControl = new ContractControl();
contractControl.ContractID = contract.Contract.Id;
contractControl.TileControl = tile;
contractControl.DetailControl = detail;
controls.Add(contractControl);
myRepeater.DataSource = controls;
myRepeater.DataBind();
Yet the resulting table contains the right number of items, but the child 'CompositeControl's do not get rendered at all, only the contractMainDiv shows up.

Related

Cannot implicitly convert type 'System.Web.UI.CompiledTemplateBuilder' to custom ITemplate

When you create a ListView, there is a property on the control that allows you to specify the ID of the PlaceHolder in the LayoutTemplate that is used to hold the controls for each item.
<asp:ListView ID="lvTest" runat="server" ItemPlaceholderID="testPlaceholder">
<LayoutTemplate>
<ul>
<asp:PlaceHolder ID="testPlaceholder" runat="server" />
</ul>
</LayoutTemplate>
<ItemTemplate>
<li>Item</li>
</ItemTemplate>
</asp:ListView>
I am trying to create a custom templated server control but would like to have a property on the template itself as I expect to have several different templates. My extremely simplified, trivial example is as follows:
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace ControlLibrary
{
[ParseChildren(true)]
public class TestContainer : Control, INamingContainer
{
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(TestContainer))]
public TestTemplate TestTemplate { get; set; }
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
DataBind();
}
public override void DataBind()
{
base.DataBind();
CreateChildControls();
}
protected override void CreateChildControls()
{
Controls.Clear();
var phTest = new PlaceHolder();
phTest.ID = "phTest";
Controls.Add(phTest);
if (this.TestTemplate != null)
this.TestTemplate.InstantiateIn(phTest);
this.ChildControlsCreated = true;
}
}
public class TestTemplate : ITemplate
{
public string Name { get; set; }
public void InstantiateIn(Control container)
{
container.Controls.Add(new LiteralControl(this.Name));
}
}
}
However, if I add the template to my .aspx file like below I get an error saying CS0029: Cannot implicitly convert type 'System.Web.UI.CompiledTemplateBuilder' to 'ControlLibrary.TestTemplate'
<CustomControls:TestContainer ID="testContainer" runat="server">
<TestTemplate>
test
</TestTemplate>
</CustomControls:TestContainer>
Is it possible to do this or will I have to put all configurable properties on the parent container itself?
Thanks for any help you can provide!
You did everything correctly except the template property declaration. Any template must be declared as System.Web.UI.ITemplate. Just replace your template declaration with:
public ITemplate TestTemplate { get; set; }
And it should start working. For reference see How to: Create Templated ASP.NET User Controls.
To do a trick with custom attributes for template you need to add one more class inheriting from collection:
public class TestTemplateList : List<TestTemplate> { }
And change declaration of the control as following:
[ParseChildren(true, DefaultProperty = "TestTemplates")]
public class TestContainer : Control, INamingContainer
{
[PersistenceMode(PersistenceMode.InnerProperty)]
public TestTemplateList TestTemplates { get; set; }
// ... OnLoad and DataBind left intact
protected override void CreateChildControls()
{
Controls.Clear();
var phTest = new PlaceHolder();
phTest.ID = "phTest";
Controls.Add(phTest);
if (this.TestTemplates != null)
{
foreach (var testTemplate in TestTemplates)
{
((ITemplate)testTemplate).InstantiateIn(phTest);
}
}
this.ChildControlsCreated = true;
}
}
After that you should be able to declare control in both ways. You can skip specifying TestTemplates because it is declared as a DefaulProperty in ParseChildrenAttribute:
<CustomControls:TestContainer runat="server" ID="testContainer">
<CustomControls:TestTemplate Name="test">
</CustomControls:TestTemplate>
</CustomControls:TestContainer>
<%-- OR --%>
<CustomControls:TestContainer runat="server" ID="wc11">
<TestTemplates>
<CustomControls:TestTemplate Name="test">
</CustomControls:TestTemplate>
</TestTemplates>
</CustomControls:TestContainer>

Binding List<CompositeControl> to a Repeater

I have a collection of generated custom controls that extend CompositeControl, defined as:
[PersistChildren(true)]
[ToolboxData("<{0}:ContractControl runat=server></{0}:ContractControl>")]
public class ContractControl : CompositeControl
{
private int contractID = 0;
private ContractTileControl tileControl = null;
private ContractDetailControl detailControl = null;
private HtmlGenericControl contractMainDiv = null;
public int ContractID
{
get { return this.contractID; }
set { this.contractID = value; }
}
public ContractTileControl TileControl
{
get { return this.tileControl; }
set { this.tileControl = value; }
}
public ContractDetailControl DetailControl
{
get { return this.detailControl; }
set { this.detailControl = value; }
}
public ContractControl()
{
this.contractMainDiv = new HtmlGenericControl("div");
this.contractMainDiv.ID = "contractMainDiv";
this.contractMainDiv.Attributes.Add("class", "contractMain");
}
#region protected override void OnPreRender(EventArgs e)
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
//CreateChildControls();
}
#endregion
#region protected override void CreateChildControls()
protected override void CreateChildControls()
{
base.CreateChildControls();
if (tileControl != null)
{
this.contractMainDiv.Controls.Add(tileControl);
}
if (detailControl != null)
{
this.contractMainDiv.Controls.Add(detailControl);
}
this.Controls.Add(contractMainDiv);
//base.CreateChildControls();
}
#endregion
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
CreateChildControls();
}
protected override void OnInit(EventArgs e)
{
base.OnLoad(e);
EnsureChildControls();
}
}
Where ContractTileControl and ContractDetailControl are another custom controls derived from CompositeControl.
When I add them to a asp:PlaceHolder control set they render fine, but when I define a repeater like:
<asp:Repeater ID="myRepeater" runat="server" >
<HeaderTemplate>
<table border="0" cellpadding="0" cellspacing="0">
</HeaderTemplate>
<ItemTemplate>
<tr><td><easit:ContractControl ID="contractControl" runat="server" />
</td></tr>
</ItemTemplate>
<FooterTemplate>
</table>
</FooterTemplate>
</asp:Repeater>
And bind them to it:
private void FillContractPlaceHolder()
{
List<ContractControl> controls = new List<ContractControl>();
foreach(KeyValuePair<Customer, List<TFSContract>> pair in contractList)
{
Label customerNameLbl = new Label();
customerNameLbl.ID = "customerNameLbl";
customerNameLbl.CssClass = "customerName";
customerNameLbl.Text = pair.Key.Name;
contractListPlaceHolder.Controls.Add(customerNameLbl);
foreach (TFSContract contract in pair.Value)
{
ContractStatusBarControl status = new ContractStatusBarControl();
status.WidthPercent = GetFillPercent(contract.NumberOfTasks, contract.NumberOfFinishedTasks);
string[] contractNameParts = Regex.Split(contract.Contract.Name, #"[A-Z]{3}-[0-9|A-Z]{2}-[0-9|A-Z]{2}", RegexOptions.IgnoreCase);
ContractDetailControl detail = new ContractDetailControl();
detail.ContractName = contractNameParts.Last();
detail.DateStarted = contract.StartDate;
detail.DateFinished = contract.FinishDate;
detail.StatusBar = status;
ContractTileControl tile = new ContractTileControl();
Match match = Regex.Match(contract.Contract.Name, #"[A-Z]{3}-[0-9|A-Z]{2}-[0-9|A-Z]{2}", RegexOptions.IgnoreCase);
if (match.Value.Length != 0)
{
tile.ContractNumber = match.Value;
}
tile.ContractTasksFinished = contract.NumberOfFinishedTasks;
tile.ContractTasksTotal = contract.NumberOfTasks;
ContractControl contractControl = new ContractControl();
contractControl.ContractID = contract.Contract.Id;
contractControl.TileControl = tile;
contractControl.DetailControl = detail;
//contractListPlaceHolder.Controls.Add(contractControl);
controls.Add(contractControl);
}
}
myRepeater.DataSource = controls;
myRepeater.DataBind();
}
The table gets created, but only the non-composite part contractMainDiv of ContractControl gets rendered, as the Repeater insists that both tileControl and detailControl are null, even though they are properly set to instances of their respective types.
When the Repeater is data-bound, it creates an instance of the ItemTemplate for each item in the data-source, set its DataItem to the item from the data-source, and data-binds the children.
In this case, the item from the data-source is an instance of your ContractControl, and your ItemTemplate has no data-binding, so you'll end up with a blank instance of the ContractControl for each item you've added to the list.
The quick and dirty solution is to add a handler for the ItemDataBound event of your Repeater, and copy the properties to the real control:
protected void myRepeater_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
switch (e.Item.ItemType)
{
case ListItemType.Item:
case ListItemType.AlternatingItem:
case ListItemType.SelectedItem:
case ListItemType.EditItem:
{
var source = (ContractControl)e.Item.DataItem;
var destination = (ContractControl)e.Item.FindControl("contractControl");
destination.ContractID = source.ContractID;
destination.TileControl = source.TileControl;
destination.DetailControl = source.DetailControl;
break;
}
}
}
A better solution would be to bind your Repeater to a list of TFSContract objects, and moving the code to build the ContractControl into the ItemDataBound event handler.
EDIT
Updated to only process real items, ignoring headers, footers, etc.

How to create a custom control with INamingContainer?

I am trying the following:
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(TemplateContainer))]
public virtual ITemplate LayoutTemplate { get; set; }
protected void Page_Init(object sender, EventArgs e)
{
this.Controls.Clear();
if (LayoutTemplate != null)
{
var data = Enumerable.Range(0, 10);
foreach (int index in data)
{
TemplateContainer container = new TemplateContainer(index);
LayoutTemplate.InstantiateIn(container);
this.Controls.Add(container);
}
}
}
My container class:
public class TemplateContainer : Control, INamingContainer
{
public int Index { get; set; }
internal TemplateContainer(int index)
{
this.Index = index;
}
}
And my markup:
<uc:TemplateControl ID="ucTemplateControl" runat="server">
<LayoutTemplate>
<b>Index:</b>
<asp:TextBox ID="Literal1" runat="server"
Text='<%# Container.Index %>'
ReadOnly="true" />
<br />
</LayoutTemplate>
</uc:TemplateControl>
But for some reason Container.Index is not rendering any value, just empty. 10 controls are being created, but none shows a value.
What did I do wrong? How can I fix it so it will show the Index value?
I tried something similar to MSDN example:
How to: Create Templated ASP.NET User Controls
To bind the value you need to call the DataBind method of the custom control.
Calling
ucTemplateControl.DataBind();
from the page made the data to be bound on the template.

Unable to populate RadioButtonList inside ASP.NET Repeater Template

I am trying to populate a repeater containing a label and a RadioButtonList in an ASP.NET webform to make a small quiz.
These are the classes I am using:
public class Option
{
private string _body;
private bool? _isCorrect;
#region properties
public string Body
{
get { return _body; }
}
public bool? IsCorrect
{
get { return _isCorrect; }
}
#endregion
#region constructors
public Option(string body)
{
_body = body;
_isCorrect = null;
}
public Option(string body, bool isCorrect)
{
_body = body;
_isCorrect = isCorrect;
}
#endregion
#region methods
public override string ToString()
{
return _body;
}
#endregion
}
and:
public class Question
{
private string _body;
private Option[] _optionsArray;
private List<Option> _optionsList;
#region properties
public string Body
{
get { return _body; }
}
public Option[] Options
{
get { return _optionsArray; }
}
public List<Option> OptionsList
{
get { return _optionsList; }
}
#endregion
#region constructors
public Question(string body, Option[] options)
{
_body = body;
_optionsArray = options;
_optionsList = new List<Option>();
foreach (Option opt in options)
{
_optionsList.Add(opt);
}
}
#endregion
#region methods
public override string ToString()
{
return _body;
}
public List<Option> GetOptions()
{
return OptionsList;
}
#endregion
}
My webform looks like this:
<div runat="server" ID="quizDiv">
<br />
<br />
<asp:Repeater ID="Repeater1" runat="server">
<ItemTemplate>
<%--<asp:Label ID="questionBody" runat="server" Text=<%# DataBinder.Eval(Container.DataItem, "Body") %>>--%>
<asp:Label ID="questionBody" runat="server" Text=<%# ((Question)Container.DataItem).Body %>>
<asp:radiobuttonlist ID="blah" runat="server" DataTextField="Body" DataValueField="Body" DataSource=<%# ((Question)Container.DataItem).OptionsList %> >
</asp:radiobuttonlist>
</asp:Label>
</ItemTemplate>
</asp:Repeater>
<br />
</div>
and the code-behind like so:
protected void btnStart_Click(object sender, EventArgs e)
{
Dataset1 ds = new Dataset1();
question = ds.CreateQuestion();
List<Question> qts = new List<Question>();
qts.Add(question);
Repeater1.DataSource = qts;
Repeater1.DataBind();
}
For the time being I am just using one question. The label displaying my question shows up fine, but no radio buttons show up for displaying the answer options. I have gone through many samples, and this seems to work for people when they use data from a DB via a DataTable or DataSet. However no matter how much I to play around with the Datasource, DataValueField and DataTextField parameters the RadioButtonList remains utterly barren. As you can see, I initially used an array of Option and also tried a List but to no avail.
What am I missing here?
EDIT - Found my mistake
I had the <asp:label> and <asp:radiobuttonlist> tags nested wrong! The label was encapsulating the radiobuttonlist and causing the problem from what I can make out.
ALTERNATIVE - thanks to balexandre
balexandre did provide a workable alternative by using only code-behing. Thank you!
I am including the code listing of this alternative for anyone who needs it and happens across this post.
Change the markup to look like this:
<asp:Repeater ID="Repeater1" runat="server" OnItemDataBound="Repeater1_ItemDataBound">
<ItemTemplate>
<%--<asp:Label ID="questionBody" runat="server" Text=<%# DataBinder.Eval(Container.DataItem, "Body") %>>--%>
<asp:Label ID="questionBody" runat="server" Text=<%# ((Question)Container.DataItem).Body %>>
</asp:Label>
<asp:radiobuttonlist ID="blah" runat="server" >
</asp:radiobuttonlist>
</ItemTemplate>
</asp:Repeater>
and the code-behind must have the event handler:
protected void Repeater1_ItemDataBound(Object Sender, RepeaterItemEventArgs e)
{
if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
{
RadioButtonList rbl = (RadioButtonList)e.Item.FindControl("blah");
// do anything with your rbl
foreach (Option opt in question.Options)
{
rbl.Items.Add(opt.ToString());
}
}
}
you can only access the template using the Repeater method OnItemDataBound witch is invoked before it draws anything to the page.
void Repeater1_ItemDataBound(Object Sender, RepeaterItemEventArgs e) {
if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem) {
RadioButtonList rbl = (RadioButtonList)e.Item.FindControl["blah"];
// do anything with your rbl
}
}
Or by looping through all the Controls in the Repeater Collection, witch for example you have submited the page.
BTW, and just for your information
this code:
private string _body;
private bool? _isCorrect;
public string Body
{
get { return _body; }
}
public bool? IsCorrect
{
get { return _isCorrect; }
}
is the same as
public string Body { private get; }
public bool? IsCorrect { private get; }

Storing Viewstate in Database disables my asp:ImageButtons events

I followed the guide here to save my ViewState into the database. I did some changes to it to fit my projects code design, but the essential parts are there. But when I implant this solution all my asp:ImageButtons events stops working, but regular asp:Buttons seems to work. Why doesn't the events from asp:ImageButtons work?
Code:
public class DatabasePageStatePersister : PageStatePersister
{
//This object handles the saving and loading from database
CiroLightLibrary.BLL.ViewState myViewState;
public DatabasePageStatePersister(Page p, string GUID): base(p)
{
myViewState = new CiroLightLibrary.BLL.ViewState();
myViewState.GUID = GUID;
}
public override void Load()
{
myViewState.Load();
this.ViewState = this.StateFormatter.Deserialize(myViewState.Value);
}
public override void Save()
{
myViewState.Value = this.StateFormatter.Serialize(this.ViewState);
myViewState.Save();
}
}
public class PageViewStateDatabaseStored : Page
{
private PageStatePersister _PageStatePersister;
protected override System.Web.UI.PageStatePersister PageStatePersister
{
get
{
if (_PageStatePersister == null)
{
CiroLightLibrary.BLL.ViewState myViewState = new ViewState();
if (Request["__DATABASE_VIEWSTATE"] != null)
myViewState.GUID = Request["__DATABASE_VIEWSTATE"].ToString();
else
myViewState.GUID = Guid.NewGuid().ToString();
_PageStatePersister = new DatabasePageStatePersister(this, myViewState.GUID);
Literal l = new Literal();
l.Text = "<div><input type=\"hidden\" name=\"__DATABASE_VIEWSTATE\" value=\"" + myViewState.GUID + "\" /></div>";
this.Form.Controls.Add(l);
}
return _PageStatePersister;
}
}
}
And heres a Test page:
public partial class test : PageViewStateDatabaseStored
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
imgButton1.CommandArgument = "1";
btnButton1.CommandArgument = "1";
}
}
protected void imgButton_OnCommand(object sender, CommandEventArgs e)
{
Response.Write(e.CommandArgument.ToString());
}
protected void imgButton_OnClick(object sender, EventArgs e)
{
Response.Write("Click");
}
}
Asp.net Page
<form runat="server">
<asp:ImageButton ID="imgButton1" runat="server" OnCommand="imgButton_OnCommand" />
<asp:ImageButton ID="imgButton2" runat="server" OnClick="imgButton_OnClick" />
<asp:Button ID="btnButton1" runat="server" OnCommand="imgButton_OnCommand" />
<asp:Button ID="btnButton2" runat="server" OnClick="imgButton_OnClick" />
</form>
I tried to override LoadPageStateFromPersistenceMedium() and SavePageStateToPersistenceMedium() instead, and now my asp:ImageButtons also fires the events.
public class PageViewStateDatabaseStored : Page
{
protected override object LoadPageStateFromPersistenceMedium()
{
CiroLightLibrary.BLL.ViewState myViewState = new ViewState();
if (Request["__DATABASE_VIEWSTATE"] != null)
{
myViewState.GUID = Request["__DATABASE_VIEWSTATE"].ToString();
myViewState.Load();
}
LosFormatter myFormatter = new LosFormatter();
return myFormatter.Deserialize(myViewState.Value);
}
protected override void SavePageStateToPersistenceMedium(object viewState)
{
CiroLightLibrary.BLL.ViewState myViewState = new ViewState();
if (Request["__DATABASE_VIEWSTATE"] != null)
myViewState.GUID = Request["__DATABASE_VIEWSTATE"].ToString();
else
myViewState.GUID = Guid.NewGuid().ToString();
LosFormatter myFormatter = new LosFormatter();
StringWriter myStringWriter = new StringWriter();
myFormatter.Serialize(myStringWriter, viewState);
myViewState.Value = myStringWriter.ToString();
myViewState.Save();
ScriptManager.RegisterHiddenField(this, "__DATABASE_VIEWSTATE", myViewState.GUID);
}
}
I know this is an old post.... But the answer is simply that you're saving the viewstate not the controlstate.
public override void Load()
{
myViewState.Load();
var pair = (Pair)this.StateFormatter.Deserialize(myViewState.Value);
this.ViewState = pair.First;
this.ControlState = pair.Second;
}
public override void Save()
{
myViewState.Value = this.StateFormatter.Serialize(new Pair(this.ViewState, this.ControlState));
myViewState.Save();
}
Personally I'd inherit my PageStatePersister from HiddenFieldPageStatePersister then in my save method write the guid into the viewstate property and set the controlstate to null, then call the base Save method. In the Load, call the load the base.load then get the GUID from the viewstate property before pushing the db aquired values into the viewstate, controlstate properties. That way we're not modifying the control tree.... Like so:
public class MyPageStatePersister : System.Web.UI.HiddenFieldPageStatePersister
{
public MyPageStatePersister(Page page)
: base(page)
{
}
public override void Load()
{
base.Load();
this.CurrentKey = (Guid)this.ViewState;
var s = this.CurrentKey;
var state = SomeDAOManager.GetState(s);
if (state != null)
{
this.ViewState = state.First;
this.ControlState = state.Second;
}
else
{
this.ControlState = null;
this.ViewState = null;
}
}
public Guid CurrentKey {get;set;}
public override void Save()
{
if (CurrentKey == Guid.Empty)
{
this.CurrentKey = Guid.NewGuid();
}
Guid guid = CurrentKey;
SomeDAOManager.SaveState(guid, new Pair(this.ViewState, this.ControlState));
this.ViewState = guid;
this.ControlState = null;
base.Save();
}
}

Categories