javascript – 在长时间运行的服务器操作期间如何与用户交互(例如确认对话框)?

我有一个MVC5应用程序.有一个特定的操作可以处理上传的大型CSV文件,有时在此任务期间需要用户提供其他信息.例如,在第5行,软件需要向用户显示他确实想要用它做什么,等等.在
Winforms环境中,这很容易,但我不知道如何在网上实现相同的功能. .

我更喜欢同步方式,以便在确认之前阻止服务器线程.否则我觉得我必须完全重写逻辑.

让事情变得更加困难的是,我不仅需要简单的确认,而且还会不时地为用户提供更复杂的选择,这些选择无法在客户端同步实现(只有原生的简单确认)是同步AFAIK).

任何建议或提示将不胜感激,更完整的简短指南.

在这个例子中,客户端调用一个返回数字0,1,2,…,99,100的方法.假设我们的用户可能讨厌5个可分割的数字.我们需要实现一个允许用户使用的功能如果他们这样做,就排除这些数字.用户不喜欢为未来做好计划,因此他们希望在处理过程中实时选择他们喜欢的数字.

[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(', '));
});

最佳答案 我把一个例子放在一起,然后把它放在github上让你看一看.

MVC5 long running input-required example.

请注意,这不一定是设计此方法的最佳方式.这是我最初的方法,没有经过深思熟虑.可能存在更灵活或更复杂或更推荐的模式.

基本上它只是在更改时将Job的状态存储在数据库中(在示例中使用Entity Framework).
与某些类型的长期运行的“同步”方法相比,持久化到磁盘或数据库具有明显的优势.

>它在等待输入时不会锁定资源
>在发生崩溃或服务器超时的情况下,它可以防止数据丢失
>如果您希望在扩展环境中运行或恢复,或者在完全不同的服务器(例如,非前置VM)上,它可以提供灵活性.
>它可以更好地管理当前正在运行的作业.

对于这个例子,我选择不使用Signalr,因为它不会增加重要的价值.在长时间运行的作业(例如,5分钟)的情况下,亚秒响应不会增加用户体验.我建议你每1-2秒从javascript轮询一次.更简单.

请注意,一些代码是非常hackish;例如,在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's resumed, otherwise we need another Get to check the state. 
        return Json(job);
    }

    /// <summary>
    /// Stuff this in it's 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;
        }
    }
}
点赞