如何在长时间运行的服务器操作期间与用户交互(例如确认对话框)
How to interact with user (for example a confirm dialog) during a long running server action?
我有一个MVC5应用程序。有一个特定的操作处理上传的大型CSV文件,有时在此任务期间需要用户提供额外的信息。例如,在第5行,软件需要向用户显示一个确认,如果他真的想用它做些什么,等等。在Winforms环境中,这是非常容易的,但是我不知道如何在web上实现相同的。
我更喜欢同步的方式,这样服务器线程将被阻塞,直到确认。否则我觉得我将不得不完全重写逻辑。
使事情变得更加困难的是,我不仅需要简单的确认,而且还需要不时地为用户提供更复杂的选择,这些选择不能在客户端同步实现(只有本地简单的confirm
是同步的)。
任何建议或提示将不胜感激,一个完整的简短指南甚至更多。
例子在这个例子中,客户端调用一个返回数字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长时间运行输入要求示例。
请注意,这不一定是最好的设计方法。这是我最初的方法,没有经过很多思考。可能有更灵活或更复杂的模式,或者更推荐的模式。
基本上,它只是在数据库中存储作业的状态(在示例中使用实体框架),每当它发生变化时。与某些类型的长时间运行的"同步"方法相比,持久化到磁盘或数据库具有明显的优势。
- 在等待输入时不锁定资源
- 在崩溃或服务器超时的情况下防止数据丢失
- 它允许灵活性,如果你想在一个扩展的环境中运行或恢复,或者在一个完全不同的服务器上(例如,一个非前端VM)。
- 允许更好地管理当前运行的作业。
对于本例,我选择不使用Signalr,因为它不会增加重要的值。对于长时间运行的作业(比如5分钟以上),亚秒级的响应不会增加用户体验。我建议每1-2秒从javascript轮询一次。更加简单。
请注意,有些代码相当粗糙;例如,在ResumableJobState表上复制Input字段。
流可能看起来像这样,
- 上传文件>返回文件名//在我的例子中不是impl
- call StartJob(filename)>返回(Json)Job
- Poll GetJobState(jobId)>返回(Json)Job
- 如果(Json)工作。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;
}
}
}
- Javascript没有't更新DOM,直到用户交互
- 使用JavaScript验证用户交互/输入-这是一种很好的方法
- Ajax 调用在 IOS 中的用户交互之前不起作用
- 当用户与谷歌地图交互时,如何从中获取信息
- 在C#中,我如何对客户端用户交互和javascript(jQuery)代码进行单元测试
- 用户键盘交互:视图或事件
- JavaScript/Wicket:如何显示阻止用户交互的覆盖
- 对用户交互进行单选按钮检查
- 谷歌应用程序脚本:如何在没有用户交互的情况下调用绑定到处理程序的函数
- 用户交互启用了 Javascript
- 设置用户交互计时器
- HTML5 + JS:跟踪用户交互
- 是否可以像真实用户交互那样进行JavaScript操作来更改文档的activeElement
- HTML,JS:加载音频是否需要在移动浏览器上进行用户交互
- 有没有办法防止用户交互暂停动画
- 好奇:禁用jQuery和Javascript交互,除非用户已登录
- 跟踪网站上的用户交互
- 当用户与IFrame JavaScript交互时进行检测
- 我可以在手机上自动播放上一页用户交互的音频吗
- 从web应用程序,如何在没有用户交互的情况下在客户端计算机上运行exe