diff --git a/src/Umbraco.Web.Common/Constants/Constants.cs b/src/Umbraco.Web.Common/Constants/Constants.cs new file mode 100644 index 0000000000..1acbe761c1 --- /dev/null +++ b/src/Umbraco.Web.Common/Constants/Constants.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Web.Common.Constants +{ + /// + /// constants + /// + internal static class Constants + { + internal const string ViewLocation = "~/Views"; + + internal const string DataTokenCurrentViewContext = "umbraco-current-view-context"; + } +} diff --git a/src/Umbraco.Web.Common/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs new file mode 100644 index 0000000000..43058616de --- /dev/null +++ b/src/Umbraco.Web.Common/Controllers/RenderController.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Umbraco.Web.Common.Controllers +{ + public abstract class RenderController : Controller + { + + } +} diff --git a/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs new file mode 100644 index 0000000000..bddb8b3653 --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs @@ -0,0 +1,87 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Umbraco.Web.Common.Controllers; + +namespace Umbraco.Web.Common.Filters +{ + /// + /// This is a special filter which is required for the RTE to be able to render Partial View Macros that + /// contain forms when the RTE value is resolved outside of an MVC view being rendered + /// + /// + /// The entire way that we support partial view macros that contain forms isn't really great, these forms + /// need to be executed as ChildActions so that the ModelState,ViewData,TempData get merged into that action + /// so the form can show errors, viewdata, etc... + /// Under normal circumstances, macros will be rendered after a ViewContext is created but in some cases + /// developers will resolve the RTE value in the controller, in this case the Form won't be rendered correctly + /// with merged ModelState from the controller because the special DataToken hasn't been set yet (which is + /// normally done in the UmbracoViewPageOfModel when a real ViewContext is available. + /// So we need to detect if the currently rendering controller is IRenderController and if so we'll ensure that + /// this DataToken exists before the action executes in case the developer resolves an RTE value that contains + /// a partial view macro form. + /// + internal class EnsurePartialViewMacroViewContextFilterAttribute : ActionFilterAttribute + { + + /// + /// Ensures the custom ViewContext datatoken is set before the RenderController action is invoked, + /// this ensures that any calls to GetPropertyValue with regards to RTE or Grid editors can still + /// render any PartialViewMacro with a form and maintain ModelState + /// + /// + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!(context.Controller is Controller controller)) return; + + //ignore anything that is not IRenderController + if (!(controller is RenderController)) return; + + SetViewContext(context, controller); + } + + /// + /// Ensures that the custom ViewContext datatoken is set after the RenderController action is invoked, + /// this ensures that any custom ModelState that may have been added in the RenderController itself is + /// passed onwards in case it is required when rendering a PartialViewMacro with a form + /// + /// The filter context. + public override void OnResultExecuting(ResultExecutingContext context) + { + if (!(context.Controller is Controller controller)) return; + + //ignore anything that is not IRenderController + if (!(controller is RenderController)) return; + + SetViewContext(context, controller); + } + + private void SetViewContext(ActionContext context, Controller controller) + { + var viewCtx = new ViewContext( + context, + new DummyView(), + controller.ViewData, + controller.TempData, + new StringWriter(), + new HtmlHelperOptions()); + + //set the special data token + context.RouteData.DataTokens[Constants.Constants.DataTokenCurrentViewContext] = viewCtx; + } + + private class DummyView : IView + { + public Task RenderAsync(ViewContext context) + { + return Task.CompletedTask; + } + + public string Path { get; } + } + } +}