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());
}
}
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
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
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:
This is a very basic version of the code I'm using, has anyone got this before:
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
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:
Then in a BaseSurfaceController.cs that all my SurfaceController classes inherit from added this method:
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/
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:
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
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
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
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
Otherwise, worked great, thanks Chris.
is working on a reply...