From 06afecf600390b83d916cec24d21707e05f71028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 19 Nov 2023 14:56:12 +0100 Subject: [PATCH] AppendableDataPager added --- .../JavascriptTranslatableMethodCollection.cs | 12 +- .../Framework/Controls/AppendableDataPager.cs | 110 ++++++++++++ src/Framework/Framework/Controls/DataPager.cs | 3 +- src/Framework/Framework/Controls/GridView.cs | 2 +- .../GridViewDataSetBindingProvider.cs | 163 ++++++++++++------ .../Controls/GridViewDataSetCommandType.cs | 4 +- .../Scripts/binding-handlers/all-handlers.ts | 4 +- .../binding-handlers/appendable-data-pager.ts | 41 +++++ .../Resources/Scripts/dataset/loader.ts | 65 ++++--- .../Resources/Scripts/dataset/translations.ts | 78 ++++----- .../Resources/Scripts/dotvvm-root.ts | 3 +- .../Resources/Scripts/state-manager.ts | 4 +- .../Common/DotVVM.Samples.Common.csproj | 1 + .../AppendableDataPagerViewModel.cs | 58 +++++++ .../AppendableDataPager.dothtml | 34 ++++ .../GridView/GridViewStaticCommand.dothtml | 4 +- src/Tests/ViewModel/GridViewDataSetTests.cs | 6 +- 17 files changed, 448 insertions(+), 144 deletions(-) create mode 100644 src/Framework/Framework/Controls/AppendableDataPager.cs create mode 100644 src/Framework/Framework/Resources/Scripts/binding-handlers/appendable-data-pager.ts create mode 100644 src/Samples/Common/ViewModels/ControlSamples/AppendableDataPager/AppendableDataPagerViewModel.cs create mode 100644 src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPager.dothtml diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index b5c5a3ed53..43f41f370a 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -811,11 +811,15 @@ JsExpression wrapInRound(JsExpression a) => private void AddDataSetOptionsTranslations() { - var dataSetHelper = new JsSymbolicParameter(JavascriptTranslator.KnockoutContextParameter).Member("$gridViewDataSetHelper"); // GridViewDataSetBindingProvider - AddMethodTranslator(() => GridViewDataSetBindingProvider.DataSetClientSideLoad(null!), new GenericMethodCompiler(args => - new JsIdentifierExpression("dotvvm").Member("dataSet").Member("loadDataSet").Invoke(args[1].WithAnnotation(ShouldBeObservableAnnotation.Instance), dataSetHelper.Clone().Member("loadDataSet")).WithAnnotation(new ResultIsPromiseAnnotation(e => e)))); - + 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), + args[2], + dataSetHelper.Clone().Member("loadDataSet"), + dataSetHelper.Clone().Member("postProcessor") + ).WithAnnotation(new ResultIsPromiseAnnotation(e => e)))); AddMethodTranslator(() => GridViewDataSetBindingProvider.GetCurrentGridDataSet(), new GenericMethodCompiler(args => dataSetHelper.Clone().Member("dataSet") )); diff --git a/src/Framework/Framework/Controls/AppendableDataPager.cs b/src/Framework/Framework/Controls/AppendableDataPager.cs new file mode 100644 index 0000000000..1433895eb0 --- /dev/null +++ b/src/Framework/Framework/Controls/AppendableDataPager.cs @@ -0,0 +1,110 @@ +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; + +namespace DotVVM.Framework.Controls +{ + /// + /// Renders a pager for that allows the user to append more items to the end of the list. + /// + [ControlMarkupOptions(AllowContent = false, DefaultContentProperty = nameof(LoadTemplate))] + public class AppendableDataPager : HtmlGenericControl + { + private readonly GridViewDataSetBindingProvider gridViewDataSetBindingProvider; + + [MarkupOptions(AllowBinding = false, MappingMode = MappingMode.InnerElement)] + [DataPagerApi.AddParameterDataContextChange("_dataPager")] + public ITemplate? LoadTemplate + { + get { return (ITemplate?)GetValue(LoadTemplateProperty); } + set { SetValue(LoadTemplateProperty, value); } + } + public static readonly DotvvmProperty LoadTemplateProperty + = DotvvmProperty.Register(c => c.LoadTemplate, null); + + [MarkupOptions(AllowBinding = false, MappingMode = MappingMode.InnerElement)] + public ITemplate? EndTemplate + { + get { return (ITemplate?)GetValue(EndTemplateProperty); } + set { SetValue(EndTemplateProperty, value); } + } + public static readonly DotvvmProperty EndTemplateProperty + = DotvvmProperty.Register(c => c.EndTemplate, null); + + [MarkupOptions(Required = true, AllowHardCodedValue = false)] + public IPageableGridViewDataSet DataSet + { + get { return (IPageableGridViewDataSet)GetValue(DataSetProperty)!; } + set { SetValue(DataSetProperty, value); } + } + public static readonly DotvvmProperty DataSetProperty + = DotvvmProperty.Register(c => c.DataSet, null); + + public ICommandBinding? LoadData + { + get => (ICommandBinding?)GetValue(LoadDataProperty); + set => SetValue(LoadDataProperty, value); + } + public static readonly DotvvmProperty LoadDataProperty = + DotvvmProperty.Register(nameof(LoadData)); + + + private DataPagerBindings? dataPagerCommands = null; + + + public AppendableDataPager(GridViewDataSetBindingProvider gridViewDataSetBindingProvider) : base("div") + { + this.gridViewDataSetBindingProvider = gridViewDataSetBindingProvider; + } + + protected internal override void OnLoad(IDotvvmRequestContext context) + { + var dataSetBinding = GetValueBinding(DataSetProperty)!; + var commandType = LoadData is { } ? GridViewDataSetCommandType.LoadDataDelegate : GridViewDataSetCommandType.Default; + dataPagerCommands = gridViewDataSetBindingProvider.GetDataPagerCommands(this.GetDataContextType()!, dataSetBinding, commandType); + + if (LoadTemplate != null) + { + LoadTemplate.BuildContent(context, this); + } + + if (EndTemplate != null) + { + var container = new HtmlGenericControl("div") + .SetProperty(p => p.Visible, dataPagerCommands.IsLastPage); + Children.Add(container); + + EndTemplate.BuildContent(context, container); + } + } + + protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context) + { + var dataSetBinding = GetDataSetBinding().GetKnockoutBindingExpression(this, unwrapped: true); + + var helperBinding = new KnockoutBindingGroup(); + helperBinding.Add("dataSet", dataSetBinding); + if (this.LoadData is { } loadData) + { + 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)); + helperBinding.Add("postProcessor", "dotvvm.dataSet.postProcessors.append"); + } + 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); + } + + private IValueBinding GetDataSetBinding() + => GetValueBinding(DataSetProperty) ?? throw new DotvvmControlException(this, "The DataSet property of the dot:DataPager control must be set!"); + } +} diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index 1c7a3d2165..1d85221e90 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -162,7 +162,8 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) var dataSetBinding = GetValueBinding(DataSetProperty)!; var dataSetType = dataSetBinding.ResultType; - var commandType = LoadData is {} ? GridViewDataSetCommandType.StaticCommand : GridViewDataSetCommandType.Command; + var commandType = LoadData is {} ? GridViewDataSetCommandType.LoadDataDelegate : GridViewDataSetCommandType.Default; + pagerBindings = gridViewDataSetBindingProvider.GetDataPagerCommands(this.GetDataContextType().NotNull(), dataSetBinding, commandType); var enabled = GetValueOrBinding(EnabledProperty)!; diff --git a/src/Framework/Framework/Controls/GridView.cs b/src/Framework/Framework/Controls/GridView.cs index 271bc02dd8..257eda1ac3 100644 --- a/src/Framework/Framework/Controls/GridView.cs +++ b/src/Framework/Framework/Controls/GridView.cs @@ -322,7 +322,7 @@ protected virtual ICommandBinding BuildDefaultSortCommandBinding() protected virtual ICommandBinding? BuildLoadDataSortCommandBinding() { var dataContextStack = this.GetDataContextType()!; - var commandType = LoadData is { } ? GridViewDataSetCommandType.StaticCommand : GridViewDataSetCommandType.Command; + var commandType = LoadData is { } ? GridViewDataSetCommandType.LoadDataDelegate : GridViewDataSetCommandType.Default; return gridViewDataSetBindingProvider.GetGridViewCommands(dataContextStack, GetDataSourceBinding(), commandType).SetSortExpression; } diff --git a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs index e12794d0d2..80a8095224 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -10,6 +10,7 @@ using DotVVM.Framework.Binding.Properties; using DotVVM.Framework.Compilation; using DotVVM.Framework.Compilation.ControlTree; +using DotVVM.Framework.Utils; namespace DotVVM.Framework.Controls; @@ -148,6 +149,24 @@ private GridViewCommands GetGridViewCommandsCore(DataContextStack dataContextSta } private ICommandBinding CreateCommandBinding(GridViewDataSetCommandType commandType, Expression dataSet, DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression = null) + { + if (commandType == GridViewDataSetCommandType.Default) + { + // create a binding {command: dataSet.XXXOptions.YYY(args); dataSet.RequestRefresh() } + return CreateDefaultCommandBinding(dataSet, dataContextStack, methodName, arguments, transformExpression); + } + else if (commandType == GridViewDataSetCommandType.LoadDataDelegate) + { + // create a binding {staticCommand: GridViewDataSetBindingProvider.LoadDataSet(dataSet, options => { options.XXXOptions.YYY(args); }, loadDataDelegate, postProcessor) } + return CreateLoadDataDelegateCommandBinding(dataSet, dataContextStack, methodName, arguments, transformExpression); + } + else + { + throw new NotSupportedException($"The command type {commandType} is not supported!"); + } + } + + private ICommandBinding CreateDefaultCommandBinding(Expression dataSet, DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression) { var body = new List(); @@ -161,63 +180,78 @@ private ICommandBinding CreateCommandBinding(GridViewDataSetC arguments); body.Add(callMethodOnOptions); - if (commandType == GridViewDataSetCommandType.Command) - { - // if we are on a server, call the dataSet.RequestRefresh if supported - if (typeof(IRefreshableGridViewDataSet).IsAssignableFrom(dataSet.Type)) - { - var callRequestRefresh = Expression.Call( - Expression.Convert(dataSet, typeof(IRefreshableGridViewDataSet)), - typeof(IRefreshableGridViewDataSet).GetMethod(nameof(IRefreshableGridViewDataSet.RequestRefresh))! - ); - body.Add(callRequestRefresh); - } - - // build command binding - Expression expression = Expression.Block(body); - if (transformExpression != null) - { - expression = transformExpression(expression); - } - return new CommandBindingExpression(service, - new object[] - { - new ParsedExpressionBindingProperty(expression), - new OriginalStringBindingProperty($"DataPager: _dataSet.{methodName}({string.Join(", ", arguments.AsEnumerable())})"), // For ID generation - dataContextStack - }); - } - else if (commandType == GridViewDataSetCommandType.StaticCommand) + // if we are on a server, call the dataSet.RequestRefresh if supported + if (typeof(IRefreshableGridViewDataSet).IsAssignableFrom(dataSet.Type)) { - // on the client, wrap the call into client-side loading procedure - body.Add(CallClientSideLoad(dataSet)); - Expression expression = Expression.Block(body); - if (transformExpression != null) - { - expression = transformExpression(expression); - } - return new StaticCommandBindingExpression(service, - new object[] - { - new ParsedExpressionBindingProperty(expression), - BindingParserOptions.StaticCommand, - dataContextStack - }); + var callRequestRefresh = Expression.Call( + Expression.Convert(dataSet, typeof(IRefreshableGridViewDataSet)), + typeof(IRefreshableGridViewDataSet).GetMethod(nameof(IRefreshableGridViewDataSet.RequestRefresh))! + ); + body.Add(callRequestRefresh); } - else + + // build command binding + Expression expression = Expression.Block(body); + if (transformExpression != null) { - throw new NotSupportedException($"The command type {commandType} is not supported!"); + expression = transformExpression(expression); } + + return new CommandBindingExpression(service, + new object[] + { + new ParsedExpressionBindingProperty(expression), + new OriginalStringBindingProperty($"DataPager: _dataSet.{methodName}({string.Join(", ", arguments.AsEnumerable())})"), // For ID generation + dataContextStack + }); } - - /// - /// Invoked the client-side loadDataSet function with the loader from $gridViewDataSetHelper - /// - private static Expression CallClientSideLoad(Expression dataSetParam) + + private ICommandBinding CreateLoadDataDelegateCommandBinding(Expression dataSet, DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression) { - // call static method DataSetClientLoad - var method = typeof(GridViewDataSetBindingProvider).GetMethod(nameof(DataSetClientSideLoad))!; - return Expression.Call(method, dataSetParam); + var loadDataSetMethod = typeof(GridViewDataSetBindingProvider).GetMethod(nameof(DataSetClientSideLoad))!; + + // build the concrete type of GridViewDataSetOptions<,,> + GetOptionsConcreteType(dataSet.Type, out var filteringOptionsProperty); + GetOptionsConcreteType(dataSet.Type, out var sortingOptionsProperty); + GetOptionsConcreteType(dataSet.Type, out var pagingOptionsProperty); + GetOptionsConcreteType(dataSet.Type, out var itemProperty); + var itemType = itemProperty.PropertyType.GetEnumerableType()!; + + var optionsType = typeof(GridViewDataSetOptions<,,>).MakeGenericType(filteringOptionsProperty.PropertyType, sortingOptionsProperty.PropertyType, pagingOptionsProperty.PropertyType); + var resultType = typeof(GridViewDataSetResult<,,,>).MakeGenericType(itemType, filteringOptionsProperty.PropertyType, sortingOptionsProperty.PropertyType, pagingOptionsProperty.PropertyType); + + // get concrete type from implementation of IXXXableGridViewDataSet + var modifiedOptionsType = GetOptionsConcreteType(dataSet.Type, out var modifiedOptionsProperty); + + // call options.XXXOptions.Method(...); + var optionsParameter = Expression.Parameter(optionsType, "options"); + var callMethodOnOptions = Expression.Call( + Expression.Convert(Expression.Property(optionsParameter, modifiedOptionsProperty.Name), modifiedOptionsType), + modifiedOptionsType.GetMethod(methodName)!, + arguments); + + // build options => options.XXXOptions.Method(...) + var optionsTransformLambdaType = typeof(Action<>).MakeGenericType(optionsType); + var optionsTransformLambda = Expression.Lambda(optionsTransformLambdaType, callMethodOnOptions, optionsParameter); + + var expression = (Expression)Expression.Call( + loadDataSetMethod.MakeGenericMethod(dataSet.Type, itemType, filteringOptionsProperty.PropertyType, sortingOptionsProperty.PropertyType, pagingOptionsProperty.PropertyType), + dataSet, + optionsTransformLambda, + Expression.Constant(null, typeof(Func<,>).MakeGenericType(optionsType, typeof(Task<>).MakeGenericType(resultType))), + Expression.Constant(null, typeof(Action<,>).MakeGenericType(dataSet.Type, resultType))); + + if (transformExpression != null) + { + expression = transformExpression(expression); + } + return new StaticCommandBindingExpression(service, + new object[] + { + new ParsedExpressionBindingProperty(expression), + BindingParserOptions.StaticCommand, + dataContextStack + }); } private static Expression CurrentGridDataSetExpression(Type datasetType) @@ -230,7 +264,14 @@ private static Expression CurrentGridDataSetExpression(Type datasetType) /// A sentinel method which is translated to load the GridViewDataSet on the client side using the Load delegate. /// Do not call this method on the server. /// - public static Task DataSetClientSideLoad(IBaseGridViewDataSet dataSet) + public static Task DataSetClientSideLoad( + IBaseGridViewDataSet dataSet, + Action> optionsTransformer, + Func, Task>> loadDataDelegate, + Action> postProcessor) + where TFilteringOptions : IFilteringOptions + where TSortingOptions : ISortingOptions + where TPagingOptions : IPagingOptions { throw new InvalidOperationException("This method cannot be called on the server!"); } @@ -243,24 +284,32 @@ public static T GetCurrentGridDataSet() where T : IBaseGridViewDataSet private static Type GetOptionsConcreteType(Type dataSetConcreteType, out PropertyInfo optionsProperty) { - if (!typeof(TDataSetInterface).IsGenericType || !typeof(TDataSetInterface).IsAssignableFrom(dataSetConcreteType)) + if (!typeof(TDataSetInterface).IsAssignableFrom(dataSetConcreteType)) { throw new ArgumentException($"The type {typeof(TDataSetInterface)} must be a generic type and must be implemented by the type {dataSetConcreteType} specified in {nameof(dataSetConcreteType)} argument!"); } // resolve options property - var genericInterface = typeof(TDataSetInterface).GetGenericTypeDefinition(); - if (genericInterface == typeof(IFilterableGridViewDataSet<>)) + var genericInterface = typeof(TDataSetInterface).IsGenericType ? typeof(TDataSetInterface).GetGenericTypeDefinition() : typeof(TDataSetInterface); + if (genericInterface == typeof(IFilterableGridViewDataSet<>) || genericInterface == typeof(IFilterableGridViewDataSet)) { optionsProperty = typeof(TDataSetInterface).GetProperty(nameof(IFilterableGridViewDataSet.FilteringOptions))!; + genericInterface = typeof(IFilterableGridViewDataSet<>); } - else if (genericInterface == typeof(ISortableGridViewDataSet<>)) + else if (genericInterface == typeof(ISortableGridViewDataSet<>) || genericInterface == typeof(ISortableGridViewDataSet)) { optionsProperty = typeof(TDataSetInterface).GetProperty(nameof(ISortableGridViewDataSet.SortingOptions))!; + genericInterface = typeof(ISortableGridViewDataSet<>); } - else if (genericInterface == typeof(IPageableGridViewDataSet<>)) + else if (genericInterface == typeof(IPageableGridViewDataSet<>) || genericInterface == typeof(IPageableGridViewDataSet)) { optionsProperty = typeof(TDataSetInterface).GetProperty(nameof(IPageableGridViewDataSet.PagingOptions))!; + genericInterface = typeof(IPageableGridViewDataSet<>); + } + else if (genericInterface == typeof(IBaseGridViewDataSet<>) || genericInterface == typeof(IBaseGridViewDataSet)) + { + optionsProperty = typeof(TDataSetInterface).GetProperty(nameof(IBaseGridViewDataSet.Items))!; + genericInterface = typeof(IBaseGridViewDataSet<>); } else { diff --git a/src/Framework/Framework/Controls/GridViewDataSetCommandType.cs b/src/Framework/Framework/Controls/GridViewDataSetCommandType.cs index 4e47480917..49fd31c317 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetCommandType.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetCommandType.cs @@ -2,7 +2,7 @@ namespace DotVVM.Framework.Controls { public enum GridViewDataSetCommandType { - Command, - StaticCommand + Default, + LoadDataDelegate } } \ No newline at end of file diff --git a/src/Framework/Framework/Resources/Scripts/binding-handlers/all-handlers.ts b/src/Framework/Framework/Resources/Scripts/binding-handlers/all-handlers.ts index ccacf44549..7530abd30a 100644 --- a/src/Framework/Framework/Resources/Scripts/binding-handlers/all-handlers.ts +++ b/src/Framework/Framework/Resources/Scripts/binding-handlers/all-handlers.ts @@ -10,6 +10,7 @@ import gridviewdataset from './gridviewdataset' import namedCommand from './named-command' import fileUpload from './file-upload' import jsComponents from './js-component' +import appendableDataPager from './appendable-data-pager' type KnockoutHandlerDictionary = { [name: string]: KnockoutBindingHandler @@ -26,7 +27,8 @@ const allHandlers: KnockoutHandlerDictionary = { ...gridviewdataset, ...namedCommand, ...fileUpload, - ...jsComponents + ...jsComponents, + ...appendableDataPager } export default allHandlers diff --git a/src/Framework/Framework/Resources/Scripts/binding-handlers/appendable-data-pager.ts b/src/Framework/Framework/Resources/Scripts/binding-handlers/appendable-data-pager.ts new file mode 100644 index 0000000000..453b34a664 --- /dev/null +++ b/src/Framework/Framework/Resources/Scripts/binding-handlers/appendable-data-pager.ts @@ -0,0 +1,41 @@ +type AppendableDataPagerBinding = { + autoLoadWhenInViewport: boolean, + loadNextPage: () => Promise +}; + +export default { + 'dotvvm-appendable-data-pager': { + init: (element: HTMLInputElement, valueAccessor: () => AppendableDataPagerBinding, allBindingsAccessor: KnockoutAllBindingsAccessor) => { + const binding = valueAccessor(); + if (binding.autoLoadWhenInViewport) { + let isLoading = false; + + // track the scroll position and load the next page when the element is in the viewport + const observer = new IntersectionObserver(async (entries) => { + if (isLoading) return; + + let entry = entries[0]; + while (entry.isIntersecting) { + const dataSet = allBindingsAccessor.get("dotvvm-gridviewdataset").dataSet as DotvvmObservable; + if (dataSet.state.PagingOptions.IsLastPage) { + return; + } + + isLoading = true; + try { + await binding.loadNextPage(); + + // when the loading was finished, check whether we need to load another page + entry = observer.takeRecords()[0]; + } + finally { + isLoading = false; + } + } + }); + observer.observe(element); + ko.utils.domNodeDisposal.addDisposeCallback(element, () => observer.disconnect()); + } + } + } +} diff --git a/src/Framework/Framework/Resources/Scripts/dataset/loader.ts b/src/Framework/Framework/Resources/Scripts/dataset/loader.ts index 78f8d3121f..6d7f2e307c 100644 --- a/src/Framework/Framework/Resources/Scripts/dataset/loader.ts +++ b/src/Framework/Framework/Resources/Scripts/dataset/loader.ts @@ -1,4 +1,6 @@ -type GridViewDataSet = { +import { StateManager } from "../state-manager"; + +type GridViewDataSet = { PagingOptions: DotvvmObservable, SortingOptions: DotvvmObservable, FilteringOptions: DotvvmObservable, @@ -6,9 +8,9 @@ IsRefreshRequired?: DotvvmObservable }; type GridViewDataSetOptions = { - PagingOptions: DotvvmObservable, - SortingOptions: DotvvmObservable, - FilteringOptions: DotvvmObservable + PagingOptions: any, + SortingOptions: any, + FilteringOptions: any }; type GridViewDataSetResult = { Items: any[], @@ -17,34 +19,41 @@ type GridViewDataSetResult = { FilteringOptions: any }; -export async function loadDataSet(dataSetObservable: KnockoutObservable, loadData: (options: GridViewDataSetOptions) => Promise) { - const dataSet = ko.unwrap(dataSetObservable); - if (dataSet.IsRefreshRequired) { - dataSet.IsRefreshRequired.setState(true); - } +export async function loadDataSet( + dataSetObservable: DotvvmObservable, + transformOptions: (options: GridViewDataSetOptions) => void, + loadData: (options: GridViewDataSetOptions) => Promise, + postProcessor: (dataSet: DotvvmObservable, result: GridViewDataSetResult) => void = postProcessors.replace +) { + const dataSet = dataSetObservable.state; - const result = await loadData({ - FilteringOptions: dataSet.FilteringOptions.state, - SortingOptions: dataSet.SortingOptions.state, - PagingOptions: dataSet.PagingOptions.state - }); + const options: GridViewDataSetOptions = { + FilteringOptions: structuredClone(dataSet.FilteringOptions), + SortingOptions: structuredClone(dataSet.SortingOptions), + PagingOptions: structuredClone(dataSet.PagingOptions) + }; + transformOptions(options); + + const result = await loadData(options); const commandResult = result.commandResult as GridViewDataSetResult; - dataSet.Items.setState([]); - dataSet.Items.setState(commandResult.Items); + postProcessor(dataSetObservable, commandResult); +} - if (commandResult.FilteringOptions && ko.isWriteableObservable(dataSet.FilteringOptions)) { - dataSet.FilteringOptions.setState(commandResult.FilteringOptions); - } - if (commandResult.SortingOptions && ko.isWriteableObservable(dataSet.SortingOptions)) { - dataSet.SortingOptions.setState(commandResult.SortingOptions); - } - if (commandResult.PagingOptions && ko.isWriteableObservable(dataSet.PagingOptions)) { - dataSet.PagingOptions.setState(commandResult.PagingOptions); - } +export const postProcessors = { - if (dataSet.IsRefreshRequired) { - dataSet.IsRefreshRequired.setState(false); + replace(dataSet: DotvvmObservable, result: GridViewDataSetResult) { + dataSet.patchState(result); + }, + + append(dataSet: DotvvmObservable, result: GridViewDataSetResult) { + const currentItems = (dataSet.state as any).Items as any[]; + dataSet.patchState({ + FilteringOptions: result.FilteringOptions, + SortingOptions: result.SortingOptions, + PagingOptions: result.PagingOptions, + Items: [...currentItems, ...result.Items] + }); } -} +}; diff --git a/src/Framework/Framework/Resources/Scripts/dataset/translations.ts b/src/Framework/Framework/Resources/Scripts/dataset/translations.ts index 2be8789c37..a4fa1b9609 100644 --- a/src/Framework/Framework/Resources/Scripts/dataset/translations.ts +++ b/src/Framework/Framework/Resources/Scripts/dataset/translations.ts @@ -20,77 +20,71 @@ type SortingOptions = { export const translations = { PagingOptions: { - goToFirstPage(options: DotvvmObservable) { - options.patchState({ PageIndex: 0 }); + goToFirstPage(options: PagingOptions) { + options.PageIndex = 0; }, - goToLastPage(options: DotvvmObservable) { - options.patchState({ PageIndex: options.state.PagesCount - 1 }); + goToLastPage(options: PagingOptions) { + options.PageIndex = options.PagesCount - 1; }, - goToNextPage(options: DotvvmObservable) { - if (options.state.PageIndex < options.state.PagesCount - 1) { - options.patchState({ PageIndex: options.state.PageIndex + 1 }); + goToNextPage(options: PagingOptions) { + if (options.PageIndex < options.PagesCount - 1) { + options.PageIndex = options.PageIndex + 1; } }, - goToPreviousPage(options: DotvvmObservable) { - if (options.state.PageIndex > 0) { - options.patchState({ PageIndex: options.state.PageIndex - 1 }); + goToPreviousPage(options: PagingOptions) { + if (options.PageIndex > 0) { + options.PageIndex = options.PageIndex - 1; } }, - goToPage(options: DotvvmObservable, pageIndex: number) { - if (options.state.PageIndex >= 0 && options.state.PageIndex < options.state.PagesCount) { - options.patchState({ PageIndex: pageIndex }); + goToPage(options: PagingOptions, pageIndex: number) { + if (options.PageIndex >= 0 && options.PageIndex < options.PagesCount) { + options.PageIndex = pageIndex; } } }, NextTokenPagingOptions: { - goToFirstPage(options: DotvvmObservable) { - options.patchState({ CurrentToken: null }); + goToFirstPage(options: NextTokenPagingOptions) { + options.CurrentToken = null; }, - goToNextPage(options: DotvvmObservable) { - if (options.state.NextPageToken) { - options.patchState({ CurrentToken: options.state.NextPageToken }); + goToNextPage(options: NextTokenPagingOptions) { + if (options.NextPageToken) { + options.CurrentToken = options.NextPageToken; } } }, NextTokenHistoryPagingOptions: { - goToFirstPage(options: DotvvmObservable) { - options.patchState({ PageIndex: 0 }); + goToFirstPage(options: NextTokenHistoryPagingOptions) { + options.PageIndex = 0; }, - goToNextPage(options: DotvvmObservable) { - if (options.state.PageIndex < options.state.TokenHistory.length - 1) { - options.patchState({ PageIndex: options.state.PageIndex + 1 }); + goToNextPage(options: NextTokenHistoryPagingOptions) { + if (options.PageIndex < options.TokenHistory.length - 1) { + options.PageIndex = options.PageIndex + 1; } }, - goToPreviousPage(options: DotvvmObservable) { - if (options.state.PageIndex > 0) { - options.patchState({ PageIndex: options.state.PageIndex - 1 }); + goToPreviousPage(options: NextTokenHistoryPagingOptions) { + if (options.PageIndex > 0) { + options.PageIndex = options.PageIndex - 1; } }, - goToPage(options: DotvvmObservable, pageIndex: number) { - if (options.state.PageIndex >= 0 && options.state.PageIndex < options.state.TokenHistory.length) { - options.patchState({ PageIndex: pageIndex }); + goToPage(options: NextTokenHistoryPagingOptions, pageIndex: number) { + if (options.PageIndex >= 0 && options.PageIndex < options.TokenHistory.length) { + options.PageIndex = pageIndex; } } }, SortingOptions: { - setSortExpression(options: DotvvmObservable, sortExpression: string) { + setSortExpression(options: SortingOptions, sortExpression: string) { if (sortExpression == null) { - options.patchState({ - SortExpression: null, - SortDescending: false - }); + options.SortExpression = null; + options.SortDescending = false; } - else if (sortExpression == options.state.SortExpression) { - options.patchState({ - SortDescending: !options.state.SortDescending - }); + else if (sortExpression == options.SortExpression) { + options.SortDescending = !options.SortDescending; } else { - options.patchState({ - SortExpression: sortExpression, - SortDescending: false - }); + options.SortExpression = sortExpression; + options.SortDescending = false; } } } diff --git a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts index 223c4a7aac..ba1cb6bc95 100644 --- a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts +++ b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts @@ -27,7 +27,7 @@ import * as metadataHelper from './metadata/metadataHelper' import { StateManager } from "./state-manager" import { DotvvmEvent } from "./events" import translations from './translations/translations' -import { loadDataSet } from './dataset/loader' +import { loadDataSet, postProcessors as loaderPostProcessors } from './dataset/loader' import * as dataSetTranslations from './dataset/translations' if (window["dotvvm"]) { @@ -126,6 +126,7 @@ const dotvvmExports = { translations: translations as any, dataSet: { loadDataSet: loadDataSet, + postProcessors: loaderPostProcessors, translations: dataSetTranslations.translations }, StateManager, diff --git a/src/Framework/Framework/Resources/Scripts/state-manager.ts b/src/Framework/Framework/Resources/Scripts/state-manager.ts index 3dc584242b..3839858366 100644 --- a/src/Framework/Framework/Resources/Scripts/state-manager.ts +++ b/src/Framework/Framework/Resources/Scripts/state-manager.ts @@ -46,7 +46,7 @@ export class StateManager { constructor( initialState: DeepReadonly, - public stateUpdateEvent: DotvvmEvent> + public stateUpdateEvent?: DotvvmEvent> ) { this._state = coerce(initialState, initialState.$type || { type: "dynamic" }) this.stateObservable = createWrappedObservable(initialState, (initialState as any)["$type"], u => this.update(u as any)) @@ -73,7 +73,7 @@ export class StateManager { isViewModelUpdating = true ko.delaySync.pause() - this.stateUpdateEvent.trigger(this._state); + this.stateUpdateEvent?.trigger(this._state); (this.stateObservable as any)[notifySymbol as any](this._state) } finally { diff --git a/src/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj index d59a3ae001..f2721a825a 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Samples/Common/ViewModels/ControlSamples/AppendableDataPager/AppendableDataPagerViewModel.cs b/src/Samples/Common/ViewModels/ControlSamples/AppendableDataPager/AppendableDataPagerViewModel.cs new file mode 100644 index 0000000000..b37a7ce8c9 --- /dev/null +++ b/src/Samples/Common/ViewModels/ControlSamples/AppendableDataPager/AppendableDataPagerViewModel.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.ViewModel; +using DotVVM.Framework.Controls; +using DotVVM.Samples.BasicSamples.ViewModels.ControlSamples.GridView; + +namespace DotVVM.Samples.Common.ViewModels.ControlSamples.AppendableDataPager +{ + public class AppendableDataPagerViewModel : DotvvmViewModelBase + { + public GridViewDataSet Customers { get; set; } = new() { + PagingOptions = new PagingOptions { + PageSize = 3 + } + }; + + private static IQueryable GetData() + { + return new[] + { + new CustomerData {CustomerId = 1, Name = "John Doe", BirthDate = DateTime.Parse("1976-04-01")}, + new CustomerData {CustomerId = 2, Name = "John Deer", BirthDate = DateTime.Parse("1984-03-02")}, + new CustomerData {CustomerId = 3, Name = "Johnny Walker", BirthDate = DateTime.Parse("1934-01-03")}, + new CustomerData {CustomerId = 4, Name = "Jim Hacker", BirthDate = DateTime.Parse("1912-11-04")}, + new CustomerData {CustomerId = 5, Name = "Joe E. Brown", BirthDate = DateTime.Parse("1947-09-05")}, + new CustomerData {CustomerId = 6, Name = "Jack Daniels", BirthDate = DateTime.Parse("1956-07-06")}, + new CustomerData {CustomerId = 7, Name = "James Bond", BirthDate = DateTime.Parse("1965-05-07")}, + new CustomerData {CustomerId = 8, Name = "John Smith", BirthDate = DateTime.Parse("1974-03-08")}, + new CustomerData {CustomerId = 9, Name = "Jack & Jones", BirthDate = DateTime.Parse("1976-03-22")}, + new CustomerData {CustomerId = 10, Name = "Jim Bill", BirthDate = DateTime.Parse("1974-09-20")}, + new CustomerData {CustomerId = 11, Name = "James Joyce", BirthDate = DateTime.Parse("1982-11-28")}, + new CustomerData {CustomerId = 12, Name = "Joudy Jane", BirthDate = DateTime.Parse("1958-12-14")} + }.AsQueryable(); + } + public override Task PreRender() + { + // fill dataset + if (!Context.IsPostBack) + { + Customers.LoadFromQueryable(GetData()); + } + return base.PreRender(); + } + + [AllowStaticCommand] + public static async Task> LoadNextPage(GridViewDataSetOptions options) + { + var dataSet = new GridViewDataSet(); + dataSet.ApplyOptions(options); + dataSet.LoadFromQueryable(GetData()); + return dataSet; + } + } +} + diff --git a/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPager.dothtml b/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPager.dothtml new file mode 100644 index 0000000000..11c27628be --- /dev/null +++ b/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPager.dothtml @@ -0,0 +1,34 @@ +@viewModel DotVVM.Samples.Common.ViewModels.ControlSamples.AppendableDataPager.AppendableDataPagerViewModel, DotVVM.Samples.Common + + + + + + + + + + + + + + + + + + + + + + + + + You reached to the end of the Earth. Now you shall see the 🐢🐢🐢🐢 and 🐘. + + + + + + + diff --git a/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml b/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml index 7e1c2fc9b4..fdf6fb7cf2 100644 --- a/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml +++ b/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml @@ -23,7 +23,7 @@ -

NextToken paging options

+ <%--

NextToken paging options

@@ -57,7 +57,7 @@ - + --%> diff --git a/src/Tests/ViewModel/GridViewDataSetTests.cs b/src/Tests/ViewModel/GridViewDataSetTests.cs index b01890620c..e3347cdd4c 100644 --- a/src/Tests/ViewModel/GridViewDataSetTests.cs +++ b/src/Tests/ViewModel/GridViewDataSetTests.cs @@ -58,7 +58,7 @@ public void GridViewDataSet_DataPagerCommands_Command() control.Children.Add(pageIndexControl); // get pager commands - var commands = commandProvider.GetDataPagerCommands(dataContextStack, dataSetBinding, GridViewDataSetCommandType.Command); + var commands = commandProvider.GetDataPagerCommands(dataContextStack, dataSetBinding, GridViewDataSetCommandType.Default); // test evaluation of commands Assert.IsNotNull(commands.GoToLastPage); @@ -96,7 +96,7 @@ public void GridViewDataSet_DataPagerCommands_Command() public void GridViewDataSet_GridViewCommands_Command() { // get gridview commands - var commands = commandProvider.GetGridViewCommands(dataContextStack, dataSetBinding, GridViewDataSetCommandType.Command); + var commands = commandProvider.GetGridViewCommands(dataContextStack, dataSetBinding, GridViewDataSetCommandType.Default); // test evaluation of commands Assert.IsNotNull(commands.SetSortExpression); @@ -124,7 +124,7 @@ public void GridViewDataSet_GridViewCommands_Command() public void GridViewDataSet_DataPagerCommands_StaticCommand() { // get pager commands - var commands = commandProvider.GetDataPagerCommands(dataContextStack, dataSetBinding, GridViewDataSetCommandType.StaticCommand); + var commands = commandProvider.GetDataPagerCommands(dataContextStack, dataSetBinding, GridViewDataSetCommandType.LoadDataDelegate); var goToFirstPage = CompileBinding(commands.GoToFirstPage); Console.WriteLine(goToFirstPage);