SQL Server ASP.NET : show progress of a stored procedure

p5fdfcr1  于 2023-08-02  发布在  .NET
关注(0)|答案(2)|浏览(72)

In my ASP.NET web application I want to show to the end user the progress of the stored procedure.

The stored procedure computes a batch of calculations depending on how many the user added to the task basket.

It works like this:

  • the user after having selected the tasks clicks the button on the page
  • on server side tasks are added to a table in SQL Server
  • then a stored procedure is executed in SQL Server which calculates tasks one by one

Something like this:

Protected Sub Button4_Click(sender As Object, e As EventArgs) Handles Button4.Click

SaveTasks()
sqlWorker.SelectCommand = "EXECUTE dbo.spCalc " & hidBatchValue
    sqlWorker.Select(DataSourceSelectArguments.Empty) 'sqldatasource

End Sub

The progress would be very easy to get from the server using another query.

The problem is that the page is waiting for the stored procedure to be ended.

I tried to use a timer on the update panel but it doesn't work until the stored procedure has completed.

I also tried async (marked the page async, made the button call also async), but it still waits for the stored procedure to complete.

Public Async Function GetDataSetAsync(sconn As String, ssql As String) As Task(Of Integer)
  
    Dim newconnection As New SqlClient.SqlConnection(sconn)
    Dim mycommand = New SqlClient.SqlCommand(ssql, newconnection)

    mycommand.CommandType = CommandType.Text
    Await newconnection.OpenAsync().ConfigureAwait(False)

    Return Await mycommand.ExecuteNonQueryAsync().ConfigureAwait(False)

End Function

I know that I am using very simple measures for my web app so I would appreciate you advise on how to solve the problem.

9ceoxa92

9ceoxa921#

OK, let's break this up a bit.

As you note, it's kind of easy to setup some table to "process" the routines. The challenge part is getting this information BACK to the client-side browser.
The progress would be very easy to get from the server using another query.

Hum, not really!

However, I would take your "table" idea, and run with that idea.

However, while we can't really show the progress for EACH stored procedure, we can certainly with ease show the progress of EACH stored procedure being called.

In other words, say the user picks 5 options (out of 10) to run?

Then we can certainly have code to call each stored procedure, and we can certainly update our progress as each stored procedure is done.

In other words, for ease of design, and ESPECIALLY our desired ability to update the client-side browser?

I suggest we move that table of "processing" to be done to the application side and NOT use SQL server for that task list.

I suppose it depends, and if the SQL stored procedures take a huge amount of time, then this approach I am suggesting here probably not the best. But, since you ALREADY asking this question, then I have to reasonably assume the time taken for each stored procedure call is not too large anyway, right?

And since we need/want the user to select the list of things to process, we might as well get double duty out of that table! (By double duty, let's use that table for the list of things to do, but ALSO for user selection, and ALSO for the progress of how far we are along! So, now we killing multiple birds with one stone so to speak).

So, our simple table:
| ID | ProcNameRun | Description |
| ------------ | ------------ | ------------ |
| 1 | Proc1 | Give dog bowel of water |
| 2 | Proc2 | Put collar on dog |
| 3 | Proc3 | Open door to go outside |
| 4 | Proc4 | Take dog for walk |

OK so now our grid.

And we don't actually have to wrap all this in an update panel, but let’s do so.

So, our grid view:

<asp:UpdatePanel ID="UpdatePanel1" runat="server">
            <ContentTemplate>

                <h3>Select Items to Process</h3>
                <asp:GridView ID="GridView1" runat="server"
                    DataKeyNames="ID"
                    AutoGenerateColumns="false"
                    CssClass="table" Width="40%">
                    <Columns>
                        <asp:BoundField DataField="ProcNameRun" HeaderText="Procedure" />
                        <asp:BoundField DataField="Description" HeaderText="Task Description" />
                        <asp:TemplateField HeaderText="Select to Process"
                            HeaderStyle-HorizontalAlign="Center"
                            ItemStyle-HorizontalAlign="Center">
                            <ItemTemplate>
                                <asp:CheckBox ID="chkSel" runat="server" />
                            </ItemTemplate>
                        </asp:TemplateField>
                        <asp:TemplateField HeaderText="Status"
                            HeaderStyle-HorizontalAlign="Center"
                            ItemStyle-HorizontalAlign="Center">
                            <ItemTemplate>
                                <asp:Label ID="lblStatus" runat="server" Width="150px" Font-Bold="true">
                                </asp:Label>
                            </ItemTemplate>
                        </asp:TemplateField>
                    </Columns>
                </asp:GridView>
                <asp:Button ID="runOne" runat="server" Text="runone"
                    ClientIDMode="Static"
                    OnClick="runOne_Click"
                    Style="display: none" />

                <asp:HiddenField ID="ProcNum" runat="server" ClientIDMode="Static" />

                <div id="startarea" runat="server">
                    <div style="float: left">
                        <asp:Button ID="cmdStart" runat="server" Text="Start the Reactor!"
                            CssClass="btn btn-lg"
                            OnClick="cmdStart_Click" />
                    </div>
                    <div style="float: left; margin-left: 20px">
                        <asp:Label ID="lblbutMsg" runat="server" Text=""
                            Style="color: red; font: bold">
                        </asp:Label>
                    </div>
                </div>
                <div id="processarea" runat="server" style="clear: both; float: left; display: none">
                    <asp:Label ID="Label1" runat="server" Font-Size="Large" Text="Processing..."></asp:Label>
                    <img src="../Content/wait2.gif" style="width: 16px; height: 16px" />
                    <br />

                    <asp:Label ID="lblStep" runat="server" Text="Step"
                        Font-Size="Larger" Font-Bold="true"></asp:Label>
                </div>
                </div>
            </ContentTemplate>
        </asp:UpdatePanel>

Below the Grid, we have a button area (to start processing), and a simple label for a message (say when user does not select anything).

And right after that, we have a "processing" area that shows what step we are on, and a "spinner gif".

And right after the update panel we have a wee bit of JavaScript code. (I also assumed jQuery here).

So, we have this:

<script>
        var prm = Sys.WebForms.PageRequestManager.getInstance();
        prm.add_endRequest(myrunone);

        function myrunone() {
            // run one step of processing
            var MyStep = $('#ProcNum').val()

            if (MyStep >= 0) {
                $('#runOne').click()
            }
        }
    </script>

Now, all the above script does? Well, an update panel does not trigger "page ready client side, so the above does quite much the same thing, but in fact triggers our update panel to run again. (Any button clicked inside of an update panel is a post-back for that panel.

So, now the code:

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

    If Not IsPostBack Then
        ProcNum.Value = "-1"
        LoadGrid()
    End If

End Sub

Sub LoadGrid()

    GridView1.DataSource =
        MyRst("SELECT * FROM tblProcess ORDER BY ID")
    GridView1.DataBind()

End Sub

That loads up our grid, and we now have this:

(And to be fair, one might hide/not show the actual SQL stored procedure that we going to call - I included that for this demo. (Probably a good idea to hide that one column, and doing so with a visible = false will NOT render that content client side (don't want users changing the stored procedure name!).

So now the button click code (start the reactor).

Protected Sub cmdStart_Click(sender As Object, e As EventArgs)

    Dim sProcID As String = ""
    For Each gRow As GridViewRow In GridView1.Rows
        Dim ckBox As CheckBox = gRow.FindControl("chkSel")
        If ckBox.Checked Then
            If sProcID <> "" Then sProcID &= ","
            sProcID &= gRow.RowIndex
        End If
    Next

    ProcNum.Value = "-1"
    If sProcID = "" Then
        ' no process selected, give message
        lblbutMsg.Text = "No Process selected<br/>Please select at least one Process"
        Return
    Else
        lblbutMsg.Text = ""
    End If

    ' setup the process loop
    ProcNum.Value = "0"
    ViewState("ProcList") = Split(sProcID, ",")
    startarea.Style.Add("display", "none")      ' hide start button area
    processarea.Style.Add("display", "inline")  ' show procesisng area

    Call ProcessInfo()

End Sub

So, in above, we gather up the selected rows, create an array of row "id", and then save that into view state.

So, when you click start, the update panel updates, and then the client-side JavaScript code triggers the button click, and we now run code behind. (And the button not clicked from client side if process number is < 0 (i.e.: -1).

A routine to setup the information, highlight the one grid row.

Sub ProcessInfo()
    ' get the grid row
    Dim pList() As String = ViewState("ProcList")
    Dim gIndex As Integer = pList(ProcNum.Value)
    Dim gRow As GridViewRow = GridView1.Rows(gIndex)

    ' turn grid row to light blue
    gRow.Style.Add("background-color", "lightsteelblue")
    Dim lblStatus As Label = gRow.FindControl("lblStatus")
    lblStatus.Text = "Processing..."    ' update row status
    lblStep.Text = gRow.Cells(1).Text   ' update process area text

End Sub

And now of course our routine to process (call) the one stored procedure.

(This routine is called by the client-side JavaScript click).

Protected Sub runOne_Click(sender As Object, e As EventArgs)

    Dim pList() As String = ViewState("ProcList")
    Dim gIndex As Integer = pList(ProcNum.Value)
    Dim gRow As GridViewRow = GridView1.Rows(gIndex)
    Dim sProcTorun As String = gRow.Cells(0).Text

    Dim cmdSQL As New SqlCommand(sProcTorun)
    cmdSQL.CommandType = CommandType.StoredProcedure
    MyRstPE(cmdSQL)

    ' one SQL process done, 
    gRow.Style.Add("background-color", "white")
    Dim lblStatus As Label = gRow.FindControl("lblStatus")
    lblStatus.Text = "Complete!"
    Dim chkSel As CheckBox = gRow.FindControl("chkSel")
    chkSel.Checked = False
    ProcNum.Value += 1

    If ProcNum.Value < pList.Length Then
        ProcessInfo()   ' we have more processing to do, setup 
    Else
        ProcNum.Value = "-1"
        startarea.Style.Add("display", "inline")    ' show start button area
        processarea.Style.Add("display", "none")    ' hide procesisng area
    End If

End Sub

And that's about it.

It's not really a lot of code, and no worse than trying to setup a complex asynchronous system.

So, the end result is now this:

So, I can't think of a code solution that would be much smaller here.

Most of that code is for UI stuff, and then included in that loop is the calling of the stored procedures.

For completeness, I used two of my helper routines. I'm sure everyone has such routines to save one wearing out keyboards for simple SQL statements, or calling some stored procedure.

Public Sub MyRstPE(cmdSQL As SqlCommand)
    Dim rstData As New DataTable
    Using conn As New SqlConnection(My.Settings.TEST5)
        Using (cmdSQL)
            cmdSQL.Connection = conn
            conn.Open()
            cmdSQL.ExecuteNonQuery()
        End Using
    End Using
End Sub

Public Function MyRst(strSQL As String) As DataTable
    Dim rstData As New DataTable
    Using conn As New SqlConnection(My.Settings.TEST5)
        Using cmdSQL As New SqlCommand(strSQL, conn)
            conn.Open()
            rstData.Load(cmdSQL.ExecuteReader)
            rstData.TableName = strSQL
        End Using
    End Using
    Return rstData
End Function

So, by moving each of the stored procedure calls to the code behind then we can rather easy track and show a progress of each routine. This approach does not require a timer system, no polling, nor any ajax calls.

So, each process will take as long as the stored procedure call.

My test/example stored proc was this:

CREATE PROCEDURE [dbo].[Proc1] 
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;
    WAITFOR DELAY '00:00:03'
END

Edit: Have browser scroll current process row into view

Ok, we can add this markup. (a hidden control in which we set the current label in the row "client id".

So, add this to the markup (right after the gridview).

<asp:HiddenField ID="ProcNum" runat="server" 
   ClientIDMode="Static" />

and then change the script to this:

<script>
        var prm = Sys.WebForms.PageRequestManager.getInstance();
        prm.add_endRequest(myrunone);

        function myrunone() {
            // run one step of processing
            var MyStep = $('#ProcNum').val()
            var MyCtrl = $('#HRowSel')
            if (MyStep >= 0) {
                var btn = $('#runOne')
                var scrollCtrl = $(MyCtrl.val())
                // alert(MyCtrl)
                btn.click()
                scrollCtrl.get(0).scrollIntoView();
                window.scrollBy(0, -60); 
            }
        }
    </script>

And in code behind, then we add this:

Protected Sub runOne_Click(sender As Object, e As EventArgs)

    Dim pList() As String = ViewState("ProcList")
    Dim gIndex As Integer = pList(ProcNum.Value)
    Dim gRow As GridViewRow = GridView1.Rows(gIndex)
    Dim sProcTorun As String = gRow.Cells(0).Text

    'Dim cmdSQL As New SqlCommand(sProcTorun)
    'cmdSQL.CommandType = CommandType.StoredProcedure
    'MyRstPE(cmdSQL)

    System.Threading.Thread.Sleep(1000)

    ' one SQL process done, 
    gRow.Style.Add("background-color", "white")
    Dim lblStatus As Label = gRow.FindControl("lblStatus")
    lblStatus.Text = "Complete!"
    Dim chkSel As CheckBox = gRow.FindControl("chkSel")
    chkSel.Checked = False
    ProcNum.Value += 1

    HRowSel.Value = lblStatus.ClientID  ' <--- add this

    If ProcNum.Value < pList.Length Then

        ProcessInfo()   ' we have more processing to do, setup 
    Else
        ProcNum.Value = "-1"
        startarea.Style.Add("display", "inline")    ' show start button area
        processarea.Style.Add("display", "none")    ' hide processing area
    End If

End Sub
im9ewurl

im9ewurl2#

There's a ton of different ways you can approach this. One option would be to explore using threads - https://learn.microsoft.com/en-us/dotnet/standard/threading/using-threads-and-threading provides some documentation on this. Threads are not a simple tool, though, and it's very easy to mess your system up if you aren't careful with how you use them.

Another option is to use SQL Server Service Broker to make the requests process in an asynchronous manner on the server itself. I'll warn you that service broker is not a simple tool to use. It is, however, quite powerful. If you want to explore it, the MSDN page is here - https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-service-broker?view=sql-server-ver16 . I'd suggest familiarizing yourself with the basics, and if you have any specific questions, you can ask those after.

Yet another option would be to have a separate application handle processing of the stuff completely independently of your website.

The simplest option may just be to have SQL Server Agent run a job which checks the table periodically for data to process, and then runs the stored proc if it finds anything.

相关问题