Skip to content

Commit

Permalink
Repeater: Support for DataSource={resource: ...}
Browse files Browse the repository at this point in the history
  • Loading branch information
exyi committed May 25, 2022
1 parent 3af592e commit 189e55c
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -414,12 +414,13 @@ public ThisBindingProperty GetThisBinding(IBinding binding, DataContextStack sta
return new ThisBindingProperty(thisBinding);
}

public CollectionElementDataContextBindingProperty GetCollectionElementDataContext(DataContextStack dataContext, ResultTypeBindingProperty resultType)
public CollectionElementDataContextBindingProperty GetCollectionElementDataContext(DataContextStack dataContext, ResultTypeBindingProperty resultType, IBinding binding)
{
return new CollectionElementDataContextBindingProperty(DataContextStack.Create(
ReflectionUtils.GetEnumerableType(resultType.Type).NotNull(),
parent: dataContext,
extensionParameters: new CollectionElementDataContextChangeAttribute(0).GetExtensionParameters(new ResolvedTypeDescriptor(dataContext.DataContextType)).ToArray()
extensionParameters: new CollectionElementDataContextChangeAttribute(0).GetExtensionParameters(new ResolvedTypeDescriptor(dataContext.DataContextType)).ToArray(),
serverSideOnly: binding is not IValueBinding
));
}

Expand Down
5 changes: 3 additions & 2 deletions src/Framework/Framework/Controls/DataItemContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,17 @@ public int? DataItemIndex
set { this.index = value; SetValue(Internal.UniqueIDProperty, value?.ToString()); }
}

public bool RenderItemBinding { get; set; } = true;

protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext context)
{
var maybeIndex = DataItemIndex;
if (maybeIndex is int index)
if (RenderItemBinding && maybeIndex is int index)
writer.WriteKnockoutDataBindComment("dotvvm-SSR-item", index.ToString());

base.RenderControl(writer, context);

if (maybeIndex is int)
if (RenderItemBinding && maybeIndex is int)
writer.WriteKnockoutDataBindEndComment();
}
}
Expand Down
11 changes: 6 additions & 5 deletions src/Framework/Framework/Controls/EmptyData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,15 @@ public EmptyData()

protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext context)
{
var dataSourceBinding = GetValueBinding(DataSourceProperty);
TagName = WrapperTagName;
// if RenderOnServer && DataSource is not empty then don't render anything
if (!RenderOnServer || GetIEnumerableFromDataSource()?.GetEnumerator()?.MoveNext() != true)
// if DataSource is resource binding && DataSource is not empty then don't render anything
if (dataSourceBinding is {} || GetIEnumerableFromDataSource()?.GetEnumerator()?.MoveNext() != true)
{
if (!RenderOnServer)
if (dataSourceBinding is {})
{
var visibleBinding = GetBinding(DataSourceProperty)
.NotNull("DataSource property must contain a binding")
var visibleBinding =
dataSourceBinding
.GetProperty<DataSourceLengthBinding>().Binding
.GetProperty<IsMoreThanZeroBindingProperty>().Binding
.GetProperty<NegatedBindingExpression>().Binding
Expand Down
5 changes: 3 additions & 2 deletions src/Framework/Framework/Controls/GridView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext
head?.Render(writer, context);

// render body
var foreachBinding = GetForeachDataBindExpression().GetKnockoutBindingExpression(this);
var foreachBinding = TryGetKnockoutForeachingExpression().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,8 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest
var mapping = userColumnMappingService.GetMapping(itemType!);
var mappingJson = JsonConvert.SerializeObject(mapping);

writer.AddKnockoutDataBind("dotvvm-gridviewdataset", $"{{'mapping':{mappingJson},'dataSet':{GetDataSourceBinding().GetKnockoutBindingExpression(this, unwrapped: true)}}}");
var dataBinding = TryGetKnockoutForeachingExpression(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
2 changes: 1 addition & 1 deletion src/Framework/Framework/Controls/HierarchyRepeater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat
Children.Add(clientRootLevel);
clientRootLevel.AppendChildren(new HierarchyRepeaterLevel {
IsRoot = true,
ForeachExpression = GetForeachDataBindExpression().GetKnockoutBindingExpression(this),
ForeachExpression = this.TryGetKnockoutForeachingExpression(),
ItemTemplateId = clientItemTemplateId,
});
}
Expand Down
40 changes: 30 additions & 10 deletions src/Framework/Framework/Controls/ItemsControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
using System.Linq.Expressions;
using DotVVM.Framework.Hosting;
using Microsoft.Extensions.DependencyInjection;
using FastExpressionCompiler;
using DotVVM.Framework.Compilation;

namespace DotVVM.Framework.Controls
{
Expand Down Expand Up @@ -55,29 +57,46 @@ public ItemsControl(string tagName) : base(tagName, false)
/// <summary>
/// Gets the data source binding.
/// </summary>
protected IValueBinding GetDataSourceBinding()
protected IStaticValueBinding GetDataSourceBinding()
{
var binding = GetValueBinding(DataSourceProperty);
if (binding == null)
var binding = GetBinding(DataSourceProperty);
if (binding is null)
{
throw new DotvvmControlException(this, $"The DataSource property of the '{GetType().Name}' control must be set!");
}
return binding;
if (binding is not IStaticValueBinding resourceBinding)
throw new BindingHelper.BindingNotSupportedException(binding) { RelatedControl = this };
return resourceBinding;
}

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

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

protected IValueBinding GetForeachDataBindExpression() =>
(IValueBinding)GetDataSourceBinding().GetProperty<DataSourceAccessBinding>().Binding;
protected IStaticValueBinding GetForeachDataBindExpression() =>
(IStaticValueBinding)GetDataSourceBinding().GetProperty<DataSourceAccessBinding>().Binding;

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

protected string GetPathFragmentExpression()
{
var binding = GetDataSourceBinding();
var stringified =
binding.GetProperty<OriginalStringBindingProperty>(ErrorHandlingMode.ReturnNull)?.Code.Trim() ??
binding.GetProperty<KnockoutExpressionBindingProperty>(ErrorHandlingMode.ReturnNull)?.Code.FormatKnockoutScript(this, binding) ??
binding.GetProperty<ParsedExpressionBindingProperty>(ErrorHandlingMode.ReturnNull)?.Expression.ToCSharpString();

if (stringified is null)
throw new DotvvmControlException(this, $"Can't create path fragment from binding {binding}, it does not have OriginalString, ParsedExpression, nor KnockoutExpression property.");

return stringified;
}

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

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

control.SetProperty(new ResolvedPropertyBinding(Internal.CurrentIndexBindingProperty,
new ResolvedBinding(bindingService, new Compilation.BindingParserOptions(typeof(ValueBindingExpression)), dataContext,
new ResolvedBinding(bindingService, bindingType, dataContext,
parsedExpression: Expression.Parameter(typeof(int), "_index").AddParameterAnnotation(
new BindingParameterAnnotation(dataContext, new CurrentCollectionIndexExtensionParameter())))));
}
Expand Down
56 changes: 32 additions & 24 deletions src/Framework/Framework/Controls/Repeater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using DotVVM.Framework.Utils;

namespace DotVVM.Framework.Controls
{
Expand Down Expand Up @@ -145,16 +146,19 @@ protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext
{
TagName = WrapperTagName;

var (bindingName, bindingValue) = RenderOnServer ?
("dotvvm-SSR-foreach", GetServerSideForeachBindingGroup()) :
GetForeachKnockoutBindingGroup(context);
if (RenderWrapperTag)
if (GetValueBinding(DataSourceProperty) is {})
{
writer.AddKnockoutDataBind(bindingName, bindingValue);
}
else
{
writer.WriteKnockoutDataBindComment(bindingName, bindingValue.ToString());
var (bindingName, bindingValue) = RenderOnServer ?
("dotvvm-SSR-foreach", GetServerSideForeachBindingGroup()) :
GetForeachKnockoutBindingGroup(context);
if (RenderWrapperTag)
{
writer.AddKnockoutDataBind(bindingName, bindingValue);
}
else
{
writer.WriteKnockoutDataBindComment(bindingName, bindingValue.ToString());
}
}

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

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

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


var javascriptDataSourceExpression = GetForeachDataBindExpression().GetKnockoutBindingExpression(this);
var javascriptDataSourceExpression = TryGetKnockoutForeachingExpression().NotNull();
value.Add(
useTemplate ? "foreach" : "data",
javascriptDataSourceExpression);
Expand Down Expand Up @@ -204,7 +208,6 @@ private KnockoutBindingGroup GetServerSideForeachBindingGroup() =>
/// </summary>
protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext context)
{
Debug.Assert((clientSideTemplate == null) == this.RenderOnServer);
if (clientSideTemplate == null)
{
Debug.Assert(clientSeparator == null);
Expand All @@ -223,19 +226,18 @@ protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext c
{
base.RenderEndTag(writer, context);
}
else
else if (GetValueBinding(DataSourceProperty) is {})
{
writer.WriteKnockoutDataBindEndComment();
}

emptyDataContainer?.Render(writer, context);
}

private DotvvmControl GetEmptyItem(IDotvvmRequestContext context)
private DotvvmControl GetEmptyItem(IDotvvmRequestContext context, IStaticValueBinding dataSourceBinding)
{
if (emptyDataContainer == null)
{
var dataSourceBinding = GetDataSourceBinding();
emptyDataContainer = new EmptyData();
emptyDataContainer.SetValue(EmptyData.RenderWrapperTagProperty, GetValueRaw(RenderWrapperTagProperty));
emptyDataContainer.SetValue(EmptyData.WrapperTagNameProperty, GetValueRaw(WrapperTagNameProperty));
Expand All @@ -247,24 +249,25 @@ private DotvvmControl GetEmptyItem(IDotvvmRequestContext context)
}

private ConditionalWeakTable<object, DataItemContainer> childrenCache = new ConditionalWeakTable<object, DataItemContainer>();
private DotvvmControl GetItem(IDotvvmRequestContext context, object? item = null, int? index = null, bool allowMemoizationRetrieve = false, bool allowMemoizationStore = false)
private DotvvmControl GetItem(IDotvvmRequestContext context, object? item = null, int? index = null, bool serverOnly = false, bool allowMemoizationRetrieve = false, bool allowMemoizationStore = false)
{
if (allowMemoizationRetrieve && item != null && childrenCache.TryGetValue(item, out var container2) && container2.Parent == null)
{
Debug.Assert(item == container2.GetValueRaw(DataContextProperty));
SetUpServerItem(context, item, (int)index!, container2);
SetUpServerItem(context, item, (int)index!, serverOnly, container2);
return container2;
}

var container = new DataItemContainer();
container.SetDataContextTypeFromDataSource(GetBinding(DataSourceProperty)!);
if (item == null && index == null)
{
Debug.Assert(!serverOnly);
SetUpClientItem(context, container);
}
else
{
SetUpServerItem(context, item!, (int)index!, container);
SetUpServerItem(context, item!, (int)index!, serverOnly, container);
}

ItemTemplate.BuildContent(context, container);
Expand Down Expand Up @@ -297,24 +300,28 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat
clientSeparator = null;
clientSideTemplate = null;

if (DataSource != null)
var dataSource = GetIEnumerableFromDataSource();
var dataSourceBinding = GetDataSourceBinding();
var serverOnly = dataSourceBinding is not IValueBinding;

if (dataSource != null)
{
var index = 0;
foreach (var item in GetIEnumerableFromDataSource()!)
foreach (var item in dataSource)
{
if (SeparatorTemplate != null && index > 0)
{
Children.Add(GetSeparator(context));
}
Children.Add(GetItem(context, item, index,
Children.Add(GetItem(context, item, index, serverOnly,
allowMemoizationRetrieve: context.IsPostBack && !memoizeReferences, // on GET request we are not initializing the Repeater twice
allowMemoizationStore: memoizeReferences
));
index++;
}
}

if (renderClientTemplate)
if (renderClientTemplate && !serverOnly)
{
if (SeparatorTemplate != null)
{
Expand All @@ -326,7 +333,7 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat

if (EmptyDataTemplate != null)
{
Children.Add(GetEmptyItem(context));
Children.Add(GetEmptyItem(context, dataSourceBinding));
}
}

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

private void SetUpServerItem(IDotvvmRequestContext context, object item, int index, DataItemContainer container)
private void SetUpServerItem(IDotvvmRequestContext context, object item, int index, bool serverOnly, DataItemContainer container)
{
container.DataItemIndex = index;
container.DataContext = item;
container.RenderItemBinding = !serverOnly;
container.SetValue(Internal.PathFragmentProperty, GetPathFragmentExpression() + "/[" + index + "]");
container.ID = index.ToString();
}
Expand Down
42 changes: 42 additions & 0 deletions src/Tests/ControlTests/ResourceDataContextTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,48 @@ public async Task BasicDataContext()
Assert.AreEqual((string)r.ViewModel.CommandData, "Server o. Customer");
}

[TestMethod]
public async Task Repeater()
{
var r = await cth.RunPage(typeof(TestViewModel), @"
<!-- without wrapper tag -->
<dot:Repeater DataSource={resource: Customers.Items} RenderWrapperTag=false>
<EmptyDataTemplate> This would be here if the Customers.Items were empty </EmptyDataTemplate>
<SeparatorTemplate>
-------------------
</SeparatorTemplate>
<span class=name data-id={resource: Id}>{{resource: Name}}</span>
<span>{{value: _parent.CommandData}}</span>
<dot:Button Click={command: _root.TestMethod(Name)} />
</dot:Repeater>
<!-- with wrapper tag -->
<dot:Repeater DataSource={resource: Customers} WrapperTagName=div>
<EmptyDataTemplate> This would be here if the Customers.Items were empty </EmptyDataTemplate>
<SeparatorTemplate>
-------------------
</SeparatorTemplate>
<span class=name data-id={resource: Id}>{{resource: Name}}</span>
<span>{{value: _parent.CommandData}}</span>
<dot:Button Click={command: _root.TestMethod(Name)} />
</dot:Repeater>
"
);

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

await r.RunCommand("_root.TestMethod(Name)", x => x is TestViewModel.CustomerData { Id: 1 });
Assert.AreEqual((string)r.ViewModel.CommandData, "One");

await r.RunCommand("_root.TestMethod(Name)", x => x is TestViewModel.CustomerData { Id: 2 });
Assert.AreEqual((string)r.ViewModel.CommandData, "Two");
}

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

0 comments on commit 189e55c

Please sign in to comment.