Effectively avoiding ViewBag in ASP.NET MVC

强颜欢笑 提交于 2019-12-04 07:15:40

Razor views are fairly simplistic. You interact with a single model, which is strongly-typed. Anything you want strongly-typed in your view, then, needs to be on your model. If you have something you don't want on your model or that is one-off, then ViewBag is provided as a generic catch-all for all non-model data, which is why it is a dynamic. To be strongly-typed would limit it's ability to be a catch-all.

Short and simple: if you want strongly-typed add Messages to your View Model. Otherwise, stick with ViewBag. Those are your choices.

I agree with Chris' answer and personally I would throw it in the viewbag.

But just to play devils advocate, technically, you can bend the rules...

Edit: Just thinking about it now, you could probably replace HttpContext.Items below with ViewBag so that you technically were still using ViewBag for storage but just adding a wrapper to give it that warm safe strongly typed feeling.

E.g. you could have something like this:

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;
            }
        }
    }
}

Then just get it in your controller like this:

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

    return View();
}

Or you could have a BaseController with a shortcut getter... e.g.

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

Then in your controller than inherits from it

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

    return View();
}

To get it in your view, simple alter your web.config (you might need to do this in a few places (i.e. your main web.config, views directory web.config and area views directories 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>

Then in your views you should be able to do:

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

In ASP.NET MVC, you have at your disposal ViewBag, ViewData, and TempData (for more info, see this blog post). The ViewBag is a dynamic wrapper around the ViewData dictionary. If you do ViewBag.Prop = "value" it is equivalent to ViewData["Prop"] = "value". When you use the Model property in a view, you're retrieving ViewData.Model. Look for yourself:

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; } }
}

We can achieve your end by using either ViewBag or ViewData to hold your special properties. The first step is to create a custom derivation of WebViewPage<TModel> with the property you want:

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

Now go to your view and replace the line @model YourModelClass (the first line) with the following:

@inherits CustomWebViewPage<YourModelClass>

You can now use the Messages property in your view.

@String.Join(", ", Messages)

To use it in your controllers, you'll probably want to derive from Controller and add the property there, too.

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

Now if you derive from that controller, you can use your new property. Anything you put in the list will also be available to you in the view.

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

I used ViewBag because it made the property getter shorter. You can do the same thing with ViewData if you prefer (ViewData["Messages"]).

This isn't quite the same as how Model is implemented because someone can overwrite your property accidentally if they happen to use a key you're saving, but it's close enough as to be functionally equivalent if you just make sure to use a unique key.

If you dig deeper, you may be able to derive from ViewDataDictionary and put your property in there, then override some of the controller and view methods to use it instead. Then your property would be exactly the same as Model. But I'll leave that to you-- I don't think it's worth it.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!