Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support PostBack.Handlers in CompositeControls #1703

Merged
merged 1 commit into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 static DotvvmControl GetContents(
}
}

[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>
Loading