Skip to content

Commit

Permalink
AppendableDataPager: make it work in automatic mode
Browse files Browse the repository at this point in the history
* introduced symbolic parameters for $gridViewDataSet.loadData function.
   we need to substitute this loadData function on a single element, so
   we cannot use dotvvm-gridviewdataset to change the data context.
* refactored PostbackOptions to a record to allow using `with { ... }` syntax
  • Loading branch information
exyi committed Nov 19, 2023
1 parent 4eff8ca commit 5ed8388
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -817,12 +817,9 @@ private void AddDataSetOptionsTranslations()
new JsIdentifierExpression("dotvvm").Member("dataSet").Member("loadDataSet").Invoke(
args[1].WithAnnotation(ShouldBeObservableAnnotation.Instance),
args[2],
dataSetHelper.Clone().Member("loadDataSet"),
dataSetHelper.Clone().Member("postProcessor")
GridViewDataSetBindingProvider.LoadDataDelegate.ToExpression(),
GridViewDataSetBindingProvider.PostProcessorDelegate.ToExpression()
).WithAnnotation(new ResultIsPromiseAnnotation(e => e))));
AddMethodTranslator(() => GridViewDataSetBindingProvider.GetCurrentGridDataSet<Generic.DataSet>(), new GenericMethodCompiler(args =>
dataSetHelper.Clone().Member("dataSet")
));

// _dataPager.Load()
AddMethodTranslator(() => default(DataPagerApi)!.Load(), new GenericMethodCompiler(args =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
Expand Down Expand Up @@ -56,7 +56,8 @@ public override string ToString()
return Precedence + (IsPreferredSide ? "+" : "-") + " (" + name + ")";
}

public static readonly OperatorPrecedence Max = new OperatorPrecedence(20, true);
/// <summary> Assume that the expression is an atomic </summary>
public static OperatorPrecedence Max => new OperatorPrecedence(20, true);

/// <summary> atomic expression, like `x`, `(x + y)`, `0`, `{"f": 123}`, `x[1]`, ... </summary>
public const byte Atomic = 20;
Expand Down
37 changes: 26 additions & 11 deletions src/Framework/Framework/Controls/AppendableDataPager.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using System;
using System;
using System.Collections.Generic;
using System.Text;
using DotVVM.Framework.Binding;
using DotVVM.Framework.Binding.Expressions;
using DotVVM.Framework.Binding.HelperNamespace;
using DotVVM.Framework.Compilation.Javascript;
using DotVVM.Framework.Hosting;
using DotVVM.Framework.Utils;

namespace DotVVM.Framework.Controls
{
Expand Down Expand Up @@ -45,6 +46,7 @@ public IPageableGridViewDataSet DataSet
public static readonly DotvvmProperty DataSetProperty
= DotvvmProperty.Register<IPageableGridViewDataSet, AppendableDataPager>(c => c.DataSet, null);

[MarkupOptions(Required = true)]
public ICommandBinding? LoadData
{
get => (ICommandBinding?)GetValue(LoadDataProperty);
Expand Down Expand Up @@ -87,19 +89,32 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest
{
var dataSetBinding = GetDataSetBinding().GetKnockoutBindingExpression(this, unwrapped: true);

var helperBinding = new KnockoutBindingGroup();
helperBinding.Add("dataSet", dataSetBinding);
if (this.LoadData is { } loadData)
var loadData = this.LoadData.NotNull("AppendableDataPager.LoadData is currently required.");
var loadDataCore = KnockoutHelper.GenerateClientPostbackLambda("LoadDataCore", loadData, this, PostbackScriptOptions.KnockoutBinding with { AllowPostbackHandlers = false });
var loadNextPage = KnockoutHelper.GenerateClientPostbackLambda("LoadData", dataPagerCommands!.GoToNextPage!, this, PostbackScriptOptions.KnockoutBinding with {
ParameterAssignment = p =>
p == GridViewDataSetBindingProvider.LoadDataDelegate ? new CodeParameterAssignment(loadDataCore, default) :
p == GridViewDataSetBindingProvider.PostProcessorDelegate ? new CodeParameterAssignment("dotvvm.dataSet.postProcessors.append", OperatorPrecedence.Max) :
default
});

if (LoadTemplate is null)
{
var binding = new KnockoutBindingGroup();
binding.Add("dataSet", dataSetBinding);
binding.Add("loadNextPage", loadNextPage);
binding.Add("autoLoadWhenInViewport", "true");
writer.AddKnockoutDataBind("dotvvm-appendable-data-pager", binding);
}
else
{
helperBinding.Add("loadDataSet", KnockoutHelper.GenerateClientPostbackLambda("LoadDataCore", loadData, this, new PostbackScriptOptions(elementAccessor: "$element", koContext: CodeParameterAssignment.FromIdentifier("$context"))));
helperBinding.Add("loadNextPage", KnockoutHelper.GenerateClientPostbackLambda("LoadData", dataPagerCommands!.GoToNextPage!, this));
var helperBinding = new KnockoutBindingGroup();
helperBinding.Add("dataSet", dataSetBinding);
// helperBinding.Add("loadDataSet", KnockoutHelper.GenerateClientPostbackLambda("LoadDataCore", loadData, this, PostbackScriptOptions.KnockoutBinding);
helperBinding.Add("loadNextPage", loadNextPage);
helperBinding.Add("postProcessor", "dotvvm.dataSet.postProcessors.append");
writer.AddKnockoutDataBind("dotvvm-gridviewdataset", helperBinding.ToString());
}
writer.AddKnockoutDataBind("dotvvm-gridviewdataset", helperBinding.ToString());

var binding = new KnockoutBindingGroup();
binding.Add("autoLoadWhenInViewport", LoadTemplate == null ? "true" : "false");
writer.AddKnockoutDataBind("dotvvm-appendable-data-pager", binding);

base.AddAttributesToRender(writer, context);
}
Expand Down
21 changes: 10 additions & 11 deletions src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
using DotVVM.Framework.Binding.Properties;
using DotVVM.Framework.Compilation;
using DotVVM.Framework.Compilation.ControlTree;
using DotVVM.Framework.Compilation.Javascript;
using DotVVM.Framework.Compilation.Javascript.Ast;
using DotVVM.Framework.Utils;

namespace DotVVM.Framework.Controls;
Expand Down Expand Up @@ -254,11 +256,14 @@ private ICommandBinding CreateLoadDataDelegateCommandBinding<TDataSetInterface>(
});
}

private static Expression CurrentGridDataSetExpression(Type datasetType)
{
var method = typeof(GridViewDataSetBindingProvider).GetMethod(nameof(GetCurrentGridDataSet))!.MakeGenericMethod(datasetType);
return Expression.Call(method);
}
public static CodeSymbolicParameter LoadDataDelegate = new CodeSymbolicParameter(
"LoadDataDelegate",
CodeParameterAssignment.FromExpression(JavascriptTranslator.KnockoutContextParameter.ToExpression().Member("$gridViewDataSetHelper").Member("loadDataSet"))
);
public static CodeSymbolicParameter PostProcessorDelegate = new CodeSymbolicParameter(
"PostProcessorDelegate",
CodeParameterAssignment.FromExpression(JavascriptTranslator.KnockoutContextParameter.ToExpression().Member("$gridViewDataSetHelper").Member("postProcessor"))
);

/// <summary>
/// A sentinel method which is translated to load the GridViewDataSet on the client side using the Load delegate.
Expand All @@ -276,12 +281,6 @@ public static Task DataSetClientSideLoad<TGridViewDataSet, TItem, TFilteringOpti
throw new InvalidOperationException("This method cannot be called on the server!");
}

/// <summary> Returns the DataSet we currently work on from the $context.$gridViewDataSetHelper.dataSet </summary>
public static T GetCurrentGridDataSet<T>() where T : IBaseGridViewDataSet
{
throw new InvalidOperationException("This method cannot be called on the server!");
}

private static Type GetOptionsConcreteType<TDataSetInterface>(Type dataSetConcreteType, out PropertyInfo optionsProperty)
{
if (!typeof(TDataSetInterface).IsAssignableFrom(dataSetConcreteType))
Expand Down
18 changes: 11 additions & 7 deletions src/Framework/Framework/Controls/KnockoutHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,20 +114,20 @@ public static void AddKnockoutForeachDataBind(this IHtmlWriter writer, string ex
/// <summary> Generates a function expression that invokes the command with specified commandArguments. Creates code like `(...commandArguments) => dotvvm.postBack(...)` </summary>
public static string GenerateClientPostbackLambda(string propertyName, ICommandBinding command, DotvvmBindableObject control, PostbackScriptOptions? options = null)
{
options ??= new PostbackScriptOptions(
elementAccessor: "$element",
koContext: CodeParameterAssignment.FromIdentifier("$context", true)
);
options ??= PostbackScriptOptions.KnockoutBinding;

var hasArguments = command is IStaticCommandBinding || command.CommandJavascript.EnumerateAllParameters().Any(p => p == CommandBindingExpression.CommandArgumentsParameter);
options.CommandArgs = hasArguments ? new CodeParameterAssignment(new ParametrizedCode("args", OperatorPrecedence.Max)) : default;
var addArguments = options.CommandArgs is null && (command is IStaticCommandBinding || command.CommandJavascript.EnumerateAllParameters().Any(p => p == CommandBindingExpression.CommandArgumentsParameter));
if (addArguments)
{
options = options with { CommandArgs = new CodeParameterAssignment(new ParametrizedCode("args", OperatorPrecedence.Max)) };
}
// just few commands have arguments so it's worth checking if we need to clutter the output with argument propagation
var call = KnockoutHelper.GenerateClientPostBackExpression(
propertyName,
command,
control,
options);
return hasArguments ? $"(...args)=>({call})" : $"()=>({call})";
return addArguments ? $"(...args)=>({call})" : $"()=>({call})";
}

/// <summary> Generates Javascript code which executes the specified command binding <paramref name="expression" />. </summary>
Expand Down Expand Up @@ -205,6 +205,10 @@ string getHandlerScript()
var adjustedExpression =
JavascriptTranslator.AdjustKnockoutScriptContext(jsExpression,
dataContextLevel: expression.FindDataContextTarget(control).stepsUp);
if (options.ParameterAssignment is {})
{
adjustedExpression = adjustedExpression.AssignParameters(options.ParameterAssignment);
}
// when the expression changes the dataContext, we need to override the default knockout context fo the command binding.
CodeParameterAssignment knockoutContext;
CodeParameterAssignment viewModel = default;
Expand Down
47 changes: 36 additions & 11 deletions src/Framework/Framework/Controls/PostbackScriptOptions.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
using System;
using System.Collections.Generic;
using System.Text;
using DotVVM.Framework.Compilation.Javascript;

namespace DotVVM.Framework.Controls
{
/// <summary> Options for the <see cref="KnockoutHelper.GenerateClientPostBackExpression(string, Binding.Expressions.ICommandBinding, DotvvmBindableObject, PostbackScriptOptions)" /> method. </summary>
public class PostbackScriptOptions
public sealed record PostbackScriptOptions
{
/// <summary>If true, the command invocation will be wrapped in window.setTimeout with timeout 0. This is necessary for some event handlers, when the handler is invoked before the change is actually applied.</summary>
public bool UseWindowSetTimeout { get; set; }
public bool UseWindowSetTimeout { get; init; }
/// <summary>Return value of the event handler. If set to false, the script will also include event.stopPropagation()</summary>
public bool? ReturnValue { get; set; }
public bool IsOnChange { get; set; }
public bool? ReturnValue { get; init; }
public bool IsOnChange { get; init; }
/// <summary>Javascript variable where the sender element can be found. Set to $element when in knockout binding.</summary>
public CodeParameterAssignment ElementAccessor { get; set; }
public CodeParameterAssignment ElementAccessor { get; init; }
/// <summary>Javascript variable current knockout binding context can be found. By default, `ko.contextFor({elementAccessor})` is used</summary>
public CodeParameterAssignment? KoContext { get; set; }
public CodeParameterAssignment? KoContext { get; init; }
/// <summary>Javascript expression returning an array of command arguments.</summary>
public CodeParameterAssignment? CommandArgs { get; set; }
public CodeParameterAssignment? CommandArgs { get; init; }
/// <summary>When set to false, postback handlers will not be invoked for this command.</summary>
public bool AllowPostbackHandlers { get; }
public bool AllowPostbackHandlers { get; init; }
/// <summary>Javascript expression returning <see href="https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal">AbortSignal</see> which can be used to cancel the postback (it's a JS variant of CancellationToken). </summary>
public CodeParameterAssignment? AbortSignal { get; }
public CodeParameterAssignment? AbortSignal { get; init; }
public Func<CodeSymbolicParameter, CodeParameterAssignment>? ParameterAssignment { get; init; }

/// <param name="useWindowSetTimeout">If true, the command invocation will be wrapped in window.setTimeout with timeout 0. This is necessary for some event handlers, when the handler is invoked before the change is actually applied.</param>
/// <param name="returnValue">Return value of the event handler. If set to false, the script will also include event.stopPropagation()</param>
Expand All @@ -36,7 +40,8 @@ public PostbackScriptOptions(bool useWindowSetTimeout = false,
CodeParameterAssignment? koContext = null,
CodeParameterAssignment? commandArgs = null,
bool allowPostbackHandlers = true,
CodeParameterAssignment? abortSignal = null)
CodeParameterAssignment? abortSignal = null,
Func<CodeSymbolicParameter, CodeParameterAssignment>? parameterAssignment = null)
{
this.UseWindowSetTimeout = useWindowSetTimeout;
this.ReturnValue = returnValue;
Expand All @@ -45,7 +50,27 @@ public PostbackScriptOptions(bool useWindowSetTimeout = false,
this.KoContext = koContext;
this.CommandArgs = commandArgs;
this.AllowPostbackHandlers = allowPostbackHandlers;
AbortSignal = abortSignal;
this.AbortSignal = abortSignal;
this.ParameterAssignment = parameterAssignment;
}

/// <summary> Default postback options, optimal for placing the script into a `onxxx` event attribute. </summary>
public static readonly PostbackScriptOptions JsEvent = new PostbackScriptOptions();
public static readonly PostbackScriptOptions KnockoutBinding = new PostbackScriptOptions(elementAccessor: "$element", koContext: new CodeParameterAssignment("$context", OperatorPrecedence.Max, isGlobalContext: true));

public override string ToString()
{
var fields = new List<string>();
if (UseWindowSetTimeout) fields.Add("useWindowSetTimeout: true");
if (ReturnValue != false) fields.Add($"returnValue: {(ReturnValue == true ? "true" : "null")}");
if (IsOnChange) fields.Add("isOnChange: true");
if (ElementAccessor.ToString() != "this") fields.Add($"elementAccessor: \"{ElementAccessor}\"");
if (KoContext != null) fields.Add($"koContext: \"{KoContext}\"");
if (CommandArgs != null) fields.Add($"commandArgs: \"{CommandArgs}\"");
if (!AllowPostbackHandlers) fields.Add("allowPostbackHandlers: false");
if (AbortSignal != null) fields.Add($"abortSignal: \"{AbortSignal}\"");
if (ParameterAssignment != null) fields.Add($"parameterAssignment: \"{ParameterAssignment}\"");
return new StringBuilder("new PostbackScriptOptions(").AppendJoin(", ", fields.ToArray()).Append(")").ToString();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
type AppendableDataPagerBinding = {
import { getStateManager } from "../dotvvm-base";

type AppendableDataPagerBinding = {
autoLoadWhenInViewport: boolean,
loadNextPage: () => Promise<any>
loadNextPage: () => Promise<any>,
dataSet: any
};

export default {
Expand All @@ -15,16 +18,18 @@ export default {
if (isLoading) return;

let entry = entries[0];
while (entry.isIntersecting) {
const dataSet = allBindingsAccessor.get("dotvvm-gridviewdataset").dataSet as DotvvmObservable<any>;
if (dataSet.state.PagingOptions.IsLastPage) {
while (entry?.isIntersecting) {
const dataSet = valueAccessor().dataSet;
if (dataSet.PagingOptions().IsLastPage()) {
return;
}

isLoading = true;
try {
await binding.loadNextPage();

// getStateManager().doUpdateNow();

// when the loading was finished, check whether we need to load another page
entry = observer.takeRecords()[0];
}
Expand Down

0 comments on commit 5ed8388

Please sign in to comment.