From 2e293e21383b9cd7a2266a878d00f6f76ce5b053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 27 Sep 2023 15:46:26 +0200 Subject: [PATCH] Support PostBack.Handlers in CompositeControls PostBack.Handlers are copied onto the created controls, if they contain a matching command binding. * We map postback handlers onto the command bindings set on the CompositeControl by comparing the EventName and property name * For each control returned from GetContents, we enumerate its command bindings and compare them to the command bindings found on the CompositeControl. * If a matching command binding is found, we clone its postback handlers onto the child control. EventName is adjusted to match the new property name The control enumeration is done recursively before adding the control to Children, thus walking only through the tree created in this CompositeControl - nested CompositeControls, initialized Repeaters, ... are not included. In order to support templatws created in the CompositeControl, we recurse into CloneTemplates. For DelegateTemplate or any other more advanced needs, a protected CopyPostBackHandlersRecursive method is provided. resolves #1699 --- .../ControlPrecompilationVisitor.cs | 5 +- .../Framework/Controls/CompositeControl.cs | 106 +++++++++++++++++- .../ControlTests/CompositeControlTests.cs | 41 ++++++- ...ntrolTests.AutoclonedPostbackHandlers.html | 19 ++++ ...ControlTests.CommandDataContextChange.html | 12 +- 5 files changed, 173 insertions(+), 10 deletions(-) create mode 100644 src/Tests/ControlTests/testoutputs/CompositeControlTests.AutoclonedPostbackHandlers.html 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 @@
-
- +
+
- +