1

I have what may be a rather complicated issue. I have an extended gridview control that I swear used to work all the time, but I went away for a while, came back, and it doesn't work anymore (I'm the sole programmer).

The extended gridview is designed so that it always shows a footer row (for inserting new rows). It loads and displays existing data correctly. If there are no rows, then adding the data works fine. But if I'm adding a new row to a gridview that already has existing rows, I get an issue where gvPhones.FooterRow is null, so it can't find the control I'm referencing.

Here's the extended gridview class (gotten from a stackoverflow page):

using System.Linq;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;

//https://stackoverflow.com/questions/994895/always-show-footertemplate-even-no-data/10891744#10891744
namespace WebForms.LocalCodeLibrary.Controls
{
//modified from https://stackoverflow.com/questions/3437581/show-gridview-footer-on-empty-grid
public class GridViewExtended : GridView
{

    private GridViewRow _footerRow;
    [DefaultValue(false), Category("Appearance"), Description("Include the footer when the table is empty")]
    public bool ShowFooterWhenEmpty { get; set; }

    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), Browsable(false)]
    public override GridViewRow FooterRow
    {
        get
        {
            if ((this._footerRow == null))
            {
                this.EnsureChildControls();
            }
            return this._footerRow;
        }
    }

    protected override int CreateChildControls(System.Collections.IEnumerable dataSource, bool dataBinding)
    {
        //creates all the rows that would normally be created when instantiating the grid
        int returnVal = base.CreateChildControls(dataSource, dataBinding);
        //if no rows were created (i.e. returnVal == 0), and we need to show the footer row, then we need to create and bind the footer row.
        if (returnVal == 0 && this.ShowFooterWhenEmpty)
        {
            Table table = this.Controls.OfType<Table>().First<Table>();
            DataControlField[] dcf = new DataControlField[this.Columns.Count];
            this.Columns.CopyTo(dcf, 0);
            //creates the footer row
            this._footerRow = this.CreateRow(-1, -1, DataControlRowType.Footer, DataControlRowState.Normal, dataBinding, null, dcf, table.Rows, null);
            if (!this.ShowFooter)
            {
                _footerRow.Visible = false;
            }
        }
        return returnVal;
    }

    private GridViewRow CreateRow(int rowIndex, int dataSourceIndex, DataControlRowType rowType, DataControlRowState rowState, bool dataBind, object dataItem, DataControlField[] fields, TableRowCollection rows, PagedDataSource pagedDataSource)
    {
        GridViewRow row = this.CreateRow(rowIndex, dataSourceIndex, rowType, rowState);
        GridViewRowEventArgs e = new GridViewRowEventArgs(row);
        if ((rowType != DataControlRowType.Pager))
        {
            this.InitializeRow(row, fields);
        }
        else
        {
            this.InitializePager(row, fields.Length, pagedDataSource);
        }
        //if the row has data, sets the data item
        if (dataBind)
        {
            row.DataItem = dataItem;
        }
        //Raises the RowCreated event
        this.OnRowCreated(e);
        //adds the row to the gridview's row collection
        rows.Add(row);
        //explicitly binds the data item to the row, including the footer row and raises the RowDataBound event.
        if (dataBind)
        {
            row.DataBind();
            this.OnRowDataBound(e);
            row.DataItem = null;
        }
        return row;
    }

}

}

Here's the relevant stuff in the ASPX page:

<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="ContactEdit.aspx.cs" Inherits="WebForms.Directory.ContactEdit" %>
<%@ Register TagPrefix="gcctl" Namespace="WebForms.LocalCodeLibrary.Controls" Assembly="WebForms" %>
<asp:Content ID="Content1" ContentPlaceHolderID="Head" runat="server">
    <style>
    </style>
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<div id="bodycontent" class="body-content">
    <h2><asp:Literal ID="formheader" runat="server" /></h2>

    <!-- Start: Main Customer section -->
    <asp:Panel ID="mainformcontent" runat="server" CssClass="formsection">
        <section id="mainform" class="simplelayoutform">
            <asp:TextBox id="customerid" type="hidden" runat="server" />
            <asp:TextBox id="contactid" type="hidden" runat="server" />
        </section>
    </asp:Panel>
    <!-- End: Main Customer section -->

    <!-- Start: Phones section -->
    <asp:SqlDataSource ID="gvPhonesDataSource" runat="server" OnInserted="gvPhonesDataSource_Inserted"
        ConnectionString="<%$ ConnectionStrings:ConnString %>" 
        SelectCommand="SELECT p.[CustomerPhoneID]
                    ,p.[CustomerID]
                    ,LTRIM(COALESCE(cc.FirstName,'') + ' ' + COALESCE(cc.LastName,'')) AS ContactFullName
                    ,p.CustomerContactID
                    ,p.PhoneTypeID
                    ,lp.PhoneType
                    ,p.[PhoneNumber]
                    ,p.[Extension]
                    ,p.[FormattedPhone]
                    ,p.[IsActive]
                    ,CASE WHEN p.LocationID IS NULL THEN CASE WHEN p.CustomerContactID IS NULL THEN 0 ELSE 1 END ELSE 2 END AS SortOrder
                FROM [dbo].[Phones] p
                LEFT JOIN dbo.Contacts cc ON p.CustomerContactID = cc.CustomerContactID
                LEFT JOIN list.PhoneTypes lp ON p.PhoneTypeID = lp.PhoneTypeID
                WHERE p.CustomerContactID = @CustomerContactID"
        DeleteCommand="DELETE FROM [dbo].[Phones] WHERE [CustomerPhoneID] = @CustomerPhoneID" 
        InsertCommand="INSERT INTO [dbo].[Phones] ([CustomerID]
                    , [CustomerContactID]
                    , [PhoneNumber]
                    , [Extension]
                    , [PhoneTypeID]
                    , LastModifiedByStaffID) 
                VALUES (@CustomerID
                    , @CustomerContactID
                    , CASE WHEN COALESCE(@FormattedPhone, '')='' THEN NULL ELSE LTRIM(RTRIM(LEFT(dbo.RemoveNonNumeric(@FormattedPhone),10))) END
                    , CASE WHEN COALESCE(@FormattedPhone, '')='' THEN NULL ELSE CASE WHEN LTRIM(RTRIM(SUBSTRING(dbo.RemoveNonNumeric(@FormattedPhone),11,1000))) = '' THEN NULL ELSE LTRIM(RTRIM(SUBSTRING(dbo.RemoveNonNumeric(@FormattedPhone),11,1000))) END END
                    , @PhoneTypeID
                    , @StaffID)" 
        UpdateCommand="UPDATE [dbo].[CustomerPhones] 
                    SET [CustomerContactID] = @CustomerContactID
                    , [PhoneNumber] = CASE WHEN COALESCE(@FormattedPhone, '')='' THEN NULL ELSE LTRIM(RTRIM(LEFT(dbo.RemoveNonNumeric(@FormattedPhone),10))) END
                    , [Extension] = CASE WHEN COALESCE(@FormattedPhone, '')='' THEN NULL ELSE CASE WHEN LTRIM(RTRIM(SUBSTRING(dbo.RemoveNonNumeric(@FormattedPhone),11,1000))) = '' THEN NULL ELSE LTRIM(RTRIM(SUBSTRING(dbo.RemoveNonNumeric(@FormattedPhone),11,1000))) END END
                    , [PhoneTypeID] = @PhoneTypeID
                    , [IsActive] = @IsActive
                    , [DateModified] = getdate()
                    , [LastModifiedByStaffID] = @StaffID
                WHERE [CustomerPhoneID] = @CustomerPhoneID">
        <SelectParameters>
            <asp:ControlParameter Name="CustomerContactID" Type="Int32" ControlID="contactid" PropertyName="Text" />
        </SelectParameters>
        <DeleteParameters>
            <asp:Parameter Name="CustomerPhoneID" Type="Int32" />
        </DeleteParameters>
        <UpdateParameters>
            <asp:ControlParameter Name="CustomerContactID" Type="Int32" ControlID="contactid" PropertyName="Text" />
            <asp:Parameter Name="FormattedPhone" Type="String" />
            <asp:Parameter Name="PhoneTypeID" Type="Int32" />
            <asp:Parameter Name="IsActive" Type="Boolean" />
            <asp:SessionParameter Name="StaffID" Type="Int32" SessionField="StaffID" />
            <asp:Parameter Name="CustomerPhoneID" Type="Int32" />
        </UpdateParameters>
        <InsertParameters>
            <asp:ControlParameter Name="CustomerID" Type="Int32" ControlID="customerid" PropertyName="Text" />
            <asp:ControlParameter Name="CustomerContactID" Type="Int32" ControlID="contactid" PropertyName="Text" />
            <asp:Parameter Name="PhoneTypeID" Type="Int32" />
            <asp:Parameter Name="FormattedPhone" Type="String" />
            <asp:SessionParameter Name="StaffID" Type="Int32" SessionField="StaffID" />
        </InsertParameters>
    </asp:SqlDataSource>

    <asp:Panel ID="phonesformcontent" runat="server" CssClass="formsection separate">
        <section id="phonesform" class="simplelayoutform">
            <h3>All Phones</h3>
            <gcctl:MyCheckBox ID="chkPhoneShowInactive" Text="Show Inactive?" Checked="false" AutoPostBack="true" OnCheckedChanged="chkPhoneShowInactive_CheckedChanged" runat="server" />
            <asp:label id="lblPhoneMessage" CssClass="responsemsg" runat="server" enableviewstate="False" />
            <gcctl:gridviewextended ID="gvPhones" runat="server" DataSourceID="gvPhonesDataSource"
                AutoGenerateColumns="False" DataKeyNames="CustomerPhoneID" EmptyDataText="No phones on record."
                CssClass="searchresultsgrid" ShowFooter="True" OnRowCommand="gvPhones_RowCommand" AllowSorting="True"
                ShowFooterWhenEmpty="true" OnRowDataBound="gvPhones_RowDataBound">
                <Columns>
                    <asp:BoundField DataField="CustomerPhoneID" InsertVisible="false" ReadOnly="true" Visible="False" />

                    <asp:TemplateField HeaderText="Phone Type" SortExpression="PhoneType">
                        <FooterTemplate>
                            <asp:DropDownList ID="cboPhoneTypeID" runat="server"
                                DataSourceID="DataSourcePhoneTypes" DataTextField="PhoneType" DataValueField="PhoneTypeID"
                                SelectedValue='<%# Bind("PhoneTypeID") %>'>
                            </asp:DropDownList>
                        </FooterTemplate>
                        <EditItemTemplate>
                            <asp:DropDownList ID="cboPhoneTypeID" runat="server"
                                DataSourceID="DataSourcePhoneTypes" DataTextField="PhoneType" DataValueField="PhoneTypeID"
                                SelectedValue='<%# Bind("PhoneTypeID") %>'>
                            </asp:DropDownList>
                        </EditItemTemplate>
                        <ItemTemplate>
                            <asp:Label ID="lblPhoneTypeID" runat="server" Text='<%# Bind("PhoneType") %>'></asp:Label>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <asp:TemplateField HeaderText="Phone" SortExpression="PhoneNumber">
                        <FooterTemplate>
                            <asp:TextBox runat="server" Text='<%# Bind("FormattedPhone") %>' ID="txtPhone"></asp:TextBox>
                        </FooterTemplate>
                        <EditItemTemplate>
                            <asp:TextBox runat="server" Text='<%# Bind("FormattedPhone") %>' ID="txtPhone"></asp:TextBox>
                        </EditItemTemplate>
                        <ItemTemplate>
                            <asp:Label runat="server" Text='<%# Bind("FormattedPhone") %>' ID="lblPhone"></asp:Label>
                        </ItemTemplate>
                    </asp:TemplateField>
                    <asp:TemplateField HeaderText="Active?" SortExpression="IsActive">
                        <FooterTemplate>
                            <asp:CheckBox runat="server" Checked='<%# Bind("IsActive") %>' ID="chkPhoneIsActive"></asp:CheckBox>
                        </FooterTemplate>
                        <EditItemTemplate>
                            <asp:CheckBox runat="server" Checked='<%# Bind("IsActive") %>' ID="chkPhoneIsActive"></asp:CheckBox>
                        </EditItemTemplate>
                        <ItemTemplate>
                            <asp:Label runat="server" Text='<%# Bind("IsActive") %>' ID="lblPhoneIsActive"></asp:Label>
                        </ItemTemplate>
                    </asp:TemplateField>

                    <asp:TemplateField ShowHeader="False">
                        <EditItemTemplate>
                            <asp:LinkButton runat="server" Text="Update" CommandName="Update" CausesValidation="True" ID="PhoneUpdate"></asp:LinkButton>&nbsp;<asp:LinkButton runat="server" Text="Cancel" CommandName="Cancel" CausesValidation="False" ID="PhoneEditCancel"></asp:LinkButton>
                        </EditItemTemplate>
                        <ItemTemplate>
                            <asp:LinkButton runat="server" Text="Edit" CommandName="Edit" CausesValidation="False" ID="PhoneEdit"></asp:LinkButton>&nbsp;<asp:LinkButton runat="server" Text="Delete" CommandName="Delete" CausesValidation="False" ID="PhoneDelete"></asp:LinkButton>
                        </ItemTemplate>
                        <FooterTemplate>
                            <asp:LinkButton runat="server" Text="Save New Phone" CommandName="FooterInsert" CausesValidation="True" ID="PhoneInsert"></asp:LinkButton>
                        </FooterTemplate>
                    </asp:TemplateField>
                </Columns>
            </gcctl:gridviewextended>
            <div id="phonenotes" class="tip">
                <div>NUMBERS ONLY - NO LETTER CODES IN THE PHONE FIELD!</div>
                <div>Be sure to always enter the area code, especially if you're also adding an extension.</div>
                <div>Note that only numbers will stay in the "Phone" field. Anything else you enter will disappear once it goes behind the scenes. The first 10 digits will become the phone number, and any remaining digits will become the extension.</div> 
            </div>
        </section>
    </asp:Panel>
    <!-- End: Phones section -->

    <div id="responsetextdiv" class="error"><asp:Literal ID="responsetext" runat="server"></asp:Literal></div>

</div>

<asp:XmlDataSource ID="DataSourcePhoneTypes" runat="server" DataFile="~/XML/PhoneTypes.xml" EnableCaching="true">
</asp:XmlDataSource>

</asp:Content>

Here's the code where I get the error:

protected void gvPhones_RowCommand(object sender, GridViewCommandEventArgs e)
{
    // Insert data if the CommandName == "Insert" 
    // and the validation controls indicate valid data...
    if (e.CommandName == "FooterInsert" && Page.IsValid)
    {
//ERROR HAPPENS ON THE FOLLOWING LINE:
        DropDownList PhoneTypeID = (DropDownList)gvPhones.FooterRow.FindControl("cboPhoneTypeID");
        TextBox FormattedPhone = (TextBox)gvPhones.FooterRow.FindControl("txtPhone");

        gvPhonesDataSource.InsertParameters["PhoneTypeID"].DefaultValue = PhoneTypeID.SelectedValue.ToString();

        string sFormattedPhone = null;
        if (!string.IsNullOrEmpty(FormattedPhone.Text))
            sFormattedPhone = FormattedPhone.Text;
        gvPhonesDataSource.InsertParameters["FormattedPhone"].DefaultValue = sFormattedPhone;

        gvPhonesDataSource.InsertParameters["CustomerID"].DefaultValue = customerid.Text.ToString();
       gvPhonesDataSource.InsertParameters["CustomerContactID"].DefaultValue = contactid.Text.ToString();
        gvPhonesDataSource.InsertParameters["StaffID"].DefaultValue = System.Web.HttpContext.Current.Session["StaffID"].ToString();

        // Insert new record
        gvPhonesDataSource.Insert();
    }
}

The full error I get is:


Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.

Source Error: 


Line 276:            if (e.CommandName == "FooterInsert" && Page.IsValid)
Line 277:            {
Line 278:                DropDownList PhoneTypeID = (DropDownList)gvPhones.FooterRow.FindControl("cboPhoneTypeID");
Line 279:                TextBox FormattedPhone = (TextBox)gvPhones.FooterRow.FindControl("txtPhone");
Line 280:

Source File: <snip>    Line: 278 

Stack Trace: 


[NullReferenceException: Object reference not set to an instance of an object.]
   GCWebForms.Directory.ContactEdit.gvPhones_RowCommand(Object sender, GridViewCommandEventArgs e) in <snip>ContactEdit.aspx.cs:278
   System.Web.UI.WebControls.GridView.OnRowCommand(GridViewCommandEventArgs e) +137
   System.Web.UI.WebControls.GridView.HandleEvent(EventArgs e, Boolean causesValidation, String validationGroup) +95
   System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +49
   System.Web.UI.WebControls.GridViewRow.OnBubbleEvent(Object source, EventArgs e) +146
   System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +49
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +5450

When stepping through (when trying to add a new row to a gridview that already has data in it), I found that gvPhones.FooterRow says that it's null. Again, this only happens if there is data in gvPhones. If the datatable is empty, then the footerrow insert code works without a hitch.

Any help would be greatly appreciated! :-)

EDIT: adding the relevant code behind Page_Load. I just added the DataBind() statement, but it didn't make a difference.

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            bool bolNewRec = (this.iContactID == null);
            phonesformcontent.Visible = (!bolNewRec);

            if (bolNewRec)
            { //snipping unrelated code
            }
            else
            {
                //snipping code that loads the data into the page
                gvPhones.Sort("SortOrder, PhoneType", SortDirection.Ascending);
            }
        }

        if (phonesformcontent.Visible)
            gvPhones.DataBind();
    }

...and, just in case, here's RowDataBound:

    protected void gvPhones_RowDataBound(object sender, GridViewRowEventArgs e)
    {
        if (e.Row.RowType == DataControlRowType.DataRow)
        {
            DataRowView rowView = (DataRowView)e.Row.DataItem;
            bool bolShowInactive = chkPhoneShowInactive.Checked;
            if (!bolShowInactive && (Convert.ToBoolean(rowView["IsActive"]) == false))
                e.Row.Visible = false;
            else
                e.Row.Visible = true;
            rowView = null;
        }

        if (e.Row.RowType == DataControlRowType.Footer)
        {
            CheckBox chkIsActive = (CheckBox)e.Row.FindControl("chkPhoneIsActive");
            chkIsActive.Checked = true;
            chkIsActive = null;
        }
    }
Katerine459
  • 465
  • 1
  • 3
  • 13

2 Answers2

1

Try using the sender in your code as below:

Replace this line:

DropDownList PhoneTypeID = (DropDownList)gvPhones.FooterRow.FindControl("cboPhoneTypeID");

For this:

DropDownList PhoneTypeID = (DropDownList)((GridView)sender).FooterRow.FindControl("cboPhoneTypeID");

Also, check the page load if the problem is not with the postback.

My answer is based on this question: Unable to get gridview footer values in RowCommand

UPDATE:

Change your GridViewExtended class,

ShowFooterWhenEmpty property:

    [Category("Behavior")]
    [Themeable(true)]
    [Bindable(BindableSupport.No)]
    public bool ShowFooterWhenEmpty
    {
        get
        {
            if (this.ViewState["ShowFooterWhenEmpty"] == null)
            {
                this.ViewState["ShowFooterWhenEmpty"] = false;
            }

            return (bool)this.ViewState["ShowFooterWhenEmpty"];
        }
        set
        {
            this.ViewState["ShowFooterWhenEmpty"] = value;
        }
    }

GridViewRow:

    private GridViewRow _footerRow;
    public override GridViewRow FooterRow
    {
        get
        {
            GridViewRow f = base.FooterRow;
            if (f != null)
                return f;
            else
                return _footerRow;
        }
    }

I based my changes on this link: Always show FooterTemplate, even no data

erickpa
  • 90
  • 4
  • Thanks. :) Unfortunately, the code change didn't work, and the link you provided has the following instruction "If you are binding your gridview on page load then you must have to put check on page post back. So if page is not post back then bind grid only. That may be the case as well. " --- I don't know how to do that when the gridview is bound to an sqldatasource control within the aspx page itself, not in the PageLoad code. Any instructions? – Katerine459 Mar 17 '18 at 01:14
  • I've added the Page_Load code. I've tried updating the code rows to use (Gridview)sender, but it hasn't made a difference. Any other suggestions? – Katerine459 Mar 19 '18 at 19:39
  • Sorry for the delay, I'm trying to do some testing to reproduce the problem. I think the problem is in one of two methods: CreateChildControls or EnsureChildControls. – erickpa Mar 20 '18 at 13:15
  • Try to add "gvPhones.DataBind();" inside the (!IsPostBack) block. – erickpa Mar 20 '18 at 18:03
  • Many thanks for your help. :) Re: your second comment: that's where it was originally (just before the "gvPhones.Sort" line), but I moved it yesterday because I thought it might help (didn't make a difference). Q: some of the stuff in the Sort command is outdated... could that be related? – Katerine459 Mar 20 '18 at 18:57
  • Update: I've put the "gvPhones.DataBind();" command back where it used to be, and also commented out the "gvPhones.Sort" command. No change. – Katerine459 Mar 20 '18 at 19:14
  • Hi, try following the suggested step-by-step on this link to use the gridview footer to insert the records: http://www.dotnetfunda.com/articles/show/2741/working-with-footer-template-in-gridview – erickpa Mar 21 '18 at 20:46
  • Thanks again. :) Ok, I've rewritten the code to use a standard gridview and a datasource that's set in the cs page, as it's done in those instructions. The result is that the add functionality now works, but the edit/update/delete/cancel functionalities are now completely broken. I'd really like to keep using the gridviewextended class instead, as it simplifies the code a great deal, and I'm hoping maybe this news helps with troubleshooting? – Katerine459 Mar 22 '18 at 16:40
  • I think I've found a solution. Remove your "public override GridViewRow FooterRow" in GridViewExtended.cs Leave only the "private GridViewRow _footerRow;" I did a test here simulating the gridview with its extension and it worked correctly. – erickpa Mar 23 '18 at 17:22
  • Sorry about the delay in responding. Thanks for this. It worked... almost. Basically, it reversed the problem. Now the error doesn't happen if there are existing rows in the Gridview, but it does happen if this is the first row to be added (the entire point of GridViewExtended is to allow adding via the footer row even when there are no existing rows). I'm beginning to think the best option is to scrap the existing class and make one that just hides a row that I manually create via UNION SELECT, so there's just always at least one row. – Katerine459 Mar 27 '18 at 23:47
  • Thank you for all of your help. Sorry I didn't upvote you before (I have now :) ). FYI, I've decided that maybe it's better to go with the "always have at least one row in the gridview and then just hide the extra row" route, to make sure there's always a footer row, and scrap this entire class. I would still like to have a class that does the hiding automatically, and I've asked that question here: https://stackoverflow.com/questions/49524212/c-sharp-gridview-want-to-create-an-extended-gridview-class-that-automatically-h – Katerine459 Mar 28 '18 at 17:44
  • I appreciate it :). I'll try to help you with this problem. – erickpa Mar 28 '18 at 18:54
  • I made some changes in the code and it now ran in both scenarios. I updated my answer with the changes, please do a test and check if it is working now. – erickpa Mar 28 '18 at 20:49
0

I wound up scrapping this entire class. Instead, I made regular asp:gridviews that are based on datasources that have union selects with one row with -1 in the key column (since all of my tables have single autoincrement PKs, no row will legitimately have -1 in the key column), and then put the following in RowDataBound:

if (e.Row.RowType == DataControlRowType.DataRow)
{
    DataRowView rowView = (DataRowView)e.Row.DataItem;
    string sKeyName = gvPhones.DataKeyNames[0].ToString();
    if ((rowView[sKeyName].ToString() == "-1"))
        e.Row.Visible = false;
    else
        e.Row.Visible = true;
    rowView = null;
}

This hides any row with -1 in the key column. So there's always at least one row in the gridview (even if that one row is hidden), and the footer row always shows.

Katerine459
  • 465
  • 1
  • 3
  • 13