diff --git a/src/Framework/Framework/Compilation/ControlPrecompilationVisitor.cs b/src/Framework/Framework/Compilation/ControlPrecompilationVisitor.cs index 136cc64566..7f4009f62e 100644 --- a/src/Framework/Framework/Compilation/ControlPrecompilationVisitor.cs +++ b/src/Framework/Framework/Compilation/ControlPrecompilationVisitor.cs @@ -164,7 +164,10 @@ void checkProperty(IControlAttributeDescriptor property, Type targetType) var content = runtimeControl.ExecuteGetContents(null!); var config = services.GetService(); - return content.Select(c => ResolvedControlHelper.FromRuntimeControl(c, control.DataContextTypeStack, config)).ToArray(); + return content.Select(c => { + runtimeControl.CopyPostBackHandlersRecursive(c); + return ResolvedControlHelper.FromRuntimeControl(c, control.DataContextTypeStack, config); + }).ToArray(); } /// Returns true if we can send binding into the property without evaluating it diff --git a/src/Framework/Framework/Controls/CompositeControl.cs b/src/Framework/Framework/Controls/CompositeControl.cs index 783a2d7ff1..fd8e7664df 100644 --- a/src/Framework/Framework/Controls/CompositeControl.cs +++ b/src/Framework/Framework/Controls/CompositeControl.cs @@ -13,6 +13,9 @@ using DotVVM.Framework.Utils; using DotVVM.Framework.Compilation; using FastExpressionCompiler; +using DotVVM.Framework.Compilation.Directives; +using System.Windows.Input; +using System.Diagnostics.Tracing; namespace DotVVM.Framework.Controls { @@ -128,6 +131,106 @@ internal IEnumerable ExecuteGetContents(IDotvvmRequestContext con return Array.Empty(); } + /// Copies postback handlers onto controls with copied command bindings from this control. + /// Then adds those control into the Children collection (we have to this in one iteration to avoid enumerating the IEnumerable twice) + private void CopyPostbackHandlersAndAdd(IEnumerable childControls) + { + if (this.GetValue(PostBack.HandlersProperty) is not PostBackHandlerCollection handlers || handlers.Count == 0) + { + foreach (var child in childControls) + this.Children.Add(child); + return; + } + + var commands = new List<(string, ICommandBinding)>(); + foreach (var (property, value) in this.Properties) + { + if (value is ICommandBinding command) + commands.Add((property.Name, command)); + } + + foreach (var child in childControls) + { + CopyPostBackHandlersRecursive(handlers, commands, child); + this.Children.Add(child); + } + + } + + /// + /// Copies postback handlers declared on the CompositeControl to the target child control, its children and properties recursively (including CloneTemplate, but not other ITemplate implementations). + /// Only postback handlers with a matching command binding are copied - Note that you have to set the command bindings before calling this method. + /// DotVVM copies the postback handlers automatically onto all controls returned from the GetContents method, but you might need to call this method to copy the handlers onto controls inside DelegateTemplate + /// + protected internal T CopyPostBackHandlersRecursive(T target) + where T: DotvvmBindableObject + { + if (this.GetValue(PostBack.HandlersProperty) is not PostBackHandlerCollection handlers || handlers.Count == 0) + return target; + + var commands = new List<(string, ICommandBinding)>(); + foreach (var (property, value) in this.Properties) + { + if (value is ICommandBinding command) + commands.Add((property.Name, command)); + } + CopyPostBackHandlersRecursive(handlers, commands, target); + + return target; + } + + private static void CopyPostBackHandlersRecursive(PostBackHandlerCollection handlers, List<(string, ICommandBinding)> commands, DotvvmBindableObject target) + { + PostBackHandlerCollection? childHandlers = null; + foreach (var (property, value) in target.Properties) + { + if (value is ICommandBinding command) + { + foreach (var (oldName, matchedCommand) in commands) + { + if (object.ReferenceEquals(command, matchedCommand)) + { + CopyMatchingPostBackHandlers(handlers, oldName, property.Name, ref childHandlers); + break; + } + } + } + else if (value is CloneTemplate template) + { + foreach (var c in template.Controls) + { + CopyPostBackHandlersRecursive(handlers, commands, c); + } + } + else if (value is DotvvmBindableObject child) + { + CopyPostBackHandlersRecursive(handlers, commands, child); + } + } + if (childHandlers is { }) + target.SetValue(PostBack.HandlersProperty, childHandlers); + + if (target is DotvvmControl targetControl) + foreach (var c in targetControl.Children) + { + CopyPostBackHandlersRecursive(handlers, commands, c); + } + } + static void CopyMatchingPostBackHandlers(PostBackHandlerCollection handlers, string oldName, string newName, ref PostBackHandlerCollection? newHandlers) + { + foreach (var h in handlers) + { + var name = h.EventName; + if (name == oldName || name is null) + { + var newHandler = (PostBackHandler)h.CloneControl(); + newHandler.EventName = newName; + newHandlers ??= new(); + newHandlers.Add(newHandler); + } + } + } + protected internal override void OnLoad(IDotvvmRequestContext context) { if (!this.HasOnlyWhiteSpaceContent()) @@ -140,8 +243,7 @@ protected internal override void OnLoad(IDotvvmRequestContext context) if (this.Children.Count > 0) throw new DotvvmControlException(this, $"{GetType().Name}.GetContents may not modify the Children collection, it should return the new children and it will be handled automatically."); - foreach (var child in content) - this.Children.Add(child); + CopyPostbackHandlersAndAdd(content); base.OnLoad(context); } diff --git a/src/Tests/ControlTests/CompositeControlTests.cs b/src/Tests/ControlTests/CompositeControlTests.cs index 57a272810c..96b281bbe1 100644 --- a/src/Tests/ControlTests/CompositeControlTests.cs +++ b/src/Tests/ControlTests/CompositeControlTests.cs @@ -131,6 +131,8 @@ public async Task BindingMapping() check.CheckString(r.FormattedHtml, fileExtension: "html"); } + + [TestMethod] public async Task CommandDataContextChange() { // RepeatedButton2 creates button in repeater, but also @@ -151,6 +153,34 @@ public async Task CommandDataContextChange() Assert.AreEqual(15, (int)r.ViewModel.@int); } + [TestMethod] + public async Task AutoclonedPostbackHandlers() + { + var r = await cth.RunPage(typeof(BasicTestViewModel), """ + + + + + + + + + + + + """ + ); + + check.CheckString(r.FormattedHtml, fileExtension: "html"); + + XAssert.Contains("Test not precompiled", r.OutputString); + XAssert.Contains("Test precompiled", r.OutputString); + } + [TestMethod] public async Task MarkupControlCreatedFromCodeControl() { @@ -394,15 +424,20 @@ public static DotvvmControl GetContents( } } - [ControlMarkupOptions(Precompile = ControlPrecompilationMode.Always)] + [ControlMarkupOptions(Precompile = ControlPrecompilationMode.IfPossible)] public class RepeatedButton2: CompositeControl { - public static DotvvmControl GetContents( + public DotvvmControl GetContents( IValueBinding> dataSource, - ICommandBinding itemClick = null + ICommandBinding itemClick = null, + bool precompile = true ) { + if (!precompile && this.GetValue(Internal.RequestContextProperty) is null) + { + throw new SkipPrecompilationException(); + } // Places itemClick in two different data contexts var repeater = new Repeater() { RenderAsNamedTemplate = false, diff --git a/src/Tests/ControlTests/testoutputs/CompositeControlTests.AutoclonedPostbackHandlers.html b/src/Tests/ControlTests/testoutputs/CompositeControlTests.AutoclonedPostbackHandlers.html new file mode 100644 index 0000000000..563223e03c --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/CompositeControlTests.AutoclonedPostbackHandlers.html @@ -0,0 +1,19 @@ + + + + + +
+
+ +
+ +
+
+
+ +
+ +
+ + diff --git a/src/Tests/ControlTests/testoutputs/CompositeControlTests.CommandDataContextChange.html b/src/Tests/ControlTests/testoutputs/CompositeControlTests.CommandDataContextChange.html index 065cfbcf04..a3128b97aa 100644 --- a/src/Tests/ControlTests/testoutputs/CompositeControlTests.CommandDataContextChange.html +++ b/src/Tests/ControlTests/testoutputs/CompositeControlTests.CommandDataContextChange.html @@ -4,7 +4,7 @@
-
+
@@ -12,10 +12,14 @@
-
- +
+
- +