How to interact with the user (for example, a confirmation dialog box) during a long server operation?

I have an MVC5 application. There is a specific action that processes the downloaded large CSV file, and sometimes it requires additional information from the user during this task. For example, in line 5, the software should show confirmation to the user that he really wants to do something with him, etc. In Winforms, this was very simple, but I have no idea how I can implement the same thing on the Internet.

I would prefer a synchronous path so that the server thread will be blocked until confirmation. Otherwise, I feel like I have to completely rewrite the logic.

What makes things even more complicated is that I will need not only simple confirmation, but from time to time there may be more complex options for the user that cannot be implemented synchronously on the client side (only native simple confirmis synchronous AFAIK) .

Any suggestions or tips would be appreciated, a complete short guide even further.

Example

In this example, the client calls a method that returns numbers 0, 1, 2, ..., 99, 100. Let them say that our users potentially hate numbers that are divisible by 5. We need to implement a function that allows users to exclude these numbers if they want to. Users do not like to plan for the future, so they want to choose whether they like the number or not in real time when the processing takes place.

[Controller]

public enum ConfirmResult {
  Yes = 0,
  No = 1,
  YesToAll = 2,
  NoToAll = 3
}

...

public JsonResult SomeProcessingAction() {
  var result = new List<int>();
  for (int i = 0; i <= 100; i++) {
    if (i%5==0) {
      // sketch implementation for example purposes
      if (Confirm(string.Format("The number {0} is dividable by 5. Are you sure you want to include it?", i) == ConfirmResult.No)
        continue;
    }
    result.Add(i);
  }
  return Json(result);
}

public ConfirmResult Confirm(string message) {
  // ... show confirm message on client-side and block until the response comes back... or anything else 
}


[Javascript]
// sketch...
$.post('mycontroller/someprocessing', function(result) {
  $('#results').text("Your final numbers: " + result.join(', '));
});
+4
1

github, . MVC5 -.

, . . , .

( Entity Framework ) , . "" .

  • - .
  • , (, , ).
  • .

Signalr, . (, 5 + ), . javascript 1-2 . .

, ; , , ResumableJobState.

,

  • > // impl
  • StartJob (_) > (Json) Job
  • GetJobState (jobId) > (Json) Job
  • (Json) Job.RequiredInputType , ,
  • PostInput

JobController.

public class JobController : Controller
{
    private Context _context;
    private JobinatorService _jobinatorService;
    public JobController()
    {
        _context = new Context();
        _jobinatorService = new JobinatorService(_context);
    }

    public ActionResult Index()
    {
        ViewBag.ActiveJobs = _context.LongRunningJobs.Where(t => t.State != "Completed").ToList();//TODO, filter by logged in User
        return View();
    }

    [HttpPost]
    public JsonResult StartJob(string filename)//or maybe you've already uploaded and have a fileId instead
    {
        var jobState = new ResumableJobState
        {
            CurrentIteration = 0,
            InputFile = filename,
            OutputFile = filename + "_output.csv"
        };

        var job = new LongRunningJob
        {
            State = "Running",
            ResumableJobState = jobState
        };

        _context.ResumableJobStates.Add(jobState);
        _context.LongRunningJobs.Add(job);
        var result = _context.SaveChanges();
        if (result == 0) throw new Exception("Error saving to database");

        _jobinatorService.StartOrResume(job);

        return Json(job);
    }

    [HttpGet]
    public JsonResult GetJobState(int jobId)
    {
        var job = _context.LongRunningJobs.Include("ResumableJobState.RequiredInputType").FirstOrDefault(t => t.Id == jobId);
        if (job == null)
            throw new HttpException(404, "No job found with that Id");
        return Json(job, JsonRequestBehavior.AllowGet);
    }

    [HttpPost]
    public JsonResult PostInput(int jobId, RequiredInputType userInput)
    {
        if (!ModelState.IsValid)
            throw new HttpException(500, "Bad input");

        var job = _context.LongRunningJobs.Include("ResumableJobState.RequiredInputType").FirstOrDefault(t => t.Id == jobId);
        job.ResumableJobState.BoolInput = userInput.BoolValue;
        job.ResumableJobState.IntInput = userInput.IntValue;
        job.ResumableJobState.FloatInput = userInput.FloatValue;
        job.ResumableJobState.StringInput = userInput.StringValue;
        _context.SaveChanges();

        if (job == null)
            throw new HttpException(404, "No job found with that Id");

        if (userInput.InputName == job.ResumableJobState.RequiredInputType.InputName)//Do some checks to see if they provided input matching the requirements
            _jobinatorService.StartOrResume(job);
        //TODO have the jobinator return the State after it resumed, otherwise we need another Get to check the state. 
        return Json(job);
    }

    /// <summary>
    /// Stuff this in it own service.  This way, you could use it in other places; for example starting scheduled jobs from a cron job
    /// </summary>
    public class JobinatorService//Ideally use Dependency Injection, or something good practicey to get an instance of this
    {
        private Context _context = new Context();
        private string _filePath = "";
        public JobinatorService(Context context)
        {
            _context = context;
            _filePath = AppDomain.CurrentDomain.GetData("DataDirectory").ToString() + "/";
        }

        public void StartOrResume(LongRunningJob job)
        {
            Task.Run(() =>
            {
                using (var inputFile = System.IO.File.OpenRead(_filePath + job.ResumableJobState.InputFile))
                using (var outputFile = System.IO.File.OpenWrite(_filePath + job.ResumableJobState.OutputFile))
                {
                    inputFile.Position = job.ResumableJobState.CurrentIteration;
                    for (int i = (int)inputFile.Position; i < inputFile.Length; i++)//casting long to int, what could possibly go wrong?
                    {

                        if (job.State == "Input Required" && job.ResumableJobState.RequiredInputType != null)
                        {//We needed input and received it
                            //You might want to do a switch..case on the various inputs, and branch into different functions

                            if (job.ResumableJobState.RequiredInputType.InputName == "6*7")
                                if (job.ResumableJobState.RequiredInputType.IntValue.Value == 42)
                                    break;//Pass Go, collect 42 dollars;
                        }
                        outputFile.WriteByte((byte)inputFile.ReadByte());//Don't try this at home!

                        job.ResumableJobState.CurrentIteration = i;//or row, or line, or however you delimit processing
                        job.ResumableJobState.InputFileBufferReadPosition = inputFile.Position;//or something

                        if (i % 7 == 0)
                            job.ResumableJobState.RequiredInputType = _context.RequiredInputTypes.First(t => t.InputName == "Row 7 Input");
                        if (i % 42 == 0)
                            job.ResumableJobState.RequiredInputType = _context.RequiredInputTypes.First(t => t.InputName == "6*7");

                        if (job.ResumableJobState.RequiredInputType != null)
                            job.State = "Input Required";
                        _context.SaveChanges();
                        if (job.State != "Running")
                            return;
                    }
                    job.State = "Completed";
                    _context.SaveChanges();
                }
            });
            return;
        }
    }
}
+2

Source: https://habr.com/ru/post/1608345/


All Articles