Setting complex members of a control inside a Repeater - asp.net

TL;DR
The Control inside my Repeater requires special instantiation before it can be useful. How do I perform this special instantiation for each control as they are constructed by the repeater?
The Setup
I have three classes:
ComplexData.
Contains many public members of varying types, such as:
int favorite_integer
Food what_you_had_for_breakfast_this_morning
string capital_of_alaska
PhaseOfMoon when_magic_runes_will_reveal_themselves_on_the_mysterious_artifact
etc, etc, etc
ComplexDataDisplay
A control used to render an instance of ComplexData in an aesthetically pleasing way. Exposes a single public member, ComplexMember DataToDisplay. When the user sets DataToDisplay, the labels within the control will populate themselves with the appropriate data.
MultiComplexDataDisplay
A control that uses a Repeater to display multiple ComplexDataDisplays in a row. It contains a private List<ComplexData> datasToDisplay, which is the data source for the repeater. It provides two public methods, addComplexData(ComplexData) and emptyAllData(), which let the user manipulate datasToDisplay.
The Problem
During the data bind process, I don't know how to set each ComplexDataDisplay's DataToDisplay member.
It appears that controls within repeaters are normally populated from the front end, for example
<asp:Repeater runat="server" id="linkRepeater">
<%# getDescription(Container.DataItem) %>
</asp:Repeater>
As I understand it, during data bind, the repeater instantiates each anchor element, using whatever the data source is to set the href and description.
My attempt to replicate this behavior only led to a blank page:
<asp:Repeater runat="server" id="displayRepeater">
<ComplexDataDisplay id="dataDisplay" DataToDisplay="<%# Container.DataItem %>" runat="server"
</asp:Repeater>
The only other thing I can think to try is to modify ComplexDataDisplay so it has a public member for each member of ComplexData. Then within the repeater I can do:
<asp:Repeater runat="server" id="displayRepeater">
<ComplexDataDisplay id="dataDisplay" runat="server"
favorite_integer="<%# get_favorite_integer(Container.DataItem) %>"
what_you_had_for_breakfast_this_morning="<# get_what_you_had_for_breakfast_this_morning(Container.DataItem) %>"
<%--etc etc etc--%>
/>
</asp:Repeater>
This seems highly undesirable because for every member of ComplexData, I'll have to write a corresponding public member in ComplexDataDisplay, and a get_whatever(DataItem) method in MultiComplexDataDisplay. On top of that, I don't even know if it will work, because half of ComplexData's members are complex data types, which may or may not be settable in this way.
What I'm looking for in an answer
One of the following:
A direct answer to the question posed in the TLDR section - a way to perform special instantiation for each control in the repeater during databind, or a way to iterate through them shortly after the fact.
A recommendation on how to best restructure the code, such so that special instantiation is no longer necessary to display my many Complex Datas. I am willing to add/update/delete any class besides ComplexData, as I am obligated to maintain its interface as-is.
best practices/references/documentation related to my problem, which will guide me in finding a solution myself.

You can bind you child controls inside of ItemDataBound, I couldn't follow which objects get bound when so replace where needed:
<asp:Repeater runat="server" id="displayRepeater" OnItemDataBound="displayRepeater_ItemDataBound">
<uc1:ComplexDataDisplay id="dataDisplay" runat="server" />
</asp:Repeater>
protected void displayRepeater_ItemDataBound(Object Sender, RepeaterItemEventArgs e)
{
// Execute the following logic for Items and Alternating Items.
if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
{
//get object bound to row
ComplexData c = (ComplexData)e.Item.DataItem;
//find the child control to bind
ComplexDataDisplay cds = (ComplexDataDisplay)e.Item.FindControl("dataDisplay");
//set properties or any other complex things you need to do
cds.DataToDisplay = c.ComplexMember;
cds.DataBind(); // if this control has repeaters, it's ItemDataBound will fire, repeat this process untill all your controls are bound properly
}
}

Related

How to get access to DropDownList control if I add them from code

I am trying to access a DropDownList within a div after I add them from the code-behind.
If I add DropDownList to my .aspx page like this:
<div id="FiltersDiv" runat="server">
<asp:DropDownList ID="DropDownListMenu" runat="server"
DataTextField="name" DataValueField="id"
AutoPostBack="true"
onselectedindexchanged="DropDownListMenu_SelectedIndexChanged" >
</asp:DropDownList>
I can get access in any event
protected void AddButton_Click(object sender, EventArgs e)
{
var dl = addProductLocationFiltersDiv.Controls.OfType<DropDownList>();
}
but not if I add from code
var ddl = new DropDownList();
ddl.DataSource = flist;
ddl.DataTextField = "name";
ddl.DataValueField = "id";
ddl.DataBind();
FiltersDiv.Controls.Add(ddl);
I need to add several DropDownList’s and I do not now how many will be. So, I need to looping thru.
Did I miss some attribute or how I need to do it?
When you add a control via code, you access it through the variable name.
If you need access to several of these, consider putting then in a List<DropDownList> (or List<Control> if you have different types of controls).
If you need to share these across methods in your code behind, use a field instead of a local variable.
Note, that due to how the ASP.NET page lifecycle works, you will probably need to recreate these on every postback, otherwise you will not be able to access them. The best location to recreate such controls dynamically is in the OnInit handler.
foreach(DropDownList ddl in FiltersDiv.Controls.OfType<DropDownList>())
{
Trace.Write(ddl.SelectedValue);
}
This is off the top of my head, so you may need to cast the FiltersDiv to a drop down list - As said, you'll need to make sure the controls have been recreated as soon as possible on the page before the control data is written or read.
If you try this too soon, or havn't created the controls you'll get no results.

ASP.net Repeater individual cells

Is it possible to grab the cell data from Repeater items property such as
Dim test As String = myRepeater.Items(index).DataItem(2)
where myRepeater has x rows and 10 columns and was bound from the DataTable
Seems like it should be trivial, but looks like individual cells CAN NOT be accessed. Please shed some light on this.
Not directly -- remember, repeaters are very, very simple webcontrols and the repeater itself really has a limited idea of what is underneath it.
But you can easily get at items within. The best method is to use a control within the item to help you find stuff. EG, given this repeater "row":
<ItemTemplate>
<asp:Button runat="server" ID="theButton" text="Click Me!" OnClick="doIt" /> <asp:TextBox id="theTextBox" value="Whateeavs" />
</ItemTemplate>
You can use the button's click handler to find other items in the row:
protected void doIt(object s, EventArgs e) {
var c = (Control)s;
var tbRef = c.NamingContainer.FindControl("theTextBox");
var tb = (ITextControl)tbRef;
doSomethingWith(tb.Text);
}
Typically much cleaner than finding things in rows using absolute positioning -- you don't have to worry about indexes and such.
PS: it has been a long time since I laid down significant web forms code, no garuntees I got the class names, etc, exactly right.

Creating dynamic DataList controls with ID's based on bound data

As a workaround for the fact that asp:Checkboxes don't have values, I am attempting to dynamically create the ID's of checkboxes in a DataList so that it inserts the primary keys into the control ID. This is surprisingly difficult.
I have placed a PlaceHolder in my DataList ItemTemplate, then in the ItemCreated I create the checkboxes using string.Format("Checkbox{0}", DataBinder(e.Item.DataItem, "ID")). The problem is that this only works in a non-postback condition, as on postback the DataItem is null. And of course ItemDataBound isn't called on PostBack so that won't work either.
I can't seem to find a good way to handle this short of if (IsPostback) dataList.Bind(), which i don't think is a good way to do it.
Can anyone provide me with any alternatives here?
EDIT:
Some additional information. I just realized that part of the problem was because I actually have a DataList within a DataList. The reason DataItem is null is because there is no databinding on postback, and the child data is not saved to viewstate.
Basically, what i'm doing is This, although it's using a DataList rather than Repeater. So, on postback, the Children collection doesn't get set because ItemDataBound isn't called on postback.
EDIT2: To clarify, the problem is largely because of the nested datalists. I have to set the datasource of the nested datalist to a collection field of the first datalist's individual rows fields. On postback, there is no databinding, so it doesn't work.
You could use a similar technique to the one I wrote up in this answer - add a regular CheckBox, and a HiddenField control in the ItemTemplate, and bind the HiddenField to the primary key value e.g.
<ItemTemplate>
<tr>
<td>
<asp:CheckBox runat="server" ID="MyCheckBox" AutoPostBack="true" oncheckedchanged="MyCheckBox_CheckedChanged" />
<asp:HiddenField runat="server" id="DatabaseKeyHiddenField" Value='<%# Eval("DatabaseKey") %>' />
</td>
</tr>
</ItemTemplate>
protected void MyCheckBox_CheckedChanged(object sender, EventArgs e)
{
CheckBox selectedCheckBox;
DataListItem selectedDataListItem;
HiddenField databaseKeyHiddenField;
string databaseKey;
// Cast the sender object to a CheckBox
selectedCheckBox = (CheckBox)sender;
// Walk up the tree one level so we get the container for both controls
selectedDataListItem = (DataListItem)selectedCheckBox.Parent;
// Get the HiddenField control ...
databaseKeyHiddenField = (HiddenField)selectedDataListItem.FindControl("DatabaseKeyHiddenField");
// ... and read the value
databaseKey = databaseKeyHiddenField.Value;
// Go off and do a database update based on the key we now have
...
}
It's a bit of a workaround rather than exactly what you want to do, but it works!

How do I get many property values from View to Presenter in WebFormsMvp?

What is the best way to get a number of property values of a business object from the View to the Presenter in a WebFormsMvp page?
Bearing in mind this issue with DataSources.
Here is what i propose:
The scenario is, I have a business object called Quote which i would like to load form the database, edit and then save. The Quote class has heaps of properties on it. The form is concerned with about 20 of these properties. I have existing methods to load/save a Quote object to/from the database. I now need to wire this all together.
So, in the View_Load handler on my presenter i intend to do something like this:
public void View_Load(object sender, EventArgs e)
{
View.Model.Quote = quoteService.Read(quoteId);
}
And then bind all my controls as follows:
<asp:TextBox ID="TotalPriceTextBox" runat="server"
Text="<%# Model.Quote.TotalPrice %>" />
All good, the data is on the screen.
The user then makes a bunch of changes and hits a "Submit" button. Here is where I'm unsure.
I create a class called QuoteEventArgs exposing the 20 properties the form is able to edit. When the View raises the Submit button's event, I set these properties to the values of the controls in the code behind. Then raise the event for the presenter to respond to. The presenter re-loads the Quote object from the database, sets all the properties and saves it to the database.
Is this the right way to do this? If not, what is?
"A nicer way" (/alternative) is to make use of the 2-way binding, therefore what will be passed back to the Presenter for processing will be your Quote object.
This can be achieved through the use of an asp:FormView in conjunction with the mvp:PageDataSource that specifies an UpdateMethod and the Bind() method.
The WebFormsMVP sample project demonstrates this via the 'EditWidgetControl', including the methods required on the View code-behind file.
As an option your view can simply implement only the EditItemTemplate for asp:FormView making use of DefaultMode="Edit" on the FormView.
Sample Structure:
<asp:FormView DataSourceID="theSource" DefaultMode="Edit">
<EditItemTemplate>
<fieldset>
<asp:TextBox id="totp" value='<%# Bind("TotalPrice") %>' runat="server" />
</fieldset>
</EditItemTemplate>
</asp:FormView>
<mvp:PageDataSource ID="theSource" runat="server"
DataObjectTypeName="Your.NameSpace.Quote"
UpdateMethod="UpdateQuote">
</mvp:PageDataSource>
Code-behind:
public void UpdateQuote(Quote q, Quote ori)
{
OnUpdatingQuote(q, ori);
}
public event EventHandler<UpdateQuoteEventArgs> UpdatingQuote;
private void OnUpdatingQuote(Quote q, Quote ori)
{
if (UpdatingUserGroup != null)
{
UpdatingUserGroup(this, new UpdateQuoteEventArgs(q, ori));
}
}
How to use the GridView inside a FormView.
Because I have list to populate the grid in a entity.

How can I stack an unknown number of ASP.NET Repeaters?

I am building a pop-out menu, and the client wants it to be able to pop out continously based on a heirarchy.
For example, the first pane is a list of options. When they are hovered over, another pane should pop up next to it with the next level of options, and so on until the last level of options is reached.
I can handle all the javascript and stuff, but I can't think of a way to continously embed repeaters inside repeaters. I know I could do it once by putting a repeater inside another, but then I would only have two layers.
I need to be able to continously embed repeaters for each layer of options, or achieve this with a similar technique using a different control.
Any help is great, thanks!
You won't be able to build this in mark up. You'll have to add the controls dynamically in your code behind by building the Repeater for each level and adding it to the previous Repeater's template. It will require a full postback for each option selected because the nested Repeater could potentially be of different depth depending on which option is chosen.
You might be better off doing this all client-side, though, using AJAX and javascript. When an option is chosen, fire off an AJAX request to see if that option has sub-options. If it does (return them), then dynamically build the new options control using javascript and add it to the page. When a different option is chosen, you'll remove the elements from the DOM holding the previously chosen options sub-options.
If you can get your menu out in form of a list of MenuItem objects, each of which has a (sometimes empty) list of sub items (and I really mean a List<MenuItem> here... we're going to use this collection as a datasource for a sub-repeater, so it needs to implement IEnumerable<T>) as a property MenuItem.SubItems, you could probably make use of a UserControl that loops out one menu level, and calls upon itself for the next.
In the UserControl you'd have something like this:
<li><a href='<%= this.MenuItem.Url %>'><%= this.MenuItem.LinkText %></a></li>
<asp:Repeater ID="UCRepeater" runat="server">
<HeaderTemplate>
<ul>
<ItemTemplate>
<menu:MenuItem ID="MenuItemUC" runat="server" />
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
The UserControl in the ItemTemplate is the same one, so for each item template the same thing will be rendered.
Below is the Code Behind for this user control, and this is where the magic happens:
public partial class MenuItemUserControl : UserControl
{
// A property we'll use as the data source
public MenuItem MenuItem { get; set; }
protected void Page_Load(object sender, EventArgs e)
{
// If the current menu item has sub items, we bind the repeater to them
// And by the way, there is no use doing this on every postback. First
// page load is good enough...
if(!Page.IsPostBack) {
{
if(MenuItem.SubItems.Count > 0)
{
UCRepeater.DataSource = MenuItem.SubItems;
UCRepeater.DataBind();
}
}
}
protected void UCRepeater_OnItemDataBound(object sender,
RepeaterDataBoundEventArgs e)
{
// Every time an Item is bound to the repeater, we take the current
// item which will be contained in e.DataItem, and set it as the
// MenuItem on the UserControl
// We only want to do this for the <ItemTemplate> and
// <AlternatingItemTemplate>
if(e.Item.ItemType == ListItemType.Item ||
e.Item.ItemType == ListItemType.AlternatingItem)
{
var uc = (MenuItemUserControl)e.Item.FindControl("MenuItemUC");
if(uc != null)
{
// This is the magic. Abrakadabra!
uc.MenuItem = (MenuItem)e.DataItem;
}
}
}
}
So in order to get this to work, the only thing missing is really a nice way of getting your data out as a hierarchical list of MenuItems. This I'll leave to your data access layer (and it would be cheap easy using LINQ to SQL or Entity Framework... ;) )
DISCLAIMER: This code is provided as is, and I wrote it off the top of my head. I have not tested it, but I think it will work - and if it doesn't, it could at least give you an idea of how to solve the problem. If you have problems, please post them in comments and I'll try to help out - but there are no promises of success here. Just a willingness to help out! =)

Resources