Before anyone answers this, I found the bug in my code, and posted my answer below. I’m leaving this question up (unless someone really cares) as a cautionary tale to always bind your bleeping GridView.
Preface/Background
I’m (ab)using the ASP.NET WebForms controls in an odd way. I know.
I’m working on an application where students are expected to list some school activities they’ve participated in. Students can add several rows, and — just for general workflow reasons — I decided to separate the GridView that displays their “saved” entries from the “details” area where they add/update their activities.
The Edit/Update pieces work beautifully. I can add new items, use baked-in validation that I tied into my classes (which is what all the extra span tags that have class="error" attributes display). I really wanted to make this idiot-proof, so the button they click to add a new activity will also validate and add their information to the GridView (provided the item passes validation), clear the Details controls, and behave as normal.
Problem Description
I started some basic testing on this, and found that if I edit an activity, break one (or more) of the validation rules in that class, and click the “Add New Activity” button, my GridView reverts that row from the EditItemTemplate back to the normal ItemTemplate.
As far as I can tell, my EditIndex never strays from the row I’m still editing. In fact, if I trigger another postback, the GridView shows the correct EditIndex!
I’ve tried re-setting the EditIndex and switched my e.Cancel = true to a return statement. Nothing’s worked. Considering that the EditIndex's value stays constant through multiple postbacks, I have to think there’s something weird going on here.
I’ve posted the markup and C# code below. Both are pretty absurdly long — I refactor the hell out of my functions — so I apologize in advance, and thank anyone brave enough (or bored enough) to offer advice.
Also, I can provide screenshots if anyone would rather see this happening instead of (or in addition to) reading through my markup/code.
GridView
<asp:GridView ID="schoolActivities" AutoGenerateColumns="false" runat="server">
<Columns>
<asp:TemplateField>
<ItemTemplate>
<asp:Button ID="edit" CommandName="Edit" runat="server" Text="Edit" />
</ItemTemplate>
<EditItemTemplate></EditItemTemplate>
</asp:TemplateField>
<asp:TemplateField>
<HeaderTemplate></HeaderTemplate>
<ItemTemplate>
<%# DataBinder.GetPropertyValue(Container.DataItem, "Details") %>
</ItemTemplate>
<EditItemTemplate>Editing...</EditItemTemplate>
</asp:TemplateField>
<asp:TemplateField>
<HeaderTemplate>
Months per Year
</HeaderTemplate>
<ItemTemplate><%# DataBinder.GetPropertyValue(Container.DataItem, "MonthsPerYear") %></ItemTemplate>
<EditItemTemplate></EditItemTemplate>
</asp:TemplateField>
<asp:TemplateField>
<HeaderTemplate>Date</HeaderTemplate>
<ItemTemplate>
<%# DataBinder.GetPropertyValue(Container.DataItem, "ActivityDate") %>
</ItemTemplate>
<EditItemTemplate></EditItemTemplate>
</asp:TemplateField>
<asp:TemplateField>
<HeaderTemplate>
Total Hours Outside Class Time
</HeaderTemplate>
<ItemTemplate>
<%# DataBinder.GetPropertyValue(Container.DataItem, "TotalHours") %>
</ItemTemplate>
<EditItemTemplate></EditItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
“Details” section
<br />
<button type="button" id="newActivity" runat="server">Add New Activity</button>
<br />
<fieldset id="schoolActivityFields" runat="server" visible="false">
<legend>Activity</legend>
<span>Details:</span>
<span>
<textarea id="schoolActivityDetails" class="schoolActivityDetails" runat="server"></textarea>
<br />
<span id="schoolActivityDetailsError" class="error" runat="server"></span>
</span>
<br />
<span>Months per Year:</span>
<span>
<select id="schoolActivityMonthsPerYear" runat="server">
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>6</option>
<option>7</option>
<option>8</option>
<option>9</option>
<option>10</option>
<option>11</option>
<option>12</option>
</select>
</span>
<span id="schoolActivityMonthsPerYearError" class="error" runat="server"></span>
<br />
<span>Date:</span>
<span>
<select id="schoolActivityDate" runat="server">
<option>2005</option>
<option>2006</option>
<option>2007</option>
<option>2008</option>
<option>2009</option>
<option>2010</option>
<option>2011</option>
<option>2012</option>
</select>
</span>
<span id="schoolActivityDateError" class="error" runat="server"></span>
<br />
<span>Total Hours:</span>
<span>
<input type="text" id="schoolActivityTotalHours" runat="server" />
</span>
<span id="schoolActivityTotalHoursError" class="error" runat="server"></span>
<br />
<button type="button" id="addActivity" runat="server">Add Activity</button>
<button type="button" id="updateActivity" runat="server" visible="false">Update Activity</button>
<button type="button" id="cancelEditActivity" runat="server" visible="false">Cancel Editing Activity</button>
<button type="button" id="deleteActivity" runat="server" visible="false">Delete Activity</button>
</fieldset>
The C# is monstrous in length, but for any brave souls:
void addActivity_ServerClick(object sender, EventArgs e)
{
AddSchoolActivity();
}
private bool AddSchoolActivity()
{
ResetSchoolActivityErrors();
SchoolActivity activityToAdd = GetSchoolActivity();
try
{
SessionApplication.SchoolActivities.Add(activityToAdd);
BindSchoolActivities();
ResetSchoolActivityFields();
HideSchoolActivityFields();
}
catch (BrokenRuleException)
{
MapSchoolActivityErrors(activityToAdd);
return false;
}
return true;
}
void updateActivity_ServerClick(object sender, EventArgs e)
{
UpdateSchoolActivity();
}
private bool UpdateSchoolActivity()
{
ResetSchoolActivityErrors();
SchoolActivity activityToUpdate = GetSchoolActivity();
try
{
SessionApplication.SchoolActivities[schoolActivities.EditIndex] = activityToUpdate;
ResetSchoolActivitiesEditIndex();
BindSchoolActivities();
ResetSchoolActivityFields();
HideSchoolActivityFields();
}
catch (BrokenRuleException)
{
MapSchoolActivityErrors(activityToUpdate);
SetSchoolActivitiesEditIndex(schoolActivities.EditIndex);
return false;
}
return true;
}
private SchoolActivity GetSchoolActivity()
{
SchoolActivity currentActivity = new SchoolActivity();
currentActivity.Details = schoolActivityDetails.Value;
TryGetSchoolActivityMonthsPerYear(currentActivity);
TryGetSchoolActivityDate(currentActivity);
TryGetSchoolActivityTotalHours(currentActivity);
return currentActivity;
}
private void TryGetSchoolActivityMonthsPerYear(SchoolActivity currentActivity)
{
byte monthsPerYear;
if (byte.TryParse(schoolActivityMonthsPerYear.Value, out monthsPerYear))
{
currentActivity.MonthsPerYear = monthsPerYear;
}
}
private void TryGetSchoolActivityDate(SchoolActivity currentActivity)
{
short activityDate = -1;
if (short.TryParse(schoolActivityDate.Value, out activityDate))
{
currentActivity.ActivityDate = activityDate;
}
}
private void TryGetSchoolActivityTotalHours(SchoolActivity currentActivity)
{
short totalHours = -1;
if (schoolActivityTotalHours.Value.IsNullOrWhiteSpace() == false
&& short.TryParse(schoolActivityTotalHours.Value, out totalHours))
{
currentActivity.TotalHours = totalHours;
}
}
private void ResetSchoolActivityFields()
{
schoolActivityDetails.Value = string.Empty;
schoolActivityMonthsPerYear.SelectedIndex = 0;
schoolActivityDate.SelectedIndex = 0;
schoolActivityTotalHours.Value = string.Empty;
}
private void HideSchoolActivityFields()
{
schoolActivityFields.Visible = false;
}
private void MapSchoolActivityErrors(int activityIndex)
{
SchoolActivity updatedActivity = SessionApplication.SchoolActivities[activityIndex];
MapSchoolActivityErrors(updatedActivity);
}
private void MapSchoolActivityErrors(SchoolActivity updatedActivity)
{
foreach (RuleViolation currentViolation in updatedActivity.GetRuleViolations())
{
schoolActivityErrorMapping[currentViolation].InnerText = currentViolation.ErrorMessage;
}
}
private void ResetSchoolActivityErrors()
{
foreach (RuleViolation currentViolation in schoolActivityErrorMapping.Keys)
{
schoolActivityErrorMapping[currentViolation].InnerText = string.Empty;
}
}
void schoolActivities_RowEditing(object sender, GridViewEditEventArgs e)
{
if (schoolActivities.EditIndex != e.NewEditIndex
&& schoolActivities.EditIndex != -1)
{
ResetSchoolActivityErrors();
SchoolActivity activityToUpdate = GetSchoolActivity();
try
{
SessionApplication.SchoolActivities[schoolActivities.EditIndex] = activityToUpdate;
}
catch (BrokenRuleException)
{
MapSchoolActivityErrors(activityToUpdate);
e.Cancel = true;
}
}
if (e.NewEditIndex != -1)
{
SchoolActivity activityToEdit = SessionApplication.SchoolActivities[e.NewEditIndex];
SetSchoolActivityFields(activityToEdit);
ShowSchoolActivityFields();
ShowModifyActivityButtons();
}
SetSchoolActivitiesEditIndex(e.NewEditIndex);
BindSchoolActivities();
}
private void ShowModifyActivityButtons()
{
addActivity.Visible = false;
updateActivity.Visible = true;
cancelEditActivity.Visible = true;
deleteActivity.Visible = true;
}
private void SetSchoolActivityFields(SchoolActivity activityToEdit)
{
schoolActivityDetails.Value = activityToEdit.Details;
schoolActivityMonthsPerYear.Value = activityToEdit.MonthsPerYear.ToString();
schoolActivityDate.Value = activityToEdit.ActivityDate.ToString();
schoolActivityTotalHours.Value = activityToEdit.TotalHours.ToString();
}
private bool ShowSchoolActivityFields()
{
return schoolActivityFields.Visible = true;
}
private void BindSchoolActivities()
{
schoolActivities.DataSource = SessionApplication.SchoolActivities;
schoolActivities.DataBind();
}
void newActivity_ServerClick(object sender, EventArgs e)
{
if (SessionApplication.SchoolActivities.Count < SchoolActivityList.Rules.MaxCount)
{
if (schoolActivities.EditIndex != -1)
{
if (UpdateSchoolActivity() == false)
{
return;
}
}
else if (schoolActivityFields.Visible)
{
if (AddSchoolActivity() == false)
{
return;
}
}
ShowSchoolActivityFields();
ShowAddActivityButtons();
}
}
private void ShowAddActivityButtons()
{
addActivity.Visible = true;
updateActivity.Visible = false;
cancelEditActivity.Visible = false;
deleteActivity.Visible = false;
}
void ResetSchoolActivitiesEditIndex()
{
SetSchoolActivitiesEditIndex(-1);
}
void SetSchoolActivitiesEditIndex(int rowIndex)
{
schoolActivities.EditIndex = rowIndex;
}
Sigh. I missed a call to my binding function.
UpdateSchoolActivity()should look like this: