Skip to content

Commit 4de462d

Browse files
authored
Merge pull request #1441 from riganti/markup-control-property-group
markup control: Basic support for property groups
2 parents 9b2db43 + 512bff0 commit 4de462d

File tree

6 files changed

+125
-19
lines changed

6 files changed

+125
-19
lines changed

src/Framework/Framework/Compilation/ControlTree/UsedPropertiesFindingVisitor.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Reflection;
55
using DotVVM.Framework.Binding;
66
using DotVVM.Framework.Binding.Expressions;
7+
using DotVVM.Framework.Compilation.ControlTree;
78
using DotVVM.Framework.Compilation.ControlTree.Resolved;
89
using DotVVM.Framework.Controls;
910
using DotVVM.Framework.Utils;
@@ -17,6 +18,7 @@ class UsedPropertiesFindingVisitor : ResolvedControlTreeVisitor
1718
class ExpressionInspectingVisitor: ExpressionVisitor
1819
{
1920
public HashSet<DotvvmProperty> UsedProperties { get; } = new();
21+
public HashSet<DotvvmPropertyGroup> UsedPropertyGroups { get; } = new();
2022
public bool UsesViewModel { get; set; }
2123
protected override Expression VisitConstant(ConstantExpression node)
2224
{
@@ -28,6 +30,12 @@ protected override Expression VisitMember(MemberExpression node)
2830
{
2931
if (typeof(DotvvmProperty).IsAssignableFrom(node.Type) && node.Member is FieldInfo { IsStatic: true } field)
3032
UsedProperties.Add((DotvvmProperty)field.GetValue(null).NotNull());
33+
34+
if (node.Expression is {} &&
35+
typeof(DotvvmBindableObject).IsAssignableFrom(node.Expression.Type) &&
36+
DotvvmPropertyGroup.ResolvePropertyGroup(node.Expression.Type, node.Member.Name) is {} propertyGroup)
37+
UsedPropertyGroups.Add(propertyGroup);
38+
3139
return base.VisitMember(node);
3240
}
3341

@@ -60,7 +68,8 @@ public override void VisitView(ResolvedTreeRoot view)
6068
base.VisitView(view);
6169

6270
var props = exprVisitor.UsedProperties.OrderBy(p => p.Name).ToArray();
63-
var info = new ControlUsedPropertiesInfo(props, exprVisitor.UsesViewModel);
71+
var propertyGroups = exprVisitor.UsedPropertyGroups.OrderBy(p => p.Name).ToArray();
72+
var info = new ControlUsedPropertiesInfo(props, propertyGroups, exprVisitor.UsesViewModel);
6473

6574
view.SetProperty(new ResolvedPropertyValue(Internal.UsedPropertiesInfoProperty, info));
6675
}
@@ -69,6 +78,7 @@ public override void VisitView(ResolvedTreeRoot view)
6978
[HandleAsImmutableObjectInDotvvmPropertyAttribute]
7079
sealed record ControlUsedPropertiesInfo(
7180
DotvvmProperty[] ClientSideUsedProperties,
81+
DotvvmPropertyGroup[] ClientSideUsedPropertyGroups,
7282
bool UsesViewModelClientSide
7383
);
7484
}

src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,6 @@ JsExpression dictionarySetIndexer(JsExpression[] args, MethodInfo method) =>
153153
AddMethodTranslator(typeof(Dictionary<,>), "set_Item", new GenericMethodCompiler(dictionarySetIndexer));
154154
AddMethodTranslator(typeof(IDictionary<,>), "set_Item", new GenericMethodCompiler(dictionarySetIndexer));
155155
AddMethodTranslator(typeof(IReadOnlyDictionary<,>), "get_Item", new GenericMethodCompiler(dictionaryGetIndexer));
156-
AddMethodTranslator(typeof(ReadOnlyDictionary<,>), "get_Item", new GenericMethodCompiler(dictionaryGetIndexer));
157-
AddMethodTranslator(typeof(ReadOnlyCollection<>), "get_Item", new GenericMethodCompiler(listGetIndexer));
158156
AddMethodTranslator(typeof(Array).GetMethod(nameof(Array.SetValue), new[] { typeof(object), typeof(int) }), new GenericMethodCompiler(arrayElementSetter));
159157
AddPropertyGetterTranslator(typeof(Nullable<>), "Value", new GenericMethodCompiler((JsExpression[] args, MethodInfo method) => args[0]));
160158
AddPropertyGetterTranslator(typeof(Nullable<>), "HasValue",
@@ -551,9 +549,10 @@ private void AddDefaultDictionaryTranslations()
551549
{
552550
AddMethodTranslator(typeof(Dictionary<,>), "Clear", parameterCount: 0, translator: new GenericMethodCompiler(args =>
553551
new JsIdentifierExpression("dotvvm").Member("translations").Member("dictionary").Member("clear").Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance))));
554-
AddMethodTranslator(typeof(Dictionary<,>), "ContainsKey", parameterCount: 1, translator: new GenericMethodCompiler(args =>
555-
new JsIdentifierExpression("dotvvm").Member("translations").Member("dictionary").Member("containsKey").Invoke(args[0], args[1])));
556-
AddMethodTranslator(typeof(Dictionary<,>), "Remove", parameterCount: 1, translator: new GenericMethodCompiler(args =>
552+
var containsKey = new GenericMethodCompiler(args => new JsIdentifierExpression("dotvvm").Member("translations").Member("dictionary").Member("containsKey").Invoke(args[0], args[1]));
553+
AddMethodTranslator(typeof(IDictionary<,>), "ContainsKey", parameterCount: 1, translator: containsKey);
554+
AddMethodTranslator(typeof(IReadOnlyDictionary<,>), "ContainsKey", parameterCount: 1, translator: containsKey);
555+
AddMethodTranslator(typeof(IDictionary<,>), "Remove", parameterCount: 1, translator: new GenericMethodCompiler(args =>
557556
new JsIdentifierExpression("dotvvm").Member("translations").Member("dictionary").Member("remove").Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance), args[1])));
558557
}
559558

@@ -679,14 +678,18 @@ JsExpression wrapInRound(JsExpression a) =>
679678
return result;
680679
}
681680

682-
foreach (var iface in method.DeclaringType!.GetInterfaces())
681+
if (!method.DeclaringType!.IsInterface)
683682
{
684-
if (Interfaces.Contains(iface))
683+
// attempt to match a translation defined on an interface. For example Dictionary`2.ContainsKey should match the IDictionary`2.ContainsKey translation
684+
foreach (var iface in method.DeclaringType.GetInterfaces())
685685
{
686-
var map = method.DeclaringType.GetInterfaceMap(iface);
687-
var imIndex = Array.IndexOf(map.TargetMethods, method);
688-
if (imIndex >= 0 && MethodTranslators.TryGetValue(map.InterfaceMethods[imIndex], out var translator) && translator.TryTranslateCall(context, args, method) is JsExpression result)
689-
return result;
686+
if (Interfaces.Contains(iface) || iface.IsConstructedGenericType && Interfaces.Contains(iface.GetGenericTypeDefinition()))
687+
{
688+
var map = method.DeclaringType.GetInterfaceMap(iface);
689+
var imIndex = Array.IndexOf(map.TargetMethods, method);
690+
if (imIndex >= 0 && TryTranslateCall(context, args, map.InterfaceMethods[imIndex]) is JsExpression result)
691+
return result;
692+
}
690693
}
691694
}
692695
if (method.DeclaringType.IsGenericType && !method.DeclaringType.IsGenericTypeDefinition)

src/Framework/Framework/Controls/DotvvmMarkupControl.cs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
using System.Collections.Immutable;
44
using System.Diagnostics;
55
using System.Linq;
6+
using System.Text;
67
using DotVVM.Framework.Binding;
78
using DotVVM.Framework.Binding.Expressions;
89
using DotVVM.Framework.Compilation;
10+
using DotVVM.Framework.Compilation.ControlTree;
911
using DotVVM.Framework.Compilation.Javascript;
1012
using DotVVM.Framework.Compilation.Parser;
1113
using DotVVM.Framework.Configuration;
@@ -57,7 +59,7 @@ internal override void OnPreInit(IDotvvmRequestContext context)
5759
}
5860

5961
var viewModule = GetValue<ViewModuleReferenceInfo>(Internal.ReferencedViewModuleInfoProperty);
60-
if (viewModule is object)
62+
if (viewModule is {})
6163
{
6264
Debug.Assert(viewModule.IsMarkupControl);
6365
context.ResourceManager.AddRequiredResource(viewModule.ImportResourceName);
@@ -71,21 +73,43 @@ internal override void OnPreInit(IDotvvmRequestContext context)
7173
protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context)
7274
{
7375
var properties = new KnockoutBindingGroup();
74-
var usedProperties =
75-
GetValue<ControlUsedPropertiesInfo>(Internal.UsedPropertiesInfoProperty)?.ClientSideUsedProperties
76-
?? GetDeclaredProperties();
77-
foreach (var p in usedProperties)
76+
var usedProperties = GetValue<ControlUsedPropertiesInfo>(Internal.UsedPropertiesInfoProperty);
77+
foreach (var p in usedProperties?.ClientSideUsedProperties ?? GetDeclaredProperties())
7878
{
7979
if (p.DeclaringType.IsAssignableFrom(typeof(DotvvmMarkupControl)))
8080
continue;
8181

8282
var pinfo = GetPropertySerializationInfo(p); // migrate to use the KnockoutBindingGroup helpers
83-
if (pinfo.Js is object)
83+
if (pinfo.Js is {})
8484
{
8585
properties.Add(p.Name, pinfo.Js);
8686
}
8787
}
8888

89+
foreach (var pg in usedProperties?.ClientSideUsedPropertyGroups ?? DotvvmPropertyGroup.GetPropertyGroups(this.GetType()))
90+
{
91+
if (pg.DeclaringType.IsAssignableFrom(typeof(DotvvmMarkupControl)))
92+
continue;
93+
94+
var js = new StringBuilder().Append("[");
95+
96+
var values = new VirtualPropertyGroupDictionary<object>(this, pg);
97+
foreach (var p in values.Properties.OrderBy(p => p.GroupMemberName))
98+
{
99+
var pinfo = GetPropertySerializationInfo(p); // migrate to use the KnockoutBindingGroup helpers
100+
if (pinfo.Js is {})
101+
{
102+
js.Append("{Key: ")
103+
.Append(JsonConvert.ToString(p.GroupMemberName, '"', StringEscapeHandling.EscapeHtml))
104+
.Append(", Value: ")
105+
.Append(pinfo.Js)
106+
.Append("},");
107+
}
108+
}
109+
js.Append("]");
110+
properties.Add(pg.Name, js.ToString());
111+
}
112+
89113
if (!properties.IsEmpty)
90114
{
91115
if (RendersHtmlTag)
@@ -98,7 +122,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest
98122
}
99123

100124
var viewModule = this.GetValue<ViewModuleReferenceInfo>(Internal.ReferencedViewModuleInfoProperty);
101-
if (viewModule is object)
125+
if (viewModule is {})
102126
{
103127
var settings = DefaultSerializerSettingsProvider.Instance.GetSettingsCopy();
104128
settings.StringEscapeHandling = StringEscapeHandling.EscapeHtml;

src/Tests/ControlTests/MarkupControlTests.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
using DotVVM.Framework.Compilation.Styles;
1515
using AngleSharp;
1616
using DotVVM.Framework.Compilation;
17+
using DotVVM.Framework.Hosting;
18+
using DotVVM.Framework.ResourceManagement;
19+
using DotVVM.Framework.Compilation.ControlTree;
1720

1821
namespace DotVVM.Framework.Tests.ControlTests
1922
{
@@ -22,10 +25,12 @@ public class MarkupControlTests
2225
{
2326
static readonly ControlTestHelper cth = new ControlTestHelper(config: config => {
2427
_ = Repeater.RenderAsNamedTemplateProperty;
28+
config.Resources.RegisterScriptModuleUrl("somemodule", "http://localhost:99999/somemodule.js", null);
2529
config.Markup.AddMarkupControl("cc", "CustomControl", "CustomControl.dotcontrol");
2630
config.Markup.AddMarkupControl("cc", "CustomControlWithCommand", "CustomControlWithCommand.dotcontrol");
2731
config.Markup.AddMarkupControl("cc", "CustomControlWithProperty", "CustomControlWithProperty.dotcontrol");
2832
config.Markup.AddMarkupControl("cc", "CustomControlWithInvalidVM", "CustomControlWithInvalidVM.dotcontrol");
33+
config.Markup.AddMarkupControl("cc", "CustomControlWithInternalProperty", "CustomControlWithInternalProperty.dotcontrol");
2934
config.Styles.Register<Repeater>().SetProperty(r => r.RenderAsNamedTemplate, false, StyleOverrideOptions.Ignore);
3035
}, services: s => {
3136
s.AddSingleton<TestService>();
@@ -134,6 +139,32 @@ @baseType DotVVM.Framework.Tests.ControlTests.CustomControlWithProperty
134139
check.CheckString(r.Html.QuerySelector(".r1 .static-command-button").ToHtml(), fileExtension: "html");
135140
}
136141

142+
[TestMethod]
143+
public async Task MarkupControl_InternalProperty()
144+
{
145+
// test that passing a string -> string dictionary into a _js.Invoke works
146+
// the dictionary is either in a internal DotvvmProperty or declared as a property group
147+
148+
var r = await cth.RunPage(typeof(BasicTestViewModel), @"
149+
<cc:CustomControlWithInternalProperty PropGroup-const='AA' PropGroup-resource={resource: 'XX' + Integer} PropGroup-value={value: Integer} />
150+
",
151+
markupFiles: new Dictionary<string, string> {
152+
["CustomControlWithInternalProperty.dotcontrol"] = @"
153+
@viewModel object
154+
@js somemodule
155+
@baseType DotVVM.Framework.Tests.ControlTests.CustomControlWithInternalProperty
156+
157+
<dot:Button Click={staticCommand: _js.Invoke('xx', _control.Something)} />
158+
<dot:Button Click={staticCommand: _js.Invoke('xx', _control.PropGroup)} />
159+
160+
{{value: _control.PropGroup.ContainsKey('test')}}
161+
"
162+
}
163+
);
164+
165+
check.CheckString(r.FormattedHtml, fileExtension: "html");
166+
}
167+
137168
[TestMethod]
138169
public async Task ShouldFailReasonablyWhenControlHasInvalidViewModel()
139170
{
@@ -186,4 +217,20 @@ public void IncrementProperty()
186217
this.SetValueToSource(PProperty, (int)GetValue(PProperty) + 1);
187218
}
188219
}
220+
221+
public class CustomControlWithInternalProperty : DotvvmMarkupControl
222+
{
223+
internal static readonly DotvvmProperty SomethingProperty =
224+
DotvvmProperty.Register<Dictionary<string, string>, CustomControlWithInternalProperty>("Something");
225+
226+
[PropertyGroup("PropGroup-", ValueType = typeof(bool))]
227+
public VirtualPropertyGroupDictionary<string> PropGroup => new(this, PropGroupGroupDescriptor);
228+
public static DotvvmPropertyGroup PropGroupGroupDescriptor =
229+
DotvvmPropertyGroup.Register<string, CustomControlWithInternalProperty>("PropGroup-", nameof(PropGroup));
230+
231+
protected internal override void OnPreRender(IDotvvmRequestContext context)
232+
{
233+
this.SetValue(SomethingProperty, new Dictionary<string, string> { { "test", "test" }, {"x", "y"} });
234+
}
235+
}
189236
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<html>
2+
<head></head>
3+
<body>
4+
<div data-bind="dotvvm-with-control-properties: { Something: [{&quot;Key&quot;:&quot;test&quot;,&quot;Value&quot;:&quot;test&quot;},{&quot;Key&quot;:&quot;x&quot;,&quot;Value&quot;:&quot;y&quot;}], PropGroup: [{Key: &quot;const&quot;, Value: &quot;AA&quot;},{Key: &quot;resource&quot;, Value: &quot;XX10000000&quot;},{Key: &quot;value&quot;, Value: dotvvm.globalize.bindingNumberToString(int)},] }, dotvvm-with-view-modules: { modules: [&quot;somemodule&quot;] }">
5+
<input onclick="dotvvm.applyPostbackHandlers((options) => {
6+
dotvvm.viewModules.call(this, &quot;xx&quot;, [options.knockoutContext.$control.Something.state], false);
7+
},this).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="">
8+
<input onclick="dotvvm.applyPostbackHandlers((options) => {
9+
dotvvm.viewModules.call(this, &quot;xx&quot;, [options.knockoutContext.$control.PropGroup.state], false);
10+
},this).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="">
11+
12+
<!-- ko text: dotvvm.translations.dictionary.containsKey($control.PropGroup(), "test") -->
13+
<!-- /ko -->
14+
</div>
15+
</body>
16+
</html>

src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,6 +1418,12 @@
14181418
"type": "System.Object"
14191419
}
14201420
},
1421+
"DotVVM.Framework.Tests.ControlTests.CustomControlWithInternalProperty": {
1422+
"PropGroup": {
1423+
"prefix": "PropGroup-",
1424+
"type": "System.String"
1425+
}
1426+
},
14211427
"DotVVM.Framework.Tests.ControlTests.RepeatedButton": {
14221428
"Attributes": {
14231429
"prefixes": [

0 commit comments

Comments
 (0)