Search code examples
asp.netaspxgridview

TextBox OnTextChanged Event inside Gridview Template Field Loses Next Field Focus After Update


I have a GridView containing TextBox in <asp:TemplateField /> and The GridView is residing inside an AJAX Update Panel. Using TextChanged Event for the textbox inside the GridView and the event fires when user enters something in the Templatefield. Upto that it is okay but the problem is that once I exit the Textbox after the update the cursor losses the focus instead of moving to the next control.

When I press the tab again it Starts from the first row first cell of the template field. How to handle this? How to make the cursor to move to the next field instead of losing the current row focus.

ASPX Code

<asp:TemplateField HeaderText="Amount"
    HeaderStyle-Width="255"  ItemStyle-Width="255">
    <ItemTemplate>
        <asp:TextBox ID="txtAmount" runat="server" OnTextChanged = "OnTextChanged" AutoPostBack = "true"
            Visible="TRUE" Height="30">
        </asp:TextBox>
    </ItemTemplate>
</asp:TemplateField>

<asp:TemplateField HeaderText="Qty"
    HeaderStyle-Width="255"  ItemStyle-Width="255">
    <ItemTemplate>
        <asp:TextBox ID="txtQty" runat="server" OnTextChanged = "OnTextChanged" AutoPostBack = "true"
            Visible="TRUE" Height="30">
        </asp:TextBox>
    </ItemTemplate>
</asp:TemplateField>

etc.,

Code Behind

Protected Sub OnTextChanged(sender As Object, e As EventArgs)
    Dim textBox As TextBox = CType(sender, TextBox)
    Dim id As String = textBox.ID

    UpdateTotalValue(Val(textBox.Text))
End Sub

Solution

  • This can be difficult, since if you re-binding the gv, then you causing a post back. Even with introduction of a update panel, then we still get/have what is called a partial post back. That means the page class is re-created, page load does fire again, and your code behind runs. so, while this "looks" like no post-back occurs, it is in a way kind of fooling you. While a update panel does/can solve MANY browser re-plot issues, do keep in mind that UP's are still a post-back, just a limited kind of post-back. (so page life cycle and round tripping does occur - just more limited, but not "zero").

    In fact, the correct term is a "partial post back".

    You don't show what UpdateTotalValue code does, but if it re-binds the GV, then you have a gv that is being re-loaded.

    If your onchange/update code ONLY changes the current row, and then maybe re-calc the final sum total, but does not re-bind gv, then this could work better.

    if not, then we might have to introduce ajax, or some client side js code.

    I guess, much depends on what UpdateTotal routine does.

    if you can run/get the gv total, shove in say some total value (asuming at botton of GV, and not re-bind the gv, then you probably ok).

    However, if you re-bind the gv, then we may well have to try/attempt to put in some code that sets the focus, but such code tends to be a bit "hacky".

    Edit: So, GV is not being re-bound

    there are 2 approaches I have used with success.

    The easy:

    What I do is execute a control.SetFocus on the text box. What this means is the user enters a value, hits tab, and everything udpates, but the focus returns to the text box. In effect, if the user is tabbing around in the GV, then tab works - (almost like jumping around in a excel sheet). But, if they enter/change that value, then you find you have to hit tab 2 times. Often, users don't care, since they are only changing say the one value, and then move on. However, say a mouse click to a text box a few rows below - they will have to click two times.

    I suggest the above, since it is the least effort and near zero code changes to what you have.

    However, after time and messing around trying various approaches?

    I have found using some client side JavaScript results in a much better user experience and thus tabbing is not messed up. And overall, the total amount of code is the about the same, or maybe even a wee bit less then server side code. However, the down side is we now having to write JavaScript code, and that can be "painfull", or at the very least more difficult then your posted simple + clean + easy server side code.

    I'll later today post both samples as a vb.net gv. (set focus, and 100% client JavaScript. With the JavaScript, then that means we can dump the update panel. (not a huge deal, but it does reduce page post-backs, and the user experience is much better).

    Edit2: Set focus, 100% server side code.

    Ok, so what we can do is after we run the code, we can re-set the focus of the control.

    In this example, we have 2 columns (Number of Nights X Price) for a hotel room list.

    So, 2 columns need to "update" the math for the CURRENT row, and then of course update the total at the bottom of the GV.

    So, we have this markup:

    <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="ID" 
        ShowFooter="True" CssClass="table">
        <Columns>
            <asp:BoundField DataField="FirstName" HeaderText="FirstName"  />
            <asp:BoundField DataField="LastName" HeaderText="LastName"  />
            <asp:BoundField DataField="HotelName" HeaderText="HotelName"  />
            <asp:BoundField DataField="Description" HeaderText="Description"  />
    
            <asp:TemplateField HeaderText="Nights">
                <ItemTemplate>
                    <asp:TextBox ID="txtNights" runat="server" Text='<%# Eval("Nights") %>' Width="50px"
                        OnTextChanged="txtNights_TextChanged" AutoPostBack="true"
                        style="text-align:right">
                    </asp:TextBox>
                </ItemTemplate>
            </asp:TemplateField>
            <asp:TemplateField HeaderText="Price" SortExpression="Price">
                <ItemTemplate>
                    <asp:TextBox ID="txtPrice" runat="server" Text='<%# Eval("Price", "{0:f2}") %>' Width="70px"
                        style="text-align:right"
                        OnTextChanged="txtPrice_TextChanged"  AutoPostBack="true"
                            ></asp:TextBox>
                </ItemTemplate>
            </asp:TemplateField>
            <asp:TemplateField HeaderText="Tamount" 
                ItemStyle-HorizontalAlign="Right"  FooterStyle-HorizontalAlign="Right" >
                <ItemTemplate>
                    <asp:Label ID="lblAmount" runat="server" Text='<%# Eval("Tamount", "{0:f2}") %>'></asp:Label>
                </ItemTemplate>
                    <FooterTemplate >
                    <asp:Label ID="LblTotal" runat="server" Text="0"></asp:Label>
                </FooterTemplate>                        
            </asp:TemplateField>                
        </Columns>                
    </asp:GridView>
    

    And code to load is this:

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        If Not IsPostBack Then
            LoadGrid()
        End If
    End Sub
    
    Sub LoadGrid()
    
        Using conn As New SqlConnection(My.Settings.TEST4)
            Using cmdSQL As New SqlCommand(
                "SELECT * FROM tblHotelsA ORDER BY HOtelName", conn)
                conn.Open()
                Dim rstData As New DataTable
                rstData.Load(cmdSQL.ExecuteReader)
                GridView1.DataSource = rstData
                GridView1.DataBind()
                SumRows()
            End Using
        End Using
    
    End Sub
    

    And we have one routine to show sum at botton

    This:

    Sub SumRows()
        ' sum value - set footer total value
        Dim MyTotal As Double
    
        For Each gRow As GridViewRow In GridView1.Rows
            Dim lblAmount As Label = gRow.FindControl("lblAmount")
            MyTotal += Nz(lblAmount.Text, 0)
        Next
    
        Dim lblTotal As Label = GridView1.FooterRow.FindControl("lblTotal")
        lblTotal.Text = MyTotal.ToString("C2")
    
    End Sub
    

    Ok, and now we see/have this:

    I will tab past the first row (or so), then change a value, note the cursor does stay in place.

    So this:

    enter image description here

    So, the trick simple involves using control.SetFocus.

    So, if I change Nights then I set focus to Price.

    if I change price, I set focus to NEXT row and Nights.

    So, this code:

    Protected Sub txtNights_TextChanged(sender As Object, e As EventArgs)
    
        Dim tQty As TextBox = sender
        Dim gRow As GridViewRow = tQty.NamingContainer
        Call RowCalc(gRow)
    
        ' we were in Qty control - set focus to next (price) control
        Dim tPrice As TextBox = gRow.FindControl("txtPrice")
        tPrice.Focus()
    
        SumRows()
    
    
    End Sub
    
    Protected Sub txtPrice_TextChanged(sender As Object, e As EventArgs)
    
        Dim tPrice As TextBox = sender
        Dim gRow As GridViewRow = tPrice.NamingContainer
        Call RowCalc(gRow)
        SumRows()
    
        ' we in Price - we jump to next row below (or cycle to first row)
    
        Dim gRowNext As GridViewRow = Nothing
        If gRow.RowIndex + 1 < GridView1.Rows.Count Then
            gRowNext = GridView1.Rows(gRow.RowIndex + 1)
        Else
            ' jump to top row
            gRowNext = GridView1.Rows(0)
        End If
        Dim tQty As TextBox = gRowNext.FindControl("txtNights")
        tQty.Focus()
    
    End Sub
    

    Since the row calculation is for the 2 values, then I broke that out into a routine called row calc (so both text box can call that one same routine).

    this one:

    Sub RowCalc(gRow As GridViewRow)
    
        Dim tQty As TextBox = gRow.FindControl("txtNights")
        Dim tPrice As TextBox = gRow.FindControl("txtPrice")
        Dim lblAmount As Label = gRow.FindControl("lblAmount")
    
        lblAmount.Text = Nz(tQty.Text, 0) * Nz(tPrice.Text, 0)
    
    End Sub
    

    so, it not perfect, but it not all that bad If a user changes a value, but THEN in place of tab decides to use the mouse, then they will in most cases have to click two times, since lost focus will trigger our calc code, and then set focus (forcing the user to click again if using mouse to click on a row).

    however, it above works not too bad.

    I going out for a break, but when I come back, we can try/see the above same example with JavaScript. the js example is somewhat nicer, since then the focus and click (mouse) does not get messed up, since we not using a post-back at all.