Skip to content

Commit

Permalink
datasets: Data context fixes in AppendableDataPager + some tests
Browse files Browse the repository at this point in the history
  • Loading branch information
exyi authored and tomasherceg committed Apr 27, 2024
1 parent c9bacd8 commit 3324bf6
Show file tree
Hide file tree
Showing 12 changed files with 179 additions and 21 deletions.
38 changes: 30 additions & 8 deletions src/Framework/Framework/Binding/BindingHelper.cs
Expand Up @@ -400,7 +400,13 @@ public static TExpression AddParameterAnnotation<TExpression>(this TExpression e
return dataContextType;
}

var (childType, extensionParameters) = ApplyDataContextChange(dataContextType, property.DataContextChangeAttributes, obj, property);
var (childType, extensionParameters, addLayer) = ApplyDataContextChange(dataContextType, property.DataContextChangeAttributes, obj, property);

if (!addLayer)
{
Debug.Assert(childType == dataContextType.DataContextType);
return DataContextStack.Create(dataContextType.DataContextType, dataContextType.Parent, dataContextType.NamespaceImports, extensionParameters.Concat(dataContextType.ExtensionParameters).ToArray(), dataContextType.BindingPropertyResolvers);
}

if (childType is null) return null; // childType is null in case there is some error in processing (e.g. enumerable was expected).
else return DataContextStack.Create(childType, dataContextType, extensionParameters: extensionParameters.ToArray());
Expand All @@ -423,41 +429,57 @@ public static DataContextStack GetDataContextType(this DotvvmProperty property,
return dataContextType;
}

var (childType, extensionParameters) = ApplyDataContextChange(dataContextType, property.DataContextChangeAttributes, obj, property);
var (childType, extensionParameters, addLayer) = ApplyDataContextChange(dataContextType, property.DataContextChangeAttributes, obj, property);

if (!addLayer)
{
Debug.Assert(childType == dataContextType.DataContextType);
return DataContextStack.Create(dataContextType.DataContextType, dataContextType.Parent, dataContextType.NamespaceImports, extensionParameters.Concat(dataContextType.ExtensionParameters).ToArray(), dataContextType.BindingPropertyResolvers);
}

if (childType is null)
childType = typeof(UnknownTypeSentinel);

return DataContextStack.Create(childType, dataContextType, extensionParameters: extensionParameters.ToArray());
}

public static (Type? type, List<BindingExtensionParameter> extensionParameters) ApplyDataContextChange(DataContextStack dataContext, DataContextChangeAttribute[] attributes, ResolvedControl control, DotvvmProperty? property)
public static (Type? type, List<BindingExtensionParameter> extensionParameters, bool addLayer) ApplyDataContextChange(DataContextStack dataContext, DataContextChangeAttribute[] attributes, ResolvedControl control, DotvvmProperty? property)
{
var type = ResolvedTypeDescriptor.Create(dataContext.DataContextType);
var extensionParameters = new List<BindingExtensionParameter>();
var addLayer = false;
foreach (var attribute in attributes.OrderBy(a => a.Order))
{
if (type == null) break;
extensionParameters.AddRange(attribute.GetExtensionParameters(type));
type = attribute.GetChildDataContextType(type, dataContext, control, property);
if (attribute.NestDataContext)
{
addLayer = true;
type = attribute.GetChildDataContextType(type, dataContext, control, property);
}
}
return (ResolvedTypeDescriptor.ToSystemType(type), extensionParameters);
return (ResolvedTypeDescriptor.ToSystemType(type), extensionParameters, addLayer);
}


private static (Type? childType, List<BindingExtensionParameter> extensionParameters) ApplyDataContextChange(DataContextStack dataContextType, DataContextChangeAttribute[] attributes, DotvvmBindableObject obj, DotvvmProperty property)
private static (Type? childType, List<BindingExtensionParameter> extensionParameters, bool addLayer) ApplyDataContextChange(DataContextStack dataContextType, DataContextChangeAttribute[] attributes, DotvvmBindableObject obj, DotvvmProperty property)
{
Type? type = dataContextType.DataContextType;
var extensionParameters = new List<BindingExtensionParameter>();
var addLayer = false;

foreach (var attribute in attributes.OrderBy(a => a.Order))
{
if (type == null) break;
extensionParameters.AddRange(attribute.GetExtensionParameters(new ResolvedTypeDescriptor(type)));
type = attribute.GetChildDataContextType(type, dataContextType, obj, property);
if (attribute.NestDataContext)
{
addLayer = true;
type = attribute.GetChildDataContextType(type, dataContextType, obj, property);
}
}

return (type, extensionParameters);
return (type, extensionParameters, addLayer);
}

/// <summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Framework/Framework/Binding/DataContextChangeAttribute.cs
Expand Up @@ -8,6 +8,7 @@
using System.Linq.Expressions;
using DotVVM.Framework.Compilation.ControlTree;
using DotVVM.Framework.Compilation.ControlTree.Resolved;
using System.ComponentModel;

namespace DotVVM.Framework.Binding
{
Expand All @@ -26,6 +27,10 @@ public abstract class DataContextChangeAttribute : Attribute
/// Returning null means that the data context should not be changed. This overload is used at runtime, by `DotvvmProperty.GetDataContextType(DotvvmBindableObject)` helper method.
public abstract Type? GetChildDataContextType(Type dataContext, DataContextStack controlContextStack, DotvvmBindableObject control, DotvvmProperty? property = null);

/// Whether new layer of DataContext should be created, or the current one should be adjusted (extension parameters will be added).
[DefaultValue(true)]
public virtual bool NestDataContext => true;

/// Gets the extension parameters that should be made available to the bindings inside.
public virtual IEnumerable<BindingExtensionParameter> GetExtensionParameters(ITypeDescriptor dataContext) => Enumerable.Empty<BindingExtensionParameter>();

Expand Down
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using DotVVM.Core.Storage;
using DotVVM.Framework.Compilation.ControlTree;
using DotVVM.Framework.Compilation.ControlTree.Resolved;
using DotVVM.Framework.Compilation.Javascript.Ast;
Expand All @@ -24,7 +23,7 @@ public DataPagerExtensionParameter(string identifier, bool inherit = true) : bas
}

public override JsExpression GetJsTranslation(JsExpression dataContext) =>
new JsObjectExpression();
dataContext;
public override Expression GetServerEquivalent(Expression controlParameter) =>
Expression.New(typeof(DataPagerApi));
}
Expand All @@ -44,6 +43,7 @@ public AddParameterDataContextChangeAttribute(string name = "_dataPager", int or
dataContext;
public override Type? GetChildDataContextType(Type dataContext, DataContextStack controlContextStack, DotvvmBindableObject control, DotvvmProperty? property = null) => dataContext;

public override bool NestDataContext => false;
public override IEnumerable<BindingExtensionParameter> GetExtensionParameters(ITypeDescriptor dataContext)
{
return new BindingExtensionParameter[] {
Expand Down
Expand Up @@ -745,9 +745,10 @@ protected virtual bool IsControlProperty(IPropertyDescriptor property)

try
{
var (type, extensionParameters) = ApplyContextChange(dataContext, attributes, control, property);
var (type, extensionParameters, addLayer) = ApplyContextChange(dataContext, attributes, control, property);

if (type == null) return dataContext;
else if (!addLayer) return CreateDataContextTypeStack(dataContext.DataContextType, dataContext.Parent, dataContext.NamespaceImports, extensionParameters.Concat(dataContext.ExtensionParameters).ToArray());
else return CreateDataContextTypeStack(type, parentDataContextStack: dataContext, extensionParameters: extensionParameters.ToArray());
}
catch (Exception exception)
Expand All @@ -759,17 +760,23 @@ protected virtual bool IsControlProperty(IPropertyDescriptor property)
}
}

public static (ITypeDescriptor? type, List<BindingExtensionParameter> extensionParameters) ApplyContextChange(IDataContextStack dataContext, DataContextChangeAttribute[] attributes, IAbstractControl control, IPropertyDescriptor? property)
public static (ITypeDescriptor? type, List<BindingExtensionParameter> extensionParameters, bool addLayer) ApplyContextChange(IDataContextStack dataContext, DataContextChangeAttribute[] attributes, IAbstractControl control, IPropertyDescriptor? property)
{
var type = dataContext.DataContextType;
var extensionParameters = new List<BindingExtensionParameter>();
var addLayer = false;
foreach (var attribute in attributes.OrderBy(a => a.Order))
{
if (type == null) break;
extensionParameters.AddRange(attribute.GetExtensionParameters(type));
type = attribute.GetChildDataContextType(type, dataContext, control, property);
if (attribute.NestDataContext)
{
addLayer = true;
type = attribute.GetChildDataContextType(type, dataContext, control, property);
}

}
return (type, extensionParameters);
return (type, extensionParameters, addLayer);
}


Expand Down
Expand Up @@ -837,7 +837,6 @@ private void AddDefaultConvertTranslations()
private void AddDataSetOptionsTranslations()
{
// GridViewDataSetBindingProvider
var dataSetHelper = new JsSymbolicParameter(JavascriptTranslator.KnockoutContextParameter).Member("$gridViewDataSetHelper");
AddMethodTranslator(typeof(GridViewDataSetBindingProvider), nameof(GridViewDataSetBindingProvider.DataSetClientSideLoad), new GenericMethodCompiler(args =>
new JsIdentifierExpression("dotvvm").Member("dataSet").Member("loadDataSet").Invoke(
args[1].WithAnnotation(ShouldBeObservableAnnotation.Instance),
Expand All @@ -848,7 +847,7 @@ private void AddDataSetOptionsTranslations()

// _dataPager.Load()
AddMethodTranslator(() => default(DataPagerApi)!.Load(), new GenericMethodCompiler(args =>
dataSetHelper.Clone().Member("loadNextPage").Invoke().WithAnnotation(new ResultIsPromiseAnnotation(e => e))));
args[0].Member("$gridViewDataSetHelper").Member("loadNextPage").Invoke().WithAnnotation(new ResultIsPromiseAnnotation(e => e))));

// PagingOptions
AddMethodTranslator(() => default(PagingOptions)!.GoToFirstPage(),new GenericMethodCompiler(args =>
Expand Down
5 changes: 4 additions & 1 deletion src/Framework/Framework/Controls/AppendableDataPager.cs
Expand Up @@ -72,7 +72,10 @@ protected internal override void OnLoad(IDotvvmRequestContext context)

if (LoadTemplate != null)
{
LoadTemplate.BuildContent(context, this);
var container = new PlaceHolder();
container.SetDataContextType(LoadTemplateProperty.GetDataContextType(this));
LoadTemplate.BuildContent(context, container);
Children.Add(container);
}

if (EndTemplate != null)
Expand Down
2 changes: 0 additions & 2 deletions src/Framework/Framework/Controls/DataPager.cs
Expand Up @@ -191,8 +191,6 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context)
{
// number fields
var liTemplate = new HtmlGenericControl("li");
// li.SetDataContextType(currentPageTextContext);
// li.SetBinding(DataContextProperty, GetNearIndexesBinding(context, i, dataContextType));
liTemplate.CssClasses.Add(ActiveItemCssClass, new ValueOrBinding<bool>(pagerBindings.IsActivePage.NotNull()));
var link = new LinkButton();
link.SetBinding(ButtonBase.ClickProperty, pagerBindings.GoToPage.NotNull());
Expand Down
4 changes: 4 additions & 0 deletions src/Framework/Framework/Controls/KnockoutBindingGroup.cs
Expand Up @@ -87,7 +87,11 @@ public override string ToString()
if (entries.Count == 0) return "{}";
bool multiline = false;
foreach (var entry in entries)
#if DotNetCore
if (entry.Expression.Contains('\n'))
#else
if (entry.Expression.Contains("\n"))
#endif
{
multiline = true;
break;
Expand Down
54 changes: 52 additions & 2 deletions src/Tests/ControlTests/DataPagerTests.cs
Expand Up @@ -16,6 +16,7 @@
using DotVVM.Framework.Binding;
using FastExpressionCompiler;
using DotVVM.Framework.Binding.Expressions;
using System.Reflection.Metadata;

namespace DotVVM.Framework.Tests.ControlTests
{
Expand All @@ -29,9 +30,9 @@ public class DataPagerTests
[TestMethod]
public async Task CommandDataPager()
{
var r = await cth.RunPage(typeof(GridViewModel), @"
var r = await cth.RunPage(typeof(GridViewModel), """
<dot:DataPager DataSet={value: Customers} />
"
"""
);

var commandExpressions = r.Commands
Expand All @@ -51,6 +52,49 @@ public async Task CommandDataPager()
Assert.AreEqual(1, (int)r.ViewModel.Customers.PagingOptions.PageIndex);
}

[TestMethod]
public async Task StaticCommandPager()
{
var r = await cth.RunPage(typeof(GridViewModel), """
<dot:DataPager DataSet={value: Customers} LoadData={staticCommand: RootViewModel.LoadCustomers} />
"""
);
check.CheckString(r.FormattedHtml, fileExtension: "html");
}

[TestMethod]
public async Task StaticCommandApendablePager()
{
var r = await cth.RunPage(typeof(GridViewModel), """
<dot:AppendableDataPager DataSet={value: Customers} LoadData={staticCommand: RootViewModel.LoadCustomers}>
<LoadTemplate>
<div DataContext={value: 1}>
<dot:Button Text="Load more" Click="{staticCommand: _dataPager.Load()}" />
</div>
</LoadTemplate>
<EndTemplate> end </EndTemplate>
</dot:AppendableDataPager>
"""
);

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

var commandExpressions = r.Commands
.Select(c => (c.control, c.command, str: c.command.GetProperty<ParsedExpressionBindingProperty>().Expression.ToCSharpString().Trim().TrimEnd(';')))
.OrderBy(c => c.str)
.ToArray();
check.CheckLines(commandExpressions.GroupBy(c => c.command).Select(c => c.First().str), checkName: "command-bindings", fileExtension: "txt");

var nextPage = commandExpressions.Single(c => c.str.Contains(".GoToNextPage()"));
var prevPage = commandExpressions.Single(c => c.str.Contains(".GoToPreviousPage()"));
var firstPage = commandExpressions.Single(c => c.str.Contains(".GoToFirstPage()"));
var lastPage = commandExpressions.Single(c => c.str.Contains(".GoToLastPage()"));

await r.RunCommand((CommandBindingExpression)nextPage.command, nextPage.control);
Assert.AreEqual(1, (int)r.ViewModel.Customers.PagingOptions.PageIndex);

}

public class GridViewModel: DotvvmViewModelBase
{
public GridViewDataSet<CustomerData> Customers { get; set; } = new GridViewDataSet<CustomerData>()
Expand All @@ -77,6 +121,12 @@ public class CustomerData
[Required]
public string Name { get; set; }
}

[AllowStaticCommand]
public static GridViewDataSetResult<CustomerData, NoFilteringOptions, SortingOptions, PagingOptions> LoadCustomers(GridViewDataSetOptions request)
{
throw new NotImplementedException();
}
}
}
}
@@ -0,0 +1,25 @@
<html>
<head></head>
<body>
<div data-bind="dotvvm-gridviewdataset: {
dataSet: Customers(),
loadNextPage: (...args)=>(dotvvm.applyPostbackHandlers(async (options) => {
let cx = $context;
return await dotvvm.dataSet.loadDataSet(options.viewModel.Customers, (options) => dotvvm.dataSet.translations.PagingOptions.goToNextPage(ko.unwrap(options).PagingOptions), ((...args)=>(dotvvm.applyPostbackHandlers(async (options) => await dotvvm.staticCommandPostback(&quot;WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkNvbnRyb2xUZXN0cy5EYXRhUGFnZXJUZXN0cytHcmlkVmlld01vZGVsLCBEb3RWVk0uRnJhbWV3b3JrLlRlc3RzIiwiTG9hZEN1c3RvbWVycyIsW10sMSxudWxsLCJBQT09Il0=&quot;, [args[0]], options),$element,[],args,$context))), dotvvm.dataSet.postProcessors.append);
},$element,[],args,$context)),
postProcessor: dotvvm.dataSet.postProcessors.append
}">

<!-- ko with: 1 -->
<div>
<input onclick="dotvvm.applyPostbackHandlers(async (options) => {
await options.knockoutContext.$parentContext.$gridViewDataSetHelper.loadNextPage();
},this).catch(dotvvm.log.logPostBackScriptError);event.stopPropagation();return false;" type="button" value="Load more">
</div>
<!-- /ko -->
<div data-bind="visible: Customers()?.PagingOptions()?.IsLastPage" style="display:none">
end
</div>
</div>
</body>
</html>

0 comments on commit 3324bf6

Please sign in to comment.