locked
Label Not Displaying Status Updates While Using jQuery and Service (asmx) RRS feed

  • Question

  • User1986916315 posted

    I've built a script to perform an install, but I'm wanting it to display progress updates along the way after each operation is completed. My main issue is it seems the [webmethod] in the service isn't getting the public values...it's always returning string.empty for the StatusMessage. However, after I set the value in the codebehind, I retrieved it in the next line and got the value back...so I think the get set is working, it just seems like the web method isn't getting the value to send it back to the label on the page.

    Here's generally what I have (I've removed unrelated sections to simplify it). Here's the C# codebehind

    public async Task LoadPageItems()
    {
        // A lot of PayPal API stuff here to get to:
        if(this.Flow.Items[i].IsRedirectApproved)
        {
            StartAccountSetupProcess();
        }
        else
        {
            // Other stuff not related to this issue - displaying a pay button
        }
    }
    
    public void StartAccountSetupProcess()
    {
        Task.Run(() => StartRunningNewAccountSetupTasks());
    }
    
    public void StartRunningNewAccountSetupTasks()
    {
        // This is the area most related to my issue
        // I'm passing the progress update to the asmx service page as a string
        NewAccountSetupTasks create = new NewAccountSetupTasks();
        var progressService = new WebServices.ProgressService();
        progressService.Completed = "No";
        progressService.StatusMessage = "Creating webspace...";
        create.CreateWebspace(UserInfoObject);
    
        progressService.StatusMessage = "Copying files...";
        create.CreateRepository(UserInfoObject);
    }

    In the aspx page, I am referencing this javascript

    $(document).ready(function () {
        var processComplete = false;
        var intervalListener = window.setInterval(function () {
            if (!processComplete)
                CallCheckProgress();
        }, 2000);
    
        function CallCheckProgress() {
            $.ajax({
                type: "POST",
                url: "MyService.asmx/CheckStatus",
                contentType: "application/json; charset=utf-8",
                dataType: "json",
                data: '{ "statusMessage": "StatusMessage", "completed": "Completed"}',
                success: function (r) {
                    updateStatus('completed', JSON.parse(r.d)["StatusMessage"]);
                    if (r.d) {
                        console.log(r.d);
                        if (JSON.parse(r.d)["Completed"] === "yes") {
                            processComplete = true;
                            $('#ProcessStartedLbl').hide();
                        }
                    }
                },
                error: function (r) {
                    console.log('Check error : ' + r.responseText);
                },
                failure: function (r) {
                    console.log('Check failure : ' + r.responseText);
                }
            });
            if (processComplete)
                window.clearInterval(intervalListener);
        }
        function updateStatus(status, msg) {
            if (msg)
                $('#ProgressLbl').html(msg);
        }
    });

    And here's the service code

    using System.Web.Script.Serialization;
    using System.Web.Services;
    
    namespace MyDotCom.WebServices
    {
        [WebService(Namespace = "http://tempuri.org/")]
        [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
        [System.ComponentModel.ToolboxItem(false)]
        // To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line. 
        [System.Web.Script.Services.ScriptService]
    
        public class ProgressService : WebService
        {
            public string StatusMessage { get; set; }
            public string Completed { get; set; }
    
            [WebMethod]
            public string CheckStatus()
            {
                Domain.SetupTasks.ProgressInfo progressInfo;
    
                progressInfo = new Domain.SetupTasks.ProgressInfo
                {
                    StatusMessage = StatusMessage != null ? StatusMessage : string.Empty,
                    Completed = Completed != null ? Completed : "no"
                };
                var ajaxJsonString = new JavaScriptSerializer().Serialize(progressInfo);
                return ajaxJsonString;
            }
        }
    }

    P.S. - I thought I had this working once while using the webmethod in the codebehind (everything seemed great), until testing on two different computers at the same time. I was using static string in the webmethod and I'm guessing that's what the issue was, so I switched to trying the web service route.

    Saturday, September 14, 2019 1:51 AM

Answers

  • User475983607 posted

    Your approach does not work because web applications have one request and one response.  The client sends the request and waits for the server's response.  In your example code the code on the server runs to completion then sends the response.  The code does not identity the running process.  Generally, progress state is stored in a table or cache and I don't see where this is happening in your code.

    Perhaps look into SignalR

    https://dotnet.microsoft.com/apps/aspnet/signalr

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Saturday, September 14, 2019 12:23 PM
  • User-474980206 posted

    every web request create a new instance of the page class, so all variables are new (except statics). as you found statics are shared for all requests. to do what you want, you need to use statics and have a static for each progress (assuming there is only one server, if webfarm you will need to use persistent store).

    just create a static collection. generate a guid by starting the process, and pass to the client. use this guid as a key to get status.

    public static Dictionary<Guid,ProgressInfo> = new Dictionary<Guid,ProgressInfo>();

    hint. when done, you can delete the entry, and use missing entry as done flag.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Monday, September 16, 2019 3:29 PM

All replies

  • User475983607 posted

    Your approach does not work because web applications have one request and one response.  The client sends the request and waits for the server's response.  In your example code the code on the server runs to completion then sends the response.  The code does not identity the running process.  Generally, progress state is stored in a table or cache and I don't see where this is happening in your code.

    Perhaps look into SignalR

    https://dotnet.microsoft.com/apps/aspnet/signalr

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Saturday, September 14, 2019 12:23 PM
  • User-2054057000 posted

    Check the browser console area for any js errors. 

    You can also debug the code by placing breakpoint over the .asmx service.

    Saturday, September 14, 2019 12:31 PM
  • User1986916315 posted

    mgebhard

    Your approach does not work because web applications have one request and one response.  The client sends the request and waits for the server's response.  In your example code the code on the server runs to completion then sends the response. 

    If I create a global variable on the aspx page, create a new instance of it in the Page_Load method, set the values in the StartRunningNewAccountSetupTasks method and move the Web Method to the aspx page (but I have to set the CheckStatus as a static string), it all works smoothly if only one person was ever using it at a time. This is when I started trying other things, because if I run this on two different computers, they won't show independent information...one of them always resets to show the information of the other so they match exactly.

    aspx.cs global variable

    public static ProgressInfo progressInfo;

    ProgressInfo class.

    public class ProgressInfo
    {
        public string StatusMessage { get; set; }
        public string Completed { get; set; }
    }

    aspx.cs page load

    public void Page_Load(object sender, EventArgs e)
    {
        progressInfo = new ProgressInfo();
    }

    This is the adjusted StartRunningNewAccountSetupTasks method

    public void StartRunningNewAccountSetupTasks()
    {
        NewAccountSetupTasks create = new NewAccountSetupTasks();
    
        progressInfo.Completed = "No";
        progressInfo.StatusMessage = "Creating webspace...";
        create.CreateWebspace(UserInfoObject);
    
        progressInfo.StatusMessage = "Copying files...";
        create.CreateRepository(UserInfoObject);
    
        // ... completes each other step of the setup in the same way
    }

    The javascript remains the same, except it is changed to reference this aspx page instead of the service file.

    Then here's the Web Method that is now in the aspx.cs page.

    [WebMethod]
    public static string CheckStatus()
    {
        if (progressInfo != null)
        {
        progressInfo.StatusMessage = progressInfo != null ? progressInfo.StatusMessage : string.Empty;
        progressInfo.Completed = progressInfo != null ? progressInfo.Completed : "no";
            var ajaxJsonString = new JavaScriptSerializer().Serialize(progressInfo);
            return ajaxJsonString;
        }
        else
            return null;
    }

    Like I mentioned earlier, this all works together to update the page label right before each step of the setup and I thought it was all working great, until I ran it on two computers at the same time and noticed the issue with one computer changing to display a mirror image of the other process. For example: If the first computer has reached the second step and the label is now displaying "Copying files..." and I start the process on the second computer, the first computer's label returns to "Creating webspace..." and they both match from there to the end. After looking into it more, it looks like the issue must be the static string, so I attempted the service asmx approach to see if it would be a way around it.

    Saturday, September 14, 2019 4:40 PM
  • User1986916315 posted

    Check the browser console area for any js errors. 

    You can also debug the code by placing breakpoint over the .asmx service.

    It wasn't throwing any errors in the console.

    When I was setting breakpoints, I saw that if I set the web service's global variable from the codebehind of the aspx page and retrieved it in the next line, the value I'd set was returned. However, once I hit a breakpoint inside the web method, the updated global variable value I was trying to retrieve from the get set in the web service was null.

    Saturday, September 14, 2019 4:52 PM
  • User475983607 posted

    When I was setting breakpoints, I saw that if I set the web service's global variable from the codebehind of the aspx page and retrieved it in the next line, the value I'd set was returned. However, once I hit a breakpoint inside the web method, the updated global variable value I was trying to retrieve from the get set in the web service was null.

    And the expected results.  Each request instantiates a new ASMX class.  As suggested above, you need to write code that stores state.  A table or cache works.

    Saturday, September 14, 2019 5:29 PM
  • User1986916315 posted

    I'll look into this more. I also had another detailed reply post in this thread that isn't showing for some reason. It's at https://forums.asp.net/post/6277924.aspx

    Saturday, September 14, 2019 5:38 PM
  • User-474980206 posted

    every web request create a new instance of the page class, so all variables are new (except statics). as you found statics are shared for all requests. to do what you want, you need to use statics and have a static for each progress (assuming there is only one server, if webfarm you will need to use persistent store).

    just create a static collection. generate a guid by starting the process, and pass to the client. use this guid as a key to get status.

    public static Dictionary<Guid,ProgressInfo> = new Dictionary<Guid,ProgressInfo>();

    hint. when done, you can delete the entry, and use missing entry as done flag.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Monday, September 16, 2019 3:29 PM
  • User1986916315 posted

    Okay, I started taking this down a whole new path after not making any progress with my last approach and starting giving SignalR a try, as recommended. SignalR seems pretty neat, however...I've made a lot of changes and am pretty much back to the same problem. I can only get it to display the progress messages to everyone.

    I've searched and searched for ways to display it uniquely to each user, and every example keeps saying use something like var connectionId = Context.ConnectionId; in the Hub to get the connection id, then pass it back from the Hub like Clients.Client(connectionId).showProgress(ajaxJsonStringHere); instead of the only thing I can get to work: hubContext.Clients.All.showProgress(ajaxJsonString); However, Context.ConnectionId is always null for me. I set breakpoints and as soon as the code reaches it in the Hub, it throws an error alert saying it's null: Microsoft.AspNet.SignalR.Hubs.HubBase.Context.get returned null.

    Here's some updated code for what I'm trying.

    Startup Class

    public class Startup1
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }

    Hub. Yes, as I look at this, I see it's a bit messy. Just trying to get it to work before cleaning it up...

    public class ProgressReporter : Hub
    {
        public void ShowProgress(string statusMessage, string completed)
        {
            var hubContext = GlobalHost.ConnectionManager.GetHubContext<ProgressReporter>();
    
            var progressInfo = new ProgressInfo
            {
                StatusMessage = statusMessage,
                Completed = completed
            };
            LogMessage(progressInfo, hubContext);
        }
    
        private void LogMessage(ProgressInfo progressInfo, IHubContext hubContext)
        {
            progressInfo.StatusMessage = progressInfo.StatusMessage != null ? progressInfo.StatusMessage : string.Empty;
            progressInfo.Completed = progressInfo.Completed != null ? progressInfo.Completed : "no";
    
            var ajaxJsonString = new JavaScriptSerializer().Serialize(progressInfo);
            try
            {
                hubContext.Clients.All.showProgress(ajaxJsonString);
    // If I could get var connectionId = Context.ConnectionId; maybe I could do:
    // Clients.Client(connectionId).showProgress(ajaxJsonString); //??? } catch(Exception ex) { Domain.Utilities.Log.LogError(ex); } } }

    Javascript in Aspx

    <script src="Scripts/jquery.signalR-2.4.1.min.js"></script>
    <script src="signalr/hubs"></script>
    <script type="text/javascript">
        $(function () {
            var reporter = $.connection.progressReporter;
            reporter.client.showProgress = function (msg) {
                var message = JSON.parse(msg)["StatusMessage"];
                var completed = JSON.parse(msg)["Completed"];
                $("#ProgressLbl").html(message);
                if (completed === "yes") {
                    $('#<%=CogIconDiv.ClientID%>').hide();
                    $('#<%=ProcessStartedLbl.ClientID%>').hide();
                }
            };
            $.connection.hub.start().done(function () {
                console.log($.connection.hub.id);
            });
        });
    </script>

    Codebehind. I'm just pausing the thread now to simulate the process.

    public void StartRunningNewAccountSetupTasks()
    {
        //NewAccountSetupTasks create = new NewAccountSetupTasks();
    
        Domain.SignalR.ProgressReporter progressReporter = new Domain.SignalR.ProgressReporter();
        progressInfo = new ProgressInfo();
        progressReporter.ShowProgress("Creating webspace...", "no");
    
        // create.CreateWebspace(UserInfoObject);
        System.Threading.Thread.Sleep(5000);
    
        progressReporter.ShowProgress("Copying files...", "no");
        // create.CreateRepository(UserInfoObject);
        System.Threading.Thread.Sleep(5000);
    
        // Continues with more like this...
    }

    Tuesday, September 17, 2019 2:10 AM
  • User283571144 posted

    Hi srelliott,

    I found you directly call the ShowProgress method in another method ,it will not create the connection. That means it will not contain the connection id.

    In my opinion, the right way is you should store the connection id when connected firstly in the session or something else.

    Then you could use this connection id in the StartRunningNewAccountSetupTasks method.

    Best Regards,

    Brando

    Tuesday, September 17, 2019 9:17 AM