Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Effectively avoiding ViewBag in ASP.NET MVC

How do you deal with avoiding ViewBag due to its risk of error with being dynamic but also avoid having to populate a new ViewModel and pass it back to the view each time. For instance, I don't want to necessarily change the follow to expose common data normally stuffed in ViewBag.

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

to

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

    return View(messages);
}

Where in the pipeline would I add a property like ViewBag that is custom and strongly typed but have it exposed elegantly in the Controller and also the View. I'd rather do this when I don't need a specific ViewModel all the time...

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

    return View();
}

And on the view side, instead of @((IMessageCollection)ViewBag.Messages).Errors id rather have something like @Messages.Errors that is strongly typed and available everywhere. Also, I don't want to just cast it out in a code block at the top of my Razor view.

In WebForms, I would have done something like put this a base page and then have a usercontrol that can hidden or shown on pages as needed. With the Controller decoupled from the View, I'm not sure how to replicate similar behavior.

Is this possible or what is the best design approach?

Thanks, Scott

like image 319
Scott Avatar asked Mar 22 '13 20:03

Scott


2 Answers

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.

like image 155
Chris Pratt Avatar answered Sep 22 '22 17:09

Chris Pratt


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>
like image 22
Charlino Avatar answered Sep 21 '22 17:09

Charlino