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.

  • This forum is in read-only mode while we transition to the new forum.

    You can continue this topic on the new forum by tapping the "Continue discussion" link below.

Please Sign in or register to post replies