ASP.Net MVC和状态 - 如何在请求之间保持状态

54
作为一名经验相当丰富的ASP.Net开发人员,最近开始使用MVC,我发现自己有点难以从传统的“服务器控件和事件处理程序”的方式切换到更动态的MVC方式。我想我正在慢慢适应,但有时MVC的“魔法”会让我感到困惑。
我的当前场景是创建一个网页,允许用户浏览本地文件,将其上传到服务器,并重复此过程,直到他拥有一个要处理的文件列表。当他对文件列表感到满意(它将显示在页面上的网格中)时,他将单击一个按钮来处理这些文件并提取一些数据,这些数据将存储在数据库中。
最后一部分不太重要,现在我正在努力解决一些微不足道的事情,例如建立文件列表并在请求之间保持该列表的持久性。在传统方法中,这非常简单 - 数据将保留在ViewState中。但在MVC中,我需要在控制器和视图之间传递数据,我并不完全明白这应该如何工作。
我想我最好发布一下我编写的不太完整的代码来解释问题。
为了保留我的文件列表数据,我创建了一个视图模型,基本上是文件类型列表以及一些额外的元数据:
public class ImportDataViewModel
{
    public ImportDataViewModel()
    {
        Files = new List<ImportDataFile>();
    }

    public List<ImportDataFile> Files { get; set; }
...

在视图中,我有一个用于浏览和上传文件的表单:

<form action="AddImportFile" method="post" enctype="multipart/form-data">
  <label for="file">
    Filename:</label>
  <input type="file" name="file" id="file" />
  <input type="submit" />
  </form>
视图使用viewmodel作为其模型:
@model MHP.ViewModels.ImportDataViewModel

这会将文件发送到我的操作:

public ActionResult AddImportFile(HttpPostedFileBase file, ImportDataViewModel importData)
    {

        if (file.ContentLength > 0)
        {
            ImportDataFile idFile = new ImportDataFile { File = file };
            importData.Files.Add(idFile);
        }

        return View("DataImport", importData);
    }

该操作返回DataImport页面的视图,以及包含文件列表的viewmodel实例。

在某个点之前,这很好用,我可以浏览文件并上传它,我可以在操作内部看到viewmodel数据,如果在视图中设置断点并调试"this.Model" ,那一切都很好。

但是,如果我尝试上传另一个文件,当在AddImportFile操作内设置断点时,importData参数为空。因此,视图显然没有将其模型的当前实例传递给操作。

在我经历过的MVC示例中,模型实例会“神奇地”作为参数传递给操作方法,那么为什么现在为空呢?

我认为真正的问题是我的MVC理解有限,可能有一个非常简单的解决方案。无论如何,如果有人能指点我正��的方向,我将不胜感激。


你没有在服务器上存储上传的文件。你希望它们在postbacks之间持久化,但是如何实现呢?在你的<form>中只有一个单独的文件输入字段。因此,在控制器操作中,你只能期望得到这个文件。没有更多了。在ASP.NET MVC中没有ViewState,也没有魔法。 - Darin Dimitrov
Darin,感谢您的评论。我没有将文件存储在服务器上是正确的。我可以将它存储在数据库中,但这会浪费数据库空间和用户耐心,因为这需要很长时间,而且实际上并不需要。我想做的就是将其作为变量保存在服务器内存中,直到用户决定处理它。然后,我将把处理的输出存储到数据库中。我认为我可能是在错误地使用MVC,或者我只是不知道该怎么做。我还不知道哪一个... - TMan
3
在服务器内存中保存文件是最糟糕的事情之一,不要以任何代价这样做。这不仅会消耗服务器大量的内存,而且请勿忘记 IIS 可能随时回收应用程序域,导致所有保存在内存中的数据丢失。此外,如果您在服务器群集中运行,如果将数据存储在服务器的内存中,这意味着农场中只有一个服务器拥有这些信息,如果负载均衡器将后续请求分派到农场中的另一台服务器,则会丢失该信息。 - Darin Dimitrov
1
读这些“旧”的问题,想想世界在短短五年内发生了多少变化,真是有趣!自那时以来,我已经处理过很多次文件上传,现在对它的看法与当时完全不同。我同意史蒂夫的建议,暂时存储文件可能是最好的选择,尽管我倾向于使用Azure Blob存储而不是Web服务器上的文件夹。甚至我不再使用IIS,而是全部使用Azure Web应用程序或Azure函数。 - TMan
1
我也收回之前关于在内存中存储东西的说法,我几乎不敢相信我竟然认为那是个好主意! :) 我想,为了辩护自己,当时涉及的应用程序是一个小型企业应用程序,用户很少,文件也很小,但无论如何,今天我都不会考虑这样做!我想我们都应该感激能够回顾过去糟糕错误的能力,并得出结论,我们已经取得了很大的进步 :) 干杯! - TMan
显示剩余2条评论
2个回答

51

从我发布这个问题以来已经过了一些时间,当时我的MVC经验和知识有限,因此问题的描述有点片面。尽管如此,我收到了一些非常有用的回答,最终帮助我找到了解决方案,并对MVC有了更深入的理解。

最初让我困惑的是,你可以在控制器中使用强类型对象作为参数,像这样:

public ActionResult DoSomething(MyClass myObject)...

这个对象来自相同的控制器:

...
return View(myObject);
...

这让我相信这个对象在这两个步骤中一直存在,我想我可以期望你将其发送到视图,做一些操作,然后“神奇地”再次将其发送回控制器。

阅读有关模型绑定的文章后,我明白这当然不是事实。视图是完全死亡和静态的,除非您将信息存储在其他地方,否则它会消失。

回到问题上来,即从客户端选择和上传文件,并建立要显示的这些文件列表,我意识到通常有三种方法在MVC中在请求之间存储信息:

  1. 您可以将信息存储在视图中的表单字段中,稍后将其提交回控制器
  2. 您可以将其持久化在某种存储中,例如文件或数据库
  3. 通过访问贯穿请求的对象在服务器内存中存储它,例如会话变量

在我的情况下,我基本上有两种类型的信息需要持久化: 1. 文件元数据(文件名、文件大小等) 2. 文件内容

“按照规范”的方法可能是将元数据存储在表单字段中,将文件内容存储在文件或数据库中。但也有另一种方法。由于我知道我的文件非常小,只有几个,并且这种解决方案永远不会部署在服务器群组或类似的地方,因此我想探索第三种选项:会话变量。这些文件也没有持续存储的意义-它们被处理并且被丢弃,所以我不想将它们存储在我的数据库中。

阅读了这篇优秀的文章:使用Dynamics访问ASP.NET会话数据之后。

我被说服了。我简单地按照文章中描述的创建了一个sessionbag类,然后我可以在我的控制器中执行以下操作:

    [HttpPost]
    public ActionResult AddImportFile(HttpPostedFileBase file)
    {

        ImportDataViewModel importData = SessionBag.Current.ImportData;
        if (importData == null) importData = new ImportDataViewModel();

        if (file == null)
            return RedirectToAction("DataImport");

        if (file.ContentLength > 0)
        {
            ImportDataFile idFile = new ImportDataFile { File = file };
            importData.Files.Add(idFile);
        }

        SessionBag.Current.ImportData = importData;

        return RedirectToAction("DataImport");
    }

我充分意识到,在大多数情况下,这将是一个糟糕的解决方案。但由于文件占用的几KB服务器内存以及一切的简单性,我认为这对我非常有效。

使用SessionBag的额外好处是,如果用户选择了不同的菜单项,然后返回,文件列表仍将存在。如果选择表单字段/文件存储选项,则不会出现这种情况。

最后,我意识到SessionBag非常容易被滥用,因为它的使用方式非常简单。但是,如果您将其用于它的用途,也就是会话数据,我认为它可以成为一个强大的工具。


好的总结和解释。 - Lester
1
会话过期怎么办? - garik
@garik 处理会话重置的一种解决方案是使用一个 Out-of-process 会话状态服务器或 SQL Server,例如:https://technet.microsoft.com/zh-cn/library/cc754032(v=ws.10).aspx - Jeremy Thompson

11

关于上传

1) 可以考虑使用带有HTML的AJAX上传程序,让用户在将文件发送到服务器之前选择多个文件。这个名为BlueImp Jquery AJAX文件上传程序非常惊人,具有出色的api:Blueimp Jquery File Upload。它可以让用户拖放或多选多个文件,编辑文件顺序,包含/排除等等,然后当他们满意时,可以按“上传”按钮将其发送到您的控制器或上传处理程序进行服务器端处理。

2) 您可以使每次上传都持久保存到数据库中,但是您需要重新加载整个页面并编写一些额外的视图模型和剃刀代码来实现列表效果,这可能无法响应...

关于保持WebForms/MVC状态

保持请求之间的状态有点像黑魔法和巫术。进入ASP.NET MVC时,请理解Web应用程序使用请求和响应进行通信。因此,请接受Web作为无状态,并从那里开始开发!当您的模型通过控制器发布时,它随着控制器中的任何变量一起消失!不过,在它消失之前,您可以将其内容存储在数据库中以供稍后检索。

Web应用程序无法像桌面应用程序一样保持真正的状态。人们采用许多Ajax框架和一些巫术工具来模拟HTTP环境中的状态。状态的模拟实际上只是状态性的虚假模仿。ASP.NET Web Forms尽最大努力模拟状态,将HTTP的无状态特性隐藏在开发者背后。当您试图将自己的AJAX代码与Web Forms标记代码及其自己的Ajax框架结合使用时,可能会遇到很多问题。

我很高兴看到您正在学习MVC

开玩笑的,如果您了解MVC/HTTP/无状态的思维方式,那么将很容易将这些模式应用于其他超级流行的框架,如Ruby on Rails、SpringMVC(Java)、Django(Python)、CakePHP等...这种知识的轻松转移将帮助您成为一个更好的开发者,并非常擅长Ajax。

很高兴你在学习MVC 3,我曾经实习过几个大公司,他们有一些非常庞大的ASP.NET Web Forms项目,代码到处飞来飞去只是为了更改数据库中几个数字值 (-_-') 感觉就像是用剪刀编织婴儿袜子。一次简单的错误操作,整个项目都会瘫痪。这就像是在PHP上开发一样,容易出汗,不确定发生了什么和在哪一行。几乎不可能进行调试和更新。


虽然有点显而易见,但我会在2)a)中添加使用隐藏字段来维护请求之间的文件列表。最终,Web表单中的ViewState抽象使用了隐藏字段,所以这并不是一个坏主意,而且比数据库选项更容易实现。 - Kiran
Max,感谢你详细的回答。关于1)- 是的,我可能会在以后尝试实现多文件上传功能,使其更加用户友好,但这并不是真正的核心问题。我认为这里的核心问题更多地涉及有状态与无状态。我之前也遇到过这种讨论,但我并不完全理解整个“拥抱MVC的无状态本质”的事情。你是说我们不应该在我们的Web应用程序中实现状态吗?并不是所有东西都应该存储在数据库中。以购物车为例。人类天生就是有状态的,那么为什么Web应用程序不能是有状态的呢? - TMan
Kiran,我认为你是对的。我现在正在阅读有关模型绑定的内容,它似乎代表了我这种情况中所感知到的“魔法”的很大一部分——即视图中的表单字段如何突然转换为控制器中的强类型类实例。如果在不理解基本原理的情况下尝试将“魔法”应用于不同的场景,那么当然会出错。我想我现在走在了正确的道路上。 - TMan
我认为不能像WinForms或桌面应用程序那样使Web应用程序真正具有状态。关于ASP.NET状态和其他形式的AJAX状态保持都是对状态的可敬模仿 :- \。当然,它可能会像有状态的应用程序一样运行,但在调试“几乎有状态”的Web应用程序时,您必须对自己诚实并意识到不存在状态。这就是我认为“拥抱无状态性质”的含义。 - Max Alexander
这对我关于MVC的一些事情进行了确认,非常有帮助。谢谢!好建议。 - Steve

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接