Skip to content

Commit

Permalink
Merge pull request #928 from riganti/feature/staticCommand-in-markupC…
Browse files Browse the repository at this point in the history
…ontrol

Static Commands in MarkupControl
  • Loading branch information
quigamdev authored Feb 17, 2021
2 parents c05e0a8 + 2d22d7c commit 757a217
Show file tree
Hide file tree
Showing 24 changed files with 701 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using DotVVM.Framework.Runtime.Filters;
using System.Collections.Immutable;
using DotVVM.Framework.Compilation.Javascript.Ast;
using DotVVM.Framework.Binding;

namespace DotVVM.Framework.Tests.Binding
{
Expand All @@ -24,7 +25,7 @@ public class StaticCommandCompilationTests
/// Gets translation of the specified binding expression if it would be passed in static command
/// For better readability, the returned code does not include null checks
public string CompileBinding(string expression, bool niceMode, params Type[] contexts) => CompileBinding(expression, niceMode, contexts, expectedType: typeof(Command));
public string CompileBinding(string expression, bool niceMode, Type[] contexts, Type expectedType)
public string CompileBinding(string expression, bool niceMode, Type[] contexts, Type expectedType, Type currentMarkupControl = null)
{
var configuration = DotvvmTestHelper.CreateConfiguration();

Expand All @@ -43,14 +44,22 @@ public string CompileBinding(string expression, bool niceMode, Type[] contexts,
.WithAnnotation(new ResultIsPromiseAnnotation(e => e))
), 2, allowMultipleMethods: true);

var context = DataContextStack.Create(
contexts.FirstOrDefault() ?? typeof(object),
extensionParameters: new BindingExtensionParameter[]{
var parameters =
new BindingExtensionParameter[]{
new CurrentCollectionIndexExtensionParameter(),
new BindingPageInfoExtensionParameter(),
new InjectedServiceExtensionParameter("injectedService", new ResolvedTypeDescriptor(typeof(TestService)))
}
.Concat(configuration.Markup.DefaultExtensionParameters);

if (currentMarkupControl != null)
{
parameters = parameters.Append(new CurrentMarkupControlExtensionParameter(new ResolvedTypeDescriptor(currentMarkupControl)));
}

}.Concat(configuration.Markup.DefaultExtensionParameters).ToArray(),
var context = DataContextStack.Create(
contexts.FirstOrDefault() ?? typeof(object),
extensionParameters: parameters.ToArray(),
imports: configuration.Markup.ImportedNamespaces.ToImmutableList());

for (int i = 1; i < contexts.Length; i++)
Expand Down Expand Up @@ -292,12 +301,117 @@ public void StaticCommandCompilation_PromiseReturningTranslatedCall_NeedsReorder
AreEqual(control, result);
}

[TestMethod]
public void StaticCommandCompilation_MarkupControlCommandPropertyUsed_SimpleCall_CorrectCommandExecturionOrder()
{
TestMarkupControl.CreateInitialized();

var result = CompileBinding("_control.Save()", niceMode: true, new[] { typeof(object) }, typeof(Command), typeof(TestMarkupControl));

var expectedReslt = @"
(function(a) {
return new Promise(function(resolve, reject) {
Promise.resolve(a.$control.Save()()).then(function(r_0) {
resolve(r_0);
}, reject);
});
}(ko.contextFor(this)))
";

AreEqual(expectedReslt, result);
}

[TestMethod]
public void StaticCommandCompilation_MarkupControlCommandPropertyUsed_AsArgument_CorrectCommandExecturionOrder()
{
TestMarkupControl.CreateInitialized();

var result = CompileBinding("injectedService.Load(_control.Load())", niceMode: true, new[] { typeof(object) }, typeof(Command), typeof(TestMarkupControl));

var expectedReslt = @"
(function(a, b) {
return new Promise(function(resolve, reject) {
Promise.resolve(a.$control.Load()()).then(function(r_0) {
dotvvm.staticCommandPostback(b, ""WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuVGVzdFNlcnZpY2UsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiTG9hZCIsW10sIkFRQT0iXQ=="", [r_0], options).then(function(r_1) {
resolve(r_1);
}, reject);
}, reject);
});
}(ko.contextFor(this), this))
";

AreEqual(expectedReslt, result);
}

[TestMethod]
public void StaticCommandCompilation_MarkupControlCommandPropertyUsed_WithSamePropertyDependancy_CorrectCommandExecturionOrder()
{
TestMarkupControl.CreateInitialized();

var result = CompileBinding("StringProp = _control.Chanege(StringProp) + injectedService.Load(StringProp)", niceMode: true, new[] { typeof(TestViewModel) }, typeof(Command), typeof(TestMarkupControl));

var expectedReslt = @"
(function(a, c, b) {
return new Promise(function(resolve, reject) {
(
b = dotvvm.staticCommandPostback(a, ""WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuVGVzdFNlcnZpY2UsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiTG9hZCIsW10sIkFRQT0iXQ=="", [c.$data.StringProp()], options) ,
Promise.resolve(c.$control.Chanege()(c.$data.StringProp())).then(function(r_0) {
b.then(function(r_1) {
resolve(c.$data.StringProp(r_0 + r_1).StringProp());
}, reject);
}, reject)
);
});
}(this, ko.contextFor(this)))";

AreEqual(expectedReslt, result);
}


public void AreEqual(string expected, string actual)
=> Assert.AreEqual(RemoveWhitespaces(expected), RemoveWhitespaces(actual));
=> Assert.AreEqual(RemoveWhitespaces(expected), RemoveWhitespaces(actual));

public string RemoveWhitespaces(string source) => string.Concat(source.Where(c => !char.IsWhiteSpace(c)));
}

public class TestMarkupControl : DotvvmMarkupControl
{
public Command Save
{
get => (Command)GetValue(SaveProperty);
set => SetValue(SaveProperty, value);
}
public static readonly DotvvmProperty SaveProperty
= DotvvmProperty.Register<Command, TestMarkupControl>(c => c.Save, null);

public Func<string> Load
{
get { return (Func<string>)GetValue(LoadProperty); }
set { SetValue(LoadProperty, value); }
}
public static readonly DotvvmProperty LoadProperty
= DotvvmProperty.Register<Func<string>, TestMarkupControl>(c => c.Load, null);

public Func<string, string> Change
{
get { return (Func<string, string>)GetValue(ChangeProperty); }
set { SetValue(ChangeProperty, value); }
}
public static readonly DotvvmProperty ChangeProperty
= DotvvmProperty.Register<Func<string, string>, TestMarkupControl>(c => c.Change, null);


public static TestMarkupControl CreateInitialized()
{
var control = new TestMarkupControl();
control.SetBinding(SaveProperty, new FakeCommandBinding(new ParametrizedCode("test"), null));
control.SetBinding(LoadProperty, new FakeCommandBinding(new ParametrizedCode("test2"), null));
control.SetBinding(ChangeProperty, new FakeCommandBinding(new ParametrizedCode("test3"), null));
return control;
}

}

public class FakeCommandBinding : ICommandBinding
{
private readonly ParametrizedCode commandJavascript;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ public ControlTestHelper(bool debug = true, Action<DotvvmConfiguration> config =
if (!fileLoader.MarkupFiles.TryAdd(fileName, markup))
throw new Exception($"File {fileName} already exists");

if (markupFiles is object) foreach (var markupFile in markupFiles)
{
if (!fileLoader.MarkupFiles.TryAdd(markupFile.Key, markupFile.Value))
throw new Exception($"File {markupFile.Value} already exists");
}

return controlBuilderFactory.GetControlBuilder(fileName);
}

Expand Down Expand Up @@ -108,6 +114,7 @@ public async Task<PageRunResult> RunPage(
Type viewModel,
string markup,
Dictionary<string, string> markupFiles = null,
string directives = "",
bool renderResources = false,
[CallerMemberName] string fileName = null)
{
Expand All @@ -127,7 +134,7 @@ public async Task<PageRunResult> RunPage(
{
markup = "<tc:FakeHeadResourceLink />" + markup;
}
markup = $"@viewModel {viewModel.ToString().Replace("+", ".")}\n\n{markup}";
markup = $"@viewModel {viewModel.ToString().Replace("+", ".")}\n{directives}\n\n{markup}";
var request = PreparePage(markup, markupFiles, fileName);
await presenter.ProcessRequest(request);
return CreatePageResult(request);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using CheckTestOutput;
using DotVVM.Framework.Binding;
using DotVVM.Framework.Binding.Expressions;
using DotVVM.Framework.Controls;
using DotVVM.Framework.Tests.Binding;
using DotVVM.Framework.ViewModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DotVVM.Framework.Tests.Common.ControlTests
{
[TestClass]
public class MarkupControlTests
{
ControlTestHelper cth = new ControlTestHelper(config: config => {
config.Markup.AddMarkupControl("cc", "CustomControlWithCommand", "CustomControlWithCommand.dotcontrol");
}, services: s => {
s.AddSingleton<TestService>();
});
OutputChecker check = new OutputChecker(
"testoutputs");

[TestMethod]
public async Task MarkupControl_PassingStaticCommand()
{
var r = await cth.RunPage(typeof(BasicTestViewModel), @"
<cc:CustomControlWithCommand DataContext={value: Integer} Click={staticCommand: s.Save(_parent.Integer)} />
<dot:Repeater DataSource={value: Collection}>
<cc:CustomControlWithCommand Click={staticCommand: s.Save(_this)} />
</dot:Repeater>
",
directives: $"@service s = {typeof(TestService)}",
markupFiles: new Dictionary<string, string> {
["CustomControlWithCommand.dotcontrol"] = @"
@viewModel int
@baseType DotVVM.Framework.Tests.Common.ControlTests.CustomControlWithCommand
@wrapperTag div
<dot:Button Click={staticCommand: _control.Click()} />"
}
);

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

public class BasicTestViewModel : DotvvmViewModelBase
{
[Bind(Name = "int")]
public int Integer { get; set; } = 10000000;

public List<int> Collection { get; set; } = new List<int> { 10, -20 };
}
}

public class CustomControlWithCommand : DotvvmMarkupControl
{
public static readonly DotvvmProperty ClickProperty =
DotvvmProperty.Register<Command, CustomControlWithCommand>("Click");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<html>
<head></head>
<body>

<!-- ko with: int -->
<div>
<!-- ko dotvvm-with-control-properties: { "Click": function(){return dotvvm.applyPostbackHandlers(function(options){return (function(a, b) {
return new Promise(function(resolve, reject) {
dotvvm.staticCommandPostback(a, "WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuVGVzdFNlcnZpY2UsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiU2F2ZSIsW10sIkFRQT0iXQ==", [b.$parent.int()], options).then(function(r_0) {
resolve(r_0);
}, reject);
});
}($element, ko.contextFor($element)))}.bind(this),$element,[])} } -->
<input type="button" value="" onclick="dotvvm.applyPostbackHandlers(function(options){return (function(a, b) {
return new Promise(function(resolve, reject) {
Promise.resolve((b = a.$control.Click()) &amp;&amp; b()).then(function(r_0) {
resolve(r_0);
}, reject);
});
}(ko.contextFor(this)))}.bind(this),this,[]).catch(function(){});event.stopPropagation();return false;">
<!-- /ko -->
</div>
<!-- /ko -->
<div data-bind="foreach: { &quot;data&quot;: Collection }">
<div>
<!-- ko dotvvm-with-control-properties: { "Click": function(){return dotvvm.applyPostbackHandlers(function(options){return (function(a, b) {
return new Promise(function(resolve, reject) {
dotvvm.staticCommandPostback(a, "WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuVGVzdFNlcnZpY2UsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiU2F2ZSIsW10sIkFRQT0iXQ==", [b.$data], options).then(function(r_0) {
resolve(r_0);
}, reject);
});
}($element, ko.contextFor($element)))}.bind(this),$element,[])} } -->
<input type="button" value="" onclick="dotvvm.applyPostbackHandlers(function(options){return (function(a, b) {
return new Promise(function(resolve, reject) {
Promise.resolve((b = a.$control.Click()) &amp;&amp; b()).then(function(r_0) {
resolve(r_0);
}, reject);
});
}(ko.contextFor(this)))}.bind(this),this,[]).catch(function(){});event.stopPropagation();return false;">
<!-- /ko -->
</div>
</div>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ public JsExpression CompileToJavascript(DataContextStack dataContext, Expression
// var resultPromiseVariable = new JsNewExpression("DotvvmPromise"));
var senderVariable = new JsTemporaryVariableParameter(new JsSymbolicParameter(CommandBindingExpression.SenderElementParameter));

var invocationRewriter = new InvocationRewriterExpressionVisitor();
expression = invocationRewriter.Visit(expression);

var rewriter = new TaskSequenceRewriterExpressionVisitor();
expression = rewriter.Visit(expression);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Linq.Expressions;

namespace DotVVM.Framework.Compilation.Javascript
{
public class InvocationRewriterExpressionVisitor : ExpressionVisitor
{
protected override Expression VisitInvocation(InvocationExpression node)
{
var invokeMethod = node.Expression.Type.GetMethod("Invoke");

return Expression.Call(node.Expression, invokeMethod, node.Arguments); ;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,25 @@

namespace DotVVM.Framework.Compilation.Javascript
{
public class DelegateInvokeMethodTranslator : IJavascriptMethodTranslator
{
public JsExpression TryTranslateCall(LazyTranslatedExpression context, LazyTranslatedExpression[] arguments, MethodInfo method)
{
if (method == null)
{
return null;
}

if (method.Name == "Invoke" && typeof(Delegate).IsAssignableFrom(method.DeclaringType))
{
var invocationTargetExpresionCall = context.JsExpression().Invoke(arguments.Select(a => a.JsExpression()));
return invocationTargetExpresionCall
.WithAnnotation(new ResultIsPromiseAnnotation(a=> new JsIdentifierExpression("Promise").Member("resolve").Invoke(a)));
}
return null;
}
}

public class JavascriptTranslatableMethodCollection : IJavascriptMethodTranslator
{
public readonly Dictionary<MethodInfo, IJavascriptMethodTranslator> MethodTranslators = new Dictionary<MethodInfo, IJavascriptMethodTranslator>();
Expand Down Expand Up @@ -188,11 +207,18 @@ JsExpression indexer(JsExpression[] args, MethodInfo method) =>

public JsExpression TryTranslateCall(LazyTranslatedExpression context, LazyTranslatedExpression[] args, MethodInfo method)
{
if (method == null) return null;
if (method == null)
{
return null;
}

{
if (MethodTranslators.TryGetValue(method, out var translator) && translator.TryTranslateCall(context, args, method) is JsExpression result)
{
return result;
}
}

if (method.IsGenericMethod)
{
var genericMethod = method.GetGenericMethodDefinition();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ public JavascriptTranslatorConfiguration()
{
Translators.Add(MethodCollection = new JavascriptTranslatableMethodCollection());
Translators.Add(new EnumToStringMethodTranslator());
Translators.Add(new DelegateInvokeMethodTranslator());
}

public JsExpression TryTranslateCall(LazyTranslatedExpression context, LazyTranslatedExpression[] arguments, MethodInfo method) =>
Expand Down
Loading

0 comments on commit 757a217

Please sign in to comment.