diff --git a/src/Framework/Framework/Binding/CommonBindings.cs b/src/Framework/Framework/Binding/CommonBindings.cs deleted file mode 100644 index ae5e3c9fe4..0000000000 --- a/src/Framework/Framework/Binding/CommonBindings.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using DotVVM.Framework.Binding.Expressions; - -namespace DotVVM.Framework.Binding -{ - public class CommonBindings - { - public CommonBindings(BindingCompilationService service) - { - this.BindingCompilationService = service; - } - - public BindingCompilationService BindingCompilationService { get; } - } -} diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index ad843b4789..5c9cfb854f 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -110,6 +110,12 @@ public void AddPropertyTranslator(Expression> propertyAccess, IJavasc } } + public void AddMethodTranslator(Expression methodCall, IJavascriptMethodTranslator translator) + { + var method = (MethodInfo)MethodFindingHelper.GetMethodFromExpression(methodCall); + AddMethodTranslator(method, translator); + } + public void AddMethodTranslator(Type declaringType, string methodName, IJavascriptMethodTranslator translator, int parameterCount, bool allowMultipleMethods = false, Func? parameterFilter = null) { var methods = declaringType.GetMethods() @@ -243,8 +249,9 @@ public void AddDefaultMethodTranslators() AddDefaultMathTranslations(); AddDefaultDateTimeTranslations(); AddDefaultConvertTranslations(); + AddDataSetOptionsTranslations(); } - + private void AddDefaultToStringTranslations() { AddMethodTranslator(typeof(object).GetMethod("ToString", Type.EmptyTypes), new PrimitiveToStringTranslator()); @@ -777,6 +784,58 @@ private void AddDefaultConvertTranslations() } } } + + private void AddDataSetOptionsTranslations() + { + // GridViewDataSetBindingProvider + AddMethodTranslator(typeof(GridViewDataSetBindingProvider), "DataSetClientSideLoad", new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("loadDataSet").Invoke(args[1], args[2]))); + + // PagingOptions + AddMethodTranslator(() => default(PagingOptions)!.GoToFirstPage(),new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("PagingOptions").Member("goToFirstPage") + .Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance)))); + AddMethodTranslator(() => default(PagingOptions)!.GoToLastPage(), new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("PagingOptions").Member("goToLastPage") + .Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance)))); + AddMethodTranslator(() => default(PagingOptions)!.GoToPreviousPage(), new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("PagingOptions").Member("goToPreviousPage") + .Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance)))); + AddMethodTranslator(() => default(PagingOptions)!.GoToNextPage(), new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("PagingOptions").Member("goToNextPage") + .Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance)))); + AddMethodTranslator(() => default(PagingOptions)!.GoToPage(default(int)), new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("PagingOptions").Member("goToPage") + .Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance), args[1]))); + + // NextTokenPagingOptions + AddMethodTranslator(() => default(NextTokenPagingOptions)!.GoToFirstPage(), new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("NextTokenPagingOptions").Member("goToFirstPage") + .Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance)))); + AddMethodTranslator(() => default(NextTokenPagingOptions)!.GoToNextPage(), new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("NextTokenPagingOptions").Member("goToNextPage") + .Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance)))); + + // NextTokenHistoryPagingOptions + AddMethodTranslator(() => default(NextTokenHistoryPagingOptions)!.GoToFirstPage(), new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("NextTokenHistoryPagingOptions").Member("goToFirstPage") + .Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance)))); + AddMethodTranslator(() => default(NextTokenHistoryPagingOptions)!.GoToPreviousPage(), new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("NextTokenHistoryPagingOptions").Member("goToPreviousPage") + .Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance)))); + AddMethodTranslator(() => default(NextTokenHistoryPagingOptions)!.GoToNextPage(), new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("NextTokenHistoryPagingOptions").Member("goToNextPage") + .Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance)))); + AddMethodTranslator(() => default(NextTokenHistoryPagingOptions)!.GoToPage(default(int)), new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("NextTokenHistoryPagingOptions").Member("goToPage") + .Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance), args[1]))); + + // SortingOptions + AddMethodTranslator(() => default(SortingOptions)!.SetSortExpression(default(string?)), new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("SortingOptions").Member("setSortExpression") + .Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance), args[1]))); + } + public JsExpression? TryTranslateCall(LazyTranslatedExpression? context, LazyTranslatedExpression[] args, MethodInfo method) { { diff --git a/src/Framework/Framework/Compilation/Javascript/MemberInfoHelper.cs b/src/Framework/Framework/Compilation/Javascript/MemberInfoHelper.cs index 63442dde8a..723d4961b0 100644 --- a/src/Framework/Framework/Compilation/Javascript/MemberInfoHelper.cs +++ b/src/Framework/Framework/Compilation/Javascript/MemberInfoHelper.cs @@ -31,6 +31,12 @@ static Expression UnwrapConversions(Expression e) e = unary.Operand; return e; } + + public static MethodBase GetMethodFromExpression(Expression expression) + { + return GetMethodFromExpression((Expression)expression); + } + static MethodBase GetMethodFromExpression(Expression expression) { var originalExpression = expression; diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index 6bc04a8fe3..a8f9317574 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -20,32 +20,17 @@ namespace DotVVM.Framework.Controls [ControlMarkupOptions(AllowContent = false)] public class DataPager : HtmlGenericControl { - public class CommonBindings - { - public readonly CommandBindingExpression GoToNextPageCommand; - public readonly CommandBindingExpression GoToThisPageCommand; - public readonly CommandBindingExpression GoToPrevPageCommand; - public readonly CommandBindingExpression GoToFirstPageCommand; - public readonly CommandBindingExpression GoToLastPageCommand; + private readonly GridViewDataSetBindingProvider gridViewDataSetBindingProvider; + private readonly BindingCompilationService bindingCompilationService; - public CommonBindings(BindingCompilationService service) - { - GoToNextPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).GoToNextPageAndRefresh(), "__$DataPager_GoToNextPage"); - GoToThisPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[1]).GoToPageAndRefresh((int)h[0]), "__$DataPager_GoToThisPage"); - GoToPrevPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).GoToPreviousPageAndRefresh(), "__$DataPager_GoToPrevPage"); - GoToFirstPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).GoToFirstPageAndRefresh(), "__$DataPager_GoToFirstPage"); - GoToLastPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).GoToLastPageAndRefresh(), "__$DataPager_GoToLastPage"); - } - } + private DataPagerCommands? pagerCommands; - private readonly CommonBindings commonBindings; - private readonly BindingCompilationService bindingService; - public DataPager(CommonBindings commonBindings, BindingCompilationService bindingService) + public DataPager(GridViewDataSetBindingProvider gridViewDataSetBindingProvider, BindingCompilationService bindingCompilationService) : base("div", false) { - this.commonBindings = commonBindings; - this.bindingService = bindingService; + this.gridViewDataSetBindingProvider = gridViewDataSetBindingProvider; + this.bindingCompilationService = bindingCompilationService; } /// @@ -171,19 +156,18 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) ContentWrapper = CreateWrapperList(dataContextType); Children.Add(ContentWrapper); - var bindings = context.Services.GetRequiredService(); - + pagerCommands = gridViewDataSetBindingProvider.GetDataPagerCommands(dataContextType, GridViewDataSetCommandType.Command); object enabledValue = GetValueRaw(EnabledProperty)!; - + if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataContextType.DataContextType)) { - GoToFirstPageButton = CreateNavigationButton("««", FirstPageTemplate, enabledValue, bindings.GoToFirstPageCommand, context); + GoToFirstPageButton = CreateNavigationButton("««", FirstPageTemplate, enabledValue, pagerCommands.GoToFirstPage!, context); ContentWrapper.Children.Add(GoToFirstPageButton); } if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataContextType.DataContextType)) { - GoToPreviousPageButton = CreateNavigationButton("«", PreviousPageTemplate, enabledValue, bindings.GoToPrevPageCommand, context); + GoToPreviousPageButton = CreateNavigationButton("«", PreviousPageTemplate, enabledValue, pagerCommands.GoToPreviousPage!, context); ContentWrapper.Children.Add(GoToPreviousPageButton); } @@ -205,7 +189,7 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) li.Attributes.Set("class", ActiveItemCssClass); } var link = new LinkButton() { Text = (number + 1).ToString() }; - link.SetBinding(ButtonBase.ClickProperty, bindings.GoToThisPageCommand); + link.SetBinding(ButtonBase.ClickProperty, pagerCommands.GoToPage!); if (!true.Equals(enabledValue)) link.SetValue(LinkButton.EnabledProperty, enabledValue); li.Children.Add(link); NumberButtonsPlaceHolder.Children.Add(li); @@ -217,13 +201,13 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataContextType.DataContextType)) { - GoToNextPageButton = CreateNavigationButton("»", NextPageTemplate, enabledValue, bindings.GoToNextPageCommand, context); + GoToNextPageButton = CreateNavigationButton("»", NextPageTemplate, enabledValue, pagerCommands.GoToNextPage!, context); ContentWrapper.Children.Add(GoToNextPageButton); } if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataContextType.DataContextType)) { - GoToLastPageButton = CreateNavigationButton("»»", LastPageTemplate, enabledValue, bindings.GoToLastPageCommand, context); + GoToLastPageButton = CreateNavigationButton("»»", LastPageTemplate, enabledValue, pagerCommands.GoToLastPage!, context); ContentWrapper.Children.Add(GoToLastPageButton); } } @@ -269,7 +253,7 @@ private ValueBindingExpression GetNearIndexesBinding(IDotvvmRequestContext conte _nearIndexesBindingCache.GetOrCreateValue(context.Configuration) .GetOrAdd(i, _ => ValueBindingExpression.CreateBinding( - bindingService.WithoutInitialization(), + bindingCompilationService.WithoutInitialization(), h => ((IPageableGridViewDataSet)h[0]!).PagingOptions.NearPageIndexes[i], dataContext)); } @@ -358,7 +342,7 @@ protected virtual void RenderPageNumberButton(IHtmlWriter writer, IDotvvmRequest var currentPageTextContext = DataContextStack.Create(typeof(int), NumberButtonsPlaceHolder!.GetDataContextType()); li.SetDataContextType(currentPageTextContext); li.DataContext = null; - var currentPageTextBinding = ValueBindingExpression.CreateBinding(bindingService.WithoutInitialization(), + var currentPageTextBinding = ValueBindingExpression.CreateBinding(bindingCompilationService.WithoutInitialization(), vm => ((int) vm[0]! + 1).ToString(), currentPageTextJs, currentPageTextContext); @@ -394,7 +378,7 @@ protected virtual void RenderPageNumberButton(IHtmlWriter writer, IDotvvmRequest li.Children.Add(link); link.SetDataContextType(currentPageTextContext); link.SetBinding(ButtonBase.TextProperty, currentPageTextBinding); - link.SetBinding(ButtonBase.ClickProperty, commonBindings.GoToThisPageCommand); + link.SetBinding(ButtonBase.ClickProperty, pagerCommands.GoToPage!); object enabledValue = GetValueRaw(EnabledProperty)!; if (!true.Equals(enabledValue)) link.SetValue(LinkButton.EnabledProperty, enabledValue); @@ -409,5 +393,4 @@ protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext c 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/DataPagerCommands.cs b/src/Framework/Framework/Controls/DataPagerCommands.cs new file mode 100644 index 0000000000..9833261de0 --- /dev/null +++ b/src/Framework/Framework/Controls/DataPagerCommands.cs @@ -0,0 +1,13 @@ +using DotVVM.Framework.Binding.Expressions; + +namespace DotVVM.Framework.Controls +{ + public class DataPagerCommands + { + public ICommandBinding? GoToFirstPage { get; init; } + public ICommandBinding? GoToPreviousPage { get; init; } + public ICommandBinding? GoToNextPage { get; init; } + public ICommandBinding? GoToLastPage { get; init; } + public ICommandBinding? GoToPage { get; init; } + } +} \ No newline at end of file diff --git a/src/Framework/Framework/Controls/GridViewCommands.cs b/src/Framework/Framework/Controls/GridViewCommands.cs new file mode 100644 index 0000000000..52cf0597fb --- /dev/null +++ b/src/Framework/Framework/Controls/GridViewCommands.cs @@ -0,0 +1,9 @@ +using DotVVM.Framework.Binding.Expressions; + +namespace DotVVM.Framework.Controls +{ + public class GridViewCommands + { + public ICommandBinding? SetSortExpression { get; init; } + } +} \ No newline at end of file diff --git a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs new file mode 100644 index 0000000000..7eafffe236 --- /dev/null +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Binding.Properties; +using DotVVM.Framework.Compilation; +using DotVVM.Framework.Compilation.ControlTree; + +namespace DotVVM.Framework.Controls; + +public class GridViewDataSetBindingProvider +{ + private readonly BindingCompilationService service; + + private readonly ConcurrentDictionary<(DataContextStack, GridViewDataSetCommandType), DataPagerCommands> dataPagerCommands = new(); + private readonly ConcurrentDictionary<(DataContextStack, GridViewDataSetCommandType), GridViewCommands> gridViewCommands = new(); + + public GridViewDataSetBindingProvider(BindingCompilationService service) + { + this.service = service; + } + + public DataPagerCommands GetDataPagerCommands(DataContextStack dataContextStack, GridViewDataSetCommandType commandType) + { + return dataPagerCommands.GetOrAdd((dataContextStack, commandType), _ => GetDataPagerCommandsCore(dataContextStack, commandType)); + } + + public GridViewCommands GetGridViewCommands(DataContextStack dataContextStack, GridViewDataSetCommandType commandType) + { + return gridViewCommands.GetOrAdd((dataContextStack, commandType), _ => GetGridViewCommandsCore(dataContextStack, commandType)); + } + + private DataPagerCommands GetDataPagerCommandsCore(DataContextStack dataContextStack, GridViewDataSetCommandType commandType) + { + ICommandBinding? GetCommandOrNull(ParameterExpression dataSetParam, DataContextStack dataContextStack, string methodName, params Expression[] arguments) + { + return typeof(T).IsAssignableFrom(dataSetParam.Type) + ? CreateCommandBinding(commandType, dataSetParam, dataContextStack, methodName, arguments) + : null; + } + ParameterExpression CreateParameter(DataContextStack dataContextStack) + { + return Expression.Parameter(dataContextStack.DataContextType).AddParameterAnnotation(new BindingParameterAnnotation(dataContextStack)); + } + + return new DataPagerCommands() + { + GoToFirstPage = GetCommandOrNull>( + CreateParameter(dataContextStack), + dataContextStack, + nameof(IPagingFirstPageCapability.GoToFirstPage)), + + GoToPreviousPage = GetCommandOrNull>( + CreateParameter(dataContextStack), + dataContextStack, + nameof(IPagingPreviousPageCapability.GoToPreviousPage)), + + GoToNextPage = GetCommandOrNull>( + CreateParameter(dataContextStack), + dataContextStack, + nameof(IPagingNextPageCapability.GoToNextPage)), + + GoToLastPage = GetCommandOrNull>( + CreateParameter(dataContextStack), + dataContextStack, + nameof(IPagingLastPageCapability.GoToLastPage)), + + GoToPage = GetCommandOrNull>( + CreateParameter(dataContextStack), + DataContextStack.Create(typeof(int), dataContextStack), + nameof(IPagingPageIndexCapability.GoToPage), + CreateParameter(DataContextStack.Create(typeof(int), dataContextStack))) + }; + } + + private GridViewCommands GetGridViewCommandsCore(DataContextStack dataContextStack, GridViewDataSetCommandType commandType) + { + ICommandBinding? GetCommandOrNull(ParameterExpression dataSetParam, string methodName, Expression[] arguments, Func transformExpression) + { + return typeof(T).IsAssignableFrom(dataSetParam.Type) + ? CreateCommandBinding(commandType, dataSetParam, dataContextStack, methodName, arguments, transformExpression) + : null; + } + ParameterExpression CreateParameter(DataContextStack dataContextStack) + { + return Expression.Parameter(dataContextStack.DataContextType).AddParameterAnnotation(new BindingParameterAnnotation(dataContextStack)); + } + + var setSortExpressionParam = Expression.Parameter(typeof(string)); + return new GridViewCommands() + { + SetSortExpression = GetCommandOrNull>( + CreateParameter(dataContextStack), + nameof(ISortingSetSortExpressionCapability.SetSortExpression), + new Expression[] { setSortExpressionParam }, + // transform to sortExpression => command lambda + e => Expression.Lambda(e, setSortExpressionParam)) + }; + } + + private ICommandBinding CreateCommandBinding(GridViewDataSetCommandType commandType, ParameterExpression dataSetParam, DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression = null) + { + var body = new List(); + + // get concrete type from implementation of IXXXableGridViewDataSet + var optionsConcreteType = GetOptionsConcreteType(dataSetParam.Type, out var optionsProperty); + + // call dataSet.XXXOptions.Method(...); + var callMethodOnOptions = Expression.Call( + Expression.Convert(Expression.Property(dataSetParam, optionsProperty), optionsConcreteType), + optionsConcreteType.GetMethod(methodName)!, + arguments); + body.Add(callMethodOnOptions); + + if (commandType == GridViewDataSetCommandType.Command) + { + // if we are on a server, call the dataSet.RequestRefresh if supported + if (typeof(IRefreshableGridViewDataSet).IsAssignableFrom(dataSetParam.Type)) + { + var callRequestRefresh = Expression.Call( + Expression.Convert(dataSetParam, 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), + dataContextStack + }); + } + else if (commandType == GridViewDataSetCommandType.StaticCommand) + { + // on the client, wrap the call into client-side loading procedure + var expression = WrapInDataSetClientLoad(dataSetParam, body); + if (transformExpression != null) + { + expression = transformExpression(expression); + } + return new StaticCommandBindingExpression(service, + new object[] + { + new ParsedExpressionBindingProperty(expression), + BindingParserOptions.StaticCommand, + dataContextStack + }); + } + else + { + throw new NotSupportedException($"The command type {commandType} is not supported!"); + } + } + + /// + /// Wraps the block expression { dataSet.XXXOptions.Method(); dataSet.RequestRefresh(); } + /// as loaderFunction => { ...; return GridViewDataSetBindingProvider.DataSetClientSideLoad(dataSet, loaderFunction); }); + /// + private static Expression WrapInDataSetClientLoad(Expression dataSetParam, List body) + { + // get options and data set item type + var dataSetType = dataSetParam.Type; + var filteringOptionsConcreteType = GetOptionsConcreteType>(dataSetType, out _); + var sortingOptionsConcreteType = GetOptionsConcreteType>(dataSetType, out _); + var pagingOptionsConcreteType = GetOptionsConcreteType>(dataSetType, out _); + var dataSetItemType = dataSetType.GetInterfaces() + .Single(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IBaseGridViewDataSet<>)) + .GetGenericArguments()[0]; + + // resolve generic method and its parameter + var method = typeof(GridViewDataSetBindingProvider).GetMethod(nameof(DataSetClientSideLoad))! + .MakeGenericMethod(dataSetType, dataSetItemType, filteringOptionsConcreteType, sortingOptionsConcreteType, pagingOptionsConcreteType); + var loaderFunctionParam = Expression.Parameter(method.GetParameters().Single(p => p.Name == "loaderFunction").ParameterType, "loaderFn"); + + // call static method DataSetClientLoad + var callClientLoad = Expression.Call(method, dataSetParam, loaderFunctionParam); + return Expression.Lambda(Expression.Block(body.Concat(new [] { callClientLoad })), loaderFunctionParam); + } + + /// + /// 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 + ( + TGridViewDataSet dataSet, + Func, Task>> loaderFunction + ) + where TGridViewDataSet : IBaseGridViewDataSet, IFilterableGridViewDataSet, ISortableGridViewDataSet, IPageableGridViewDataSet + where TFilteringOptions : IFilteringOptions + where TSortingOptions : ISortingOptions + where TPagingOptions : IPagingOptions + { + throw new InvalidOperationException("This method cannot be called on the server!"); + } + + private static Type GetOptionsConcreteType(Type dataSetConcreteType, out PropertyInfo optionsProperty) + { + if (!typeof(TDataSetInterface).IsGenericType || !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<>)) + { + optionsProperty = typeof(TDataSetInterface).GetProperty(nameof(IFilterableGridViewDataSet.FilteringOptions))!; + } + else if (genericInterface == typeof(ISortableGridViewDataSet<>)) + { + optionsProperty = typeof(TDataSetInterface).GetProperty(nameof(ISortableGridViewDataSet.SortingOptions))!; + } + else if (genericInterface == typeof(IPageableGridViewDataSet<>)) + { + optionsProperty = typeof(TDataSetInterface).GetProperty(nameof(IPageableGridViewDataSet.PagingOptions))!; + } + else + { + throw new ArgumentException($"The {typeof(TDataSetInterface)} can only be {nameof(IFilterableGridViewDataSet)}, {nameof(ISortableGridViewDataSet)} or {nameof(IPageableGridViewDataSet)} with one generic argument!"); + } + + var interfaces = dataSetConcreteType.GetInterfaces() + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == genericInterface) + .ToList(); + if (interfaces.Count < 1) + { + throw new ArgumentException($"The {dataSetConcreteType} doesn't implement {genericInterface.Name}."); + } + else if (interfaces.Count > 1) + { + throw new ArgumentException($"The {dataSetConcreteType} implements multiple interfaces where {genericInterface.Name}. Only one implementation is allowed."); + } + + var pagingOptionsConcreteType = interfaces[0].GetGenericArguments()[0]; + return pagingOptionsConcreteType; + } +} diff --git a/src/Framework/Framework/Controls/GridViewDataSetCommandType.cs b/src/Framework/Framework/Controls/GridViewDataSetCommandType.cs new file mode 100644 index 0000000000..4e47480917 --- /dev/null +++ b/src/Framework/Framework/Controls/GridViewDataSetCommandType.cs @@ -0,0 +1,8 @@ +namespace DotVVM.Framework.Controls +{ + public enum GridViewDataSetCommandType + { + Command, + StaticCommand + } +} \ No newline at end of file diff --git a/src/Framework/Framework/Controls/GridViewDataSetOptions.cs b/src/Framework/Framework/Controls/GridViewDataSetOptions.cs new file mode 100644 index 0000000000..ff05a123cc --- /dev/null +++ b/src/Framework/Framework/Controls/GridViewDataSetOptions.cs @@ -0,0 +1,14 @@ +namespace DotVVM.Framework.Controls +{ + public class GridViewDataSetOptions + where TFilteringOptions : IFilteringOptions + where TSortingOptions : ISortingOptions + where TPagingOptions : IPagingOptions + { + public TFilteringOptions? FilteringOptions { get; init; } = default; + + public TSortingOptions? SortingOptions { get; init; } = default; + + public TPagingOptions? PagingOptions { get; init; } = default; + } +} \ No newline at end of file diff --git a/src/Framework/Framework/Controls/GridViewDataSetResult.cs b/src/Framework/Framework/Controls/GridViewDataSetResult.cs new file mode 100644 index 0000000000..ab4ff1fcee --- /dev/null +++ b/src/Framework/Framework/Controls/GridViewDataSetResult.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace DotVVM.Framework.Controls +{ + public class GridViewDataSetResult + where TFilteringOptions : IFilteringOptions + where TSortingOptions : ISortingOptions + where TPagingOptions : IPagingOptions + { + public GridViewDataSetResult(List items, TFilteringOptions? filteringOptions = default, TSortingOptions? sortingOptions = default, TPagingOptions? pagingOptions = default) + { + Items = items; + FilteringOptions = filteringOptions; + SortingOptions = sortingOptions; + PagingOptions = pagingOptions; + } + + public List Items { get; init; } + + public TFilteringOptions? FilteringOptions { get; init; } + + public TSortingOptions? SortingOptions { get; init; } + + public TPagingOptions? PagingOptions { get; init; } + } +} \ No newline at end of file diff --git a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs index e4825638b3..d4664a7f32 100644 --- a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs +++ b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs @@ -75,7 +75,7 @@ public static IServiceCollection RegisterDotVVMServices(IServiceCollection servi services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/src/Framework/Framework/Resources/Scripts/dataset/translations.ts b/src/Framework/Framework/Resources/Scripts/dataset/translations.ts index 4f0a4844b6..2be8789c37 100644 --- a/src/Framework/Framework/Resources/Scripts/dataset/translations.ts +++ b/src/Framework/Framework/Resources/Scripts/dataset/translations.ts @@ -56,9 +56,6 @@ export const translations = { goToFirstPage(options: DotvvmObservable) { options.patchState({ PageIndex: 0 }); }, - goToLastPage(options: DotvvmObservable) { - options.patchState({ PageIndex: options.state.TokenHistory.length - 1 }); - }, goToNextPage(options: DotvvmObservable) { if (options.state.PageIndex < options.state.TokenHistory.length - 1) { options.patchState({ PageIndex: options.state.PageIndex + 1 }); diff --git a/src/Tests/ViewModel/GridViewDataSetTests.cs b/src/Tests/ViewModel/GridViewDataSetTests.cs new file mode 100644 index 0000000000..f9df671b88 --- /dev/null +++ b/src/Tests/ViewModel/GridViewDataSetTests.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Compilation.ControlTree; +using DotVVM.Framework.Compilation.Javascript; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Controls.Infrastructure; +using DotVVM.Framework.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotVVM.Framework.Tests.ViewModel +{ + [TestClass] + public class GridViewDataSetTests + { + private readonly GridViewDataSetBindingProvider commandProvider; + private readonly GridViewDataSet vm; + private readonly DataContextStack dataContextStack; + private readonly DotvvmControl control; + + public GridViewDataSetTests() + { + var bindingService = DotvvmTestHelper.DefaultConfig.ServiceProvider.GetRequiredService(); + commandProvider = new GridViewDataSetBindingProvider(bindingService); + + // build viewmodel + vm = new GridViewDataSet() + { + PagingOptions = + { + PageSize = 10, + TotalItemsCount = 65 + }, + SortingOptions = { SortExpression = nameof(TestDto.Id) } + }; + + // create page + dataContextStack = DataContextStack.Create(vm.GetType()); + control = new DotvvmView() { DataContext = vm }; + } + + [TestMethod] + public void GridViewDataSet_DataPagerCommands_Command() + { + // create control with page index data context + var pageIndexControl = new PlaceHolder(); + var pageIndexDataContextStack = DataContextStack.Create(typeof(int), dataContextStack); + pageIndexControl.SetDataContextType(pageIndexDataContextStack); + pageIndexControl.SetProperty(p => p.DataContext, ValueOrBinding.FromBoxedValue(1)); + control.Children.Add(pageIndexControl); + + // get pager commands + var commands = commandProvider.GetDataPagerCommands(dataContextStack, GridViewDataSetCommandType.Command); + + // test evaluation of commands + Assert.IsNotNull(commands.GoToLastPage); + vm.IsRefreshRequired = false; + commands.GoToLastPage.Evaluate(control); + Assert.AreEqual(6, vm.PagingOptions.PageIndex); + Assert.IsTrue(vm.IsRefreshRequired); + + Assert.IsNotNull(commands.GoToPreviousPage); + vm.IsRefreshRequired = false; + commands.GoToPreviousPage.Evaluate(control); + Assert.AreEqual(5, vm.PagingOptions.PageIndex); + Assert.IsTrue(vm.IsRefreshRequired); + + Assert.IsNotNull(commands.GoToNextPage); + vm.IsRefreshRequired = false; + commands.GoToNextPage.Evaluate(control); + Assert.AreEqual(6, vm.PagingOptions.PageIndex); + Assert.IsTrue(vm.IsRefreshRequired); + + Assert.IsNotNull(commands.GoToFirstPage); + vm.IsRefreshRequired = false; + commands.GoToFirstPage.Evaluate(control); + Assert.AreEqual(0, vm.PagingOptions.PageIndex); + Assert.IsTrue(vm.IsRefreshRequired); + + Assert.IsNotNull(commands.GoToPage); + vm.IsRefreshRequired = false; + commands.GoToPage.Evaluate(pageIndexControl); + Assert.AreEqual(1, vm.PagingOptions.PageIndex); + Assert.IsTrue(vm.IsRefreshRequired); + } + + [TestMethod] + public void GridViewDataSet_GridViewCommands_Command() + { + // get gridview commands + var commands = commandProvider.GetGridViewCommands(dataContextStack, GridViewDataSetCommandType.Command); + + // test evaluation of commands + Assert.IsNotNull(commands.SetSortExpression); + vm.IsRefreshRequired = false; + commands.SetSortExpression.Evaluate(control, _ => "Name"); + Assert.AreEqual("Name", vm.SortingOptions.SortExpression); + Assert.IsFalse(vm.SortingOptions.SortDescending); + Assert.IsTrue(vm.IsRefreshRequired); + + vm.IsRefreshRequired = false; + commands.SetSortExpression.Evaluate(control, _ => "Name"); + Assert.AreEqual("Name", vm.SortingOptions.SortExpression); + Assert.IsTrue(vm.SortingOptions.SortDescending); + Assert.IsTrue(vm.IsRefreshRequired); + + vm.IsRefreshRequired = false; + commands.SetSortExpression.Evaluate(control, _ => "Id"); + Assert.AreEqual("Id", vm.SortingOptions.SortExpression); + Assert.IsFalse(vm.SortingOptions.SortDescending); + Assert.IsTrue(vm.IsRefreshRequired); + } + + + [TestMethod] + public void GridViewDataSet_DataPagerCommands_StaticCommand() + { + // get pager commands + var commands = commandProvider.GetDataPagerCommands(dataContextStack, GridViewDataSetCommandType.StaticCommand); + + var goToFirstPage = CompileBinding(commands.GoToFirstPage); + } + + private string CompileBinding(ICommandBinding staticCommand) + { + return KnockoutHelper.GenerateClientPostBackExpression( + "", + staticCommand, + new Literal(), + new PostbackScriptOptions( + allowPostbackHandlers: false, + returnValue: null, + commandArgs: CodeParameterAssignment.FromLiteral("commandArguments") + )); + } + + class TestDto + { + public int Id { get; set; } + + public string Name { get; set; } + } + } +}