Execute multiple controller actions in one call with MVC.Net 5
Asked Answered
B

1

1

We have recently upgraded our code base from .Net 4.0 to .Net 4.5.1 and from MVC 2.0 to MVC 5.2.2.

We have a custom method in our base controller class which allowed us to update multiple parts of our views within a single request. Since upgrading, this no longer works.

Original Code:

protected void IncludeAction(string actionName, string controllerName, object routeValues)
{
    //Get Url
    RouteValueDictionary routes = null;
    if (routeValues != null)
        routes = new RouteValueDictionary(routeValues);
    else
        routes = new RouteValueDictionary();
    routes.Add("Action", actionName);
    if (!string.IsNullOrEmpty(controllerName))
        routes.Add("Controller", controllerName);
    else
        routes.Add("Controller", this.ControllerContext.RouteData.Values["Controller"].ToString());
    var url = RouteTable.Routes.GetVirtualPath(this.ControllerContext.RequestContext, routes).VirtualPath;

    //Rewrite path
    System.Web.HttpContext.Current.RewritePath(url, false);
    IHttpHandler httpHandler = new MvcHttpHandler();
    httpHandler.ProcessRequest(System.Web.HttpContext.Current);
}

We receive errors on the httpHandler.ProcessRequest call. We used this technique in a number of places. After much googling it seemed that we should use Server.TransferRequest instead.

New Code

protected void IncludeAction(string actionName, string controllerName, object routeValues)
{
    //Get Url
    RouteValueDictionary routes = null;
    if (routeValues != null)
        routes = new RouteValueDictionary(routeValues);
    else
        routes = new RouteValueDictionary();
    routes.Add("Action", actionName);
    if (!string.IsNullOrEmpty(controllerName))
        routes.Add("Controller", controllerName);
    else
        routes.Add("Controller", this.ControllerContext.RouteData.Values["Controller"].ToString());
    var url = RouteTable.Routes.GetVirtualPath(this.ControllerContext.RequestContext, routes).VirtualPath;

    //Rewrite path
    System.Web.HttpContext.Current.RewritePath(url, false);
    System.Web.HttpContext.Current.Server.TransferRequest(url, true);
}

When called from code like this:

IncludeAction("OptInBanner", "Person");
IncludeAction("NavMenu", "Person");
return Transfer(returnurl);

Our new code generates this error:

Type:
    System.InvalidOperationException
Message:
    TransferRequest cannot be invoked more than once.
Stack Trace:
    at System.Web.HttpServerUtility.TransferRequest(String path, Boolean preserveForm)
    at MyProject.MyNamspace.MyBaseController.IncludeAction(String actionName, String controllerName, Object routeValues)
    at MyProject.MyNamspace.MyBaseController.IncludeAction(String actionName, String controllerName)
    at MyProject.MyNamspace.MyController.MyAction(Boolean myChoice, String returnurl)
    at .lambda_method(Closure , ControllerBase , Object[] )

Since the message plainly says I cannot call TransferRequest more than once, but my code needs to execute two controller actions in addition to redirecting and performing a third action, I thought I'd revert to the old code. However, that generates this error:

Type:
    System.InvalidOperationException
Message:
    'HttpContext.SetSessionStateBehavior' can only be invoked before 'HttpApplication.AcquireRequestState' event is raised.
Stack Trace:
    at System.Web.Routing.UrlRoutingHandler.ProcessRequest(HttpContextBase httpContext)
    at MyProject.MyNamspace.MyBaseController.IncludeAction(String actionName, String controllerName, Object routeValues)
    at MyProject.MyNamspace.MyBaseController.IncludeAction(String actionName, String controllerName)
    at MyProject.MyNamspace.MyController.MyAction(Boolean myChoice, String returnurl)
    at .lambda_method(Closure , ControllerBase , Object[] )

For this function, how can I retain the original behavior, without errors, that we had under .Net 4.0 and MVC 2.0 while using the newer framework and MVC?

Bamboozle answered 23/10, 2014 at 17:20 Comment(7)
Hard to say without seeing the rest of your code base, but my gut reaction is that you're putting too much business logic in your controllers, if you're sending folks to multiple controller actions.Wistrup
@Wistrup Some of our code has is quite complex and probably has too much going on. In this case, however, we want to update some common partial views, including their back-end processing, along with executing the final controller action. The above code is part of our site's Terms of Service agreement page and has to return people to different controller actions depending upon where they started from. The extra partial views must always be updated when leaving the TOS page, but rarely by the final actions.Bamboozle
This code gives me the willies - especially any mention of Server.Transfer in a MVC app. It sounds like there is something fundamentally flawed with your approach - you should either be hitting a single controller action and wrapping functionality common to multiple pages in child actions or partials, or you should be using AJAX to update parts of the view asynchronously. If you need to return users to different places conditionally, use redirects and pass GET parameters for conditional rendering.Detergency
@AntP Our Transfer method actually uses similar code to the above. However, in that case we actually want to transfer all processing and no longer continue with the current processing. This link suggests the use of a custom ActionResult class: #800011. The accepted answer shows two techniques, depending upon the installed version of MVC. Before upgrading we used the pre-MVC3 technique (same as the "old" code above) but now we use the MVC3+ technique (Server.TrasnferRequest).Bamboozle
I'm still not clear on why you'd want to do this - the reason you've run into problems is because you're abusing the framework. Instead of asking how to implement this particular solution, why not describe what you're actually trying to achieve in your question and then better alternatives can be suggested.Detergency
Agreed w/ @AntP; and as I said before, nothing you're describing sounds like it needs to be done the way you're doing it, and in fact is somewhat against best practices, which is probably why you're having difficulty.Wistrup
So we have an MVC 5 website. On one screen we have many partial views. When we post an update we need to conditionally update multiple partial views, some of which require processing in a controller action. What is best practice? What other approaches should we use? Should we refactor our whole application?Bamboozle
B
0

After a lot of research, I came up with this:

protected void IncludeAction(string actionName, string controllerName, object routeValues)
{
    string targetController = null;
    if (!string.IsNullOrWhiteSpace(controllerName))
    {
        targetController = controllerName;
    }
    else
    {
        targetController = this.ControllerContext.RouteData.Values["Controller"].ToString();
    }

    HtmlHelper htmlHelper = new HtmlHelper(
        new ViewContext(
            this.ControllerContext,
            new WebFormView(this.ControllerContext, actionName),
            this.ViewData,
            this.TempData,
            this.Response.Output
        ),
        new ViewPage()
    );

    htmlHelper.RenderAction(actionName, targetController, routeValues);
}

The HtmlHelper is constructed with a new ViewContext. The new ViewContext is constructed with much of the data from the current ControllerContext and the current Response object's TextWriter (this.Response.Output).

Calling RenderAction on the HtmlHelper will render my chosen Controller-Action results to the Response stream, allowing us to call multiple Controller Actions from other Controller Actions and letting our client side AJAX framework apply the results to the correct portions of the page.

For places where it is okay to throw out the pending Response stream, we can still use Server.TransferRequest. But this now allows us to append multiple Controller-Action results to the same response stream, from the server.

Bamboozle answered 4/11, 2014 at 16:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.