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)