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

Binding translation issue in PostBackHandler properties #1878

Merged
merged 6 commits into from
Oct 31, 2024
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
84 changes: 64 additions & 20 deletions src/Framework/Framework/Compilation/Javascript/ParametrizedCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,25 @@ public ParametrizedCode AssignParameters(Func<CodeSymbolicParameter, CodeParamet
if (stringParts == null) return this;
Debug.Assert(parameters is object);

var assignment = FindAssignment(parameterAssignment, optional: true, allIsDefault: out bool allIsDefault);
var (assignment, newDefaults) = FindAssignment(parameterAssignment);

if (allIsDefault) return this;
if (assignment is null)
{
if (newDefaults is null)
return this;
else
{
// just change the content of parameters, no need to rebuild strings
var newParams = parameters.AsSpan().ToArray();
for (int i = 0; i < newParams.Length; i++)
{
ref var p = ref newParams[i];
if (newDefaults[i] is { } newDefault)
p = new CodeParameterInfo(p.Parameter, p.OperatorPrecedence, p.IsSafeMemberAccess, newDefault);
}
return new ParametrizedCode(stringParts, newParams, OperatorPrecedence);
}
}

// PERF: reduce allocations here, used at runtime
var builder = new Builder();
Expand All @@ -141,20 +157,26 @@ public ParametrizedCode AssignParameters(Func<CodeSymbolicParameter, CodeParamet
for (int i = 0; i < assignment.Length; i++)
{
var a = assignment[i];
if (a.Code == null)
var param = parameters![i];
if (a.Code == null) // not assigned by `parameterAssignment`
{
builder.Add(parameters![i]);
// assign recursively in the default assignment
if (newDefaults is {} && newDefaults[i] is { } newDefault)
builder.Add(new CodeParameterInfo(param.Parameter, param.OperatorPrecedence, param.IsSafeMemberAccess, newDefault));
else
builder.Add(param);

builder.Add(stringParts[1 + i]);
}
else
{
var isGlobalContext = a.IsGlobalContext && parameters![i].IsSafeMemberAccess;
var isGlobalContext = a.IsGlobalContext && param.IsSafeMemberAccess;

if (isGlobalContext)
builder.Add(stringParts[1 + i].AsSpan(1, stringParts[i].Length - 1).DotvvmInternString()); // skip `.`
builder.Add(stringParts[1 + i].AsSpan(1, stringParts[1 + i].Length - 1).DotvvmInternString()); // skip `.`
else
{
builder.Add(a.Code, parameters![i].OperatorPrecedence);
builder.Add(a.Code, param.OperatorPrecedence);
builder.Add(stringParts[1 + i]);
}
}
Expand Down Expand Up @@ -183,35 +205,57 @@ public void CopyTo(Builder builder)

private (CodeParameterAssignment parameter, string code)[] FindStringAssignment(Func<CodeSymbolicParameter, CodeParameterAssignment> parameterAssigner, out bool allIsDefault)
{
var pp = FindAssignment(parameterAssigner, optional: false, allIsDefault: out allIsDefault);
allIsDefault = true;
var codes = new(CodeParameterAssignment parameter, string code)[parameters!.Length];
for (int i = 0; i < parameters.Length; i++)
{
codes[i] = (pp[i], pp[i].Code!.ToString(parameterAssigner, out bool allIsDefault_local));
var assignment = parameterAssigner(parameters[i].Parameter);
if (assignment.Code == null)
{
assignment = parameters[i].DefaultAssignment;
if (assignment.Code == null)
throw new InvalidOperationException($"Assignment of parameter '{parameters[i].Parameter}' was not found.");
}
else
allIsDefault = false;

codes[i] = (assignment, assignment.Code!.ToString(parameterAssigner, out bool allIsDefault_local));
allIsDefault &= allIsDefault_local;
}
return codes;
}

private CodeParameterAssignment[] FindAssignment(Func<CodeSymbolicParameter, CodeParameterAssignment> parameterAssigner, bool optional, out bool allIsDefault)
private (CodeParameterAssignment[]? assigned, ParametrizedCode?[]? newDefaults) FindAssignment(Func<CodeSymbolicParameter, CodeParameterAssignment> parameterAssigner)
{
allIsDefault = true;
var pp = new CodeParameterAssignment[parameters!.Length];
if (parameters is null)
return (null, null);

// these are different variables, as we have to preserve the tree-like structure of the ParametrizedCodes,
// when we assign parameters in the default values.
// newDefaults -> we will change the code in the parameters[i].DefaultAssignment
// assigned -> this parameter will be removed and its assignment inlined into stringParts[i] and parameters[i]
CodeParameterAssignment[]? assigned = null; // when null, all are default
ParametrizedCode[]? newDefaults = null; // when null, no defaults were changed
for (int i = 0; i < parameters.Length; i++)
{
if ((pp[i] = parameterAssigner(parameters[i].Parameter)).Code == null)
var p = parameterAssigner(parameters[i].Parameter);
if (p.Code is not null)
{
assigned ??= new CodeParameterAssignment[parameters.Length];
assigned[i] = p;
}
else if (parameters[i].DefaultAssignment is { Code: { HasParameters: true } } defaultAssignment)
{
if (!optional)
// check if the default assignment contains any of the assigned parameters, and adjust the default if necessary
var newDefault = defaultAssignment.Code.AssignParameters(parameterAssigner);
if (newDefault != defaultAssignment.Code)
{
pp[i] = parameters[i].DefaultAssignment;
if (pp[i].Code == null)
throw new InvalidOperationException($"Assignment of parameter '{parameters[i].Parameter}' was not found.");
newDefaults ??= new ParametrizedCode[parameters.Length];
newDefaults[i] = newDefault;
}
}
else
allIsDefault = false;
}
return pp;
return (assigned, newDefaults);
}

public IEnumerable<CodeSymbolicParameter> EnumerateAllParameters()
Expand Down
4 changes: 2 additions & 2 deletions src/Framework/Framework/Controls/KnockoutHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,8 @@ private static JsExpression TransformOptionValueToExpression(DotvvmBindableObjec
{
case IValueBinding binding: {
var adjustedCode = binding.GetParametrizedKnockoutExpression(handler, unwrapped: true).AssignParameters(o =>
o == JavascriptTranslator.KnockoutContextParameter ? new ParametrizedCode("c") :
o == JavascriptTranslator.KnockoutViewModelParameter ? new ParametrizedCode("d") :
o == JavascriptTranslator.KnockoutContextParameter ? CodeParameterAssignment.FromIdentifier("c") :
tomasherceg marked this conversation as resolved.
Show resolved Hide resolved
o == JavascriptTranslator.KnockoutViewModelParameter ? CodeParameterAssignment.FromIdentifier("d") :
default(CodeParameterAssignment)
);
return new JsSymbolicParameter(new CodeSymbolicParameter("tmp symbol", defaultAssignment: adjustedCode));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DotVVM.Framework.ViewModel;
using DotVVM.Framework.Hosting;

namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.PostBack
{
public class PostBackHandlerBindingViewModel : DotvvmViewModelBase
{
public bool Enabled { get; set; } = false;

public int Counter { get; set; } = 0;

public string[] Items { get; set; } = new string[] { "Item 1", "Item 2", "Item 3" };
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.PostBack.PostBackHandlerBindingViewModel, DotVVM.Samples.Common

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>

<p>
<dot:CheckBox Text="Suppress postbacks" Checked="{value: Enabled}" />
</p>
<p>Counter: <span class="result">{{value: Counter}}</span></p>

<p>Click on grid rows to increment the counter.</p>

<dot:GridView DataSource="{value: Items}">
<Columns>
<dot:GridViewTextColumn HeaderText="Column 1" ValueBinding="{value: _this}" />
</Columns>
<RowDecorators>
<dot:Decorator Events.Click="{command: _parent.Counter = _parent.Counter + 1}" style="cursor: pointer">
<PostBack.Handlers>
<dot:SuppressPostBackHandler Suppress="{value: _parent.Enabled}" />
</PostBack.Handlers>
</dot:Decorator>
</RowDecorators>
</dot:GridView>

</body>
</html>


Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions src/Samples/Tests/Tests/Feature/PostBackTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,24 @@ public void Feature_PostBack_PostBackHandlers_Localization()
});
}

[Fact]
public void Feature_PostBack_PostBackHandlerBinding()
{
RunInAllBrowsers(browser => {
browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_PostBack_PostBackHandlerBinding);

var counter = browser.Single(".result");
AssertUI.TextEquals(counter, "0");

browser.ElementAt("td", 0).Click();
AssertUI.TextEquals(counter, "1");

browser.Single("input[type=checkbox]").Click();
browser.ElementAt("td", 0).Click();
AssertUI.TextEquals(counter, "1");
});
}

private void ValidatePostbackHandlersComplexSection(string sectionSelector, IBrowserWrapper browser)
{
IElementWrapper section = null;
Expand Down
2 changes: 1 addition & 1 deletion src/Tests/Binding/BindingCompilationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1493,7 +1493,7 @@ public override string ToString()
return SomeString + ": " + MyProperty;
}
}
class TestViewModel3 : DotvvmViewModelBase
public class TestViewModel3 : DotvvmViewModelBase
{
public string SomeString { get; set; }
}
Expand Down
34 changes: 34 additions & 0 deletions src/Tests/Binding/JavascriptCompilationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,40 @@ public void JavascriptCompilation_CustomPrimitiveParse()
Assert.AreEqual("VehicleNumber()==\"123\"", result);
}

[TestMethod]
public void JavascriptCompilation_ParametrizedCode_ParentContexts()
{
// root: TestViewModel
// parent: TestViewModel2 + _index + _collection
// this: TestViewModel3 + _index
var context0 = DataContextStack.Create(typeof(TestViewModel));
var context1 = DataContextStack.Create(typeof(TestViewModel2), parent: context0, extensionParameters: [ new CurrentCollectionIndexExtensionParameter(), new BindingCollectionInfoExtensionParameter("_collection") ]);
var context2 = DataContextStack.Create(typeof(TestViewModel3), parent: context1, extensionParameters: [ new CurrentCollectionIndexExtensionParameter() ]);

var result = bindingHelper.ValueBindingToParametrizedCode("_root.IntProp + _parent._index + _parent._collection.Index + _parent.MyProperty + _index + SomeString.Length", context2);

Assert.AreEqual("$parents[1].IntProp() + $parentContext.$index() + $parentContext.$index() + $parent.MyProperty() + $index() + SomeString().length", result.ToDefaultString());

// assign `context` and `vm` variables
var formatted2 = result.ToString(o =>
o == JavascriptTranslator.KnockoutContextParameter ? new ParametrizedCode("context", OperatorPrecedence.Max) :
o == JavascriptTranslator.KnockoutViewModelParameter ? new ParametrizedCode("vm", OperatorPrecedence.Max) :
default(CodeParameterAssignment)
);
Console.WriteLine(formatted2);

var symbolicParameters = result.Parameters.ToArray();
Assert.AreEqual(6, symbolicParameters.Length);
Assert.AreEqual(2, XAssert.IsAssignableFrom<JavascriptTranslator.ViewModelSymbolicParameter>(symbolicParameters[0].Parameter).ParentIndex);
Assert.AreEqual(JavascriptTranslator.KnockoutContextParameter, symbolicParameters[1].Parameter); // JavascriptTranslator.ParentKnockoutContextParameter would also work
Assert.AreEqual(JavascriptTranslator.KnockoutContextParameter, symbolicParameters[2].Parameter); // JavascriptTranslator.ParentKnockoutContextParameter would also work
Assert.AreEqual(JavascriptTranslator.ParentKnockoutViewModelParameter, symbolicParameters[3].Parameter);
Assert.AreEqual(JavascriptTranslator.KnockoutContextParameter, symbolicParameters[4].Parameter);
Assert.AreEqual(JavascriptTranslator.KnockoutViewModelParameter, symbolicParameters[5].Parameter);

Assert.AreEqual("context.$parents[1].IntProp() + context.$parentContext.$index() + context.$parentContext.$index() + context.$parent.MyProperty() + context.$index() + vm.SomeString().length", formatted2);
}

public class TestMarkupControl: DotvvmMarkupControl
{
public string SomeProperty
Expand Down
62 changes: 62 additions & 0 deletions src/Tests/ControlTests/PostbackHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Threading.Tasks;
using CheckTestOutput;
using DotVVM.Framework.Compilation;
using DotVVM.Framework.Controls;
using DotVVM.Framework.Tests.Binding;
using DotVVM.Framework.ViewModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using DotVVM.Framework.Testing;
using System.Security.Claims;
using System.Collections;
using DotVVM.Framework.Binding;
using DotVVM.Framework.Compilation.ControlTree;
using DotVVM.Framework.Hosting;

namespace DotVVM.Framework.Tests.ControlTests
{
[TestClass]
public class PostbackHandlerTests
{
static readonly ControlTestHelper cth = new ControlTestHelper(config: config => {
});
readonly OutputChecker check = new OutputChecker("testoutputs");


[TestMethod]
public async Task ButtonHandlers()
{
var r = await cth.RunPage(typeof(BasicTestViewModel), """
<!-- suppress postback -->
<dot:Button DataContext={value: Nested} Click={staticCommand: 0} Text="Test supress">
<Postback.Handlers>
<dot:SuppressPostBackHandler Suppress={value: _parent.Integer > 100} />
<dot:SuppressPostBackHandler Suppress={value: SomeString.Length < 5} />
</Postback.Handlers>
</dot:Button>

<!-- confirm -->
<dot:Button DataContext={value: Nested} Click={staticCommand: 0} Text="Test confirm">
<Postback.Handlers>
<dot:ConfirmPostBackHandler Message={value: $"String={_root.String} SomeString={SomeString}"} />
</Postback.Handlers>
</dot:Button>
"""
);

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

public class BasicTestViewModel: DotvvmViewModelBase
{
public int Integer { get; set; } = 123;
public bool Boolean { get; set; } = false;
public string String { get; set; } = "some-string";

public TestViewModel3 Nested { get; set; } = new TestViewModel3 { SomeString = "a" };
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<html>
<head></head>
<body>

<!-- suppress postback -->
<!-- ko with: Nested -->
<input onclick="dotvvm.applyPostbackHandlers((options) => {
0;
},this,[[&quot;suppress&quot;,(c,d)=>({suppress:c.$parent.Integer() > 100})],[&quot;suppress&quot;,(c,d)=>({suppress:d.SomeString()?.length < 5})]]).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="Test supress">
<!-- /ko -->
<!-- confirm -->
<!-- ko with: Nested -->
<input onclick="dotvvm.applyPostbackHandlers((options) => {
0;
},this,[[&quot;confirm&quot;,(c,d)=>({message:dotvvm.translations.string.format(&quot;String={0} SomeString={1}&quot;, [
c.$parent.String()
, d.SomeString()
])})]]).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="Test confirm">
<!-- /ko -->
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Two handlers
<input data-msg="ahoj" onclick="dotvvm.postBack(this,[],&quot;2suRIvLX7+StPC7x&quot;,&quot;&quot;,null,[[&quot;confirm&quot;,{message:&quot;a&quot;}],[&quot;confirm&quot;,{message:&quot;ahoj&quot;}]],[],undefined).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="">
Two handlers, value binding message
<input data-bind="attr: { &quot;data-msg&quot;: Label }" data-msg="My Label" onclick="dotvvm.postBack(this,[],&quot;b80b5Lh9K50jj3q2&quot;,&quot;&quot;,null,[[&quot;confirm&quot;,{message:&quot;a&quot;}],[&quot;confirm&quot;,(c,d)=>({message:(d).Label()})]],[],undefined).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="">
<input data-bind="attr: { &quot;data-msg&quot;: Label }" data-msg="My Label" onclick="dotvvm.postBack(this,[],&quot;b80b5Lh9K50jj3q2&quot;,&quot;&quot;,null,[[&quot;confirm&quot;,{message:&quot;a&quot;}],[&quot;confirm&quot;,(c,d)=>({message:d.Label()})]],[],undefined).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="">
Two handlers, resource binding message
<input data-msg="My Label" onclick="dotvvm.postBack(this,[],&quot;mH2HraWjqc1sx3p+&quot;,&quot;&quot;,null,[[&quot;confirm&quot;,{message:&quot;a&quot;}],[&quot;confirm&quot;,{message:&quot;My Label&quot;}]],[],undefined).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="">
Two handlers, default message
Expand Down
Loading
Loading