Skip to content

Commit f02cfc5

Browse files
authored
Merge pull request #1563 from riganti/styles-capability-control
styles: support controls in capabilities
2 parents fd03342 + 1c4a65f commit f02cfc5

File tree

14 files changed

+359
-17
lines changed

14 files changed

+359
-17
lines changed

src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedControl.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.Diagnostics.CodeAnalysis;
45
using System.Linq;
56
using DotVVM.Framework.Binding;
@@ -57,6 +58,8 @@ public void SetProperty(ResolvedPropertySetter value, bool replace = false)
5758

5859
public bool SetProperty(ResolvedPropertySetter value, bool replace, [NotNullWhen(false)] out string? error)
5960
{
61+
if (value is ResolvedPropertyCapability capability)
62+
return SetCapabilityProperty(capability, replace, out error);
6063
error = null;
6164
if (!Properties.TryGetValue(value.Property, out var oldValue) || replace)
6265
{
@@ -82,8 +85,25 @@ public bool SetProperty(ResolvedPropertySetter value, bool replace, [NotNullWhen
8285
return true;
8386
}
8487

88+
bool SetCapabilityProperty(ResolvedPropertyCapability capability, bool replace, [NotNullWhen(false)] out string? error)
89+
{
90+
foreach (var v in capability.Values)
91+
{
92+
Debug.Assert(v.Value.Property == v.Key);
93+
if (!SetProperty(v.Value, replace, out var innerError))
94+
{
95+
error = $"{v.Key}: {innerError}";
96+
return false;
97+
}
98+
}
99+
error = null;
100+
return true;
101+
}
102+
85103
public bool RemoveProperty(DotvvmProperty property)
86104
{
105+
if (property is DotvvmCapabilityProperty capability)
106+
throw new NotSupportedException("Cannot remove capability property, remove each of its properties manually.");
87107
if (Properties.TryGetValue(property, out _))
88108
{
89109
Properties.Remove(property);
@@ -124,7 +144,7 @@ public bool RemoveProperty(DotvvmProperty property)
124144

125145
if (properties.Count == 0)
126146
return null;
127-
return new ResolvedPropertyCapability(capability, properties);
147+
return new ResolvedPropertyCapability(capability, properties) { Parent = this };
128148
}
129149

130150
public override void Accept(IResolvedControlTreeVisitor visitor)

src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedPropertyCapability.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public override void Accept(IResolvedControlTreeVisitor visitor)
4343

4444
public override void AcceptChildren(IResolvedControlTreeVisitor visitor) { }
4545

46-
public object? ToCapabilityObject(bool throwExceptions = false)
46+
public object? ToCapabilityObject(IServiceProvider? services, bool throwExceptions = false)
4747
{
4848
var capability = this.Property;
4949

@@ -69,7 +69,6 @@ public override void AcceptChildren(IResolvedControlTreeVisitor visitor) { }
6969
else
7070
return t.GetConstructor(new [] { elementType })!.Invoke(new [] { value });
7171
}
72-
// TODO: controls and templates
7372
if (throwExceptions)
7473
throw new NotSupportedException($"Can not convert {value} to {t}");
7574
return null;
@@ -79,7 +78,7 @@ public override void AcceptChildren(IResolvedControlTreeVisitor visitor) { }
7978
foreach (var (p, dotprop) in mapping)
8079
{
8180
if (this.Values.TryGetValue(dotprop, out var value))
82-
p.SetValue(obj, convertValue(value.GetValue(), p.PropertyType));
81+
p.SetValue(obj, convertValue(Styles.ResolvedControlHelper.ToRuntimeValue(value, services), p.PropertyType));
8382
}
8483

8584
if (capability.PropertyGroupMapping is not { Length: > 0 } groupMappingList)
@@ -105,7 +104,7 @@ public override void AcceptChildren(IResolvedControlTreeVisitor visitor) { }
105104
{
106105

107106
foreach (var p in properties)
108-
dictionary.Add(p.GroupMemberName, convertValue(this.Values[p].GetValue(), dictionaryElementType));
107+
dictionary.Add(p.GroupMemberName, convertValue(Styles.ResolvedControlHelper.ToRuntimeValue(this.Values[p], services), dictionaryElementType));
109108
}
110109
if (propertyOriginalValue is null)
111110
prop.SetValue(obj, dictionary);

src/Framework/Framework/Compilation/ControlTree/ResolvedTreeHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public static Type GetResultType(this ResolvedPropertySetter property) =>
3333
ResolvedPropertyTemplate value => value.Content,
3434
ResolvedPropertyControl value => value.Control,
3535
ResolvedPropertyControlCollection value => value.Controls,
36-
ResolvedPropertyCapability value => value.ToCapabilityObject(throwExceptions: false),
36+
ResolvedPropertyCapability value => value,
3737
_ => throw new NotSupportedException()
3838
};
3939

src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using DotVVM.Framework.Hosting;
1919
using DotVVM.Framework.Compilation.ViewCompiler;
2020
using DotVVM.Framework.Runtime;
21+
using FastExpressionCompiler;
2122

2223
namespace DotVVM.Framework.Compilation.Styles
2324
{
@@ -149,7 +150,18 @@ value is null ||
149150

150151
public static ResolvedPropertySetter TranslateProperty(DotvvmProperty property, object? value, DataContextStack dataContext, DotvvmConfiguration? config)
151152
{
152-
if (value is ResolvedPropertySetter resolvedSetter)
153+
if (value is ResolvedPropertyCapability resolvedCapability)
154+
{
155+
if (resolvedCapability.Property == property && resolvedCapability.GetDataContextStack(null) == dataContext)
156+
return resolvedCapability;
157+
if (property is not DotvvmCapabilityProperty capabilityProperty)
158+
throw new NotSupportedException($"Property {property.Name} must capability property.");
159+
if (resolvedCapability.Property.PropertyType != capabilityProperty.PropertyType)
160+
throw new NotSupportedException($"Property {property.Name} have type {resolvedCapability.Property.PropertyType.ToCode()}.");
161+
162+
value = resolvedCapability.ToCapabilityObject(config?.ServiceProvider);
163+
}
164+
else if (value is ResolvedPropertySetter resolvedSetter)
153165
{
154166
value = resolvedSetter.GetValue();
155167
}
@@ -280,12 +292,36 @@ public static void SetContent(ResolvedControl control, ResolvedControl[] innerCo
280292
static DotvvmBindableObject ToLazyRuntimeControl(this ResolvedControl c, Type expectedType, IServiceProvider services)
281293
{
282294
if (expectedType == typeof(DotvvmControl))
283-
return new LazyRuntimeControl(c);
295+
return new LazyRuntimeControl(c, services);
284296
else
285297
return ToRuntimeControl(c, services);
286298
}
287299

288-
static object? ToRuntimeValue(this ResolvedPropertySetter setter, IServiceProvider services)
300+
static IEnumerable<T> CreateEnumerable<T>(Type type, IEnumerable<T> items)
301+
{
302+
var elementType = ReflectionUtils.GetEnumerableType(type)!;
303+
if (type.IsArray)
304+
{
305+
var array = Array.CreateInstance(elementType, items.Count());
306+
foreach (var (item, i) in items.Select((item, i) => (item, i)))
307+
array.SetValue(item, i);
308+
return (IEnumerable<T>)array;
309+
}
310+
else
311+
{
312+
if (type.IsInterface)
313+
{
314+
// hope that List implements the interface
315+
type = typeof(List<>).MakeGenericType(elementType);
316+
}
317+
var list = (System.Collections.IList)Activator.CreateInstance(type)!;
318+
foreach (var i in items)
319+
list.Add(i);
320+
return (IEnumerable<T>)list;
321+
}
322+
}
323+
324+
public static object? ToRuntimeValue(this ResolvedPropertySetter setter, IServiceProvider? services)
289325
{
290326
if (setter is ResolvedPropertyValue valueSetter)
291327
return valueSetter.Value;
@@ -295,16 +331,31 @@ static DotvvmBindableObject ToLazyRuntimeControl(this ResolvedControl c, Type ex
295331
var expectedType = setter.Property.PropertyType;
296332

297333
if (setter is ResolvedPropertyControl controlSetter)
334+
{
335+
if (services is null)
336+
throw new ArgumentNullException(nameof(services), "Cannot convert a control to a runtime value without a service provider.");
337+
298338
return controlSetter.Control?.ToLazyRuntimeControl(expectedType, services);
339+
}
299340
else if (setter is ResolvedPropertyControlCollection controlCollectionSetter)
300341
{
342+
if (services is null)
343+
throw new ArgumentNullException(nameof(services), "Cannot convert a control to a runtime value without a service provider.");
344+
301345
var expectedControlType = ReflectionUtils.GetEnumerableType(expectedType)!;
302-
return controlCollectionSetter.Controls.Select(c => c.ToLazyRuntimeControl(expectedControlType, services)).ToList();
346+
return CreateEnumerable(
347+
expectedType,
348+
controlCollectionSetter.Controls.Select(c => c.ToLazyRuntimeControl(expectedControlType, services))
349+
);
303350
}
304351
else if (setter is ResolvedPropertyTemplate templateSetter)
305352
{
306353
return new ResolvedControlTemplate(templateSetter.Content.ToArray());
307354
}
355+
else if (setter is ResolvedPropertyCapability capability)
356+
{
357+
return capability.ToCapabilityObject(services);
358+
}
308359
else
309360
throw new NotSupportedException($"Property setter {setter.GetType().Name} is not supported.");
310361
}
@@ -343,12 +394,15 @@ public static DotvvmBindableObject ToRuntimeControl(this ResolvedControl c, ISer
343394
public sealed class LazyRuntimeControl: DotvvmControl
344395
{
345396
public ResolvedControl ResolvedControl { get; set; }
397+
readonly IServiceProvider services;
398+
346399
private bool initialized = false;
347400

348-
public LazyRuntimeControl(ResolvedControl resolvedControl)
401+
public LazyRuntimeControl(ResolvedControl resolvedControl, IServiceProvider services)
349402
{
350403
ResolvedControl = resolvedControl;
351404
LifecycleRequirements = ControlLifecycleRequirements.Init;
405+
this.services = services;
352406
}
353407

354408
void InitializeChildren(IDotvvmRequestContext? context)
@@ -359,10 +413,9 @@ void InitializeChildren(IDotvvmRequestContext? context)
359413
{
360414
if (initialized) return;
361415

362-
if (context is null)
363-
throw new InvalidOperationException("Internal.RequestContextProperty property is not set.");
416+
var services = context?.Services ?? this.services;
364417

365-
Children.Add((DotvvmControl)ResolvedControl.ToRuntimeControl(context.Services));
418+
Children.Add((DotvvmControl)ResolvedControl.ToRuntimeControl(services));
366419
initialized = true;
367420
}
368421
}

src/Framework/Framework/Compilation/Styles/StyleMatchContextMethods.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,13 @@ public static T PropertyValue<T>(this IStyleMatchContext c, DotvvmProperty prope
249249
if (c.Control.GetProperty(property) is {} s)
250250
{
251251
var value = s.GetValue();
252+
// If they ask for ResolvedControl, we should return it. Otherwise, try converting to runtime value (DotvvmControl or capability object)
252253
if (value is T or null)
253254
return (T?)value;
255+
256+
var runtimeValue = s.ToRuntimeValue(c.Configuration.ServiceProvider);
257+
if (runtimeValue is T)
258+
return (T?)runtimeValue;
254259
}
255260
return GetDefault<T>(property);
256261
}

src/Samples/Common/DotVVM.Samples.Common.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
<None Remove="Views\FeatureSamples\Attribute\ToStringConversion.dothtml" />
8484
<None Remove="Views\FeatureSamples\AutoUI\AutoEditor.dothtml" />
8585
<None Remove="Views\FeatureSamples\AutoUI\AutoForm.dothtml" />
86+
<None Remove="Views\FeatureSamples\AutoUI\AutoGridViewColumns.dothtml" />
8687
<None Remove="Views\FeatureSamples\BindingVariables\StaticCommandVariablesWithService_Complex.dothtml" />
8788
<None Remove="Views\FeatureSamples\BindingVariables\StaticCommandVariablesWithService_Complex2.dothtml" />
8889
<None Remove="Views\FeatureSamples\BindingVariables\StaticCommandVariablesWithService_Simple.dothtml" />
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using DotVVM.Framework.Controls;
7+
using DotVVM.Framework.ViewModel;
8+
9+
namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.AutoUI
10+
{
11+
public class AutoGridViewColumnsViewModel : DotvvmViewModelBase
12+
{
13+
14+
public GridViewDataSet<BasicSamples.ViewModels.ControlSamples.GridView.CustomerData> Customers { get; set; } = new();
15+
16+
17+
public override Task PreRender()
18+
{
19+
if (Customers.IsRefreshRequired)
20+
{
21+
Customers.LoadFromQueryable(GetData());
22+
}
23+
return base.PreRender();
24+
}
25+
26+
private static IQueryable<BasicSamples.ViewModels.ControlSamples.GridView.CustomerData> GetData()
27+
{
28+
return new[]
29+
{
30+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 1, Name = "John Doe", BirthDate = DateTime.Parse("1976-04-01"), MessageReceived = false},
31+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 2, Name = "John Deer", BirthDate = DateTime.Parse("1984-03-02"), MessageReceived = false },
32+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 3, Name = "Johnny Walker", BirthDate = DateTime.Parse("1934-01-03"), MessageReceived = true},
33+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 4, Name = "Jim Hacker", BirthDate = DateTime.Parse("1912-11-04"), MessageReceived = true},
34+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 5, Name = "Joe E. Brown", BirthDate = DateTime.Parse("1947-09-05"), MessageReceived = false},
35+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 6, Name = "Jim Harris", BirthDate = DateTime.Parse("1956-07-06"), MessageReceived = false},
36+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 7, Name = "J. P. Morgan", BirthDate = DateTime.Parse("1969-05-07"), MessageReceived = false },
37+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 8, Name = "J. R. Ewing", BirthDate = DateTime.Parse("1987-03-08"), MessageReceived = false},
38+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 9, Name = "Jeremy Clarkson", BirthDate = DateTime.Parse("1994-04-09"), MessageReceived = false },
39+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 10, Name = "Jenny Green", BirthDate = DateTime.Parse("1947-02-10"), MessageReceived = false},
40+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 11, Name = "Joseph Blue", BirthDate = DateTime.Parse("1948-12-11"), MessageReceived = false},
41+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 12, Name = "Jack Daniels", BirthDate = DateTime.Parse("1968-10-12"), MessageReceived = true},
42+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 13, Name = "Jackie Chan", BirthDate = DateTime.Parse("1978-08-13"), MessageReceived = false},
43+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 14, Name = "Jasper", BirthDate = DateTime.Parse("1934-06-14"), MessageReceived = false},
44+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 15, Name = "Jumbo", BirthDate = DateTime.Parse("1965-06-15"), MessageReceived = false },
45+
new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 16, Name = "Junkie Doodle", BirthDate = DateTime.Parse("1977-05-16"), MessageReceived = false }
46+
}.AsQueryable();
47+
}
48+
}
49+
}
50+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.AutoUI.AutoGridViewColumnsViewModel, DotVVM.Samples.Common
2+
3+
<!DOCTYPE html>
4+
5+
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
6+
<head>
7+
<meta charset="utf-8" />
8+
<title></title>
9+
</head>
10+
<body>
11+
12+
<dot:GridView DataSource="{value: Customers}">
13+
<auto:GridViewColumns>
14+
<ContentTemplate-Name>
15+
<h2>{{value: Name}}</h2>
16+
</ContentTemplate-Name>
17+
</auto:GridViewColumns>
18+
</dot:GridView>
19+
20+
</body>
21+
</html>
22+
23+

src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Samples/Tests/Tests/Feature/AutoUITests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,5 +110,27 @@ public void Feature_AutoUI_AutoForm()
110110
AssertUI.IsDisplayed(streetField.ParentElement.ParentElement.Single(".help"));
111111
});
112112
}
113+
114+
[Fact]
115+
public void Feature_AutoUI_AutoGridViewColumns()
116+
{
117+
RunInAllBrowsers(browser => {
118+
browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_AutoUI_AutoGridViewColumns);
119+
120+
var headerCells = browser.FindElements("thead tr:first-child th")
121+
.ThrowIfDifferentCountThan(4);
122+
AssertUI.TextEquals(headerCells[0], "Customer id");
123+
AssertUI.TextEquals(headerCells[1], "Person or company name");
124+
AssertUI.TextEquals(headerCells[2], "Birth date");
125+
AssertUI.TextEquals(headerCells[3], "Message received");
126+
127+
var cells = browser.FindElements("tbody tr:first-child td")
128+
.ThrowIfDifferentCountThan(4);
129+
AssertUI.TextEquals(cells[0].Single("span"), "1");
130+
AssertUI.TextEquals(cells[1].Single("h2"), "John Doe");
131+
AssertUI.TextEquals(cells[2].Single("span"), "4/1/1976 12:00:00 AM");
132+
AssertUI.IsNotChecked(cells[3].Single("input[type=checkbox]"));
133+
});
134+
}
113135
}
114136
}

0 commit comments

Comments
 (0)