How to Use ViewData and ViewBag with Umbraco Surface Controllers

4.2k views Asked by At

I have just spent 2 hours trying to work out why when I put a string in to View.Bag/ViewData inside my Surface controller, when I try and get the string back in the view I get null.

In the end I have solved the problem by putting the string in to a session variable insted.

Would like to know though why it wasn't working, and how to fix it.

Thanks in advance.

2

There are 2 answers

0
twamley On BEST ANSWER

Update: Are you posting and redirecting? When you refresh the form does it prompt you about posting again? If not, it's because you have accidentally followed the best practice of 302ing from a form post (prevents a user refreshing and reposting form data). The examples I was following for login surface controllers all used return RedirectToCurrentUmbracoPage() which I blindly followed. But, as the name implies that really is doing a redirect and it is really two requests! (I stubbornly had to verify in Fiddler before I believed it). ViewData and ViewBag are only good for one request--so they are fundamentally broken in a POST 302. Session is good for multiple requests which is why it worked for you. TempData will work for you too, because as it turns out, TempData is a construct that is built on top of session and was specifically designed to carry state between two posts (removed on retrieve). I read somewhere that TempData would have been better named RedirectData and that helped it click for me.

So when you're dealing with Surface Controllers and POSTing you have three options that I know work:

  • Session (which you proved worked)
  • TempData (which is built on session, and from what I've read is both best practice and built specifically for this situation)
  • Use return CurrentUmbracoPage(); in your form post. I just verified in Fiddler that this is exactly one request (refreshing in the browser prompts a repost warning). I also verified that ViewData works this way. But, because the surface controller is rendered as a Child Action using @Html.Action(...) you have to use ParentActionViewContext to get at the right ViewData (my first answer which I'll leave for others that find this question).

Original answer is still useful when there is no redirect involved (GET or a POST that returns CurrentUmbracoPage())...

In many cases you're actually making a child action. Usually you're only one level deep but if you mix macros and partials you can actually get multiple levels deep. There is a ViewData for each level and you have to walk your way up the stack with ParentActionViewContext to get to the top ViewData that you populated in your controller.

See this comment from Shannon in answer to a question about surface controllers and viewdata (Shannon is a core contributor on the HQ team and has a lot of great content out there). Quoting here:

If you want to access the ViewData that you've set on the master ViewContext's on a ChildAction being rendered from the master's ViewContext then you need to use @ViewContext.ParentActionViewContext.ViewData["ErrorMessage"]

The ParentActionViewContext in this example is the ViewContext that is rendering the Umbraco template, not the ChildAction. That is because when you POST (whether inside of Umbraco or normal MVC), you are posting to a new Action and the rendering process starts from scratch, when you validate your model, update the ViewData, etc... this all happens on what will become the 'master' ViewContext when the view renders. This view then will render your ChildAction.

0
uniquelau On

Twamley's answer above is excellent, in addition to this, I have found that using TempData.Add(key, value) works nicely.

An bare bones would look like:

SurfaceController

public class MyController : Umbraco.Web.Mvc.SurfaceController 
{
    public MyController() 
    {}

    public ActionResult DoSomething() 
    {
        // surface controller does something

        // get a page by it's document/model type alias
        var umbracoHelper = new UmbracoHelper(UmbracoContext.Current);
        var node = umbracoHelper.TypedContentSingleAtXPath("//" + "Home")      

        TempData.Add("Message", "This value will be passed through");
        return redirectToUmbracoPage(node);
    }
}

View

@inherits UmbracoTemplatePage
@{
    Layout = null;
}
@if (TempData.ContainsKey("Message"))
{
   <p>@TempData["Message"]</p>
}

http://localhost/umbraco/Surface/My/DoSomething