ObjectDataSource reacts to commented-out GridView? - c#

I came across a very strange behavior in asp.net's ObjectDataSource, the description to reproduce is somewhat long, so bear with me while I set the scene.
So, imagine a trivial ObjectDataSource/GridView combo in a User Control. The ObjectDataSource calls a method which returns a List of objects, and the GridView shows these objects in tabular form:
<div runat="server" ID="ControlWrapper">
<asp:GridView ID="GridView1" AutoGenerateColumns="true" DataSourceID="ObjDataSource1" OnRowDataBound="GridView1_RowBound" runat="server">
</asp:GridView>
</div>
<asp:ObjectDataSource ID="ObjDataSource1" runat="server" SelectMethod="GetBundle" OnSelecting="FixDataSource_Selecting" OnSelected="FixDataSource_Selected"
TypeName="West.VitalSigns.Contracts.ProdFixController">
</asp:ObjectDataSource>
This approach will work with pretty much nothing in the code-behind. But let's say that we want to create n number of GridView-s depending on the contents of the database. So we comment out the GridView in the markup...
<div runat="server" ID="ControlWrapper">
<!--
<asp:GridView ID="GridView1" AutoGenerateColumns="true" DataSourceID="ObjDataSource1" OnRowDataBound="GridView1_RowBound" runat="server">
</asp:GridView>
-->
</div>
...and add something like this to the ObjectDataSource's Selected event handler:
protected void FixDataSource_Selected(object sender, ObjectDataSourceStatusEventArgs args)
{
HashSet<string> components = new HashSet<string<()
foreach (ProdFix fix in (List<ProdFix>)args.ReturnValue)
{
if (!components.Contains(fix.Component))
{
GridView v = new GridView();
v.ID=fix.Component.Replace(" " ,"").Replace("-","");
v.AutoGenerateColumns = true;
v.DataSource = args.ReturnValue;
v.RowDataBound +=new GridViewRowEventHandler(BundleGrid_RowBound);
ControlWrapper.Controls.Add(v);
components.Add(fix.Component);
}
}
}
This code works (or at least the un-simplified version works on my machine), so you decide to remove the commented-out section from the markup (don't want that cruft hanging around, after all!)
<div runat="server" ID="ControlWrapper">
</div>
When you do this, however, the code no longer works! The ObjectDataSource won't fire, which means that the Selected event will never happen, which means you won't get your GridView-s. It looks like ObjectDataSource is reacting to commented-out markup in the aspx file?
So, is this:
A bug in ASP.NET?
A non-standard way of dynamically creating GridViews?
A WTF that I shouldn't have tried anyway?
All of the above?

Your gridview control in the markup is not hidden, even with the comments. HTML comments do not apply to server-side tags. Use server side comments instead:
<% /* %> <% */ %>
or
<%-– and -–%>

24hrs later, I noticed that this approach to getting N number of grid-views was pretty silly. Instead of using an ObjectDataSource I refactored my code to just call the GetBundle method directly from Page_Load() and create the GridViews programatically.
cdonner has the answer correct about the server-side comments. I didn't realize that there was a difference.

This should work too, since you're "disabling" the tag with it:
<!--asp:GridView ID="GridView1" AutoGenerateColumns="true" DataSourceID="ObjDataSource1" OnRowDataBound="GridView1_RowBound" runat="server">
</asp:GridView-->

Related

Was the asp.net GridView ever capable of using ViewState?

I'm working on an old site that originated as a Visual Studio Website project, and which I'm converting to a Web Application in addition to other work. One page on the site uses a GridView, and sets the DataSource in code-behind using a DataTable. The GridView exposes a couple of BoundFields, as well as one TemplateField that has a checkbox in it. The GridView is configured to use EnableViewState.
<asp:GridView ID="Results" runat="server" AutoGenerateColumns="False" CellPadding="4" ForeColor="#333333" GridLines="None" EnableViewState="true">
<AlternatingRowStyle BackColor="White" ForeColor="#284775" />
<Columns>
<asp:BoundField DataField="Type" HeaderText="Type"/>
<asp:BoundField DataField="ProcessDate" HeaderText="Process Date" />
<asp:BoundField DataField="Classification" HeaderText="Classification" />
<asp:BoundField DataField="Email" HeaderText="Email" />
<asp:TemplateField HeaderText="Notify?" ItemStyle-HorizontalAlign="Center">
<EditItemTemplate>
<asp:CheckBox ID="CheckBox1" runat="server" onclick="EnableSubmit(this);" />
</EditItemTemplate>
<ItemTemplate>
<asp:CheckBox ID="CheckBox1" runat="server" onclick="EnableSubmit(this);" />
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
The page also has a Submit button, and in the _Click handler, the code attempts to grab Results.Rows, and then digs around in order to determine which rows have their checkboxes checked. It does its subsequent processing based on the checked rows.
The old site code does IsPostBack checking, and only calls DataBind when it's false.
The page doesn't define either EnableViewState or ViewStateMode, which means they're both defaulting to true, according to MSDN.
There's no call to DataBind in a master page or anything.
The old site code does not do any manual persisting to the ViewState dictionary.
Amazingly, the production version of this site actually works.
Since I've been working on this, I've found only threads and articles discussing how GridView doesn't actually use ViewState, and how in order to use data sent to the user, the DataTable used as the DataSource must be manually inserted into ViewState in the code behind, etc.
The version of the site I'm working on has been converted to a Web Application project, and it's targeting .NET 4.5. When I debug the site and break in Page_Load or Submit_Click, the Results GridView has a null DataSource and the Rows property has 0 count. This seems to agree with the current conventional wisdom concerning how GridView "works."
I'm well aware that for what this site is doing, this is a heinous implementation, and there are much better ways to do it. However, what I'm most concerned about is that I can't find any explanation as to how the old version is working.
Did GridView change at some point in its history to ignore its EnableViewState property? What does EnableViewState on GridView actually do? Could it be a difference between Website and Web Application projects?
How can this possibly be working?
UPDATE: Based on Bert Evans' example page, I tried out this modified page.
<%# Page Language="C#" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
public class Thing
{
public int ID { get; set; }
public string Name { get; set; }
}
IEnumerable<Thing> Things
{
get
{
var things = new List<Thing>();
things.Add(new Thing { ID = 1, Name = "One" });
things.Add(new Thing { ID = 2, Name = "Two" });
things.Add(new Thing { ID = 3, Name = "Three" });
things.Add(new Thing { ID = 4, Name = "Four" });
things.Add(new Thing { ID = 5, Name = "Five" });
return things;
}
}
void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
ThingGridView.DataSource = Things;
ThingGridView.DataBind();
}
pageLoadLabel.Text = string.Format("On page Load, row count: '{0}'", ThingGridView.Rows.Count);
}
void OnClickMe(object sender, EventArgs e)
{
onClickLabel.Text = string.Format("On click, row count: '{0}'", ThingGridView.Rows.Count);
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:GridView runat="server" ID="ThingGridView">
<Columns>
<asp:BoundField HeaderText="Name" DataField="Name" />
</Columns>
</asp:GridView>
</div>
<asp:Button runat="server" ID="ThingButton" Text="Click Me!" OnClick="OnClickMe" />
<asp:Label runat="server" ID="onClickLabel" Text="[set on click]" />
<asp:Label runat="server" ID="pageLoadLabel" Text="[set on page load]" />
</form>
</body>
</html>
The only differences are the labels that print out the row count in the page body. When executing this page. the first Page_Load call prints row count 5 to the page; however, in postback, the row count is 0 in both page load and the OnClick method.
Databinding is the process of translating the specified datasource for a control into a control tree that is rendered to the page. For GridViews, essentially, setting the DataSource property and calling the DataBind method translates an IEnumerable, or an IEnumerable extracted from another source like a DataSet or DataTable, into an HTML table with controls containing the data.
Each of the controls rendered into the table cells maintains it's own ViewState, so that when the page is posted back to the server, the GridView rebuilds the control structure, and populates the controls with the data in the ViewState (or rather, the Page initiates the rebuilding of the control structure and the GridView just participates).
The DataSource is not saved to the ViewState, only the state of the rendered controls.
Disabling the ViewState on a GridView prevents it from saving certain elements of it's own state, which will prevent it from being able to do things like pagination and sorting. Furthermore, if you disable the ViewState on the GridView, and you perform a postback (trigger an event on the client side that submits the page back to the server), then the GridView will show nothing when the page is re-rendered, because EnableViewState is inherited and the child controls will be prevented from saving their own state. The only way to get a GridView with a disabled ViewState to show data again after a post back would be to either re bind the data (call DataBind again with a data source that has data) or to have manually enabled ViewState on the child controls contained inside the GridView with the disabled ViewState. You mentioned that the DataBind on the page you are working on is guarded with !IsPostback, so it is only binding on the initial load of the page.
I don't know of anything changing such that the GridView at one point in time saved the DataSource, I believe this is how it always worked.
Thanks to Bert Evans and his example page I eventually isolated the problem. The bug is actually in the remarkably poor MSDN code example for UnityHttpModule, which integrates Unity DI with ASP.NET. UnityHttpModule detailed at https://msdn.microsoft.com/en-us/library/ff664534(v=pandp.50).aspx.
In addition to not actually even compiling as listed, the class wires up the DI code on the InitComplete event, which takes place before ViewState is loaded, as described in this article: ASP.NET 4.0 GridView OnRowEditing events not firing when using Unity 2.0 http module. In my case, moving the DI code to execute on PreLoad fixed my problem.
Finally, for completeness, and for anyone else having problems with the MSDN Unity DI HttpModule, a different SO thread also provides a working example: ASP.NET Dependency Injection HTTP Module (MS Enterprise Library).

Calling function in usercontrol inline code doesn't always work

I have constructed an ASP.NET user control "Box.ascx" wtih the following code.
<div id="divContent" runat="server" visible='<%# AllowedToView(this.Privacy) %>'>
Content
</div>
In the codebehind, "Box.ascx.cs" has the following code.
public string Privacy = string.Empty;
public bool AllowedToView(string privacy)
{
return true;
}
When I use this control in a repeater, AllowedToView() function is hit. If I use this control without a repeater, AllowedToView() function isn't called. I want to know why this weird situation happens and how can I make the control call the AllowedToView() function when used without repeater.
Details are below.
I use this control in a repeater in "Default.aspx".
<asp:Repeater ID="rpRecords" runat="server">
<ItemTemplate>
<uc1:Box ID="myBox" runat="server" RecordID = '<%# Eval("RecordID") %>' />
</ItemTemplate>
</asp:Repeater>
The repeater is databound in "Default.aspx.cs" with the following code:
DataTable dt = FillTable();
rpRecords.DataSource = dt;
rpRecords.DataBind();
I use the "Box.ascx" control in "ShowBox.aspx" with the following code.
<body>
<uc1:Box ID="myBox" runat="server" />
</body>
I give values to the user control from the codebehind with the following code.
protected void Page_Load(object sender, EventArgs e)
{
myBox.RecordID = "1";
}
As mentioned in another answer, the # means it will require databinding to be executed.
So to answer your question "How to make it run outside of the repeater" the simple answer is to call myBox.DataBind().
Your question is very similar to asp.net inline code <%# MyboolVal %>. The problem is that <%= is equal to Response.Write and outputs straight HTML, so it won't work when setting the visible property.
I don't think you need the # but instead = in the ASP tag. Pretty sure # is only for databinding events and that's why it works in a repeater because a repeater performs a databound for rendering.
Check this link: http://blogs.msdn.com/b/dancre/archive/2007/02/13/the-difference-between-lt-and-lt-in-asp-net.aspx
Im no expert on webforms but i think that your problem is that you are trying to databind that method and thats not working for you, try putting it in a <%= AllowedToView(this.Privacy) %>

Getting Xml Columns from LinqDataSource to appear in a GridView

I have a LinqDataSource and a GridView displaying a table. Columns of type xml don't show up. I'd like them to show up something like they do in Sql Server query outputs, with a link to display the clickable xml, though there may be another approach i'm not considering (maybe a styled display of the xml data, etc.). Two things i'd like to know how to do
First, get the xml converted to a string, and display it in the table. Maybe the first 30 chars.
Finally, style the xml into something useful, like a clickable link to display the full xml, or a sub-table, or a styled string.
So the following works, and displays a nice table, with edit and delete links. But Xml fields are missing. How would you go about adding support for the Xml fields?
<form id="form1" runat="server">
<div>
<asp:LinqDataSource ID="OrdersDataSource"
OnContextCreating="LinqDataSource_ContextCreating"
runat="server" ContextTypeName="MyDbDataContext"
EnableUpdate="True" TableName="orders"
EnableDelete="true"
OrderBy="Status, UserId">
</asp:LinqDataSource>
<asp:GridView ID="OrdersGridView" DataSourceID="OrdersDataSource"
CssClass="gridview" PageSize="30" AutoGenerateDeleteButton="true"
AutoGenerateEditButton="true" AllowPaging="True" AllowSorting="True"
AlternatingRowStyle-BackColor="Beige" DataKeyNames="OrderId"
runat="server" Width="705px">
<Columns>
</Columns>
</asp:GridView>
</div>
</form>
Page_Load is empty at the moment.
The best approach would be to use GridView's RowDataBound event. This would look something like this:
protected void OrdersGridView_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
var dataItem = e.Row.DataItem;
...
}
}
I'm not sure what the type of dataItem is, but probably you can cast it to your order type (easiest to see in the debugger, just set a breakpoint in the RowDataBound event handler). You should be able to get your xml data from this object. When you have the xml data, you can convert it to a string and get the first 30 characters, for example.
The last thing to do is set this text in the correct cell. See the example on MSDN for this.

How Do I Get a Dynamic Control's Value after Postback?

I have a listview that adds controls in the ItemDataBound Event. When the postback occurs, I cannot find the new controls. After a bit of research, I found that ASP .NET needs these controls created every time, even after postback. From there I moved the function to bind the ListView outside of the if (!Page.IsPostBack) conditional. Now I get the dynamic controls values but the static controls I have are set to their defaults. Here is a sample of what I am trying to accomplish:
For brevity, I left some obvious things out of this example.
<asp:ListView runat="server" ID="MyList" OnItemDataBound="MyList_ItemDataBound">
<LayoutTemplate>
<asp:PlaceHolder runat="server" ID="itemPlaceholder" />
</LayoutTemplate>
<ItemTemplate>
<asp:PlaceHolder runat="server" ID="ProductPlaceHolder">
<asp:TextBox runat="server" ID="StaticField" Text="DefaultText" />
<asp:PlaceHolder ID="DynamicItems" runat="server" />
</asp:PlaceHolder>
</ItemTemplate>
</asp:ListView>
and here is the codebehind:
protected void MyList_ItemDataBound(object sender, System.Web.UI.WebControls.ListViewItemEventArgs e) {
PlaceHolder DynamicItems = (PlaceHolder)e.Item.FindControl("DynamicItems");
DynamicItems.Controls.Add(textbox);
}
So, like I said, if I only databind when Page != PostBack then I cant find my dynamic controls on postback. If I bind every time the page loads then my static fields get set to their default text.
Try moving the data binding of the ListView into the OnInit() event.
Very similar question (instead of populating a ListView the guy is generating a set of buttons). Briefly, you'll find that you have to store the items in the list in your Viestate - than fish it out on Postback and re-populate the list.
Note that this solutions implies dropping data-binding (which you might not wanna do for others reasons).
Hope it helps.

UpdatePanel and Repeater render page unresponsive after post-back

I have a page with an UpdatePanel that contains a Repeater and a text box with the number of items in the repeater. When I change the value, the page is supposed to post back and redraw the Repeater with the updated number of items. This works in principle, but the page ends up frozen after post-backs and does not accept any input - in IE 8 only. It works perfectly fine in Firefox. For instance, the context menu does not appear when I right-click in controls, and I cannot enter text in text boxes.
When I take out the UpdatePanel, the page works fine, but of course refreshes on every post-back event. This is not necessarily related to the Repeater on the page. I think I am seeing this on other pages. What's the trick here?
<asp:UpdatePanel ID="uPanel" runat="server" UpdateMode="Conditional"
EnableViewState="true" ChildrenAsTriggers="true">
<ContentTemplate>
<asp:Panel ID="Panel1" runat="server" DefaultButton="btnSubmit">
<asp:TextBox ID="tbItems" runat="server" AutoPostback="true"
OnTextChanged="textchanged_Items"/>
<asp:Repeater id="rptItems" runat="server"
OnItemDataBound="repeaterItem_Databound">
<...>
</asp:Repeater>
protected void textchanged_Items(object sender, EventArgs e) {
try {
// this methods rebinds the repeater to a List after changing
// the number of items in the list
ReflowItemRepeater();
// This is not really necessary, since Databind() appears to
// cause an update. I tried it anyways.
uPanel.Update();
}
catch (Exception ex) {
ShowError(this, "Error displaying the item list.", ex, true);
}
}
I ended up removing the update panel.
One month later, different page, I am still and again fighting this. The situation is the same.
An update panel, a repeater (actually 2 nested repeaters), and a control in the repeater that fires a postback event. The server processes the event correctly and returns control, but the browser (IE8) never refreshes the update panel. The page is unresponsive, as if in some sort of dead-lock situation. I can unlock it by clicking on a button that fires another postback event (also in the update panel). But the text boxes in the panel are not clickable or editable when this happens.
Also, it happens only the first time. Once I have "freed up" the lock, or whatever it is, it will not happen again on this page, even when I repeat the exact same steps that led to it.
When this happens, the JIT debugger does not report anything.
I would actually set Triggers within your updatepanel.
I'm not sure you need to call .Update in your code behind as the updatepanel will be updated when the trigger occurs.
Try this:
My gut feeling is that it has something to do with the use of the OnTextChanged event. For kicks, try adding a button next to the text box, and reflow the repeater when the button is clicked instead. Does IE still freeze?
So I stripped this page down to the minimum and I found out what is doing it - the AjaxToolkit:CalendarExtender. If I take it out, everything works fine. Still, I would be curious to know if there is a workaround.
Here is a link to my test page. I will keep it up for a few days.
To see the issue, select "2" from the drop-down, then type something into the first quantity field and tab out. The cursor will blink in the next field, but it does not allow input. This happened in IE8, not in Firefox.
Edit: Actually, when I went back to the full page and removed the CalendarExtender, it was still not working. I do suspect that this issue has to do with controls posting back in the UpdatePanel, but I just can't pin it down. It seems seems to be one of these things where a combination of x things does not work, while any combination of (x-1) things does work.
Regarding the initial question, here's a working sample. I don't know if it's anyhow helpful, but just to make sure...
<%# Page Language="C#" %>
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server"><title>Ajax Test</title></head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager runat="server" />
<asp:UpdatePanel runat="server" ChildrenAsTriggers="true">
<ContentTemplate>
<asp:Label runat="server" AssociatedControlID="txtTest">
Enter 'fruit' or 'vegetables':
</asp:Label>
<asp:TextBox
runat="server" ID="txtTest" AutoPostBack="true"
OnTextChanged="Handler_Test_TextChanged"
/>
<asp:Repeater runat="server" ID="rptItems">
<HeaderTemplate><ul></HeaderTemplate>
<ItemTemplate><li><%# Container.DataItem.ToString() %></li></ItemTemplate>
<FooterTemplate></ul></FooterTemplate>
</asp:Repeater>
</ContentTemplate>
</asp:UpdatePanel>
</form>
</body>
</html>
<script runat="server">
static readonly string[] Fruit = new string[]
{ "Apples", "Oranges", "Bananas", "Pears" };
static readonly string[] Veg = new string[]
{ "Potatoes", "Carrots", "Tomatoes", "Onion" };
void Handler_Test_TextChanged(object s, EventArgs e)
{
if(txtTest.Text == "fruit") rptItems.DataSource = Fruit;
else if(txtTest.Text == "vegetables") rptItems.DataSource = Veg;
else return;
rptItems.DataBind();
}
</script>

Categories