Skip to content

Commit

Permalink
postbacki handlers: Fix knockout context access in value bindings
Browse files Browse the repository at this point in the history
Fixes accesses to parent (or higher) knockout contexts or view models.

It is necessary to call the AssignParameters function recursively on default assignments of the parent VM/context parameters.
  • Loading branch information
exyi committed Oct 26, 2024
1 parent 468a7ce commit 60b4226
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 8 deletions.
21 changes: 17 additions & 4 deletions src/Framework/Framework/Compilation/Javascript/ParametrizedCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,20 +141,33 @@ 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 (param.DefaultAssignment.Code is {})
{
var newDefault = param.DefaultAssignment.Code.AssignParameters(parameterAssignment);
if (newDefault != param.DefaultAssignment.Code)
builder.Add(new CodeParameterInfo(param.Parameter, param.OperatorPrecedence, param.IsSafeMemberAccess, newDefault));
else
builder.Add(param);
}
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 `.`
else
{
builder.Add(a.Code, parameters![i].OperatorPrecedence);
builder.Add(a.Code, param.OperatorPrecedence);
builder.Add(stringParts[1 + i]);
}
}
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") :
o == JavascriptTranslator.KnockoutViewModelParameter ? CodeParameterAssignment.FromIdentifier("d") :
default(CodeParameterAssignment)
);
return new JsSymbolicParameter(new CodeSymbolicParameter("tmp symbol", defaultAssignment: adjustedCode));
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
61 changes: 61 additions & 0 deletions src/Tests/ControlTests/PostbackHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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 || 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 || 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

0 comments on commit 60b4226

Please sign in to comment.