Effectively avoiding ViewBag in ASP.NET MVC
Asked Answered
S

3

7

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

Scolex answered 22/3, 2013 at 20:53 Comment(1)
you really have 2 choices here: ViewBag and a dedicated property in your modelRentschler
N
10

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.

Nonmaterial answered 22/3, 2013 at 20:58 Comment(2)
Yeah, I think this is probably the best advice that I would see other's coming to this question follow as well. I was really looking to see if there was something in the MVC architecture that allowed it to be extended to hook your own catch all where you know what those would be consistently. Thanks!Scolex
Bare in mind that at least up to MVC5 (possibly MVC6/core as well) getting/setting any property of the ViewBag (e.g. ViewBag.Title) causes an exception internal to the framework which gets suppressed and handled appropriately. You can see this by disabling the 'just my code' option. This is inherent to the design of the 'dynamic' implementation and although "it works" it's a serious impediment for achieving high performance in traffic-intensive websites. More food for thought: mvolo.com/…Khrushchev
I
2

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>
Ingrown answered 22/3, 2013 at 21:22 Comment(1)
This is essentially what I came away with too as a "workaround" but I didn't want to put it as part of the question out of fear of closing up new and creative ideas quickly. I am on the same page as both you and Chris but I was hoping that maybe there was something in the MVC architecture that allowed for an extension point to deal with this that I wasn't aware of.Scolex
A
0

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.

Antalya answered 11/5, 2015 at 19:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.