ASP.NET MVC 和视图状态
-
16-09-2019 - |
题
现在我已经看到了一些这样的问题,但这并不完全是我想问的,所以对于所有那些尖叫的重复,我深表歉意:)。
我几乎没有接触过 ASP.NET MVC,但据我了解,没有 ViewState/ControlState...美好的。所以我的问题是保留控件状态的替代方法是什么?我们是否回到旧式 ASP,在其中我们可以通过使用控件的状态创建隐藏表单输入来模拟 ASP.NET ViewState/ControlState 的功能,或者使用 MVC,我们是否始终假设 AJAX 并保留所有客户端状态并进行 AJAX调用更新?
这个问题有一些答案, 在 Asp.net mvc 中维护视图状态?, ,但不完全是我在答案中寻找的内容。
更新:感谢到目前为止所有的答案。只是为了弄清楚我不寻找什么和我正在寻找什么:
不寻找:
- 会话解决方案
- 饼干解决方案
- 不想在 MVC 中模仿 WebForms
我正在/正在寻找什么:
- 如果数据未反弹到控件,则仅保留回发状态的方法。将 WebForms 想象为仅在初始页面加载时绑定网格的场景,即仅在必要时重新绑定数据。正如我提到的,我并不是想模仿 WebForms,只是想知道 MVC 提供了什么机制。
解决方案
该公约已经可用,无需跨越太多障碍。诀窍是根据传递到视图中的模型连接 TextBox 值。
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult CreatePost()
{
return View();
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult CreatePost(FormCollection formCollection)
{
try
{
// do your logic here
// maybe u want to stop and return the form
return View(formCollection);
}
catch
{
// this will pass the collection back to the ViewEngine
return View(formCollection);
}
}
接下来发生的事情是 ViewEngine 采用 formCollection 并使用 Html 帮助程序将集合中的键与视图中的 ID 名称/值进行匹配。例如:
<div id="content">
<% using (Html.BeginForm()) { %>
Enter the Post Title: <%= Html.TextBox("Title", Model["Title"], 50) %><br />
Enter the Post Body: <%= Html.TextArea("Body", Model["Body"]) %><br />
<%= Html.SubmitButton() %>
<% } %>
</div>
注意到文本框和文本区域有标题和正文的 ID 了吗?现在,请注意我如何设置视图模型对象的值?由于您传入了 FormCollection(并且您应该使用 FormCollection 将视图设置为强类型),因此您现在可以访问它。或者,如果没有强类型,您可以简单地使用 ViewData["Title"] (我认为)。
普罗夫 你神奇的 ViewState。这个概念称为约定优于配置。
现在,上面的代码是使用 FormCollection 的最简单、最原始的形式。当您开始使用 ViewModel 而不是 FormCollection 时,事情会变得有趣。您可以开始添加自己的模型/视图模型验证,并让控制器自动冒泡显示自定义验证错误。不过,这是另一天的答案。
我建议使用 PostFormViewModel 而不是 Post 对象,但每个人都有自己的。无论哪种方式,通过在操作方法上要求一个对象,您现在可以获得一个可以调用的 IsValid() 方法。
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult CreatePost(Post post)
{
// errors should already be in the collection here
if (false == ModelState.IsValid())
return View(post);
try
{
// do your logic here
// maybe u want to stop and return the form
return View(post);
}
catch
{
// this will pass the collection back to the ViewEngine
return View(post);
}
}
您的强类型视图需要调整:
<div id="content">
<% using (Html.BeginForm()) { %>
Enter the Post Title: <%= Html.TextBox("Title", Model.Title, 50) %><br />
Enter the Post Body: <%= Html.TextArea("Body", Model.Body) %><br />
<%= Html.SubmitButton() %>
<% } %>
</div>
您可以更进一步,直接从您在控制器中设置的 ModelState 在视图中显示错误。
<div id="content">
<%= Html.ValidationSummary() %>
<% using (Html.BeginForm()) { %>
Enter the Post Title:
<%= Html.TextBox("Title", Model.Title, 50) %>
<%= Html.ValidationMessage("Title") %><br />
Enter the Post Body:
<%= Html.TextArea("Body", Model.Body) %>
<%= Html.ValidationMessage("Body") %><br />
<%= Html.SubmitButton() %>
<% } %>
</div>
这种方法的有趣之处在于,您会注意到我没有设置验证摘要,也没有在视图中设置单独的验证消息。我喜欢实践 DDD 概念,这意味着我的验证消息(和摘要)在我的域中控制并以集合的形式传递。然后,我循环访问该集合(如果存在任何错误)并将它们添加到当前的 ModelState.AddErrors 集合中。当你返回 View(post) 时,其余的都是自动的。
很多约定都已经结束了。我强烈推荐的几本书更详细地介绍了这些模式:
按照这个顺序,第一个部分涵盖了整个 MVC 框架的原始细节。后者涵盖了 Microsoft 官方领域之外的先进技术,并提供了一些外部工具来让您的生活变得更加轻松(Castle Windsor、Moq 等)。
其他提示
该视图应该在MVC模式中是愚蠢的,只显示控制器给它的内容(显然我们经常在那里获得某种逻辑,但前提是不可能),因此,控件不负责他们的状态,每次都会来自控制器。
我不能推荐史蒂文·桑德森(Steven Sanderson)的书ASP.NET MVC,足以掌握这种模式和这种实现。
在Web表单中,控制值在ViewState中维护,因此您(理论上)不需要重新初始化,并且每个回发。这些值(理论上再次)由框架维护。
在ASP.NET MVC中,如果您遵循范式,则无需在表单元素上维护状态。表单元素值可在邮政上可用,您的控制器可以在其中作用(验证,数据库更新等)。对于一旦帖子处理后显示的任何形式元素,您(开发人员)负责初始化它们 - 框架不会自动为您做到这一点。
也就是说,我已经阅读了一种称为tempdata的机制,该机制使您的控制器可以在重定向后传递数据到另一个控制器。它实际上是一个会话变量(或cookie,如果您以此为单位),但是在下一个请求之后会自动清理。
答案实际上取决于您要维护状态的控件类型。对于基本的HTML控件,使用模型非常容易维护状态,您需要创建一个强烈键入的视图。
因此,如果我们拥有具有属性的用户模型:用户名,fullname,电子邮件,我们可以在视图中执行以下操作:
<%= Html.ValidationSummary() %>
<% using (Html.BeginForm()) { %>
<fieldset>
<legend>User details</legend>
<%= Html.AntiForgeryToken() %>
<p>
<label for="Username">Username:</label>
<%= Html.Textbox("Username", Model.Username, "*") %>
</p>
<p>
<label for="FullName">FullName:</label>
<%= Html.Textbox("FullName", Model.FullName, "*") %>
</p>
<p>
<label for="Email">Email:</label>
<%= Html.Textbox("Email", Model.Email, "*") %>
</p>
<p>
<input type+"submit" value="Save user" />
</p>
</fieldset>
<% } %>
然后,我们将有两个显示此视图的控制器操作,一个用于获取,另一种是发帖:
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult User()
{
return View(new User())
}
[AcceptVerbs(HttpVerbs.Post)]
[ValidateAntiForgeryToken]
public ActionResult User([Bind(Include = "Username,FullName,Email")]User user)
{
if (!ModelState.IsValid()) return View(user);
try
{
user.save()
// return the view again or redirect the user to another page
}
catch(Exception e)
{
ViewData["Message"] = e.Message;
return View(user)
}
}
这是你想要的?还是要维护未在请求之间以形式显示的模型状态?
要记住的关键是,您的代码在请求和结束的过程中在服务器上执行,您可以在请求之间传递的唯一信息是基本的HTML表单数据,URL参数和会话信息。
正如其他人提到的那样,我强烈建议史蒂夫·桑德森(Steve Sandersan)的Pro ASP.NET MVC框架,以完全了解使用MVC框架。
隐藏的字段,例如:
<% using (Html.BeginForm<SomeController>(c=>c.SomeAction(null))) {%> <%= Html.Hidden("SomeField", Model.SomeField)%> <%= Html.Hidden("AnotherField", Model.AnotherField)%>
设置特定模型并且没有任何显式字段(给您隐藏的字段)。在下面的示例中,该模型由控制器填充,并从上一篇文章中收到的值填充,因此在页面中可以根据状态过滤的页面中没有JS选项:
Some Filter: <% using( Html.BeginForm<SomeController>( c => c.SomeAction(model.SomeField, model.AnotherField, model.YetAnotherField, null, model.SomeOtherField) )) { %> <%= Html.DropDownList("status", Model.StatusSelectList)%> <input type="submit" value="Filter" class="button" /> <% } %>
- 使用扩展方法来创建字段,如果您只想在您显示在提交表单上显示失败的验证消息时填充有帖子值的字段
- 在ASP.NET MVC 2上,他们引入了一种在隐藏字段中保存实例的方法...编码 +(我认为)签名
- tempdata如果以上所有内容都没有这样做(通过会话 - 在下一个请求中清理)
- 正如您提到的,当使用AJAX时,状态已经在客户端站点的先前加载字段中。如果您需要完成完整的帖子,请更新您可能需要使用JS的任何字段。
以上是可以在不同情况下使用的所有独立选项。我没有提及更多的选项,即cookie,会话,将东西存储在数据库中(例如,对于重新启动的多步向导),参数传递给了一个操作。没有一种单一的机制来统治它们,也不应该。
我认为,这样做的最好方法是将您的原始模型序列化为隐藏的字段,然后对其进行序列化并在Post上更新模型。这与ViewState方法有些类似,只有您必须自己实施。我用这个:
首先,我需要一些使事情变得更容易的方法:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using LuvDaSun.Extensions;
using System.Web.UI;
namespace LuvDaSun.Web.Mvc
{
public static class HtmlHelperExtensions
{
static LosFormatter _losFormatter = new LosFormatter();
public static string Serialize(this HtmlHelper helper, object objectInstance)
{
var sb = new StringBuilder();
using (var writer = new System.IO.StringWriter(sb))
{
_losFormatter.Serialize(writer, objectInstance);
}
return sb.ToString();
}
}
[AttributeUsage(AttributeTargets.Parameter)]
public class DeserializeAttribute : CustomModelBinderAttribute
{
public override IModelBinder GetBinder()
{
return new DeserializeModelBinder();
}
}
public class DeserializeModelBinder : IModelBinder
{
static LosFormatter _losFormatter = new LosFormatter();
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType.IsArray)
{
var type = bindingContext.ModelType.GetElementType();
var serializedObjects = (string[])bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ConvertTo(typeof(string[]));
var deserializedObjects = Array.CreateInstance(bindingContext.ModelType.GetElementType(), serializedObjects.Length);
for (var index = 0; index < serializedObjects.Length; index++)
{
var serializedObject = serializedObjects[index];
var deserializedObject = _losFormatter.Deserialize(serializedObject);
deserializedObjects.SetValue(deserializedObject, index);
}
return deserializedObjects;
}
else
{
var serializedObject = (string)bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ConvertTo(typeof(string));
var deserializedObject = _losFormatter.Deserialize(serializedObject);
return deserializedObject;
}
}
}
}
然后在我的控制器中,我有这样的东西(更新产品)
public ActionResult Update(string productKey)
{
var model = _shopping.RetrieveProduct(productKey);
return View(model);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Update([Deserialize]Shopping.IProduct _model, FormCollection collection)
{
UpdateModel(model);
model.Save();
return RedirectAfterPost();
}
我需要一个隐藏的字段,该字段将序列化对象以形式保存:
<%
using (Html.BeginRouteForm("Product", FormMethod.Post, new { id = UniqueID, }))
{
%>
<%= Html.Hidden("Model", Html.Serialize(Model)) %>
<h1>
Product bewerken</h1>
<p>
<label for="<%=UniqueID %>_Name">
Naam:</label>
<input id="<%=UniqueID %>_Name" name="Name" type="text" value="<%= Html.AttributeEncode(Model.Name) %>"
class="required" />
<br />
</p>
<p>
Omschrijving:<br />
<textarea id="<%= UniqueID %>_Description" name="Description" cols="40" rows="8"><%= Html.Encode(Model.Description) %></textarea>
<br />
</p>
<p>
<label for="<%=UniqueID %>_Price">
Prijs:</label>
<input id="<%= UniqueID %>_Price" name="Price" type="text" value="<%= Model.Price.ToString("0.00") %>"
class="required" />
<br />
</p>
<ul class="Commands">
<li><a href="" class="ClosePopup">Annuleren</a></li>
<li>
<input type="submit" value="Opslaan" /></li>
</ul>
<%
}
%>
<script type="text/javascript">
jQuery('#<%= UniqueID %>').validate();
</script>
如您所见,将隐藏的字段(模型)添加到表单中。它包含原始对象的序列化信息。当发布表单时,隐藏的字段也将(当然)发布,并且内容由自定义模型框对原始对象进行了应对,然后由控制器更新和保存。
请注意,您要序列化的对象需要用可序列化属性进行装饰,或者需要具有可以将对象转换为字符串的键入verterter。
WebForms中的ViewState使用了LosFormatter(有限的对象序列化)。它还提供序列化数据的加密。
问候...
Ajax调用是我们的工作。如果您一般谈论网格,请查看 JQGrid 以及他们如何推荐Ajax实施。