diff --git a/DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcHostControl.cs b/DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcHostControl.cs new file mode 100644 index 00000000000..86a69665582 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcHostControl.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.Mvc +{ + using System; + using System.Globalization; + using System.Threading; + using System.Threading.Tasks; + using System.Web; + using System.Web.UI; + + using DotNetNuke.Services.Exceptions; + using DotNetNuke.UI.Modules; + using DotNetNuke.Web.Mvc.Routing; + + public class AsyncMvcHostControl : MvcHostControl, IAsyncModuleControl + { + public AsyncMvcHostControl() + : base() + { + } + + public AsyncMvcHostControl(string controlKey) + : base(controlKey) + { + } + + protected override void OnInitInternal(EventArgs e) + { + if (this.ExecuteModuleImmediately) + { + this.Page.RegisterAsyncTask(new PageAsyncTask(this.ExecuteModuleAsync)); + } + } + + protected override void OnPreRenderInternal(EventArgs e) + { + // We need to defer execution to after the async task registered in OnInitInternal above, which will only get executed at the WebForms async point, just before PreRenderComplete. + this.Page.RegisterAsyncTask(new PageAsyncTask(this.OnPreRenderAsync)); + } + + protected async Task ExecuteModuleAsync(CancellationToken cancellationToken) + { + try + { + HttpContextBase httpContext = new HttpContextWrapper(HttpContext.Current); + + var moduleExecutionEngine = GetModuleExecutionEngine(); + + this.Result = await moduleExecutionEngine.ExecuteModuleAsync(this.GetModuleRequestContext(httpContext), cancellationToken); + + this.ModuleActions = this.LoadActions(this.Result); + + httpContext.SetModuleRequestResult(this.Result); + } + catch (Exception exc) + { + Exceptions.ProcessModuleLoadException(this, exc); + } + } + + private Task OnPreRenderAsync(CancellationToken cancellationToken) + { + try + { + if (this.Result == null) + { + return Task.CompletedTask; + } + + var mvcString = RenderModule(this.Result); + if (!string.IsNullOrEmpty(Convert.ToString(mvcString, CultureInfo.InvariantCulture))) + { + this.Controls.Add(new LiteralControl(Convert.ToString(mvcString, CultureInfo.InvariantCulture))); + } + } + catch (Exception exc) + { + Exceptions.ProcessModuleLoadException(this, exc); + } + + return Task.CompletedTask; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcSettingsControl.cs b/DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcSettingsControl.cs new file mode 100644 index 00000000000..e728722dd5b --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcSettingsControl.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.Mvc +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + using DotNetNuke.Entities.Modules; + using DotNetNuke.UI.Modules; + + public class AsyncMvcSettingsControl : AsyncMvcHostControl, IAsyncSettingsControl + { + public AsyncMvcSettingsControl() + : base("Settings") + { + this.ExecuteModuleImmediately = false; + } + + /// + public void LoadSettings() + { + throw new NotSupportedException("Async controls need to call LoadSettingsAsync."); + } + + /// + public Task LoadSettingsAsync(CancellationToken cancellationToken) + { + return this.ExecuteModuleAsync(cancellationToken); + } + + /// + public void UpdateSettings() + { + throw new NotSupportedException("Async controls need to call UpdateSettingsAsync."); + } + + /// + public async Task UpdateSettingsAsync(CancellationToken cancellationToken) + { + await this.ExecuteModuleAsync(cancellationToken); + + ModuleController.Instance.UpdateModule(this.ModuleContext.Configuration); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.Mvc/DnnMvcHandler.cs b/DNN Platform/DotNetNuke.Web.Mvc/DnnMvcHandler.cs index 5f37f95e7c5..ed18521c349 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/DnnMvcHandler.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/DnnMvcHandler.cs @@ -4,6 +4,8 @@ namespace DotNetNuke.Web.Mvc { using System; + using System.Threading; + using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using System.Web.Routing; @@ -26,7 +28,7 @@ namespace DotNetNuke.Web.Mvc using Microsoft.Extensions.DependencyInjection; - public class DnnMvcHandler : IHttpHandler, IRequiresSessionState + public class DnnMvcHandler : HttpTaskAsyncHandler, IRequiresSessionState { public static readonly string MvcVersionHeaderName = "X-AspNetMvc-Version"; @@ -41,19 +43,13 @@ public DnnMvcHandler(RequestContext requestContext) public RequestContext RequestContext { get; private set; } - /// - bool IHttpHandler.IsReusable => this.IsReusable; - internal ControllerBuilder ControllerBuilder { get => this.controllerBuilder ??= ControllerBuilder.Current; set => this.controllerBuilder = value; } - protected virtual bool IsReusable => false; - - /// - void IHttpHandler.ProcessRequest(HttpContext httpContext) + public override async Task ProcessRequestAsync(HttpContext context) { SetThreadCulture(); MembershipModule.AuthenticateRequest( @@ -65,10 +61,12 @@ void IHttpHandler.ProcessRequest(HttpContext httpContext) Globals.GetCurrentServiceProvider().GetRequiredService(), this.RequestContext.HttpContext, allowUnknownExtensions: true); - this.ProcessRequest(httpContext); + + var httpContextBase = new HttpContextWrapper(context); + await this.ProcessRequestAsync(httpContextBase, httpContextBase.Response.ClientDisconnectedToken); } - protected internal virtual void ProcessRequest(HttpContextBase httpContext) + protected internal virtual async Task ProcessRequestAsync(HttpContextBase httpContext, CancellationToken cancellationToken) { try { @@ -76,7 +74,7 @@ protected internal virtual void ProcessRequest(HttpContextBase httpContext) // Check if the controller supports IDnnController var moduleResult = - moduleExecutionEngine.ExecuteModule(this.GetModuleRequestContext(httpContext)); + await moduleExecutionEngine.ExecuteModuleAsync(this.GetModuleRequestContext(httpContext), cancellationToken); httpContext.SetModuleRequestResult(moduleResult); this.RenderModule(moduleResult); } @@ -85,12 +83,6 @@ protected internal virtual void ProcessRequest(HttpContextBase httpContext) } } - protected virtual void ProcessRequest(HttpContext httpContext) - { - HttpContextBase httpContextBase = new HttpContextWrapper(httpContext); - this.ProcessRequest(httpContextBase); - } - private static void SetThreadCulture() { var portalSettings = PortalController.Instance.GetCurrentSettings(); diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionFilters/ModuleActionItemsAttribute.cs b/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionFilters/ModuleActionItemsAttribute.cs index 56b2ca54d47..e01f1db23f4 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionFilters/ModuleActionItemsAttribute.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionFilters/ModuleActionItemsAttribute.cs @@ -7,6 +7,7 @@ namespace DotNetNuke.Web.Mvc.Framework.ActionFilters using System; using System.Globalization; using System.Reflection; + using System.Threading.Tasks; using System.Web.Mvc; using DotNetNuke.Entities.Modules.Actions; @@ -55,12 +56,20 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) methodName = this.MethodName; } - var method = GetMethod(type, methodName); + var method = GetMethod(type, methodName, controller.IsAsync); - controller.ModuleActions = method.Invoke(instance, null) as ModuleActionCollection; + var result = method.Invoke(instance, null); + if (result is ModuleActionCollection moduleActions) + { + controller.ModuleActions = moduleActions; + } + else if (result is Task taskResult) + { + controller.ModuleActionsAsync = taskResult; + } } - private static MethodInfo GetMethod(Type type, string methodName) + private static MethodInfo GetMethod(Type type, string methodName, bool supportsAsync) { var method = type.GetMethod(methodName); @@ -69,13 +78,13 @@ private static MethodInfo GetMethod(Type type, string methodName) throw new NotImplementedException($"The expected method to get the module actions cannot be found. Type: {type.FullName}, Method: {methodName}"); } - var returnType = method.ReturnType.FullName; - if (returnType != "DotNetNuke.Entities.Modules.Actions.ModuleActionCollection") + var returnType = method.ReturnType; + if (returnType == typeof(ModuleActionCollection) || (supportsAsync && returnType == typeof(Task))) { - throw new InvalidOperationException("The method must return an object of type ModuleActionCollection"); + return method; } - return method; + throw new InvalidOperationException("The method must return an object of type ModuleActionCollection"); } } } diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionResults/DnnPartialViewResult.cs b/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionResults/DnnPartialViewResult.cs index 789586640eb..e12d0b5f9cf 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionResults/DnnPartialViewResult.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionResults/DnnPartialViewResult.cs @@ -31,7 +31,7 @@ public void ExecuteResult(ControllerContext context, TextWriter writer) if (this.View == null) { - result = this.ViewEngineCollection.FindPartialView(context, this.ViewName); + result = this.FindView(context); this.View = result.View; } diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionResults/DnnViewResult.cs b/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionResults/DnnViewResult.cs index b6ee426c795..0518caaab96 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionResults/DnnViewResult.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionResults/DnnViewResult.cs @@ -27,7 +27,7 @@ public void ExecuteResult(ControllerContext context, TextWriter writer) if (this.View == null) { - result = this.ViewEngineCollection.FindView(context, this.ViewName, this.MasterName); + result = this.FindView(context); this.View = result.View; } diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Controllers/DnnController.cs b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Controllers/DnnController.cs index 61385d42a9e..c585d1b06b5 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Controllers/DnnController.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Controllers/DnnController.cs @@ -7,6 +7,7 @@ namespace DotNetNuke.Web.Mvc.Framework.Controllers using System; using System.Diagnostics.CodeAnalysis; using System.Text; + using System.Threading.Tasks; using System.Web.Mvc; using System.Web.Routing; using System.Web.UI; @@ -72,6 +73,12 @@ public ActionResult ResultOfLastExecute /// public ModuleActionCollection ModuleActions { get; set; } + /// + public Task ModuleActionsAsync { get; set; } + + /// + public bool IsAsync { get; set; } + /// public ModuleInstanceContext ModuleContext { get; set; } diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Controllers/IDnnController.cs b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Controllers/IDnnController.cs index cbc3c70f86b..59e91dead81 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Controllers/IDnnController.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Controllers/IDnnController.cs @@ -5,6 +5,7 @@ namespace DotNetNuke.Web.Mvc.Framework.Controllers { using System.Diagnostics.CodeAnalysis; + using System.Threading.Tasks; using System.Web.Mvc; using System.Web.UI; @@ -24,6 +25,10 @@ public interface IDnnController : IController ModuleActionCollection ModuleActions { get; set; } + Task ModuleActionsAsync { get; set; } + + bool IsAsync { get; set; } + ModuleInstanceContext ModuleContext { get; set; } bool ValidateRequest { get; set; } diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/IModuleExecutionEngine.cs b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/IModuleExecutionEngine.cs index 1aba9092d57..60b24f406e2 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/IModuleExecutionEngine.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/IModuleExecutionEngine.cs @@ -5,11 +5,15 @@ namespace DotNetNuke.Web.Mvc.Framework.Modules { using System.IO; + using System.Threading; + using System.Threading.Tasks; public interface IModuleExecutionEngine { ModuleRequestResult ExecuteModule(ModuleRequestContext moduleRequestContext); + Task ExecuteModuleAsync(ModuleRequestContext moduleRequestContext, CancellationToken cancellationToken); + void ExecuteModuleResult(ModuleRequestResult moduleResult, TextWriter writer); } } diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ModuleApplication.cs b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ModuleApplication.cs index 59daa7a514d..93eb57eb52a 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ModuleApplication.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ModuleApplication.cs @@ -6,8 +6,12 @@ namespace DotNetNuke.Web.Mvc.Framework.Modules using System; using System.Globalization; using System.Reflection; + using System.Runtime.Remoting.Contexts; + using System.Threading; + using System.Threading.Tasks; using System.Web; using System.Web.Mvc; + using System.Web.Mvc.Async; using System.Web.Routing; using DotNetNuke.Common; @@ -138,6 +142,85 @@ public virtual ModuleRequestResult ExecuteRequest(ModuleRequestContext context) } } + public virtual async Task ExecuteRequestAsync(ModuleRequestContext context, CancellationToken cancellationToken) + { + this.EnsureInitialized(); + this.RequestContext = this.RequestContext ?? new RequestContext(context.HttpContext, context.RouteData); + var currentContext = HttpContext.Current; + if (currentContext != null) + { + var isRequestValidationEnabled = ValidationUtility.IsValidationEnabled(currentContext); + if (isRequestValidationEnabled == true) + { + ValidationUtility.EnableDynamicValidation(currentContext); + } + } + + this.AddVersionHeader(this.RequestContext.HttpContext); + this.RemoveOptionalRoutingParameters(); + + var controllerName = this.RequestContext.RouteData.GetRequiredString("controller"); + + // Construct the controller using the ControllerFactory + var controller = this.ControllerFactory.CreateController(this.RequestContext, controllerName); + try + { + // Check if the controller supports IDnnController + var moduleController = controller as IDnnController; + + // If we couldn't adapt it, we fail. We can't support IController implementations directly :( + // Because we need to retrieve the ActionResult without executing it, IController won't cut it + if (moduleController == null) + { + throw new InvalidOperationException("Could Not Construct Controller"); + } + + moduleController.IsAsync = true; + + moduleController.ValidateRequest = false; + + moduleController.DnnPage = context.DnnPage; + + moduleController.ModuleContext = context.ModuleContext; + + moduleController.LocalResourceFile = + $"~/DesktopModules/MVC/{context.ModuleContext.Configuration.DesktopModule.FolderName}/{Localization.LocalResourceDirectory}/{controllerName}.resx"; + + moduleController.ViewEngineCollectionEx = this.ViewEngines; + + if (controller is not IAsyncController asyncController) + { + // the base System.Web.Mvc.Controller class implements IAsyncController so this should normally never happen. + throw new NotSupportedException("Synchronous only Controller implementation is not supported."); + } + + // Execute the controller and capture the result + // if our ActionFilter is executed after the ActionResult has triggered an Exception the filter + // MUST explicitly flip the ExceptionHandled bit otherwise the view will not render + await Task.Factory.FromAsync(asyncController.BeginExecute, asyncController.EndExecute, this.RequestContext, null); + if (moduleController.ModuleActionsAsync != null) + { + moduleController.ModuleActions = await moduleController.ModuleActionsAsync; + } + + var result = moduleController.ResultOfLastExecute; + + // Return the final result + return new ModuleRequestResult + { + ActionResult = result, + ControllerContext = moduleController.ControllerContext, + ModuleActions = moduleController.ModuleActions, + ModuleContext = context.ModuleContext, + ModuleApplication = this, + }; + } + finally + { + this.ControllerFactory.ReleaseController(controller); + } + } + protected internal virtual void Init() { var prefix = NormalizeFolderPath(this.FolderPath); diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ModuleExecutionEngine.cs b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ModuleExecutionEngine.cs index b48dc4d22e0..711b06c3a62 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ModuleExecutionEngine.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ModuleExecutionEngine.cs @@ -6,6 +6,8 @@ namespace DotNetNuke.Web.Mvc.Framework.Modules { using System; using System.IO; + using System.Threading; + using System.Threading.Tasks; using DotNetNuke.Common; using DotNetNuke.Web.Mvc.Framework.ActionResults; @@ -26,6 +28,19 @@ public ModuleRequestResult ExecuteModule(ModuleRequestContext moduleRequestConte return null; } + public async Task ExecuteModuleAsync(ModuleRequestContext moduleRequestContext, CancellationToken cancellationToken) + { + Requires.NotNull("moduleRequestContext", moduleRequestContext); + + if (moduleRequestContext.ModuleApplication != null) + { + // Run the module + return await moduleRequestContext.ModuleApplication.ExecuteRequestAsync(moduleRequestContext, cancellationToken); + } + + return null; + } + /// public virtual void ExecuteModuleResult(ModuleRequestResult moduleResult, TextWriter writer) { diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ResultCapturingActionInvoker.cs b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ResultCapturingActionInvoker.cs index 66f5e27c45d..c5183413769 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ResultCapturingActionInvoker.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ResultCapturingActionInvoker.cs @@ -7,8 +7,9 @@ namespace DotNetNuke.Web.Mvc.Framework.Modules using System; using System.Collections.Generic; using System.Web.Mvc; + using System.Web.Mvc.Async; - public class ResultCapturingActionInvoker : ControllerActionInvoker + public class ResultCapturingActionInvoker : AsyncControllerActionInvoker { public ActionResult ResultOfLastInvoke { get; set; } @@ -20,6 +21,13 @@ protected override ActionExecutedContext InvokeActionMethodWithFilters(Controlle return context; } + protected override ActionExecutedContext EndInvokeActionMethodWithFilters(IAsyncResult asyncResult) + { + var context = base.EndInvokeActionMethodWithFilters(asyncResult); + this.ResultOfLastInvoke = context.Result; + return context; + } + /// protected override ExceptionContext InvokeExceptionFilters(ControllerContext controllerContext, IList filters, Exception exception) { diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Framework/ViewEngineCollectionExt.cs b/DNN Platform/DotNetNuke.Web.Mvc/Framework/ViewEngineCollectionExt.cs index 935007132de..1b6ab31f00d 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/ViewEngineCollectionExt.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/ViewEngineCollectionExt.cs @@ -49,7 +49,7 @@ public static ViewEngineResult FindView(this ViewEngineCollection viewEngineColl var parameters = new object[] { new Func(e => e.FindView(controllerContext, viewName, masterName, false)), - false, + true, // allow SearchedLocations tracking to improve error messages up the stack. }; var cacheArg = new CacheItemArgs(cacheKey, 120, CacheItemPriority.Default, "Find", viewEngineCollection, parameters); @@ -79,7 +79,7 @@ public static ViewEngineResult FindPartialView(this ViewEngineCollection viewEng var parameters = new object[] { new Func(e => e.FindPartialView(controllerContext, partialViewName, false)), - false, + true, // allow SearchedLocations tracking to improve error messages up the stack. }; var cacheArg = new CacheItemArgs(cacheKey, 120, CacheItemPriority.Default, "Find", viewEngineCollection, parameters); diff --git a/DNN Platform/DotNetNuke.Web.Mvc/MvcHostControl.cs b/DNN Platform/DotNetNuke.Web.Mvc/MvcHostControl.cs index 5a481d0241a..e9852e02bac 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/MvcHostControl.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/MvcHostControl.cs @@ -29,28 +29,52 @@ namespace DotNetNuke.Web.Mvc /// WebForms control for hosting an MVC module control. public class MvcHostControl : ModuleControlBase, IActionable { - private ModuleRequestResult result; - private string controlKey; - /// Initializes a new instance of the class. public MvcHostControl() { - this.controlKey = string.Empty; + this.ControlKey = string.Empty; } /// Initializes a new instance of the class. /// The module control key. public MvcHostControl(string controlKey) { - this.controlKey = controlKey; + this.ControlKey = controlKey; } /// - public ModuleActionCollection ModuleActions { get; private set; } + public ModuleActionCollection ModuleActions { get; protected set; } + + protected ModuleRequestResult Result { get; set; } + + protected string ControlKey { get; set; } /// Gets or sets a value indicating whether the module controller should execute immediately (i.e. during rather than ). protected bool ExecuteModuleImmediately { get; set; } = true; + protected static IModuleExecutionEngine GetModuleExecutionEngine() + { + var moduleExecutionEngine = ComponentFactory.GetComponent(); + + if (moduleExecutionEngine == null) + { + moduleExecutionEngine = new ModuleExecutionEngine(); + ComponentFactory.RegisterComponentInstance(moduleExecutionEngine); + } + + return moduleExecutionEngine; + } + + protected static MvcHtmlString RenderModule(ModuleRequestResult moduleResult) + { + using var writer = new StringWriter(CultureInfo.CurrentCulture); + var moduleExecutionEngine = ComponentFactory.GetComponent(); + + moduleExecutionEngine.ExecuteModuleResult(moduleResult, writer); + + return MvcHtmlString.Create(writer.ToString()); + } + /// Runs and renders the MVC action. protected void ExecuteModule() { @@ -60,11 +84,11 @@ protected void ExecuteModule() var moduleExecutionEngine = GetModuleExecutionEngine(); - this.result = moduleExecutionEngine.ExecuteModule(this.GetModuleRequestContext(httpContext)); + this.Result = moduleExecutionEngine.ExecuteModule(this.GetModuleRequestContext(httpContext)); - this.ModuleActions = this.LoadActions(this.result); + this.ModuleActions = this.LoadActions(this.Result); - httpContext.SetModuleRequestResult(this.result); + httpContext.SetModuleRequestResult(this.Result); } catch (Exception exc) { @@ -76,7 +100,11 @@ protected void ExecuteModule() protected override void OnInit(EventArgs e) { base.OnInit(e); + this.OnInitInternal(e); + } + protected virtual void OnInitInternal(EventArgs e) + { if (this.ExecuteModuleImmediately) { this.ExecuteModule(); @@ -87,14 +115,19 @@ protected override void OnInit(EventArgs e) protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); + this.OnPreRenderInternal(e); + } + + protected virtual void OnPreRenderInternal(EventArgs e) + { try { - if (this.result == null) + if (this.Result == null) { return; } - var mvcString = RenderModule(this.result); + var mvcString = RenderModule(this.Result); if (!string.IsNullOrEmpty(Convert.ToString(mvcString, CultureInfo.InvariantCulture))) { this.Controls.Add(new LiteralControl(Convert.ToString(mvcString, CultureInfo.InvariantCulture))); @@ -106,59 +139,7 @@ protected override void OnPreRender(EventArgs e) } } - private static ModuleApplication GetModuleApplication( - IBusinessControllerProvider businessControllerProvider, - DesktopModuleInfo desktopModule, - RouteData defaultRouteData) - { - // Check if the MVC Module overrides the base ModuleApplication class. - var moduleApplication = businessControllerProvider.GetInstance(desktopModule); - if (moduleApplication != null) - { - defaultRouteData.Values["controller"] = moduleApplication.DefaultControllerName; - defaultRouteData.Values["action"] = moduleApplication.DefaultActionName; - defaultRouteData.DataTokens["namespaces"] = moduleApplication.DefaultNamespaces; - return moduleApplication; - } - - var defaultControllerName = (string)defaultRouteData.Values["controller"]; - var defaultActionName = (string)defaultRouteData.Values["action"]; - var defaultNamespaces = (string[])defaultRouteData.DataTokens["namespaces"]; - - return new ModuleApplication - { - DefaultActionName = defaultControllerName, - DefaultControllerName = defaultActionName, - DefaultNamespaces = defaultNamespaces, - ModuleName = desktopModule.ModuleName, - FolderPath = desktopModule.FolderName, - }; - } - - private static IModuleExecutionEngine GetModuleExecutionEngine() - { - var moduleExecutionEngine = ComponentFactory.GetComponent(); - - if (moduleExecutionEngine == null) - { - moduleExecutionEngine = new ModuleExecutionEngine(); - ComponentFactory.RegisterComponentInstance(moduleExecutionEngine); - } - - return moduleExecutionEngine; - } - - private static MvcHtmlString RenderModule(ModuleRequestResult moduleResult) - { - using var writer = new StringWriter(CultureInfo.CurrentCulture); - var moduleExecutionEngine = ComponentFactory.GetComponent(); - - moduleExecutionEngine.ExecuteModuleResult(moduleResult, writer); - - return MvcHtmlString.Create(writer.ToString()); - } - - private ModuleRequestContext GetModuleRequestContext(HttpContextBase httpContext) + protected ModuleRequestContext GetModuleRequestContext(HttpContextBase httpContext) { var module = this.ModuleContext.Configuration; @@ -177,9 +158,9 @@ private ModuleRequestContext GetModuleRequestContext(HttpContextBase httpContext var queryString = httpContext.Request.QueryString; - if (string.IsNullOrEmpty(this.controlKey)) + if (string.IsNullOrEmpty(this.ControlKey)) { - this.controlKey = queryString.GetValueOrDefault("ctl", string.Empty); + this.ControlKey = queryString.GetValueOrDefault("ctl", string.Empty); } var moduleId = Null.NullInteger; @@ -191,14 +172,14 @@ private ModuleRequestContext GetModuleRequestContext(HttpContextBase httpContext } } - if (moduleId != this.ModuleContext.ModuleId && string.IsNullOrEmpty(this.controlKey)) + if (moduleId != this.ModuleContext.ModuleId && string.IsNullOrEmpty(this.ControlKey)) { // Set default routeData for module that is not the "selected" module routeData = defaultRouteData; } else { - var control = ModuleControlControllerAdapter.Instance.GetModuleControlByControlKey(this.controlKey, module.ModuleDefID); + var control = ModuleControlControllerAdapter.Instance.GetModuleControlByControlKey(this.ControlKey, module.ModuleDefID); routeData = ModuleRoutingProvider.Instance().GetRouteData(httpContext, control); } @@ -214,7 +195,7 @@ private ModuleRequestContext GetModuleRequestContext(HttpContextBase httpContext return moduleRequestContext; } - private ModuleActionCollection LoadActions(ModuleRequestResult requestResult) + protected ModuleActionCollection LoadActions(ModuleRequestResult requestResult) { var actions = new ModuleActionCollection(); @@ -229,5 +210,34 @@ private ModuleActionCollection LoadActions(ModuleRequestResult requestResult) return actions; } + + private static ModuleApplication GetModuleApplication( + IBusinessControllerProvider businessControllerProvider, + DesktopModuleInfo desktopModule, + RouteData defaultRouteData) + { + // Check if the MVC Module overrides the base ModuleApplication class. + var moduleApplication = businessControllerProvider.GetInstance(desktopModule); + if (moduleApplication != null) + { + defaultRouteData.Values["controller"] = moduleApplication.DefaultControllerName; + defaultRouteData.Values["action"] = moduleApplication.DefaultActionName; + defaultRouteData.DataTokens["namespaces"] = moduleApplication.DefaultNamespaces; + return moduleApplication; + } + + var defaultControllerName = (string)defaultRouteData.Values["controller"]; + var defaultActionName = (string)defaultRouteData.Values["action"]; + var defaultNamespaces = (string[])defaultRouteData.DataTokens["namespaces"]; + + return new ModuleApplication + { + DefaultActionName = defaultControllerName, + DefaultControllerName = defaultActionName, + DefaultNamespaces = defaultNamespaces, + ModuleName = desktopModule.ModuleName, + FolderPath = desktopModule.FolderName, + }; + } } } diff --git a/DNN Platform/DotNetNuke.Web.Mvc/MvcHttpModule.cs b/DNN Platform/DotNetNuke.Web.Mvc/MvcHttpModule.cs index 0223db0d994..2cdc6ee601b 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/MvcHttpModule.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/MvcHttpModule.cs @@ -11,7 +11,6 @@ namespace DotNetNuke.Web.Mvc using System.Web; using System.Web.Helpers; using System.Web.Mvc; - using System.Web.Routing; using System.Xml; using DotNetNuke.Abstractions.Application; @@ -23,7 +22,6 @@ namespace DotNetNuke.Web.Mvc using DotNetNuke.Entities.Host; using DotNetNuke.Entities.Modules; using DotNetNuke.Entities.Portals; - using DotNetNuke.Framework.Reflections; using DotNetNuke.Services.Log.EventLog; using DotNetNuke.Web.Mvc.Framework; using DotNetNuke.Web.Mvc.Framework.Modules; diff --git a/DNN Platform/DotNetNuke.Web.Mvc/MvcModuleControlFactory.cs b/DNN Platform/DotNetNuke.Web.Mvc/MvcModuleControlFactory.cs index b6956e4832b..c04631a0087 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/MvcModuleControlFactory.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/MvcModuleControlFactory.cs @@ -23,12 +23,22 @@ public override bool SupportsControl(ModuleInfo moduleConfiguration, string cont /// public override Control CreateControl(TemplateControl containerControl, string controlKey, string controlSrc) { + if (IsAsyncControl(controlSrc)) + { + return new AsyncMvcHostControl(controlKey); + } + return new MvcHostControl(controlKey); } /// public override Control CreateModuleControl(TemplateControl containerControl, ModuleInfo moduleConfiguration) { + if (IsAsyncControl(moduleConfiguration.ModuleControl.ControlSrc)) + { + return new AsyncMvcHostControl(); + } + return new MvcHostControl(); } @@ -37,9 +47,9 @@ public override ModuleControlBase CreateModuleControl(ModuleInfo moduleConfigura { ModuleControlBase moduleControl = base.CreateModuleControl(moduleConfiguration); - var segments = moduleConfiguration.ModuleControl.ControlSrc.Replace(".mvc", string.Empty).Split('/'); + var segments = moduleConfiguration.ModuleControl.ControlSrc.Split('/'); - moduleControl.LocalResourceFile = $"~/DesktopModules/MVC/{moduleConfiguration.DesktopModule.FolderName}/{Localization.LocalResourceDirectory}/{segments[0]}.resx"; + moduleControl.LocalResourceFile = $"~/DesktopModules/MVC/{moduleConfiguration.DesktopModule.FolderName}/{Localization.LocalResourceDirectory}/{(segments.Length == 2 ? segments[0] : segments[1])}.resx"; return moduleControl; } @@ -47,7 +57,18 @@ public override ModuleControlBase CreateModuleControl(ModuleInfo moduleConfigura /// public override Control CreateSettingsControl(TemplateControl containerControl, ModuleInfo moduleConfiguration, string controlSrc) { + if (IsAsyncControl(controlSrc)) + { + return new AsyncMvcSettingsControl(); + } + return new MvcSettingsControl(); } + + private static bool IsAsyncControl(string controlSrc) + { + var segments = controlSrc.Split('/'); + return segments.Length == 4 && segments[2].Equals("async", System.StringComparison.OrdinalIgnoreCase); + } } } diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Routing/StandardModuleRoutingProvider.cs b/DNN Platform/DotNetNuke.Web.Mvc/Routing/StandardModuleRoutingProvider.cs index 47f544694a5..52a06754dbb 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Routing/StandardModuleRoutingProvider.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Routing/StandardModuleRoutingProvider.cs @@ -61,7 +61,13 @@ public override RouteData GetRouteData(HttpContextBase httpContext, ModuleContro string routeNamespace = string.Empty; string routeControllerName; string routeActionName; - if (segments.Length == 3) + if (segments.Length == 4) + { + routeNamespace = segments[0]; + routeControllerName = segments[1]; + routeActionName = segments[3]; + } + else if (segments.Length == 3) { routeNamespace = segments[0]; routeControllerName = segments[1]; diff --git a/DNN Platform/Library/UI/Containers/ActionBase.cs b/DNN Platform/Library/UI/Containers/ActionBase.cs index 5b7b1e170af..bfa20a57060 100644 --- a/DNN Platform/Library/UI/Containers/ActionBase.cs +++ b/DNN Platform/Library/UI/Containers/ActionBase.cs @@ -5,6 +5,8 @@ namespace DotNetNuke.UI.Containers { using System; using System.Diagnostics.CodeAnalysis; + using System.Threading; + using System.Threading.Tasks; using System.Web.UI; using DotNetNuke.Abstractions.Logging; @@ -108,6 +110,25 @@ protected virtual void OnAction(ActionEventArgs e) /// ProcessAction processes the action event. /// The id of the action. protected void ProcessAction(string actionID) + { + if (this.ModuleControl is IAsyncModuleControl) + { + // We need to defer accesing this.Actions as it could only be accesible after the WebForms async point. + this.Page.RegisterAsyncTask(new PageAsyncTask(ct => this.ProcessActionInternalAsync(actionID, ct))); + } + else + { + this.ProcessActionInternal(actionID); + } + } + + protected Task ProcessActionInternalAsync(string actionID, CancellationToken cancellationToken) + { + this.ProcessActionInternal(actionID); + return Task.CompletedTask; + } + + protected void ProcessActionInternal(string actionID) { if (int.TryParse(actionID, out var output)) { @@ -126,21 +147,35 @@ protected void ProcessAction(string actionID) /// The event arguments. protected override void OnLoad(EventArgs e) { - try + if (this.ModuleControl is IAsyncModuleControl) { - if (this.ModuleControl == null) - { - return; - } + // We need to defer accesing this.Actions as it could only be accesible after the WebForms async point. + this.Page.RegisterAsyncTask(new PageAsyncTask(this.LoadActionsAsync)); + } + else + { + this.LoadActions(); + } + base.OnLoad(e); + } + + private Task LoadActionsAsync(CancellationToken cancellationToken) + { + this.LoadActions(); + return Task.CompletedTask; + } + + private void LoadActions() + { + try + { this.ActionRoot.Actions.AddRange(this.Actions); } catch (Exception exc) { Exceptions.ProcessModuleLoadException(this, exc); } - - base.OnLoad(e); } } } diff --git a/DNN Platform/Library/UI/Modules/IAsyncSettingsControl.cs b/DNN Platform/Library/UI/Modules/IAsyncSettingsControl.cs new file mode 100644 index 00000000000..4e4e30a9e35 --- /dev/null +++ b/DNN Platform/Library/UI/Modules/IAsyncSettingsControl.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.UI.Modules +{ + using System.Threading; + using System.Threading.Tasks; + + /// IAsyncSettingsControl provides a common Interface for Module Settings Controls that need to execute async work. + public interface IAsyncSettingsControl : ISettingsControl + { + /// Loads the module settings asynchronously. + /// cancellationToken. + /// A representing the asynchronous operation. + Task LoadSettingsAsync(CancellationToken cancellationToken); + + /// Updates the module settings asynchronously. + /// cancellationToken. + /// A representing the asynchronous operation. + Task UpdateSettingsAsync(CancellationToken cancellationToken); + } +} diff --git a/DNN Platform/Library/UI/Modules/IModuleControl.cs b/DNN Platform/Library/UI/Modules/IModuleControl.cs index d4581ecb3d6..4f44a9ca90b 100644 --- a/DNN Platform/Library/UI/Modules/IModuleControl.cs +++ b/DNN Platform/Library/UI/Modules/IModuleControl.cs @@ -22,5 +22,9 @@ public interface IModuleControl /// Gets or sets the local resource localization file for the control. string LocalResourceFile { get; set; } + } + + public interface IAsyncModuleControl + { } } diff --git a/DNN Platform/Library/UI/Modules/ModuleControlFactory.cs b/DNN Platform/Library/UI/Modules/ModuleControlFactory.cs index ba854534ca0..5e15536437a 100644 --- a/DNN Platform/Library/UI/Modules/ModuleControlFactory.cs +++ b/DNN Platform/Library/UI/Modules/ModuleControlFactory.cs @@ -162,9 +162,9 @@ public static partial Control CreateModuleControl(ModuleInfo moduleConfiguration switch (extension) { case ".mvc": - var segments = moduleConfiguration.ModuleControl.ControlSrc.Replace(".mvc", string.Empty).Split('/'); + var segments = moduleConfiguration.ModuleControl.ControlSrc.Split('/'); - moduleControl.LocalResourceFile = $"~/DesktopModules/MVC/{moduleConfiguration.DesktopModule.FolderName}/{Localization.LocalResourceDirectory}/{segments[0]}.resx"; + moduleControl.LocalResourceFile = $"~/DesktopModules/MVC/{moduleConfiguration.DesktopModule.FolderName}/{Localization.LocalResourceDirectory}/{(segments.Length == 2 ? segments[0] : segments[1])}.resx"; break; default: moduleControl.LocalResourceFile = moduleConfiguration.ModuleControl.ControlSrc.Replace(Path.GetFileName(moduleConfiguration.ModuleControl.ControlSrc), string.Empty) + diff --git a/DNN Platform/Library/UI/Modules/ModuleInstanceContext.cs b/DNN Platform/Library/UI/Modules/ModuleInstanceContext.cs index aa1c2f2f89e..181d8abe338 100644 --- a/DNN Platform/Library/UI/Modules/ModuleInstanceContext.cs +++ b/DNN Platform/Library/UI/Modules/ModuleInstanceContext.cs @@ -510,6 +510,13 @@ private void LoadActions(HttpRequest request) var actionable = this.moduleControl as IActionable; if (actionable != null) { + // Async module controls populate ModuleActions only after their async task executes. + if (this.moduleControl is IAsyncModuleControl && actionable.ModuleActions == null) + { + throw new InvalidOperationException("Too early to access the ModuleActions. For async controls, ModuleActions collection is available only after the framework executes the `Page.ExecuteRegisteredAsyncTasks()`. " + + "More specifically, you have to either register an async task using `Page.RegisterAsyncTask()` or use any of the sync events starting from PreRenderComplete to access them."); + } + this.moduleSpecificActions = new ModuleAction(this.GetNextActionID(), Localization.GetString("ModuleSpecificActions.Action", Localization.GlobalResourceFile), string.Empty, string.Empty, string.Empty); ModuleActionCollection moduleActions = actionable.ModuleActions; diff --git a/DNN Platform/Website/Default.aspx.cs b/DNN Platform/Website/Default.aspx.cs index 53bafe4e5be..5fbf6061297 100644 --- a/DNN Platform/Website/Default.aspx.cs +++ b/DNN Platform/Website/Default.aspx.cs @@ -630,9 +630,9 @@ private void InitializePage() switch (extension) { case ".mvc": - var segments = slaveModule.ModuleControl.ControlSrc.Replace(".mvc", string.Empty).Split('/'); + var segments = slaveModule.ModuleControl.ControlSrc.Split('/'); control.LocalResourceFile = - $"~/DesktopModules/MVC/{slaveModule.DesktopModule.FolderName}/{Localization.LocalResourceDirectory}/{segments[0]}.resx"; + $"~/DesktopModules/MVC/{slaveModule.DesktopModule.FolderName}/{Localization.LocalResourceDirectory}/{(segments.Length == 2 ? segments[0] : segments[1])}.resx"; break; default: var controlFileName = Path.GetFileName(slaveModule.ModuleControl.ControlSrc); diff --git a/DNN Platform/Website/admin/Menus/ModuleActions/ModuleActions.ascx.cs b/DNN Platform/Website/admin/Menus/ModuleActions/ModuleActions.ascx.cs index dd1b6b4c6b7..684682123ca 100644 --- a/DNN Platform/Website/admin/Menus/ModuleActions/ModuleActions.ascx.cs +++ b/DNN Platform/Website/admin/Menus/ModuleActions/ModuleActions.ascx.cs @@ -6,6 +6,8 @@ namespace DotNetNuke.Admin.Containers { using System; using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; using System.Web.Script.Serialization; using System.Web.UI; @@ -154,65 +156,104 @@ protected override void OnLoad(EventArgs e) this.clientResourceController.RegisterScript("~/admin/menus/ModuleActions/dnnQuickSettings.js"); } - if (this.ActionRoot.Visible) + if (this.ModuleControl is IAsyncModuleControl) { - // Add Menu Items - foreach (ModuleAction rootAction in this.ActionRoot.Actions) + // We need to defer accesing this.Actions as it could only be accesible after the WebForms async point. + this.Page.RegisterAsyncTask(new PageAsyncTask(this.ProcessActionsAsync)); + } + else + { + this.ProcessActions(); + } + } + catch (Exception exc) + { + Exceptions.ProcessModuleLoadException(this, exc); + } + } + + /// + protected override void Render(HtmlTextWriter writer) + { + base.Render(writer); + + foreach (int id in this.validIDs) + { + this.Page.ClientScript.RegisterForEventValidation(this.actionButton.UniqueID, id.ToString()); + } + } + + private Task ProcessActionsAsync(CancellationToken cancellationToken) + { + this.ProcessActions(); + return Task.CompletedTask; + } + + private void ProcessActions() + { + try + { + if (!this.ActionRoot.Visible) + { + return; + } + + // Add Menu Items + foreach (ModuleAction rootAction in this.ActionRoot.Actions) + { + // Process Children + var actions = new List(); + foreach (ModuleAction action in rootAction.Actions) { - // Process Children - var actions = new List(); - foreach (ModuleAction action in rootAction.Actions) + if (action.Visible) { - if (action.Visible) + if ((this.EditMode && Globals.IsAdminControl() == false) || + (action.Secure != SecurityAccessLevel.Anonymous && action.Secure != SecurityAccessLevel.View)) { - if ((this.EditMode && Globals.IsAdminControl() == false) || - (action.Secure != SecurityAccessLevel.Anonymous && action.Secure != SecurityAccessLevel.View)) + if (!action.Icon.Contains("://") + && !action.Icon.StartsWith("/", StringComparison.Ordinal) + && !action.Icon.StartsWith("~/", StringComparison.Ordinal)) + { + action.Icon = "~/images/" + action.Icon; + } + + if (action.Icon.StartsWith("~/", StringComparison.Ordinal)) + { + action.Icon = Globals.ResolveUrl(action.Icon); + } + + actions.Add(action); + + if (string.IsNullOrEmpty(action.Url)) { - if (!action.Icon.Contains("://") - && !action.Icon.StartsWith("/", StringComparison.Ordinal) - && !action.Icon.StartsWith("~/", StringComparison.Ordinal)) - { - action.Icon = "~/images/" + action.Icon; - } - - if (action.Icon.StartsWith("~/", StringComparison.Ordinal)) - { - action.Icon = Globals.ResolveUrl(action.Icon); - } - - actions.Add(action); - - if (string.IsNullOrEmpty(action.Url)) - { - this.validIDs.Add(action.ID); - } + this.validIDs.Add(action.ID); } } } + } - var oSerializer = new JavaScriptSerializer(); - if (rootAction.Title == Localization.GetString("ModuleGenericActions.Action", Localization.GlobalResourceFile)) + var oSerializer = new JavaScriptSerializer(); + if (rootAction.Title == Localization.GetString("ModuleGenericActions.Action", Localization.GlobalResourceFile)) + { + this.AdminActionsJSON = oSerializer.Serialize(actions); + } + else + { + if (rootAction.Title == Localization.GetString("ModuleSpecificActions.Action", Localization.GlobalResourceFile)) { - this.AdminActionsJSON = oSerializer.Serialize(actions); + this.CustomActionsJSON = oSerializer.Serialize(actions); } else { - if (rootAction.Title == Localization.GetString("ModuleSpecificActions.Action", Localization.GlobalResourceFile)) - { - this.CustomActionsJSON = oSerializer.Serialize(actions); - } - else - { - this.SupportsMove = actions.Count > 0; - this.Panes = oSerializer.Serialize(this.PortalSettings.ActiveTab.Panes); - } + this.SupportsMove = actions.Count > 0; + this.Panes = oSerializer.Serialize(this.PortalSettings.ActiveTab.Panes); } } - - this.IsShared = this.ModuleContext.Configuration.AllTabs - || PortalGroupController.Instance.IsModuleShared(this.ModuleContext.ModuleId, PortalController.Instance.GetPortal(this.PortalSettings.PortalId)) - || TabController.Instance.GetTabsByModuleID(this.ModuleContext.ModuleId).Count > 1; } + + this.IsShared = this.ModuleContext.Configuration.AllTabs + || PortalGroupController.Instance.IsModuleShared(this.ModuleContext.ModuleId, PortalController.Instance.GetPortal(this.PortalSettings.PortalId)) + || TabController.Instance.GetTabsByModuleID(this.ModuleContext.ModuleId).Count > 1; } catch (Exception exc) { @@ -220,17 +261,6 @@ protected override void OnLoad(EventArgs e) } } - /// - protected override void Render(HtmlTextWriter writer) - { - base.Render(writer); - - foreach (int id in this.validIDs) - { - this.Page.ClientScript.RegisterForEventValidation(this.actionButton.UniqueID, id.ToString()); - } - } - private void ActionButton_Click(object sender, EventArgs e) { this.ProcessAction(this.Request.Params["__EVENTARGUMENT"]); diff --git a/DNN Platform/Website/admin/Modules/Modulesettings.ascx.cs b/DNN Platform/Website/admin/Modules/Modulesettings.ascx.cs index f3e4b2501ca..6102888422b 100644 --- a/DNN Platform/Website/admin/Modules/Modulesettings.ascx.cs +++ b/DNN Platform/Website/admin/Modules/Modulesettings.ascx.cs @@ -12,6 +12,7 @@ namespace DotNetNuke.Modules.Admin.Modules using System.Linq; using System.Text; using System.Threading; + using System.Threading.Tasks; using System.Web.UI; using DotNetNuke.Abstractions; @@ -320,7 +321,25 @@ protected override void OnLoad(EventArgs e) { // Get the module settings from the PortalSettings and pass the // two settings hashtables to the sub control to process - this.SettingsControl.LoadSettings(); + if (this.SettingsControl is IAsyncSettingsControl asyncSettingsControl) + { + this.Page.RegisterAsyncTask(new PageAsyncTask(async cancellationToken => + { + try + { + await asyncSettingsControl.LoadSettingsAsync(cancellationToken); + } + catch (Exception exc) + { + Exceptions.ProcessModuleLoadException(this, exc); + } + })); + } + else + { + this.SettingsControl.LoadSettings(); + } + this.specificSettingsTab.Visible = true; this.fsSpecific.Visible = true; } @@ -506,12 +525,33 @@ protected void OnUpdateClick(object sender, EventArgs e) this.Module.AllModules = this.chkAllModules.Checked; ModuleController.Instance.UpdateModule(this.Module); + var executeAsync = false; + // Update Custom Settings if (this.SettingsControl != null) { try { - this.SettingsControl.UpdateSettings(); + if (this.SettingsControl is IAsyncSettingsControl asyncSettingsControl) + { + executeAsync = true; + this.Page.RegisterAsyncTask(new PageAsyncTask(async cancellationToken => + { + try + { + await asyncSettingsControl.UpdateSettingsAsync(cancellationToken); + Continuation(); + } + catch (Exception exc) + { + Exceptions.ProcessModuleLoadException(this, exc); + } + })); + } + else + { + this.SettingsControl.UpdateSettings(); + } } catch (ThreadAbortException exc) { @@ -525,71 +565,79 @@ protected void OnUpdateClick(object sender, EventArgs e) } } - // These Module Copy/Move statements must be - // at the end of the Update as the Controller code assumes all the - // Updates to the Module have been carried out. + if (!executeAsync) + { + Continuation(); + } - // Check if the Module is to be Moved to a new Tab - if (!this.chkAllTabs.Checked) + void Continuation() { - var newTabId = int.Parse(this.cboTab.SelectedValue); - if (this.TabId != newTabId) + // These Module Copy/Move statements must be + // at the end of the Update as the Controller code assumes all the + // Updates to the Module have been carried out. + + // Check if the Module is to be Moved to a new Tab + if (!this.chkAllTabs.Checked) { - // First check if there already is an instance of the module on the target page - var tmpModule = ModuleController.Instance.GetModule(this.moduleId, newTabId, false); - if (tmpModule == null) - { - // Move module - ModuleController.Instance.MoveModule(this.moduleId, this.TabId, newTabId, Globals.glbDefaultPane); - } - else + var newTabId = int.Parse(this.cboTab.SelectedValue); + if (this.TabId != newTabId) { - // Warn user - Skin.AddModuleMessage(this, Localization.GetString("ModuleExists", this.LocalResourceFile), ModuleMessage.ModuleMessageType.RedError); - return; + // First check if there already is an instance of the module on the target page + var tmpModule = ModuleController.Instance.GetModule(this.moduleId, newTabId, false); + if (tmpModule == null) + { + // Move module + ModuleController.Instance.MoveModule(this.moduleId, this.TabId, newTabId, Globals.glbDefaultPane); + } + else + { + // Warn user + Skin.AddModuleMessage(this, Localization.GetString("ModuleExists", this.LocalResourceFile), ModuleMessage.ModuleMessageType.RedError); + return; + } } } - } - // Check if Module is to be Added/Removed from all Tabs - if (allTabsChanged) - { - var listTabs = TabController.GetPortalTabs(this.hostSettings, this.appStatus, this.PortalSettings.PortalId, Null.NullInteger, false, true); - if (this.chkAllTabs.Checked) + // Check if Module is to be Added/Removed from all Tabs + if (allTabsChanged) { - if (!this.chkNewTabs.Checked) + var listTabs = TabController.GetPortalTabs(this.hostSettings, this.appStatus, this.PortalSettings.PortalId, Null.NullInteger, false, true); + if (this.chkAllTabs.Checked) { - foreach (var destinationTab in listTabs) + if (!this.chkNewTabs.Checked) { - var module = ModuleController.Instance.GetModule(this.moduleId, destinationTab.TabID, false); - if (module != null) + foreach (var destinationTab in listTabs) { - if (module.IsDeleted) + var module = ModuleController.Instance.GetModule(this.moduleId, destinationTab.TabID, false); + if (module != null) { - ModuleController.Instance.RestoreModule(module); + if (module.IsDeleted) + { + ModuleController.Instance.RestoreModule(module); + } } - } - else - { - if (!this.PortalSettings.ContentLocalizationEnabled || (this.Module.CultureCode == destinationTab.CultureCode)) + else { - ModuleController.Instance.CopyModule(this.Module, destinationTab, this.Module.PaneName, true); + if (!this.PortalSettings.ContentLocalizationEnabled || (this.Module.CultureCode == destinationTab.CultureCode)) + { + ModuleController.Instance.CopyModule(this.Module, destinationTab, this.Module.PaneName, true); + } } } } } + else + { + ModuleController.Instance.DeleteAllModules(this.moduleId, this.TabId, listTabs, true, false, false); + } } - else + + if (!this.DoNotRedirectOnUpdate) { - ModuleController.Instance.DeleteAllModules(this.moduleId, this.TabId, listTabs, true, false, false); + // Navigate back to admin page + this.Response.Redirect(this.ReturnURL, true); } } - - if (!this.DoNotRedirectOnUpdate) - { - // Navigate back to admin page - this.Response.Redirect(this.ReturnURL, true); - } } } catch (Exception exc)