Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Chris 34 posts 134 karma points
    Feb 25, 2016 @ 11:34
    Chris
    0

    Async form post throws exception

    I have a Template with a form that posts back to a SurfaceController, and in the method that handles the form post I'm checking the ModelState and if invalid returning the CurrentUmbracoPage(). The template gets data from a database so I'm using Tasks. All this works, however when the form posts back and is invalid I'm getting the Exception:

    The asynchronous action method 'BookCollection' returns a Task, which cannot be executed synchronously. I've looked in the source and the error is caused within the UmbracoPageResult class, when it calls controller.Execute(context.RequestContext);
    

    This is a very basic version of the code I'm using, has anyone got this before:

    public class BookCollectionViewModel
    {
    
    }
    
    public class BookCollectionSurfaceController : SurfaceController
    {
        [HttpPost]
        public async Task<ActionResult> PostBookCollectionForm(BookCollectionViewModel viewModel)
        {
    
            if (!ModelState.IsValid)
            {
                //this throws the exception in the ExecuteControllerAction method of UmbracoPageResult class
                return CurrentUmbracoPage();
            }
            return RedirectToCurrentUmbracoPage();
        }
    }
    
    public class BookCollectionController : RenderMvcController
    {
        public async Task<ActionResult> BookCollection()
        {
    
            //do async lookups here
    
            return CurrentTemplate(new BookCollectionViewModel());
        }
    }
    
  • Mel Lota 10 posts 51 karma points
    Jun 02, 2016 @ 15:31
    Mel Lota
    0

    Hi,

    Did you ever solve this one? I'm having the same problem on a partial which uses a surface controller in the same way.

    Thanks

    Mel

  • Chris 34 posts 134 karma points
    Jun 02, 2016 @ 15:43
    Chris
    0

    I did, but it was a complete HACK so I would not recommend this to anyone unless there is something I missed:

    Create a new class that extended UmbracoPageResult and takes an actionOverride:

    public class AsyncUmbracoPageResult : UmbracoPageResult
    {
        private readonly ProfilingLogger _profilingLogger;
        private readonly string _actionOverride;
    
        public AsyncUmbracoPageResult(ProfilingLogger profilingLogger, string actionOverride)
            : base(profilingLogger)
        {
            _profilingLogger = profilingLogger;
            _actionOverride = actionOverride;
        }
    
        public override void ExecuteResult(ControllerContext context)
        {
            ResetRouteData(context.RouteData);
    
            ValidateRouteData(context.RouteData);
    
            var routeDef = (RouteDefinition)context.RouteData.DataTokens["umbraco-route-def"];
    
    
            var factory = ControllerBuilder.Current.GetControllerFactory();
            context.RouteData.Values["action"] = routeDef.ActionName;
            context.RouteData.Values["controller"] = routeDef.ControllerName;
            if (!string.IsNullOrEmpty(_actionOverride))
            {
                context.RouteData.Values["action"] = _actionOverride;
            }
            ControllerBase controller = null;
    
            try
            {
                controller = CreateController(context, factory, routeDef);
    
                CopyControllerData(context, controller);
    
                ExecuteControllerAction(context, controller);
            }
            finally
            {
                CleanupController(controller, factory);
            }
    
        }
    
        /// <summary>
        /// Executes the controller action
        /// </summary>
        private void ExecuteControllerAction(ControllerContext context, IController controller)
        {
            using (_profilingLogger.TraceDuration<UmbracoPageResult>("Executing Umbraco RouteDefinition controller", "Finished"))
            {
                controller.Execute(context.RequestContext);
            }
        }
    
        /// <summary>
        /// Since we could be returning the current page from a surface controller posted values in which the routing values are changed, we 
        /// need to revert these values back to nothing in order for the normal page to render again.
        /// </summary>
        private static void ResetRouteData(RouteData routeData)
        {
            routeData.DataTokens["area"] = null;
            routeData.DataTokens["Namespaces"] = null;
        }
    
        /// <summary>
        /// Validate that the current page execution is not being handled by the normal umbraco routing system
        /// </summary>
        private static void ValidateRouteData(RouteData routeData)
        {
            if (routeData.DataTokens.ContainsKey("umbraco-route-def") == false)
            {
                throw new InvalidOperationException("Can only use " + typeof(UmbracoPageResult).Name +
                                                    " in the context of an Http POST when using a SurfaceController form");
            }
        }
    
        /// <summary>
        /// Ensure ModelState, ViewData and TempData is copied across
        /// </summary>
        private static void CopyControllerData(ControllerContext context, ControllerBase controller)
        {
            controller.ViewData.ModelState.Merge(context.Controller.ViewData.ModelState);
    
            foreach (var d in context.Controller.ViewData)
                controller.ViewData[d.Key] = d.Value;
    
            //We cannot simply merge the temp data because during controller execution it will attempt to 'load' temp data
            // but since it has not been saved, there will be nothing to load and it will revert to nothing, so the trick is 
            // to Save the state of the temp data first then it will automatically be picked up.
            // http://issues.umbraco.org/issue/U4-1339
    
            var targetController = controller as Controller;
            var sourceController = context.Controller as Controller;
            if (targetController != null && sourceController != null)
            {
                targetController.TempDataProvider = sourceController.TempDataProvider;
                targetController.TempData = sourceController.TempData;
                targetController.TempData.Save(sourceController.ControllerContext, sourceController.TempDataProvider);
            }
    
        }
    
        /// <summary>
        /// Creates a controller using the controller factory
        /// </summary>
        private static ControllerBase CreateController(ControllerContext context, IControllerFactory factory, RouteDefinition routeDef)
        {
            var controller = factory.CreateController(context.RequestContext, routeDef.ControllerName) as ControllerBase;
    
            if (controller == null)
                throw new InvalidOperationException("Could not create controller with name " + routeDef.ControllerName + ".");
    
            return controller;
        }
    
        /// <summary>
        /// Cleans up the controller by releasing it using the controller factory, and by disposing it.
        /// </summary>
        private static void CleanupController(IController controller, IControllerFactory factory)
        {
            if (controller != null)
                factory.ReleaseController(controller);
    
            if (controller != null)
                controller.DisposeIfDisposable();
        }
    
        private class DummyView : IView
        {
            public void Render(ViewContext viewContext, TextWriter writer)
            {
            }
        }
    }
    

    Then in a BaseSurfaceController.cs that all my SurfaceController classes inherit from added this method:

    protected AsyncUmbracoPageResult CurrentUmbracoPageResultAsync(string overrideActionName)
        {
            return new AsyncUmbracoPageResult(ApplicationContext.ProfilingLogger, overrideActionName);
        }
    

    On my RenderController, I now have 2 methods for the same View. The synchronous one just calls the async one using this nuget package: https://www.nuget.org/packages/Nito.AsyncEx/

    public ActionResult NonAsyncAction()
        {
            var result = AsyncContext.Run(() => AsyncAction());
    
            return result;
        }
    
    
        public async Task<ActionResult> AsyncAction()
        {
            //do async stuff here
            return View();
        }
    

    And finally, the horrible HACK, whenever I have an async POST action in my surface controller that can return the current page for validation, I've got this:

      if (!ModelState.IsValid)
            {
                return CurrentUmbracoPageResultAsync("NonAsyncAction");
            }
    

    Not pretty I know, but it works and as a lot of this project posts data to 3rd party web services it was worth using to get the increase in performance

  • Conor Breen 11 posts 100 karma points
    Sep 03, 2017 @ 21:43
    Conor Breen
    0

    I registered just to say thanks for this - saved my bacon. Very surprised Umbraco doesn't have better async support OOTB on this - seems a fairly obvious omission to me, with async used as standard by most developers these days!

    One thing I would add - I was originally calling

    return CurrentTemplate(myModel);
    

    Inside my async controller action, but when called from the non-async version, this would give an error as it was looking for a view by the name of the non-async method, as it determines what View to use based on

    RouteData.Values["action"]
    

    Only way I could get it to work was by specifying the name of the current view, i.e. inside my async method I had to return

    return View("Home", customModel);
    

    Otherwise, worked great, thanks Chris.

Please Sign in or register to post replies

Write your reply to:

Draft