Skip to content

Commit 3bb96ab

Browse files
committed
HierarchyRepeater support for resource binding in DataSource
1 parent f8a281f commit 3bb96ab

File tree

7 files changed

+133
-35
lines changed

7 files changed

+133
-35
lines changed

src/Framework/Framework/Controls/GridView.cs

Lines changed: 1 addition & 1 deletion
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 + "}");

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!, clientItemTemplate! }))
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-
CreateServerLevel(this.Children, context, GetIEnumerableFromDataSource()!);
173+
CreateServerLevel(this.Children,
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.
@@ -185,7 +192,7 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat
185192
Children.Add(clientRootLevel);
186193
clientRootLevel.Children.Add(new HierarchyRepeaterLevel {
187194
IsRoot = true,
188-
ForeachExpression = this.TryGetKnockoutForeachingExpression(),
195+
ForeachExpression = this.TryGetKnockoutForeachExpression().NotNull(),
189196
ItemTemplateId = clientItemTemplateId,
190197
});
191198
}
@@ -200,19 +207,9 @@ private DotvvmControl CreateServerLevel(
200207
IList<DotvvmControl> c,
201208
IDotvvmRequestContext context,
202209
IEnumerable items,
203-
ImmutableArray<int> parentPath = default,
204-
string? foreachExpression = default)
210+
ImmutableArray<int> parentPath,
211+
string? foreachExpression)
205212
{
206-
if (parentPath.IsDefault)
207-
{
208-
parentPath = ImmutableArray<int>.Empty;
209-
}
210-
211-
foreachExpression ??= ((IValueBinding)GetDataSourceBinding()
212-
.GetProperty<DataSourceAccessBinding>()
213-
.Binding)
214-
.GetKnockoutBindingExpression(this);
215-
216213
var dataContextLevelWrapper = new HierarchyRepeaterLevel {
217214
ForeachExpression = foreachExpression
218215
};
@@ -228,7 +225,7 @@ private DotvvmControl CreateServerLevel(
228225
var index = 0;
229226
foreach (var item in items)
230227
{
231-
CreateServerItem(levelWrapper.Children, context, item, parentPath, index);
228+
CreateServerItem(levelWrapper.Children, context, item, parentPath, index, foreachExpression is null);
232229
index++;
233230
}
234231
return dataContextLevelWrapper;
@@ -239,11 +236,12 @@ private DotvvmControl CreateServerItem(
239236
IDotvvmRequestContext context,
240237
object item,
241238
ImmutableArray<int> parentPath,
242-
int index)
239+
int index,
240+
bool serverOnly)
243241
{
244242
var itemWrapper = ItemWrapperCapability.GetWrapper();
245243
c.Add(itemWrapper);
246-
var dataItem = new DataItemContainer { DataItemIndex = index };
244+
var dataItem = new DataItemContainer { DataItemIndex = index, RenderItemBinding = !serverOnly };
247245
itemWrapper.Children.Add(dataItem);
248246
dataItem.SetDataContextTypeFromDataSource(GetDataSourceBinding());
249247
// NB: the placeholder is needed because during data context resolution DataItemContainers are looked up
@@ -263,7 +261,8 @@ private DotvvmControl CreateServerItem(
263261
var itemChildren = GetItemChildren(item);
264262
if (itemChildren.Any())
265263
{
266-
var foreachExpression = ((IValueBinding)ItemChildrenBinding!
264+
var foreachExpression = serverOnly ? null : ((IValueBinding)ItemChildrenBinding
265+
.NotNull("ItemChildrenBinding property is required")
267266
.GetProperty<DataSourceAccessBinding>()
268267
.Binding)
269268
.GetParametrizedKnockoutExpression(dataItem)
@@ -349,6 +348,36 @@ private IEnumerable<object> GetItemChildren(object item)
349348
return ItemChildrenBinding!.Evaluate(tempContainer) ?? Enumerable.Empty<object>();
350349
}
351350

351+
[ControlUsageValidator]
352+
public static IEnumerable<ControlUsageError> ValidateUsage(ResolvedControl control)
353+
{
354+
if (!control.TryGetProperty(DataSourceProperty, out var dataSource))
355+
{
356+
yield return new("DataSource is required on HierarchyRepeater");
357+
yield break;
358+
}
359+
if (dataSource is not ResolvedPropertyBinding { Binding: var dataSourceBinding })
360+
{
361+
yield return new("HierarchyRepeater.DataSource must be a binding");
362+
yield break;
363+
}
364+
if (!control.TryGetProperty(ItemChildrenBindingProperty, out var itemChildren) ||
365+
itemChildren is not ResolvedPropertyBinding { Binding: var itemChildrenBinding })
366+
{
367+
yield break;
368+
}
369+
370+
if (dataSourceBinding.ParserOptions.BindingType != itemChildrenBinding.ParserOptions.BindingType)
371+
{
372+
yield return new(
373+
"HierarchyRepeater.DataSource and HierarchyRepeater.ItemChildrenBinding must have the same binding type, use `value` or `resource` binding for both properties.",
374+
dataSourceBinding.DothtmlNode,
375+
itemChildrenBinding.DothtmlNode
376+
);
377+
}
378+
}
379+
380+
352381
/// <summary>
353382
/// An internal control for a level of the <see cref="HierarchyRepeater"/> that renders
354383
/// 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
{
@@ -128,7 +132,7 @@ protected IBinding GetIndexBinding(IDotvvmRequestContext context)
128132
{
129133
// slower path: create the _index binding at runtime
130134
var bindingService = context.Services.GetRequiredService<BindingCompilationService>();
131-
var dataContext = GetDataSourceBinding().GetProperty<CollectionElementDataContextBindingProperty>().DataContext;
135+
var dataContext = GetChildDataContext();
132136
return bindingService.Cache.CreateCachedBinding("_index", new object[] { dataContext }, () =>
133137
new ValueBindingExpression<int>(bindingService, new object?[] {
134138
dataContext,

src/Framework/Framework/Controls/Repeater.cs

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

171171
private KnockoutBindingGroup GetServerSideForeachBindingGroup() =>
172172
new KnockoutBindingGroup {
173-
{ "data", TryGetKnockoutForeachingExpression().NotNull() }
173+
{ "data", TryGetKnockoutForeachExpression().NotNull() }
174174
};
175175

176176
private (string bindingName, KnockoutBindingGroup bindingValue) GetForeachKnockoutBindingGroup(IDotvvmRequestContext context)
@@ -179,7 +179,7 @@ private KnockoutBindingGroup GetServerSideForeachBindingGroup() =>
179179
var value = new KnockoutBindingGroup();
180180

181181

182-
var javascriptDataSourceExpression = TryGetKnockoutForeachingExpression().NotNull();
182+
var javascriptDataSourceExpression = TryGetKnockoutForeachExpression().NotNull();
183183
value.Add(
184184
useTemplate ? "foreach" : "data",
185185
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
@@ -750,7 +750,7 @@
750750
"mappingMode": "InnerElement"
751751
},
752752
"ItemChildrenBinding": {
753-
"type": "DotVVM.Framework.Binding.Expressions.IValueBinding`1[[System.Collections.Generic.IEnumerable`1[[System.Object, CoreLibrary]], CoreLibrary]], DotVVM.Framework",
753+
"type": "DotVVM.Framework.Binding.Expressions.IStaticValueBinding`1[[System.Collections.Generic.IEnumerable`1[[System.Object, CoreLibrary]], CoreLibrary]], DotVVM.Framework",
754754
"dataContextChange": [
755755
{
756756
"$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework",

0 commit comments

Comments
 (0)