Skip to content

Commit 76fd0f2

Browse files
committed
Repeater: Support for DataSource={resource: ...}
1 parent aa5db88 commit 76fd0f2

File tree

9 files changed

+145
-46
lines changed

9 files changed

+145
-46
lines changed

src/Framework/Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,12 +414,13 @@ public ThisBindingProperty GetThisBinding(IBinding binding, DataContextStack sta
414414
return new ThisBindingProperty(thisBinding);
415415
}
416416

417-
public CollectionElementDataContextBindingProperty GetCollectionElementDataContext(DataContextStack dataContext, ResultTypeBindingProperty resultType)
417+
public CollectionElementDataContextBindingProperty GetCollectionElementDataContext(DataContextStack dataContext, ResultTypeBindingProperty resultType, IBinding binding)
418418
{
419419
return new CollectionElementDataContextBindingProperty(DataContextStack.Create(
420420
ReflectionUtils.GetEnumerableType(resultType.Type).NotNull(),
421421
parent: dataContext,
422-
extensionParameters: new CollectionElementDataContextChangeAttribute(0).GetExtensionParameters(new ResolvedTypeDescriptor(dataContext.DataContextType)).ToArray()
422+
extensionParameters: new CollectionElementDataContextChangeAttribute(0).GetExtensionParameters(new ResolvedTypeDescriptor(dataContext.DataContextType)).ToArray(),
423+
serverSideOnly: binding is not IValueBinding
423424
));
424425
}
425426

src/Framework/Framework/Controls/DataItemContainer.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,17 @@ public int? DataItemIndex
5656
set { this.index = value; SetValue(Internal.UniqueIDProperty, value?.ToString()); }
5757
}
5858

59+
public bool RenderItemBinding { get; set; } = true;
5960

6061
protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext context)
6162
{
6263
var maybeIndex = DataItemIndex;
63-
if (maybeIndex is int index)
64+
if (RenderItemBinding && maybeIndex is int index)
6465
writer.WriteKnockoutDataBindComment("dotvvm-SSR-item", index.ToString());
6566

6667
base.RenderControl(writer, context);
6768

68-
if (maybeIndex is int)
69+
if (RenderItemBinding && maybeIndex is int)
6970
writer.WriteKnockoutDataBindEndComment();
7071
}
7172
}

src/Framework/Framework/Controls/EmptyData.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,15 @@ public EmptyData()
4949

5050
protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext context)
5151
{
52+
var dataSourceBinding = GetValueBinding(DataSourceProperty);
5253
TagName = WrapperTagName;
53-
// if RenderOnServer && DataSource is not empty then don't render anything
54-
if (!RenderOnServer || GetIEnumerableFromDataSource()?.GetEnumerator()?.MoveNext() != true)
54+
// if DataSource is resource binding && DataSource is not empty then don't render anything
55+
if (dataSourceBinding is {} || GetIEnumerableFromDataSource()?.GetEnumerator()?.MoveNext() != true)
5556
{
56-
if (!RenderOnServer)
57+
if (dataSourceBinding is {})
5758
{
58-
var visibleBinding = GetBinding(DataSourceProperty)
59-
.NotNull("DataSource property must contain a binding")
59+
var visibleBinding =
60+
dataSourceBinding
6061
.GetProperty<DataSourceLengthBinding>().Binding
6162
.GetProperty<IsMoreThanZeroBindingProperty>().Binding
6263
.GetProperty<NegatedBindingExpression>().Binding

src/Framework/Framework/Controls/GridView.cs

Lines changed: 3 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 = GetForeachDataBindExpression().GetKnockoutBindingExpression(this);
434+
var foreachBinding = TryGetKnockoutForeachingExpression().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,8 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest
536536
var mapping = userColumnMappingService.GetMapping(itemType!);
537537
var mappingJson = JsonConvert.SerializeObject(mapping);
538538

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

src/Framework/Framework/Controls/HierarchyRepeater.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat
184184
Children.Add(clientRootLevel);
185185
clientRootLevel.AppendChildren(new HierarchyRepeaterLevel {
186186
IsRoot = true,
187-
ForeachExpression = GetForeachDataBindExpression().GetKnockoutBindingExpression(this),
187+
ForeachExpression = this.TryGetKnockoutForeachingExpression(),
188188
ItemTemplateId = clientItemTemplateId,
189189
});
190190
}

src/Framework/Framework/Controls/ItemsControl.cs

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
using System.Linq.Expressions;
1515
using DotVVM.Framework.Hosting;
1616
using Microsoft.Extensions.DependencyInjection;
17+
using FastExpressionCompiler;
18+
using DotVVM.Framework.Compilation;
1719

1820
namespace DotVVM.Framework.Controls
1921
{
@@ -55,29 +57,46 @@ public ItemsControl(string tagName) : base(tagName, false)
5557
/// <summary>
5658
/// Gets the data source binding.
5759
/// </summary>
58-
protected IValueBinding GetDataSourceBinding()
60+
protected IStaticValueBinding GetDataSourceBinding()
5961
{
60-
var binding = GetValueBinding(DataSourceProperty);
61-
if (binding == null)
62+
var binding = GetBinding(DataSourceProperty);
63+
if (binding is null)
6264
{
6365
throw new DotvvmControlException(this, $"The DataSource property of the '{GetType().Name}' control must be set!");
6466
}
65-
return binding;
67+
if (binding is not IStaticValueBinding resourceBinding)
68+
throw new BindingHelper.BindingNotSupportedException(binding) { RelatedControl = this };
69+
return resourceBinding;
6670
}
6771

6872
protected IValueBinding GetItemBinding()
6973
{
70-
return (IValueBinding)GetForeachDataBindExpression().GetProperty<DataSourceCurrentElementBinding>().Binding;
74+
return GetForeachDataBindExpression().GetProperty<DataSourceCurrentElementBinding>().Binding as IValueBinding ??
75+
throw new DotvvmControlException(this, $"The Item property of the '{GetType().Name}' control must be set to a value binding!");
7176
}
7277

7378
public IEnumerable? GetIEnumerableFromDataSource() =>
7479
(IEnumerable?)GetForeachDataBindExpression().Evaluate(this);
7580

76-
protected IValueBinding GetForeachDataBindExpression() =>
77-
(IValueBinding)GetDataSourceBinding().GetProperty<DataSourceAccessBinding>().Binding;
81+
protected IStaticValueBinding GetForeachDataBindExpression() =>
82+
(IStaticValueBinding)GetDataSourceBinding().GetProperty<DataSourceAccessBinding>().Binding;
7883

79-
protected string GetPathFragmentExpression() =>
80-
GetDataSourceBinding().GetKnockoutBindingExpression(this);
84+
protected string? TryGetKnockoutForeachingExpression(bool unwrapped = false) =>
85+
(GetForeachDataBindExpression() as IValueBinding)?.GetKnockoutBindingExpression(this, unwrapped);
86+
87+
protected string GetPathFragmentExpression()
88+
{
89+
var binding = GetDataSourceBinding();
90+
var stringified =
91+
binding.GetProperty<OriginalStringBindingProperty>(ErrorHandlingMode.ReturnNull)?.Code.Trim() ??
92+
binding.GetProperty<KnockoutExpressionBindingProperty>(ErrorHandlingMode.ReturnNull)?.Code.FormatKnockoutScript(this, binding) ??
93+
binding.GetProperty<ParsedExpressionBindingProperty>(ErrorHandlingMode.ReturnNull)?.Expression.ToCSharpString();
94+
95+
if (stringified is null)
96+
throw new DotvvmControlException(this, $"Can't create path fragment from binding {binding}, it does not have OriginalString, ParsedExpression, nor KnockoutExpression property.");
97+
98+
return stringified;
99+
}
81100

82101
[ApplyControlStyle]
83102
public static void OnCompilation(ResolvedControl control, BindingCompilationService bindingService)
@@ -87,9 +106,10 @@ public static void OnCompilation(ResolvedControl control, BindingCompilationServ
87106
if (!(dataSourceProperty is ResolvedPropertyBinding dataSourceBinding)) return;
88107

89108
var dataContext = dataSourceBinding.Binding.Binding.GetProperty<CollectionElementDataContextBindingProperty>().DataContext;
109+
var bindingType = dataContext.ServerSideOnly ? BindingParserOptions.Resource : BindingParserOptions.Value;
90110

91111
control.SetProperty(new ResolvedPropertyBinding(Internal.CurrentIndexBindingProperty,
92-
new ResolvedBinding(bindingService, new Compilation.BindingParserOptions(typeof(ValueBindingExpression)), dataContext,
112+
new ResolvedBinding(bindingService, bindingType, dataContext,
93113
parsedExpression: Expression.Parameter(typeof(int), "_index").AddParameterAnnotation(
94114
new BindingParameterAnnotation(dataContext, new CurrentCollectionIndexExtensionParameter())))));
95115
}

src/Framework/Framework/Controls/Repeater.cs

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Diagnostics;
1010
using System.Linq;
1111
using System.Runtime.CompilerServices;
12+
using DotVVM.Framework.Utils;
1213

1314
namespace DotVVM.Framework.Controls
1415
{
@@ -145,16 +146,19 @@ protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext
145146
{
146147
TagName = WrapperTagName;
147148

148-
var (bindingName, bindingValue) = RenderOnServer ?
149-
("dotvvm-SSR-foreach", GetServerSideForeachBindingGroup()) :
150-
GetForeachKnockoutBindingGroup(context);
151-
if (RenderWrapperTag)
149+
if (GetValueBinding(DataSourceProperty) is {})
152150
{
153-
writer.AddKnockoutDataBind(bindingName, bindingValue);
154-
}
155-
else
156-
{
157-
writer.WriteKnockoutDataBindComment(bindingName, bindingValue.ToString());
151+
var (bindingName, bindingValue) = RenderOnServer ?
152+
("dotvvm-SSR-foreach", GetServerSideForeachBindingGroup()) :
153+
GetForeachKnockoutBindingGroup(context);
154+
if (RenderWrapperTag)
155+
{
156+
writer.AddKnockoutDataBind(bindingName, bindingValue);
157+
}
158+
else
159+
{
160+
writer.WriteKnockoutDataBindComment(bindingName, bindingValue.ToString());
161+
}
158162
}
159163

160164
if (RenderWrapperTag)
@@ -165,7 +169,7 @@ protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext
165169

166170
private KnockoutBindingGroup GetServerSideForeachBindingGroup() =>
167171
new KnockoutBindingGroup {
168-
{ "data", GetForeachDataBindExpression().GetKnockoutBindingExpression(this) }
172+
{ "data", TryGetKnockoutForeachingExpression().NotNull() }
169173
};
170174

171175
private (string bindingName, KnockoutBindingGroup bindingValue) GetForeachKnockoutBindingGroup(IDotvvmRequestContext context)
@@ -174,7 +178,7 @@ private KnockoutBindingGroup GetServerSideForeachBindingGroup() =>
174178
var value = new KnockoutBindingGroup();
175179

176180

177-
var javascriptDataSourceExpression = GetForeachDataBindExpression().GetKnockoutBindingExpression(this);
181+
var javascriptDataSourceExpression = TryGetKnockoutForeachingExpression().NotNull();
178182
value.Add(
179183
useTemplate ? "foreach" : "data",
180184
javascriptDataSourceExpression);
@@ -204,7 +208,6 @@ private KnockoutBindingGroup GetServerSideForeachBindingGroup() =>
204208
/// </summary>
205209
protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext context)
206210
{
207-
Debug.Assert((clientSideTemplate == null) == this.RenderOnServer);
208211
if (clientSideTemplate == null)
209212
{
210213
Debug.Assert(clientSeparator == null);
@@ -223,19 +226,18 @@ protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext c
223226
{
224227
base.RenderEndTag(writer, context);
225228
}
226-
else
229+
else if (GetValueBinding(DataSourceProperty) is {})
227230
{
228231
writer.WriteKnockoutDataBindEndComment();
229232
}
230233

231234
emptyDataContainer?.Render(writer, context);
232235
}
233236

234-
private DotvvmControl GetEmptyItem(IDotvvmRequestContext context)
237+
private DotvvmControl GetEmptyItem(IDotvvmRequestContext context, IStaticValueBinding dataSourceBinding)
235238
{
236239
if (emptyDataContainer == null)
237240
{
238-
var dataSourceBinding = GetDataSourceBinding();
239241
emptyDataContainer = new EmptyData();
240242
emptyDataContainer.SetValue(EmptyData.RenderWrapperTagProperty, GetValueRaw(RenderWrapperTagProperty));
241243
emptyDataContainer.SetValue(EmptyData.WrapperTagNameProperty, GetValueRaw(WrapperTagNameProperty));
@@ -247,24 +249,25 @@ private DotvvmControl GetEmptyItem(IDotvvmRequestContext context)
247249
}
248250

249251
private ConditionalWeakTable<object, DataItemContainer> childrenCache = new ConditionalWeakTable<object, DataItemContainer>();
250-
private DotvvmControl GetItem(IDotvvmRequestContext context, object? item = null, int? index = null, bool allowMemoizationRetrieve = false, bool allowMemoizationStore = false)
252+
private DotvvmControl GetItem(IDotvvmRequestContext context, object? item = null, int? index = null, bool serverOnly = false, bool allowMemoizationRetrieve = false, bool allowMemoizationStore = false)
251253
{
252254
if (allowMemoizationRetrieve && item != null && childrenCache.TryGetValue(item, out var container2) && container2.Parent == null)
253255
{
254256
Debug.Assert(item == container2.GetValueRaw(DataContextProperty));
255-
SetUpServerItem(context, item, (int)index!, container2);
257+
SetUpServerItem(context, item, (int)index!, serverOnly, container2);
256258
return container2;
257259
}
258260

259261
var container = new DataItemContainer();
260262
container.SetDataContextTypeFromDataSource(GetBinding(DataSourceProperty)!);
261263
if (item == null && index == null)
262264
{
265+
Debug.Assert(!serverOnly);
263266
SetUpClientItem(context, container);
264267
}
265268
else
266269
{
267-
SetUpServerItem(context, item!, (int)index!, container);
270+
SetUpServerItem(context, item!, (int)index!, serverOnly, container);
268271
}
269272

270273
ItemTemplate.BuildContent(context, container);
@@ -297,24 +300,28 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat
297300
clientSeparator = null;
298301
clientSideTemplate = null;
299302

300-
if (DataSource != null)
303+
var dataSource = GetIEnumerableFromDataSource();
304+
var dataSourceBinding = GetDataSourceBinding();
305+
var serverOnly = dataSourceBinding is not IValueBinding;
306+
307+
if (dataSource != null)
301308
{
302309
var index = 0;
303-
foreach (var item in GetIEnumerableFromDataSource()!)
310+
foreach (var item in dataSource)
304311
{
305312
if (SeparatorTemplate != null && index > 0)
306313
{
307314
Children.Add(GetSeparator(context));
308315
}
309-
Children.Add(GetItem(context, item, index,
316+
Children.Add(GetItem(context, item, index, serverOnly,
310317
allowMemoizationRetrieve: context.IsPostBack && !memoizeReferences, // on GET request we are not initializing the Repeater twice
311318
allowMemoizationStore: memoizeReferences
312319
));
313320
index++;
314321
}
315322
}
316323

317-
if (renderClientTemplate)
324+
if (renderClientTemplate && !serverOnly)
318325
{
319326
if (SeparatorTemplate != null)
320327
{
@@ -326,7 +333,7 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat
326333

327334
if (EmptyDataTemplate != null)
328335
{
329-
Children.Add(GetEmptyItem(context));
336+
Children.Add(GetEmptyItem(context, dataSourceBinding));
330337
}
331338
}
332339

@@ -337,10 +344,11 @@ private void SetUpClientItem(IDotvvmRequestContext context, DataItemContainer co
337344
container.SetValue(Internal.ClientIDFragmentProperty, this.GetIndexBinding(context));
338345
}
339346

340-
private void SetUpServerItem(IDotvvmRequestContext context, object item, int index, DataItemContainer container)
347+
private void SetUpServerItem(IDotvvmRequestContext context, object item, int index, bool serverOnly, DataItemContainer container)
341348
{
342349
container.DataItemIndex = index;
343350
container.DataContext = item;
351+
container.RenderItemBinding = !serverOnly;
344352
container.SetValue(Internal.PathFragmentProperty, GetPathFragmentExpression() + "/[" + index + "]");
345353
container.ID = index.ToString();
346354
}

src/Tests/ControlTests/ResourceDataContextTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,48 @@ public async Task BasicDataContext()
6868
Assert.AreEqual((string)r.ViewModel.CommandData, "Server o. Customer");
6969
}
7070

71+
[TestMethod]
72+
public async Task Repeater()
73+
{
74+
var r = await cth.RunPage(typeof(TestViewModel), @"
75+
76+
<!-- without wrapper tag -->
77+
<dot:Repeater DataSource={resource: Customers.Items} RenderWrapperTag=false>
78+
<EmptyDataTemplate> This would be here if the Customers.Items were empty </EmptyDataTemplate>
79+
<SeparatorTemplate>
80+
-------------------
81+
</SeparatorTemplate>
82+
<span class=name data-id={resource: Id}>{{resource: Name}}</span>
83+
84+
<span>{{value: _parent.CommandData}}</span>
85+
86+
<dot:Button Click={command: _root.TestMethod(Name)} />
87+
</dot:Repeater>
88+
89+
<!-- with wrapper tag -->
90+
<dot:Repeater DataSource={resource: Customers} WrapperTagName=div>
91+
<EmptyDataTemplate> This would be here if the Customers.Items were empty </EmptyDataTemplate>
92+
<SeparatorTemplate>
93+
-------------------
94+
</SeparatorTemplate>
95+
<span class=name data-id={resource: Id}>{{resource: Name}}</span>
96+
97+
<span>{{value: _parent.CommandData}}</span>
98+
99+
<dot:Button Click={command: _root.TestMethod(Name)} />
100+
</dot:Repeater>
101+
"
102+
);
103+
104+
check.CheckString(r.FormattedHtml, fileExtension: "html");
105+
106+
await r.RunCommand("_root.TestMethod(Name)", x => x is TestViewModel.CustomerData { Id: 1 });
107+
Assert.AreEqual((string)r.ViewModel.CommandData, "One");
108+
109+
await r.RunCommand("_root.TestMethod(Name)", x => x is TestViewModel.CustomerData { Id: 2 });
110+
Assert.AreEqual((string)r.ViewModel.CommandData, "Two");
111+
}
112+
71113
public class TestViewModel: DotvvmViewModelBase
72114
{
73115
public string NullableString { get; } = null;

0 commit comments

Comments
 (0)