I am working on an ASP.NET MVC 4 Application that imports and processes a CSV file. I am using a standard form and controller for the upload. Here is an overview of what I am doing currently:
Controller Logic
public ActionResult ImportRecords(HttpPostedFileBase importFile){
var fp = Path.Combine(HttpContext.Server.MapPath("~/ImportUploads"), Path.GetFileName(uploadFile.FileName));
uploadFile.SaveAs(fp);
var fileIn = new FileInfo(fp);
var reader = fileIn.OpenText();
var tfp = new TextFieldParser(reader) {TextFieldType = FieldType.Delimited, Delimiters = new[] {","}};
while(!tfp.EndOfData){
//Parse records into domain object and save to database
}
...
}
HTML
#using (Html.BeginForm("ImportRecords", "Import", FormMethod.Post, new { #id = "upldFrm", #enctype = "multipart/form-data" }))
{
<input id="uploadFile" name="uploadFile" type="file" />
<input id="subButton" type="submit" value="UploadFile" title="Upload File" />
}
The import file can contain a large number of records (average 40K+) and can take quite some time to complete. I'd rather not have a user sitting at the import screen for 5+ minutes for each file processed. I have considered adding a console application to watch the uploads folder for new files, and process when something new is added, but would like to see what input I receive from the community before starting my journey down this path.
Is there a more efficient way to handle this operation?
Is there a way to perform this action, allowing the user to continue about his/her merry way, and then notify the user when processing is done?
The solution to the issue I was having is a bit complex, but works similar to the IFrame fix. The result is a pop-up window that handles the processing, allowing the user to continue navigation throughout the site.
The file is submitted to the server (UploadCSV controller), a Success page is returned with a bit of JavaScript to handle the initial kick-off of the processing. When the user clicks "Begin Processing", a new window is opened (ImportProcessing/Index) that loads the initial status (kicking off an interval loop that retrieves status updates) and then makes a call to the "StartProcessing" action, kicking off the processing process.
The "FileProcessor" class that I am using is housed in a static dictionairy variable within the ImportProcessing controller; allowing for status results based on the key. The FileProcessor is promptly removed after the operation is complete or an error is encountered.
Upload Controller:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult UploadCSV(HttpPostedFileBase uploadFile)
{
var filePath = string.Empty;
if (uploadFile.ContentLength <= 0)
{
return View();
}
filePath = Path.Combine(Server.MapPath(this.UploadPath), "DeptartmentName",Path.GetFileName(uploadFile.FileName));
if (new FileInfo(filePath).Exists)
{
ViewBag.ErrorMessage =
"The file currently exists on the server. Please rename the file you are trying to upload, delete the file from the server," +
"or contact IT if you are unsure of what to do.";
return View();
}
else
{
uploadFile.SaveAs(filePath);
return RedirectToAction("UploadSuccess", new {fileName = uploadFile.FileName, processType = "sonar"});
}
}
[HttpGet]
public ActionResult UploadSuccess(string fileName, string processType)
{
ViewBag.FileName = fileName;
ViewBag.PType = processType;
return View();
}
Upload Success HTML:
#{
ViewBag.Title = "UploadSuccess";
}
<h2>File was uploaded successfully</h2>
<p>Your file was uploaded to the server and is now ready to be processed. To begin processing this file, click the "Process File" button below.
</p>
<button id="beginProcess" >Process File</button>
<script type="text/javascript">
$(function () {
$("#beginProcess").click(BeginProcess);
function BeginProcess() {
window.open("/SomeController/ImportProcessing/Index?fileName=#ViewBag.FileName&type=#ViewBag.PType", "ProcessStatusWin", "width=400, height=250, status=0, toolbar=0, scrollbars=0, resizable=0");
window.location = "/Department/Import/Index";
}
});
</script>
Once this new window is opened up, the file processing begins. Updates are retrieved from a custom FileProcessing class.
ImportProcessing Controller:
public ActionResult Index(string fileName, string type)
{
ViewBag.File = fileName;
ViewBag.PType = type;
switch (type)
{
case "somematch":
if (!_fileProcessors.ContainsKey(fileName)) _fileProcessors.Add(fileName, new SonarCsvProcessor(Path.Combine(Server.MapPath(this.UploadPath), "DepartmentName", fileName), true));
break;
default:
break;
}
return PartialView();
}
ImportProcessing Index:
#{
ViewBag.Title = "File Processing Status";
}
#Scripts.Render("~/Scripts/jquery-1.8.2.js")
<div id="StatusWrapper">
<div id="statusWrap"></div>
</div>
<script type="text/javascript">
$(function () {
$.ajax({
url: "GetStatusPage",
data: { fileName: "#ViewBag.File" },
type: "GET",
success: StartStatusProcess,
error: function () {
$("#statusWrap").html("<h3>Unable to load status checker</h3>");
}
});
function StartStatusProcess(result) {
$("#statusWrap").html(result);
$.ajax({
url: "StartProcessing",
data: { fileName: "#ViewBag.File" },
type: "GET",
success: function (data) {
var messag = 'Processing complete!\n Added ' + data.CurrentRecord + ' of ' + data.TotalRecords + " records in " + data.ElapsedTime + " seconds";
$("#statusWrap #message").html(messag);
$("#statusWrap #progressBar").attr({ value: 100, max: 100 });
setTimeout(function () {
window.close();
}, 5000);
},
error: function (xhr, status) {
alert("Error processing file");
}
});
}
});
</script>
Finally the Status Checker html:
#{
ViewBag.Title = "GetStatusPage";
}
<h2>Current Processing Status</h2>
<h5>Processing: #ViewBag.File</h5>
<h5>Updated: <span id="processUpdated"></span></h5>
<span id="message"></span>
<br />
<progress id="progressBar"></progress>
<script type="text/javascript">
$(function () {
var checker = undefined;
GetStatus();
function GetStatus() {
if (checker == undefined) {
checker = setInterval(GetStatus, 3000);
}
$.ajax({
url: "GetStatus?fileName=#ViewBag.File",
type: "GET",
success: function (result) {
result = result || {
Available: false,
Status: {
TotalRecords: -1,
CurrentRecord: -1,
ElapsedTime: -1,
Message: "No status data returned"
}
};
if (result.Available == true) {
$("#progressBar").attr({ max: result.Status.TotalRecords, value: result.Status.CurrentRecord });
$("#processUpdated").text(result.Status.Updated);
$("#message").text(result.Status.Message);
} else {
clearInterval(checker);
}
},
error: function () {
$("#statusWrap").html("<h3>Unable to load status checker</h3>");
clearInterval(checker);
}
});
}
});
</script>
Just a thought but you could thread the processing of your CSV files and on completion of that task call another method that basically provides a modal dialog or some kind of javascript alert on the client side letting the user know that the processing has completed.
Task.Factory.StartNew(() => ProcessCsvFile(fp)).ContinueWith((x) => NotifyUser());
or something along those lines. I think that ultimately you are gonna wanna look at some kind of threading because it certainly does not make sense for a user to be stuck looking at a screen while some kind of server side processing takes place.
Related
I'm a little lost on what should I do. I'm trying to upload a file along with its form data in one click, but I can't get the file. I tried to check the file in client and it's okay, but when receiving the file in the Controller, it's empty.
Problem
How do I upload the file along its formData using jQuery?
View
Assuming the other fields
<form id="_RegisterProduct" enctype="multipart/form-data">
<div>
<label>Product Description</label>
<textarea id="product_description" name="_product_description"></textarea>
<input type="file"
id="product_file"
name="product_file"
class="dropify" />
</div>
<button type="submit" id="snippet_new_save">Register Product</button>
</form>
<script>
$(function() {
rules: {
text: { required: true, minlength: 5 },
number: { required: true, minlength: 1 }
},
submitHandler: function (form) {
var fileUpload = $("#product_file").val();
var formData = $("#_RegisterForm").serialize();
var url = "#Url.Action("RegisterProduct", "Product")";
$.get(url, { fileUpload: fileUpload, formData }, function (e) {
if (e >= 1) {
console.log("success");
} else {
console.log("error");
}
});
}
})
</script>
Controller
public string RegisterProduct(HttpPostedFileBase fileUpload, AB_ProductModel formData)
{
var data = "";
using (var con = new SqlConnection(Conn.MyConn()))
{
var path = Server.MapPath("~/Content/uploads/products");
var Extension = "";
var fileName = "";
try
{
if(fileUpload.ContentLength > 0)
{
Extension = Path.GetExtension(fileUpload.FileName);
fileName = Path.GetFileName(fileUpload.FileName);
var com = new SqlCommand("dbo.sp_some_stored_procedure_for_saving_data",
con);
con.Open
data = Convert.ToString(com.ExecuteScalar());
var file_path = Path.Combine(path, data + Extension);
fileUpload.SaveAs(file_path);
}
}
catch (Exception ex)
{
data = ex.Message;
}
// data returns id if success or error message
}
return data;
}
Why serialize the form? This method creates a string that can be sent over to the server, but that is not what you want to do... In case of a file upload; see .serialize() function description here.
FormData type automatically manages the enctype for your forms (see here on MDN), so you can omit that—although you should consider using it, because it helps other members on the team understand the intent. If you want to use plain jQuery, you can simply attach the formData variable to the data field of the $.ajax call. See like here,
/*
* i know id-based selection should only have 1 element,
* otherwise HTML is invalid for containing multiple elements
* with the same id, but this is the exact code i used back then, so using it again.
**/
var formData = new FormData($('#form')[0]);
$.ajax({
type: 'POST',
processData: false,
contentType: false,
data: formData,
success: function (data) {
// The file was uploaded successfully...
$('.result').text('File was uploaded.');
},
error: function (data) {
// there was an error.
$('.result').text('Whoops! There was an error in the request.');
}
});
This of course requires that your HTML DOM contains these elements—I used the code I wrote for my article quite a few years back. Secondly, for my other part of the feature, I used Request.Files to capture the files that might have been uploaded with the request.
files = Request.Files.Count;
if(files > 0) {
// Files are sent!
for (int i = 0; i < files; i++) {
var file = Request.Files[i];
// Got the image...
string fileName = Path.GetFileName(file.FileName);
// Save the file...
file.SaveAs(Server.MapPath("~/" + fileName));
}
}
This way, I uploaded the files using jQuery and FormData.
You can check out the complete article I posted here, Uploading the files — HTML5 and jQuery Way!
Oh, and do not forget the suggestion made in the comment,
using (var com = new SqlCommand("dbo.sp_some_stored_procedure_for_saving_data", con))
{
con.Open(); // missed call?
data = Convert.ToString(com.ExecuteScalar());
// although, using should close here!
var file_path = Path.Combine(path, data + Extension);
fileUpload.SaveAs(file_path);
}
So, this was pretty much how you can do this.
View code
<input type="text" id="txtImageName"></input>
<input type="text" id="txtImageDescription"></input>
<input type="file" id="txtImageName"></input>
<input type="button" id="btnUpload" Value="Upload">
Jquery Code
<script>
$('#btnUpload').click(function () {
var file = $("#txtImageName").get(0).files;
$.ajax({
type: 'post',
url: "#Url.Action("HomeAddNew", "Admin")",
data:
{
ImageName: $('#txtImageNameoption').val(),
ImageDescription:$('#txtImageDescription').val(),
Image: file
},
success: function (data) {
alert('hai');
},
error: function (data) {
alert('error');
}
});
});
</script>
Controller code
public ActionResult HomeAddNew(string ImageName, string ImageDescription, HttpPostedFileBase Image)
{
//in the above HttpPostedFilebase I'm getting the null value but, I don't want to use .fileupload method or formdata class in jquery.
return View();
}
Explanation: Whenever I check the file in controller method it is showing as null. Please send me the perfect solution for this. It will help me a lot. My email id is ganesh#concai.co.uk
You can't upload a file with ajax. If your browser requirements starts from IE10then you can use xhr.
Code and information about how you should do it HERE.
Also, there's a plugin called FileAPI, which you can find HERE
Simple demo with progress bar
function startUpload() {
var fileInput = document.getElementById("fileInput");
if (fileInput.files.length == 0) {
alert("Please choose a file");
return;
}
var progressBar = document.getElementById("progressBar");
var xhr = new XMLHttpRequest();
xhr.upload.onprogress = function(e) {
var percentComplete = (e.loaded / e.total) * 100;
progressBar.value = percentComplete;
};
xhr.onload = function() {
if (xhr.status == 200) {
alert("Sucess! Upload completed");
} else {
alert("Error! Upload failed");
}
};
xhr.onerror = function() {
alert("Error! Upload failed. Can not connect to server.");
};
progressBar.value = 0;
xhr.open("POST", "ajax-upload", true);
xhr.setRequestHeader("Content-Type", fileInput.files[0].type);
xhr.send(fileInput.files[0]);
}
I have a client built in Angular. I am trying to upload files that are to be processed by the asp.net server side code. Although I have managed to get files in the file control but I don't know how I can pass the binary data to server side. My client side code looks as follows
HTML Code
<div>
<form name="form1" method="POST" enctype="multipart/form-data">
<div>
{{repopulatecftcModel.title}}
</div>
<div style="padding-top:15px;padding-bottom:15px;"><b>Toolkit - Repopulate Cftc Data</b></div>
<div>
<input type="file" id="updCftcFileUploader" name="files[]" multiple />
</div>
<div>
<input type="button" value="Upload Files" title="Upload Files" ng-click="UploadCFTCFiles()" />
</div>
<div>
<label ng-model="repopulatecftc.validationtext"></label>
</div>
<div>
{{repopulatecftcModel.validationtext}}
</div>
</form>
Controller Code in Angular
function controller($scope, $http, AppModel, WebFunctionService) {
$scope.UploadFiles = function (evt) {
var files = document.getElementById('updFileUploader').files;
for(var i = 0; i < files.length; i++) {
var file = files[i];
if (file) {
// getAsText(file);
alert("Name: " + file.name + "\n" + "Last Modified Date :" + file.lastModifiedDate);
}
}
var parameters = { directory: $scope.defaultsSaveDirectory, filename: "" };
WebFunctionService.promiseWebMethod('UploadFiles', angular.fromJson(parameters), true)
.success(function () {
$scope.userMessage = "File(s) successfully uploaded";
console.log($scope.userMessage);
})
.error(function (error) {
$scope.userMessage = "ERROR uploading files" + error;
console.log($scope.userMessage);
});
};
};
Server side code where I want to access the uploaded files from
[Route("UploadFiles")]
[HttpPost]
public void UploadFiles()
{
}
When I run the code I do get alerts for each of the file being uploaded. Then the code get into the server side as shown in the image below
Its here that I want to access the files from. I have seen in the web where they show Request.Files shows the collection of files being uploaded but when I try to do that the compiler starts complaining.
Anyone have any clues as to how I should be able to pass binary data being uploaded from client side in this case and access it from the server side
In angular when you call the server side you can use $upload to upload a file here is an example:
var uploadFile = function (file, args) {
var deferred = $q.defer();
$upload.upload({
url: "<your url goes here>",
method: "POST",
file: file,
data: args
}).progress(function (evt) {
// get upload percentage
console.log('percent: ' + parseInt(100.0 * evt.loaded / evt.total));
}).success(function (data, status, headers, config) {
// file is uploaded successfully
deferred.resolve(data);
}).error(function (data, status, headers, config) {
// file failed to upload
deferred.reject();
});
return deferred.promise;
}
The above function will pass the file along with extra args if you need it.
Server side
[HttpPost]
public async Task<HttpResponseMessage> AddFile()
{
if (!Request.Content.IsMimeMultipartContent())
{
this.Request.CreateResponse(HttpStatusCode.UnsupportedMediaType);
}
string root = HttpContext.Current.Server.MapPath("~/temp/uploads");
var provider = new MultipartFormDataStreamProvider(root);
var result = await Request.Content.ReadAsMultipartAsync(provider);
// On upload, files are given a generic name like "BodyPart_26d6abe1-3ae1-416a-9429-b35f15e6e5d5"
// so this is how you can get the original file name
var originalFileName = GetDeserializedFileName(result.FileData.First());
var uploadedFileInfo = new FileInfo(result.FileData.First().LocalFileName);
string path = result.FileData.First().LocalFileName;
//Do whatever you want to do with your file here
return this.Request.CreateResponse(HttpStatusCode.OK, originalFileName );
}
private string GetDeserializedFileName(MultipartFileData fileData)
{
var fileName = GetFileName(fileData);
return JsonConvert.DeserializeObject(fileName).ToString();
}
public string GetFileName(MultipartFileData fileData)
{
return fileData.Headers.ContentDisposition.FileName;
}
I have the following code in my view :
<script type="text/javascript">
function OnCancelClick(e)
{
var jobId = e;
var flag = confirm('You are about to cancel job : ' + jobId + '. Are you sure you want to cancel this job?');
if (flag) {
$.ajax({
url: '/job/CancelJob',
type: 'POST',
data: { jobId: jobId },
dataType: 'html',
success: function (result) { alert('Job ' + jobId + ' was cancelled.'); document.location = "#Url.Action("Index", "Job")"; },
error: function () { alert('Something went wrong. Check the log for more information.'); }
});
}
return false;
}
</script>
In my view I also have :
<input type="submit" id="cancelButton" value="Cancel" onclick="javascript: return OnCancelClick(#Model.Id);" />
In my controller I have :
[HttpPost]
public ActionResult CancelJob(int jobId)
{
try
{
logger.LogInfo(string.Format("<start> Cancel-button clicked for job : {0}", jobId), jobId);
JobCommandService.ChangeStatus(jobId, 6);
logger.LogInfo(string.Format("<end> Cancel-button clicked for job : {0}", jobId), jobId);
return RedirectToAction("Index", "Job");
}
catch (Exception ex)
{
logger.LogError(ex.Message, ex, jobId);
Response.StatusCode = (int)HttpStatusCode.InternalServerError;
return Json(new { Success = false, Message = ex.Message });
}
}
When I run this in my VS2012 it works just fine.
When I deploy it to the server, I'm getting the message that something went wrong.
In my logging there is no trace of the button being clicked.
As per your comment, when deployed your app is installed in accindigoapps.blabla.lok/jobmonitor.
However your script has the url hardcoded as url: '/job/CancelJob'. That will mean:
when you are debugging from VS your script will work because the request is being sent to a url like http://localhost:XXX/job/CancelJob
however in production, the request will be sent to http://accindigoapps.blabla.lok/job/CancelJob, missing the jobmonitor part.
You need a way to inform your JS code about the base url of your application:
You could generate the Url in a Razor view using Url.Action("CancelJob","job") and pass that Url into your javascript code.
Another option would be to use Url.Content("~/") in some javascript of your base layout. That helper Url.Content("~/") will return only your application folder, / in your dev environment and /jobmonitor/ when deployed. That way you will have your app root-relative url available to any script, so you can use it to build root-relative urls as you were doing in your script:
<script>
var myApp = {};
myApp.BaseUrl = '#Url.Content("~/")';
</script>
//Some other script like yours would be able to keep using root-relative urls as:
$.ajax({
url: myApp.BaseUrl + 'job/CancelJob',
...
If you prefer to generate full urls, you could follow a similar approach. Have a look at this question
Hope it helps!
Can anyone tell me how to show animated progress bar while the data from an Excel sheet to a SQL database table is being transfered?
I have a form in .aspx page. In that form, there is one FileUpload control that uploads an Excel file. While uploading that file and saving it on the server at the same time I'm transferring the data from the Excel sheet to a SQL table. During this transfer, I want to show a ProgressBar and after transferring all data it will be removed automatically..
Is there something I can do to achieve this?
I would probably submit the form using jQuery's ajaxForm().
Then, onSuccess, call a function that begins further AJAX requests to poll the progress of the upload from your webserver, using JSON. Apart from having a URL to handle the file upload in ASP.NET, you would also need to have another means of returning the progress of some kind of asynchronous worker in JSON format.
Once you get the JSON back, you can then feed this to a jQueryUI progress bar.
For example, in an ASP .NET MVC application, I did something like this:
In the view Upload.aspx, begin the submission
<% using (Html.BeginForm(null, null, FormMethod.Post, new { enctype = "multipart/form-data", id = "UploadForm" }))
{ %>
<div>
<input type="file" name="CSVFile" id="CSVFile" />
<button>Upload</button>
</div>
<% } %>
var pb = $('#prog');
var pbContainer = $('#pbcont');
var pbPercent = $('#progp');
var uploadForm = $('#UploadForm');
var status = $('#StatusDetail');
uploadForm.ajaxForm({
iframe: true,
dataType: 'jason',
success: function (data) {
beginProcessing($.parseJSON($(data).text()), '" + Url.Action("UploadStatus", "Upload") + #"', pb, pbContainer, status, pbPercent);
},
error: function (xhr, textStatus, error) {
alert('Error: ' + textStatus);
}
});
Controller method to handle the initial upload
Here, I'm creating a unique ID for the upload when it begins, this is so I can identify the upload later on, when I want to find out it's progress.
I'm using a worker class I wrote which handles the processing asynchronously - this is where you would want to asynchronously begin inserting your data into the database.
By the time we reach this controller method, the FileStream should have reached the server, and so we can pass that to our worker to read the stream, parse the CSV and do the database work. Notice that here, I pass the StreamReader to my worker so it can handle all of that.
// NOTE: The parameter to this action MUST match the ID and Name parameters of the file input in the view;
// if not, it won't bind.
[HttpPost]
public JsonResult Upload(HttpPostedFileBase CSVFile)
{
try
{
if (CSVFile == null || String.IsNullOrWhiteSpace(CSVFile.FileName))
return Json("You must provide the path to your CSV file", "text/plain");
if (!CSVFile.FileName.ToLower().Contains(".csv"))
return Json("You can only upload CSV files", "text/plain");
Guid id = worker.BeginImport(dataReporistory, new StreamReader(CSVFile.InputStream));
//return some JSON
var json = new
{
ID = id,
name = CSVFile.FileName,
size = CSVFile.ContentLength
};
return Json(json, "text/plain");
}
catch (Exception e)
{
return Json(Utilities.DisplayExceptionMessage(e), "text/plain");
}
}
Controller method to return progress update
[HttpPost]
public JsonResult UploadStatus(Guid id)
{
UploadJob job = Worker.GetJobStatus(id);
return Json(job);
}
JavaScript in the view to handle the progress bar updating
As you will see above, the ajaxForm.Submit() method will call beginProcessing() from here during the onSuccess event, when the file has finished uploading.
It will also pass the JSON it got from the Upload() controller method, which tells our view the ID of the upload to pass to the update URL when fetching the progress of the job from our worker.
Once beginProcessing is called, it will do some work to setup a progress bar but basically then starts calling updateProgress() on a set timer interval. updateProgress is the function which does all the work of fetching the JSON from the webserver's UploadStatus page.
Once updateProgress gets the JSON update from the webserver, it does some work to feed that into the jQuery UI progress bar that was inserted into a div on the page.
<div id="pbcont">
<p style="display: inline-block;"><strong>Processing...</strong></p>
<h3 style="display: inline-block;" id="progp"></h3>
<div id="prog"></div>
<br />
<div id="StatusDetail"></div>
</div>
function beginProcessing(response, url, pb, pbContainer, statusContainer, pbPercent) {
if (!response.ID) {
alert('Error: ' + response);
return;
}
pb.progressbar({
value: 0
});
pbContainer
.css('opacity', 0)
.css('display', 'block');
//Set the interval to update process.
var hasUpdated = false;
var intervalID = setInterval(function () {
updateProgress(url + '/' + response.ID, pb, statusContainer, pbPercent, intervalID);
}, 500);
}
function updateProgress(url, pb, statusContainer, pbPercent, intervalID) {
//Make an AJAX post to get the current progress from the server
$.post(url,
function (job) {
var newValue = 0;
var currentValue = pb.progressbar('value');
//The percentage value retrived from server:
newValue = (job != null && job.TotalItems != 0 ? (job.ProcessedItems / job.TotalItems * 100) : 0);
if (newValue > 0)
hasUpdated = true;
if (hasUpdated && job == null) {
newValue = 100;
statusContainer.html("<strong>Status:</strong> Finished");
clearInterval(intervalID);
}
if (!hasUpdated)
currentValue = currentValue + 1;
newValue = Math.max(currentValue, newValue);
pb.progressbar("value", newValue);
pbPercent.text(Math.round(newValue, 0) + '%');
if (job != null)
statusContainer.html("<strong>Upload:</strong> " + job.Status);
});
}
You can try something like:
1) show ProgressBar on FileLoadButton Click use javascript
2) when server completed load file use inside .aspx.cs ScriptManager.RegisterStartupScript for run javascript for hide ProgressBar