In my controller I have an ActionResult which returns a File.

<!-- language: lang-cs -->
[HttpPost]
public ActionResult ExportCSV(ReportResultViewModel model)   
{     
    var content = "hello,world";
    return File(Encoding.UTF8.GetBytes(content),"text/csv","export.csv");
}

In my view, when I post to this ActionResult, I display a modal saying "please wait".

<!-- language: lang-html -->
<!--modal-->
<div class="modal fade" id="pleaseWaitDialog" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content" style="background: #EBF3EB;">
            <div class="modal-header">
                <h1>Please wait...</h1>
            </div>
            <div class="modal-body">
                <div id="loader"></div>
            </div>
       </div>
    </div>
</div>
@using (Html.BeginForm("ExportCSV", "Reporting", FormMethod.Post, new { name = "back", id = "back", style = "width:100%" }))
{
     @Html.HiddenFor(m => m.A)
     @Html.HiddenFor(m => m.LOT)
     @Html.HiddenFor(m => m.OF)
     @Html.HiddenFor(m => m.THINGS)
                            
     <input type="submit" data-toggle="modal" data-target="#pleaseWaitDialog" value="Export CSV" style="width: 100%; background: #fff" class="btn btn-default"  />
}

I want to hide it when the file is finally returned to the page.

Is there a way to detect client side (with JavaScript maybe) when the file arrives so I can hide the modal?

If you want to capture the return event, you'll have to submit the original request yourself over AJAX. Which means JavaScript will have to be ultimately responsible for downloading the file, instead of the browser via the Content-Disposition: attachment header.

In your controller, instead of returning a FileActionResult, you can convert the file into a Base64String and return that like this:

<!-- language: lang-cs -->
Byte[] bytes = File.ReadAllBytes("path");

if (Request.IsAjaxRequest())
{
    var file64 = Convert.ToBase64String(bytes);
    return Json( new {File = file64, MimeType = mimeType, FileName = fileName});
}
else
{
    // return file - should just auto download
    return File(bytes, mimeType, fileName);
}

Then in your AJAX handler, you can use HTML5/Javascript to generate and save a file using base64 encoding like this:

<!-- language: lang-js -->
$(":submit").on("click", function(e) {
    e.preventDefault();
    var $form = $(this).closest('form');

    // disable buttons
    var $btns = $(":submit").addClass("disabled");
    
    $.ajax({
        type: $form.attr("method"),
        url: $form.attr("action"),
        data: $form.serializeArray(),
        success: function (data) {

            var pom = document.createElement('a');
            pom.setAttribute('href', 'data:' + data.MimeType + ';base64,' + data.File);
            pom.setAttribute('download', data.FileName);

            if (document.createEvent) {
                var event = document.createEvent('MouseEvents');
                event.initEvent('click', true, true);
                pom.dispatchEvent(event);
            }
            else {
                pom.click();
            }

            // reset buttons
            $btns.removeClass("disabled");
        },
        error: function (jqXHR, textStatus, errorThrown) {
            console.log(jqXHR);
        }
    });
});

IE / Edge Support

For IE /Edge, you'll need to use window.navigator.msSaveBlob. If you're sending back a base64 string, you'll also need to Creating a Blob from a base64 string in JavaScript

Further Reading: