From d12c894087f585b66bfebc1f822fdc1c84c11409 Mon Sep 17 00:00:00 2001 From: Robert Dima Date: Tue, 10 Oct 2023 12:12:15 +0300 Subject: [PATCH 1/5] Support for async execution of MVC Controller Actions. --- .../DotNetNuke.Web.Mvc/DnnMvcHandler.cs | 26 ++-- .../Modules/IModuleExecutionEngine.cs | 4 + .../Framework/Modules/ModuleApplication.cs | 81 ++++++++++ .../Modules/ModuleExecutionEngine.cs | 15 ++ .../Modules/ResultCapturingActionInvoker.cs | 10 +- .../DotNetNuke.Web.Mvc/MvcHostControl.cs | 33 ++++- .../DotNetNuke.Web.Mvc/MvcSettingsControl.cs | 23 ++- .../Library/UI/Containers/ActionBase.cs | 20 ++- .../UI/Modules/IAsyncSettingsControl.cs | 23 +++ .../Menus/ModuleActions/ModuleActions.ascx.cs | 128 +++++++++------- .../admin/Modules/Modulesettings.ascx.cs | 138 ++++++++++++------ 11 files changed, 377 insertions(+), 124 deletions(-) create mode 100644 DNN Platform/Library/UI/Modules/IAsyncSettingsControl.cs 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/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..473f5157267 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,83 @@ 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.ValidateRequest = false; + + moduleController.DnnPage = context.DnnPage; + + moduleController.ModuleContext = context.ModuleContext; + + moduleController.LocalResourceFile = string.Format( + "~/DesktopModules/MVC/{0}/{1}/{2}.resx", + context.ModuleContext.Configuration.DesktopModule.FolderName, + Localization.LocalResourceDirectory, + controllerName); + + moduleController.ViewEngineCollectionEx = this.ViewEngines; + + // 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 + if (moduleController is IAsyncController asyncController) + { + await Task.Factory.FromAsync(asyncController.BeginExecute, asyncController.EndExecute, this.RequestContext, null); + } + else + { + moduleController.Execute(this.RequestContext); + } + + 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/MvcHostControl.cs b/DNN Platform/DotNetNuke.Web.Mvc/MvcHostControl.cs index 5a481d0241a..1fbdde7f7a2 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/MvcHostControl.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/MvcHostControl.cs @@ -6,6 +6,8 @@ namespace DotNetNuke.Web.Mvc using System; using System.Globalization; using System.IO; + using System.Threading; + using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using System.Web.Routing; @@ -72,6 +74,26 @@ protected void ExecuteModule() } } + protected async Task ExecuteModuleAsync(CancellationToken cancellationToken) + { + try + { + HttpContextBase httpContext = new HttpContextWrapper(HttpContext.Current); + + var moduleExecutionEngine = this.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); + } + } + /// protected override void OnInit(EventArgs e) { @@ -79,7 +101,7 @@ protected override void OnInit(EventArgs e) if (this.ExecuteModuleImmediately) { - this.ExecuteModule(); + this.Page.RegisterAsyncTask(new PageAsyncTask(this.ExecuteModuleAsync)); } } @@ -87,11 +109,16 @@ protected override void OnInit(EventArgs e) protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); + this.Page.RegisterAsyncTask(new PageAsyncTask(this.OnPreRenderAsync)); + } + + private Task OnPreRenderAsync(CancellationToken cancellationToken) + { try { if (this.result == null) { - return; + return Task.CompletedTask; } var mvcString = RenderModule(this.result); @@ -104,6 +131,8 @@ protected override void OnPreRender(EventArgs e) { Exceptions.ProcessModuleLoadException(this, exc); } + + return Task.CompletedTask; } private static ModuleApplication GetModuleApplication( diff --git a/DNN Platform/DotNetNuke.Web.Mvc/MvcSettingsControl.cs b/DNN Platform/DotNetNuke.Web.Mvc/MvcSettingsControl.cs index b151e25fb59..f2f1ee270e2 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/MvcSettingsControl.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/MvcSettingsControl.cs @@ -4,10 +4,13 @@ namespace DotNetNuke.Web.Mvc { + using System.Threading; + using System.Threading.Tasks; + using DotNetNuke.Entities.Modules; using DotNetNuke.UI.Modules; - public class MvcSettingsControl : MvcHostControl, ISettingsControl + public class MvcSettingsControl : MvcHostControl, IAsyncSettingsControl { public MvcSettingsControl() : base("Settings") @@ -18,15 +21,33 @@ public MvcSettingsControl() /// public void LoadSettings() { + // TODO: This should now throw as control needs to always be executed asynchronously. + // throw new NotSupportedException(); this.ExecuteModule(); } /// + public Task LoadSettingsAsync(CancellationToken cancellationToken) + { + return this.ExecuteModuleAsync(cancellationToken); + } + + /// public void UpdateSettings() { + // TODO: This should now throw as control needs to always be executed asynchronously. + // throw new NotSupportedException(); this.ExecuteModule(); ModuleController.Instance.UpdateModule(this.ModuleContext.Configuration); } + + /// + public async Task UpdateSettingsAsync(CancellationToken cancellationToken) + { + await this.ExecuteModuleAsync(cancellationToken); + + ModuleController.Instance.UpdateModule(this.ModuleContext.Configuration); + } } } diff --git a/DNN Platform/Library/UI/Containers/ActionBase.cs b/DNN Platform/Library/UI/Containers/ActionBase.cs index 5b7b1e170af..16fdde1003e 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; @@ -126,13 +128,19 @@ protected void ProcessAction(string actionID) /// The event arguments. protected override void OnLoad(EventArgs e) { - try + if (this.ModuleControl != null) { - 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)); + } + base.OnLoad(e); + } + + private Task LoadActionsAsync(CancellationToken cancellationToken) + { + try + { this.ActionRoot.Actions.AddRange(this.Actions); } catch (Exception exc) @@ -140,7 +148,7 @@ protected override void OnLoad(EventArgs e) Exceptions.ProcessModuleLoadException(this, exc); } - base.OnLoad(e); + return Task.CompletedTask; } } } 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/Website/admin/Menus/ModuleActions/ModuleActions.ascx.cs b/DNN Platform/Website/admin/Menus/ModuleActions/ModuleActions.ascx.cs index dd1b6b4c6b7..3bf06d3203d 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,86 +156,108 @@ protected override void OnLoad(EventArgs e) this.clientResourceController.RegisterScript("~/admin/menus/ModuleActions/dnnQuickSettings.js"); } - if (this.ActionRoot.Visible) + this.Page.RegisterAsyncTask(new PageAsyncTask(this.ProcessActionsAsync)); + } + 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) + { + try + { + if (!this.ActionRoot.Visible) { - // Add Menu Items - foreach (ModuleAction rootAction in this.ActionRoot.Actions) + return Task.CompletedTask; + } + + // 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)) { - 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); - } + action.Icon = Globals.ResolveUrl(action.Icon); + } + + actions.Add(action); + + if (string.IsNullOrEmpty(action.Url)) + { + 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) { Exceptions.ProcessModuleLoadException(this, exc); } + + return Task.CompletedTask; } - /// - protected override void Render(HtmlTextWriter writer) + private void ActionButton_Click(object sender, EventArgs e) { - base.Render(writer); - - foreach (int id in this.validIDs) - { - this.Page.ClientScript.RegisterForEventValidation(this.actionButton.UniqueID, id.ToString()); - } + this.Page.RegisterAsyncTask(new PageAsyncTask(ct => this.ActionButton_ClickAsync(sender, e, ct))); } - private void ActionButton_Click(object sender, EventArgs e) + private Task ActionButton_ClickAsync(object sender, EventArgs e, CancellationToken cancellationToken) { this.ProcessAction(this.Request.Params["__EVENTARGUMENT"]); + return Task.CompletedTask; } } } 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) From 32451169660864d9dd7767d27722a75cf36033b1 Mon Sep 17 00:00:00 2001 From: Robert Dima Date: Tue, 27 Feb 2024 01:10:55 +0200 Subject: [PATCH 2/5] Better separate Async and non-async module hosts. + Avoids breaking changes for existing (non-async) MVC modules. + Fix selection of ControllerName from the segments of the ControlSrc when it has a length greater than 2. --- .../DotNetNuke.Web.Mvc/AsyncMvcHostControl.cs | 85 +++++++++ .../AsyncMvcSettingsControl.cs | 48 +++++ .../ModuleActionItemsAttribute.cs | 23 ++- .../Framework/Controllers/DnnController.cs | 7 + .../Framework/Controllers/IDnnController.cs | 5 + .../Framework/Modules/ModuleApplication.cs | 23 +-- .../DotNetNuke.Web.Mvc/MvcHostControl.cs | 165 ++++++++---------- .../DotNetNuke.Web.Mvc/MvcHttpModule.cs | 2 - .../MvcModuleControlFactory.cs | 25 ++- .../DotNetNuke.Web.Mvc/MvcSettingsControl.cs | 23 +-- .../Routing/StandardModuleRoutingProvider.cs | 8 +- .../Library/UI/Containers/ActionBase.cs | 32 +++- .../Library/UI/Modules/IModuleControl.cs | 4 + .../UI/Modules/ModuleControlFactory.cs | 4 +- DNN Platform/Website/Default.aspx.cs | 4 +- .../Menus/ModuleActions/ModuleActions.ascx.cs | 25 +-- 16 files changed, 329 insertions(+), 154 deletions(-) create mode 100644 DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcHostControl.cs create mode 100644 DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcSettingsControl.cs diff --git a/DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcHostControl.cs b/DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcHostControl.cs new file mode 100644 index 00000000000..706da19d7e2 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcHostControl.cs @@ -0,0 +1,85 @@ +// 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) + { + 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/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/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/ModuleApplication.cs b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ModuleApplication.cs index 473f5157267..05d43886d8f 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ModuleApplication.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ModuleApplication.cs @@ -167,6 +167,7 @@ public virtual async Task ExecuteRequestAsync(ModuleRequest { // Check if the controller supports IDnnController var moduleController = controller as IDnnController; + moduleController.IsAsync = true; // 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 @@ -181,24 +182,24 @@ public virtual async Task ExecuteRequestAsync(ModuleRequest moduleController.ModuleContext = context.ModuleContext; - moduleController.LocalResourceFile = string.Format( - "~/DesktopModules/MVC/{0}/{1}/{2}.resx", - context.ModuleContext.Configuration.DesktopModule.FolderName, - Localization.LocalResourceDirectory, - controllerName); + 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 - if (moduleController is IAsyncController asyncController) - { - await Task.Factory.FromAsync(asyncController.BeginExecute, asyncController.EndExecute, this.RequestContext, null); - } - else + await Task.Factory.FromAsync(asyncController.BeginExecute, asyncController.EndExecute, this.RequestContext, null); + if (moduleController.ModuleActionsAsync != null) { - moduleController.Execute(this.RequestContext); + moduleController.ModuleActions = await moduleController.ModuleActionsAsync; } var result = moduleController.ResultOfLastExecute; diff --git a/DNN Platform/DotNetNuke.Web.Mvc/MvcHostControl.cs b/DNN Platform/DotNetNuke.Web.Mvc/MvcHostControl.cs index 1fbdde7f7a2..e9852e02bac 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/MvcHostControl.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/MvcHostControl.cs @@ -6,8 +6,6 @@ namespace DotNetNuke.Web.Mvc using System; using System.Globalization; using System.IO; - using System.Threading; - using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using System.Web.Routing; @@ -31,62 +29,66 @@ 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; - /// Runs and renders the MVC action. - protected void ExecuteModule() + protected static IModuleExecutionEngine GetModuleExecutionEngine() { - try + var moduleExecutionEngine = ComponentFactory.GetComponent(); + + if (moduleExecutionEngine == null) { - HttpContextBase httpContext = new HttpContextWrapper(HttpContext.Current); + moduleExecutionEngine = new ModuleExecutionEngine(); + ComponentFactory.RegisterComponentInstance(moduleExecutionEngine); + } - var moduleExecutionEngine = GetModuleExecutionEngine(); + return moduleExecutionEngine; + } - this.result = moduleExecutionEngine.ExecuteModule(this.GetModuleRequestContext(httpContext)); + protected static MvcHtmlString RenderModule(ModuleRequestResult moduleResult) + { + using var writer = new StringWriter(CultureInfo.CurrentCulture); + var moduleExecutionEngine = ComponentFactory.GetComponent(); - this.ModuleActions = this.LoadActions(this.result); + moduleExecutionEngine.ExecuteModuleResult(moduleResult, writer); - httpContext.SetModuleRequestResult(this.result); - } - catch (Exception exc) - { - Exceptions.ProcessModuleLoadException(this, exc); - } + return MvcHtmlString.Create(writer.ToString()); } - protected async Task ExecuteModuleAsync(CancellationToken cancellationToken) + /// Runs and renders the MVC action. + protected void ExecuteModule() { try { HttpContextBase httpContext = new HttpContextWrapper(HttpContext.Current); - var moduleExecutionEngine = this.GetModuleExecutionEngine(); + var moduleExecutionEngine = GetModuleExecutionEngine(); - this.result = await moduleExecutionEngine.ExecuteModuleAsync(this.GetModuleRequestContext(httpContext), cancellationToken); + 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) { @@ -98,10 +100,14 @@ protected async Task ExecuteModuleAsync(CancellationToken cancellationToken) protected override void OnInit(EventArgs e) { base.OnInit(e); + this.OnInitInternal(e); + } + protected virtual void OnInitInternal(EventArgs e) + { if (this.ExecuteModuleImmediately) { - this.Page.RegisterAsyncTask(new PageAsyncTask(this.ExecuteModuleAsync)); + this.ExecuteModule(); } } @@ -109,19 +115,19 @@ protected override void OnInit(EventArgs e) protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); - this.Page.RegisterAsyncTask(new PageAsyncTask(this.OnPreRenderAsync)); + this.OnPreRenderInternal(e); } - private Task OnPreRenderAsync(CancellationToken cancellationToken) + protected virtual void OnPreRenderInternal(EventArgs e) { try { - if (this.result == null) + if (this.Result == null) { - return Task.CompletedTask; + 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))); @@ -131,63 +137,9 @@ private Task OnPreRenderAsync(CancellationToken cancellationToken) { Exceptions.ProcessModuleLoadException(this, exc); } - - return Task.CompletedTask; } - 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; @@ -206,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; @@ -220,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); } @@ -243,7 +195,7 @@ private ModuleRequestContext GetModuleRequestContext(HttpContextBase httpContext return moduleRequestContext; } - private ModuleActionCollection LoadActions(ModuleRequestResult requestResult) + protected ModuleActionCollection LoadActions(ModuleRequestResult requestResult) { var actions = new ModuleActionCollection(); @@ -258,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/MvcSettingsControl.cs b/DNN Platform/DotNetNuke.Web.Mvc/MvcSettingsControl.cs index f2f1ee270e2..b151e25fb59 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/MvcSettingsControl.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/MvcSettingsControl.cs @@ -4,13 +4,10 @@ namespace DotNetNuke.Web.Mvc { - using System.Threading; - using System.Threading.Tasks; - using DotNetNuke.Entities.Modules; using DotNetNuke.UI.Modules; - public class MvcSettingsControl : MvcHostControl, IAsyncSettingsControl + public class MvcSettingsControl : MvcHostControl, ISettingsControl { public MvcSettingsControl() : base("Settings") @@ -21,33 +18,15 @@ public MvcSettingsControl() /// public void LoadSettings() { - // TODO: This should now throw as control needs to always be executed asynchronously. - // throw new NotSupportedException(); this.ExecuteModule(); } /// - public Task LoadSettingsAsync(CancellationToken cancellationToken) - { - return this.ExecuteModuleAsync(cancellationToken); - } - - /// public void UpdateSettings() { - // TODO: This should now throw as control needs to always be executed asynchronously. - // throw new NotSupportedException(); this.ExecuteModule(); ModuleController.Instance.UpdateModule(this.ModuleContext.Configuration); } - - /// - public async Task UpdateSettingsAsync(CancellationToken cancellationToken) - { - await this.ExecuteModuleAsync(cancellationToken); - - ModuleController.Instance.UpdateModule(this.ModuleContext.Configuration); - } } } 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 16fdde1003e..f28bd1c1ed3 100644 --- a/DNN Platform/Library/UI/Containers/ActionBase.cs +++ b/DNN Platform/Library/UI/Containers/ActionBase.cs @@ -110,6 +110,24 @@ 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) + { + 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)) { @@ -128,16 +146,26 @@ protected void ProcessAction(string actionID) /// The event arguments. protected override void OnLoad(EventArgs e) { - if (this.ModuleControl != null) + 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(this.LoadActionsAsync)); } + else + { + this.LoadActions(); + } base.OnLoad(e); } private Task LoadActionsAsync(CancellationToken cancellationToken) + { + this.LoadActions(); + return Task.CompletedTask; + } + + private void LoadActions() { try { @@ -147,8 +175,6 @@ private Task LoadActionsAsync(CancellationToken cancellationToken) { Exceptions.ProcessModuleLoadException(this, exc); } - - return Task.CompletedTask; } } } 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/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 3bf06d3203d..60a456809e5 100644 --- a/DNN Platform/Website/admin/Menus/ModuleActions/ModuleActions.ascx.cs +++ b/DNN Platform/Website/admin/Menus/ModuleActions/ModuleActions.ascx.cs @@ -156,7 +156,14 @@ protected override void OnLoad(EventArgs e) this.clientResourceController.RegisterScript("~/admin/menus/ModuleActions/dnnQuickSettings.js"); } - this.Page.RegisterAsyncTask(new PageAsyncTask(this.ProcessActionsAsync)); + if (this.ModuleControl is IAsyncModuleControl) + { + this.Page.RegisterAsyncTask(new PageAsyncTask(this.ProcessActionsAsync)); + } + else + { + this.ProcessActions(); + } } catch (Exception exc) { @@ -176,12 +183,18 @@ protected override void Render(HtmlTextWriter writer) } private Task ProcessActionsAsync(CancellationToken cancellationToken) + { + this.ProcessActions(); + return Task.CompletedTask; + } + + private void ProcessActions() { try { if (!this.ActionRoot.Visible) { - return Task.CompletedTask; + return; } // Add Menu Items @@ -245,19 +258,11 @@ private Task ProcessActionsAsync(CancellationToken cancellationToken) { Exceptions.ProcessModuleLoadException(this, exc); } - - return Task.CompletedTask; } private void ActionButton_Click(object sender, EventArgs e) - { - this.Page.RegisterAsyncTask(new PageAsyncTask(ct => this.ActionButton_ClickAsync(sender, e, ct))); - } - - private Task ActionButton_ClickAsync(object sender, EventArgs e, CancellationToken cancellationToken) { this.ProcessAction(this.Request.Params["__EVENTARGUMENT"]); - return Task.CompletedTask; } } } From b5e72447fb350bef7bca6874c91e1f07fc3ad89e Mon Sep 17 00:00:00 2001 From: Robert Dima Date: Tue, 27 Feb 2024 23:03:39 +0200 Subject: [PATCH 3/5] MVC: Improve error messages when view template is not found by writing out the SearchedLocations. --- .../Framework/ActionResults/DnnPartialViewResult.cs | 2 +- .../Framework/ActionResults/DnnViewResult.cs | 2 +- .../DotNetNuke.Web.Mvc/Framework/ViewEngineCollectionExt.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) 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/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); From 1a091c36edf7c8520ff11eded64ee03a51b82d87 Mon Sep 17 00:00:00 2001 From: Robert Dima Date: Fri, 3 Apr 2026 23:13:00 +0300 Subject: [PATCH 4/5] NullReferenceException fixes. --- .../Framework/Modules/ModuleApplication.cs | 3 ++- .../Library/UI/Modules/ModuleInstanceContext.cs | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ModuleApplication.cs b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ModuleApplication.cs index 05d43886d8f..93eb57eb52a 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ModuleApplication.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ModuleApplication.cs @@ -167,7 +167,6 @@ public virtual async Task ExecuteRequestAsync(ModuleRequest { // Check if the controller supports IDnnController var moduleController = controller as IDnnController; - moduleController.IsAsync = true; // 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 @@ -176,6 +175,8 @@ public virtual async Task ExecuteRequestAsync(ModuleRequest throw new InvalidOperationException("Could Not Construct Controller"); } + moduleController.IsAsync = true; + moduleController.ValidateRequest = false; moduleController.DnnPage = context.DnnPage; diff --git a/DNN Platform/Library/UI/Modules/ModuleInstanceContext.cs b/DNN Platform/Library/UI/Modules/ModuleInstanceContext.cs index aa1c2f2f89e..83c905b94f1 100644 --- a/DNN Platform/Library/UI/Modules/ModuleInstanceContext.cs +++ b/DNN Platform/Library/UI/Modules/ModuleInstanceContext.cs @@ -510,6 +510,16 @@ 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. + // Leave this.actions as null so the Actions getter retries once results are available. + if (this.moduleControl is IAsyncModuleControl && actionable.ModuleActions == null) + { + this.actions = null; + + // TODO: Should we throw instead to be able to identify code that tries to call it too early? + return; + } + this.moduleSpecificActions = new ModuleAction(this.GetNextActionID(), Localization.GetString("ModuleSpecificActions.Action", Localization.GlobalResourceFile), string.Empty, string.Empty, string.Empty); ModuleActionCollection moduleActions = actionable.ModuleActions; From eb6c71fb914365a5d5b46c06950c06bad55da664 Mon Sep 17 00:00:00 2001 From: Robert Dima Date: Thu, 9 Apr 2026 12:58:54 +0300 Subject: [PATCH 5/5] Throw if ModuleActions is accessed to early. + some code comments. --- DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcHostControl.cs | 1 + DNN Platform/Library/UI/Containers/ActionBase.cs | 1 + DNN Platform/Library/UI/Modules/ModuleInstanceContext.cs | 7 ++----- .../admin/Menus/ModuleActions/ModuleActions.ascx.cs | 1 + 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcHostControl.cs b/DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcHostControl.cs index 706da19d7e2..86a69665582 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcHostControl.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcHostControl.cs @@ -36,6 +36,7 @@ protected override void OnInitInternal(EventArgs e) 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)); } diff --git a/DNN Platform/Library/UI/Containers/ActionBase.cs b/DNN Platform/Library/UI/Containers/ActionBase.cs index f28bd1c1ed3..bfa20a57060 100644 --- a/DNN Platform/Library/UI/Containers/ActionBase.cs +++ b/DNN Platform/Library/UI/Containers/ActionBase.cs @@ -113,6 +113,7 @@ 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 diff --git a/DNN Platform/Library/UI/Modules/ModuleInstanceContext.cs b/DNN Platform/Library/UI/Modules/ModuleInstanceContext.cs index 83c905b94f1..181d8abe338 100644 --- a/DNN Platform/Library/UI/Modules/ModuleInstanceContext.cs +++ b/DNN Platform/Library/UI/Modules/ModuleInstanceContext.cs @@ -511,13 +511,10 @@ private void LoadActions(HttpRequest request) if (actionable != null) { // Async module controls populate ModuleActions only after their async task executes. - // Leave this.actions as null so the Actions getter retries once results are available. if (this.moduleControl is IAsyncModuleControl && actionable.ModuleActions == null) { - this.actions = null; - - // TODO: Should we throw instead to be able to identify code that tries to call it too early? - return; + 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); diff --git a/DNN Platform/Website/admin/Menus/ModuleActions/ModuleActions.ascx.cs b/DNN Platform/Website/admin/Menus/ModuleActions/ModuleActions.ascx.cs index 60a456809e5..684682123ca 100644 --- a/DNN Platform/Website/admin/Menus/ModuleActions/ModuleActions.ascx.cs +++ b/DNN Platform/Website/admin/Menus/ModuleActions/ModuleActions.ascx.cs @@ -158,6 +158,7 @@ protected override void OnLoad(EventArgs e) 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(this.ProcessActionsAsync)); } else