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
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.