Skip to content

Commit d99fb15

Browse files
committed
HierarchyRepeater support for resource binding in DataSource
1 parent 5b4d11d commit d99fb15

File tree

7 files changed

+134
-36
lines changed

7 files changed

+134
-36
lines changed

src/Framework/Framework/Controls/GridView.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext
431431
head?.Render(writer, context);
432432

433433
// render body
434-
var foreachBinding = TryGetKnockoutForeachingExpression().NotNull("GridView does not support DataSource={resource: ...} at this moment.");
434+
var foreachBinding = TryGetKnockoutForeachExpression().NotNull("GridView does not support DataSource={resource: ...} at this moment.");
435435
if (RenderOnServer)
436436
{
437437
writer.AddKnockoutDataBind("dotvvm-SSR-foreach", "{data:" + foreachBinding + "}");
@@ -536,7 +536,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest
536536
var mapping = userColumnMappingService.GetMapping(itemType!);
537537
var mappingJson = JsonConvert.SerializeObject(mapping);
538538

539-
var dataBinding = TryGetKnockoutForeachingExpression(unwrapped: true).NotNull("GridView does not support DataSource={resource: ...} at this moment.");
539+
var dataBinding = TryGetKnockoutForeachExpression(unwrapped: true).NotNull("GridView does not support DataSource={resource: ...} at this moment.");
540540
writer.AddKnockoutDataBind("dotvvm-gridviewdataset", $"{{'mapping':{mappingJson},'dataSet':{dataBinding}}}");
541541
base.AddAttributesToRender(writer, context);
542542
}

src/Framework/Framework/Controls/HierarchyRepeater.cs

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
using DotVVM.Framework.Binding;
88
using DotVVM.Framework.Binding.Expressions;
99
using DotVVM.Framework.Binding.Properties;
10+
using DotVVM.Framework.Compilation.ControlTree.Resolved;
1011
using DotVVM.Framework.Compilation.Javascript;
12+
using DotVVM.Framework.Compilation.Validation;
1113
using DotVVM.Framework.Controls;
1214
using DotVVM.Framework.Hosting;
1315
using DotVVM.Framework.ResourceManagement;
@@ -39,14 +41,14 @@ public HierarchyRepeater() : base("div")
3941
[ControlPropertyBindingDataContextChange(nameof(DataSource))]
4042
[BindingCompilationRequirements(new[] { typeof(DataSourceAccessBinding) }, new[] { typeof(DataSourceLengthBinding) })]
4143
[MarkupOptions(Required = true)]
42-
public IValueBinding<IEnumerable<object>>? ItemChildrenBinding
44+
public IStaticValueBinding<IEnumerable<object>>? ItemChildrenBinding
4345
{
44-
get => (IValueBinding<IEnumerable<object>>?)GetValue(ItemChildrenBindingProperty);
46+
get => (IStaticValueBinding<IEnumerable<object>>?)GetValue(ItemChildrenBindingProperty);
4547
set => SetValue(ItemChildrenBindingProperty, value);
4648
}
4749

4850
public static readonly DotvvmProperty ItemChildrenBindingProperty
49-
= DotvvmProperty.Register<IValueBinding<IEnumerable<object>>?, HierarchyRepeater>(t => t.ItemChildrenBinding);
51+
= DotvvmProperty.Register<IStaticValueBinding<IEnumerable<object>>?, HierarchyRepeater>(t => t.ItemChildrenBinding);
5052

5153
/// <summary>
5254
/// Gets or sets the template for each HierarchyRepeater item.
@@ -147,7 +149,7 @@ protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext c
147149

148150
protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext context)
149151
{
150-
if (RenderOnServer)
152+
if (clientRootLevel is null)
151153
{
152154
foreach (var child in Children.Except(new[] { emptyDataContainer! }))
153155
{
@@ -156,7 +158,7 @@ protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext
156158
}
157159
else
158160
{
159-
clientRootLevel!.Render(writer, context);
161+
clientRootLevel.Render(writer, context);
160162
}
161163
}
162164

@@ -166,12 +168,17 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat
166168
emptyDataContainer = null;
167169
clientItemTemplate = null;
168170

169-
if (DataSource is not null)
171+
if (GetIEnumerableFromDataSource() is {} enumerable)
170172
{
171-
this.AppendChildren(CreateServerLevel(context, GetIEnumerableFromDataSource()!));
173+
this.AppendChildren(CreateServerLevel(
174+
context,
175+
enumerable,
176+
parentPath: ImmutableArray<int>.Empty,
177+
foreachExpression: this.TryGetKnockoutForeachExpression()
178+
));
172179
}
173180

174-
if (renderClientTemplate)
181+
if (renderClientTemplate && GetDataSourceBinding() is IValueBinding)
175182
{
176183
// whenever possible, we use the dotvvm deterministic ids, but if we are in a client-side template,
177184
// we'd get a binding... so we just generate a random Guid, not ideal but it will work.
@@ -184,7 +191,7 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat
184191
Children.Add(clientRootLevel);
185192
clientRootLevel.AppendChildren(new HierarchyRepeaterLevel {
186193
IsRoot = true,
187-
ForeachExpression = this.TryGetKnockoutForeachingExpression(),
194+
ForeachExpression = this.TryGetKnockoutForeachExpression().NotNull(),
188195
ItemTemplateId = clientItemTemplateId,
189196
});
190197
}
@@ -198,19 +205,9 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat
198205
private DotvvmControl CreateServerLevel(
199206
IDotvvmRequestContext context,
200207
IEnumerable items,
201-
ImmutableArray<int> parentPath = default,
202-
string? foreachExpression = default)
208+
ImmutableArray<int> parentPath,
209+
string? foreachExpression)
203210
{
204-
if (parentPath.IsDefault)
205-
{
206-
parentPath = ImmutableArray<int>.Empty;
207-
}
208-
209-
foreachExpression ??= ((IValueBinding)GetDataSourceBinding()
210-
.GetProperty<DataSourceAccessBinding>()
211-
.Binding)
212-
.GetKnockoutBindingExpression(this);
213-
214211
var dataContextLevelWrapper = new HierarchyRepeaterLevel {
215212
ForeachExpression = foreachExpression
216213
};
@@ -220,7 +217,7 @@ private DotvvmControl CreateServerLevel(
220217
var index = 0;
221218
foreach (var item in items)
222219
{
223-
levelWrapper.AppendChildren(CreateServerItem(context, item, parentPath, index));
220+
levelWrapper.AppendChildren(CreateServerItem(context, item, parentPath, index, foreachExpression is null));
224221
index++;
225222
}
226223
return dataContextLevelWrapper;
@@ -230,10 +227,11 @@ private DotvvmControl CreateServerItem(
230227
IDotvvmRequestContext context,
231228
object item,
232229
ImmutableArray<int> parentPath,
233-
int index)
230+
int index,
231+
bool serverOnly)
234232
{
235233
var itemWrapper = ItemWrapperCapability.GetWrapper();
236-
var dataItem = new DataItemContainer { DataItemIndex = index };
234+
var dataItem = new DataItemContainer { DataItemIndex = index, RenderItemBinding = !serverOnly };
237235
itemWrapper.Children.Add(dataItem);
238236
dataItem.SetDataContextTypeFromDataSource(GetDataSourceBinding());
239237
// NB: the placeholder is needed because during data context resolution DataItemContainers are looked up
@@ -252,7 +250,8 @@ private DotvvmControl CreateServerItem(
252250
var itemChildren = GetItemChildren(item);
253251
if (itemChildren.Any())
254252
{
255-
var foreachExpression = ((IValueBinding)ItemChildrenBinding!
253+
var foreachExpression = serverOnly ? null : ((IValueBinding)ItemChildrenBinding
254+
.NotNull("ItemChildrenBinding property is required")
256255
.GetProperty<DataSourceAccessBinding>()
257256
.Binding)
258257
.GetParametrizedKnockoutExpression(dataItem)
@@ -336,6 +335,36 @@ private IEnumerable<object> GetItemChildren(object item)
336335
return ItemChildrenBinding!.Evaluate(tempContainer) ?? Enumerable.Empty<object>();
337336
}
338337

338+
[ControlUsageValidator]
339+
public static IEnumerable<ControlUsageError> ValidateUsage(ResolvedControl control)
340+
{
341+
if (!control.TryGetProperty(DataSourceProperty, out var dataSource))
342+
{
343+
yield return new("DataSource is required on HierarchyRepeater");
344+
yield break;
345+
}
346+
if (dataSource is not ResolvedPropertyBinding { Binding: var dataSourceBinding })
347+
{
348+
yield return new("HierarchyRepeater.DataSource must be a binding");
349+
yield break;
350+
}
351+
if (!control.TryGetProperty(ItemChildrenBindingProperty, out var itemChildren) ||
352+
itemChildren is not ResolvedPropertyBinding { Binding: var itemChildrenBinding })
353+
{
354+
yield break;
355+
}
356+
357+
if (dataSourceBinding.ParserOptions.BindingType != itemChildrenBinding.ParserOptions.BindingType)
358+
{
359+
yield return new(
360+
"HierarchyRepeater.DataSource and HierarchyRepeater.ItemChildrenBinding must have the same binding type, use `value` or `resource` binding for both properties.",
361+
dataSourceBinding.DothtmlNode,
362+
itemChildrenBinding.DothtmlNode
363+
);
364+
}
365+
}
366+
367+
339368
/// <summary>
340369
/// An internal control for a level of the <see cref="HierarchyRepeater"/> that renders
341370
/// the appropriate foreach binding.

src/Framework/Framework/Controls/ItemsControl.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ protected IValueBinding GetItemBinding()
8181
protected IStaticValueBinding GetForeachDataBindExpression() =>
8282
(IStaticValueBinding)GetDataSourceBinding().GetProperty<DataSourceAccessBinding>().Binding;
8383

84-
protected string? TryGetKnockoutForeachingExpression(bool unwrapped = false) =>
84+
protected string? TryGetKnockoutForeachExpression(bool unwrapped = false) =>
8585
(GetForeachDataBindExpression() as IValueBinding)?.GetKnockoutBindingExpression(this, unwrapped);
8686

8787
protected string GetPathFragmentExpression()
@@ -98,6 +98,10 @@ protected string GetPathFragmentExpression()
9898
return stringified;
9999
}
100100

101+
/// <summary> Returns data context which is expected in the ItemTemplate </summary>
102+
protected DataContextStack GetChildDataContext() =>
103+
GetDataSourceBinding().GetProperty<CollectionElementDataContextBindingProperty>().DataContext;
104+
101105
[ApplyControlStyle]
102106
public static void OnCompilation(ResolvedControl control, BindingCompilationService bindingService)
103107
{
@@ -125,7 +129,7 @@ protected IBinding GetIndexBinding(IDotvvmRequestContext context)
125129
{
126130
// slower path: create the _index binding at runtime
127131
var bindingService = context.Services.GetRequiredService<BindingCompilationService>();
128-
var dataContext = GetDataSourceBinding().GetProperty<CollectionElementDataContextBindingProperty>().DataContext;
132+
var dataContext = GetChildDataContext();
129133
return bindingService.Cache.CreateCachedBinding("_index", new object[] { dataContext }, () =>
130134
new ValueBindingExpression<int>(bindingService, new object?[] {
131135
dataContext,

src/Framework/Framework/Controls/Repeater.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext
169169

170170
private KnockoutBindingGroup GetServerSideForeachBindingGroup() =>
171171
new KnockoutBindingGroup {
172-
{ "data", TryGetKnockoutForeachingExpression().NotNull() }
172+
{ "data", TryGetKnockoutForeachExpression().NotNull() }
173173
};
174174

175175
private (string bindingName, KnockoutBindingGroup bindingValue) GetForeachKnockoutBindingGroup(IDotvvmRequestContext context)
@@ -178,7 +178,7 @@ private KnockoutBindingGroup GetServerSideForeachBindingGroup() =>
178178
var value = new KnockoutBindingGroup();
179179

180180

181-
var javascriptDataSourceExpression = TryGetKnockoutForeachingExpression().NotNull();
181+
var javascriptDataSourceExpression = TryGetKnockoutForeachExpression().NotNull();
182182
value.Add(
183183
useTemplate ? "foreach" : "data",
184184
javascriptDataSourceExpression);

src/Tests/ControlTests/ResourceDataContextTests.cs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,22 +138,53 @@ public async Task DataContextRevert()
138138
check.CheckString(r.FormattedHtml, fileExtension: "html");
139139
}
140140

141+
[TestMethod]
142+
public async Task HierarchyRepeater_SimpleTemplate()
143+
{
144+
var r = await cth.RunPage(typeof(TestViewModel), @"
145+
146+
<!-- without wrapper tags -->
147+
<dot:HierarchyRepeater DataSource={resource: Customers.Items}
148+
ItemChildrenBinding={resource: NextLevelCustomers}
149+
RenderWrapperTag=false>
150+
<EmptyDataTemplate> This would be here if the Customers.Items were empty </EmptyDataTemplate>
151+
<span data-id={resource: Id}>{{resource: Name}}</span>
152+
</dot:HierarchyRepeater>
153+
154+
<!-- with wrapper tags -->
155+
<dot:HierarchyRepeater DataSource={resource: Customers.Items}
156+
ItemChildrenBinding={resource: NextLevelCustomers}
157+
LevelWrapperTagName=ul
158+
ItemWrapperTagName=li>
159+
<span data-id={resource: Id}>{{resource: Name}}</span>
160+
</dot:HierarchyRepeater>
161+
"
162+
);
163+
164+
check.CheckString(r.FormattedHtml, fileExtension: "html");
165+
}
166+
167+
141168
public class TestViewModel: DotvvmViewModelBase
142169
{
143170
public string NullableString { get; } = null;
144171

145172

146173
[Bind(Direction.None)]
147-
public CustomerData ServerOnlyCustomer { get; set; } = new CustomerData(100, "Server o. Customer");
174+
public CustomerData ServerOnlyCustomer { get; set; } = new CustomerData(100, "Server o. Customer", new());
148175

149176
public GridViewDataSet<CustomerData> Customers { get; set; } = new GridViewDataSet<CustomerData>() {
150177
RowEditOptions = {
151178
EditRowId = 1,
152179
PrimaryKeyPropertyName = nameof(CustomerData.Id)
153180
},
154181
Items = {
155-
new CustomerData(1, "One"),
156-
new CustomerData(2, "Two")
182+
new CustomerData(1, "One", new()),
183+
new CustomerData(2, "Two", new() {
184+
new CustomerData(21, "first pyramid customer", new() {
185+
new CustomerData(211, "second pyramid customer", new())
186+
})
187+
})
157188
}
158189
};
159190

@@ -167,7 +198,8 @@ public record CustomerData(
167198
int Id,
168199
[property: Required]
169200
string Name,
170-
bool Enabled = true
201+
// software for running MLM 😂
202+
List<CustomerData> NextLevelCustomers
171203
);
172204

173205
public string CommandData { get; set; }
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<html>
2+
<head></head>
3+
<body>
4+
5+
<!-- without wrapper tags -->
6+
<span data-id="1">One</span>
7+
<span data-id="2">Two</span>
8+
<span data-id="21">first pyramid customer</span>
9+
<span data-id="211">second pyramid customer</span>
10+
11+
<!-- with wrapper tags -->
12+
<div>
13+
<ul>
14+
<li>
15+
<span data-id="1">One</span>
16+
</li>
17+
<li>
18+
<span data-id="2">Two</span>
19+
<ul>
20+
<li>
21+
<span data-id="21">first pyramid customer</span>
22+
<ul>
23+
<li>
24+
<span data-id="211">second pyramid customer</span>
25+
</li>
26+
</ul>
27+
</li>
28+
</ul>
29+
</li>
30+
</ul>
31+
</div>
32+
</body>
33+
</html>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@
615615
"mappingMode": "InnerElement"
616616
},
617617
"ItemChildrenBinding": {
618-
"type": "DotVVM.Framework.Binding.Expressions.IValueBinding`1[[System.Collections.Generic.IEnumerable`1[[System.Object, System.Private.CoreLib, Version=***, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Private.CoreLib, Version=***, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], DotVVM.Framework",
618+
"type": "DotVVM.Framework.Binding.Expressions.IStaticValueBinding`1[[System.Collections.Generic.IEnumerable`1[[System.Object, System.Private.CoreLib, Version=***, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Private.CoreLib, Version=***, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], DotVVM.Framework",
619619
"dataContextChange": [
620620
{
621621
"$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework",

0 commit comments

Comments
 (0)