【问题标题】:Effectively avoiding ViewBag in ASP.NET MVC有效避免 ASP.NET MVC 中的 ViewBag
【发布时间】:2013-03-12 20:39:00
【问题描述】:

您如何处理避免 ViewBag 的问题,因为它存在动态出错的风险,但也避免每次都必须填充新的 ViewModel 并将其传递回视图。例如,我不想更改以下内容以公开通常填充在 ViewBag 中的常见数据。

[HttpGet]
void Index() 
{ 
    return View(); 
}

[HttpGet]
void Index() 
{
    var messages = new MessageCollection();
    messages.AddError("Uh oh!");

    return View(messages);
}

我应该在管道中的什么位置添加一个像 ViewBag 这样的自定义且强类型的属性,但它会优雅地暴露在 Controller 和 View 中。当我不需要特定的 ViewModel 时,我宁愿这样做......

[HttpGet]
void Index()
{
    Messages.AddError("Uh oh!");

    return View();
}

在视图方面,而不是@((IMessageCollection)ViewBag.Messages).Errors id 而是具有类似@Messages.Errors 的东西,它是强类型的并且随处可用。另外,我不想只是将它投射到我的 Razor 视图顶部的代码块中。

在 WebForms 中,我会做一些事情,比如把它作为一个基本页面,然后有一个用户控件,可以根据需要在页面上隐藏或显示。控制器与视图分离后,我不确定如何复制类似的行为。

这可能吗?或者最好的设计方法是什么?

谢谢, 斯科特

【问题讨论】:

  • 你真的有两个选择:ViewBag 和模型中的专用属性

标签: asp.net-mvc architecture viewmodel viewbag


【解决方案1】:

在 ASP.NET MVC 中,您可以使用 ViewBagViewDataTempData(有关详细信息,请参阅 this blog post)。 ViewBagViewData 字典的动态包装器。如果您执行ViewBag.Prop = "value",则相当于ViewData["Prop"] = "value"。当您在视图中使用Model 属性时,您正在检索ViewData.Model。寻找自己:

public abstract class WebViewPage<TModel> : WebViewPage
{
    private ViewDataDictionary<TModel> _viewData;
    public new AjaxHelper<TModel> Ajax { get; set; }
    public new HtmlHelper<TModel> Html { get; set; }
    public new TModel Model { get { return ViewData.Model; } }
}

我们可以通过使用ViewBagViewData 来保存您的特殊属性来达到您的目的。第一步是使用您想要的属性创建WebViewPage&lt;TModel&gt; 的自定义派生:

public abstract class CustomWebViewPage<TModel> : WebViewPage<TModel>
{
    public IList<string> Messages 
    { 
        get { return ViewBag.Messages ?? (ViewBag.Messages = new List<string>()); }
    } 
}

现在转到您的视图并将@model YourModelClass(第一行)行替换为以下内容:

@inherits CustomWebViewPage<YourModelClass>

您现在可以在视图中使用Messages 属性。

@String.Join(", ", Messages)

要在您的控制器中使用它,您可能需要从 Controller 派生并在那里添加属性。

public abstract class CustomControllerBase : Controller
{
    public IList<string> Messages 
    {
        get
        {
            return ViewBag.Messages ?? (ViewBag.Messages = new List<string>());
        }
    } 
}

现在,如果您从该控制器派生,则可以使用您的新属性。您放入列表中的任何内容也将在视图中提供给您。

public class ExampleController : CustomControllerBase
{
    public ActionResult Index()
    {
        Messages.Add("This is a message");
        return View();
    }
}

我使用 ViewBag 是因为它使属性 getter 更短。如果您愿意 (ViewData["Messages"]),可以使用 ViewData 做同样的事情。

这与Model 的实现方式并不完全相同,因为如果有人碰巧使用了您正在保存的密钥,他们可能会意外覆盖您的属性,但如果您确保它足够接近,在功能上是等效的使用唯一键。

如果你深入挖掘,你也许可以从ViewDataDictionary 派生并将你的属性放在那里,然后重写一些控制器和视图方法来使用它。那么您的属性将与Model 完全相同。但我会把它留给你——我认为这不值得。

【讨论】:

    【解决方案2】:

    我同意 Chris 的回答,并且我个人会将其放入 viewbag。

    但只是为了扮演魔鬼的拥护者,从技术上讲,你可以改变规则......

    编辑:现在想想,您可能可以用ViewBag 替换下面的HttpContext.Items,这样您在技术上仍然使用 ViewBag 进行存储,但只需添加一个包装器以使其温暖安全的强类型感觉。

    例如你可以有这样的东西:

    namespace Your.Namespace
    {
        public class MessageCollection : IMessageCollection
        {
            public IList<string> Errors { get; protected set; }
            protected MessageCollection()
            {
                //Initialization stuff here
                Errors = new List<string>();
            }
    
            private const string HttpContextKey = "__MessageCollection";
            public static MessageCollection Current
            {
                get
                {
                    var httpContext = HttpContext.Current;
                    if (httpContext == null) throw new InvalidOperationException("MessageCollection must be used in the context of a web application.");
    
                    if (httpContext.Items[HttpContextKey] == null)
                    {
                        httpContext.Items[HttpContextKey] = new MessageCollection();
                    }
    
                    return httpContext.Items[HttpContextKey] as MessageCollection;
                }
            }
        }
    }
    

    然后像这样在你的控制器中获取它:

    [HttpGet]
    public ActionResult Index()
    {
        MessageCollection.Current.AddError("Uh oh!");
    
        return View();
    }
    

    或者你可以有一个带有快捷方式 getter 的 BaseController...例如

    protected MessageCollection Messages { get { return MessageCollection.Current; } }
    

    然后在你的控制器中比从它继承

    [HttpGet]
    public ActionResult Index()
    {
        Messages.AddError("Uh oh!");
    
        return View();
    }
    

    要在您的视图中获取它,只需更改您的 web.config(您可能需要在几个地方执行此操作(即您的主 web.config、视图目录 web.config 和区域视图目录 web.config)

    <system.web.webPages.razor>
      <!-- blah -->
      <pages pageBaseType="System.Web.Mvc.WebViewPage">
        <namespaces>
          <!-- blah -->
          <add namespace="Your.Namespace" />
        </namespaces>
      </pages>
    </system.web.webPages.razor>
    

    那么在你看来你应该可以做到:

    <div class="messages">
        @foreach (var error in MessageCollection.Current.Errors)
        {
            <span>@error</span>
        }
    </div>
    

    【讨论】:

    • 这本质上也是我作为“解决方法”得出的结论,但我不想把它作为问题的一部分,因为害怕很快关闭新的和创造性的想法。我与您和 Chris 处于同一页面,但我希望 MVC 架构中可能有一些东西允许扩展点来处理我不知道的问题。
    【解决方案3】:

    Razor 视图相当简单。您与单个模型进行交互,该模型是强类型的。那么,任何你想在视图中强类型化的东西都需要在你的模型上。如果您的模型中有不想要的东西或者一次性的,那么ViewBag 是作为所有非模型数据的通用包罗万象提供的,这就是为什么它是动态的。强类型会限制它成为一个包罗万象的能力。

    简短而简单:如果您想要强类型的消息,请将消息添加到您的视图模型中。否则,请坚持使用ViewBag。这些是你的选择。

    【讨论】:

    • 是的,我认为这可能是最好的建议,我会看到其他人也会回答这个问题。我真的很想看看 MVC 架构中是否有一些东西可以扩展它以挂钩你自己的捕获所有你知道那些会始终如一的地方。谢谢!
    • 请记住,至少到 MVC5(也可能是 MVC6/core)获取/设置 ViewBag 的任何属性(例如 ViewBag.Title)会导致框架内部的异常被抑制并且处理得当。您可以通过禁用“只是我的代码”选项来查看这一点。这是“动态”实现的设计所固有的,尽管“它有效”,但它是在流量密集型网站中实现高性能的严重障碍。更多深思:mvolo.com/…
    猜你喜欢
    • 1970-01-01
    • 2018-07-09
    • 1970-01-01
    • 2014-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-12
    相关资源
    最近更新 更多