Skip to content

Commit

Permalink
HierarchyRepeater support for resource binding in DataSource
Browse files Browse the repository at this point in the history
  • Loading branch information
exyi committed May 25, 2022
1 parent 5b4d11d commit d99fb15
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 36 deletions.
4 changes: 2 additions & 2 deletions src/Framework/Framework/Controls/GridView.cs
Expand Up @@ -431,7 +431,7 @@ protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext
head?.Render(writer, context);

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

var dataBinding = TryGetKnockoutForeachingExpression(unwrapped: true).NotNull("GridView does not support DataSource={resource: ...} at this moment.");
var dataBinding = TryGetKnockoutForeachExpression(unwrapped: true).NotNull("GridView does not support DataSource={resource: ...} at this moment.");
writer.AddKnockoutDataBind("dotvvm-gridviewdataset", $"{{'mapping':{mappingJson},'dataSet':{dataBinding}}}");
base.AddAttributesToRender(writer, context);
}
Expand Down
79 changes: 54 additions & 25 deletions src/Framework/Framework/Controls/HierarchyRepeater.cs
Expand Up @@ -7,7 +7,9 @@
using DotVVM.Framework.Binding;
using DotVVM.Framework.Binding.Expressions;
using DotVVM.Framework.Binding.Properties;
using DotVVM.Framework.Compilation.ControlTree.Resolved;
using DotVVM.Framework.Compilation.Javascript;
using DotVVM.Framework.Compilation.Validation;
using DotVVM.Framework.Controls;
using DotVVM.Framework.Hosting;
using DotVVM.Framework.ResourceManagement;
Expand Down Expand Up @@ -39,14 +41,14 @@ public HierarchyRepeater() : base("div")
[ControlPropertyBindingDataContextChange(nameof(DataSource))]
[BindingCompilationRequirements(new[] { typeof(DataSourceAccessBinding) }, new[] { typeof(DataSourceLengthBinding) })]
[MarkupOptions(Required = true)]
public IValueBinding<IEnumerable<object>>? ItemChildrenBinding
public IStaticValueBinding<IEnumerable<object>>? ItemChildrenBinding
{
get => (IValueBinding<IEnumerable<object>>?)GetValue(ItemChildrenBindingProperty);
get => (IStaticValueBinding<IEnumerable<object>>?)GetValue(ItemChildrenBindingProperty);
set => SetValue(ItemChildrenBindingProperty, value);
}

public static readonly DotvvmProperty ItemChildrenBindingProperty
= DotvvmProperty.Register<IValueBinding<IEnumerable<object>>?, HierarchyRepeater>(t => t.ItemChildrenBinding);
= DotvvmProperty.Register<IStaticValueBinding<IEnumerable<object>>?, HierarchyRepeater>(t => t.ItemChildrenBinding);

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

protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext context)
{
if (RenderOnServer)
if (clientRootLevel is null)
{
foreach (var child in Children.Except(new[] { emptyDataContainer! }))
{
Expand All @@ -156,7 +158,7 @@ protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext
}
else
{
clientRootLevel!.Render(writer, context);
clientRootLevel.Render(writer, context);
}
}

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

if (DataSource is not null)
if (GetIEnumerableFromDataSource() is {} enumerable)
{
this.AppendChildren(CreateServerLevel(context, GetIEnumerableFromDataSource()!));
this.AppendChildren(CreateServerLevel(
context,
enumerable,
parentPath: ImmutableArray<int>.Empty,
foreachExpression: this.TryGetKnockoutForeachExpression()
));
}

if (renderClientTemplate)
if (renderClientTemplate && GetDataSourceBinding() is IValueBinding)
{
// whenever possible, we use the dotvvm deterministic ids, but if we are in a client-side template,
// we'd get a binding... so we just generate a random Guid, not ideal but it will work.
Expand All @@ -184,7 +191,7 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat
Children.Add(clientRootLevel);
clientRootLevel.AppendChildren(new HierarchyRepeaterLevel {
IsRoot = true,
ForeachExpression = this.TryGetKnockoutForeachingExpression(),
ForeachExpression = this.TryGetKnockoutForeachExpression().NotNull(),
ItemTemplateId = clientItemTemplateId,
});
}
Expand All @@ -198,19 +205,9 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat
private DotvvmControl CreateServerLevel(
IDotvvmRequestContext context,
IEnumerable items,
ImmutableArray<int> parentPath = default,
string? foreachExpression = default)
ImmutableArray<int> parentPath,
string? foreachExpression)
{
if (parentPath.IsDefault)
{
parentPath = ImmutableArray<int>.Empty;
}

foreachExpression ??= ((IValueBinding)GetDataSourceBinding()
.GetProperty<DataSourceAccessBinding>()
.Binding)
.GetKnockoutBindingExpression(this);

var dataContextLevelWrapper = new HierarchyRepeaterLevel {
ForeachExpression = foreachExpression
};
Expand All @@ -220,7 +217,7 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat
var index = 0;
foreach (var item in items)
{
levelWrapper.AppendChildren(CreateServerItem(context, item, parentPath, index));
levelWrapper.AppendChildren(CreateServerItem(context, item, parentPath, index, foreachExpression is null));
index++;
}
return dataContextLevelWrapper;
Expand All @@ -230,10 +227,11 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat
IDotvvmRequestContext context,
object item,
ImmutableArray<int> parentPath,
int index)
int index,
bool serverOnly)
{
var itemWrapper = ItemWrapperCapability.GetWrapper();
var dataItem = new DataItemContainer { DataItemIndex = index };
var dataItem = new DataItemContainer { DataItemIndex = index, RenderItemBinding = !serverOnly };
itemWrapper.Children.Add(dataItem);
dataItem.SetDataContextTypeFromDataSource(GetDataSourceBinding());
// NB: the placeholder is needed because during data context resolution DataItemContainers are looked up
Expand All @@ -252,7 +250,8 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat
var itemChildren = GetItemChildren(item);
if (itemChildren.Any())
{
var foreachExpression = ((IValueBinding)ItemChildrenBinding!
var foreachExpression = serverOnly ? null : ((IValueBinding)ItemChildrenBinding
.NotNull("ItemChildrenBinding property is required")
.GetProperty<DataSourceAccessBinding>()
.Binding)
.GetParametrizedKnockoutExpression(dataItem)
Expand Down Expand Up @@ -336,6 +335,36 @@ private IEnumerable<object> GetItemChildren(object item)
return ItemChildrenBinding!.Evaluate(tempContainer) ?? Enumerable.Empty<object>();
}

[ControlUsageValidator]
public static IEnumerable<ControlUsageError> ValidateUsage(ResolvedControl control)
{
if (!control.TryGetProperty(DataSourceProperty, out var dataSource))
{
yield return new("DataSource is required on HierarchyRepeater");
yield break;
}
if (dataSource is not ResolvedPropertyBinding { Binding: var dataSourceBinding })
{
yield return new("HierarchyRepeater.DataSource must be a binding");
yield break;
}
if (!control.TryGetProperty(ItemChildrenBindingProperty, out var itemChildren) ||
itemChildren is not ResolvedPropertyBinding { Binding: var itemChildrenBinding })
{
yield break;
}

if (dataSourceBinding.ParserOptions.BindingType != itemChildrenBinding.ParserOptions.BindingType)
{
yield return new(
"HierarchyRepeater.DataSource and HierarchyRepeater.ItemChildrenBinding must have the same binding type, use `value` or `resource` binding for both properties.",
dataSourceBinding.DothtmlNode,
itemChildrenBinding.DothtmlNode
);
}
}


/// <summary>
/// An internal control for a level of the <see cref="HierarchyRepeater"/> that renders
/// the appropriate foreach binding.
Expand Down
8 changes: 6 additions & 2 deletions src/Framework/Framework/Controls/ItemsControl.cs
Expand Up @@ -81,7 +81,7 @@ protected IValueBinding GetItemBinding()
protected IStaticValueBinding GetForeachDataBindExpression() =>
(IStaticValueBinding)GetDataSourceBinding().GetProperty<DataSourceAccessBinding>().Binding;

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

protected string GetPathFragmentExpression()
Expand All @@ -98,6 +98,10 @@ protected string GetPathFragmentExpression()
return stringified;
}

/// <summary> Returns data context which is expected in the ItemTemplate </summary>
protected DataContextStack GetChildDataContext() =>
GetDataSourceBinding().GetProperty<CollectionElementDataContextBindingProperty>().DataContext;

[ApplyControlStyle]
public static void OnCompilation(ResolvedControl control, BindingCompilationService bindingService)
{
Expand Down Expand Up @@ -125,7 +129,7 @@ protected IBinding GetIndexBinding(IDotvvmRequestContext context)
{
// slower path: create the _index binding at runtime
var bindingService = context.Services.GetRequiredService<BindingCompilationService>();
var dataContext = GetDataSourceBinding().GetProperty<CollectionElementDataContextBindingProperty>().DataContext;
var dataContext = GetChildDataContext();
return bindingService.Cache.CreateCachedBinding("_index", new object[] { dataContext }, () =>
new ValueBindingExpression<int>(bindingService, new object?[] {
dataContext,
Expand Down
4 changes: 2 additions & 2 deletions src/Framework/Framework/Controls/Repeater.cs
Expand Up @@ -169,7 +169,7 @@ protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext

private KnockoutBindingGroup GetServerSideForeachBindingGroup() =>
new KnockoutBindingGroup {
{ "data", TryGetKnockoutForeachingExpression().NotNull() }
{ "data", TryGetKnockoutForeachExpression().NotNull() }
};

private (string bindingName, KnockoutBindingGroup bindingValue) GetForeachKnockoutBindingGroup(IDotvvmRequestContext context)
Expand All @@ -178,7 +178,7 @@ protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext
var value = new KnockoutBindingGroup();


var javascriptDataSourceExpression = TryGetKnockoutForeachingExpression().NotNull();
var javascriptDataSourceExpression = TryGetKnockoutForeachExpression().NotNull();
value.Add(
useTemplate ? "foreach" : "data",
javascriptDataSourceExpression);
Expand Down
40 changes: 36 additions & 4 deletions src/Tests/ControlTests/ResourceDataContextTests.cs
Expand Up @@ -138,22 +138,53 @@ public async Task DataContextRevert()
check.CheckString(r.FormattedHtml, fileExtension: "html");
}

[TestMethod]
public async Task HierarchyRepeater_SimpleTemplate()
{
var r = await cth.RunPage(typeof(TestViewModel), @"
<!-- without wrapper tags -->
<dot:HierarchyRepeater DataSource={resource: Customers.Items}
ItemChildrenBinding={resource: NextLevelCustomers}
RenderWrapperTag=false>
<EmptyDataTemplate> This would be here if the Customers.Items were empty </EmptyDataTemplate>
<span data-id={resource: Id}>{{resource: Name}}</span>
</dot:HierarchyRepeater>
<!-- with wrapper tags -->
<dot:HierarchyRepeater DataSource={resource: Customers.Items}
ItemChildrenBinding={resource: NextLevelCustomers}
LevelWrapperTagName=ul
ItemWrapperTagName=li>
<span data-id={resource: Id}>{{resource: Name}}</span>
</dot:HierarchyRepeater>
"
);

check.CheckString(r.FormattedHtml, fileExtension: "html");
}


public class TestViewModel: DotvvmViewModelBase
{
public string NullableString { get; } = null;


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

public GridViewDataSet<CustomerData> Customers { get; set; } = new GridViewDataSet<CustomerData>() {
RowEditOptions = {
EditRowId = 1,
PrimaryKeyPropertyName = nameof(CustomerData.Id)
},
Items = {
new CustomerData(1, "One"),
new CustomerData(2, "Two")
new CustomerData(1, "One", new()),
new CustomerData(2, "Two", new() {
new CustomerData(21, "first pyramid customer", new() {
new CustomerData(211, "second pyramid customer", new())
})
})
}
};

Expand All @@ -167,7 +198,8 @@ public record CustomerData(
int Id,
[property: Required]
string Name,
bool Enabled = true
// software for running MLM 😂
List<CustomerData> NextLevelCustomers
);

public string CommandData { get; set; }
Expand Down
@@ -0,0 +1,33 @@
<html>
<head></head>
<body>

<!-- without wrapper tags -->
<span data-id="1">One</span>
<span data-id="2">Two</span>
<span data-id="21">first pyramid customer</span>
<span data-id="211">second pyramid customer</span>

<!-- with wrapper tags -->
<div>
<ul>
<li>
<span data-id="1">One</span>
</li>
<li>
<span data-id="2">Two</span>
<ul>
<li>
<span data-id="21">first pyramid customer</span>
<ul>
<li>
<span data-id="211">second pyramid customer</span>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
</body>
</html>
Expand Up @@ -615,7 +615,7 @@
"mappingMode": "InnerElement"
},
"ItemChildrenBinding": {
"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",
"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",
"dataContextChange": [
{
"$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework",
Expand Down

0 comments on commit d99fb15

Please sign in to comment.