1

Following this tutorial, I am attempting to display the progress of various steps in a long operation. I was able to successfully simulate a long operation within the hub based on the example, reporting updates back to the client with each step.

Taking this a step further, I now want to display the status of a realtime, long-running process that occurs in an MVC action method with the [HttpPost] attribute.

The problem is that I cant seem to update the client from a hub context. I realize that I must create a hub context to communicate using the hub. One difference that I know of is that I must use hubContext.Clients.All.sendMessage(); VS. hubContext.Clients.Caller.sendMessage(); listed in the example. Based on my findings in ASP.NET SignalR Hubs API Guide - Server I should be able to use Clients.Caller as stated in the example but I am limited to only using it in the hub class. Mainly, I just want to understand why I cant get a signal from the action method.

I appreciate the help in advance.

I have created my OWIN Startup() class like so...

using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(HL7works.Startup))]

namespace HL7works
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
}

My hub is written as such...

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AspNet.SignalR;

namespace HL7works
{
    public class ProgressHub : Hub
    {
        public string msg = string.Empty;
        public int count = 0;

        public void CallLongOperation()
        {
            Clients.Caller.sendMessage(msg, count);
        }
    }
}

My Controller...

// POST: /Task/ParseToExcel/
[HttpPost]
public ActionResult ParseToExcel(HttpPostedFileBase[] filesUpload)
{
    // Initialize Hub context
    var hubContext = GlobalHost.ConnectionManager.GetHubContext<ProgressHub>();
    hubContext.Clients.All.sendMessage("Initalizing...", 0);

    double fileProgressMax = 100.0;
    int currentFile = 1;
    int fileProgress = Convert.ToInt32(Math.Round(currentFile / fileProgressMax * 100, 0));

    try
    {
        // Map server path for temporary file placement (Generate new serialized path for each instance)
        var tempGenFolderName = SubstringExtensions.GenerateRandomString(10, false);
        var tempPath = Server.MapPath("~/" + tempGenFolderName + "/");

        // Create Temporary Serialized Sub-Directory
        System.IO.FileInfo thisFilePath = new System.IO.FileInfo(tempPath + tempGenFolderName);
        thisFilePath.Directory.Create();

        // Iterate through PostedFileBase collection
        foreach (HttpPostedFileBase file in filesUpload)
        {

            // Does this iteration of file have content?
            if (file.ContentLength > 0)
            {
                // Indicate file is being uploaded
                hubContext.Clients.All.sendMessage("Uploading " + Path.GetFileName(file.FileName), fileProgress);

                file.SaveAs(thisFilePath + file.FileName);
                currentFile++;
            }
        }

        // Initialize new ClosedXML/Excel workbook
        var hl7Workbook = new XLWorkbook();

        // Start current file count at 1
        currentFile = 1;

        // Iterate through the files saved in the Temporary File Path
        foreach (var file in Directory.EnumerateFiles(tempPath))
        {
            var fileNameTmp = Path.GetFileName(file);

            // Update status
            hubContext.Clients.All.sendMessage("Parsing " + Path.GetFileName(file), fileProgress);

            // Initialize string to capture text from file
            string fileDataString = string.Empty;

            // Use new Streamreader instance to read text
            using (StreamReader sr = new StreamReader(file))
            {
                fileDataString = sr.ReadToEnd();
            }

            // Do more work with the file, adding file contents to a spreadsheet...


            currentFile++;
        }


        // Delete temporary file 
        thisFilePath.Directory.Delete();


        // Prepare Http response for downloading the Excel workbook
        Response.Clear();
        Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
        Response.AddHeader("content-disposition", "attachment;filename=\"hl7Parse_" + DateTime.Now.ToString("MM-dd-yyyy") + ".xlsx\"");

        // Flush the workbook to the Response.OutputStream
        using (MemoryStream memoryStream = new MemoryStream())
        {
            hl7Workbook.SaveAs(memoryStream);
            memoryStream.WriteTo(Response.OutputStream);
            memoryStream.Close();
        }

        Response.End();
    }
    catch (Exception ex)
    {
        ViewBag.TaskMessage =
            "<div style=\"margin-left:15px;margin-right:15px\" class=\"alert alert-danger\">"
            + "<i class=\"fa fa-exclamation-circle\"></i> "
            + "An error occurred during the process...<br />"
            + "-" + ex.Message.ToString()
            + "</div>"
            ;
    }

    return View();
}

In My View (Updated to reflect Detail's answer)...

@using (Html.BeginForm("ParseToExcel", "Task", FormMethod.Post, new { enctype = "multipart/form-data", id = "parseFrm" }))
{

    <!-- File Upload Row -->
    <div class="row">

        <!-- Select Files -->
        <div class="col-lg-6">
            <input type="file" multiple="multiple" accept=".adt" name="filesUpload" id="filesUpload" />
        </div>


        <!-- Upload/Begin Parse -->
        <div class="col-lg-6 text-right">
            <button id="beginParse" class="btn btn-success"><i class="fa fa-download"></i>&nbsp;Parse and Download Spreadsheet</button>
        </div>

    </div>

}



 <!-- Task Progress Row -->
<div class="row">

    <!-- Space Column -->
    <div class="col-lg-12">
        &nbsp;
    </div>

    <!-- Progress Indicator Column -->
    <script type="text/javascript" language="javascript">

        $(document).ready(function () {
            $('.progress').hide();

            $('#beginParse').on('click', function () {
                $('#parseFrm').submit();
            })

            $('#parseFrm').on('submit', function (e) {

                e.preventDefault();

                $.ajax({
                    url: '/Task/ParseToExcel',
                    type: "POST",
                    //success: function () {
                    //    console.log("done");
                    //}
                });

                // initialize the connection to the server
                var progressNotifier = $.connection.progressHub;

                // client-side sendMessage function that will be called from the server-side
                progressNotifier.client.sendMessage = function (message, count) {
                    // update progress
                    UpdateProgress(message, count);
                };

                // establish the connection to the server and start server-side operation
                $.connection.hub.start().done(function () {
                    // call the method CallLongOperation defined in the Hub
                    progressNotifier.server.callLongOperation();
                });
            });
        });

        function UpdateProgress(message, count) {

            // get status div
            var status = $("#status");

            // set message
            status.html(message);

            // get progress bar
            if (count > 0) {
                $('.progress').show();
            }

            $('.progress-bar').css('width', count + '%').attr('aria-valuenow', count);
            $('.progress-bar').html(count + '%');

        }


    </script>

    <div class="col-lg-12">
        <div id="status">Ready</div>
    </div>

    <div class="col-lg-12">
        <div class="progress">
            <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="min-width:20px;">
                0%
            </div>
        </div>
    </div>
</div>
<!-- Task Message Row -->
<div class="row">
    <div clss="col-lg-12">
        @Html.Raw(ViewBag.TaskMessage)
    </div>
</div>

Update: The solution to my problem ended up being Detail's answer but with the AJAX post method modified slightly to pass files to my action method ..

e.preventDefault();

$.ajax({
    url: '/Task/ParseToExcel',
    type: "POST",
    data: new FormData( this ),
    processData: false,
    contentType: false,
    //success: function () {
    //    console.log("done");
    //}
});

Reference.. http://portfolio.planetjon.ca/2014/01/26/submit-file-input-via-ajax-jquery-easy-way/

Cody Hicks
  • 420
  • 9
  • 24
  • Probably best not to include [unnecessary tags](http://meta.stackexchange.com/questions/19190/should-questions-include-tags-in-their-titles) – lloyd Jun 08 '15 at 13:59
  • @lloyd, Thanks. I'll keep that in mind next time. – Cody Hicks Jun 08 '15 at 14:01
  • 1
    No problem, is this [solution relevant](http://stackoverflow.com/questions/7549179/signalr-posting-a-message-to-a-hub-via-an-action-method) – lloyd Jun 08 '15 at 14:29
  • @lloyd, My reference to the hub class and the invocation of the hub method is exactly what they are doing in the solution you have referenced. I fear I may be missing another piece of the puzzle however. I'm just not sure what that could be. – Cody Hicks Jun 08 '15 at 14:39
  • I don't think you would need to send all clients the message... just the one whose hooked up – Callum Linington Jun 08 '15 at 15:19
  • @CallumLinington, I would suspect as much. I'm just interested in getting the message across to the client(s) at the moment. I will look into which client should receive the message afterwards. – Cody Hicks Jun 08 '15 at 15:28

2 Answers2

2

Ok, I've had a bit of a play with this and I think you'll be better off using a plugin called 'jQuery Form Plugin' (http://jquery.malsup.com/form) which will help with the HttpPostedFiles issue.

I've taken your code and made a few adjustments and got it working. You needed to recalculate your fileProgress during each turn of the loop (both loops), and with the button that you've added into the form there's no need to trigger the post via jQuery any more so I commented that out.

Also, I think the CallLongOperation() function is redundant now (I guess that was just a demo thing from the source material), so I've removed that call from your hub start logic and replaced it with a line which shows the button - until signalR is ready you should probably prevent the user from beginning the upload, but signalR starts almost instantly so I don't think you'll even notice that delay.

I had to comment some of the code out as I don't have these objects (the XLWorkbook stuff, the openxml bits, etc) but you should be able to run this without those bits and trace through the code to follow the logic, then add those bits back in yourself.

This was a fun problem to work on, hope I've helped :)

CONTROLLER:

public class TaskController : Controller
{
    [HttpPost]
    public ActionResult ParseToExcel(HttpPostedFileBase[] filesUpload)
    {
        decimal currentFile = 1.0M;
        int fileProgress = 0;
        int maxCount = filesUpload.Count();

        // Initialize Hub context
        var hubContext = GlobalHost.ConnectionManager.GetHubContext<ProgressHub>();
        hubContext.Clients.All.sendMessage("Initalizing...", fileProgress);            

        try
        {
            // Map server path for temporary file placement (Generate new serialized path for each instance)
            var tempGenFolderName = DateTime.Now.ToString("yyyyMMdd_HHmmss"); //SubstringExtensions.GenerateRandomString(10, false);
            var tempPath = Server.MapPath("~/" + tempGenFolderName + "/");

            // Create Temporary Serialized Sub-Directory
            FileInfo thisFilePath = new FileInfo(tempPath);
            thisFilePath.Directory.Create();

            // Iterate through PostedFileBase collection
            foreach (HttpPostedFileBase file in filesUpload)
            {
                // Does this iteration of file have content?
                if (file.ContentLength > 0)
                {
                    fileProgress = Convert.ToInt32(Math.Round(currentFile / maxCount * 100, 0));

                    // Indicate file is being uploaded
                    hubContext.Clients.All.sendMessage("Uploading " + Path.GetFileName(file.FileName), fileProgress);

                    file.SaveAs(Path.Combine(thisFilePath.FullName, file.FileName));
                    currentFile++;
                }
            }

            // Initialize new ClosedXML/Excel workbook
            //var hl7Workbook = new XLWorkbook();

            // Restart progress
            currentFile = 1.0M;
            maxCount = Directory.GetFiles(tempPath).Count();

            // Iterate through the files saved in the Temporary File Path
            foreach (var file in Directory.EnumerateFiles(tempPath))
            {
                var fileNameTmp = Path.GetFileName(file);

                fileProgress = Convert.ToInt32(Math.Round(currentFile / maxCount * 100, 0));

                // Update status
                hubContext.Clients.All.sendMessage("Parsing " + Path.GetFileName(file), fileProgress);

                // Initialize string to capture text from file
                string fileDataString = string.Empty;

                // Use new Streamreader instance to read text
                using (StreamReader sr = new StreamReader(file))
                {
                    fileDataString = sr.ReadToEnd();
                }

                // Do more work with the file, adding file contents to a spreadsheet...
                currentFile++;
            }


            // Delete temporary file 
            thisFilePath.Directory.Delete();


            // Prepare Http response for downloading the Excel workbook
            //Response.Clear();
            //Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
            //Response.AddHeader("content-disposition", "attachment;filename=\"hl7Parse_" + DateTime.Now.ToString("MM-dd-yyyy") + ".xlsx\"");

            // Flush the workbook to the Response.OutputStream
            //using (MemoryStream memoryStream = new MemoryStream())
            //{
            //    hl7Workbook.SaveAs(memoryStream);
            //    memoryStream.WriteTo(Response.OutputStream);
            //    memoryStream.Close();
            //}

            //Response.End();
        }
        catch (Exception ex)
        {
            ViewBag.TaskMessage =
                "<div style=\"margin-left:15px;margin-right:15px\" class=\"alert alert-danger\">"
                + "<i class=\"fa fa-exclamation-circle\"></i> "
                + "An error occurred during the process...<br />"
                + "-" + ex.Message.ToString()
                + "</div>"
                ;
        }

        return View();
    }
}

VIEW:

@using (Html.BeginForm("ParseToExcel", "Task", FormMethod.Post, new { enctype = "multipart/form-data", id = "parseFrm" }))
{
    <!-- File Upload Row -->
    <div class="row">

        <!-- Select Files -->
        <div class="col-lg-6">
            <input type="file" multiple="multiple" accept=".adt" name="filesUpload" id="filesUpload" />
        </div>

        <!-- Upload/Begin Parse -->
        <div class="col-lg-6 text-right">
            <button id="beginParse" class="btn btn-success"><i class="fa fa-download"></i>&nbsp;Parse and Download Spreadsheet</button>
        </div>
    </div>
}

<!-- Task Progress Row -->
<div class="row">

    <!-- Progress Indicator Column -->
    <script type="text/javascript" language="javascript">

        $(document).ready(function () {

            $('.progress').hide();
            $('#beginParse').hide();

            // initialize the connection to the server
            var progressNotifier = $.connection.progressHub;

            // client-side sendMessage function that will be called from the server-side
            progressNotifier.client.sendMessage = function (message, count) {
                // update progress
                UpdateProgress(message, count);
            };

            // establish the connection to the server
            $.connection.hub.start().done(function () {
                //once we're connected, enable the upload button
                $('#beginParse').show();
            });

            //no need for this, the button submits the form
            //$('#beginParse').on('click', function () {
            //    $('#parseFrm').submit();
            //})

            //ajaxify the form post
            $('#parseFrm').on('submit', function (e) {
                e.preventDefault();
                $('#parseFrm').ajaxSubmit();
            });
        });

        function UpdateProgress(message, count) {

            // get status div
            var status = $("#status");

            // set message
            status.html(message);

            // get progress bar
            if (count > 0) {
                $('.progress').show();
            }

            $('.progress-bar').css('width', count + '%').attr('aria-valuenow', count);
            $('.progress-bar').html(count + '%');

        }


    </script>

    <div class="col-lg-12">
        <div id="status">Ready</div>
    </div>

    <div class="col-lg-12">
        <div class="progress">
            <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="min-width:20px;">
                0%
            </div>
        </div>
    </div>
</div>
<!-- Task Message Row -->
<div class="row">
    <div clss="col-lg-12">
        @Html.Raw(ViewBag.TaskMessage)
    </div>
</div>

p.s. don't forget to add a script reference to jQuery Form Plugin in your _Layout.cshtml:

<script src="http://malsup.github.com/jquery.form.js"></script>
Detail
  • 785
  • 9
  • 23
  • Super. Wow, I appreciate the walk through. I hooked everything up in a similar fashion, using the jQuery form plugin and got it to recognize my file input/ HttpPostedFileBase array. All of my controller action logic seems to execute except for returning the file. What's interesting is that after the last file is indicated for having been processed, I get an error on the client (Firebug).. "Not well formed".. and then some ascii wildcard characters. Among them, I see "xl/workbook.xml" as if the controller is sending the file back but not over the HttpHeader. I'll have to look into this. – Cody Hicks Jun 09 '15 at 03:30
  • Otherwise, this is great. I'll remember this jQuery Form plugin in the future. As for signalR, I had some searching, trying to find a clever way to indicate real time status from the server. Many solutions where AJAX based of course and then I read where a user was utilizing signalR. I had heard of the classic chat applications for signalR but had never tried it. First time for everything. Seems pretty interesting. Life would be boring without problems like the one we're trying to solve. – Cody Hicks Jun 09 '15 at 03:30
  • @Detail: How to achieve this about ajaxify form post, means need to using default post method – Rocky Jan 19 '17 at 14:01
  • @Rocky: Sorry, I'm not clear what you're asking... can you rephrase it? – Detail Jan 20 '17 at 15:21
  • I mean what if I don't want to use $('#parseFrm').on('submit', function (e) { e.preventDefault(); $('#parseFrm').ajaxSubmit(); }); – Rocky Jan 24 '17 at 11:40
0

It's not clear exactly what the issue is for you, but using your code I've got this working on my end with a few changes.

Firstly, the form post is reloading the page, if you're going to use POST for this then you need to do that asynchronously by trapping the post event and preventing the default action (then use jQuery to take over). I wasn't sure how you were intending the post to trigger (maybe I just missed it in your code), so I added a button and hooked into that, but alter that as necessary:

<!-- Progress Indicator Column -->
<script type="text/javascript" language="javascript">

    $(document).ready(function () {
        $('.progress').hide();
        $('#button1').on('click', function () {
            $('#form1').submit();
        })

        $('#form1').on('submit', function (e) {

            e.preventDefault();

            $.ajax({
                url: '/Progress/DoTest',
                type: "POST",
                success: function () {
                    console.log("done");
                }
            });

            // initialize the connection to the server
            var progressNotifier = $.connection.progressHub;

            // client-side sendMessage function that will be called from the server-side
            progressNotifier.client.sendMessage = function (message, count) {
                // update progress
                UpdateProgress(message, count);
            };

            // establish the connection to the server and start server-side operation
            $.connection.hub.start().done(function () {
                // call the method CallLongOperation defined in the Hub
                progressNotifier.server.callLongOperation();
            });
        });
    });

    function UpdateProgress(message, count) {

        // get status div
        var status = $("#status");

        // set message
        status.html(message);

        // get progress bar
        if (count > 0)
        {
            $('.progress').show();
        }

        $('.progress-bar').css('width', count + '%').attr('aria-valuenow', count);
        $('.progress-bar').html(count + '%');

    }

</script>

<div class="col-lg-12">
    <div id="status">Ready</div>
</div>


<form id="form1">
    <button type="button" id="button1">Submit Form</button>
</form>

<div class="col-lg-12">
    <div class="progress">
        <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="min-width:20px;">
            0%
        </div>
    </div>
</div>

Also I simplified the Controller a bit, just to focus on the issue at hand. Break the problem down and get the mechanics working first, especially if you're having problems, and then add in the extra logic thereafter:

public class ProgressController : Controller
{
    // GET: Progress
    public ActionResult Index()
    {
        return View();
    }

    [HttpPost]
    public ActionResult DoTest()
    {
        // Initialize Hub context
        var hubContext = GlobalHost.ConnectionManager.GetHubContext<ProgressHub>();
        hubContext.Clients.All.sendMessage("Initalizing...", 0);

        int i = 0;
        do
        {
            hubContext.Clients.All.sendMessage("Uploading ", i * 10);
            Thread.Sleep(1000);
            i++;
        }
        while (i < 10);             

        return View("Index");
    }
}

Also, be sure that your javascript references are ordered correctly, jquery must load first, then signalr, then the hub script.

If you still have issues, post your exact error messages, but I'd suspect it's the synchronous form/reload thing that was your issue.

Hope this helps

Detail
  • 785
  • 9
  • 23
  • I have updated my View code to display how I currently have my form set up and the placement of the `submit` button within the form. As you indicated, there are no errors present. I'm sure this is indeed a synchronization issue. I will try your solution in a moment and update. – Cody Hicks Jun 08 '15 at 15:25
  • Your sample works when creating the test controller action above. When Implementing the same into my original controller action, I cant get it to work and I think it is because my HttpPostedFileBase is no longer reaching the controller action. The files aren't getting there so there is nothing to process. Any ideas? I will post the update. – Cody Hicks Jun 08 '15 at 16:09
  • I will go ahead and mark your solution as the answer. I think this gets me closer to what I need. I will figure out the posted file base thing on my own. You helped me figure out what is likely the main cause of my original problem however. Trapping the post event and posting asynchronously. – Cody Hicks Jun 08 '15 at 17:00