Skip to content

Commit

Permalink
Support PostBack.Handlers in CompositeControls
Browse files Browse the repository at this point in the history
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
  • Loading branch information
exyi committed Sep 27, 2023
1 parent b3dcbc7 commit 2e293e2
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@ void checkProperty(IControlAttributeDescriptor property, Type targetType)
var content = runtimeControl.ExecuteGetContents(null!);

var config = services.GetService<DotvvmConfiguration>();
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
Expand Down
106 changes: 104 additions & 2 deletions src/Framework/Framework/Controls/CompositeControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -128,6 +131,106 @@ internal IEnumerable<DotvvmControl> ExecuteGetContents(IDotvvmRequestContext con
return Array.Empty<DotvvmControl>();
}

/// <summary> 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) </summary>
private void CopyPostbackHandlersAndAdd(IEnumerable<DotvvmControl> 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);
}

}

/// <summary>
/// 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
/// </summary>
protected internal T CopyPostBackHandlersRecursive<T>(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())
Expand All @@ -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);
}
Expand Down
41 changes: 38 additions & 3 deletions src/Tests/ControlTests/CompositeControlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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), """
<!-- command -->
<cc:RepeatedButton2 DataSource={value: List}
ItemClick={command: Integer = 15}
Precompile=false>
<PostBack.Handlers>
<dot:ConfirmPostBackHandler Message='Test not precompiled' />
</PostBack.Handlers>
</cc:RepeatedButton2>
<cc:RepeatedButton2 DataSource={value: List}
ItemClick={command: Integer = 15}
Precompile=true>
<PostBack.Handlers>
<dot:ConfirmPostBackHandler Message='Test precompiled' />
</PostBack.Handlers>
</cc:RepeatedButton2>
"""
);

check.CheckString(r.FormattedHtml, fileExtension: "html");

XAssert.Contains("Test not precompiled", r.OutputString);
XAssert.Contains("Test precompiled", r.OutputString);
}

[TestMethod]
public async Task MarkupControlCreatedFromCodeControl()
{
Expand Down Expand Up @@ -394,15 +424,20 @@ public class RepeatedButton: CompositeControl
}
}

[ControlMarkupOptions(Precompile = ControlPrecompilationMode.Always)]
[ControlMarkupOptions(Precompile = ControlPrecompilationMode.IfPossible)]
public class RepeatedButton2: CompositeControl
{
public static DotvvmControl GetContents(
public DotvvmControl GetContents(
IValueBinding<IEnumerable<string>> 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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<html>
<head></head>
<body>

<!-- command -->
<div>
<div data-bind="foreach: { data: List }">
<input onclick="dotvvm.postBack(this,[&quot;List/[$index]&quot;],&quot;IYZcgJXAUDcvRzk7&quot;,&quot;&quot;,ko.contextFor(this).$parentContext,[[&quot;confirm&quot;,{message:&quot;Test not precompiled&quot;}]],[],undefined).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="Item">
</div>
<input onclick="dotvvm.postBack(this,[],&quot;IYZcgJXAUDcvRzk7&quot;,&quot;&quot;,null,[[&quot;confirm&quot;,{message:&quot;Test not precompiled&quot;}]],[],undefined).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="Last Item">
</div>
<div>
<div data-bind="foreach: { data: List }">
<input onclick="dotvvm.postBack(this,[&quot;List/[$index]&quot;],&quot;/cYhsuIE/4DfszAC&quot;,&quot;&quot;,ko.contextFor(this).$parentContext,[[&quot;confirm&quot;,{message:&quot;Test precompiled&quot;}]],[],undefined).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="Item">
</div>
<input onclick="dotvvm.postBack(this,[],&quot;/cYhsuIE/4DfszAC&quot;,&quot;&quot;,null,[[&quot;confirm&quot;,{message:&quot;Test precompiled&quot;}]],[],undefined).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="Last Item">
</div>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@

<!-- command -->
<div>
<div data-bind="foreach: { &quot;data&quot;: List }">
<div data-bind="foreach: { data: List }">
<input onclick="dotvvm.postBack(this,[&quot;List/[$index]&quot;],&quot;IYZcgJXAUDcvRzk7&quot;,&quot;&quot;,ko.contextFor(this).$parentContext,[],[],undefined).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="Item">
</div>
<input onclick="dotvvm.postBack(this,[],&quot;IYZcgJXAUDcvRzk7&quot;,&quot;&quot;,null,[],[],undefined).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="Last Item">
</div>

<!-- staticCommand -->
<div>
<div data-bind="foreach: { &quot;data&quot;: List }">
<input onclick="dotvvm.applyPostbackHandlers((options) => options.knockoutContext.$parent.int(12).int(),this).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="Item">
<div data-bind="foreach: { data: List }">
<input onclick="dotvvm.applyPostbackHandlers((options) => {
options.knockoutContext.$parent.int(12);
},this).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="Item">
</div>
<input onclick="dotvvm.applyPostbackHandlers((options) => options.viewModel.int(12).int(),this).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="Last Item">
<input onclick="dotvvm.applyPostbackHandlers((options) => {
options.viewModel.int(12);
},this).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="Last Item">
</div>
</body>
</html>

0 comments on commit 2e293e2

Please sign in to comment.