1

I have this in my view:

@{
    ViewBag.Title = "Page Title";
    ViewBag.Subtitle = "Page Subtitle";
}

Then I use this to render a page (partial view) via ajax:

public ActionResult PageView(string pageName = null, object model = null)
{
    if (Request.IsAjaxRequest())
    {
        // return PartialView(pageName, model);
        ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, pageName);
        ViewData.Model = model;

        using (StringWriter sw = new StringWriter())
        {
            ViewContext viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, sw);
            viewResult.View.Render(viewContext, sw);

            // Here ViewBag/ViewData does not have the data added by view

            Response.Headers["ex-page-title"] = ViewBag.Title ?? "Title Missing!";
            Response.Headers["ex-page-subtitle"] = ViewBag.Subtitle ?? "Subtitle Missing!";

            return new ContentResult()
            {
                Content = sw.GetStringBuilder().ToString()
            };
        }
    }
    else
    {
        return View(pageName, model);
    }
}

I cannot figure out how to get the contents of the ViewBag as changed by the view. Is this possible? Is the ViewBag used by the view visible only to the view, or should it be available after calling Render? In my case ViewBag/ViewData are always empty after View.Render.

Etherman
  • 1,777
  • 1
  • 21
  • 34
  • View bag is temporary store of information between requests. It doesn't persist. Why are you using viewbag anyway? Just use a model or post the title etc. you need as a parameter. – Wheels73 Aug 02 '18 at 11:54
  • ViewData/Bag is only available during the request, but it was not obvious that it gets cloned into child views instead of passed in by reference. – Etherman Aug 02 '18 at 12:28
  • We can use the ViewData inside the container views (e.g. _layout) because that is all part of the same render process - but not outside the render call (which as I am doing is an abnormal usage case). – Etherman Aug 02 '18 at 12:45
  • Ok.. glad you got it sorted anyway. – Wheels73 Aug 02 '18 at 12:49

2 Answers2

1

ViewBag and ViewData are cloned when the view is rendered. So any modification to them made inside the view is not accessible.

If you want to access data set inside the view, the best way would be to use an model. You set some properties of the model inside the view and you can access them when the view is rendered.

Another option is to use TempData which is not cloned like ViewData and ViewBag.

Response.Headers["ex-page-title"] = TempData["Title"] ?? "Title Missing!";
Response.Headers["ex-page-subtitle"] = TempData["Subtitle"] ?? "Subtitle Missing!";

and inside the view :

@{
    TempData["Title"] = "My page";
    TempData["Subtile"] = "has a title";
}
J.Loscos
  • 2,478
  • 1
  • 10
  • 17
  • That's what I suspected - the data is cloned and not simply passed, which is the real answer to the question. I see we can actually also set ViewContext.Controller.ViewData/ViewBag from the view. – Etherman Aug 02 '18 at 12:23
  • `TempData` is not indented to be used for that purpose, since you stated your partial view could be called from anywhere (any page), if any of these pages has more than one partial view being loaded, you may face problems with this approach. `TempData` should be used to keep data between redirects. I suggest you read [this](https://stackoverflow.com/questions/7993263/viewbag-viewdata-and-tempdata). – Alisson Reinaldo Silva Aug 02 '18 at 16:57
  • @Alisson I am not using TempData, and actually not using headers either. I have certain views that are "pages" loaded via ajax where we still need the page title to update title bars etc. I have decided to use meta tags (apparently OK in HTML5 for meta tags outside head). This is still the correct answer as it indicates that ViewData is cloned and not referenced. – Etherman Aug 03 '18 at 19:11
  • @Etherman `ViewData is cloned and not referenced` << have you tried using a model in the `ViewData` as suggested in this answer? Did it work when you tried to load it via ajax like you stated in your question? The `ViewData` or `ViewBag` won't exist there, even using a model, and the reason is not because it's cloned, this doesn't apply for ajax requests. Anyway, I'm glad you found an alternative, as the suggestions in this answer could stop working in the future, as I mentioned before. You should really share your own solution as an answer and mark it as the accepted one. – Alisson Reinaldo Silva Aug 05 '18 at 01:16
0

You should create some class to hold all the data you need to render your partial view:

public class PageViewModel
{
    public string PageName { get; set; }
    public string Title { get; set; }
    public string Subtitle { get; set; }
    public object Model { get; set; }
}

Then you use it like this:

public ActionResult PageView(PageViewModel model = null)
{
    if (Request.IsAjaxRequest())
    {
        // return PartialView(model.PageName, model.Model);
        ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, model.PageName);
        ViewData.Model = model.Model;

        using (StringWriter sw = new StringWriter())
        {
            ViewContext viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, sw);
            viewResult.View.Render(viewContext, sw);

            // Here ViewBag/ViewData does not have the data added by view

            Response.Headers["ex-page-title"] = model.Title ?? "Title Missing!";
            Response.Headers["ex-page-subtitle"] = model.Subtitle ?? "Subtitle Missing!";

            return new ContentResult()
            {
                Content = sw.GetStringBuilder().ToString()
            };
        }
    }
    else
    {
        return View(model.PageName, model.Model);
    }
}

Then you just set the appropriate properties when you render this page view through ajax.

Alisson Reinaldo Silva
  • 10,009
  • 5
  • 65
  • 83
  • The problem with this approach is it requires the logic calling PageView to know what page and page title it is loading. Seems like it should obviously know that however I have places where a single controller with wildcard routing can load any page in a folder - and in that case it makes sense for the title to be in the page. – Etherman Aug 02 '18 at 12:32
  • @Etherman I suspect you should review your logic. The accepted answer only works if the partial view call is made immediately after the main page has been rendered, because `TempData` only keeps data until the next call. So if you have another partial view being called anywhere, that won't work. Since your partial view could be called anywhere, you should be really careful where you are going to use it (even if you are the only dev in this project). – Alisson Reinaldo Silva Aug 02 '18 at 16:50
  • I am not using any of that code above anymore - I am now just rendering a meta tag instead, so my final solution no longer relates to my original question. – Etherman Aug 03 '18 at 19:16
  • @Etherman it does relate, and I think you should share your solution and post it as an answer, and explain why you decided to go with this solution. This may inspire future devs with the same problem to rethink their design, and I strongly believe using HTML works better for your problem. – Alisson Reinaldo Silva Aug 05 '18 at 01:20