【问题标题】:ASP.NET MVC and ViewStateASP.NET MVC 和 ViewState
【发布时间】:2010-12-01 04:52:51
【问题描述】:

现在我看到了一些这样的问题,但这并不是我想问的,所以对于那些尖叫重复的人,我道歉:)。

我几乎没有接触过 ASP.NET MVC,但据我了解,没有 ViewState/ControlState... 很好。所以我的问题是保留控件状态的替代方法是什么?我们是否回到老式的 ASP,在那里我们可以通过使用控件的状态或使用 MVC 创建隐藏的表单输入来模拟 ASP.NET ViewState/ControlState 所做的事情,我们是否只是假设 AJAX 始终并保留所有状态客户端并制作 AJAX来电更新?

这个问题有一些答案,Maintaining viewstate in Asp.net mvc?,但不完全是我在答案中寻找的。​​p>

更新:感谢到目前为止的所有答案。只是为了澄清我不寻找的东西和我正在寻找的东西:

不寻找:

  • 会话解决方案
  • Cookie 解决方案
  • 不想在 MVC 中模仿 WebForms

我在/正在寻找什么:

  • 一种仅在数据未反弹到控件时保留回发状态的方法。考虑只在初始页面加载时绑定网格的场景的 WebForms,即只在必要时重新绑定数据。正如我所提到的,我并不是想模仿 WebForms,只是想知道 MVC 提供了哪些机制。

【问题讨论】:

  • 您意识到 ViewState 只是使用隐藏的表单输入,对吧? HTTP 是无状态的,ASP.NET MVC 接受了它,而不是将其抽象掉。
  • @mgroves - 是的,我确实意识到它是无国籍的。这就是为什么我提到使用隐藏输入来模拟 ViewState。
  • 我只是在说明你不会“模拟” ViewState——你会做基本相同的事情。当然,使用 MVC 模式,您(理想情况下)需要做的事情要少得多。
  • 那应该是陷阱:)

标签: asp.net-mvc ajax asp.net-mvc-2 viewstate


【解决方案1】:

该约定已经可用,无需跳过太多圈。诀窍是根据传递给视图的模型连接 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>

注意到 textbox 和 textarea 有 Title 和 Body 的 ID?现在,请注意我是如何从 View 的 Model 对象中设置值的?由于您传入了 FormCollection(并且您应该使用 FormCollection 将视图设置为强类型),您现在可以访问它。或者,没有强类型,你可以简单地使用 ViewData["Title"] (我认为)。

POOF 你神奇的 ViewState。这个概念称为约定优于配置。

现在,上面的代码是使用 FormCollection 的最简单、最原始的形式。当您开始使用 ViewModels 而不是 FormCollection 时,事情会变得有趣。您可以开始添加自己的模型/视图模型验证,并让控制器自动冒泡自定义验证错误。不过,这是另一天的答案。

我会建议使用 PostFormViewModel 而不是 Post 对象,但要每个人自己拥有。无论哪种方式,通过在 action 方法上需要一个对象,您现在可以获得一个可以调用的 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 等)。

【讨论】:

    【解决方案2】:

    View 在 MVC 模式中应该是愚蠢的,只是显示控制器给它的东西(很明显,我们确实经常在那里有一些逻辑,但前提是它不是)结果,控件不是t 对它们的状态负责,它每次都会来自控制器。

    我不能推荐 Apress 的 Steven Sanderson 的书 Pro ASP.NET MVC 来掌握这种模式及其实现。

    【讨论】:

    • 所以你的意思是我应该处理一个控件的状态,MVC 没有提供控制状态的机制?阅读下面的答案,似乎它确实有一个机制。
    • 我的意思是HTML控件不应该有任何状态,它只是模型元素的表示。模型具有“状态”,控制器将模型数据提供给视图以显示。例如,如果用户更新某些内容并单击“保存”,则控制器会处理传入的“输入”并更新模型。然后,控制器将确定用户下一步去哪里,是相同的视图还是新的视图。无论哪种方式,视图的数据都是从模型中检索出来并呈现给视图的。可能很难理解,但值得付出努力。
    • 我可以看到需要像 ViewState 这样的东西,但是,如果你有一个 KendoWindow 加载一个 PartialView,并且在主窗口中需要记录在 PartialView 中的数据。应该有一种方法可以将这些数据记录在内存中,而不必涉及模型——除了首先从 PartialView 检索数据的模型和父视图的模型。问题在于将数据从一个传输到另一个。
    【解决方案3】:

    在 Web 窗体中,控件值保留在视图状态中,因此您(理论上)不需要在每次回发时重新初始化等。这些值(再次理论上)由框架维护。

    在 ASP.NET MVC 中,如果遵循范式,则无需维护表单元素的状态。表单元素值在您的控制器可以对其进行操作的帖子上可用(验证、数据库更新等)。对于帖子处理后显示的任何表单元素,您(开发人员)负责初始化它们 - 框架不会自动为您执行此操作。

    也就是说,我已经阅读了一种名为 TempData 的机制,它允许您的控制器在重定向后将数据传递给另一个控制器。它实际上是一个会话变量(或 cookie,如果你这样配置的话),但它会在下一次请求后自动清理。

    【讨论】:

      【解决方案4】:

      答案实际上取决于您尝试为其维护状态的控件类型。对于基本的 Html 控件,维护模型的状态非常容易,为此您需要创建一个强类型视图。

      因此,如果我们有一个具有以下属性的用户模型:用户名、全名、电子邮件,我们可以在视图中执行以下操作:

      <%= 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 框架。

      【讨论】:

      • 啊,是的,对不起。工作无聊,直接跳进去
      • Np。似乎我们没有得到选票,即使我们都知道这是持久化数据的方式,而控制器不必担心它,因为连接文本框不是控制器的责任 - 控制器应该只生成模型并将其传递给视图进行渲染。我们应该得到选票,但是哦,好吧。
      • 是的,我认为如果您具有 Web 表单背景,那么这将是一种不同的思考和构建 Web 应用程序的方式。我看到了第二个带有隐藏变量的响应,不得不加入响应。是的,只要加入投票就好了......会投票给你,但我还不允许投票:(如果作者也回来并接受答案会很好。
      • @the outer edge,我的答案不仅仅是隐藏字段,还提供了您必须这样做的各种选项,自定义隐藏字段,自动隐藏字段,字段的扩展方法以保持发布的值(就像在你的回答中一样),将实例保存到单个隐藏字段的新方法,临时数据,当你使用 ajax 时要做什么......当然,还有更多我没有提到的选项,即使用 cookie,使用会话,将内容存储在数据库中(例如可恢复的多步骤向导)。
      【解决方案5】:
      • 隐藏字段,例如:

        <% using (Html.BeginForm<SomeController>(c=>c.SomeAction(null))) {%>
          <%= Html.Hidden("SomeField", Model.SomeField)%>
          <%= Html.Hidden("AnotherField", Model.AnotherField)%>
        
      • 设置特定模型并且没有任何显式字段(给你隐藏字段)。在下面的示例中,控制器使用从上一篇文章中接收到的值填充模型,因此这会在页面中启用可以根据状态进行过滤的 no 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、会话、将内容存储在 db 中(例如可恢复的多步骤向导)、传递给操作的参数。没有一种单一的机制可以统治所有这些,也不应该存在。

      【讨论】:

        【解决方案6】:

        我认为最好的方法是将原始模型序列化为隐藏字段,然后对其进行反序列化并在发布时更新模型。这与视图状态方法有些相似,只是您必须自己实现它。我用这个:

        首先我需要一些让事情变得更简单的方法:

        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>
        

        如您所见,表单中添加了一个隐藏字段(模型)。它包含原始对象的序列化信息。当表单被发布时,隐藏字段也被发布(当然)并且内容被自定义模型绑定器反序列化为原始对象,然后由控制器更新和保存。

        请注意,您要序列化的对象需要使用 Serializable 属性进行修饰,或者需要具有可以将对象转换为字符串的 TypeConverter。

        Webforms 中的视图状态使用 LosFormatter(有限对象序列化)。它还提供序列化数据的加密。

        问候...

        【讨论】:

          【解决方案7】:

          AJAX 调用是我们所做的。如果您在谈论一般的网格,请查看 JQGrid 以及他们如何推荐 AJAX 实现。

          【讨论】:

            猜你喜欢
            • 2011-11-17
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2010-11-24
            • 1970-01-01
            • 2010-10-14
            • 2011-01-14
            • 2011-08-04
            相关资源
            最近更新 更多