Skip to content

Commit

Permalink
Merge pull request #1703 from riganti/compositecontrol-postback-handlers
Browse files Browse the repository at this point in the history
Support PostBack.Handlers in CompositeControls
  • Loading branch information
tomasherceg authored Sep 29, 2023
2 parents b3dcbc7 + 2e293e2 commit 7cc1ad7
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 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>

0 comments on commit 7cc1ad7

Please sign in to comment.