From 55d7388543dfa35ed003ae2cd32a5abaf43ed71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 14 Jan 2023 15:46:25 +0100 Subject: [PATCH 01/60] Refactoring of GridViewDataSet --- .../Core/Controls/GenericGridViewDataSet.cs | 96 ++++++++++ .../Core/Controls/GridViewDataSet.cs | 164 +----------------- .../Controls/GridViewDataSetExtensions.cs | 51 +++--- .../Core/Controls/IBaseGridViewDataSet.cs | 4 +- .../Controls/IFilterableGridViewDataSet.cs | 22 +++ .../Core/Controls/IGridViewDataSet.cs | 4 +- .../Core/Controls/IPageableGridViewDataSet.cs | 15 +- .../Controls/IRefreshableGridViewDataSet.cs | 8 +- .../Core/Controls/IRowEditGridViewDataSet.cs | 14 +- .../Controls/IRowInsertGridViewDataSet.cs | 12 +- .../Core/Controls/ISortableGridViewDataSet.cs | 15 +- .../DistanceNearPageIndexesProvider.cs | 37 ---- .../Controls/Options/IApplyToQueryable.cs | 9 + .../Controls/Options/IFilteringOptions.cs | 6 + .../Options/INearPageIndexesProvider.cs | 4 +- .../Core/Controls/Options/IPagingOptions.cs | 65 +++++-- .../Controls/Options/IRowInsertOptions.cs | 17 -- .../Core/Controls/Options/ISortingOptions.cs | 36 +++- .../Options/NextTokenHistoryPagingOptions.cs | 54 ++++++ .../Options/NextTokenPagingOptions.cs | 24 +++ .../Controls/Options/NoFilteringOptions.cs | 9 + .../Controls/Options/NoRowInsertOptions.cs | 6 + .../Controls/Options/PagingImplementation.cs | 22 +++ .../Core/Controls/Options/PagingOptions.cs | 61 ++++++- .../Core/Controls/Options/RowInsertOptions.cs | 5 +- .../Core/Controls/Options/SortingOptions.cs | 33 +++- .../Core/Controls/SortingImplementation.cs | 62 +++++++ src/Framework/Framework/Controls/DataPager.cs | 91 ++++++---- src/Framework/Framework/Controls/GridView.cs | 18 +- .../Framework/Controls/GridViewColumn.cs | 6 +- 30 files changed, 621 insertions(+), 349 deletions(-) create mode 100644 src/Framework/Core/Controls/GenericGridViewDataSet.cs create mode 100644 src/Framework/Core/Controls/IFilterableGridViewDataSet.cs delete mode 100644 src/Framework/Core/Controls/Options/DistanceNearPageIndexesProvider.cs create mode 100644 src/Framework/Core/Controls/Options/IApplyToQueryable.cs create mode 100644 src/Framework/Core/Controls/Options/IFilteringOptions.cs create mode 100644 src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs create mode 100644 src/Framework/Core/Controls/Options/NextTokenPagingOptions.cs create mode 100644 src/Framework/Core/Controls/Options/NoFilteringOptions.cs create mode 100644 src/Framework/Core/Controls/Options/NoRowInsertOptions.cs create mode 100644 src/Framework/Core/Controls/Options/PagingImplementation.cs create mode 100644 src/Framework/Core/Controls/SortingImplementation.cs diff --git a/src/Framework/Core/Controls/GenericGridViewDataSet.cs b/src/Framework/Core/Controls/GenericGridViewDataSet.cs new file mode 100644 index 0000000000..4700fb8c4f --- /dev/null +++ b/src/Framework/Core/Controls/GenericGridViewDataSet.cs @@ -0,0 +1,96 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace DotVVM.Framework.Controls +{ + public class GenericGridViewDataSet< + T, + TFilteringOptions, + TSortingOptions, + TPagingOptions, + TRowInsertOptions, + TRowEditOptions> + : IGridViewDataSet, + IFilterableGridViewDataSet, + ISortableGridViewDataSet, + IPageableGridViewDataSet, + IRowInsertGridViewDataSet, + IRowEditGridViewDataSet + where TFilteringOptions : IFilteringOptions + where TSortingOptions : ISortingOptions + where TPagingOptions : IPagingOptions + where TRowInsertOptions : IRowInsertOptions + where TRowEditOptions : IRowEditOptions + { + + /// + /// Gets or sets the items for the current page. + /// + public IList Items { get; set; } = new List(); + + /// + /// Gets or sets whether the data should be refreshed. This property is set to true automatically + /// when paging, sorting or other options change. + /// + public bool IsRefreshRequired { get; set; } = true; + + /// + /// Gets or sets the settings for filtering. + /// + public TFilteringOptions FilteringOptions { get; set; } + + /// + /// Gets or sets the settings for sorting. + /// + public TSortingOptions SortingOptions { get; set; } + + /// + /// Gets or sets the settings for paging. + /// + public TPagingOptions PagingOptions { get; set; } + + /// + /// Gets or sets the settings for row (item) insert feature. + /// + public TRowInsertOptions RowInsertOptions { get; set; } + + /// + /// Gets or sets the settings for row (item) edit feature. + /// + public TRowEditOptions RowEditOptions { get; set; } + + + IList IBaseGridViewDataSet.Items => Items is List list ? list : Items.ToList(); + + IFilteringOptions IFilterableGridViewDataSet.FilteringOptions => this.FilteringOptions; + + ISortingOptions ISortableGridViewDataSet.SortingOptions => this.SortingOptions; + + IPagingOptions IPageableGridViewDataSet.PagingOptions => this.PagingOptions; + + IRowInsertOptions IRowInsertGridViewDataSet.RowInsertOptions => this.RowInsertOptions; + + IRowEditOptions IRowEditGridViewDataSet.RowEditOptions => this.RowEditOptions; + + + + public GenericGridViewDataSet(TFilteringOptions filteringOptions, TSortingOptions sortingOptions, TPagingOptions pagingOptions, TRowInsertOptions rowInsertOptions, TRowEditOptions rowEditOptions) + { + FilteringOptions = filteringOptions; + SortingOptions = sortingOptions; + PagingOptions = pagingOptions; + RowInsertOptions = rowInsertOptions; + RowEditOptions = rowEditOptions; + } + + + /// + /// Requests to reload data into the . + /// + public void RequestRefresh() + { + IsRefreshRequired = true; + } + } +} diff --git a/src/Framework/Core/Controls/GridViewDataSet.cs b/src/Framework/Core/Controls/GridViewDataSet.cs index 3492a23e68..38cd02d04f 100644 --- a/src/Framework/Core/Controls/GridViewDataSet.cs +++ b/src/Framework/Core/Controls/GridViewDataSet.cs @@ -1,6 +1,4 @@ using System; -using System.Collections; -using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -12,166 +10,12 @@ namespace DotVVM.Framework.Controls /// Represents a collection of items with paging, sorting and row edit capabilities. /// /// The type of the elements. - public class GridViewDataSet : IGridViewDataSet + public class GridViewDataSet + : GenericGridViewDataSet { - /// - /// Gets or sets whether the data should be refreshed. This property is set to true automatically - /// when paging, sorting or other options change. - /// - public bool IsRefreshRequired { get; set; } = true; - - /// - /// Gets or sets the items for the current page. - /// - public IList Items { get; set; } = new List(); - - IList IBaseGridViewDataSet.Items => Items is List list ? list : Items.ToList(); - - /// - /// Gets or sets the settings for paging. - /// - public IPagingOptions PagingOptions { get; set; } = new PagingOptions(); - - /// - /// Gets or sets the settings for row (item) edit feature. - /// - public IRowEditOptions RowEditOptions { get; set; } = new RowEditOptions(); - - /// - /// Gets or sets the settings for sorting. - /// - public ISortingOptions SortingOptions { get; set; } = new SortingOptions(); - - /// - /// Navigates to the specific page. - /// - /// The zero-based index of the page to navigate to. - public void GoToPage(int index) - { - PagingOptions.PageIndex = index; - RequestRefresh(); - } - - /// - /// Loads data into the from the given source. - /// - /// The source to load data from. - public void LoadFromQueryable(IQueryable source) - { - source = ApplyFilteringToQueryable(source); - Items = ApplyOptionsToQueryable(source).ToList(); - PagingOptions.TotalItemsCount = source.Count(); - IsRefreshRequired = false; - } - - /// - /// Requests to reload data into the . - /// - public virtual void RequestRefresh() - { - IsRefreshRequired = true; - } - - /// - /// Sets the sort expression. If the specified expression is already set, switches the sort direction. - /// - [Obsolete("This method has strange side-effects, assign the expression yourself and reload the DataSet.")] - public virtual void SetSortExpression(string expression) - { - if (SortingOptions.SortExpression == expression) - { - SortingOptions.SortDescending = !SortingOptions.SortDescending; - } - else - { - SortingOptions.SortExpression = expression; - SortingOptions.SortDescending = false; - } - - GoToPage(0); - } - - /// - /// Applies filtering to the before the total number - /// of items is retrieved. - /// - /// The to modify. - public virtual IQueryable ApplyFilteringToQueryable(IQueryable queryable) - { - return queryable; - } - - /// - /// Applies options to the after the total number - /// of items is retrieved. - /// - /// The to modify. - public virtual IQueryable ApplyOptionsToQueryable(IQueryable queryable) - { - queryable = ApplySortingToQueryable(queryable); - queryable = ApplyPagingToQueryable(queryable); - return queryable; - } - - /// - /// Applies sorting to the after the total number - /// of items is retrieved. - /// - /// The to modify. - public virtual IQueryable ApplySortingToQueryable(IQueryable queryable) - { - if (SortingOptions?.SortExpression == null) - { - return queryable; - } - - var parameterExpression = Expression.Parameter(typeof(T), "p"); - Expression sortByExpression = parameterExpression; - foreach (var prop in (SortingOptions.SortExpression ?? "").Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries)) - { - var property = sortByExpression.Type.GetTypeInfo().GetProperty(prop); - if (property == null) - { - throw new Exception($"Could not sort by property '{prop}', since it does not exists."); - } - if (property.GetCustomAttribute() is BindAttribute bind && bind.Direction == Direction.None) - { - throw new Exception($"Cannot sort by an property '{prop}' that has [Bind(Direction.None)]."); - } - if (property.GetCustomAttribute() is ProtectAttribute protect && protect.Settings == ProtectMode.EncryptData) - { - throw new Exception($"Cannot sort by an property '{prop}' that is encrypted."); - } - - sortByExpression = Expression.Property(sortByExpression, property); - } - if (sortByExpression == parameterExpression) // no sorting - { - return queryable; - } - var lambdaExpression = Expression.Lambda(sortByExpression, parameterExpression); - var methodCallExpression = Expression.Call(typeof(Queryable), GetSortingMethodName(), - new[] { parameterExpression.Type, sortByExpression.Type }, - queryable.Expression, - Expression.Quote(lambdaExpression)); - return queryable.Provider.CreateQuery(methodCallExpression); - } - - /// - /// Applies paging to the after the total number - /// of items is retrieved. - /// - /// The to modify. - public virtual IQueryable ApplyPagingToQueryable(IQueryable queryable) - { - return PagingOptions != null && PagingOptions.PageSize > 0 ? - queryable.Skip(PagingOptions.PageSize * PagingOptions.PageIndex).Take(PagingOptions.PageSize) : - queryable; - } - - private string GetSortingMethodName() + public GridViewDataSet() + : base(new NoFilteringOptions(), new SortingOptions(), new PagingOptions(), new NoRowInsertOptions(), new RowEditOptions()) { - return SortingOptions.SortDescending ? "OrderByDescending" : "OrderBy"; } } } diff --git a/src/Framework/Core/Controls/GridViewDataSetExtensions.cs b/src/Framework/Core/Controls/GridViewDataSetExtensions.cs index f5c08448fc..f325d76ff6 100644 --- a/src/Framework/Core/Controls/GridViewDataSetExtensions.cs +++ b/src/Framework/Core/Controls/GridViewDataSetExtensions.cs @@ -1,43 +1,38 @@ +using System; +using System.Linq; + namespace DotVVM.Framework.Controls { public static class GridViewDataSetExtensions { - /// - /// Navigates to the first page. - /// - public static void GoToFirstPage(this IPageableGridViewDataSet dataSet) - { - dataSet.GoToPage(0); - } - /// - /// Navigates to the previous page if possible. - /// - public static void GoToPreviousPage(this IPageableGridViewDataSet dataSet) + public static void LoadFromQueryable(this IGridViewDataSet dataSet, IQueryable queryable) { - if (!dataSet.PagingOptions.IsFirstPage) + if (dataSet.FilteringOptions is not IApplyToQueryable filteringOptions) { - dataSet.GoToPage(dataSet.PagingOptions.PageIndex - 1); + throw new ArgumentException($"The FilteringOptions of {dataSet.GetType()} must implement IApplyToQueryable!"); + } + if (dataSet.SortingOptions is not IApplyToQueryable sortingOptions) + { + throw new ArgumentException($"The SortingOptions of {dataSet.GetType()} must implement IApplyToQueryable!"); + } + if (dataSet.PagingOptions is not IApplyToQueryable pagingOptions) + { + throw new ArgumentException($"The PagingOptions of {dataSet.GetType()} must implement IApplyToQueryable!"); } - } - /// - /// Navigates to the next page if possible. - /// - public static void GoToNextPage(this IPageableGridViewDataSet dataSet) - { - if (!dataSet.PagingOptions.IsLastPage) + var filtered = filteringOptions.ApplyToQueryable(queryable); + var sorted = sortingOptions.ApplyToQueryable(filtered); + var paged = pagingOptions.ApplyToQueryable(sorted); + dataSet.Items = paged.ToList(); + + if (pagingOptions is IPagingTotalItemsCountCapability pagingTotalItemsCount) { - dataSet.GoToPage(dataSet.PagingOptions.PageIndex + 1); + pagingTotalItemsCount.TotalItemsCount = filtered.Count(); } - } - /// - /// Navigates to the last page. - /// - public static void GoToLastPage(this IPageableGridViewDataSet dataSet) - { - dataSet.GoToPage(dataSet.PagingOptions.PagesCount - 1); + dataSet.IsRefreshRequired = false; } + } } diff --git a/src/Framework/Core/Controls/IBaseGridViewDataSet.cs b/src/Framework/Core/Controls/IBaseGridViewDataSet.cs index 05da21466c..63c48c70de 100644 --- a/src/Framework/Core/Controls/IBaseGridViewDataSet.cs +++ b/src/Framework/Core/Controls/IBaseGridViewDataSet.cs @@ -12,7 +12,7 @@ public interface IBaseGridViewDataSet : IBaseGridViewDataSet /// /// Gets the items for the current page. /// - new IList Items { get; } + new IList Items { get; set; } } /// @@ -25,4 +25,4 @@ public interface IBaseGridViewDataSet /// IList Items { get; } } -} \ No newline at end of file +} diff --git a/src/Framework/Core/Controls/IFilterableGridViewDataSet.cs b/src/Framework/Core/Controls/IFilterableGridViewDataSet.cs new file mode 100644 index 0000000000..5aed106e7c --- /dev/null +++ b/src/Framework/Core/Controls/IFilterableGridViewDataSet.cs @@ -0,0 +1,22 @@ +namespace DotVVM.Framework.Controls +{ + /// + /// Extends the base GridViewDataSet with filtering options. + /// + public interface IFilterableGridViewDataSet : IFilterableGridViewDataSet + where TFilteringOptions : IFilteringOptions + { + /// + /// Gets the settings for filtering. + /// + new TFilteringOptions FilteringOptions { get; } + } + + public interface IFilterableGridViewDataSet : IBaseGridViewDataSet + { + /// + /// Gets the settings for filtering. + /// + IFilteringOptions FilteringOptions { get; } + } +} \ No newline at end of file diff --git a/src/Framework/Core/Controls/IGridViewDataSet.cs b/src/Framework/Core/Controls/IGridViewDataSet.cs index cde8a61fa5..202c5fb184 100644 --- a/src/Framework/Core/Controls/IGridViewDataSet.cs +++ b/src/Framework/Core/Controls/IGridViewDataSet.cs @@ -11,7 +11,7 @@ public interface IGridViewDataSet : IGridViewDataSet, IBaseGridViewDataSet /// /// Represents a collection of items with paging, sorting and row edit capabilities. /// - public interface IGridViewDataSet : IPageableGridViewDataSet, ISortableGridViewDataSet, IRowEditGridViewDataSet, IRefreshableGridViewDataSet + public interface IGridViewDataSet : IFilterableGridViewDataSet, ISortableGridViewDataSet, IPageableGridViewDataSet, IRowInsertGridViewDataSet, IRowEditGridViewDataSet, IRefreshableGridViewDataSet { } -} \ No newline at end of file +} diff --git a/src/Framework/Core/Controls/IPageableGridViewDataSet.cs b/src/Framework/Core/Controls/IPageableGridViewDataSet.cs index b8b303d5af..f0dfd82a19 100644 --- a/src/Framework/Core/Controls/IPageableGridViewDataSet.cs +++ b/src/Framework/Core/Controls/IPageableGridViewDataSet.cs @@ -3,17 +3,20 @@ namespace DotVVM.Framework.Controls /// /// Extends the with paging functionality. /// - public interface IPageableGridViewDataSet : IBaseGridViewDataSet + public interface IPageableGridViewDataSet : IPageableGridViewDataSet + where TPagingOptions : IPagingOptions { /// /// Gets the settings for paging. /// - IPagingOptions PagingOptions { get; } + new TPagingOptions PagingOptions { get; } + } + public interface IPageableGridViewDataSet : IBaseGridViewDataSet + { /// - /// Navigates to the specific page. + /// Gets the settings for paging. /// - /// The zero-based index of the page to navigate to. - void GoToPage(int index); + IPagingOptions PagingOptions { get; } } -} \ No newline at end of file +} diff --git a/src/Framework/Core/Controls/IRefreshableGridViewDataSet.cs b/src/Framework/Core/Controls/IRefreshableGridViewDataSet.cs index 1f5c6390d4..bcd0fcf935 100644 --- a/src/Framework/Core/Controls/IRefreshableGridViewDataSet.cs +++ b/src/Framework/Core/Controls/IRefreshableGridViewDataSet.cs @@ -1,6 +1,4 @@ -using System.Threading.Tasks; - -namespace DotVVM.Framework.Controls +namespace DotVVM.Framework.Controls { /// /// Extends the with refresh functionality. @@ -11,11 +9,11 @@ public interface IRefreshableGridViewDataSet : IBaseGridViewDataSet /// Gets whether the data should be refreshed. This property is set to true automatically /// when paging, sorting or other options change. /// - bool IsRefreshRequired { get; } + bool IsRefreshRequired { get; set; } /// /// Requests to reload data into the . /// void RequestRefresh(); } -} \ No newline at end of file +} diff --git a/src/Framework/Core/Controls/IRowEditGridViewDataSet.cs b/src/Framework/Core/Controls/IRowEditGridViewDataSet.cs index 342ac19ad7..c5c95d0085 100644 --- a/src/Framework/Core/Controls/IRowEditGridViewDataSet.cs +++ b/src/Framework/Core/Controls/IRowEditGridViewDataSet.cs @@ -1,8 +1,14 @@ namespace DotVVM.Framework.Controls { - /// - /// Extends the with row edit functionality. - /// + public interface IRowEditGridViewDataSet : IRowEditGridViewDataSet + where TRowEditOptions : IRowEditOptions + { + /// + /// Gets the settings for row (item) edit feature. + /// + new TRowEditOptions RowEditOptions { get; } + } + public interface IRowEditGridViewDataSet : IBaseGridViewDataSet { /// @@ -10,4 +16,4 @@ public interface IRowEditGridViewDataSet : IBaseGridViewDataSet /// IRowEditOptions RowEditOptions { get; } } -} \ No newline at end of file +} diff --git a/src/Framework/Core/Controls/IRowInsertGridViewDataSet.cs b/src/Framework/Core/Controls/IRowInsertGridViewDataSet.cs index f8e6fc3ec0..a4e26bd8fd 100644 --- a/src/Framework/Core/Controls/IRowInsertGridViewDataSet.cs +++ b/src/Framework/Core/Controls/IRowInsertGridViewDataSet.cs @@ -1,21 +1,19 @@ namespace DotVVM.Framework.Controls { + /// /// Extends the with row insert functionality. /// - /// The type of the elements. - public interface IRowInsertGridViewDataSet : IRowInsertGridViewDataSet, IBaseGridViewDataSet - where T : class, new() + public interface IRowInsertGridViewDataSet : IRowInsertGridViewDataSet + where TRowInsertOptions : IRowInsertOptions { + /// /// Gets the settings for row (item) insert feature. /// - new IRowInsertOptions RowInsertOptions { get; } + new TRowInsertOptions RowInsertOptions { get; } } - /// - /// Extends the with row insert functionality. - /// public interface IRowInsertGridViewDataSet : IBaseGridViewDataSet { /// diff --git a/src/Framework/Core/Controls/ISortableGridViewDataSet.cs b/src/Framework/Core/Controls/ISortableGridViewDataSet.cs index 93f062fd3b..fa6b2fcedd 100644 --- a/src/Framework/Core/Controls/ISortableGridViewDataSet.cs +++ b/src/Framework/Core/Controls/ISortableGridViewDataSet.cs @@ -3,16 +3,17 @@ namespace DotVVM.Framework.Controls /// /// Extends the base GridViewDataSet with sorting functionality. /// - public interface ISortableGridViewDataSet : IBaseGridViewDataSet + public interface ISortableGridViewDataSet : ISortableGridViewDataSet + where TSortingOptions : ISortingOptions { /// /// Gets the settings for sorting. /// - ISortingOptions SortingOptions { get; } + new TSortingOptions SortingOptions { get; } + } - /// - /// Sets the sort expression. If the specified expression is already set, switches the sort direction. - /// - void SetSortExpression(string expression); + public interface ISortableGridViewDataSet : IBaseGridViewDataSet + { + ISortingOptions SortingOptions { get; } } -} \ No newline at end of file +} diff --git a/src/Framework/Core/Controls/Options/DistanceNearPageIndexesProvider.cs b/src/Framework/Core/Controls/Options/DistanceNearPageIndexesProvider.cs deleted file mode 100644 index f40ac13687..0000000000 --- a/src/Framework/Core/Controls/Options/DistanceNearPageIndexesProvider.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace DotVVM.Framework.Controls -{ - /// - /// Provides a list of page indexes near current paged based on distance. - /// - public class DistanceNearPageIndexesProvider : INearPageIndexesProvider - { - private readonly int distance; - - /// - /// Initializes a new instance of the class. - /// - /// The distance specifying the range of page indexes to return. - public DistanceNearPageIndexesProvider(int distance) - { - this.distance = distance; - } - - /// - /// Gets a list of page indexes near current page. - /// - /// The settings for paging. - public IList GetIndexes(IPagingOptions pagingOptions) - { - var firstIndex = Math.Max(pagingOptions.PageIndex - distance, 0); - var lastIndex = Math.Min(pagingOptions.PageIndex + distance + 1, pagingOptions.PagesCount); - - return Enumerable - .Range(firstIndex, lastIndex - firstIndex) - .ToList(); - } - } -} diff --git a/src/Framework/Core/Controls/Options/IApplyToQueryable.cs b/src/Framework/Core/Controls/Options/IApplyToQueryable.cs new file mode 100644 index 0000000000..d230c1108d --- /dev/null +++ b/src/Framework/Core/Controls/Options/IApplyToQueryable.cs @@ -0,0 +1,9 @@ +using System.Linq; + +namespace DotVVM.Framework.Controls +{ + public interface IApplyToQueryable + { + IQueryable ApplyToQueryable(IQueryable queryable); + } +} diff --git a/src/Framework/Core/Controls/Options/IFilteringOptions.cs b/src/Framework/Core/Controls/Options/IFilteringOptions.cs new file mode 100644 index 0000000000..e4fea3849c --- /dev/null +++ b/src/Framework/Core/Controls/Options/IFilteringOptions.cs @@ -0,0 +1,6 @@ +namespace DotVVM.Framework.Controls +{ + public interface IFilteringOptions + { + } +} \ No newline at end of file diff --git a/src/Framework/Core/Controls/Options/INearPageIndexesProvider.cs b/src/Framework/Core/Controls/Options/INearPageIndexesProvider.cs index 4f87677195..c457333d58 100644 --- a/src/Framework/Core/Controls/Options/INearPageIndexesProvider.cs +++ b/src/Framework/Core/Controls/Options/INearPageIndexesProvider.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace DotVVM.Framework.Controls @@ -5,6 +6,7 @@ namespace DotVVM.Framework.Controls /// /// Provides a list of page indexes near current page. It can be used to build data pagers. /// + [Obsolete("This interface was removed. If you want to provide your custom page numbers, inherit the PagingOptions class and override the GetNearPageIndexes method.", true)] public interface INearPageIndexesProvider { /// @@ -13,4 +15,4 @@ public interface INearPageIndexesProvider /// The settings for paging. IList GetIndexes(IPagingOptions pagingOptions); } -} \ No newline at end of file +} diff --git a/src/Framework/Core/Controls/Options/IPagingOptions.cs b/src/Framework/Core/Controls/Options/IPagingOptions.cs index ea5fabfc9f..d3141b8333 100644 --- a/src/Framework/Core/Controls/Options/IPagingOptions.cs +++ b/src/Framework/Core/Controls/Options/IPagingOptions.cs @@ -3,43 +3,84 @@ namespace DotVVM.Framework.Controls { /// - /// Represents settings for paging. + /// Represents a marker interface for GridViewDataSet paging options. /// public interface IPagingOptions { - /// - /// Gets or sets a zero-based index of the current page. - /// - int PageIndex { get; set; } + } + + public interface IPagingNextPageCapability : IPagingOptions + { + /// Sets the options to load the next page. + void GoToNextPage(); /// - /// Gets or sets the maximum number of items on a page. + /// Gets whether the represents the last page. /// - int PageSize { get; set; } + bool IsLastPage { get; } + } + + public interface IPagingPreviousPageCapability : IPagingOptions + { + /// Sets the options to load the previous page. + void GoToPreviousPage(); /// - /// Gets or sets the total number of items in the data store without respect to paging. + /// Gets whether the represents the first page. /// - int TotalItemsCount { get; set; } + bool IsFirstPage { get; } + } + + public interface IPagingFirstPageCapability : IPagingOptions + { + /// Sets the options to load the first page. + void GoToFirstPage(); /// /// Gets whether the represents the first page. /// bool IsFirstPage { get; } + } + + public interface IPagingLastPageCapability : IPagingOptions + { + void GoToLastPage(); /// /// Gets whether the represents the last page. /// bool IsLastPage { get; } + } + public interface IPagingPageIndexCapability : IPagingOptions + { /// - /// Gets the total number of pages. + /// Gets or sets a zero-based index of the current page. /// - int PagesCount { get; } + int PageIndex { get; } + + void GoToPage(int pageIndex); /// /// Gets a list of page indexes near the current page. It can be used to build data pagers. /// IList NearPageIndexes { get; } } -} \ No newline at end of file + + public interface IPagingPageSizeCapability : IPagingOptions + { + /// + /// Gets or sets the maximum number of items on a page. + /// + int PageSize { get; } + + } + + public interface IPagingTotalItemsCountCapability : IPagingOptions + { + /// + /// Gets or sets the total number of items in the data store without respect to paging. + /// + int TotalItemsCount { get; set; } + } +} diff --git a/src/Framework/Core/Controls/Options/IRowInsertOptions.cs b/src/Framework/Core/Controls/Options/IRowInsertOptions.cs index afc7eb6225..6a9dae4791 100644 --- a/src/Framework/Core/Controls/Options/IRowInsertOptions.cs +++ b/src/Framework/Core/Controls/Options/IRowInsertOptions.cs @@ -1,26 +1,9 @@ namespace DotVVM.Framework.Controls { - /// - /// Represents settings for row (item) insert feature. - /// - /// The type of inserted row. - public interface IRowInsertOptions : IRowInsertOptions - where T : class, new() - { - /// - /// Gets or sets the row to be inserted to data source. Null means that row insertion is not activated. - /// - new T? InsertedRow { get; set; } - } - /// /// Represents settings for row (item) insert feature. /// public interface IRowInsertOptions { - /// - /// Gets or sets the row to be inserted into data source. Null means that row insertion is not activated. - /// - object? InsertedRow { get; } } } diff --git a/src/Framework/Core/Controls/Options/ISortingOptions.cs b/src/Framework/Core/Controls/Options/ISortingOptions.cs index 006fd8adf4..312c78e311 100644 --- a/src/Framework/Core/Controls/Options/ISortingOptions.cs +++ b/src/Framework/Core/Controls/Options/ISortingOptions.cs @@ -1,18 +1,50 @@ +using System.Collections.Generic; + namespace DotVVM.Framework.Controls { /// /// Represents settings for sorting. /// public interface ISortingOptions + { + } + + public interface ISortingSetSortExpressionCapability : ISortingOptions + { + bool IsSortingAllowed(string sortExpression); + void SetSortExpression(string? sortExpression); + } + + public interface ISortingSingleCriterionCapability : ISortingOptions + { + + /// + /// Gets or sets whether the sort order should be descending. + /// + bool SortDescending { get; } + + /// + /// Gets or sets the name of the property that is used for sorting. Null means the grid should not be sorted. + /// + string? SortExpression { get; } + + } + + public interface ISortingMultipleCriteriaCapability : ISortingOptions + { + IList Criteria { get; } + } + + public sealed record SortCriterion { /// /// Gets or sets whether the sort order should be descending. /// - bool SortDescending { get; set; } + public bool SortDescending { get; set; } /// /// Gets or sets the name of the property that is used for sorting. Null means the grid should not be sorted. /// - string? SortExpression { get; set; } + public string? SortExpression { get; set; } } } diff --git a/src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs b/src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs new file mode 100644 index 0000000000..1c6e2ae45e --- /dev/null +++ b/src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DotVVM.Framework.Controls +{ + // TODO: comments + public class NextTokenHistoryPagingOptions : IPagingFirstPageCapability, IPagingPreviousPageCapability, IPagingPageIndexCapability + { + public List TokenHistory { get; set; } = new List { null }; + + public int PageIndex { get; set; } = 0; + + public bool IsFirstPage => PageIndex == 0; + + public void GoToFirstPage() => PageIndex = 0; + + public bool IsLastPage => PageIndex == TokenHistory.Count - 1; + + public void GoToNextPage() => PageIndex++; + + public void GoToPreviousPage() => PageIndex--; + + public void GoToPage(int pageIndex) + { + if (TokenHistory.Count <= pageIndex) + throw new ArgumentOutOfRangeException(nameof(pageIndex)); + PageIndex = pageIndex; + } + + public IList NearPageIndexes => GetNearPageIndexes(); + + /// + /// Gets a list of page indexes near the current page. Override this method to provide your own strategy. + /// + public virtual IList GetNearPageIndexes() + { + return GetDefaultNearPageIndexes(5); + } + + /// + /// Returns the near page indexes in the maximum specified distance from the current page index. + /// + protected IList GetDefaultNearPageIndexes(int distance) + { + var firstIndex = Math.Max(PageIndex - distance, 0); + var lastIndex = Math.Min(PageIndex + distance + 1, TokenHistory.Count); + return Enumerable + .Range(firstIndex, lastIndex - firstIndex) + .ToList(); + } + + } +} diff --git a/src/Framework/Core/Controls/Options/NextTokenPagingOptions.cs b/src/Framework/Core/Controls/Options/NextTokenPagingOptions.cs new file mode 100644 index 0000000000..b07b5374c9 --- /dev/null +++ b/src/Framework/Core/Controls/Options/NextTokenPagingOptions.cs @@ -0,0 +1,24 @@ +namespace DotVVM.Framework.Controls +{ + // TODO: comments, mention that null in NextPageToken means there is no next page + public class NextTokenPagingOptions : IPagingFirstPageCapability, IPagingNextPageCapability + { + public string? NextPageToken { get; set; } + + public string? CurrentToken { get; set; } + + public bool IsFirstPage => CurrentToken == null; + + public void GoToFirstPage() => CurrentToken = null; + + public bool IsLastPage => NextPageToken == null; + + public void GoToNextPage() + { + if (NextPageToken != null) + { + CurrentToken = NextPageToken; + } + } + } +} diff --git a/src/Framework/Core/Controls/Options/NoFilteringOptions.cs b/src/Framework/Core/Controls/Options/NoFilteringOptions.cs new file mode 100644 index 0000000000..f2c19667f1 --- /dev/null +++ b/src/Framework/Core/Controls/Options/NoFilteringOptions.cs @@ -0,0 +1,9 @@ +using System.Linq; + +namespace DotVVM.Framework.Controls +{ + public class NoFilteringOptions : IFilteringOptions, IApplyToQueryable + { + public IQueryable ApplyToQueryable(IQueryable queryable) => queryable; + } +} diff --git a/src/Framework/Core/Controls/Options/NoRowInsertOptions.cs b/src/Framework/Core/Controls/Options/NoRowInsertOptions.cs new file mode 100644 index 0000000000..35644e8289 --- /dev/null +++ b/src/Framework/Core/Controls/Options/NoRowInsertOptions.cs @@ -0,0 +1,6 @@ +namespace DotVVM.Framework.Controls +{ + public class NoRowInsertOptions : IRowInsertOptions + { + } +} diff --git a/src/Framework/Core/Controls/Options/PagingImplementation.cs b/src/Framework/Core/Controls/Options/PagingImplementation.cs new file mode 100644 index 0000000000..60a9d77851 --- /dev/null +++ b/src/Framework/Core/Controls/Options/PagingImplementation.cs @@ -0,0 +1,22 @@ +using System.Linq; + +namespace DotVVM.Framework.Controls +{ + public static class PagingImplementation + { + + /// + /// Applies paging to the after the total number + /// of items is retrieved. + /// + /// The to modify. + public static IQueryable ApplyPagingToQueryable(IQueryable queryable, TPagingOptions options) + where TPagingOptions : IPagingPageSizeCapability, IPagingPageIndexCapability + { + return options.PageSize > 0 + ? queryable.Skip(options.PageSize * options.PageIndex).Take(options.PageSize) + : queryable; + } + } + +} diff --git a/src/Framework/Core/Controls/Options/PagingOptions.cs b/src/Framework/Core/Controls/Options/PagingOptions.cs index 47b089cea8..3bffaae21c 100644 --- a/src/Framework/Core/Controls/Options/PagingOptions.cs +++ b/src/Framework/Core/Controls/Options/PagingOptions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using DotVVM.Framework.ViewModel; namespace DotVVM.Framework.Controls @@ -7,13 +8,14 @@ namespace DotVVM.Framework.Controls /// /// Represents settings for paging. /// - public class PagingOptions : IPagingOptions + public class PagingOptions : IPagingOptions, IPagingFirstPageCapability, IPagingLastPageCapability, IPagingPreviousPageCapability, IPagingNextPageCapability, IPagingPageIndexCapability, IPagingPageSizeCapability, IPagingTotalItemsCountCapability, IApplyToQueryable { /// /// Gets or sets the object that provides a list of page indexes near the current page. /// [Bind(Direction.None)] - public INearPageIndexesProvider NearPageIndexesProvider { get; set; } = new DistanceNearPageIndexesProvider(5); + [Obsolete("This interface was removed. If you want to provide your custom page numbers, inherit the PagingOptions class and override the GetNearPageIndexes method.", true)] + public INearPageIndexesProvider NearPageIndexesProvider { get; set; } /// /// Gets whether the represents the first page. @@ -55,9 +57,62 @@ public int PagesCount /// public int TotalItemsCount { get; set; } + public void GoToFirstPage() => PageIndex = 0; + + public void GoToLastPage() => PageIndex = PagesCount - 1; + + public void GoToNextPage() + { + if (PageIndex < PagesCount - 1) + { + PageIndex++; + } + } + public void GoToPreviousPage() + { + if (PageIndex > 0) + { + PageIndex--; + } + } + + public void GoToPage(int pageIndex) + { + if (PageIndex >= 0 && PageIndex < PagesCount) + { + PageIndex = pageIndex; + } + } + + /// /// Gets a list of page indexes near the current page. It can be used to build data pagers. /// - public IList NearPageIndexes => NearPageIndexesProvider.GetIndexes(this); + public IList NearPageIndexes => GetNearPageIndexes(); + + /// + /// Gets a list of page indexes near the current page. Override this method to provide your own strategy. + /// + public virtual IList GetNearPageIndexes() + { + return GetDefaultNearPageIndexes(5); + } + + /// + /// Returns the near page indexes in the maximum specified distance from the current page index. + /// + protected IList GetDefaultNearPageIndexes(int distance) + { + var firstIndex = Math.Max(PageIndex - distance, 0); + var lastIndex = Math.Min(PageIndex + distance + 1, PagesCount); + return Enumerable + .Range(firstIndex, lastIndex - firstIndex) + .ToList(); + } + + public IQueryable ApplyToQueryable(IQueryable queryable) + { + return PagingImplementation.ApplyPagingToQueryable(queryable, this); + } } } diff --git a/src/Framework/Core/Controls/Options/RowInsertOptions.cs b/src/Framework/Core/Controls/Options/RowInsertOptions.cs index 4f376d6162..d2f771af7e 100644 --- a/src/Framework/Core/Controls/Options/RowInsertOptions.cs +++ b/src/Framework/Core/Controls/Options/RowInsertOptions.cs @@ -4,13 +4,12 @@ /// Represents settings for row (item) insert feature. /// /// The type of inserted row. - public class RowInsertOptions : IRowInsertOptions where T : class, new() + public class RowInsertOptions : IRowInsertOptions + where T : class, new() { /// /// Gets or sets the row to be inserted to data source. /// public T? InsertedRow { get; set; } - - object? IRowInsertOptions.InsertedRow => InsertedRow; } } diff --git a/src/Framework/Core/Controls/Options/SortingOptions.cs b/src/Framework/Core/Controls/Options/SortingOptions.cs index e7fe67fc1a..f1b5afdd33 100644 --- a/src/Framework/Core/Controls/Options/SortingOptions.cs +++ b/src/Framework/Core/Controls/Options/SortingOptions.cs @@ -1,9 +1,15 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using DotVVM.Framework.ViewModel; + namespace DotVVM.Framework.Controls { /// /// Represents settings for sorting. /// - public class SortingOptions : ISortingOptions + public class SortingOptions : ISortingSingleCriterionCapability, ISortingSetSortExpressionCapability, IApplyToQueryable { /// /// Gets or sets whether the sort order should be descending. @@ -14,5 +20,30 @@ public class SortingOptions : ISortingOptions /// Gets or sets the name of the property that is used for sorting. Null means the grid should not be sorted. /// public string? SortExpression { get; set; } + + public virtual bool IsSortingAllowed(string sortExpression) => true; + + public virtual void SetSortExpression(string? sortExpression) + { + if (sortExpression == null) + { + SortExpression = null; + SortDescending = false; + } + else if (sortExpression == SortExpression) + { + SortDescending = !SortDescending; + } + else + { + SortExpression = sortExpression; + SortDescending = false; + } + } + + public IQueryable ApplyToQueryable(IQueryable queryable) + { + return SortingImplementation.ApplySortingToQueryable(queryable, this); + } } } diff --git a/src/Framework/Core/Controls/SortingImplementation.cs b/src/Framework/Core/Controls/SortingImplementation.cs new file mode 100644 index 0000000000..dbc7ddf01b --- /dev/null +++ b/src/Framework/Core/Controls/SortingImplementation.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using DotVVM.Framework.ViewModel; + +namespace DotVVM.Framework.Controls +{ + public static class SortingImplementation + { + + /// + /// Applies sorting to the after the total number + /// of items is retrieved. + /// + /// The to modify. + public static IQueryable ApplySortingToQueryable(IQueryable queryable, ISortingSingleCriterionCapability options) + { + if (options.SortExpression == null) + { + return queryable; + } + + var parameterExpression = Expression.Parameter(typeof(T), "p"); + Expression sortByExpression = parameterExpression; + foreach (var prop in (options.SortExpression ?? "").Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries)) + { + var property = sortByExpression.Type.GetProperty(prop); + if (property == null) + { + throw new Exception($"Could not sort by property '{prop}', since it does not exists."); + } + if (property.GetCustomAttribute() is BindAttribute bind && bind.Direction == Direction.None) + { + throw new Exception($"Cannot sort by an property '{prop}' that has [Bind(Direction.None)]."); + } + if (property.GetCustomAttribute() is ProtectAttribute protect && protect.Settings == ProtectMode.EncryptData) + { + throw new Exception($"Cannot sort by an property '{prop}' that is encrypted."); + } + + sortByExpression = Expression.Property(sortByExpression, property); + } + if (sortByExpression == parameterExpression) // no sorting + { + return queryable; + } + var lambdaExpression = Expression.Lambda(sortByExpression, parameterExpression); + var methodCallExpression = Expression.Call(typeof(Queryable), GetSortingMethodName(options.SortDescending), + new[] { parameterExpression.Type, sortByExpression.Type }, + queryable.Expression, + Expression.Quote(lambdaExpression)); + return queryable.Provider.CreateQuery(methodCallExpression); + } + + private static string GetSortingMethodName(bool sortDescending) + { + return sortDescending ? "OrderByDescending" : "OrderBy"; + } + + } +} diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index 69509e5c76..47089b6904 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -30,11 +30,11 @@ public class CommonBindings public CommonBindings(BindingCompilationService service) { - GoToNextPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).GoToNextPage(), "__$DataPager_GoToNextPage"); - GoToThisPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[1]).GoToPage((int)h[0]), "__$DataPager_GoToThisPage"); - GoToPrevPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).GoToPreviousPage(), "__$DataPager_GoToPrevPage"); - GoToFirstPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).GoToFirstPage(), "__$DataPager_GoToFirstPage"); - GoToLastPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).GoToLastPage(), "__$DataPager_GoToLastPage"); + GoToNextPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).PagingOptions.GoToNextPage(), "__$DataPager_GoToNextPage"); + GoToThisPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[1]).PagingOptions.GoToPage((int)h[0]), "__$DataPager_GoToThisPage"); + GoToPrevPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).PagingOptions.GoToPreviousPage(), "__$DataPager_GoToPrevPage"); + GoToFirstPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).PagingOptions.GoToFirstPage(), "__$DataPager_GoToFirstPage"); + GoToLastPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).PagingOptions.GoToLastPage(), "__$DataPager_GoToLastPage"); } } @@ -51,14 +51,14 @@ public DataPager(CommonBindings commonBindings, BindingCompilationService bindin /// /// Gets or sets the GridViewDataSet object in the viewmodel. /// - [MarkupOptions(AllowHardCodedValue = false)] - public IPageableGridViewDataSet? DataSet + [MarkupOptions(AllowHardCodedValue = false, Required = true)] + public IPageableGridViewDataSet? DataSet { - get { return (IPageableGridViewDataSet?)GetValue(DataSetProperty); } + get { return (IPageableGridViewDataSet?)GetValue(DataSetProperty); } set { SetValue(DataSetProperty, value); } } public static readonly DotvvmProperty DataSetProperty = - DotvvmProperty.Register(c => c.DataSet); + DotvvmProperty.Register?, DataPager>(c => c.DataSet); /// /// Gets or sets the template of the button which moves the user to the first page. @@ -166,7 +166,8 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) { Children.Clear(); - var dataContextType = DataContextStack.Create(typeof(IPageableGridViewDataSet), this.GetDataContextType()); + var dataSetBinding = GetValueBinding(DataSetProperty)!; + var dataContextType = DataContextStack.Create(dataSetBinding.ResultType, this.GetDataContextType()); ContentWrapper = CreateWrapperList(dataContextType); Children.Add(ContentWrapper); @@ -174,43 +175,57 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) object enabledValue = GetValueRaw(EnabledProperty)!; - GoToFirstPageButton = CreateNavigationButton("««", FirstPageTemplate, enabledValue, bindings.GoToFirstPageCommand,context); - ContentWrapper.Children.Add(GoToFirstPageButton); - - GoToPreviousPageButton = CreateNavigationButton("«", PreviousPageTemplate, enabledValue, bindings.GoToPrevPageCommand,context); - ContentWrapper.Children.Add(GoToPreviousPageButton); + if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataContextType.DataContextType)) + { + GoToFirstPageButton = CreateNavigationButton("««", FirstPageTemplate, enabledValue, bindings.GoToFirstPageCommand, context); + ContentWrapper.Children.Add(GoToFirstPageButton); + } - // number fields - NumberButtonsPlaceHolder = new PlaceHolder(); - ContentWrapper.Children.Add(NumberButtonsPlaceHolder); + if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataContextType.DataContextType)) + { + GoToPreviousPageButton = CreateNavigationButton("«", PreviousPageTemplate, enabledValue, bindings.GoToPrevPageCommand, context); + ContentWrapper.Children.Add(GoToPreviousPageButton); + } - var dataSet = DataSet; - if (dataSet != null) + if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataContextType.DataContextType)) { - var i = 0; - foreach (var number in dataSet.PagingOptions.NearPageIndexes) + // number fields + NumberButtonsPlaceHolder = new PlaceHolder(); + ContentWrapper.Children.Add(NumberButtonsPlaceHolder); + + if (DataSet is IPageableGridViewDataSet dataSet) { - var li = new HtmlGenericControl("li"); - li.SetBinding(DataContextProperty, GetNearIndexesBinding(context, i, dataContextType)); - if (number == dataSet.PagingOptions.PageIndex) + var i = 0; + foreach (var number in dataSet.PagingOptions.NearPageIndexes) { - li.Attributes.Set("class", ActiveItemCssClass); + var li = new HtmlGenericControl("li"); + li.SetBinding(DataContextProperty, GetNearIndexesBinding(context, i, dataContextType)); + if (number == dataSet.PagingOptions.PageIndex) + { + li.Attributes.Set("class", ActiveItemCssClass); + } + var link = new LinkButton() { Text = (number + 1).ToString() }; + link.SetBinding(ButtonBase.ClickProperty, bindings.GoToThisPageCommand); + if (!true.Equals(enabledValue)) link.SetValue(LinkButton.EnabledProperty, enabledValue); + li.Children.Add(link); + NumberButtonsPlaceHolder.Children.Add(li); + + i++; } - var link = new LinkButton() { Text = (number + 1).ToString() }; - link.SetBinding(ButtonBase.ClickProperty, bindings.GoToThisPageCommand); - if (!true.Equals(enabledValue)) link.SetValue(LinkButton.EnabledProperty, enabledValue); - li.Children.Add(link); - NumberButtonsPlaceHolder.Children.Add(li); - - i++; } } - GoToNextPageButton = CreateNavigationButton("»", NextPageTemplate, enabledValue, bindings.GoToNextPageCommand, context); - ContentWrapper.Children.Add(GoToNextPageButton); + if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataContextType.DataContextType)) + { + GoToNextPageButton = CreateNavigationButton("»", NextPageTemplate, enabledValue, bindings.GoToNextPageCommand, context); + ContentWrapper.Children.Add(GoToNextPageButton); + } - GoToLastPageButton = CreateNavigationButton("»»", LastPageTemplate, enabledValue, bindings.GoToLastPageCommand, context); - ContentWrapper.Children.Add(GoToLastPageButton); + if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataContextType.DataContextType)) + { + GoToLastPageButton = CreateNavigationButton("»»", LastPageTemplate, enabledValue, bindings.GoToLastPageCommand, context); + ContentWrapper.Children.Add(GoToLastPageButton); + } } protected virtual HtmlGenericControl CreateWrapperList(DataContextStack dataContext) @@ -255,7 +270,7 @@ private ValueBindingExpression GetNearIndexesBinding(IDotvvmRequestContext conte .GetOrAdd(i, _ => ValueBindingExpression.CreateBinding( bindingService.WithoutInitialization(), - h => ((IPageableGridViewDataSet)h[0]!).PagingOptions.NearPageIndexes[i], + h => ((IPageableGridViewDataSet)h[0]!).PagingOptions.NearPageIndexes[i], dataContext)); } diff --git a/src/Framework/Framework/Controls/GridView.cs b/src/Framework/Framework/Controls/GridView.cs index 7579410934..b1bc656e26 100644 --- a/src/Framework/Framework/Controls/GridView.cs +++ b/src/Framework/Framework/Controls/GridView.cs @@ -236,19 +236,13 @@ protected virtual void SortChangedCommand(string? expr) var dataSource = this.DataSource; if (dataSource is null) throw new DotvvmControlException(this, "Cannot execute sort command, DataSource is null"); - var sortOptions = (dataSource as ISortableGridViewDataSet)?.SortingOptions; - if (sortOptions is null) + + var sortOptions = (dataSource as ISortableGridViewDataSet)?.SortingOptions; + if (sortOptions is null || (expr != null && !sortOptions.IsSortingAllowed(expr))) throw new DotvvmControlException(this, "Cannot execute sort command, DataSource does not have sorting options"); - if (sortOptions.SortExpression == expr) - { - sortOptions.SortDescending ^= true; - } - else - { - sortOptions.SortExpression = expr; - sortOptions.SortDescending = false; - } - (dataSource as IPageableGridViewDataSet)?.GoToFirstPage(); + sortOptions.SetSortExpression(expr); + (dataSource as IPageableGridViewDataSet)?.PagingOptions.GoToFirstPage(); + (dataSource as IRefreshableGridViewDataSet)?.RequestRefresh(); } protected virtual void CreateHeaderRow(IDotvvmRequestContext context, Action? sortCommand) diff --git a/src/Framework/Framework/Controls/GridViewColumn.cs b/src/Framework/Framework/Controls/GridViewColumn.cs index 5c0ab92a03..9bca83bc12 100644 --- a/src/Framework/Framework/Controls/GridViewColumn.cs +++ b/src/Framework/Framework/Controls/GridViewColumn.cs @@ -234,13 +234,15 @@ public virtual void CreateFilterControls(IDotvvmRequestContext context, GridView } } - private void SetSortedCssClass(HtmlGenericControl cell, ISortableGridViewDataSet? sortableGridViewDataSet, IValueBinding dataSourceBinding) + private void SetSortedCssClass(HtmlGenericControl cell, ISortableGridViewDataSet? gridViewDataSet, IValueBinding dataSourceBinding) { - if (sortableGridViewDataSet != null) + // TODO: support multiple criteria + if (gridViewDataSet is ISortableGridViewDataSet sortableGridViewDataSet) { var cellAttributes = cell.Attributes; if (!RenderOnServer) { + // TODO: change how the css class binding is generated var gridViewDataSetExpr = dataSourceBinding.GetKnockoutBindingExpression(cell, unwrapped: true); cellAttributes["data-bind"] = $"css: {{ '{SortDescendingHeaderCssClass}': ({gridViewDataSetExpr}).SortingOptions().SortExpression() == '{GetSortExpression()}' && ({gridViewDataSetExpr}).SortingOptions().SortDescending(), '{SortAscendingHeaderCssClass}': ({gridViewDataSetExpr}).SortingOptions().SortExpression() == '{GetSortExpression()}' && !({gridViewDataSetExpr}).SortingOptions().SortDescending()}}"; } From 64a04db503ded566d1e2d5974503c275e312005e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 14 Jan 2023 17:18:00 +0100 Subject: [PATCH 02/60] Fixed warnings and unit tests --- .../Core/Controls/GridViewDataSet.cs | 2 +- .../Core/Controls/Options/IPagingOptions.cs | 54 +++++++++++++++---- .../Core/Controls/Options/ISortingOptions.cs | 24 ++++++++- .../Core/Controls/Options/PagingOptions.cs | 2 +- .../Core/Controls/Options/SortingOptions.cs | 13 ++++- ...alizationTests.SerializeDefaultConfig.json | 20 ++++++- 6 files changed, 99 insertions(+), 16 deletions(-) diff --git a/src/Framework/Core/Controls/GridViewDataSet.cs b/src/Framework/Core/Controls/GridViewDataSet.cs index 38cd02d04f..666a3e674b 100644 --- a/src/Framework/Core/Controls/GridViewDataSet.cs +++ b/src/Framework/Core/Controls/GridViewDataSet.cs @@ -9,7 +9,7 @@ namespace DotVVM.Framework.Controls /// /// Represents a collection of items with paging, sorting and row edit capabilities. /// - /// The type of the elements. + /// The type of the elements in the collection. public class GridViewDataSet : GenericGridViewDataSet { diff --git a/src/Framework/Core/Controls/Options/IPagingOptions.cs b/src/Framework/Core/Controls/Options/IPagingOptions.cs index d3141b8333..0579227e03 100644 --- a/src/Framework/Core/Controls/Options/IPagingOptions.cs +++ b/src/Framework/Core/Controls/Options/IPagingOptions.cs @@ -9,73 +9,107 @@ public interface IPagingOptions { } + /// + /// Represents a paging options which support navigating to the next page. + /// public interface IPagingNextPageCapability : IPagingOptions { - /// Sets the options to load the next page. + /// + /// Modifies the options to load the next page. + /// void GoToNextPage(); /// - /// Gets whether the represents the last page. + /// Gets whether the current page is the last page. /// bool IsLastPage { get; } } + /// + /// Represents a paging options which support navigating to the previous page. + /// public interface IPagingPreviousPageCapability : IPagingOptions { - /// Sets the options to load the previous page. + /// + /// Modifies the options to load the previous page. + /// void GoToPreviousPage(); /// - /// Gets whether the represents the first page. + /// Gets whether the current page is the first page. /// bool IsFirstPage { get; } } + /// + /// Represents a paging options which support navigating to the first page. + /// public interface IPagingFirstPageCapability : IPagingOptions { - /// Sets the options to load the first page. + /// + /// Modifies the options to load the first page. + /// void GoToFirstPage(); /// - /// Gets whether the represents the first page. + /// Gets whether the current page is the first page. /// bool IsFirstPage { get; } } + /// + /// Represents a paging options which support navigating to the last page. + /// public interface IPagingLastPageCapability : IPagingOptions { + /// + /// Modifies the options to load the last page. + /// void GoToLastPage(); /// - /// Gets whether the represents the last page. + /// Gets whether the current page is the last page. /// bool IsLastPage { get; } } + /// + /// Represents a paging options which support navigating to the a specific page by its index. + /// public interface IPagingPageIndexCapability : IPagingOptions { /// /// Gets or sets a zero-based index of the current page. /// int PageIndex { get; } - + + /// + /// Modifies the options to load the specific page. + /// + /// A zero-based index of the new page void GoToPage(int pageIndex); /// - /// Gets a list of page indexes near the current page. It can be used to build data pagers. + /// Gets a list of page indexes near the current page. It can be used to tell the DataPager control which page numbers should be shown to the user. /// IList NearPageIndexes { get; } } + /// + /// Represents a paging options which are aware of the page size. + /// public interface IPagingPageSizeCapability : IPagingOptions { /// /// Gets or sets the maximum number of items on a page. /// - int PageSize { get; } + int PageSize { get; set; } } + /// + /// Represents a paging options which are aware of the total number of items in the data set. + /// public interface IPagingTotalItemsCountCapability : IPagingOptions { /// diff --git a/src/Framework/Core/Controls/Options/ISortingOptions.cs b/src/Framework/Core/Controls/Options/ISortingOptions.cs index 312c78e311..3bc251ff67 100644 --- a/src/Framework/Core/Controls/Options/ISortingOptions.cs +++ b/src/Framework/Core/Controls/Options/ISortingOptions.cs @@ -3,18 +3,31 @@ namespace DotVVM.Framework.Controls { /// - /// Represents settings for sorting. + /// Represents a marker interface for sorting options. /// public interface ISortingOptions { } + /// + /// Represents sorting options that support changing the sort expression by the user. + /// public interface ISortingSetSortExpressionCapability : ISortingOptions { + /// + /// Determines whether the specified property can be used for sorting. + /// bool IsSortingAllowed(string sortExpression); + + /// + /// Modifies the options to be sorted by the specified expression. + /// void SetSortExpression(string? sortExpression); } + /// + /// Represents sorting options that can specify one sorting criterion. + /// public interface ISortingSingleCriterionCapability : ISortingOptions { @@ -30,11 +43,20 @@ public interface ISortingSingleCriterionCapability : ISortingOptions } + /// + /// Represents sorting options which support multiple sort criteria. + /// public interface ISortingMultipleCriteriaCapability : ISortingOptions { + /// + /// Gets or sets the list of sort criteria. + /// IList Criteria { get; } } + /// + /// Represents a sort criterion. + /// public sealed record SortCriterion { /// diff --git a/src/Framework/Core/Controls/Options/PagingOptions.cs b/src/Framework/Core/Controls/Options/PagingOptions.cs index 3bffaae21c..2027720195 100644 --- a/src/Framework/Core/Controls/Options/PagingOptions.cs +++ b/src/Framework/Core/Controls/Options/PagingOptions.cs @@ -15,7 +15,7 @@ public class PagingOptions : IPagingOptions, IPagingFirstPageCapability, IPaging /// [Bind(Direction.None)] [Obsolete("This interface was removed. If you want to provide your custom page numbers, inherit the PagingOptions class and override the GetNearPageIndexes method.", true)] - public INearPageIndexesProvider NearPageIndexesProvider { get; set; } + public INearPageIndexesProvider NearPageIndexesProvider { get; set; } = null!; /// /// Gets whether the represents the first page. diff --git a/src/Framework/Core/Controls/Options/SortingOptions.cs b/src/Framework/Core/Controls/Options/SortingOptions.cs index f1b5afdd33..c35a2b69a3 100644 --- a/src/Framework/Core/Controls/Options/SortingOptions.cs +++ b/src/Framework/Core/Controls/Options/SortingOptions.cs @@ -7,9 +7,9 @@ namespace DotVVM.Framework.Controls { /// - /// Represents settings for sorting. + /// Represents a default implementation of the sorting options. /// - public class SortingOptions : ISortingSingleCriterionCapability, ISortingSetSortExpressionCapability, IApplyToQueryable + public class SortingOptions : ISortingOptions, ISortingSingleCriterionCapability, ISortingSetSortExpressionCapability, IApplyToQueryable { /// /// Gets or sets whether the sort order should be descending. @@ -21,8 +21,14 @@ public class SortingOptions : ISortingSingleCriterionCapability, ISortingSetSort /// public string? SortExpression { get; set; } + /// + /// Determines whether the specified property can be used for sorting. + /// public virtual bool IsSortingAllowed(string sortExpression) => true; + /// + /// Modifies the options to be sorted by the specified expression. + /// public virtual void SetSortExpression(string? sortExpression) { if (sortExpression == null) @@ -41,6 +47,9 @@ public virtual void SetSortExpression(string? sortExpression) } } + /// + /// Applies the sorting options to the specified IQueryable expression. + /// public IQueryable ApplyToQueryable(IQueryable queryable) { return SortingImplementation.ApplySortingToQueryable(queryable, this); diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index 4f5cde2cf5..4eb6003823 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -517,7 +517,8 @@ }, "DotVVM.Framework.Controls.DataPager": { "DataSet": { - "type": "DotVVM.Framework.Controls.IPageableGridViewDataSet, DotVVM.Core", + "type": "DotVVM.Framework.Controls.IPageableGridViewDataSet`1[[DotVVM.Framework.Controls.IPagingOptions, DotVVM.Core, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da]], DotVVM.Core", + "required": true, "onlyBindings": true }, "Enabled": { @@ -1668,6 +1669,23 @@ "isActive": true, "isAttached": true } + }, + "DotvvmMarkupControl-33jwRoNrnlbAOVpOnCErXw==": { + "ShowDescription": { + "type": "System.Boolean", + "defaultValue": false + } + }, + "DotvvmMarkupControl-koCHqjx2oIk1rVwG1PzLJQ==": { + "Click": { + "type": "DotVVM.Framework.Binding.Expressions.Command, DotVVM.Framework", + "isCommand": true + } + }, + "DotvvmMarkupControl-YYPITyOzVEL518wclEMJZw==": { + "SomeProperty": { + "type": "System.String" + } } }, "capabilities": { From 7c5b4de788c38c8c57d4488b68ba865d6610d686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 14 Jan 2023 17:38:19 +0100 Subject: [PATCH 03/60] Fixed refreshing after changing the page --- .../Controls/GridViewDataSetExtensions.cs | 29 +++++++++++++++++++ src/Framework/Framework/Controls/DataPager.cs | 10 +++---- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Framework/Core/Controls/GridViewDataSetExtensions.cs b/src/Framework/Core/Controls/GridViewDataSetExtensions.cs index f325d76ff6..81a6774f2b 100644 --- a/src/Framework/Core/Controls/GridViewDataSetExtensions.cs +++ b/src/Framework/Core/Controls/GridViewDataSetExtensions.cs @@ -34,5 +34,34 @@ public static void LoadFromQueryable(this IGridViewDataSet dataSet, IQuery dataSet.IsRefreshRequired = false; } + public static void GoToFirstPageAndRefresh(this IPageableGridViewDataSet dataSet) + { + dataSet.PagingOptions.GoToFirstPage(); + (dataSet as IRefreshableGridViewDataSet)?.RequestRefresh(); + } + + public static void GoToLastPageAndRefresh(this IPageableGridViewDataSet dataSet) + { + dataSet.PagingOptions.GoToLastPage(); + (dataSet as IRefreshableGridViewDataSet)?.RequestRefresh(); + } + + public static void GoToPreviousPageAndRefresh(this IPageableGridViewDataSet dataSet) + { + dataSet.PagingOptions.GoToPreviousPage(); + (dataSet as IRefreshableGridViewDataSet)?.RequestRefresh(); + } + + public static void GoToNextPageAndRefresh(this IPageableGridViewDataSet dataSet) + { + dataSet.PagingOptions.GoToNextPage(); + (dataSet as IRefreshableGridViewDataSet)?.RequestRefresh(); + } + + public static void GoToPageAndRefresh(this IPageableGridViewDataSet dataSet, int pageIndex) + { + dataSet.PagingOptions.GoToPage(pageIndex); + (dataSet as IRefreshableGridViewDataSet)?.RequestRefresh(); + } } } diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index 47089b6904..6bc04a8fe3 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -30,11 +30,11 @@ public class CommonBindings public CommonBindings(BindingCompilationService service) { - GoToNextPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).PagingOptions.GoToNextPage(), "__$DataPager_GoToNextPage"); - GoToThisPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[1]).PagingOptions.GoToPage((int)h[0]), "__$DataPager_GoToThisPage"); - GoToPrevPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).PagingOptions.GoToPreviousPage(), "__$DataPager_GoToPrevPage"); - GoToFirstPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).PagingOptions.GoToFirstPage(), "__$DataPager_GoToFirstPage"); - GoToLastPageCommand = new CommandBindingExpression(service, h => ((IPageableGridViewDataSet)h[0]).PagingOptions.GoToLastPage(), "__$DataPager_GoToLastPage"); + 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"); } } From bcd1ee07be159689acd470c491da92ef5fa0a7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 14 Jan 2023 17:39:08 +0100 Subject: [PATCH 04/60] Added prototype of JS translations for paging and sorting options --- .../Framework/DotVVM.Framework.csproj | 8 ++ .../Resources/Scripts/dataset/loader.ts | 44 ++++++++ .../Resources/Scripts/dataset/translations.ts | 100 ++++++++++++++++++ .../Resources/Scripts/dotvvm-root.ts | 6 ++ 4 files changed, 158 insertions(+) create mode 100644 src/Framework/Framework/Resources/Scripts/dataset/loader.ts create mode 100644 src/Framework/Framework/Resources/Scripts/dataset/translations.ts diff --git a/src/Framework/Framework/DotVVM.Framework.csproj b/src/Framework/Framework/DotVVM.Framework.csproj index 9bafd8d706..4ec57eb775 100644 --- a/src/Framework/Framework/DotVVM.Framework.csproj +++ b/src/Framework/Framework/DotVVM.Framework.csproj @@ -81,6 +81,9 @@ TextTemplatingFilePreprocessor JQueryGlobalizeRegisterTemplate.cs + + Code + @@ -127,6 +130,11 @@ + + + + + diff --git a/src/Framework/Framework/Resources/Scripts/dataset/loader.ts b/src/Framework/Framework/Resources/Scripts/dataset/loader.ts new file mode 100644 index 0000000000..fbeb7122ac --- /dev/null +++ b/src/Framework/Framework/Resources/Scripts/dataset/loader.ts @@ -0,0 +1,44 @@ +type GridViewDataSet = { + PagingOptions: DotvvmObservable, + SortingOptions: DotvvmObservable, + FilteringOptions: DotvvmObservable, + Items: DotvvmObservable, + IsRefreshRequired?: DotvvmObservable +}; +type GridViewDataSetOptions = { + PagingOptions: DotvvmObservable, + SortingOptions: DotvvmObservable, + FilteringOptions: DotvvmObservable +}; +type GridViewDataSetResult = { + Items: DotvvmObservable, + TotalItemsCount?: DotvvmObservable +}; + +export async function loadDataSet(dataSet: GridViewDataSet, loadData: (options: GridViewDataSetOptions) => Promise) { + if (dataSet.IsRefreshRequired) { + dataSet.IsRefreshRequired.setState(true); + } + + const result = await loadData({ + FilteringOptions: dataSet.FilteringOptions, + SortingOptions: dataSet.SortingOptions, + PagingOptions: dataSet.PagingOptions + }); + + dataSet.Items.setState([]); + dataSet.Items.setState(result.Items.state); + + const pagingOptions = dataSet.PagingOptions.state; + const totalItemsCount = result.TotalItemsCount?.state; + if (totalItemsCount && ko.isWriteableObservable(pagingOptions.TotalItemsCount)) { + dataSet.PagingOptions.patchState({ + TotalItemsCount: result.TotalItemsCount + }); + } + + if (dataSet.IsRefreshRequired) { + dataSet.IsRefreshRequired.setState(false); + } +} + diff --git a/src/Framework/Framework/Resources/Scripts/dataset/translations.ts b/src/Framework/Framework/Resources/Scripts/dataset/translations.ts new file mode 100644 index 0000000000..4f0a4844b6 --- /dev/null +++ b/src/Framework/Framework/Resources/Scripts/dataset/translations.ts @@ -0,0 +1,100 @@ +type PagingOptions = { + PageIndex: number, + PagesCount: number +}; + +type NextTokenPagingOptions = { + CurrentToken: string | null, + NextPageToken: string | null +}; + +type NextTokenHistoryPagingOptions = { + PageIndex: number, + TokenHistory: (string | null)[] +}; + +type SortingOptions = { + SortExpression: string | null, + SortDescending: boolean +}; + +export const translations = { + PagingOptions: { + goToFirstPage(options: DotvvmObservable) { + options.patchState({ PageIndex: 0 }); + }, + goToLastPage(options: DotvvmObservable) { + options.patchState({ PageIndex: options.state.PagesCount - 1 }); + }, + goToNextPage(options: DotvvmObservable) { + if (options.state.PageIndex < options.state.PagesCount - 1) { + options.patchState({ PageIndex: options.state.PageIndex + 1 }); + } + }, + goToPreviousPage(options: DotvvmObservable) { + if (options.state.PageIndex > 0) { + options.patchState({ PageIndex: options.state.PageIndex - 1 }); + } + }, + goToPage(options: DotvvmObservable, pageIndex: number) { + if (options.state.PageIndex >= 0 && options.state.PageIndex < options.state.PagesCount) { + options.patchState({ PageIndex: pageIndex }); + } + } + }, + NextTokenPagingOptions: { + goToFirstPage(options: DotvvmObservable) { + options.patchState({ CurrentToken: null }); + }, + goToNextPage(options: DotvvmObservable) { + if (options.state.NextPageToken) { + options.patchState({ CurrentToken: options.state.NextPageToken }); + } + } + }, + NextTokenHistoryPagingOptions: { + 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 }); + } + }, + goToPreviousPage(options: DotvvmObservable) { + if (options.state.PageIndex > 0) { + options.patchState({ PageIndex: options.state.PageIndex - 1 }); + } + }, + goToPage(options: DotvvmObservable, pageIndex: number) { + if (options.state.PageIndex >= 0 && options.state.PageIndex < options.state.TokenHistory.length) { + options.patchState({ PageIndex: pageIndex }); + } + } + }, + + SortingOptions: { + setSortExpression(options: DotvvmObservable, sortExpression: string) { + if (sortExpression == null) { + options.patchState({ + SortExpression: null, + SortDescending: false + }); + } + else if (sortExpression == options.state.SortExpression) { + options.patchState({ + SortDescending: !options.state.SortDescending + }); + } + else { + options.patchState({ + SortExpression: sortExpression, + SortDescending: false + }); + } + } + } +}; diff --git a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts index 40ca264fc5..450203a6c0 100644 --- a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts +++ b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts @@ -27,6 +27,8 @@ 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 * as dataSetTranslations from './dataset/translations' if (window["dotvvm"]) { throw new Error('DotVVM is already loaded!') @@ -124,6 +126,10 @@ const dotvvmExports = { }, options, translations: translations as any, + dataSet: { + load: loadDataSet, + translations: dataSetTranslations + }, StateManager, DotvvmEvent, } From 21dbd102ffbb149b56277dd8838b118632297076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Fri, 20 Jan 2023 17:33:28 +0100 Subject: [PATCH 05/60] DataPager commands refactored and added support for static commands --- .../Framework/Binding/CommonBindings.cs | 17 -- .../JavascriptTranslatableMethodCollection.cs | 61 ++++- .../Javascript/MemberInfoHelper.cs | 6 + src/Framework/Framework/Controls/DataPager.cs | 49 ++-- .../Framework/Controls/DataPagerCommands.cs | 13 + .../Framework/Controls/GridViewCommands.cs | 9 + .../GridViewDataSetBindingProvider.cs | 250 ++++++++++++++++++ .../Controls/GridViewDataSetCommandType.cs | 8 + .../Controls/GridViewDataSetOptions.cs | 14 + .../Controls/GridViewDataSetResult.cs | 26 ++ .../DotVVMServiceCollectionExtensions.cs | 2 +- .../Resources/Scripts/dataset/translations.ts | 3 - src/Tests/ViewModel/GridViewDataSetTests.cs | 149 +++++++++++ 13 files changed, 552 insertions(+), 55 deletions(-) delete mode 100644 src/Framework/Framework/Binding/CommonBindings.cs create mode 100644 src/Framework/Framework/Controls/DataPagerCommands.cs create mode 100644 src/Framework/Framework/Controls/GridViewCommands.cs create mode 100644 src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs create mode 100644 src/Framework/Framework/Controls/GridViewDataSetCommandType.cs create mode 100644 src/Framework/Framework/Controls/GridViewDataSetOptions.cs create mode 100644 src/Framework/Framework/Controls/GridViewDataSetResult.cs create mode 100644 src/Tests/ViewModel/GridViewDataSetTests.cs 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 fbbf532109..f6dbd9724a 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() @@ -244,8 +250,9 @@ JsExpression dictionarySetIndexer(JsExpression[] args, MethodInfo method) => AddDefaultMathTranslations(); AddDefaultDateTimeTranslations(); AddDefaultConvertTranslations(); + AddDataSetOptionsTranslations(); } - + private void AddDefaultToStringTranslations() { AddMethodTranslator(typeof(object).GetMethod("ToString", Type.EmptyTypes), new PrimitiveToStringTranslator()); @@ -831,6 +838,58 @@ JsExpression wrapInRound(JsExpression a) => } } } + + 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 9efb2c044c..a446f281e3 100644 --- a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs +++ b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs @@ -77,7 +77,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; } + } + } +} From 04793ae38c1084da038712b2276656ad0727ec9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 7 Oct 2023 13:46:56 +0200 Subject: [PATCH 06/60] Fixed JS translations APIafter rebase --- .../Javascript/JavascriptTranslatableMethodCollection.cs | 6 ------ .../Framework/Compilation/Javascript/MemberInfoHelper.cs | 5 ----- 2 files changed, 11 deletions(-) diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index f6dbd9724a..52c5525fe4 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -80,12 +80,6 @@ public void AddMethodTranslator(Expression> methodCall, IJavascriptMe CheckNotAccidentalDefinition(method); AddMethodTranslator(method, translator); } - public void AddMethodTranslator(Expression methodCall, IJavascriptMethodTranslator translator) - { - var method = (MethodInfo)MethodFindingHelper.GetMethodFromExpression(methodCall); - CheckNotAccidentalDefinition(method); - AddMethodTranslator(method, translator); - } private void CheckNotAccidentalDefinition(MethodBase m) { diff --git a/src/Framework/Framework/Compilation/Javascript/MemberInfoHelper.cs b/src/Framework/Framework/Compilation/Javascript/MemberInfoHelper.cs index 723d4961b0..248241f59c 100644 --- a/src/Framework/Framework/Compilation/Javascript/MemberInfoHelper.cs +++ b/src/Framework/Framework/Compilation/Javascript/MemberInfoHelper.cs @@ -32,11 +32,6 @@ static Expression UnwrapConversions(Expression e) return e; } - public static MethodBase GetMethodFromExpression(Expression expression) - { - return GetMethodFromExpression((Expression)expression); - } - static MethodBase GetMethodFromExpression(Expression expression) { var originalExpression = expression; From 0100ac69ed57dbd83090b5eb4a97750c0171e74b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 7 Oct 2023 15:39:13 +0200 Subject: [PATCH 07/60] datasets: fix generating client-side reload expression --- .../JavascriptTranslatableMethodCollection.cs | 2 +- .../GridViewDataSetBindingProvider.cs | 36 ++++--------------- src/Tests/ViewModel/GridViewDataSetTests.cs | 5 +-- 3 files changed, 11 insertions(+), 32 deletions(-) diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index 52c5525fe4..6f22f18922 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -837,7 +837,7 @@ private void AddDataSetOptionsTranslations() { // GridViewDataSetBindingProvider AddMethodTranslator(typeof(GridViewDataSetBindingProvider), "DataSetClientSideLoad", new GenericMethodCompiler(args => - new JsIdentifierExpression("dotvvm").Member("dataSet").Member("loadDataSet").Invoke(args[1], args[2]))); + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("loadDataSet").Invoke(args[1].WithAnnotation(ShouldBeObservableAnnotation.Instance), new JsSymbolicParameter(JavascriptTranslator.KnockoutContextParameter).Member("$gridViewDataSetHelper").Member("loadDataSet")).WithAnnotation(new ResultIsPromiseAnnotation(e => e)))); // PagingOptions AddMethodTranslator(() => default(PagingOptions)!.GoToFirstPage(),new GenericMethodCompiler(args => diff --git a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs index 7eafffe236..fda79be2db 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -145,7 +145,8 @@ private ICommandBinding CreateCommandBinding(GridViewDataSetC else if (commandType == GridViewDataSetCommandType.StaticCommand) { // on the client, wrap the call into client-side loading procedure - var expression = WrapInDataSetClientLoad(dataSetParam, body); + body.Add(CallClientSideLoad(dataSetParam)); + Expression expression = Expression.Block(body); if (transformExpression != null) { expression = transformExpression(expression); @@ -165,43 +166,20 @@ private ICommandBinding CreateCommandBinding(GridViewDataSetC } /// - /// Wraps the block expression { dataSet.XXXOptions.Method(); dataSet.RequestRefresh(); } - /// as loaderFunction => { ...; return GridViewDataSetBindingProvider.DataSetClientSideLoad(dataSet, loaderFunction); }); + /// Invoked the client-side loadDataSet function with the loader from $gridViewDataSetHelper /// - private static Expression WrapInDataSetClientLoad(Expression dataSetParam, List body) + private static Expression CallClientSideLoad(Expression dataSetParam) { - // 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); + var method = typeof(GridViewDataSetBindingProvider).GetMethod(nameof(DataSetClientSideLoad))!; + return Expression.Call(method, dataSetParam); } /// /// 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 + public static Task DataSetClientSideLoad(IBaseGridViewDataSet dataSet) { throw new InvalidOperationException("This method cannot be called on the server!"); } diff --git a/src/Tests/ViewModel/GridViewDataSetTests.cs b/src/Tests/ViewModel/GridViewDataSetTests.cs index f9df671b88..9f10233881 100644 --- a/src/Tests/ViewModel/GridViewDataSetTests.cs +++ b/src/Tests/ViewModel/GridViewDataSetTests.cs @@ -124,6 +124,8 @@ public void GridViewDataSet_DataPagerCommands_StaticCommand() var commands = commandProvider.GetDataPagerCommands(dataContextStack, GridViewDataSetCommandType.StaticCommand); var goToFirstPage = CompileBinding(commands.GoToFirstPage); + Console.WriteLine(goToFirstPage); + XAssert.Equal("dotvvm.applyPostbackHandlers(async (options)=>{let vm=options.viewModel;dotvvm.dataSet.translations.PagingOptions.goToFirstPage(vm.PagingOptions);return await dotvvm.dataSet.loadDataSet(vm,options.knockoutContext.$gridViewDataSetHelper.loadDataSet);},this)", goToFirstPage); } private string CompileBinding(ICommandBinding staticCommand) @@ -134,8 +136,7 @@ private string CompileBinding(ICommandBinding staticCommand) new Literal(), new PostbackScriptOptions( allowPostbackHandlers: false, - returnValue: null, - commandArgs: CodeParameterAssignment.FromLiteral("commandArguments") + returnValue: null )); } From 0520ca0e123488df4a3e3fa30b2d2337079e61f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 7 Oct 2023 17:22:48 +0200 Subject: [PATCH 08/60] Add DataPager LoadData property who knows if it works... --- src/Framework/Framework/Controls/DataPager.cs | 46 +++++++---- .../GridViewDataSetBindingProvider.cs | 8 +- src/Framework/Testing/ControlTestHelper.cs | 12 ++- src/Tests/ControlTests/DataPagerTests.cs | 82 +++++++++++++++++++ ...ests.CommandDataPager-command-bindings.txt | 10 +++ .../DataPagerTests.CommandDataPager.html | 27 ++++++ 6 files changed, 160 insertions(+), 25 deletions(-) create mode 100644 src/Tests/ControlTests/DataPagerTests.cs create mode 100644 src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager-command-bindings.txt create mode 100644 src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index a8f9317574..2414cbb4aa 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -27,7 +27,7 @@ public class DataPager : HtmlGenericControl public DataPager(GridViewDataSetBindingProvider gridViewDataSetBindingProvider, BindingCompilationService bindingCompilationService) - : base("div", false) + : base("ul", false) { this.gridViewDataSetBindingProvider = gridViewDataSetBindingProvider; this.bindingCompilationService = bindingCompilationService; @@ -126,6 +126,14 @@ public bool Enabled public static readonly DotvvmProperty EnabledProperty = DotvvmPropertyWithFallback.Register(nameof(Enabled), FormControls.EnabledProperty); + public ICommandBinding? LoadData + { + get => (ICommandBinding?)GetValue(LoadDataProperty); + set => SetValue(LoadDataProperty, value); + } + public static readonly DotvvmProperty LoadDataProperty = + DotvvmProperty.Register(nameof(LoadData)); + protected HtmlGenericControl? ContentWrapper { get; set; } protected HtmlGenericControl? GoToFirstPageButton { get; set; } protected HtmlGenericControl? GoToPreviousPageButton { get; set; } @@ -156,7 +164,9 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) ContentWrapper = CreateWrapperList(dataContextType); Children.Add(ContentWrapper); - pagerCommands = gridViewDataSetBindingProvider.GetDataPagerCommands(dataContextType, GridViewDataSetCommandType.Command); + var commandType = LoadData is {} ? GridViewDataSetCommandType.StaticCommand : GridViewDataSetCommandType.Command; + + pagerCommands = gridViewDataSetBindingProvider.GetDataPagerCommands(dataContextType, commandType); object enabledValue = GetValueRaw(EnabledProperty)!; if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataContextType.DataContextType)) @@ -265,19 +275,27 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest throw new DotvvmControlException(this, "The DataPager control cannot be rendered in the RenderSettings.Mode='Server'."); } - base.AddAttributesToRender(writer, context); + var dataSetBinding = GetDataSetBinding().GetKnockoutBindingExpression(this, unwrapped: true); + var helperBinding = new KnockoutBindingGroup(); + helperBinding.Add("dataSet", dataSetBinding); + if (this.LoadData is {} loadData) + { + var loadDataExpression = KnockoutHelper.GenerateClientPostbackLambda("LoadData", loadData, this, new PostbackScriptOptions(elementAccessor: "$element", koContext: CodeParameterAssignment.FromIdentifier("$context"))); + helperBinding.Add("loadData", loadDataExpression); + } + writer.AddKnockoutDataBind("dotvvm-gridviewdataset", helperBinding); // If Visible property was set to something, it will be overwritten by this. TODO: is it how it should behave? if (HideWhenOnlyOnePage) { if (IsPropertySet(VisibleProperty)) throw new Exception("Visible can't be set on a DataPager when HideWhenOnlyOnePage is true. You can wrap it in an element that hide that or set HideWhenOnlyOnePage to false"); - writer.AddKnockoutDataBind("visible", $"({GetDataSetBinding().GetKnockoutBindingExpression(this, unwrapped: true)}).PagingOptions().PagesCount() > 1"); + writer.AddKnockoutDataBind("visible", $"({dataSetBinding}).PagingOptions().PagesCount() > 1"); } - } - protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext context) - { + writer.AddKnockoutDataBind("with", this, DataSetProperty, renderEvenInServerRenderingMode: true); + + if (GetValueBinding(EnabledProperty) is IValueBinding enabledBinding) { var disabledBinding = enabledBinding.GetProperty().Binding.CastTo(); @@ -288,8 +306,7 @@ protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext writer.AddAttribute("class", DisabledItemCssClass, true); } - writer.AddKnockoutDataBind("with", this, DataSetProperty, renderEvenInServerRenderingMode: true); - writer.RenderBeginTag("ul"); + base.AddAttributesToRender(writer, context); } protected virtual void AddItemCssClass(IHtmlWriter writer, IDotvvmRequestContext context) @@ -311,15 +328,15 @@ protected virtual void AddKnockoutActiveCssDataBind(IHtmlWriter writer, IDotvvmR protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext context) { AddItemCssClass(writer, context); - AddKnockoutDisabledCssDataBind(writer, context, "PagingOptions().IsFirstPage()"); + AddKnockoutDisabledCssDataBind(writer, context, "$gridViewDataSetHelper.dataSet.PagingOptions().IsFirstPage()"); GoToFirstPageButton!.Render(writer, context); AddItemCssClass(writer, context); - AddKnockoutDisabledCssDataBind(writer, context, "PagingOptions().IsFirstPage()"); + AddKnockoutDisabledCssDataBind(writer, context, "$gridViewDataSetHelper.dataSet.PagingOptions().IsFirstPage()"); GoToPreviousPageButton!.Render(writer, context); // render template - writer.WriteKnockoutForeachComment("PagingOptions().NearPageIndexes"); + writer.WriteKnockoutForeachComment("$gridViewDataSetHelper.dataSet.PagingOptions().NearPageIndexes"); // render page number NumberButtonsPlaceHolder!.Children.Clear(); @@ -385,11 +402,6 @@ protected virtual void RenderPageNumberButton(IHtmlWriter writer, IDotvvmRequest li.Render(writer, context); } - protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext context) - { - writer.RenderEndTag(); - } - 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/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs index fda79be2db..69a447a2a9 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -43,9 +43,9 @@ private DataPagerCommands GetDataPagerCommandsCore(DataContextStack dataContextS ? CreateCommandBinding(commandType, dataSetParam, dataContextStack, methodName, arguments) : null; } - ParameterExpression CreateParameter(DataContextStack dataContextStack) + ParameterExpression CreateParameter(DataContextStack dataContextStack, string name = "_this") { - return Expression.Parameter(dataContextStack.DataContextType).AddParameterAnnotation(new BindingParameterAnnotation(dataContextStack)); + return Expression.Parameter(dataContextStack.DataContextType, name).AddParameterAnnotation(new BindingParameterAnnotation(dataContextStack)); } return new DataPagerCommands() @@ -71,10 +71,10 @@ ParameterExpression CreateParameter(DataContextStack dataContextStack) nameof(IPagingLastPageCapability.GoToLastPage)), GoToPage = GetCommandOrNull>( - CreateParameter(dataContextStack), + CreateParameter(dataContextStack, "_parent"), DataContextStack.Create(typeof(int), dataContextStack), nameof(IPagingPageIndexCapability.GoToPage), - CreateParameter(DataContextStack.Create(typeof(int), dataContextStack))) + CreateParameter(DataContextStack.Create(typeof(int), dataContextStack), "_thisIndex")) }; } diff --git a/src/Framework/Testing/ControlTestHelper.cs b/src/Framework/Testing/ControlTestHelper.cs index 13e1fad9b3..f5a26a789b 100644 --- a/src/Framework/Testing/ControlTestHelper.cs +++ b/src/Framework/Testing/ControlTestHelper.cs @@ -335,11 +335,8 @@ public string FormattedHtml return filtered.Single(); } - public async Task RunCommand(string text, Func? viewModel = null, bool applyChanges = true, object[]? args = null, CultureInfo? culture = null) + public async Task RunCommand(CommandBindingExpression command, DotvvmBindableObject control, bool applyChanges = true, object[]? args = null, CultureInfo? culture = null) { - var (control, property, binding) = FindCommand(text, viewModel); - if (binding is CommandBindingExpression command) - { var path = control .GetAllAncestors(true) .Select(a => a.GetDataContextPathFragment()) @@ -365,6 +362,13 @@ public async Task RunCommand(string text, Func? } return r; } + public async Task RunCommand(string text, Func? viewModel = null, bool applyChanges = true, object[]? args = null, CultureInfo? culture = null) + { + var (control, property, binding) = FindCommand(text, viewModel); + if (binding is CommandBindingExpression command) + { + return await RunCommand(command, control, applyChanges, args, culture); + } else { throw new NotSupportedException($"{binding} is not supported."); diff --git a/src/Tests/ControlTests/DataPagerTests.cs b/src/Tests/ControlTests/DataPagerTests.cs new file mode 100644 index 0000000000..5b89945e0f --- /dev/null +++ b/src/Tests/ControlTests/DataPagerTests.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Threading.Tasks; +using CheckTestOutput; +using DotVVM.Framework.Compilation; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Tests.Binding; +using DotVVM.Framework.ViewModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DotVVM.Framework.Testing; +using System.Security.Claims; +using System.Linq; +using DotVVM.Framework.Binding.Properties; +using DotVVM.Framework.Binding; +using FastExpressionCompiler; +using DotVVM.Framework.Binding.Expressions; + +namespace DotVVM.Framework.Tests.ControlTests +{ + [TestClass] + public class DataPagerTests + { + static readonly ControlTestHelper cth = new ControlTestHelper(config: config => { + }); + OutputChecker check = new OutputChecker("testoutputs"); + + [TestMethod] + public async Task CommandDataPager() + { + var r = await cth.RunPage(typeof(GridViewModel), @" + + " + ); + + var commandExpressions = r.Commands + .Select(c => (c.control, c.command, str: c.command.GetProperty().Expression.ToCSharpString().Trim().TrimEnd(';'))) + .OrderBy(c => c.str) + .ToArray(); + check.CheckLines(commandExpressions.Select(c => c.str), checkName: "command-bindings", fileExtension: "txt"); + + check.CheckString(r.FormattedHtml, fileExtension: "html"); + + 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 Customers { get; set; } = new GridViewDataSet() + { + PagingOptions = new PagingOptions() + { + PageSize = 5 + }, + }; + + public override async Task PreRender() + { + if (Customers.IsRefreshRequired) + { + Customers.LoadFromQueryable( + Enumerable.Range(0, 100).Select(i => new CustomerData() { Id = i, Name = "Name" + i }).AsQueryable() + ); + } + } + + public class CustomerData + { + public int Id { get; set; } + [Required] + public string Name { get; set; } + } + } + } +} diff --git a/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager-command-bindings.txt b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager-command-bindings.txt new file mode 100644 index 0000000000..716306d083 --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager-command-bindings.txt @@ -0,0 +1,10 @@ +((PagingOptions)_parent.PagingOptions).GoToPage(_thisIndex); +((IRefreshableGridViewDataSet)_parent).RequestRefresh() +((PagingOptions)_this.PagingOptions).GoToFirstPage(); +((IRefreshableGridViewDataSet)_this).RequestRefresh() +((PagingOptions)_this.PagingOptions).GoToLastPage(); +((IRefreshableGridViewDataSet)_this).RequestRefresh() +((PagingOptions)_this.PagingOptions).GoToNextPage(); +((IRefreshableGridViewDataSet)_this).RequestRefresh() +((PagingOptions)_this.PagingOptions).GoToPreviousPage(); +((IRefreshableGridViewDataSet)_this).RequestRefresh() diff --git a/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html new file mode 100644 index 0000000000..865a9386c5 --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html @@ -0,0 +1,27 @@ + + + + + + From 5bdcbf982a0f24030c3deff82c83279520318036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 7 Oct 2023 17:45:32 +0200 Subject: [PATCH 09/60] MultiCriteriaSortingOptions implemented --- .../Core/Controls/GenericGridViewDataSet.cs | 21 +++++ .../Controls/GridViewDataSetExtensions.cs | 5 +- .../Controls/IFilterableGridViewDataSet.cs | 2 +- .../Options/GridViewDataSetOptions.cs | 18 +++++ .../IPagingOptionsLoadingPostProcessor.cs | 9 +++ .../Options/MultiCriteriaSortingOptions.cs | 77 +++++++++++++++++++ .../Core/Controls/Options/PagingOptions.cs | 7 +- .../Core/Controls/Options/SortingOptions.cs | 4 +- .../Core/Controls/SortingImplementation.cs | 8 +- .../Controls/GridViewDataSetOptions.cs | 14 ---- 10 files changed, 141 insertions(+), 24 deletions(-) create mode 100644 src/Framework/Core/Controls/Options/GridViewDataSetOptions.cs create mode 100644 src/Framework/Core/Controls/Options/IPagingOptionsLoadingPostProcessor.cs create mode 100644 src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs delete mode 100644 src/Framework/Framework/Controls/GridViewDataSetOptions.cs diff --git a/src/Framework/Core/Controls/GenericGridViewDataSet.cs b/src/Framework/Core/Controls/GenericGridViewDataSet.cs index 4700fb8c4f..3b405e6581 100644 --- a/src/Framework/Core/Controls/GenericGridViewDataSet.cs +++ b/src/Framework/Core/Controls/GenericGridViewDataSet.cs @@ -92,5 +92,26 @@ public void RequestRefresh() { IsRefreshRequired = true; } + + /// + /// Applies the options from the specified to this instance. + /// + public void ApplyOptions(GridViewDataSetOptions options) + { + if (options.FilteringOptions != null) + { + FilteringOptions = options.FilteringOptions; + } + + if (options.SortingOptions != null) + { + SortingOptions = options.SortingOptions; + } + + if (options.PagingOptions != null) + { + PagingOptions = options.PagingOptions; + } + } } } diff --git a/src/Framework/Core/Controls/GridViewDataSetExtensions.cs b/src/Framework/Core/Controls/GridViewDataSetExtensions.cs index 81a6774f2b..f10f2812ad 100644 --- a/src/Framework/Core/Controls/GridViewDataSetExtensions.cs +++ b/src/Framework/Core/Controls/GridViewDataSetExtensions.cs @@ -26,9 +26,9 @@ public static void LoadFromQueryable(this IGridViewDataSet dataSet, IQuery var paged = pagingOptions.ApplyToQueryable(sorted); dataSet.Items = paged.ToList(); - if (pagingOptions is IPagingTotalItemsCountCapability pagingTotalItemsCount) + if (pagingOptions is IPagingOptionsLoadingPostProcessor pagingOptionsLoadingPostProcessor) { - pagingTotalItemsCount.TotalItemsCount = filtered.Count(); + pagingOptionsLoadingPostProcessor.ProcessLoadedItems(filtered, dataSet.Items); } dataSet.IsRefreshRequired = false; @@ -63,5 +63,6 @@ public static void GoToPageAndRefresh(this IPageableGridViewDataSet IFilteringOptions FilteringOptions { get; } } -} \ No newline at end of file +} diff --git a/src/Framework/Core/Controls/Options/GridViewDataSetOptions.cs b/src/Framework/Core/Controls/Options/GridViewDataSetOptions.cs new file mode 100644 index 0000000000..1f47c50572 --- /dev/null +++ b/src/Framework/Core/Controls/Options/GridViewDataSetOptions.cs @@ -0,0 +1,18 @@ +namespace DotVVM.Framework.Controls +{ + public class GridViewDataSetOptions + where TFilteringOptions : IFilteringOptions + where TSortingOptions : ISortingOptions + where TPagingOptions : IPagingOptions + { + public TFilteringOptions? FilteringOptions { get; set; } = default; + + public TSortingOptions? SortingOptions { get; set; } = default; + + public TPagingOptions? PagingOptions { get; set; } = default; + } + + public class GridViewDataSetOptions : GridViewDataSetOptions + { + } +} diff --git a/src/Framework/Core/Controls/Options/IPagingOptionsLoadingPostProcessor.cs b/src/Framework/Core/Controls/Options/IPagingOptionsLoadingPostProcessor.cs new file mode 100644 index 0000000000..3f4a923a62 --- /dev/null +++ b/src/Framework/Core/Controls/Options/IPagingOptionsLoadingPostProcessor.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Linq; + +namespace DotVVM.Framework.Controls; + +public interface IPagingOptionsLoadingPostProcessor +{ + void ProcessLoadedItems(IQueryable filteredQueryable, IList items); +} diff --git a/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs b/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs new file mode 100644 index 0000000000..49c6a0f69c --- /dev/null +++ b/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DotVVM.Framework.Controls; + +public class MultiCriteriaSortingOptions : SortingOptions, ISortingMultipleCriteriaCapability +{ + public IList Criteria { get; set; } = new List(); + + public int MaxSortCriteriaCount { get; set; } = 3; + + public override IQueryable ApplyToQueryable(IQueryable queryable) + { + foreach (var criterion in Criteria.Reverse()) + { + queryable = SortingImplementation.ApplySortingToQueryable(queryable, criterion.SortExpression, criterion.SortDescending); + } + + return base.ApplyToQueryable(queryable); + } + + public override void SetSortExpression(string? sortExpression) + { + if (SortExpression == null) + { + SortExpression = sortExpression; + SortDescending = false; + } + else if (sortExpression == SortExpression) + { + if (!SortDescending) + { + SortDescending = true; + } + else if (Criteria.Any()) + { + SortExpression = Criteria[0].SortExpression; + SortDescending = Criteria[0].SortDescending; + Criteria.RemoveAt(0); + } + else + { + SortExpression = null; + SortDescending = false; + } + } + else + { + var index = Criteria.ToList().FindIndex(c => c.SortExpression == sortExpression); + if (index >= 0) + { + if (!Criteria[index].SortDescending) + { + Criteria[index].SortDescending = true; + } + else + { + Criteria.RemoveAt(index); + } + } + else + { + if (Criteria.Count < MaxSortCriteriaCount - 1) + { + Criteria.Add(new SortCriterion() { SortExpression = sortExpression }); + } + else + { + SortExpression = sortExpression; + SortDescending = false; + Criteria.Clear(); + } + } + } + } +} diff --git a/src/Framework/Core/Controls/Options/PagingOptions.cs b/src/Framework/Core/Controls/Options/PagingOptions.cs index 2027720195..0de9c546fc 100644 --- a/src/Framework/Core/Controls/Options/PagingOptions.cs +++ b/src/Framework/Core/Controls/Options/PagingOptions.cs @@ -8,7 +8,7 @@ namespace DotVVM.Framework.Controls /// /// Represents settings for paging. /// - public class PagingOptions : IPagingOptions, IPagingFirstPageCapability, IPagingLastPageCapability, IPagingPreviousPageCapability, IPagingNextPageCapability, IPagingPageIndexCapability, IPagingPageSizeCapability, IPagingTotalItemsCountCapability, IApplyToQueryable + public class PagingOptions : IPagingOptions, IPagingFirstPageCapability, IPagingLastPageCapability, IPagingPreviousPageCapability, IPagingNextPageCapability, IPagingPageIndexCapability, IPagingPageSizeCapability, IPagingTotalItemsCountCapability, IApplyToQueryable, IPagingOptionsLoadingPostProcessor { /// /// Gets or sets the object that provides a list of page indexes near the current page. @@ -114,5 +114,10 @@ public IQueryable ApplyToQueryable(IQueryable queryable) { return PagingImplementation.ApplyPagingToQueryable(queryable, this); } + + public void ProcessLoadedItems(IQueryable filteredQueryable, IList items) + { + TotalItemsCount = filteredQueryable.Count(); + } } } diff --git a/src/Framework/Core/Controls/Options/SortingOptions.cs b/src/Framework/Core/Controls/Options/SortingOptions.cs index c35a2b69a3..9d545fbe66 100644 --- a/src/Framework/Core/Controls/Options/SortingOptions.cs +++ b/src/Framework/Core/Controls/Options/SortingOptions.cs @@ -50,9 +50,9 @@ public virtual void SetSortExpression(string? sortExpression) /// /// Applies the sorting options to the specified IQueryable expression. /// - public IQueryable ApplyToQueryable(IQueryable queryable) + public virtual IQueryable ApplyToQueryable(IQueryable queryable) { - return SortingImplementation.ApplySortingToQueryable(queryable, this); + return SortingImplementation.ApplySortingToQueryable(queryable, SortExpression, SortDescending); } } } diff --git a/src/Framework/Core/Controls/SortingImplementation.cs b/src/Framework/Core/Controls/SortingImplementation.cs index dbc7ddf01b..11683505aa 100644 --- a/src/Framework/Core/Controls/SortingImplementation.cs +++ b/src/Framework/Core/Controls/SortingImplementation.cs @@ -14,16 +14,16 @@ public static class SortingImplementation /// of items is retrieved. /// /// The to modify. - public static IQueryable ApplySortingToQueryable(IQueryable queryable, ISortingSingleCriterionCapability options) + public static IQueryable ApplySortingToQueryable(IQueryable queryable, string? sortExpression, bool sortDescending) { - if (options.SortExpression == null) + if (sortExpression == null) { return queryable; } var parameterExpression = Expression.Parameter(typeof(T), "p"); Expression sortByExpression = parameterExpression; - foreach (var prop in (options.SortExpression ?? "").Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries)) + foreach (var prop in sortExpression.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries)) { var property = sortByExpression.Type.GetProperty(prop); if (property == null) @@ -46,7 +46,7 @@ public static IQueryable ApplySortingToQueryable(IQueryable queryable, return queryable; } var lambdaExpression = Expression.Lambda(sortByExpression, parameterExpression); - var methodCallExpression = Expression.Call(typeof(Queryable), GetSortingMethodName(options.SortDescending), + var methodCallExpression = Expression.Call(typeof(Queryable), GetSortingMethodName(sortDescending), new[] { parameterExpression.Type, sortByExpression.Type }, queryable.Expression, Expression.Quote(lambdaExpression)); diff --git a/src/Framework/Framework/Controls/GridViewDataSetOptions.cs b/src/Framework/Framework/Controls/GridViewDataSetOptions.cs deleted file mode 100644 index ff05a123cc..0000000000 --- a/src/Framework/Framework/Controls/GridViewDataSetOptions.cs +++ /dev/null @@ -1,14 +0,0 @@ -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 From e51b450c9c529482653e6d37b3fa9ed3a6998e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 7 Oct 2023 17:45:42 +0200 Subject: [PATCH 10/60] Sample for static command loading added --- .../GridViewStaticCommandViewModel.cs | 142 ++++++++++++++++-- .../GridView/GridViewStaticCommand.dothtml | 56 +++++-- 2 files changed, 171 insertions(+), 27 deletions(-) diff --git a/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs b/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs index 886eeed48e..d9157dd373 100644 --- a/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs +++ b/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using DotVVM.Framework.Controls; @@ -8,18 +9,18 @@ namespace DotVVM.Samples.BasicSamples.ViewModels.ControlSamples.GridView { public class GridViewStaticCommandViewModel : DotvvmViewModelBase { - public GridViewStaticCommandViewModel() - { - CustomersDataSet = new GridViewDataSet + public GridViewDataSet StandardDataSet { get; set; } = new() { + PagingOptions = new PagingOptions { - PagingOptions = new PagingOptions - { - PageSize = 10 - } - }; - } + PageSize = 10 + } + }; + + public NextTokenGridViewDataSet NextTokenDataSet { get; set; } = new(); + + public NextTokenHistoryGridViewDataSet NextTokenHistoryDataSet { get; set; } = new(); - public GridViewDataSet CustomersDataSet { get; set; } + public MultiSortGridViewDataSet MultiSortDataSet { get; set; } = new(); private static IQueryable GetData() { @@ -29,7 +30,14 @@ private static IQueryable GetData() 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 = 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(); } @@ -38,16 +46,118 @@ public override Task PreRender() // fill dataset if (!Context.IsPostBack) { - CustomersDataSet.LoadFromQueryable(GetData()); + StandardDataSet.LoadFromQueryable(GetData()); + NextTokenDataSet.LoadFromQueryable(GetData()); + NextTokenHistoryDataSet.LoadFromQueryable(GetData()); + MultiSortDataSet.LoadFromQueryable(GetData()); } return base.PreRender(); } [AllowStaticCommand] - public void DeleteCustomerData(int customerId) + public async Task> LoadStandard(GridViewDataSetOptions options) + { + var dataSet = new GridViewDataSet(); + dataSet.ApplyOptions(options); + dataSet.LoadFromQueryable(GetData()); + return dataSet; + } + + [AllowStaticCommand] + public async Task LoadToken(GridViewDataSetOptions options) + { + var dataSet = new NextTokenGridViewDataSet(); + dataSet.ApplyOptions(options); + dataSet.LoadFromQueryable(GetData()); + return dataSet; + } + + [AllowStaticCommand] + public async Task LoadTokenHistory(GridViewDataSetOptions options) + { + var dataSet = new NextTokenHistoryGridViewDataSet(); + dataSet.ApplyOptions(options); + dataSet.LoadFromQueryable(GetData()); + return dataSet; + } + + [AllowStaticCommand] + public async Task LoadMultiSort(GridViewDataSetOptions options) + { + var dataSet = new MultiSortGridViewDataSet(); + dataSet.ApplyOptions(options); + dataSet.LoadFromQueryable(GetData()); + return dataSet; + } + + public class NextTokenGridViewDataSet : GenericGridViewDataSet, RowEditOptions> + { + public NextTokenGridViewDataSet() : base(new NoFilteringOptions(), new SortingOptions(), new CustomerDataNextTokenPagingOptions(), new RowInsertOptions(), new RowEditOptions()) + { + } + } + + public class CustomerDataNextTokenPagingOptions : NextTokenPagingOptions, IApplyToQueryable, IPagingOptionsLoadingPostProcessor + { + public IQueryable ApplyToQueryable(IQueryable queryable) + { + var token = int.Parse(CurrentToken ?? "0"); + + return queryable.Cast() + .OrderBy(c => c.CustomerId) + .Where(c => c.CustomerId > token) + .Take(3) + .Cast(); + } + + public void ProcessLoadedItems(IQueryable filteredQueryable, IList items) + { + var lastToken = items.Cast() + .OrderByDescending(c => c.CustomerId) + .FirstOrDefault()?.CustomerId; + + NextPageToken = (lastToken ?? 0).ToString(); + } + } + + public class NextTokenHistoryGridViewDataSet : GenericGridViewDataSet, RowEditOptions> + { + public NextTokenHistoryGridViewDataSet() : base(new NoFilteringOptions(), new SortingOptions(), new CustomerDataNextTokenHistoryPagingOptions(), new RowInsertOptions(), new RowEditOptions()) + { + } + } + + public class CustomerDataNextTokenHistoryPagingOptions : NextTokenHistoryPagingOptions, IApplyToQueryable, IPagingOptionsLoadingPostProcessor { - var customer = CustomersDataSet.Items.First(s => s != null && s.CustomerId == customerId); - CustomersDataSet.Items.Remove(customer); + public IQueryable ApplyToQueryable(IQueryable queryable) + { + var token = PageIndex < TokenHistory.Count - 1 ? int.Parse(TokenHistory[PageIndex - 1] ?? "0") : 0; + + return queryable.Cast() + .OrderBy(c => c.CustomerId) + .Where(c => c.CustomerId > token) + .Take(3) + .Cast(); + } + + public void ProcessLoadedItems(IQueryable filteredQueryable, IList items) + { + if (PageIndex == TokenHistory.Count) + { + var lastToken = items.Cast() + .OrderByDescending(c => c.CustomerId) + .FirstOrDefault()?.CustomerId; + + TokenHistory.Add((lastToken ?? 0).ToString()); + } + } + } + + public class MultiSortGridViewDataSet : GenericGridViewDataSet, RowEditOptions> + { + public MultiSortGridViewDataSet() : base(new NoFilteringOptions(), new MultiCriteriaSortingOptions(), new PagingOptions(), new RowInsertOptions(), new RowEditOptions()) + { + } } } -} \ No newline at end of file +} diff --git a/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml b/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml index c59d0bdd3c..0e51e74c09 100644 --- a/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml +++ b/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml @@ -8,23 +8,57 @@
-

GridView with IGridViewDataSet

- +

Standard data set

+ > - - - - - - + + + + <%-- +

NextToken paging options

+ + > + + + + + + + + - +

NextTokenHistory data set

+ + > + + + + + + + + + +

MultiSort data set

+ + > + + + + + + - + + --%>
- \ No newline at end of file + From 94d15c74db312363b84a71be094602cfb9376420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 7 Oct 2023 17:52:16 +0200 Subject: [PATCH 11/60] Made DataPager work, at least with commands --- .../JavascriptTranslatableMethodCollection.cs | 6 ++++- .../Javascript/MemberInfoHelper.cs | 2 ++ src/Framework/Framework/Controls/DataPager.cs | 24 ++++++++++++------- .../GridViewDataSetBindingProvider.cs | 13 ++++++++++ 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index 6f22f18922..978b2ee64a 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -836,9 +836,13 @@ JsExpression wrapInRound(JsExpression a) => private void AddDataSetOptionsTranslations() { // GridViewDataSetBindingProvider - AddMethodTranslator(typeof(GridViewDataSetBindingProvider), "DataSetClientSideLoad", new GenericMethodCompiler(args => + AddMethodTranslator(() => GridViewDataSetBindingProvider.DataSetClientSideLoad(null!), new GenericMethodCompiler(args => new JsIdentifierExpression("dotvvm").Member("dataSet").Member("loadDataSet").Invoke(args[1].WithAnnotation(ShouldBeObservableAnnotation.Instance), new JsSymbolicParameter(JavascriptTranslator.KnockoutContextParameter).Member("$gridViewDataSetHelper").Member("loadDataSet")).WithAnnotation(new ResultIsPromiseAnnotation(e => e)))); + AddMethodTranslator(() => GridViewDataSetBindingProvider.GetCurrentGridDataSet(), new GenericMethodCompiler(args => + new JsSymbolicParameter(JavascriptTranslator.KnockoutContextParameter).Member("$gridViewDataSetHelper").Member("dataSet") + )); + // PagingOptions AddMethodTranslator(() => default(PagingOptions)!.GoToFirstPage(),new GenericMethodCompiler(args => new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("PagingOptions").Member("goToFirstPage") diff --git a/src/Framework/Framework/Compilation/Javascript/MemberInfoHelper.cs b/src/Framework/Framework/Compilation/Javascript/MemberInfoHelper.cs index 248241f59c..3e658b317c 100644 --- a/src/Framework/Framework/Compilation/Javascript/MemberInfoHelper.cs +++ b/src/Framework/Framework/Compilation/Javascript/MemberInfoHelper.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using DotVVM.Framework.Controls; namespace DotVVM.Framework.Compilation.Javascript { @@ -154,6 +155,7 @@ public static class Generic { public record T { } public enum Enum { Something } public record struct Struct { } + public class DataSet: GridViewDataSet { } } } } diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index 2414cbb4aa..ade19dd426 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -189,10 +189,12 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) if (DataSet is IPageableGridViewDataSet dataSet) { + var currentPageTextContext = DataContextStack.Create(typeof(int), dataContextType); var i = 0; foreach (var number in dataSet.PagingOptions.NearPageIndexes) { var li = new HtmlGenericControl("li"); + li.SetDataContextType(currentPageTextContext); li.SetBinding(DataContextProperty, GetNearIndexesBinding(context, i, dataContextType)); if (number == dataSet.PagingOptions.PageIndex) { @@ -283,7 +285,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest var loadDataExpression = KnockoutHelper.GenerateClientPostbackLambda("LoadData", loadData, this, new PostbackScriptOptions(elementAccessor: "$element", koContext: CodeParameterAssignment.FromIdentifier("$context"))); helperBinding.Add("loadData", loadDataExpression); } - writer.AddKnockoutDataBind("dotvvm-gridviewdataset", helperBinding); + writer.AddKnockoutDataBind("dotvvm-gridviewdataset", helperBinding.ToString()); // If Visible property was set to something, it will be overwritten by this. TODO: is it how it should behave? if (HideWhenOnlyOnePage) @@ -293,7 +295,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest writer.AddKnockoutDataBind("visible", $"({dataSetBinding}).PagingOptions().PagesCount() > 1"); } - writer.AddKnockoutDataBind("with", this, DataSetProperty, renderEvenInServerRenderingMode: true); + // writer.AddKnockoutDataBind("with", this, DataSetProperty, renderEvenInServerRenderingMode: true); if (GetValueBinding(EnabledProperty) is IValueBinding enabledBinding) @@ -345,11 +347,11 @@ protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext writer.WriteKnockoutDataBindEndComment(); AddItemCssClass(writer, context); - AddKnockoutDisabledCssDataBind(writer, context, "PagingOptions().IsLastPage()"); + AddKnockoutDisabledCssDataBind(writer, context, "$gridViewDataSetHelper.dataSet.PagingOptions().IsLastPage()"); GoToNextPageButton!.Render(writer, context); AddItemCssClass(writer, context); - AddKnockoutDisabledCssDataBind(writer, context, "PagingOptions().IsLastPage()"); + AddKnockoutDisabledCssDataBind(writer, context, "$gridViewDataSetHelper.dataSet.PagingOptions().IsLastPage()"); GoToLastPageButton!.Render(writer, context); } @@ -366,7 +368,7 @@ protected virtual void RenderPageNumberButton(IHtmlWriter writer, IDotvvmRequest if (!RenderLinkForCurrentPage) { - writer.AddKnockoutDataBind("visible", "$data == $parent.PagingOptions().PageIndex()"); + writer.AddKnockoutDataBind("visible", "$data == $parentContext.$gridViewDataSetHelper.dataSet.PagingOptions().PageIndex()"); AddItemCssClass(writer, context); writer.AddAttribute("class", ActiveItemCssClass, true); var literal = new Literal(); @@ -377,7 +379,7 @@ protected virtual void RenderPageNumberButton(IHtmlWriter writer, IDotvvmRequest li.Render(writer, context); - writer.AddKnockoutDataBind("visible", "$data != $parent.PagingOptions().PageIndex()"); + writer.AddKnockoutDataBind("visible", "$data != $parentContext.$gridViewDataSetHelper.dataSet.PagingOptions().PageIndex()"); } li = new HtmlGenericControl("li"); @@ -388,9 +390,9 @@ protected virtual void RenderPageNumberButton(IHtmlWriter writer, IDotvvmRequest AddItemCssClass(writer, context); if (RenderLinkForCurrentPage) - AddKnockoutActiveCssDataBind(writer, context, "$data == $parent.PagingOptions().PageIndex()"); + AddKnockoutActiveCssDataBind(writer, context, "$data == $parentContext.$gridViewDataSetHelper.dataSet.PagingOptions().PageIndex()"); - li.SetValue(Internal.PathFragmentProperty, "PagingOptions.NearPageIndexes[$index]"); + li.SetValue(Internal.PathFragmentProperty, "$parent.PagingOptions.NearPageIndexes[$index]"); var link = new LinkButton(); li.Children.Add(link); link.SetDataContextType(currentPageTextContext); @@ -402,6 +404,12 @@ protected virtual void RenderPageNumberButton(IHtmlWriter writer, IDotvvmRequest li.Render(writer, context); } + // protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext context) + // { + // base.RenderEndTag(writer, context); + // writer.WriteKnockoutDataBindEndComment(); + // } + 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/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs index 69a447a2a9..c9af459673 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -139,6 +139,7 @@ private ICommandBinding CreateCommandBinding(GridViewDataSetC new object[] { new ParsedExpressionBindingProperty(expression), + new OriginalStringBindingProperty($"DataPager: _dataSet.{methodName}({string.Join(", ", arguments.AsEnumerable())})"), // For ID generation dataContextStack }); } @@ -175,6 +176,12 @@ private static Expression CallClientSideLoad(Expression dataSetParam) return Expression.Call(method, dataSetParam); } + private static Expression CurrentGridDataSetExpression(Type datasetType) + { + var method = typeof(GridViewDataSetBindingProvider).GetMethod(nameof(GetCurrentGridDataSet))!.MakeGenericMethod(datasetType); + return Expression.Call(method); + } + /// /// 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. @@ -184,6 +191,12 @@ public static Task DataSetClientSideLoad(IBaseGridViewDataSet dataSet) throw new InvalidOperationException("This method cannot be called on the server!"); } + /// Returns the DataSet we currently work on from the $context.$gridViewDataSetHelper.dataSet + public static T GetCurrentGridDataSet() where T : IBaseGridViewDataSet + { + 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)) From 69b960dbcfe7df0cbe3a9d0b463b17e44244e126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 7 Oct 2023 17:57:06 +0200 Subject: [PATCH 12/60] DataPager: returned client-side data context change --- src/Framework/Framework/Controls/DataPager.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index ade19dd426..0165df034b 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -285,7 +285,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest var loadDataExpression = KnockoutHelper.GenerateClientPostbackLambda("LoadData", loadData, this, new PostbackScriptOptions(elementAccessor: "$element", koContext: CodeParameterAssignment.FromIdentifier("$context"))); helperBinding.Add("loadData", loadDataExpression); } - writer.AddKnockoutDataBind("dotvvm-gridviewdataset", helperBinding.ToString()); + writer.WriteKnockoutDataBindComment("dotvvm-gridviewdataset", helperBinding.ToString()); // If Visible property was set to something, it will be overwritten by this. TODO: is it how it should behave? if (HideWhenOnlyOnePage) @@ -295,7 +295,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest writer.AddKnockoutDataBind("visible", $"({dataSetBinding}).PagingOptions().PagesCount() > 1"); } - // writer.AddKnockoutDataBind("with", this, DataSetProperty, renderEvenInServerRenderingMode: true); + writer.AddKnockoutDataBind("with", this, DataSetProperty, renderEvenInServerRenderingMode: true); if (GetValueBinding(EnabledProperty) is IValueBinding enabledBinding) @@ -330,15 +330,15 @@ protected virtual void AddKnockoutActiveCssDataBind(IHtmlWriter writer, IDotvvmR protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext context) { AddItemCssClass(writer, context); - AddKnockoutDisabledCssDataBind(writer, context, "$gridViewDataSetHelper.dataSet.PagingOptions().IsFirstPage()"); + AddKnockoutDisabledCssDataBind(writer, context, "PagingOptions().IsFirstPage()"); GoToFirstPageButton!.Render(writer, context); AddItemCssClass(writer, context); - AddKnockoutDisabledCssDataBind(writer, context, "$gridViewDataSetHelper.dataSet.PagingOptions().IsFirstPage()"); + AddKnockoutDisabledCssDataBind(writer, context, "PagingOptions().IsFirstPage()"); GoToPreviousPageButton!.Render(writer, context); // render template - writer.WriteKnockoutForeachComment("$gridViewDataSetHelper.dataSet.PagingOptions().NearPageIndexes"); + writer.WriteKnockoutForeachComment("PagingOptions().NearPageIndexes"); // render page number NumberButtonsPlaceHolder!.Children.Clear(); @@ -347,11 +347,11 @@ protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext writer.WriteKnockoutDataBindEndComment(); AddItemCssClass(writer, context); - AddKnockoutDisabledCssDataBind(writer, context, "$gridViewDataSetHelper.dataSet.PagingOptions().IsLastPage()"); + AddKnockoutDisabledCssDataBind(writer, context, "PagingOptions().IsLastPage()"); GoToNextPageButton!.Render(writer, context); AddItemCssClass(writer, context); - AddKnockoutDisabledCssDataBind(writer, context, "$gridViewDataSetHelper.dataSet.PagingOptions().IsLastPage()"); + AddKnockoutDisabledCssDataBind(writer, context, "PagingOptions().IsLastPage()"); GoToLastPageButton!.Render(writer, context); } @@ -368,7 +368,7 @@ protected virtual void RenderPageNumberButton(IHtmlWriter writer, IDotvvmRequest if (!RenderLinkForCurrentPage) { - writer.AddKnockoutDataBind("visible", "$data == $parentContext.$gridViewDataSetHelper.dataSet.PagingOptions().PageIndex()"); + writer.AddKnockoutDataBind("visible", "$data == $parent.PagingOptions().PageIndex()"); AddItemCssClass(writer, context); writer.AddAttribute("class", ActiveItemCssClass, true); var literal = new Literal(); @@ -379,7 +379,7 @@ protected virtual void RenderPageNumberButton(IHtmlWriter writer, IDotvvmRequest li.Render(writer, context); - writer.AddKnockoutDataBind("visible", "$data != $parentContext.$gridViewDataSetHelper.dataSet.PagingOptions().PageIndex()"); + writer.AddKnockoutDataBind("visible", "$data != $parent.PagingOptions().PageIndex()"); } li = new HtmlGenericControl("li"); @@ -390,7 +390,7 @@ protected virtual void RenderPageNumberButton(IHtmlWriter writer, IDotvvmRequest AddItemCssClass(writer, context); if (RenderLinkForCurrentPage) - AddKnockoutActiveCssDataBind(writer, context, "$data == $parentContext.$gridViewDataSetHelper.dataSet.PagingOptions().PageIndex()"); + AddKnockoutActiveCssDataBind(writer, context, "$data == $parent.PagingOptions().PageIndex()"); li.SetValue(Internal.PathFragmentProperty, "$parent.PagingOptions.NearPageIndexes[$index]"); var link = new LinkButton(); @@ -404,11 +404,11 @@ protected virtual void RenderPageNumberButton(IHtmlWriter writer, IDotvvmRequest li.Render(writer, context); } - // protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext context) - // { - // base.RenderEndTag(writer, context); - // writer.WriteKnockoutDataBindEndComment(); - // } + protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext context) + { + base.RenderEndTag(writer, context); + writer.WriteKnockoutDataBindEndComment(); + } private IValueBinding GetDataSetBinding() => GetValueBinding(DataSetProperty) ?? throw new DotvvmControlException(this, "The DataSet property of the dot:DataPager control must be set!"); From 3bacfc5b58b2b7c7b8511c5ba29363a9db9ccaa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 7 Oct 2023 18:21:12 +0200 Subject: [PATCH 13/60] Fixed static command paging --- src/Framework/Framework/Controls/DataPager.cs | 2 +- .../Framework/DotVVM.Framework.csproj | 6 +--- .../Resources/Scripts/dataset/loader.ts | 32 +++++++++++-------- .../Resources/Scripts/dotvvm-root.ts | 4 +-- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index 0165df034b..7292dcd86b 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -283,7 +283,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest if (this.LoadData is {} loadData) { var loadDataExpression = KnockoutHelper.GenerateClientPostbackLambda("LoadData", loadData, this, new PostbackScriptOptions(elementAccessor: "$element", koContext: CodeParameterAssignment.FromIdentifier("$context"))); - helperBinding.Add("loadData", loadDataExpression); + helperBinding.Add("loadDataSet", loadDataExpression); } writer.WriteKnockoutDataBindComment("dotvvm-gridviewdataset", helperBinding.ToString()); diff --git a/src/Framework/Framework/DotVVM.Framework.csproj b/src/Framework/Framework/DotVVM.Framework.csproj index 4ec57eb775..0e163e9521 100644 --- a/src/Framework/Framework/DotVVM.Framework.csproj +++ b/src/Framework/Framework/DotVVM.Framework.csproj @@ -128,11 +128,7 @@ - - - - - + diff --git a/src/Framework/Framework/Resources/Scripts/dataset/loader.ts b/src/Framework/Framework/Resources/Scripts/dataset/loader.ts index fbeb7122ac..78f8d3121f 100644 --- a/src/Framework/Framework/Resources/Scripts/dataset/loader.ts +++ b/src/Framework/Framework/Resources/Scripts/dataset/loader.ts @@ -11,30 +11,36 @@ type GridViewDataSetOptions = { FilteringOptions: DotvvmObservable }; type GridViewDataSetResult = { - Items: DotvvmObservable, - TotalItemsCount?: DotvvmObservable + Items: any[], + PagingOptions: any, + SortingOptions: any, + FilteringOptions: any }; -export async function loadDataSet(dataSet: GridViewDataSet, loadData: (options: GridViewDataSetOptions) => Promise) { +export async function loadDataSet(dataSetObservable: KnockoutObservable, loadData: (options: GridViewDataSetOptions) => Promise) { + const dataSet = ko.unwrap(dataSetObservable); if (dataSet.IsRefreshRequired) { dataSet.IsRefreshRequired.setState(true); } const result = await loadData({ - FilteringOptions: dataSet.FilteringOptions, - SortingOptions: dataSet.SortingOptions, - PagingOptions: dataSet.PagingOptions + FilteringOptions: dataSet.FilteringOptions.state, + SortingOptions: dataSet.SortingOptions.state, + PagingOptions: dataSet.PagingOptions.state }); + const commandResult = result.commandResult as GridViewDataSetResult; dataSet.Items.setState([]); - dataSet.Items.setState(result.Items.state); + dataSet.Items.setState(commandResult.Items); - const pagingOptions = dataSet.PagingOptions.state; - const totalItemsCount = result.TotalItemsCount?.state; - if (totalItemsCount && ko.isWriteableObservable(pagingOptions.TotalItemsCount)) { - dataSet.PagingOptions.patchState({ - TotalItemsCount: result.TotalItemsCount - }); + 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); } if (dataSet.IsRefreshRequired) { diff --git a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts index 450203a6c0..eae8c3a9d7 100644 --- a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts +++ b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts @@ -127,8 +127,8 @@ const dotvvmExports = { options, translations: translations as any, dataSet: { - load: loadDataSet, - translations: dataSetTranslations + loadDataSet: loadDataSet, + translations: dataSetTranslations.translations }, StateManager, DotvvmEvent, From 3132d71ec270d8a3d5e85395ad73296d98f60f00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 22 Oct 2023 15:32:04 +0200 Subject: [PATCH 14/60] Introduce DataContextStart.CreateCollectionElement Creates a nested data context with _index and _collection extension parameters --- .../GeneralBindingPropertyResolvers.cs | 5 ++--- .../ControlTree/DataContextStack.cs | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/Framework/Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs b/src/Framework/Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs index e0ffccd615..e29eb171f0 100644 --- a/src/Framework/Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs +++ b/src/Framework/Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs @@ -427,10 +427,9 @@ public ThisBindingProperty GetThisBinding(IBinding binding, DataContextStack sta public CollectionElementDataContextBindingProperty GetCollectionElementDataContext(DataContextStack dataContext, ResultTypeBindingProperty resultType) { - return new CollectionElementDataContextBindingProperty(DataContextStack.Create( + return new CollectionElementDataContextBindingProperty(DataContextStack.CreateCollectionElement( ReflectionUtils.GetEnumerableType(resultType.Type).NotNull(), - parent: dataContext, - extensionParameters: new CollectionElementDataContextChangeAttribute(0).GetExtensionParameters(new ResolvedTypeDescriptor(dataContext.DataContextType)).ToArray() + dataContext )); } diff --git a/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs b/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs index a655cadc35..f43a04f963 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Linq; using System.Runtime.CompilerServices; +using DotVVM.Framework.Binding; using DotVVM.Framework.Compilation.ControlTree.Resolved; using DotVVM.Framework.Utils; using FastExpressionCompiler; @@ -172,5 +173,23 @@ public static DataContextStack Create(Type type, var dcs = new DataContextStack(type, parent, imports, extensionParameters, bindingPropertyResolvers); return dcs;// internCache.GetValue(dcs, _ => dcs); } + + + /// Creates a new data context level with _index and _collection extension parameters. + public static DataContextStack CreateCollectionElement(Type elementType, + DataContextStack? parent = null, + IReadOnlyList? imports = null, + IReadOnlyList? extensionParameters = null, + IReadOnlyList? bindingPropertyResolvers = null) + { + var indexParameters = new CollectionElementDataContextChangeAttribute(0).GetExtensionParameters(new ResolvedTypeDescriptor(elementType.MakeArrayType())); + extensionParameters = extensionParameters is null ? indexParameters.ToArray() : extensionParameters.Concat(indexParameters).ToArray(); + return DataContextStack.Create( + elementType, parent, + imports: imports, + extensionParameters: extensionParameters, + bindingPropertyResolvers: bindingPropertyResolvers + ); + } } } From 69682f0197d19c2c802bb33cd9d13fc701754dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 22 Oct 2023 15:32:57 +0200 Subject: [PATCH 15/60] Refactor DataPager to rely on Repeater, and not change its data context --- src/Framework/Framework/Controls/DataPager.cs | 188 ++++-------------- .../Framework/Controls/DataPagerCommands.cs | 12 +- .../GridViewDataSetBindingProvider.cs | 81 ++++++-- src/Tests/ControlTests/DataPagerTests.cs | 2 +- ...ests.CommandDataPager-command-bindings.txt | 20 +- .../DataPagerTests.CommandDataPager.html | 27 ++- ...alizationTests.SerializeDefaultConfig.json | 21 +- src/Tests/ViewModel/GridViewDataSetTests.cs | 11 +- 8 files changed, 147 insertions(+), 215 deletions(-) diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index 7292dcd86b..8a300df463 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -23,7 +23,7 @@ public class DataPager : HtmlGenericControl private readonly GridViewDataSetBindingProvider gridViewDataSetBindingProvider; private readonly BindingCompilationService bindingCompilationService; - private DataPagerCommands? pagerCommands; + private DataPagerBindings? pagerBindings; public DataPager(GridViewDataSetBindingProvider gridViewDataSetBindingProvider, BindingCompilationService bindingCompilationService) @@ -137,7 +137,7 @@ public ICommandBinding? LoadData protected HtmlGenericControl? ContentWrapper { get; set; } protected HtmlGenericControl? GoToFirstPageButton { get; set; } protected HtmlGenericControl? GoToPreviousPageButton { get; set; } - protected PlaceHolder? NumberButtonsPlaceHolder { get; set; } + protected Repeater? NumberButtonsRepeater { get; set; } protected HtmlGenericControl? GoToNextPageButton { get; set; } protected HtmlGenericControl? GoToLastPageButton { get; set; } protected virtual string ActiveItemCssClass => "active"; @@ -160,75 +160,70 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) Children.Clear(); var dataSetBinding = GetValueBinding(DataSetProperty)!; - var dataContextType = DataContextStack.Create(dataSetBinding.ResultType, this.GetDataContextType()); - ContentWrapper = CreateWrapperList(dataContextType); + var dataSetType = dataSetBinding.ResultType; + ContentWrapper = CreateWrapperList(); Children.Add(ContentWrapper); var commandType = LoadData is {} ? GridViewDataSetCommandType.StaticCommand : GridViewDataSetCommandType.Command; - pagerCommands = gridViewDataSetBindingProvider.GetDataPagerCommands(dataContextType, commandType); - object enabledValue = GetValueRaw(EnabledProperty)!; + pagerBindings = gridViewDataSetBindingProvider.GetDataPagerCommands(this.GetDataContextType().NotNull(), dataSetBinding, commandType); + + + var enabled = GetValueOrBinding(EnabledProperty)!; - if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataContextType.DataContextType)) + if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataSetType)) { - GoToFirstPageButton = CreateNavigationButton("««", FirstPageTemplate, enabledValue, pagerCommands.GoToFirstPage!, context); + GoToFirstPageButton = CreateNavigationButton("««", FirstPageTemplate, enabled, pagerBindings.GoToFirstPage!, context); + GoToFirstPageButton.CssClasses.Add(DisabledItemCssClass, new ValueOrBinding(pagerBindings.IsFirstPage.NotNull())); ContentWrapper.Children.Add(GoToFirstPageButton); } - if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataContextType.DataContextType)) + if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataSetType)) { - GoToPreviousPageButton = CreateNavigationButton("«", PreviousPageTemplate, enabledValue, pagerCommands.GoToPreviousPage!, context); + GoToPreviousPageButton = CreateNavigationButton("«", PreviousPageTemplate, enabled, pagerBindings.GoToPreviousPage!, context); + GoToPreviousPageButton.CssClasses.Add(DisabledItemCssClass, new ValueOrBinding(pagerBindings.IsFirstPage.NotNull())); ContentWrapper.Children.Add(GoToPreviousPageButton); } - if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataContextType.DataContextType)) + if (pagerBindings.PageNumbers is {}) { // number fields - NumberButtonsPlaceHolder = new PlaceHolder(); - ContentWrapper.Children.Add(NumberButtonsPlaceHolder); - - if (DataSet is IPageableGridViewDataSet dataSet) - { - var currentPageTextContext = DataContextStack.Create(typeof(int), dataContextType); - var i = 0; - foreach (var number in dataSet.PagingOptions.NearPageIndexes) - { - var li = new HtmlGenericControl("li"); - li.SetDataContextType(currentPageTextContext); - li.SetBinding(DataContextProperty, GetNearIndexesBinding(context, i, dataContextType)); - if (number == dataSet.PagingOptions.PageIndex) - { - li.Attributes.Set("class", ActiveItemCssClass); - } - var link = new LinkButton() { Text = (number + 1).ToString() }; - link.SetBinding(ButtonBase.ClickProperty, pagerCommands.GoToPage!); - if (!true.Equals(enabledValue)) link.SetValue(LinkButton.EnabledProperty, enabledValue); - li.Children.Add(link); - NumberButtonsPlaceHolder.Children.Add(li); - - i++; - } - } + var liTemplate = new HtmlGenericControl("li"); + // li.SetDataContextType(currentPageTextContext); + // li.SetBinding(DataContextProperty, GetNearIndexesBinding(context, i, dataContextType)); + liTemplate.CssClasses.Add(ActiveItemCssClass, new ValueOrBinding(pagerBindings.IsActivePage.NotNull())); + var link = new LinkButton(); + link.SetBinding(ButtonBase.ClickProperty, pagerBindings.GoToPage!); + link.SetBinding(ButtonBase.TextProperty, pagerBindings.PageNumberText); + if (!true.Equals(enabled)) link.SetValue(LinkButton.EnabledProperty, enabled); + liTemplate.Children.Add(link); + NumberButtonsRepeater = new Repeater() { + DataSource = pagerBindings.PageNumbers, + RenderWrapperTag = false, + RenderAsNamedTemplate = false, + ItemTemplate = new CloneTemplate(liTemplate) + }; + ContentWrapper.Children.Add(NumberButtonsRepeater); } - if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataContextType.DataContextType)) + if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataSetType)) { - GoToNextPageButton = CreateNavigationButton("»", NextPageTemplate, enabledValue, pagerCommands.GoToNextPage!, context); + GoToNextPageButton = CreateNavigationButton("»", NextPageTemplate, enabled, pagerBindings.GoToNextPage!, context); + GoToNextPageButton.CssClasses.Add(DisabledItemCssClass, new ValueOrBinding(pagerBindings.IsLastPage.NotNull())); ContentWrapper.Children.Add(GoToNextPageButton); } - if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataContextType.DataContextType)) + if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataSetType)) { - GoToLastPageButton = CreateNavigationButton("»»", LastPageTemplate, enabledValue, pagerCommands.GoToLastPage!, context); + GoToLastPageButton = CreateNavigationButton("»»", LastPageTemplate, enabled, pagerBindings.GoToLastPage!, context); + GoToLastPageButton.CssClasses.Add(DisabledItemCssClass, new ValueOrBinding(pagerBindings.IsLastPage.NotNull())); ContentWrapper.Children.Add(GoToLastPageButton); } } - protected virtual HtmlGenericControl CreateWrapperList(DataContextStack dataContext) + protected virtual HtmlGenericControl CreateWrapperList() { var list = new HtmlGenericControl("ul"); - list.SetDataContextType(dataContext); - list.SetBinding(DataContextProperty, GetDataSetBinding()); return list; } @@ -256,20 +251,6 @@ protected virtual void SetButtonContent(Hosting.IDotvvmRequestContext context, L } } - private ConditionalWeakTable> _nearIndexesBindingCache - = new ConditionalWeakTable>(); - - private ValueBindingExpression GetNearIndexesBinding(IDotvvmRequestContext context, int i, DataContextStack? dataContext = null) - { - return - _nearIndexesBindingCache.GetOrCreateValue(context.Configuration) - .GetOrAdd(i, _ => - ValueBindingExpression.CreateBinding( - bindingCompilationService.WithoutInitialization(), - h => ((IPageableGridViewDataSet)h[0]!).PagingOptions.NearPageIndexes[i], - dataContext)); - } - protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context) { if (RenderOnServer) @@ -285,7 +266,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest var loadDataExpression = KnockoutHelper.GenerateClientPostbackLambda("LoadData", loadData, this, new PostbackScriptOptions(elementAccessor: "$element", koContext: CodeParameterAssignment.FromIdentifier("$context"))); helperBinding.Add("loadDataSet", loadDataExpression); } - writer.WriteKnockoutDataBindComment("dotvvm-gridviewdataset", helperBinding.ToString()); + writer.AddKnockoutDataBind("dotvvm-gridviewdataset", helperBinding.ToString()); // If Visible property was set to something, it will be overwritten by this. TODO: is it how it should behave? if (HideWhenOnlyOnePage) @@ -295,9 +276,6 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest writer.AddKnockoutDataBind("visible", $"({dataSetBinding}).PagingOptions().PagesCount() > 1"); } - writer.AddKnockoutDataBind("with", this, DataSetProperty, renderEvenInServerRenderingMode: true); - - if (GetValueBinding(EnabledProperty) is IValueBinding enabledBinding) { var disabledBinding = enabledBinding.GetProperty().Binding.CastTo(); @@ -311,105 +289,25 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest base.AddAttributesToRender(writer, context); } - protected virtual void AddItemCssClass(IHtmlWriter writer, IDotvvmRequestContext context) - { - } - protected virtual void AddKnockoutDisabledCssDataBind(IHtmlWriter writer, IDotvvmRequestContext context, string expression) { writer.AddKnockoutDataBind("css", $"{{ '{DisabledItemCssClass}': {expression} }}"); } - protected virtual void AddKnockoutActiveCssDataBind(IHtmlWriter writer, IDotvvmRequestContext context, string expression) - { - writer.AddKnockoutDataBind("css", $"{{ '{ActiveItemCssClass}': {expression} }}"); - } - - private static ParametrizedCode currentPageTextJs = new JsBinaryExpression(new JsBinaryExpression(new JsLiteral(1), BinaryOperatorType.Plus, new JsSymbolicParameter(JavascriptTranslator.KnockoutViewModelParameter)), BinaryOperatorType.Plus, new JsLiteral("")).FormatParametrizedScript(); - protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext context) { - AddItemCssClass(writer, context); - AddKnockoutDisabledCssDataBind(writer, context, "PagingOptions().IsFirstPage()"); - GoToFirstPageButton!.Render(writer, context); - - AddItemCssClass(writer, context); - AddKnockoutDisabledCssDataBind(writer, context, "PagingOptions().IsFirstPage()"); - GoToPreviousPageButton!.Render(writer, context); - - // render template - writer.WriteKnockoutForeachComment("PagingOptions().NearPageIndexes"); - - // render page number - NumberButtonsPlaceHolder!.Children.Clear(); - RenderPageNumberButton(writer, context); - - writer.WriteKnockoutDataBindEndComment(); - - AddItemCssClass(writer, context); - AddKnockoutDisabledCssDataBind(writer, context, "PagingOptions().IsLastPage()"); - GoToNextPageButton!.Render(writer, context); - - AddItemCssClass(writer, context); - AddKnockoutDisabledCssDataBind(writer, context, "PagingOptions().IsLastPage()"); - GoToLastPageButton!.Render(writer, context); + base.RenderContents(writer, context); } - protected virtual void RenderPageNumberButton(IHtmlWriter writer, IDotvvmRequestContext context) + protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext context) { - HtmlGenericControl li = new HtmlGenericControl("li"); - var currentPageTextContext = DataContextStack.Create(typeof(int), NumberButtonsPlaceHolder!.GetDataContextType()); - li.SetDataContextType(currentPageTextContext); - li.DataContext = null; - var currentPageTextBinding = ValueBindingExpression.CreateBinding(bindingCompilationService.WithoutInitialization(), - vm => ((int) vm[0]! + 1).ToString(), - currentPageTextJs, - currentPageTextContext); - - if (!RenderLinkForCurrentPage) - { - writer.AddKnockoutDataBind("visible", "$data == $parent.PagingOptions().PageIndex()"); - AddItemCssClass(writer, context); - writer.AddAttribute("class", ActiveItemCssClass, true); - var literal = new Literal(); - literal.DataContext = 0; - literal.SetBinding(Literal.TextProperty, currentPageTextBinding); - li.Children.Add(literal); - NumberButtonsPlaceHolder!.Children.Add(li); - - li.Render(writer, context); - - writer.AddKnockoutDataBind("visible", "$data != $parent.PagingOptions().PageIndex()"); - } - - li = new HtmlGenericControl("li"); - li.SetDataContextType(currentPageTextContext); - li.DataContext = null; - - NumberButtonsPlaceHolder!.Children.Add(li); - AddItemCssClass(writer, context); - - if (RenderLinkForCurrentPage) - AddKnockoutActiveCssDataBind(writer, context, "$data == $parent.PagingOptions().PageIndex()"); - - li.SetValue(Internal.PathFragmentProperty, "$parent.PagingOptions.NearPageIndexes[$index]"); - var link = new LinkButton(); - li.Children.Add(link); - link.SetDataContextType(currentPageTextContext); - link.SetBinding(ButtonBase.TextProperty, currentPageTextBinding); - link.SetBinding(ButtonBase.ClickProperty, pagerCommands.GoToPage!); - object enabledValue = GetValueRaw(EnabledProperty)!; - if (!true.Equals(enabledValue)) link.SetValue(LinkButton.EnabledProperty, enabledValue); - - li.Render(writer, context); + // don't, delegated to the ContentWrapper html element } - protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext context) { - base.RenderEndTag(writer, context); - writer.WriteKnockoutDataBindEndComment(); } + 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 index 9833261de0..7d6045f3cb 100644 --- a/src/Framework/Framework/Controls/DataPagerCommands.cs +++ b/src/Framework/Framework/Controls/DataPagerCommands.cs @@ -1,13 +1,21 @@ +using System.Collections; +using System.Collections.Generic; using DotVVM.Framework.Binding.Expressions; namespace DotVVM.Framework.Controls { - public class DataPagerCommands + public class DataPagerBindings { 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; } + + public IStaticValueBinding? IsFirstPage { get; init; } + public IStaticValueBinding? IsLastPage { get; init; } + public IStaticValueBinding>? PageNumbers { get; init; } + public IStaticValueBinding? IsActivePage { get; init; } + public IStaticValueBinding? PageNumberText { get; init; } } -} \ No newline at end of file +} diff --git a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs index c9af459673..4f9125c784 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.Compilation.ControlTree.Resolved; namespace DotVVM.Framework.Controls; @@ -17,7 +18,7 @@ public class GridViewDataSetBindingProvider { private readonly BindingCompilationService service; - private readonly ConcurrentDictionary<(DataContextStack, GridViewDataSetCommandType), DataPagerCommands> dataPagerCommands = new(); + private readonly ConcurrentDictionary<(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType), DataPagerBindings> dataPagerCommands = new(); private readonly ConcurrentDictionary<(DataContextStack, GridViewDataSetCommandType), GridViewCommands> gridViewCommands = new(); public GridViewDataSetBindingProvider(BindingCompilationService service) @@ -25,9 +26,9 @@ public GridViewDataSetBindingProvider(BindingCompilationService service) this.service = service; } - public DataPagerCommands GetDataPagerCommands(DataContextStack dataContextStack, GridViewDataSetCommandType commandType) + public DataPagerBindings GetDataPagerCommands(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType) { - return dataPagerCommands.GetOrAdd((dataContextStack, commandType), _ => GetDataPagerCommandsCore(dataContextStack, commandType)); + return dataPagerCommands.GetOrAdd((dataContextStack, dataSetBinding, commandType), x => GetDataPagerCommandsCore(x.dataContextStack, x.dataSetBinding, x.commandType)); } public GridViewCommands GetGridViewCommands(DataContextStack dataContextStack, GridViewDataSetCommandType commandType) @@ -35,46 +36,84 @@ public GridViewCommands GetGridViewCommands(DataContextStack dataContextStack, G return gridViewCommands.GetOrAdd((dataContextStack, commandType), _ => GetGridViewCommandsCore(dataContextStack, commandType)); } - private DataPagerCommands GetDataPagerCommandsCore(DataContextStack dataContextStack, GridViewDataSetCommandType commandType) + private DataPagerBindings GetDataPagerCommandsCore(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType) { - ICommandBinding? GetCommandOrNull(ParameterExpression dataSetParam, DataContextStack dataContextStack, string methodName, params Expression[] arguments) + var dataSetExpr = dataSetBinding.GetProperty().Expression; + ICommandBinding? GetCommandOrNull(DataContextStack dataContextStack, string methodName, params Expression[] arguments) { - return typeof(T).IsAssignableFrom(dataSetParam.Type) - ? CreateCommandBinding(commandType, dataSetParam, dataContextStack, methodName, arguments) + return typeof(T).IsAssignableFrom(dataSetExpr.Type) + ? CreateCommandBinding(commandType, dataSetExpr, dataContextStack, methodName, arguments) : null; } + + IStaticValueBinding? GetValueBindingOrNull(Expression> expression) + { + if (typeof(T).IsAssignableFrom(dataSetExpr.Type)) + { + return (IStaticValueBinding)ValueOrBindingExtensions.SelectImpl(dataSetBinding, expression); + } + else + { + return null; + } + } + ParameterExpression CreateParameter(DataContextStack dataContextStack, string name = "_this") { return Expression.Parameter(dataContextStack.DataContextType, name).AddParameterAnnotation(new BindingParameterAnnotation(dataContextStack)); } - return new DataPagerCommands() + var pageIndexDataContext = DataContextStack.CreateCollectionElement( + typeof(int), dataContextStack + ); + + return new DataPagerBindings() { 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, "_parent"), - DataContextStack.Create(typeof(int), dataContextStack), + pageIndexDataContext, nameof(IPagingPageIndexCapability.GoToPage), - CreateParameter(DataContextStack.Create(typeof(int), dataContextStack), "_thisIndex")) + CreateParameter(pageIndexDataContext, "_thisIndex")), + + IsFirstPage = + GetValueBindingOrNull, bool>(d => d.PagingOptions.IsFirstPage) ?? + GetValueBindingOrNull, bool>(d => d.PagingOptions.IsFirstPage), + + IsLastPage = + GetValueBindingOrNull, bool>(d => d.PagingOptions.IsLastPage) ?? + GetValueBindingOrNull, bool>(d => d.PagingOptions.IsLastPage), + + PageNumbers = + GetValueBindingOrNull, IEnumerable>(d => d.PagingOptions.NearPageIndexes), + + IsActivePage = // _this == _parent.DataSet.PagingOptions.PageIndex + typeof(IPageableGridViewDataSet).IsAssignableFrom(dataSetExpr.Type) + ? new ValueBindingExpression(service, new object[] { + pageIndexDataContext, + new ParsedExpressionBindingProperty(Expression.Equal( + CreateParameter(pageIndexDataContext, "_thisIndex"), + Expression.Property(Expression.Property(dataSetExpr, "PagingOptions"), "PageIndex") + )), + }) + : null, + + PageNumberText = + service.Cache.CreateValueBinding("_this + 1", pageIndexDataContext) }; } @@ -103,16 +142,16 @@ ParameterExpression CreateParameter(DataContextStack dataContextStack) }; } - private ICommandBinding CreateCommandBinding(GridViewDataSetCommandType commandType, ParameterExpression dataSetParam, DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression = null) + private ICommandBinding CreateCommandBinding(GridViewDataSetCommandType commandType, Expression dataSet, 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); + var optionsConcreteType = GetOptionsConcreteType(dataSet.Type, out var optionsProperty); // call dataSet.XXXOptions.Method(...); var callMethodOnOptions = Expression.Call( - Expression.Convert(Expression.Property(dataSetParam, optionsProperty), optionsConcreteType), + Expression.Convert(Expression.Property(dataSet, optionsProperty), optionsConcreteType), optionsConcreteType.GetMethod(methodName)!, arguments); body.Add(callMethodOnOptions); @@ -120,10 +159,10 @@ private ICommandBinding CreateCommandBinding(GridViewDataSetC if (commandType == GridViewDataSetCommandType.Command) { // if we are on a server, call the dataSet.RequestRefresh if supported - if (typeof(IRefreshableGridViewDataSet).IsAssignableFrom(dataSetParam.Type)) + if (typeof(IRefreshableGridViewDataSet).IsAssignableFrom(dataSet.Type)) { var callRequestRefresh = Expression.Call( - Expression.Convert(dataSetParam, typeof(IRefreshableGridViewDataSet)), + Expression.Convert(dataSet, typeof(IRefreshableGridViewDataSet)), typeof(IRefreshableGridViewDataSet).GetMethod(nameof(IRefreshableGridViewDataSet.RequestRefresh))! ); body.Add(callRequestRefresh); @@ -146,7 +185,7 @@ private ICommandBinding CreateCommandBinding(GridViewDataSetC else if (commandType == GridViewDataSetCommandType.StaticCommand) { // on the client, wrap the call into client-side loading procedure - body.Add(CallClientSideLoad(dataSetParam)); + body.Add(CallClientSideLoad(dataSet)); Expression expression = Expression.Block(body); if (transformExpression != null) { diff --git a/src/Tests/ControlTests/DataPagerTests.cs b/src/Tests/ControlTests/DataPagerTests.cs index 5b89945e0f..f1be5bff24 100644 --- a/src/Tests/ControlTests/DataPagerTests.cs +++ b/src/Tests/ControlTests/DataPagerTests.cs @@ -38,7 +38,7 @@ public async Task CommandDataPager() .Select(c => (c.control, c.command, str: c.command.GetProperty().Expression.ToCSharpString().Trim().TrimEnd(';'))) .OrderBy(c => c.str) .ToArray(); - check.CheckLines(commandExpressions.Select(c => c.str), checkName: "command-bindings", fileExtension: "txt"); + check.CheckLines(commandExpressions.DistinctBy(c => c.command).Select(c => c.str), checkName: "command-bindings", fileExtension: "txt"); check.CheckString(r.FormattedHtml, fileExtension: "html"); diff --git a/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager-command-bindings.txt b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager-command-bindings.txt index 716306d083..dd845c50d5 100644 --- a/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager-command-bindings.txt +++ b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager-command-bindings.txt @@ -1,10 +1,10 @@ -((PagingOptions)_parent.PagingOptions).GoToPage(_thisIndex); -((IRefreshableGridViewDataSet)_parent).RequestRefresh() -((PagingOptions)_this.PagingOptions).GoToFirstPage(); -((IRefreshableGridViewDataSet)_this).RequestRefresh() -((PagingOptions)_this.PagingOptions).GoToLastPage(); -((IRefreshableGridViewDataSet)_this).RequestRefresh() -((PagingOptions)_this.PagingOptions).GoToNextPage(); -((IRefreshableGridViewDataSet)_this).RequestRefresh() -((PagingOptions)_this.PagingOptions).GoToPreviousPage(); -((IRefreshableGridViewDataSet)_this).RequestRefresh() +((PagingOptions)_this.Customers.PagingOptions).GoToFirstPage(); +((IRefreshableGridViewDataSet)_this.Customers).RequestRefresh() +((PagingOptions)_this.Customers.PagingOptions).GoToLastPage(); +((IRefreshableGridViewDataSet)_this.Customers).RequestRefresh() +((PagingOptions)_this.Customers.PagingOptions).GoToNextPage(); +((IRefreshableGridViewDataSet)_this.Customers).RequestRefresh() +((PagingOptions)_this.Customers.PagingOptions).GoToPage(_thisIndex); +((IRefreshableGridViewDataSet)_this.Customers).RequestRefresh() +((PagingOptions)_this.Customers.PagingOptions).GoToPreviousPage(); +((IRefreshableGridViewDataSet)_this.Customers).RequestRefresh() diff --git a/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html index 865a9386c5..83586b2722 100644 --- a/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html +++ b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html @@ -1,26 +1,23 @@ -
    -
  • - «« + diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index 4eb6003823..fd99dc07e1 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -540,6 +540,10 @@ "mappingMode": "InnerElement", "onlyHardcoded": true }, + "LoadData": { + "type": "DotVVM.Framework.Binding.Expressions.ICommandBinding, DotVVM.Framework", + "onlyBindings": true + }, "NextPageTemplate": { "type": "DotVVM.Framework.Controls.ITemplate, DotVVM.Framework", "mappingMode": "InnerElement", @@ -1669,23 +1673,6 @@ "isActive": true, "isAttached": true } - }, - "DotvvmMarkupControl-33jwRoNrnlbAOVpOnCErXw==": { - "ShowDescription": { - "type": "System.Boolean", - "defaultValue": false - } - }, - "DotvvmMarkupControl-koCHqjx2oIk1rVwG1PzLJQ==": { - "Click": { - "type": "DotVVM.Framework.Binding.Expressions.Command, DotVVM.Framework", - "isCommand": true - } - }, - "DotvvmMarkupControl-YYPITyOzVEL518wclEMJZw==": { - "SomeProperty": { - "type": "System.String" - } } }, "capabilities": { diff --git a/src/Tests/ViewModel/GridViewDataSetTests.cs b/src/Tests/ViewModel/GridViewDataSetTests.cs index 9f10233881..169e42fbb4 100644 --- a/src/Tests/ViewModel/GridViewDataSetTests.cs +++ b/src/Tests/ViewModel/GridViewDataSetTests.cs @@ -18,14 +18,16 @@ namespace DotVVM.Framework.Tests.ViewModel [TestClass] public class GridViewDataSetTests { + private readonly BindingCompilationService bindingService; private readonly GridViewDataSetBindingProvider commandProvider; private readonly GridViewDataSet vm; private readonly DataContextStack dataContextStack; private readonly DotvvmControl control; + private readonly ValueBindingExpression> dataSetBinding; public GridViewDataSetTests() { - var bindingService = DotvvmTestHelper.DefaultConfig.ServiceProvider.GetRequiredService(); + bindingService = DotvvmTestHelper.DefaultConfig.ServiceProvider.GetRequiredService(); commandProvider = new GridViewDataSetBindingProvider(bindingService); // build viewmodel @@ -42,6 +44,7 @@ public GridViewDataSetTests() // create page dataContextStack = DataContextStack.Create(vm.GetType()); control = new DotvvmView() { DataContext = vm }; + dataSetBinding = ValueBindingExpression.CreateThisBinding>(bindingService, dataContextStack); } [TestMethod] @@ -49,13 +52,13 @@ public void GridViewDataSet_DataPagerCommands_Command() { // create control with page index data context var pageIndexControl = new PlaceHolder(); - var pageIndexDataContextStack = DataContextStack.Create(typeof(int), dataContextStack); + var pageIndexDataContextStack = DataContextStack.CreateCollectionElement(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); + var commands = commandProvider.GetDataPagerCommands(dataContextStack, dataSetBinding, GridViewDataSetCommandType.Command); // test evaluation of commands Assert.IsNotNull(commands.GoToLastPage); @@ -121,7 +124,7 @@ public void GridViewDataSet_GridViewCommands_Command() public void GridViewDataSet_DataPagerCommands_StaticCommand() { // get pager commands - var commands = commandProvider.GetDataPagerCommands(dataContextStack, GridViewDataSetCommandType.StaticCommand); + var commands = commandProvider.GetDataPagerCommands(dataContextStack, dataSetBinding, GridViewDataSetCommandType.StaticCommand); var goToFirstPage = CompileBinding(commands.GoToFirstPage); Console.WriteLine(goToFirstPage); From 0d056559cfd57c79b1f29f9f4466f33508d39c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 22 Oct 2023 18:09:45 +0200 Subject: [PATCH 16/60] Implemented support for GridView --- .../Framework/Binding/BindingHelper.cs | 24 ++++ src/Framework/Framework/Controls/Button.cs | 5 +- .../Framework/Controls/ButtonBase.cs | 13 +++ src/Framework/Framework/Controls/GridView.cs | 109 +++++++++++++----- .../Framework/Controls/GridViewColumn.cs | 15 ++- .../GridViewDataSetBindingProvider.cs | 24 ++-- .../Framework/Controls/LinkButton.cs | 5 +- .../GridViewPagingSortingViewModel.cs | 3 + .../GridView/GridViewServerRender.dothtml | 51 +++++--- .../GridView/GridViewStaticCommand.dothtml | 2 +- src/Tests/ControlTests/DataPagerTests.cs | 2 +- .../Runtime/DotvvmControlRenderedHtmlTests.cs | 2 +- src/Tests/ViewModel/GridViewDataSetTests.cs | 2 +- 13 files changed, 181 insertions(+), 76 deletions(-) diff --git a/src/Framework/Framework/Binding/BindingHelper.cs b/src/Framework/Framework/Binding/BindingHelper.cs index eb1a4719b0..fc4a901fe5 100644 --- a/src/Framework/Framework/Binding/BindingHelper.cs +++ b/src/Framework/Framework/Binding/BindingHelper.cs @@ -253,6 +253,30 @@ public static ParametrizedCode GetParametrizedCommandJavascript(this ICommandBin JavascriptTranslator.AdjustKnockoutScriptContext(binding.CommandJavascript, dataContextLevel: FindDataContextTarget(binding, control).stepsUp); + /// + /// Gets command arguments parametrized code from the arguments collection. + /// + public static CodeParameterAssignment? GetParametrizedCommandArgs(DotvvmControl control, IEnumerable argumentsCollection) + { + var builder = new ParametrizedCode.Builder(); + var isFirst = true; + + builder.Add("["); + foreach (var arg in argumentsCollection) + { + if (!isFirst) + { + builder.Add(","); + } + isFirst = false; + + builder.Add(ValueOrBinding.FromBoxedValue(arg).GetParametrizedJsExpression(control)); + } + builder.Add("]"); + + return builder.Build(OperatorPrecedence.Max); + } + public static object? GetBindingValue(this IBinding binding, DotvvmBindableObject control) { if (binding is IStaticValueBinding valueBinding) diff --git a/src/Framework/Framework/Controls/Button.cs b/src/Framework/Framework/Controls/Button.cs index 7f03ad0a79..6cfe979170 100644 --- a/src/Framework/Framework/Controls/Button.cs +++ b/src/Framework/Framework/Controls/Button.cs @@ -131,7 +131,10 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest var clickBinding = GetCommandBinding(ClickProperty); if (clickBinding != null) { - writer.AddAttribute("onclick", KnockoutHelper.GenerateClientPostBackScript(nameof(Click), clickBinding, this), true, ";"); + writer.AddAttribute("onclick", KnockoutHelper.GenerateClientPostBackScript( + nameof(Click), clickBinding, this, + new PostbackScriptOptions(commandArgs: BindingHelper.GetParametrizedCommandArgs(this, ClickArguments))), + append: true, appendSeparator: ";"); } } diff --git a/src/Framework/Framework/Controls/ButtonBase.cs b/src/Framework/Framework/Controls/ButtonBase.cs index 6ef10903d6..e78ed46de9 100644 --- a/src/Framework/Framework/Controls/ButtonBase.cs +++ b/src/Framework/Framework/Controls/ButtonBase.cs @@ -37,6 +37,18 @@ public Command? Click public static readonly DotvvmProperty ClickProperty = DotvvmProperty.Register(t => t.Click, null); + /// + /// Gets or sets a collection of arguments passed to the command when the button is clicked. + /// This property is typically used from the code-behind to allow sharing the same binding expressions among multiple buttons. + /// + [MarkupOptions(MappingMode = MappingMode.Exclude)] + public List ClickArguments + { + get { return (List)GetValue(ClickArgumentsProperty)!; } + set { SetValue(ClickArgumentsProperty, value); } + } + public static readonly DotvvmProperty ClickArgumentsProperty + = DotvvmProperty.Register, ButtonBase>(c => c.ClickArguments, null); /// /// Gets or sets a value indicating whether the button is enabled and can be clicked on. @@ -65,6 +77,7 @@ public TextOrContentCapability TextOrContentCapability /// public ButtonBase(string tagName) : base(tagName) { + ClickArguments = new List(); } diff --git a/src/Framework/Framework/Controls/GridView.cs b/src/Framework/Framework/Controls/GridView.cs index b1bc656e26..271bc02dd8 100644 --- a/src/Framework/Framework/Controls/GridView.cs +++ b/src/Framework/Framework/Controls/GridView.cs @@ -24,6 +24,8 @@ namespace DotVVM.Framework.Controls [ControlMarkupOptions(AllowContent = false, DefaultContentProperty = nameof(Columns))] public class GridView : ItemsControl { + private readonly GridViewDataSetBindingProvider gridViewDataSetBindingProvider; + private readonly BindingCompilationService bindingCompilationService; private EmptyData? emptyDataContainer; private int numberOfRows; private HtmlGenericControl? head; @@ -31,8 +33,11 @@ public class GridView : ItemsControl private DataItemContainer? clientEditTemplate; - public GridView() : base("table") + public GridView(GridViewDataSetBindingProvider gridViewDataSetBindingProvider, BindingCompilationService bindingCompilationService) : base("table") { + this.gridViewDataSetBindingProvider = gridViewDataSetBindingProvider; + this.bindingCompilationService = bindingCompilationService; + SetValue(Internal.IsNamingContainerProperty, true); Columns = new List(); @@ -161,6 +166,19 @@ public bool InlineEditing public static readonly DotvvmProperty InlineEditingProperty = DotvvmProperty.Register(t => t.InlineEditing, false); + /// + /// Gets or sets the command that will be triggered when the GridView needs to load data (e.g. when the sort order has changed). + /// The command accepts one argument of type GridViewDataSetOptions<TFilteringOptions, TSortingOptions, TPagingOptions> and should return the new GridViewDataSet. + /// + public ICommandBinding? LoadData + { + get => (ICommandBinding?)GetValue(LoadDataProperty); + set => SetValue(LoadDataProperty, value); + } + public static readonly DotvvmProperty LoadDataProperty = + DotvvmProperty.Register(nameof(LoadData)); + + protected internal override void OnLoad(IDotvvmRequestContext context) { DataBind(context); @@ -186,16 +204,7 @@ private void DataBind(IDotvvmRequestContext context) var dataSourceBinding = GetDataSourceBinding(); var dataSource = DataSource; - var sortCommand = SortChanged; - sortCommand ??= - typeof(ISortableGridViewDataSet).IsAssignableFrom((GetBinding(DataSourceProperty) as IStaticValueBinding)?.ResultType) - ? (Action)SortChangedCommand - : null; - - sortCommand ??= s => - throw new DotvvmControlException(this, "Cannot sort when DataSource is null."); - - CreateHeaderRow(context, sortCommand); + CreateHeaderRow(context); var index = 0; if (dataSource != null) @@ -231,21 +240,7 @@ private void DataBind(IDotvvmRequestContext context) } } - protected virtual void SortChangedCommand(string? expr) - { - var dataSource = this.DataSource; - if (dataSource is null) - throw new DotvvmControlException(this, "Cannot execute sort command, DataSource is null"); - - var sortOptions = (dataSource as ISortableGridViewDataSet)?.SortingOptions; - if (sortOptions is null || (expr != null && !sortOptions.IsSortingAllowed(expr))) - throw new DotvvmControlException(this, "Cannot execute sort command, DataSource does not have sorting options"); - sortOptions.SetSortExpression(expr); - (dataSource as IPageableGridViewDataSet)?.PagingOptions.GoToFirstPage(); - (dataSource as IRefreshableGridViewDataSet)?.RequestRefresh(); - } - - protected virtual void CreateHeaderRow(IDotvvmRequestContext context, Action? sortCommand) + protected virtual void CreateHeaderRow(IDotvvmRequestContext context) { head = new HtmlGenericControl("thead"); Children.Add(head); @@ -256,14 +251,15 @@ protected virtual void CreateHeaderRow(IDotvvmRequestContext context, Action + (string sortExpression) => + { + var dataSource = this.DataSource; + if (dataSource is null) + throw new DotvvmControlException(this, "Cannot execute sort command, DataSource is null"); + + SortChanged!(sortExpression); + + (dataSource as IPageableGridViewDataSet)?.PagingOptions.GoToFirstPage(); + (dataSource as IRefreshableGridViewDataSet)?.RequestRefresh(); + }), + new IdBindingProperty($"{this.GetDotvvmUniqueId().GetValue()}_sortBinding") + }); + } + + protected virtual ICommandBinding? BuildLoadDataSortCommandBinding() + { + var dataContextStack = this.GetDataContextType()!; + var commandType = LoadData is { } ? GridViewDataSetCommandType.StaticCommand : GridViewDataSetCommandType.Command; + return gridViewDataSetBindingProvider.GetGridViewCommands(dataContextStack, GetDataSourceBinding(), commandType).SetSortExpression; + } + private static void SetCellAttributes(GridViewColumn column, HtmlGenericControl cell, bool isHeaderCell) { var cellAttributes = cell.Attributes; @@ -520,7 +562,16 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest var mapping = userColumnMappingService.GetMapping(itemType!); var mappingJson = JsonConvert.SerializeObject(mapping); - writer.AddKnockoutDataBind("dotvvm-gridviewdataset", $"{{'mapping':{mappingJson},'dataSet':{GetDataSourceBinding().GetKnockoutBindingExpression(this, unwrapped: true)}}}"); + var helperBinding = new KnockoutBindingGroup(); + helperBinding.Add("dataSet", GetDataSourceBinding().GetKnockoutBindingExpression(this, unwrapped: true)); + helperBinding.Add("mapping", mappingJson); + if (this.LoadData is { } loadData) + { + var loadDataExpression = KnockoutHelper.GenerateClientPostbackLambda("LoadData", loadData, this, new PostbackScriptOptions(elementAccessor: "$element", koContext: CodeParameterAssignment.FromIdentifier("$context"))); + helperBinding.Add("loadDataSet", loadDataExpression); + } + writer.AddKnockoutDataBind("dotvvm-gridviewdataset", helperBinding.ToString()); + base.AddAttributesToRender(writer, context); } diff --git a/src/Framework/Framework/Controls/GridViewColumn.cs b/src/Framework/Framework/Controls/GridViewColumn.cs index 9bca83bc12..45d4823c61 100644 --- a/src/Framework/Framework/Controls/GridViewColumn.cs +++ b/src/Framework/Framework/Controls/GridViewColumn.cs @@ -189,7 +189,7 @@ public virtual void CreateEditControls(IDotvvmRequestContext context, DotvvmCont EditTemplate.BuildContent(context, container); } - public virtual void CreateHeaderControls(IDotvvmRequestContext context, GridView gridView, Action? sortCommand, HtmlGenericControl cell, IGridViewDataSet? gridViewDataSet) + public virtual void CreateHeaderControls(IDotvvmRequestContext context, GridView gridView, ICommandBinding? sortCommandBinding, HtmlGenericControl cell, IGridViewDataSet? gridViewDataSet) { if (HeaderTemplate != null) { @@ -199,22 +199,21 @@ public virtual void CreateHeaderControls(IDotvvmRequestContext context, GridView if (AllowSorting) { - if (sortCommand == null) + if (sortCommandBinding == null) { throw new DotvvmControlException(this, "Cannot use column sorting where no sort command is specified. Either put IGridViewDataSet in the DataSource property of the GridView, or set the SortChanged command on the GridView to implement custom sorting logic!"); } var sortExpression = GetSortExpression(); - + var linkButton = new LinkButton(); - linkButton.SetValue(LinkButton.TextProperty, GetValueRaw(HeaderTextProperty)); + linkButton.SetValue(ButtonBase.TextProperty, GetValueRaw(HeaderTextProperty)); + linkButton.ClickArguments.Add(sortExpression); cell.Children.Add(linkButton); - var bindingId = linkButton.GetDotvvmUniqueId().GetValue() + "_sortBinding"; - var binding = new CommandBindingExpression(context.Services.GetRequiredService().WithoutInitialization(), h => sortCommand(sortExpression), bindingId); - linkButton.SetBinding(ButtonBase.ClickProperty, binding); + linkButton.SetBinding(ButtonBase.ClickProperty, sortCommandBinding); - SetSortedCssClass(cell, gridViewDataSet, gridView.GetValueBinding(GridView.DataSourceProperty)!); + SetSortedCssClass(cell, gridViewDataSet, gridView.GetValueBinding(ItemsControl.DataSourceProperty)!); } else { diff --git a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs index 4f9125c784..6dd0c2d821 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -10,7 +10,6 @@ using DotVVM.Framework.Binding.Properties; using DotVVM.Framework.Compilation; using DotVVM.Framework.Compilation.ControlTree; -using DotVVM.Framework.Compilation.ControlTree.Resolved; namespace DotVVM.Framework.Controls; @@ -19,7 +18,7 @@ public class GridViewDataSetBindingProvider private readonly BindingCompilationService service; private readonly ConcurrentDictionary<(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType), DataPagerBindings> dataPagerCommands = new(); - private readonly ConcurrentDictionary<(DataContextStack, GridViewDataSetCommandType), GridViewCommands> gridViewCommands = new(); + private readonly ConcurrentDictionary<(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType), GridViewCommands> gridViewCommands = new(); public GridViewDataSetBindingProvider(BindingCompilationService service) { @@ -31,9 +30,9 @@ public DataPagerBindings GetDataPagerCommands(DataContextStack dataContextStack, return dataPagerCommands.GetOrAdd((dataContextStack, dataSetBinding, commandType), x => GetDataPagerCommandsCore(x.dataContextStack, x.dataSetBinding, x.commandType)); } - public GridViewCommands GetGridViewCommands(DataContextStack dataContextStack, GridViewDataSetCommandType commandType) + public GridViewCommands GetGridViewCommands(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType) { - return gridViewCommands.GetOrAdd((dataContextStack, commandType), _ => GetGridViewCommandsCore(dataContextStack, commandType)); + return gridViewCommands.GetOrAdd((dataContextStack, dataSetBinding, commandType), x => GetGridViewCommandsCore(x.dataContextStack, x.dataSetBinding, x.commandType)); } private DataPagerBindings GetDataPagerCommandsCore(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType) @@ -117,24 +116,21 @@ ParameterExpression CreateParameter(DataContextStack dataContextStack, string na }; } - private GridViewCommands GetGridViewCommandsCore(DataContextStack dataContextStack, GridViewDataSetCommandType commandType) + private GridViewCommands GetGridViewCommandsCore(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType) { - ICommandBinding? GetCommandOrNull(ParameterExpression dataSetParam, string methodName, Expression[] arguments, Func transformExpression) + var dataSetExpr = dataSetBinding.GetProperty().Expression; + ICommandBinding? GetCommandOrNull(DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression) { - return typeof(T).IsAssignableFrom(dataSetParam.Type) - ? CreateCommandBinding(commandType, dataSetParam, dataContextStack, methodName, arguments, transformExpression) + return typeof(T).IsAssignableFrom(dataSetExpr.Type) + ? CreateCommandBinding(commandType, dataSetExpr, dataContextStack, methodName, arguments, transformExpression) : null; } - ParameterExpression CreateParameter(DataContextStack dataContextStack) - { - return Expression.Parameter(dataContextStack.DataContextType).AddParameterAnnotation(new BindingParameterAnnotation(dataContextStack)); - } - var setSortExpressionParam = Expression.Parameter(typeof(string)); + var setSortExpressionParam = Expression.Parameter(typeof(string), "_sortExpression"); return new GridViewCommands() { SetSortExpression = GetCommandOrNull>( - CreateParameter(dataContextStack), + dataContextStack, nameof(ISortingSetSortExpressionCapability.SetSortExpression), new Expression[] { setSortExpressionParam }, // transform to sortExpression => command lambda diff --git a/src/Framework/Framework/Controls/LinkButton.cs b/src/Framework/Framework/Controls/LinkButton.cs index 0be751d95c..f96bacb6bc 100644 --- a/src/Framework/Framework/Controls/LinkButton.cs +++ b/src/Framework/Framework/Controls/LinkButton.cs @@ -36,7 +36,10 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest var clickBinding = GetCommandBinding(ClickProperty); if (clickBinding != null) { - writer.AddAttribute("onclick", KnockoutHelper.GenerateClientPostBackScript(nameof(Click), clickBinding, this), true, ";"); + writer.AddAttribute("onclick", KnockoutHelper.GenerateClientPostBackScript( + nameof(Click), clickBinding, this, + new PostbackScriptOptions(commandArgs: BindingHelper.GetParametrizedCommandArgs(this, ClickArguments))), + append: true, appendSeparator: ";"); } } diff --git a/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewPagingSortingViewModel.cs b/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewPagingSortingViewModel.cs index 4052140606..64439a2fac 100644 --- a/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewPagingSortingViewModel.cs +++ b/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewPagingSortingViewModel.cs @@ -51,6 +51,9 @@ private static IQueryable GetData() public List Null { get; set; } + public GridViewDataSet NullDataSet { get; set; } + + public string CustomNameForName { get; set; } = "Name"; public override Task PreRender() diff --git a/src/Samples/Common/Views/ControlSamples/GridView/GridViewServerRender.dothtml b/src/Samples/Common/Views/ControlSamples/GridView/GridViewServerRender.dothtml index c2c3569821..6286b0bb69 100644 --- a/src/Samples/Common/Views/ControlSamples/GridView/GridViewServerRender.dothtml +++ b/src/Samples/Common/Views/ControlSamples/GridView/GridViewServerRender.dothtml @@ -19,12 +19,13 @@ - + Birth date - - + + + @@ -51,13 +52,25 @@

    GridView with null DataSource

    - - - + + + - - +

     

    +

     

    +

     

    + + +

    GridView with null GridViewDataSet

    + + + + + + + +

    EmptyData with data source

    This is not displayed because data is not empty @@ -67,16 +80,16 @@ This is displayed because data is empty -

    GridView with empty dataset and ShowHeaderWhenNoData true

    - - - - - - - - - +

    GridView with empty dataset and ShowHeaderWhenNoData true

    + + + + + + + + + @@ -84,4 +97,4 @@ Rename the `Name` string in Grid's headers:

    - \ No newline at end of file + diff --git a/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml b/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml index 0e51e74c09..61d7488c99 100644 --- a/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml +++ b/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml @@ -10,7 +10,7 @@

    Standard data set

    > + LoadData="{staticCommand: _root.LoadStandard}"> diff --git a/src/Tests/ControlTests/DataPagerTests.cs b/src/Tests/ControlTests/DataPagerTests.cs index f1be5bff24..b353b5f14d 100644 --- a/src/Tests/ControlTests/DataPagerTests.cs +++ b/src/Tests/ControlTests/DataPagerTests.cs @@ -38,7 +38,7 @@ public async Task CommandDataPager() .Select(c => (c.control, c.command, str: c.command.GetProperty().Expression.ToCSharpString().Trim().TrimEnd(';'))) .OrderBy(c => c.str) .ToArray(); - check.CheckLines(commandExpressions.DistinctBy(c => c.command).Select(c => c.str), checkName: "command-bindings", fileExtension: "txt"); + check.CheckLines(commandExpressions.GroupBy(c => c.command).Select(c => c.First().str), checkName: "command-bindings", fileExtension: "txt"); check.CheckString(r.FormattedHtml, fileExtension: "html"); diff --git a/src/Tests/Runtime/DotvvmControlRenderedHtmlTests.cs b/src/Tests/Runtime/DotvvmControlRenderedHtmlTests.cs index 18fc676565..1803241eb4 100644 --- a/src/Tests/Runtime/DotvvmControlRenderedHtmlTests.cs +++ b/src/Tests/Runtime/DotvvmControlRenderedHtmlTests.cs @@ -21,7 +21,7 @@ public class DotvvmControlRenderedHtmlTests : DotvvmControlTestBase [TestMethod] public void GridViewTextColumn_RenderedHtmlTest_ServerRendering() { - var gridView = new GridView() { + var gridView = new GridView(new GridViewDataSetBindingProvider(BindingService), BindingService) { Columns = new List { new GridViewTextColumn() { HeaderCssClass = "lol", HeaderText="Header Text", ValueBinding = ValueBindingExpression.CreateBinding(BindingService, h => (object)h[0], (DataContextStack)null) } diff --git a/src/Tests/ViewModel/GridViewDataSetTests.cs b/src/Tests/ViewModel/GridViewDataSetTests.cs index 169e42fbb4..b01890620c 100644 --- a/src/Tests/ViewModel/GridViewDataSetTests.cs +++ b/src/Tests/ViewModel/GridViewDataSetTests.cs @@ -96,7 +96,7 @@ public void GridViewDataSet_DataPagerCommands_Command() public void GridViewDataSet_GridViewCommands_Command() { // get gridview commands - var commands = commandProvider.GetGridViewCommands(dataContextStack, GridViewDataSetCommandType.Command); + var commands = commandProvider.GetGridViewCommands(dataContextStack, dataSetBinding, GridViewDataSetCommandType.Command); // test evaluation of commands Assert.IsNotNull(commands.SetSortExpression); From 5450478d760e0a6a512d8a12f32aa3bf8f8cb5c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 22 Oct 2023 17:20:30 +0200 Subject: [PATCH 17/60] Fix DataPager.RenderLinkForCurrentPage --- src/Framework/Framework/Controls/DataPager.cs | 12 ++++++++++-- .../GridView/GridViewStaticCommandViewModel.cs | 8 ++++---- .../GridView/GridViewStaticCommand.dothtml | 12 ++++++------ .../testoutputs/DataPagerTests.CommandDataPager.html | 5 +++++ 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index 8a300df463..78e4cab921 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -193,10 +193,18 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) // li.SetBinding(DataContextProperty, GetNearIndexesBinding(context, i, dataContextType)); liTemplate.CssClasses.Add(ActiveItemCssClass, new ValueOrBinding(pagerBindings.IsActivePage.NotNull())); var link = new LinkButton(); - link.SetBinding(ButtonBase.ClickProperty, pagerBindings.GoToPage!); - link.SetBinding(ButtonBase.TextProperty, pagerBindings.PageNumberText); + link.SetBinding(ButtonBase.ClickProperty, pagerBindings.GoToPage.NotNull()); + link.SetBinding(ButtonBase.TextProperty, pagerBindings.PageNumberText.NotNull()); if (!true.Equals(enabled)) link.SetValue(LinkButton.EnabledProperty, enabled); liTemplate.Children.Add(link); + if (!this.RenderLinkForCurrentPage) + { + var notLink = new Literal(pagerBindings.PageNumberText); + notLink.RenderSpanElement = true; + notLink.SetBinding(DotvvmControl.IncludeInPageProperty, pagerBindings.IsActivePage); + link.SetBinding(DotvvmControl.IncludeInPageProperty, pagerBindings.IsActivePage.Negate()); + liTemplate.Children.Add(notLink); + } NumberButtonsRepeater = new Repeater() { DataSource = pagerBindings.PageNumbers, RenderWrapperTag = false, diff --git a/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs b/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs index d9157dd373..0d59bd12f7 100644 --- a/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs +++ b/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs @@ -55,7 +55,7 @@ public override Task PreRender() } [AllowStaticCommand] - public async Task> LoadStandard(GridViewDataSetOptions options) + public static async Task> LoadStandard(GridViewDataSetOptions options) { var dataSet = new GridViewDataSet(); dataSet.ApplyOptions(options); @@ -64,7 +64,7 @@ public async Task> LoadStandard(GridViewDataSetOpt } [AllowStaticCommand] - public async Task LoadToken(GridViewDataSetOptions options) + public static async Task LoadToken(GridViewDataSetOptions options) { var dataSet = new NextTokenGridViewDataSet(); dataSet.ApplyOptions(options); @@ -73,7 +73,7 @@ public async Task LoadToken(GridViewDataSetOptions LoadTokenHistory(GridViewDataSetOptions options) + public static async Task LoadTokenHistory(GridViewDataSetOptions options) { var dataSet = new NextTokenHistoryGridViewDataSet(); dataSet.ApplyOptions(options); @@ -82,7 +82,7 @@ public async Task LoadTokenHistory(GridViewData } [AllowStaticCommand] - public async Task LoadMultiSort(GridViewDataSetOptions options) + public static async Task LoadMultiSort(GridViewDataSetOptions options) { var dataSet = new MultiSortGridViewDataSet(); dataSet.ApplyOptions(options); diff --git a/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml b/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml index 61d7488c99..c1c7bd0e4e 100644 --- a/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml +++ b/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml @@ -18,7 +18,7 @@ - + <%--

    NextToken paging options

    - +

    NextTokenHistory data set

    + <%--LoadData="{staticCommand: RootViewModel.LoadTokenHistory}"--%> > @@ -44,11 +44,11 @@ - +

    MultiSort data set

    + <%--LoadData="{staticCommand: RootViewModel.LoadMultiSort}"--%> > @@ -57,7 +57,7 @@ - + --%>
    diff --git a/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html index 83586b2722..7f541e0b71 100644 --- a/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html +++ b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html @@ -10,7 +10,12 @@
  • + + + + +
  • From 16156a663107b0ccf7c33dddf5f7356092df9260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 11 Nov 2023 15:10:08 +0100 Subject: [PATCH 18/60] StaticCommand Grid sample updated --- .../GridView/GridViewStaticCommandViewModel.cs | 10 +++++++++- .../GridView/GridViewStaticCommand.dothtml | 18 +++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs b/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs index 0d59bd12f7..007045388d 100644 --- a/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs +++ b/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs @@ -116,7 +116,15 @@ public void ProcessLoadedItems(IQueryable filteredQueryable, IList item .OrderByDescending(c => c.CustomerId) .FirstOrDefault()?.CustomerId; - NextPageToken = (lastToken ?? 0).ToString(); + lastToken ??= 0; + if (lastToken == 12) + { + NextPageToken = null; + } + else + { + NextPageToken = lastToken.ToString(); + } } } diff --git a/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml b/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml index c1c7bd0e4e..7e1c2fc9b4 100644 --- a/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml +++ b/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml @@ -5,12 +5,15 @@ +

    Standard data set

    + LoadData="{staticCommand: RootViewModel.LoadStandard}"> @@ -19,11 +22,10 @@ - <%-- +

    NextToken paging options

    - > + LoadData="{staticCommand: RootViewModel.LoadToken}"> @@ -35,8 +37,7 @@

    NextTokenHistory data set

    - > + LoadData="{staticCommand: RootViewModel.LoadTokenHistory}"> @@ -48,8 +49,7 @@

    MultiSort data set

    - > + LoadData="{staticCommand: RootViewModel.LoadMultiSort}"> @@ -58,7 +58,7 @@ - --%> +
    From 0460f99e63ee242def9fe681275d3f30acaa7bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 11 Nov 2023 15:28:33 +0100 Subject: [PATCH 19/60] Fix DataPager.HideWhenOnlyOnePage --- src/Framework/Framework/Controls/DataPager.cs | 33 +++++++++---------- .../Framework/Controls/DataPagerCommands.cs | 1 + .../GridViewDataSetBindingProvider.cs | 27 ++++++++++----- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index 78e4cab921..1c7a3d2165 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -161,16 +161,15 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) var dataSetBinding = GetValueBinding(DataSetProperty)!; var dataSetType = dataSetBinding.ResultType; - ContentWrapper = CreateWrapperList(); - Children.Add(ContentWrapper); var commandType = LoadData is {} ? GridViewDataSetCommandType.StaticCommand : GridViewDataSetCommandType.Command; - pagerBindings = gridViewDataSetBindingProvider.GetDataPagerCommands(this.GetDataContextType().NotNull(), dataSetBinding, commandType); - var enabled = GetValueOrBinding(EnabledProperty)!; - + + ContentWrapper = CreateWrapperList(); + Children.Add(ContentWrapper); + if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataSetType)) { GoToFirstPageButton = CreateNavigationButton("««", FirstPageTemplate, enabled, pagerBindings.GoToFirstPage!, context); @@ -232,6 +231,15 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) protected virtual HtmlGenericControl CreateWrapperList() { var list = new HtmlGenericControl("ul"); + + // If Visible property was set to something, it would be overwritten by this + if (HideWhenOnlyOnePage && pagerBindings?.HasMoreThanOnePage is {} hasMoreThanOnePage) + { + if (IsPropertySet(VisibleProperty)) + throw new Exception("Visible can't be set on a DataPager when HideWhenOnlyOnePage is true. You can wrap it in an element that hide that or set HideWhenOnlyOnePage to false"); + list.SetProperty(HtmlGenericControl.VisibleProperty, hasMoreThanOnePage); + } + return list; } @@ -266,22 +274,13 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest throw new DotvvmControlException(this, "The DataPager control cannot be rendered in the RenderSettings.Mode='Server'."); } - var dataSetBinding = GetDataSetBinding().GetKnockoutBindingExpression(this, unwrapped: true); - var helperBinding = new KnockoutBindingGroup(); - helperBinding.Add("dataSet", dataSetBinding); if (this.LoadData is {} loadData) { + var helperBinding = new KnockoutBindingGroup(); + helperBinding.Add("dataSet", GetDataSetBinding().GetKnockoutBindingExpression(this, unwrapped: true)); var loadDataExpression = KnockoutHelper.GenerateClientPostbackLambda("LoadData", loadData, this, new PostbackScriptOptions(elementAccessor: "$element", koContext: CodeParameterAssignment.FromIdentifier("$context"))); helperBinding.Add("loadDataSet", loadDataExpression); - } - writer.AddKnockoutDataBind("dotvvm-gridviewdataset", helperBinding.ToString()); - - // If Visible property was set to something, it will be overwritten by this. TODO: is it how it should behave? - if (HideWhenOnlyOnePage) - { - if (IsPropertySet(VisibleProperty)) - throw new Exception("Visible can't be set on a DataPager when HideWhenOnlyOnePage is true. You can wrap it in an element that hide that or set HideWhenOnlyOnePage to false"); - writer.AddKnockoutDataBind("visible", $"({dataSetBinding}).PagingOptions().PagesCount() > 1"); + writer.AddKnockoutDataBind("dotvvm-gridviewdataset", helperBinding.ToString()); } if (GetValueBinding(EnabledProperty) is IValueBinding enabledBinding) diff --git a/src/Framework/Framework/Controls/DataPagerCommands.cs b/src/Framework/Framework/Controls/DataPagerCommands.cs index 7d6045f3cb..bd6c0163a7 100644 --- a/src/Framework/Framework/Controls/DataPagerCommands.cs +++ b/src/Framework/Framework/Controls/DataPagerCommands.cs @@ -17,5 +17,6 @@ public class DataPagerBindings public IStaticValueBinding>? PageNumbers { get; init; } public IStaticValueBinding? IsActivePage { get; init; } public IStaticValueBinding? PageNumberText { get; init; } + public IStaticValueBinding? HasMoreThanOnePage { get; init; } } } diff --git a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs index 6dd0c2d821..e12794d0d2 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -66,6 +66,11 @@ ParameterExpression CreateParameter(DataContextStack dataContextStack, string na typeof(int), dataContextStack ); + var isFirstPage = GetValueBindingOrNull, bool>(d => d.PagingOptions.IsFirstPage) ?? + GetValueBindingOrNull, bool>(d => d.PagingOptions.IsFirstPage); + var isLastPage = GetValueBindingOrNull, bool>(d => d.PagingOptions.IsLastPage) ?? + GetValueBindingOrNull, bool>(d => d.PagingOptions.IsLastPage); + return new DataPagerBindings() { GoToFirstPage = GetCommandOrNull>( @@ -89,14 +94,8 @@ ParameterExpression CreateParameter(DataContextStack dataContextStack, string na nameof(IPagingPageIndexCapability.GoToPage), CreateParameter(pageIndexDataContext, "_thisIndex")), - IsFirstPage = - GetValueBindingOrNull, bool>(d => d.PagingOptions.IsFirstPage) ?? - GetValueBindingOrNull, bool>(d => d.PagingOptions.IsFirstPage), - - IsLastPage = - GetValueBindingOrNull, bool>(d => d.PagingOptions.IsLastPage) ?? - GetValueBindingOrNull, bool>(d => d.PagingOptions.IsLastPage), - + IsFirstPage = isFirstPage, + IsLastPage = isLastPage, PageNumbers = GetValueBindingOrNull, IEnumerable>(d => d.PagingOptions.NearPageIndexes), @@ -112,7 +111,17 @@ ParameterExpression CreateParameter(DataContextStack dataContextStack, string na : null, PageNumberText = - service.Cache.CreateValueBinding("_this + 1", pageIndexDataContext) + service.Cache.CreateValueBinding("_this + 1", pageIndexDataContext), + HasMoreThanOnePage = + GetValueBindingOrNull, bool>(d => d.PagingOptions.PagesCount > 1) ?? + (isFirstPage != null && isLastPage != null ? + new ValueBindingExpression(service, new object[] { + dataContextStack, + new ParsedExpressionBindingProperty(Expression.Not(Expression.AndAlso( + isFirstPage.GetProperty().Expression, + isLastPage.GetProperty().Expression + ))) + }) : null) }; } From 20eaa0f216733ec8979f79a365725a58fd75561b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 11 Nov 2023 16:46:38 +0100 Subject: [PATCH 20/60] Add _dataPager.Load extension parameter --- .../Binding/HelperNamespace/DataPagerApi.cs | 55 +++++++++++++++++++ .../Javascript/Ast/JsAstHelpers.cs | 5 ++ .../JavascriptTranslatableMethodCollection.cs | 9 ++- 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/Framework/Framework/Binding/HelperNamespace/DataPagerApi.cs diff --git a/src/Framework/Framework/Binding/HelperNamespace/DataPagerApi.cs b/src/Framework/Framework/Binding/HelperNamespace/DataPagerApi.cs new file mode 100644 index 0000000000..4c538ffba1 --- /dev/null +++ b/src/Framework/Framework/Binding/HelperNamespace/DataPagerApi.cs @@ -0,0 +1,55 @@ +using System; +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; +using DotVVM.Framework.Controls; + +namespace DotVVM.Framework.Binding.HelperNamespace +{ + public class DataPagerApi + { + public void Load() => throw new NotSupportedException("The _dataPager.Load method is not supported on the server, please use a staticCommand to invoke it."); + + + + + public class DataPagerExtensionParameter : BindingExtensionParameter + { + public DataPagerExtensionParameter(string identifier, bool inherit = true) : base(identifier, ResolvedTypeDescriptor.Create(typeof(DataPagerApi)), inherit) + { + } + + public override JsExpression GetJsTranslation(JsExpression dataContext) => + new JsObjectExpression(); + public override Expression GetServerEquivalent(Expression controlParameter) => + Expression.New(typeof(DataPagerApi)); + } + + public class AddParameterDataContextChangeAttribute: DataContextChangeAttribute + { + public AddParameterDataContextChangeAttribute(string name = "_dataPager", int order = 0) + { + Name = name; + Order = order; + } + + public string Name { get; } + public override int Order { get; } + + public override ITypeDescriptor? GetChildDataContextType(ITypeDescriptor dataContext, IDataContextStack controlContextStack, IAbstractControl control, IPropertyDescriptor? property = null) => + dataContext; + public override Type? GetChildDataContextType(Type dataContext, DataContextStack controlContextStack, DotvvmBindableObject control, DotvvmProperty? property = null) => dataContext; + + public override IEnumerable GetExtensionParameters(ITypeDescriptor dataContext) + { + return new BindingExtensionParameter[] { + new DataPagerExtensionParameter(Name) + }; + } + } + } +} diff --git a/src/Framework/Framework/Compilation/Javascript/Ast/JsAstHelpers.cs b/src/Framework/Framework/Compilation/Javascript/Ast/JsAstHelpers.cs index 53e710a8a8..b1f9593e3f 100644 --- a/src/Framework/Framework/Compilation/Javascript/Ast/JsAstHelpers.cs +++ b/src/Framework/Framework/Compilation/Javascript/Ast/JsAstHelpers.cs @@ -46,6 +46,11 @@ public static JsBlockStatement AsBlock(this JsStatement statement) => public static JsBlockStatement AsBlock(this IEnumerable statements) => new JsBlockStatement(statements); + public static JsExpression AsSequenceOperators(this IEnumerable expressions) => + expressions.OfType().Aggregate((a, b) => new JsBinaryExpression(a, BinaryOperatorType.Sequence, b)); + public static JsExpression AsSequenceOperators(params JsExpression?[] expressions) => + expressions.AsSequenceOperators(); + public static JsArrayExpression ArrayExpression(this IEnumerable items) => new JsArrayExpression(items); diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index 978b2ee64a..ce71362722 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -835,14 +835,19 @@ 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), new JsSymbolicParameter(JavascriptTranslator.KnockoutContextParameter).Member("$gridViewDataSetHelper").Member("loadDataSet")).WithAnnotation(new ResultIsPromiseAnnotation(e => e)))); + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("loadDataSet").Invoke(args[1].WithAnnotation(ShouldBeObservableAnnotation.Instance), dataSetHelper.Clone().Member("loadDataSet")).WithAnnotation(new ResultIsPromiseAnnotation(e => e)))); AddMethodTranslator(() => GridViewDataSetBindingProvider.GetCurrentGridDataSet(), new GenericMethodCompiler(args => - new JsSymbolicParameter(JavascriptTranslator.KnockoutContextParameter).Member("$gridViewDataSetHelper").Member("dataSet") + dataSetHelper.Clone().Member("dataSet") )); + // _dataPager.Load() + AddMethodTranslator(() => default(DataPagerApi)!.Load(), new GenericMethodCompiler(args => + dataSetHelper.Member("loadNextPage").Invoke().WithAnnotation(new ResultIsPromiseAnnotation(e => e)))); + // PagingOptions AddMethodTranslator(() => default(PagingOptions)!.GoToFirstPage(),new GenericMethodCompiler(args => new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("PagingOptions").Member("goToFirstPage") From ea4264eef70c6ed3b95eeeba4fd4437184bd9c2d 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 21/60] 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 ce71362722..086c2530d7 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -835,11 +835,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 6480b3a0a2..11c0ed71f7 100644 --- a/src/Framework/Framework/Resources/Scripts/binding-handlers/all-handlers.ts +++ b/src/Framework/Framework/Resources/Scripts/binding-handlers/all-handlers.ts @@ -11,6 +11,7 @@ import namedCommand from './named-command' import fileUpload from './file-upload' import jsComponents from './js-component' import modalDialog from './modal-dialog' +import appendableDataPager from './appendable-data-pager' type KnockoutHandlerDictionary = { [name: string]: KnockoutBindingHandler @@ -28,7 +29,8 @@ const allHandlers: KnockoutHandlerDictionary = { ...namedCommand, ...fileUpload, ...jsComponents, - ...modalDialog + ...modalDialog, + ...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 eae8c3a9d7..c783bf3398 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"]) { @@ -128,6 +128,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 737ecdd02f..858718fe72 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 216f55da77..9d17a349d5 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -33,6 +33,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); From 1af1b4f4ee936a803b77cd91416401617c7e351a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 19 Nov 2023 14:58:54 +0100 Subject: [PATCH 22/60] Uncommented commented-out samples --- .../ControlSamples/GridView/GridViewStaticCommand.dothtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml b/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml index fdf6fb7cf2..7e1c2fc9b4 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 @@ - --%> + From 3b477a30bb0b04bb0d0ca552db26104643d691b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 19 Nov 2023 20:52:37 +0100 Subject: [PATCH 23/60] AppendableDataPager: make it work in automatic mode * 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 --- .../JavascriptTranslatableMethodCollection.cs | 7 +-- .../Javascript/JsParensFixingVisitor.cs | 5 +- .../Framework/Controls/AppendableDataPager.cs | 37 ++++++++++----- .../GridViewDataSetBindingProvider.cs | 21 ++++----- .../Framework/Controls/KnockoutHelper.cs | 18 ++++--- .../Controls/PostbackScriptOptions.cs | 47 ++++++++++++++----- .../binding-handlers/appendable-data-pager.ts | 15 ++++-- 7 files changed, 98 insertions(+), 52 deletions(-) diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index 086c2530d7..5b1d0ed2f7 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -841,12 +841,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(), new GenericMethodCompiler(args => - dataSetHelper.Clone().Member("dataSet") - )); // _dataPager.Load() AddMethodTranslator(() => default(DataPagerApi)!.Load(), new GenericMethodCompiler(args => diff --git a/src/Framework/Framework/Compilation/Javascript/JsParensFixingVisitor.cs b/src/Framework/Framework/Compilation/Javascript/JsParensFixingVisitor.cs index 02a1f0a16b..44432ef220 100644 --- a/src/Framework/Framework/Compilation/Javascript/JsParensFixingVisitor.cs +++ b/src/Framework/Framework/Compilation/Javascript/JsParensFixingVisitor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -56,7 +56,8 @@ public override string ToString() return Precedence + (IsPreferredSide ? "+" : "-") + " (" + name + ")"; } - public static readonly OperatorPrecedence Max = new OperatorPrecedence(20, true); + /// Assume that the expression is an atomic + public static OperatorPrecedence Max => new OperatorPrecedence(20, true); /// atomic expression, like `x`, `(x + y)`, `0`, `{"f": 123}`, `x[1]`, ... public const byte Atomic = 20; diff --git a/src/Framework/Framework/Controls/AppendableDataPager.cs b/src/Framework/Framework/Controls/AppendableDataPager.cs index 1433895eb0..f246d4f6ef 100644 --- a/src/Framework/Framework/Controls/AppendableDataPager.cs +++ b/src/Framework/Framework/Controls/AppendableDataPager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using DotVVM.Framework.Binding; @@ -6,6 +6,7 @@ using DotVVM.Framework.Binding.HelperNamespace; using DotVVM.Framework.Compilation.Javascript; using DotVVM.Framework.Hosting; +using DotVVM.Framework.Utils; namespace DotVVM.Framework.Controls { @@ -45,6 +46,7 @@ public IPageableGridViewDataSet DataSet public static readonly DotvvmProperty DataSetProperty = DotvvmProperty.Register(c => c.DataSet, null); + [MarkupOptions(Required = true)] public ICommandBinding? LoadData { get => (ICommandBinding?)GetValue(LoadDataProperty); @@ -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); } diff --git a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs index 80a8095224..00d6376ded 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -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; @@ -254,11 +256,14 @@ private ICommandBinding CreateLoadDataDelegateCommandBinding( }); } - 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")) + ); /// /// A sentinel method which is translated to load the GridViewDataSet on the client side using the Load delegate. @@ -276,12 +281,6 @@ public static Task DataSetClientSideLoad Returns the DataSet we currently work on from the $context.$gridViewDataSetHelper.dataSet - public static T GetCurrentGridDataSet() where T : IBaseGridViewDataSet - { - throw new InvalidOperationException("This method cannot be called on the server!"); - } - private static Type GetOptionsConcreteType(Type dataSetConcreteType, out PropertyInfo optionsProperty) { if (!typeof(TDataSetInterface).IsAssignableFrom(dataSetConcreteType)) diff --git a/src/Framework/Framework/Controls/KnockoutHelper.cs b/src/Framework/Framework/Controls/KnockoutHelper.cs index e0438393ea..2d1bfb5a9f 100644 --- a/src/Framework/Framework/Controls/KnockoutHelper.cs +++ b/src/Framework/Framework/Controls/KnockoutHelper.cs @@ -114,20 +114,20 @@ public static void AddKnockoutForeachDataBind(this IHtmlWriter writer, string ex /// Generates a function expression that invokes the command with specified commandArguments. Creates code like `(...commandArguments) => dotvvm.postBack(...)` 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})"; } /// Generates Javascript code which executes the specified command binding . @@ -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; diff --git a/src/Framework/Framework/Controls/PostbackScriptOptions.cs b/src/Framework/Framework/Controls/PostbackScriptOptions.cs index 745a99db01..9d1baf4033 100644 --- a/src/Framework/Framework/Controls/PostbackScriptOptions.cs +++ b/src/Framework/Framework/Controls/PostbackScriptOptions.cs @@ -1,25 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; using DotVVM.Framework.Compilation.Javascript; namespace DotVVM.Framework.Controls { /// Options for the method. - public class PostbackScriptOptions + public sealed record PostbackScriptOptions { /// 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. - public bool UseWindowSetTimeout { get; set; } + public bool UseWindowSetTimeout { get; init; } /// Return value of the event handler. If set to false, the script will also include event.stopPropagation() - public bool? ReturnValue { get; set; } - public bool IsOnChange { get; set; } + public bool? ReturnValue { get; init; } + public bool IsOnChange { get; init; } /// Javascript variable where the sender element can be found. Set to $element when in knockout binding. - public CodeParameterAssignment ElementAccessor { get; set; } + public CodeParameterAssignment ElementAccessor { get; init; } /// Javascript variable current knockout binding context can be found. By default, `ko.contextFor({elementAccessor})` is used - public CodeParameterAssignment? KoContext { get; set; } + public CodeParameterAssignment? KoContext { get; init; } /// Javascript expression returning an array of command arguments. - public CodeParameterAssignment? CommandArgs { get; set; } + public CodeParameterAssignment? CommandArgs { get; init; } /// When set to false, postback handlers will not be invoked for this command. - public bool AllowPostbackHandlers { get; } + public bool AllowPostbackHandlers { get; init; } /// Javascript expression returning AbortSignal which can be used to cancel the postback (it's a JS variant of CancellationToken). - public CodeParameterAssignment? AbortSignal { get; } + public CodeParameterAssignment? AbortSignal { get; init; } + public Func? ParameterAssignment { get; init; } /// 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. /// Return value of the event handler. If set to false, the script will also include event.stopPropagation() @@ -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? parameterAssignment = null) { this.UseWindowSetTimeout = useWindowSetTimeout; this.ReturnValue = returnValue; @@ -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; + } + + /// Default postback options, optimal for placing the script into a `onxxx` event attribute. + 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(); + 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(); } } } 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 index 453b34a664..9b409ae41a 100644 --- a/src/Framework/Framework/Resources/Scripts/binding-handlers/appendable-data-pager.ts +++ b/src/Framework/Framework/Resources/Scripts/binding-handlers/appendable-data-pager.ts @@ -1,6 +1,9 @@ -type AppendableDataPagerBinding = { +import { getStateManager } from "../dotvvm-base"; + +type AppendableDataPagerBinding = { autoLoadWhenInViewport: boolean, - loadNextPage: () => Promise + loadNextPage: () => Promise, + dataSet: any }; export default { @@ -15,9 +18,9 @@ export default { if (isLoading) return; let entry = entries[0]; - while (entry.isIntersecting) { - const dataSet = allBindingsAccessor.get("dotvvm-gridviewdataset").dataSet as DotvvmObservable; - if (dataSet.state.PagingOptions.IsLastPage) { + while (entry?.isIntersecting) { + const dataSet = valueAccessor().dataSet; + if (dataSet.PagingOptions().IsLastPage()) { return; } @@ -25,6 +28,8 @@ export default { try { await binding.loadNextPage(); + // getStateManager().doUpdateNow(); + // when the loading was finished, check whether we need to load another page entry = observer.takeRecords()[0]; } From 1f71b0f1aca74df1685eed2aec9cc23377647949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 19 Nov 2023 20:58:57 +0100 Subject: [PATCH 24/60] Format knockout binding group as multiline expression when some expression is multiline --- .../Controls/KnockoutBindingGroup.cs | 12 +++++- .../Controls/PostbackScriptOptions.cs | 2 +- .../testoutputs/AutoUITests.BasicColumn.html | 2 +- .../testoutputs/AutoUITests.BasicGrid.html | 2 +- .../DataPagerTests.CommandDataPager.html | 2 +- ...wTests.RequiredResourceInEditTemplate.html | 2 +- ...ts.MarkupControl_PassingStaticCommand.html | 12 ++++-- ...verSideStyleTests.BindableObjectReads.html | 4 +- ...alizationTests.SerializeDefaultConfig.json | 43 +++++++++++++++++++ src/Tests/ViewModel/GridViewDataSetTests.cs | 2 +- 10 files changed, 70 insertions(+), 13 deletions(-) diff --git a/src/Framework/Framework/Controls/KnockoutBindingGroup.cs b/src/Framework/Framework/Controls/KnockoutBindingGroup.cs index aadc0a5c07..be71e3db6e 100644 --- a/src/Framework/Framework/Controls/KnockoutBindingGroup.cs +++ b/src/Framework/Framework/Controls/KnockoutBindingGroup.cs @@ -85,7 +85,17 @@ protected virtual string GetKnockoutBindingExpression(DotvvmBindableObject obj, public override string ToString() { if (entries.Count == 0) return "{}"; - return "{ " + string.Join(", ", entries) + " }"; + bool multiline = false; + foreach (var entry in entries) + if (entry.Expression.Contains("\n")) + { + multiline = true; + break; + } + if (multiline) + return "{\n" + string.Join(",\n", entries) + "\n}"; + else + return "{ " + string.Join(", ", entries) + " }"; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/src/Framework/Framework/Controls/PostbackScriptOptions.cs b/src/Framework/Framework/Controls/PostbackScriptOptions.cs index 9d1baf4033..2a77dd9cb4 100644 --- a/src/Framework/Framework/Controls/PostbackScriptOptions.cs +++ b/src/Framework/Framework/Controls/PostbackScriptOptions.cs @@ -70,7 +70,7 @@ public override string ToString() 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(); + return new StringBuilder("new PostbackScriptOptions(").Append(string.Join(", ", fields.ToArray())).Append(")").ToString(); } } } diff --git a/src/Tests/ControlTests/testoutputs/AutoUITests.BasicColumn.html b/src/Tests/ControlTests/testoutputs/AutoUITests.BasicColumn.html index 287823dd09..b418550161 100644 --- a/src/Tests/ControlTests/testoutputs/AutoUITests.BasicColumn.html +++ b/src/Tests/ControlTests/testoutputs/AutoUITests.BasicColumn.html @@ -3,7 +3,7 @@ - +
    diff --git a/src/Tests/ControlTests/testoutputs/AutoUITests.BasicGrid.html b/src/Tests/ControlTests/testoutputs/AutoUITests.BasicGrid.html index ab33b4c923..117a6f076c 100644 --- a/src/Tests/ControlTests/testoutputs/AutoUITests.BasicGrid.html +++ b/src/Tests/ControlTests/testoutputs/AutoUITests.BasicGrid.html @@ -3,7 +3,7 @@ - +
    diff --git a/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html index 7f541e0b71..0b6f4a1cde 100644 --- a/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html +++ b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html @@ -1,7 +1,7 @@ -
      +
      • ««
      • diff --git a/src/Tests/ControlTests/testoutputs/GridViewTests.RequiredResourceInEditTemplate.html b/src/Tests/ControlTests/testoutputs/GridViewTests.RequiredResourceInEditTemplate.html index 0e56043937..d164cdd122 100644 --- a/src/Tests/ControlTests/testoutputs/GridViewTests.RequiredResourceInEditTemplate.html +++ b/src/Tests/ControlTests/testoutputs/GridViewTests.RequiredResourceInEditTemplate.html @@ -3,7 +3,7 @@ - +
        diff --git a/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_PassingStaticCommand.html b/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_PassingStaticCommand.html index 1f82d436d3..43851c8d8c 100644 --- a/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_PassingStaticCommand.html +++ b/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_PassingStaticCommand.html @@ -3,9 +3,11 @@ -
        (dotvvm.applyPostbackHandlers(async (options) => { await dotvvm.staticCommandPostback("WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuVGVzdFNlcnZpY2UsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMiLCJTYXZlIixbXSwxLG51bGwsIkFRQT0iXQ==", [$context.$parent.int.state], options); -},$element,[],args,$context)) }"> +},$element,[],args,$context)) +}"> -
        (dotvvm.applyPostbackHandlers(async (options) => { await dotvvm.staticCommandPostback("WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuVGVzdFNlcnZpY2UsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMiLCJTYXZlIixbXSwxLG51bGwsIkFRQT0iXQ==", [$context.$rawData.state], options); -},$element,[],args,$context)) }"> +},$element,[],args,$context)) +}"> +
        @@ -23,7 +23,7 @@ - +
        diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index fd99dc07e1..baa1050b22 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -362,6 +362,35 @@ "mappingMode": "InnerElement" } }, + "DotVVM.Framework.Controls.AppendableDataPager": { + "DataSet": { + "type": "DotVVM.Framework.Controls.IPageableGridViewDataSet, DotVVM.Core", + "required": true, + "onlyBindings": true + }, + "EndTemplate": { + "type": "DotVVM.Framework.Controls.ITemplate, DotVVM.Framework", + "mappingMode": "InnerElement", + "onlyHardcoded": true + }, + "LoadData": { + "type": "DotVVM.Framework.Binding.Expressions.ICommandBinding, DotVVM.Framework", + "required": true, + "onlyBindings": true + }, + "LoadTemplate": { + "type": "DotVVM.Framework.Controls.ITemplate, DotVVM.Framework", + "dataContextChange": [ + { + "$type": "DotVVM.Framework.Binding.HelperNamespace.DataPagerApi+AddParameterDataContextChangeAttribute, DotVVM.Framework", + "Name": "_dataPager", + "TypeId": "DotVVM.Framework.Binding.HelperNamespace.DataPagerApi+AddParameterDataContextChangeAttribute, DotVVM.Framework" + } + ], + "mappingMode": "InnerElement", + "onlyHardcoded": true + } + }, "DotVVM.Framework.Controls.AuthenticatedView": { "AuthenticatedTemplate": { "type": "DotVVM.Framework.Controls.ITemplate, DotVVM.Framework", @@ -389,6 +418,10 @@ "type": "DotVVM.Framework.Binding.Expressions.Command, DotVVM.Framework", "isCommand": true }, + "ClickArguments": { + "type": "System.Collections.Generic.List`1[[System.Object, CoreLibrary]]", + "mappingMode": "Exclude" + }, "Enabled": { "type": "System.Boolean", "defaultValue": true @@ -745,6 +778,10 @@ "defaultValue": false, "onlyHardcoded": true }, + "LoadData": { + "type": "DotVVM.Framework.Binding.Expressions.ICommandBinding, DotVVM.Framework", + "onlyBindings": true + }, "RowDecorators": { "type": "System.Collections.Generic.List`1[[DotVVM.Framework.Controls.Decorator, DotVVM.Framework, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da]]", "dataContextChange": [ @@ -2007,6 +2044,12 @@ "baseType": "DotVVM.Framework.Controls.Decorator, DotVVM.Framework", "withoutContent": true }, + "DotVVM.Framework.Controls.AppendableDataPager": { + "assembly": "DotVVM.Framework", + "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework", + "defaultContentProperty": "LoadTemplate", + "withoutContent": true + }, "DotVVM.Framework.Controls.AuthenticatedView": { "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.ConfigurableHtmlControl, DotVVM.Framework", diff --git a/src/Tests/ViewModel/GridViewDataSetTests.cs b/src/Tests/ViewModel/GridViewDataSetTests.cs index e3347cdd4c..e812d8daea 100644 --- a/src/Tests/ViewModel/GridViewDataSetTests.cs +++ b/src/Tests/ViewModel/GridViewDataSetTests.cs @@ -128,7 +128,7 @@ public void GridViewDataSet_DataPagerCommands_StaticCommand() var goToFirstPage = CompileBinding(commands.GoToFirstPage); Console.WriteLine(goToFirstPage); - XAssert.Equal("dotvvm.applyPostbackHandlers(async (options)=>{let vm=options.viewModel;dotvvm.dataSet.translations.PagingOptions.goToFirstPage(vm.PagingOptions);return await dotvvm.dataSet.loadDataSet(vm,options.knockoutContext.$gridViewDataSetHelper.loadDataSet);},this)", goToFirstPage); + XAssert.Equal("dotvvm.applyPostbackHandlers(async (options)=>{let cx=options.knockoutContext;return await dotvvm.dataSet.loadDataSet(options.viewModel,(options)=>dotvvm.dataSet.translations.PagingOptions.goToFirstPage(ko.unwrap(options).PagingOptions),cx.$gridViewDataSetHelper.loadDataSet,cx.$gridViewDataSetHelper.postProcessor);},this)", goToFirstPage); } private string CompileBinding(ICommandBinding staticCommand) From 0cf410a1d0cdfee63dd7ce03e078ea93655938e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 19 Nov 2023 21:23:32 +0100 Subject: [PATCH 25/60] fixup! AppendableDataPager: make it work in automatic mode --- .../Javascript/JavascriptTranslatableMethodCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index 5b1d0ed2f7..f45ef5e82b 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -847,7 +847,7 @@ private void AddDataSetOptionsTranslations() // _dataPager.Load() AddMethodTranslator(() => default(DataPagerApi)!.Load(), new GenericMethodCompiler(args => - dataSetHelper.Member("loadNextPage").Invoke().WithAnnotation(new ResultIsPromiseAnnotation(e => e)))); + dataSetHelper.Clone().Member("loadNextPage").Invoke().WithAnnotation(new ResultIsPromiseAnnotation(e => e)))); // PagingOptions AddMethodTranslator(() => default(PagingOptions)!.GoToFirstPage(),new GenericMethodCompiler(args => From 912743dcd1617899e4e737e4b4070322927cd76b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 25 Feb 2024 13:49:39 +0100 Subject: [PATCH 26/60] Fixed ClickArguments to support precompilation in composite controls --- .../Framework/Binding/BindingHelper.cs | 19 +++++---- .../Styles/ResolvedControlHelper.cs | 42 +++++++++++++------ .../Framework/Controls/ButtonBase.cs | 7 ++-- .../Framework/Controls/GridViewColumn.cs | 2 +- ...ts.GridView_RowDecorators_AddChildren.html | 2 +- ...alizationTests.SerializeDefaultConfig.json | 2 +- 6 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/Framework/Framework/Binding/BindingHelper.cs b/src/Framework/Framework/Binding/BindingHelper.cs index fc4a901fe5..f5629eb31a 100644 --- a/src/Framework/Framework/Binding/BindingHelper.cs +++ b/src/Framework/Framework/Binding/BindingHelper.cs @@ -256,22 +256,27 @@ public static ParametrizedCode GetParametrizedCommandJavascript(this ICommandBin /// /// Gets command arguments parametrized code from the arguments collection. /// - public static CodeParameterAssignment? GetParametrizedCommandArgs(DotvvmControl control, IEnumerable argumentsCollection) + public static CodeParameterAssignment? GetParametrizedCommandArgs(DotvvmControl control, IEnumerable? argumentsCollection) { var builder = new ParametrizedCode.Builder(); var isFirst = true; builder.Add("["); - foreach (var arg in argumentsCollection) + if (argumentsCollection is not null) { - if (!isFirst) + foreach (var arg in argumentsCollection) { - builder.Add(","); - } - isFirst = false; + if (!isFirst) + { + builder.Add(","); + } + + isFirst = false; - builder.Add(ValueOrBinding.FromBoxedValue(arg).GetParametrizedJsExpression(control)); + builder.Add(ValueOrBinding.FromBoxedValue(arg).GetParametrizedJsExpression(control)); + } } + builder.Add("]"); return builder.Build(OperatorPrecedence.Max); diff --git a/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs b/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs index 51669e764b..24ac760cf6 100644 --- a/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs +++ b/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq.Expressions; using DotVVM.Framework.Binding; @@ -134,20 +135,35 @@ public record PropertyTranslationException(DotvvmProperty property): } private static Type[] ImmutableContainers = new[] { - typeof(ImmutableArray<>), typeof(ImmutableList<>), typeof(ImmutableDictionary<,>), typeof(ImmutableHashSet<>), typeof(ImmutableQueue<>), typeof(ImmutableSortedDictionary<,>), typeof(ImmutableSortedSet<>), typeof(ImmutableStack<>) + typeof(ImmutableArray<>), typeof(ImmutableList<>), typeof(ImmutableHashSet<>), typeof(ImmutableQueue<>), typeof(ImmutableSortedSet<>), typeof(ImmutableStack<>) }; - internal static bool IsImmutableObject(Type type) => - typeof(IBinding).IsAssignableFrom(type ?? throw new ArgumentNullException(nameof(type))) - || type.GetCustomAttribute() is object - || type.IsGenericType && ImmutableContainers.Contains(type.GetGenericTypeDefinition()) && type.GenericTypeArguments.All(IsImmutableObject); - - public static bool IsAllowedPropertyValue([NotNullWhen(false)] object? value) => - value is ValueOrBinding vob && IsAllowedPropertyValue(vob.UnwrapToObject()) || - value is null || - ReflectionUtils.IsPrimitiveType(value.GetType()) || - value is HtmlGenericControl.AttributeList || - IsImmutableObject(value.GetType()) || - value is Array && ReflectionUtils.IsPrimitiveType(value.GetType().GetElementType()!); + + public static bool IsAllowedPropertyValue([NotNullWhen(false)] object? value) + { + if (value is ValueOrBinding vob && IsAllowedPropertyValue(vob.UnwrapToObject()) || + value is HtmlGenericControl.AttributeList || + value is IBinding || + value is null) + { + return true; + } + + var type = value.GetType(); + if (ReflectionUtils.IsPrimitiveType(type) || + type.GetCustomAttribute() is not null) + { + return true; + } + + // technically we should not allow Array as it can be modified, but we use it already on many places + if (value is Array || + (type.IsGenericType && ImmutableContainers.Contains(type.GetGenericTypeDefinition()))) + { + return ((IEnumerable)value).OfType().All(IsAllowedPropertyValue); + } + + return false; + } public static ResolvedPropertySetter TranslateProperty(DotvvmProperty property, object? value, DataContextStack dataContext, DotvvmConfiguration? config) { diff --git a/src/Framework/Framework/Controls/ButtonBase.cs b/src/Framework/Framework/Controls/ButtonBase.cs index e78ed46de9..7a15382d27 100644 --- a/src/Framework/Framework/Controls/ButtonBase.cs +++ b/src/Framework/Framework/Controls/ButtonBase.cs @@ -42,13 +42,13 @@ public Command? Click /// This property is typically used from the code-behind to allow sharing the same binding expressions among multiple buttons. /// [MarkupOptions(MappingMode = MappingMode.Exclude)] - public List ClickArguments + public object?[]? ClickArguments { - get { return (List)GetValue(ClickArgumentsProperty)!; } + get { return (object?[])GetValue(ClickArgumentsProperty)!; } set { SetValue(ClickArgumentsProperty, value); } } public static readonly DotvvmProperty ClickArgumentsProperty - = DotvvmProperty.Register, ButtonBase>(c => c.ClickArguments, null); + = DotvvmProperty.Register(c => c.ClickArguments, null); /// /// Gets or sets a value indicating whether the button is enabled and can be clicked on. @@ -77,7 +77,6 @@ public TextOrContentCapability TextOrContentCapability /// public ButtonBase(string tagName) : base(tagName) { - ClickArguments = new List(); } diff --git a/src/Framework/Framework/Controls/GridViewColumn.cs b/src/Framework/Framework/Controls/GridViewColumn.cs index 45d4823c61..bf6303509d 100644 --- a/src/Framework/Framework/Controls/GridViewColumn.cs +++ b/src/Framework/Framework/Controls/GridViewColumn.cs @@ -208,7 +208,7 @@ public virtual void CreateHeaderControls(IDotvvmRequestContext context, GridView var linkButton = new LinkButton(); linkButton.SetValue(ButtonBase.TextProperty, GetValueRaw(HeaderTextProperty)); - linkButton.ClickArguments.Add(sortExpression); + linkButton.ClickArguments = new object?[] { sortExpression }; cell.Children.Add(linkButton); linkButton.SetBinding(ButtonBase.ClickProperty, sortCommandBinding); diff --git a/src/Tests/ControlTests/testoutputs/GridViewTests.GridView_RowDecorators_AddChildren.html b/src/Tests/ControlTests/testoutputs/GridViewTests.GridView_RowDecorators_AddChildren.html index 75e9ff09ae..a31bca1b8a 100644 --- a/src/Tests/ControlTests/testoutputs/GridViewTests.GridView_RowDecorators_AddChildren.html +++ b/src/Tests/ControlTests/testoutputs/GridViewTests.GridView_RowDecorators_AddChildren.html @@ -3,7 +3,7 @@ - +
        diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index baa1050b22..ae2804203e 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -419,7 +419,7 @@ "isCommand": true }, "ClickArguments": { - "type": "System.Collections.Generic.List`1[[System.Object, CoreLibrary]]", + "type": "System.Object[]", "mappingMode": "Exclude" }, "Enabled": { From 07c3d2fcb8903babd044f2304bcbfded440bad2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 25 Feb 2024 16:21:35 +0100 Subject: [PATCH 27/60] Finished multi-criterion sorting --- .../Core/Controls/Options/ISortingOptions.cs | 24 ++---- .../Options/MultiCriteriaSortingOptions.cs | 81 ++++++++----------- .../Options/NextTokenHistoryPagingOptions.cs | 4 +- .../Core/Controls/Options/PagingOptions.cs | 6 +- .../{ => Options}/SortingImplementation.cs | 2 +- .../Core/Controls/Options/SortingOptions.cs | 9 ++- .../JavascriptTranslatableMethodCollection.cs | 26 +++++- src/Framework/Framework/Controls/GridView.cs | 28 +++---- .../Framework/Controls/GridViewColumn.cs | 33 +++++--- .../Framework/Controls/GridViewCommands.cs | 29 ++++++- .../GridViewDataSetBindingProvider.cs | 44 +++++++++- .../Resources/Scripts/dataset/translations.ts | 47 +++++++++++ .../GridViewStaticCommandViewModel.cs | 46 +++++++---- .../GridView/GridViewStaticCommand.dothtml | 8 +- 14 files changed, 257 insertions(+), 130 deletions(-) rename src/Framework/Core/Controls/{ => Options}/SortingImplementation.cs (98%) diff --git a/src/Framework/Core/Controls/Options/ISortingOptions.cs b/src/Framework/Core/Controls/Options/ISortingOptions.cs index 3bc251ff67..108084ca8e 100644 --- a/src/Framework/Core/Controls/Options/ISortingOptions.cs +++ b/src/Framework/Core/Controls/Options/ISortingOptions.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace DotVVM.Framework.Controls { /// @@ -28,30 +26,18 @@ public interface ISortingSetSortExpressionCapability : ISortingOptions /// /// Represents sorting options that can specify one sorting criterion. /// - public interface ISortingSingleCriterionCapability : ISortingOptions + public interface ISortingStateCapability : ISortingOptions { - /// - /// Gets or sets whether the sort order should be descending. + /// Determines whether the column with specified sort expression is sorted in ascending order. /// - bool SortDescending { get; } + bool IsColumnSortedAscending(string? sortExpression); /// - /// Gets or sets the name of the property that is used for sorting. Null means the grid should not be sorted. + /// Determines whether the column with specified sort expression is sorted in descending order. /// - string? SortExpression { get; } - - } + bool IsColumnSortedDescending(string? sortExpression); - /// - /// Represents sorting options which support multiple sort criteria. - /// - public interface ISortingMultipleCriteriaCapability : ISortingOptions - { - /// - /// Gets or sets the list of sort criteria. - /// - IList Criteria { get; } } /// diff --git a/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs b/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs index 49c6a0f69c..f91651fb66 100644 --- a/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs +++ b/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs @@ -1,77 +1,60 @@ using System; using System.Collections.Generic; using System.Linq; +using DotVVM.Framework.Controls.Options; namespace DotVVM.Framework.Controls; -public class MultiCriteriaSortingOptions : SortingOptions, ISortingMultipleCriteriaCapability +public class MultiCriteriaSortingOptions : ISortingOptions, ISortingStateCapability, ISortingSetSortExpressionCapability, IApplyToQueryable { public IList Criteria { get; set; } = new List(); public int MaxSortCriteriaCount { get; set; } = 3; - public override IQueryable ApplyToQueryable(IQueryable queryable) + public virtual IQueryable ApplyToQueryable(IQueryable queryable) { foreach (var criterion in Criteria.Reverse()) { queryable = SortingImplementation.ApplySortingToQueryable(queryable, criterion.SortExpression, criterion.SortDescending); } - - return base.ApplyToQueryable(queryable); + return queryable; } - public override void SetSortExpression(string? sortExpression) + public virtual bool IsSortingAllowed(string sortExpression) => true; + + public virtual void SetSortExpression(string? sortExpression) { - if (SortExpression == null) + if (sortExpression == null) + { + Criteria.Clear(); + return; + } + + var index = Criteria.ToList().FindIndex(c => c.SortExpression == sortExpression); + if (index == 0) { - SortExpression = sortExpression; - SortDescending = false; + // toggle the sort direction if we clicked on the column on the front + Criteria[index].SortDescending = !Criteria[index].SortDescending; } - else if (sortExpression == SortExpression) + else if (index > 0) { - if (!SortDescending) - { - SortDescending = true; - } - else if (Criteria.Any()) - { - SortExpression = Criteria[0].SortExpression; - SortDescending = Criteria[0].SortDescending; - Criteria.RemoveAt(0); - } - else - { - SortExpression = null; - SortDescending = false; - } + // if the column is already sorted, move it to the front + Criteria.RemoveAt(index); + Criteria.Insert(0, new SortCriterion() { SortExpression = sortExpression }); } else { - var index = Criteria.ToList().FindIndex(c => c.SortExpression == sortExpression); - if (index >= 0) - { - if (!Criteria[index].SortDescending) - { - Criteria[index].SortDescending = true; - } - else - { - Criteria.RemoveAt(index); - } - } - else - { - if (Criteria.Count < MaxSortCriteriaCount - 1) - { - Criteria.Add(new SortCriterion() { SortExpression = sortExpression }); - } - else - { - SortExpression = sortExpression; - SortDescending = false; - Criteria.Clear(); - } - } + // add the column to the front + Criteria.Insert(0, new SortCriterion() { SortExpression = sortExpression }); + } + + while (Criteria.Count > MaxSortCriteriaCount) + { + Criteria.RemoveAt(Criteria.Count - 1); } } + + public bool IsColumnSortedAscending(string? sortExpression) => Criteria.Any(c => c.SortExpression == sortExpression && !c.SortDescending); + + public bool IsColumnSortedDescending(string? sortExpression) => Criteria.Any(c => c.SortExpression == sortExpression && c.SortDescending); } diff --git a/src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs b/src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs index 1c6e2ae45e..18975f565d 100644 --- a/src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs +++ b/src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs @@ -7,7 +7,7 @@ namespace DotVVM.Framework.Controls // TODO: comments public class NextTokenHistoryPagingOptions : IPagingFirstPageCapability, IPagingPreviousPageCapability, IPagingPageIndexCapability { - public List TokenHistory { get; set; } = new List { null }; + public List TokenHistory { get; set; } = new(); public int PageIndex { get; set; } = 0; @@ -33,7 +33,7 @@ public void GoToPage(int pageIndex) /// /// Gets a list of page indexes near the current page. Override this method to provide your own strategy. /// - public virtual IList GetNearPageIndexes() + protected virtual IList GetNearPageIndexes() { return GetDefaultNearPageIndexes(5); } diff --git a/src/Framework/Core/Controls/Options/PagingOptions.cs b/src/Framework/Core/Controls/Options/PagingOptions.cs index 0de9c546fc..303de1491b 100644 --- a/src/Framework/Core/Controls/Options/PagingOptions.cs +++ b/src/Framework/Core/Controls/Options/PagingOptions.cs @@ -101,7 +101,7 @@ public virtual IList GetNearPageIndexes() /// /// Returns the near page indexes in the maximum specified distance from the current page index. /// - protected IList GetDefaultNearPageIndexes(int distance) + protected virtual IList GetDefaultNearPageIndexes(int distance) { var firstIndex = Math.Max(PageIndex - distance, 0); var lastIndex = Math.Min(PageIndex + distance + 1, PagesCount); @@ -110,12 +110,12 @@ protected IList GetDefaultNearPageIndexes(int distance) .ToList(); } - public IQueryable ApplyToQueryable(IQueryable queryable) + public virtual IQueryable ApplyToQueryable(IQueryable queryable) { return PagingImplementation.ApplyPagingToQueryable(queryable, this); } - public void ProcessLoadedItems(IQueryable filteredQueryable, IList items) + public virtual void ProcessLoadedItems(IQueryable filteredQueryable, IList items) { TotalItemsCount = filteredQueryable.Count(); } diff --git a/src/Framework/Core/Controls/SortingImplementation.cs b/src/Framework/Core/Controls/Options/SortingImplementation.cs similarity index 98% rename from src/Framework/Core/Controls/SortingImplementation.cs rename to src/Framework/Core/Controls/Options/SortingImplementation.cs index 11683505aa..17fdbaaf26 100644 --- a/src/Framework/Core/Controls/SortingImplementation.cs +++ b/src/Framework/Core/Controls/Options/SortingImplementation.cs @@ -4,7 +4,7 @@ using System.Reflection; using DotVVM.Framework.ViewModel; -namespace DotVVM.Framework.Controls +namespace DotVVM.Framework.Controls.Options { public static class SortingImplementation { diff --git a/src/Framework/Core/Controls/Options/SortingOptions.cs b/src/Framework/Core/Controls/Options/SortingOptions.cs index 9d545fbe66..da2bd17df2 100644 --- a/src/Framework/Core/Controls/Options/SortingOptions.cs +++ b/src/Framework/Core/Controls/Options/SortingOptions.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using DotVVM.Framework.Controls.Options; using DotVVM.Framework.ViewModel; namespace DotVVM.Framework.Controls @@ -9,7 +10,7 @@ namespace DotVVM.Framework.Controls /// /// Represents a default implementation of the sorting options. /// - public class SortingOptions : ISortingOptions, ISortingSingleCriterionCapability, ISortingSetSortExpressionCapability, IApplyToQueryable + public class SortingOptions : ISortingOptions, ISortingStateCapability, ISortingSetSortExpressionCapability, IApplyToQueryable { /// /// Gets or sets whether the sort order should be descending. @@ -54,5 +55,11 @@ public virtual IQueryable ApplyToQueryable(IQueryable queryable) { return SortingImplementation.ApplySortingToQueryable(queryable, SortExpression, SortDescending); } + + /// + public bool IsColumnSortedAscending(string? sortExpression) => SortExpression == sortExpression && !SortDescending; + + /// + public bool IsColumnSortedDescending(string? sortExpression) => SortExpression == sortExpression && SortDescending; } } diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index f45ef5e82b..0fe0d6d82d 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -889,9 +889,29 @@ private void AddDataSetOptionsTranslations() .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]))); + AddSetSortExpressionTranslation(); + AddSortingStateTranslation(); + AddSetSortExpressionTranslation(); + AddSortingStateTranslation(); + + void AddSetSortExpressionTranslation(string? clientTypeName = null) + where TSortingOptions : ISortingOptions, ISortingSetSortExpressionCapability + { + AddMethodTranslator(typeof(TSortingOptions), nameof(ISortingSetSortExpressionCapability.SetSortExpression), new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member(clientTypeName ?? typeof(TSortingOptions).Name).Member("setSortExpression") + .Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance), args[1]))); + } + + void AddSortingStateTranslation(string? clientTypeName = null) + where TSortingOptions : ISortingOptions, ISortingStateCapability + { + AddMethodTranslator(typeof(TSortingOptions), nameof(ISortingStateCapability.IsColumnSortedAscending), new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member(clientTypeName ?? typeof(TSortingOptions).Name).Member("isColumnSortedAscending") + .Invoke(new JsIdentifierExpression("ko").Member("toJS").Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance)), args[1]))); + AddMethodTranslator(typeof(TSortingOptions), nameof(ISortingStateCapability.IsColumnSortedDescending), new GenericMethodCompiler(args => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member(clientTypeName ?? typeof(TSortingOptions).Name).Member("isColumnSortedDescending") + .Invoke(new JsIdentifierExpression("ko").Member("toJS").Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance)), args[1]))); + } } public JsExpression? TryTranslateCall(LazyTranslatedExpression? context, LazyTranslatedExpression[] args, MethodInfo method) diff --git a/src/Framework/Framework/Controls/GridView.cs b/src/Framework/Framework/Controls/GridView.cs index 257eda1ac3..cfebb59e3d 100644 --- a/src/Framework/Framework/Controls/GridView.cs +++ b/src/Framework/Framework/Controls/GridView.cs @@ -251,7 +251,7 @@ protected virtual void CreateHeaderRow(IDotvvmRequestContext context) var decoratedHeaderRow = Decorator.ApplyDecorators(headerRow, HeaderRowDecorators); head.Children.Add(decoratedHeaderRow); - var sortCommandBinding = BuildSortCommandBinding(); + var (gridViewCommands, sortCommandBindingOverride) = GetGridViewCommandsAndSortBinding(); foreach (var column in Columns.NotNull("GridView.Columns must be set")) { var cell = new HtmlGenericControl("th"); @@ -259,7 +259,7 @@ protected virtual void CreateHeaderRow(IDotvvmRequestContext context) var decoratedCell = Decorator.ApplyDecorators(cell, column.HeaderCellDecorators); headerRow.Children.Add(decoratedCell); - column.CreateHeaderControls(context, this, sortCommandBinding, cell, gridViewDataSet); + column.CreateHeaderControls(context, this, gridViewCommands, sortCommandBindingOverride, cell, gridViewDataSet); if (FilterPlacement == GridViewFilterPlacement.HeaderRow) { column.CreateFilterControls(context, this, cell, gridViewDataSet); @@ -280,21 +280,18 @@ protected virtual void CreateHeaderRow(IDotvvmRequestContext context) } } - private ICommandBinding? BuildSortCommandBinding() + private (GridViewCommands gridViewCommands, ICommandBinding? sortCommandBindingOverride) GetGridViewCommandsAndSortBinding() { if (SortChanged is { } && LoadData is { }) { throw new DotvvmControlException(this, $"The {nameof(LoadData)} and {nameof(SortChanged)} properties cannot be used together!"); } - if (SortChanged is { }) - { - return BuildDefaultSortCommandBinding(); - } - else - { - return BuildLoadDataSortCommandBinding(); - } + var dataContextStack = this.GetDataContextType()!; + var commandType = LoadData is { } ? GridViewDataSetCommandType.LoadDataDelegate : GridViewDataSetCommandType.Default; + var gridViewCommands = gridViewDataSetBindingProvider.GetGridViewCommands(dataContextStack, GetDataSourceBinding(), commandType); + + return (gridViewCommands, SortChanged is { } ? BuildDefaultSortCommandBinding() : null); } protected virtual ICommandBinding BuildDefaultSortCommandBinding() @@ -318,14 +315,7 @@ protected virtual ICommandBinding BuildDefaultSortCommandBinding() new IdBindingProperty($"{this.GetDotvvmUniqueId().GetValue()}_sortBinding") }); } - - protected virtual ICommandBinding? BuildLoadDataSortCommandBinding() - { - var dataContextStack = this.GetDataContextType()!; - var commandType = LoadData is { } ? GridViewDataSetCommandType.LoadDataDelegate : GridViewDataSetCommandType.Default; - return gridViewDataSetBindingProvider.GetGridViewCommands(dataContextStack, GetDataSourceBinding(), commandType).SetSortExpression; - } - + private static void SetCellAttributes(GridViewColumn column, HtmlGenericControl cell, bool isHeaderCell) { var cellAttributes = cell.Attributes; diff --git a/src/Framework/Framework/Controls/GridViewColumn.cs b/src/Framework/Framework/Controls/GridViewColumn.cs index bf6303509d..df5c815fde 100644 --- a/src/Framework/Framework/Controls/GridViewColumn.cs +++ b/src/Framework/Framework/Controls/GridViewColumn.cs @@ -189,7 +189,7 @@ public virtual void CreateEditControls(IDotvvmRequestContext context, DotvvmCont EditTemplate.BuildContent(context, container); } - public virtual void CreateHeaderControls(IDotvvmRequestContext context, GridView gridView, ICommandBinding? sortCommandBinding, HtmlGenericControl cell, IGridViewDataSet? gridViewDataSet) + public virtual void CreateHeaderControls(IDotvvmRequestContext context, GridView gridView, GridViewCommands gridViewCommands, ICommandBinding? sortCommandBindingOverride, HtmlGenericControl cell, IGridViewDataSet? gridViewDataSet) { if (HeaderTemplate != null) { @@ -199,6 +199,7 @@ public virtual void CreateHeaderControls(IDotvvmRequestContext context, GridView if (AllowSorting) { + var sortCommandBinding = gridViewCommands.SetSortExpression ?? sortCommandBindingOverride; if (sortCommandBinding == null) { throw new DotvvmControlException(this, "Cannot use column sorting where no sort command is specified. Either put IGridViewDataSet in the DataSource property of the GridView, or set the SortChanged command on the GridView to implement custom sorting logic!"); @@ -213,7 +214,7 @@ public virtual void CreateHeaderControls(IDotvvmRequestContext context, GridView linkButton.SetBinding(ButtonBase.ClickProperty, sortCommandBinding); - SetSortedCssClass(cell, gridViewDataSet, gridView.GetValueBinding(ItemsControl.DataSourceProperty)!); + SetSortedCssClass(cell, gridViewDataSet, gridViewCommands); } else { @@ -233,27 +234,33 @@ public virtual void CreateFilterControls(IDotvvmRequestContext context, GridView } } - private void SetSortedCssClass(HtmlGenericControl cell, ISortableGridViewDataSet? gridViewDataSet, IValueBinding dataSourceBinding) + private void SetSortedCssClass(HtmlGenericControl cell, ISortableGridViewDataSet? gridViewDataSet, GridViewCommands gridViewCommands) { - // TODO: support multiple criteria - if (gridViewDataSet is ISortableGridViewDataSet sortableGridViewDataSet) + if (gridViewDataSet is ISortableGridViewDataSet sortableGridViewDataSet) { + var sortExpression = GetSortExpression(); + var cellAttributes = cell.Attributes; if (!RenderOnServer) { - // TODO: change how the css class binding is generated - var gridViewDataSetExpr = dataSourceBinding.GetKnockoutBindingExpression(cell, unwrapped: true); - cellAttributes["data-bind"] = $"css: {{ '{SortDescendingHeaderCssClass}': ({gridViewDataSetExpr}).SortingOptions().SortExpression() == '{GetSortExpression()}' && ({gridViewDataSetExpr}).SortingOptions().SortDescending(), '{SortAscendingHeaderCssClass}': ({gridViewDataSetExpr}).SortingOptions().SortExpression() == '{GetSortExpression()}' && !({gridViewDataSetExpr}).SortingOptions().SortDescending()}}"; + if (!string.IsNullOrWhiteSpace(SortAscendingHeaderCssClass)) + { + cell.AddCssClass(SortAscendingHeaderCssClass, gridViewCommands.GetIsColumnSortedAscendingBinding(sortExpression)); + } + if (!string.IsNullOrWhiteSpace(SortDescendingHeaderCssClass)) + { + cell.AddCssClass(SortDescendingHeaderCssClass, gridViewCommands.GetIsColumnSortedDescendingBinding(sortExpression)); + } } - else if (sortableGridViewDataSet.SortingOptions.SortExpression == GetSortExpression()) + else { - if (sortableGridViewDataSet.SortingOptions.SortDescending) + if (sortableGridViewDataSet.SortingOptions.IsColumnSortedAscending(sortExpression)) { - cellAttributes["class"] = SortDescendingHeaderCssClass; + cellAttributes["class"] = SortAscendingHeaderCssClass; } - else + else if (sortableGridViewDataSet.SortingOptions.IsColumnSortedDescending(sortExpression)) { - cellAttributes["class"] = SortAscendingHeaderCssClass; + cellAttributes["class"] = SortDescendingHeaderCssClass; } } } diff --git a/src/Framework/Framework/Controls/GridViewCommands.cs b/src/Framework/Framework/Controls/GridViewCommands.cs index 52cf0597fb..d51a4d83e2 100644 --- a/src/Framework/Framework/Controls/GridViewCommands.cs +++ b/src/Framework/Framework/Controls/GridViewCommands.cs @@ -1,9 +1,36 @@ +using System; +using System.Collections.Concurrent; using DotVVM.Framework.Binding.Expressions; namespace DotVVM.Framework.Controls { public class GridViewCommands { + + private readonly ConcurrentDictionary> isSortColumnAscending = new(); + private readonly ConcurrentDictionary> isSortColumnDescending = new(); + public ICommandBinding? SetSortExpression { get; init; } + + internal IValueBinding>? IsColumnSortedAscending { get; init; } + internal IValueBinding>? IsColumnSortedDescending { get; init; } + + public IValueBinding? GetIsColumnSortedAscendingBinding(string? sortExpression) + { + if (IsColumnSortedAscending == null) + { + return null; + } + return isSortColumnAscending.GetOrAdd(sortExpression, _ => IsColumnSortedAscending.Select(a => a(sortExpression))); + } + + public IValueBinding? GetIsColumnSortedDescendingBinding(string? sortExpression) + { + if (IsColumnSortedDescending == null) + { + return null; + } + return isSortColumnDescending.GetOrAdd(sortExpression, _ => IsColumnSortedDescending.Select(a => a(sortExpression))); + } } -} \ No newline at end of file +} diff --git a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs index 00d6376ded..5af5bc910c 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -137,6 +137,12 @@ private GridViewCommands GetGridViewCommandsCore(DataContextStack dataContextSta ? CreateCommandBinding(commandType, dataSetExpr, dataContextStack, methodName, arguments, transformExpression) : null; } + IValueBinding? GetValueBindingOrNull(DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression) + { + return typeof(T).IsAssignableFrom(dataSetExpr.Type) + ? CreateValueBinding(dataSetExpr, dataContextStack, methodName, arguments, transformExpression) + : null; + } var setSortExpressionParam = Expression.Parameter(typeof(string), "_sortExpression"); return new GridViewCommands() @@ -146,10 +152,46 @@ private GridViewCommands GetGridViewCommandsCore(DataContextStack dataContextSta nameof(ISortingSetSortExpressionCapability.SetSortExpression), new Expression[] { setSortExpressionParam }, // transform to sortExpression => command lambda - e => Expression.Lambda(e, setSortExpressionParam)) + e => Expression.Lambda(e, setSortExpressionParam)), + IsColumnSortedAscending = GetValueBindingOrNull, Func>( + dataContextStack, + nameof(ISortingStateCapability.IsColumnSortedAscending), + new Expression[] { setSortExpressionParam }, + // transform to sortExpression => command lambda + e => Expression.Lambda(e, setSortExpressionParam)), + IsColumnSortedDescending = GetValueBindingOrNull, Func>( + dataContextStack, + nameof(ISortingStateCapability.IsColumnSortedDescending), + new Expression[] { setSortExpressionParam }, + // transform to sortExpression => command lambda + e => Expression.Lambda(e, setSortExpressionParam)), }; } + private IValueBinding CreateValueBinding(Expression dataSet, DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression = null) + { + // get concrete type from implementation of IXXXableGridViewDataSet + var optionsConcreteType = GetOptionsConcreteType(dataSet.Type, out var optionsProperty); + + // call dataSet.XXXOptions.Method(...); + Expression expression = Expression.Call( + Expression.Convert(Expression.Property(dataSet, optionsProperty), optionsConcreteType), + optionsConcreteType.GetMethod(methodName)!, + arguments); + + if (transformExpression != null) + { + expression = transformExpression(expression); + } + + return new ValueBindingExpression(service, + new object[] + { + new ParsedExpressionBindingProperty(expression), + dataContextStack + }); + } + private ICommandBinding CreateCommandBinding(GridViewDataSetCommandType commandType, Expression dataSet, DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression = null) { if (commandType == GridViewDataSetCommandType.Default) diff --git a/src/Framework/Framework/Resources/Scripts/dataset/translations.ts b/src/Framework/Framework/Resources/Scripts/dataset/translations.ts index a4fa1b9609..39aa694f5b 100644 --- a/src/Framework/Framework/Resources/Scripts/dataset/translations.ts +++ b/src/Framework/Framework/Resources/Scripts/dataset/translations.ts @@ -18,6 +18,16 @@ type SortingOptions = { SortDescending: boolean }; +type MultiCriteriaSortingOptions = { + Criteria: SortCriterion[], + MaxSortCriteriaCount: number +}; + +type SortCriterion = { + SortExpression: string, + SortDescending: boolean +}; + export const translations = { PagingOptions: { goToFirstPage(options: PagingOptions) { @@ -86,6 +96,43 @@ export const translations = { options.SortExpression = sortExpression; options.SortDescending = false; } + }, + isColumnSortedAscending(options: SortingOptions, sortExpression: string) { + return options.SortExpression === sortExpression && !options.SortDescending; + }, + isColumnSortedDescending(options: SortingOptions, sortExpression: string) { + return options.SortExpression === sortExpression && options.SortDescending; + } + }, + MultiCriteriaSortingOptions: { + setSortExpression(options: MultiCriteriaSortingOptions, sortExpression: string) { + if (sortExpression == null) { + options.Criteria = []; + return; + } + + const index = options.Criteria.findIndex(c => c.SortExpression == sortExpression); + if (index === 0) { + options.Criteria[index].SortDescending = !options.Criteria[index].SortDescending; + } + else if (index > 0) { + options.Criteria.splice(index, 1); + options.Criteria.unshift({ SortExpression: sortExpression, SortDescending: false }); + } + else { + options.Criteria.unshift({ SortExpression: sortExpression, SortDescending: false }); + } + + if (options.Criteria.length > options.MaxSortCriteriaCount) + { + options.Criteria.splice(options.MaxSortCriteriaCount, options.Criteria.length - options.MaxSortCriteriaCount); + } + }, + isColumnSortedAscending(options: MultiCriteriaSortingOptions, sortExpression: string) { + return options.Criteria.some(c => c.SortExpression === sortExpression && !c.SortDescending); + }, + isColumnSortedDescending(options: MultiCriteriaSortingOptions, sortExpression: string) { + return options.Criteria.some(c => c.SortExpression === sortExpression && c.SortDescending); } } }; diff --git a/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs b/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs index 007045388d..12a4e43ea6 100644 --- a/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs +++ b/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs @@ -26,18 +26,18 @@ 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")} + new CustomerData {CustomerId = 1, Name = "John Doe", BirthDate = DateTime.Parse("1976-04-01"), MessageReceived = false}, + new CustomerData {CustomerId = 2, Name = "John Deer", BirthDate = DateTime.Parse("1984-03-02"), MessageReceived = false}, + new CustomerData {CustomerId = 3, Name = "Johnny Walker", BirthDate = DateTime.Parse("1934-01-03"), MessageReceived = false}, + new CustomerData {CustomerId = 4, Name = "Jim Hacker", BirthDate = DateTime.Parse("1912-11-04"), MessageReceived = false}, + new CustomerData {CustomerId = 5, Name = "Joe E. Brown", BirthDate = DateTime.Parse("1947-09-05"), MessageReceived = true}, + new CustomerData {CustomerId = 6, Name = "Jack Daniels", BirthDate = DateTime.Parse("1934-01-03"), MessageReceived = false}, + new CustomerData {CustomerId = 7, Name = "James Bond", BirthDate = DateTime.Parse("1965-05-07"), MessageReceived = true}, + new CustomerData {CustomerId = 8, Name = "John Smith", BirthDate = DateTime.Parse("1974-03-08"), MessageReceived = false}, + new CustomerData {CustomerId = 9, Name = "Jack & Jones", BirthDate = DateTime.Parse("1934-01-03"), MessageReceived = true}, + new CustomerData {CustomerId = 10, Name = "Jim Bill", BirthDate = DateTime.Parse("1934-01-03"), MessageReceived = true}, + new CustomerData {CustomerId = 11, Name = "James Joyce", BirthDate = DateTime.Parse("1982-11-28"), MessageReceived = true}, + new CustomerData {CustomerId = 12, Name = "Joudy Jane", BirthDate = DateTime.Parse("1934-01-03"), MessageReceived = true} }.AsQueryable(); } @@ -137,24 +137,36 @@ public class NextTokenHistoryGridViewDataSet : GenericGridViewDataSet ApplyToQueryable(IQueryable queryable) { - var token = PageIndex < TokenHistory.Count - 1 ? int.Parse(TokenHistory[PageIndex - 1] ?? "0") : 0; + if (TokenHistory.Count == 0) + { + TokenHistory.Add(null); + } + var token = PageIndex < TokenHistory.Count ? int.Parse(TokenHistory[PageIndex] ?? "0") : 0; return queryable.Cast() .OrderBy(c => c.CustomerId) .Where(c => c.CustomerId > token) - .Take(3) + .Take(PageSize + 1) .Cast(); } public void ProcessLoadedItems(IQueryable filteredQueryable, IList items) { - if (PageIndex == TokenHistory.Count) + var hasMoreItems = false; + while (items.Count > PageSize) + { + items.RemoveAt(items.Count - 1); + hasMoreItems = true; + } + + if (PageIndex == TokenHistory.Count - 1 && hasMoreItems) { var lastToken = items.Cast() - .OrderByDescending(c => c.CustomerId) - .FirstOrDefault()?.CustomerId; + .LastOrDefault()?.CustomerId; TokenHistory.Add((lastToken ?? 0).ToString()); } diff --git a/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml b/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml index 7e1c2fc9b4..4b26faa7dc 100644 --- a/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml +++ b/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml @@ -7,6 +7,10 @@ @@ -58,7 +62,9 @@ - + +
      • {{value: SortExpression}} {{value: SortDescending ? "DESC" : "ASC"}}
      • +
        From b156896e1bfcdc18b73d4ac4c38120b3db0c379f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 25 Feb 2024 18:19:41 +0100 Subject: [PATCH 28/60] Sample for consuming GitHub API --- .../Options/NextTokenHistoryPagingOptions.cs | 35 +++++++- .../Common/DotVVM.Samples.Common.csproj | 2 + .../DataSet/GitHubApiViewModel.cs | 86 +++++++++++++++++++ .../FeatureSamples/DataSet/GitHubApi.dothtml | 41 +++++++++ 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/DataSet/GitHubApiViewModel.cs create mode 100644 src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApi.dothtml diff --git a/src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs b/src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs index 18975f565d..3266226068 100644 --- a/src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs +++ b/src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs @@ -5,7 +5,7 @@ namespace DotVVM.Framework.Controls { // TODO: comments - public class NextTokenHistoryPagingOptions : IPagingFirstPageCapability, IPagingPreviousPageCapability, IPagingPageIndexCapability + public class NextTokenHistoryPagingOptions : IPagingFirstPageCapability, IPagingPreviousPageCapability, IPagingPageIndexCapability, IPagingNextPageCapability { public List TokenHistory { get; set; } = new(); @@ -28,6 +28,39 @@ public void GoToPage(int pageIndex) PageIndex = pageIndex; } + /// + /// Gets the token for loading the current page. The first page token is always null. + /// + public virtual string? GetCurrentPageToken() + { + if (TokenHistory.Count == 0) + { + TokenHistory.Add(null); + } + + return PageIndex < TokenHistory.Count ? TokenHistory[PageIndex] : throw new InvalidOperationException($"Cannot get token for page {PageIndex}, because the token history contains only {TokenHistory.Count} items."); + } + + /// + /// Saves the token for loading the next page to the token history. + /// + public virtual void SaveNextPageToken(string? nextPageToken) + { + if (TokenHistory.Count == 0) + { + TokenHistory.Add(null); + } + + if (IsLastPage && nextPageToken != null) + { + TokenHistory.Add(nextPageToken); + } + else if (PageIndex > TokenHistory.Count) + { + throw new InvalidOperationException($"Cannot save token for page {PageIndex}, because the token history contains only {TokenHistory.Count} items.."); + } + } + public IList NearPageIndexes => GetNearPageIndexes(); /// diff --git a/src/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj index 9d17a349d5..1a44ff70ef 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -109,6 +109,7 @@ + @@ -216,6 +217,7 @@ all runtime; build; native; contentfiles; analyzers + diff --git a/src/Samples/Common/ViewModels/FeatureSamples/DataSet/GitHubApiViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/DataSet/GitHubApiViewModel.cs new file mode 100644 index 0000000000..76a15807ab --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/DataSet/GitHubApiViewModel.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DotVVM.Framework.Controls; +using DotVVM.Framework.ViewModel; +using DotVVM.Framework.Hosting; +using DotVVM.Samples.BasicSamples; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.DataSet +{ + public class GitHubApiViewModel : DotvvmViewModelBase + { + public GenericGridViewDataSet Issues { get; set; } = new(new(), new(), new(), new(), new()); + public GenericGridViewDataSet Issues2 { get; set; } = new(new(), new(), new(), new(), new()); + + + public override async Task PreRender() + { + if (Issues.IsRefreshRequired) + { + var result = await GetGitHubIssues(Issues.PagingOptions.CurrentToken); + Issues.Items = result.items; + Issues.PagingOptions.NextPageToken = result.nextToken; + } + + if (Issues2.IsRefreshRequired) + { + var result = await GetGitHubIssues(Issues2.PagingOptions.GetCurrentPageToken()); + Issues2.Items = result.items; + Issues2.PagingOptions.SaveNextPageToken(result.nextToken); + } + + await base.PreRender(); + } + + private async Task<(IssueDto[] items, string nextToken)> GetGitHubIssues(string currentToken) + { + var client = GetGitHubClient(); + + var response = await client.GetAsync(currentToken ?? "https://api.github.com/repos/riganti/dotvvm/issues"); + response.EnsureSuccessStatusCode(); + var items = await response.Content.ReadFromJsonAsync(); + + return (items, ParseNextToken(response)); + } + + private static HttpClient GetGitHubClient() + { + var client = new HttpClient(); + var token = SampleConfiguration.Instance.AppSettings[DotvvmStartup.GitHubTokenConfigName]; + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); + client.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28"); + client.DefaultRequestHeaders.Add("User-Agent", "tomasherceg"); + return client; + } + + private string ParseNextToken(HttpResponseMessage response) + { + var linkHeader = response.Headers.GetValues("Link").FirstOrDefault(); + if (string.IsNullOrWhiteSpace(linkHeader)) + { + return null; + } + + var match = Regex.Match(linkHeader, @"<([^>]+)>; rel=""next"""); + return match.Success ? match.Groups[1].Value : null; + } + } + + public class IssueDto + { + public long Id { get; set; } + public long Number { get; set; } + public string Title { get; set; } + + public string State { get; set; } + } +} + diff --git a/src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApi.dothtml b/src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApi.dothtml new file mode 100644 index 0000000000..e2bc62828a --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApi.dothtml @@ -0,0 +1,41 @@ +@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.DataSet.GitHubApiViewModel, DotVVM.Samples.Common + + + + + + + + + + +

        GitHub Issue Browser

        + +
        +
        +

        NextToken

        + + + + + + + +
        + +
        +

        NextTokenHistory

        + + + + + + + +
        +
        + + + + + From 645d97ff33e93874358c60fc8de1f3813b91ff07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 25 Feb 2024 15:52:02 +0100 Subject: [PATCH 29/60] Add documentation comments to token pagers --- .../IPagingOptionsLoadingPostProcessor.cs | 1 + .../Options/NextTokenHistoryPagingOptions.cs | 18 +++++++++++++++--- .../Controls/Options/NextTokenPagingOptions.cs | 18 ++++++++++++++++-- .../Framework/Binding/BindingHelper.cs | 16 +++++++--------- .../Framework/Controls/GridViewColumn.cs | 2 +- .../Resources/Scripts/dataset/translations.ts | 4 +++- 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/Framework/Core/Controls/Options/IPagingOptionsLoadingPostProcessor.cs b/src/Framework/Core/Controls/Options/IPagingOptionsLoadingPostProcessor.cs index 3f4a923a62..6356489788 100644 --- a/src/Framework/Core/Controls/Options/IPagingOptionsLoadingPostProcessor.cs +++ b/src/Framework/Core/Controls/Options/IPagingOptionsLoadingPostProcessor.cs @@ -3,6 +3,7 @@ namespace DotVVM.Framework.Controls; +/// Provides an extension point to the method, which is invoked after the items are loaded from database. public interface IPagingOptionsLoadingPostProcessor { void ProcessLoadedItems(IQueryable filteredQueryable, IList items); diff --git a/src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs b/src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs index 3266226068..8e89ddd31c 100644 --- a/src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs +++ b/src/Framework/Core/Controls/Options/NextTokenHistoryPagingOptions.cs @@ -4,23 +4,35 @@ namespace DotVVM.Framework.Controls { - // TODO: comments + /// + /// Token-based paging options that can navigate to next and all previous pages. + /// public class NextTokenHistoryPagingOptions : IPagingFirstPageCapability, IPagingPreviousPageCapability, IPagingPageIndexCapability, IPagingNextPageCapability { + /// List of all known pages, including the current page. First page has a null token. public List TokenHistory { get; set; } = new(); + /// Zero-based index of the current page in the public int PageIndex { get; set; } = 0; + /// Gets if we are on the first page (null token, zero page index). public bool IsFirstPage => PageIndex == 0; + /// Navigates to the first page, resets the to zero. public void GoToFirstPage() => PageIndex = 0; + /// Gets whether the represents the last page. public bool IsLastPage => PageIndex == TokenHistory.Count - 1; - public void GoToNextPage() => PageIndex++; + /// Navigates to the next page, if possible + public void GoToNextPage() => + PageIndex = Math.Min(PageIndex + 1, TokenHistory.Count - 1); - public void GoToPreviousPage() => PageIndex--; + /// Navigates to the previous page, if possible + public void GoToPreviousPage() => + PageIndex = Math.Max(PageIndex - 1, 0); + /// Navigates to the page specified using a zero-based index. public void GoToPage(int pageIndex) { if (TokenHistory.Count <= pageIndex) diff --git a/src/Framework/Core/Controls/Options/NextTokenPagingOptions.cs b/src/Framework/Core/Controls/Options/NextTokenPagingOptions.cs index b07b5374c9..0a894d3fd4 100644 --- a/src/Framework/Core/Controls/Options/NextTokenPagingOptions.cs +++ b/src/Framework/Core/Controls/Options/NextTokenPagingOptions.cs @@ -1,23 +1,37 @@ namespace DotVVM.Framework.Controls { - // TODO: comments, mention that null in NextPageToken means there is no next page + /// + /// Simple token-based paging options that can navigate to the next page. + /// Paging back is not supported by this class, you can use + /// public class NextTokenPagingOptions : IPagingFirstPageCapability, IPagingNextPageCapability { + /// The paging token of the next page. If this property is null, then this is the last known page. public string? NextPageToken { get; set; } + /// The paging token of the currently displayed page. Equal to null, if this is the first page. public string? CurrentToken { get; set; } + /// Gets if we are on the first page — if the is null. public bool IsFirstPage => CurrentToken == null; - public void GoToFirstPage() => CurrentToken = null; + /// Navigates to the first page, resets both the current and next token to null + public void GoToFirstPage() + { + CurrentToken = null; + NextPageToken = null; + } + /// Gets if it is on the last known page — if the is null. public bool IsLastPage => NextPageToken == null; + /// Navigates to the next page, sets the to the and resets the next token. public void GoToNextPage() { if (NextPageToken != null) { CurrentToken = NextPageToken; + NextPageToken = null; } } } diff --git a/src/Framework/Framework/Binding/BindingHelper.cs b/src/Framework/Framework/Binding/BindingHelper.cs index f5629eb31a..cc81bfaa84 100644 --- a/src/Framework/Framework/Binding/BindingHelper.cs +++ b/src/Framework/Framework/Binding/BindingHelper.cs @@ -258,23 +258,21 @@ public static ParametrizedCode GetParametrizedCommandJavascript(this ICommandBin ///
        public static CodeParameterAssignment? GetParametrizedCommandArgs(DotvvmControl control, IEnumerable? argumentsCollection) { + if (argumentsCollection is null) return null; var builder = new ParametrizedCode.Builder(); var isFirst = true; builder.Add("["); - if (argumentsCollection is not null) + foreach (var arg in argumentsCollection) { - foreach (var arg in argumentsCollection) + if (!isFirst) { - if (!isFirst) - { - builder.Add(","); - } + builder.Add(","); + } - isFirst = false; + isFirst = false; - builder.Add(ValueOrBinding.FromBoxedValue(arg).GetParametrizedJsExpression(control)); - } + builder.Add(ValueOrBinding.FromBoxedValue(arg).GetParametrizedJsExpression(control)); } builder.Add("]"); diff --git a/src/Framework/Framework/Controls/GridViewColumn.cs b/src/Framework/Framework/Controls/GridViewColumn.cs index df5c815fde..5d7ddf1828 100644 --- a/src/Framework/Framework/Controls/GridViewColumn.cs +++ b/src/Framework/Framework/Controls/GridViewColumn.cs @@ -206,7 +206,7 @@ public virtual void CreateHeaderControls(IDotvvmRequestContext context, GridView } var sortExpression = GetSortExpression(); - + var linkButton = new LinkButton(); linkButton.SetValue(ButtonBase.TextProperty, GetValueRaw(HeaderTextProperty)); linkButton.ClickArguments = new object?[] { sortExpression }; diff --git a/src/Framework/Framework/Resources/Scripts/dataset/translations.ts b/src/Framework/Framework/Resources/Scripts/dataset/translations.ts index 39aa694f5b..0c6a07d654 100644 --- a/src/Framework/Framework/Resources/Scripts/dataset/translations.ts +++ b/src/Framework/Framework/Resources/Scripts/dataset/translations.ts @@ -55,10 +55,12 @@ export const translations = { NextTokenPagingOptions: { goToFirstPage(options: NextTokenPagingOptions) { options.CurrentToken = null; + options.NextPageToken = null; }, goToNextPage(options: NextTokenPagingOptions) { - if (options.NextPageToken) { + if (options.NextPageToken != null) { options.CurrentToken = options.NextPageToken; + options.NextPageToken = null; } } }, From fb14887550ee3f284efde9d762468b931ad59767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 25 Feb 2024 18:14:00 +0100 Subject: [PATCH 30/60] datasets: add DataSource original string to the generated commands IDs --- .../Framework/Controls/GridViewDataSetBindingProvider.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs index 5af5bc910c..fc1e770bb4 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -13,6 +13,7 @@ using DotVVM.Framework.Compilation.Javascript; using DotVVM.Framework.Compilation.Javascript.Ast; using DotVVM.Framework.Utils; +using FastExpressionCompiler; namespace DotVVM.Framework.Controls; @@ -245,7 +246,7 @@ private ICommandBinding CreateDefaultCommandBinding(Expressio new object[] { new ParsedExpressionBindingProperty(expression), - new OriginalStringBindingProperty($"DataPager: _dataSet.{methodName}({string.Join(", ", arguments.AsEnumerable())})"), // For ID generation + new OriginalStringBindingProperty($"DataPager({dataSet.Type.ToCode()}): {dataSet.ToCSharpString().TrimEnd(';')}.{methodName}({string.Join(", ", arguments.AsEnumerable())})"), // For ID generation dataContextStack }); } From a36b0bcdcdaa7e6c6f8bf04f82c59671b586cdcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 25 Feb 2024 18:54:44 +0100 Subject: [PATCH 31/60] Added sample for GitHub API using static commands --- .../Core/Controls/GenericGridViewDataSet.cs | 21 ++++++- .../Controls/GridViewDataSetExtensions.cs | 16 ++++++ .../Core/Controls/GridViewDataSetResult.cs | 34 ++++++++++++ .../Options/GridViewDataSetOptions.cs | 6 +- .../Controls/GridViewDataSetResult.cs | 26 --------- .../Common/DotVVM.Samples.Common.csproj | 1 + src/Samples/Common/Services/GitHubService.cs | 55 +++++++++++++++++++ .../GitHubApiStaticCommandsViewModel.cs | 52 ++++++++++++++++++ .../DataSet/GitHubApiViewModel.cs | 55 ++----------------- .../DataSet/GitHubApiStaticCommands.dothtml | 42 ++++++++++++++ 10 files changed, 228 insertions(+), 80 deletions(-) create mode 100644 src/Framework/Core/Controls/GridViewDataSetResult.cs delete mode 100644 src/Framework/Framework/Controls/GridViewDataSetResult.cs create mode 100644 src/Samples/Common/Services/GitHubService.cs create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/DataSet/GitHubApiStaticCommandsViewModel.cs create mode 100644 src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApiStaticCommands.dothtml diff --git a/src/Framework/Core/Controls/GenericGridViewDataSet.cs b/src/Framework/Core/Controls/GenericGridViewDataSet.cs index 3b405e6581..6d0118e72d 100644 --- a/src/Framework/Core/Controls/GenericGridViewDataSet.cs +++ b/src/Framework/Core/Controls/GenericGridViewDataSet.cs @@ -1,4 +1,5 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.Generic; using System.Linq; @@ -113,5 +114,23 @@ public void ApplyOptions(GridViewDataSetOptions GetOptions() + { + return new() + { + FilteringOptions = FilteringOptions, + SortingOptions = SortingOptions, + PagingOptions = PagingOptions + }; + } + + public void ApplyResult(GridViewDataSetResult result) + { + Items = result.Items.ToList(); + FilteringOptions = result.FilteringOptions; + SortingOptions = result.SortingOptions; + PagingOptions = result.PagingOptions; + } } } diff --git a/src/Framework/Core/Controls/GridViewDataSetExtensions.cs b/src/Framework/Core/Controls/GridViewDataSetExtensions.cs index f10f2812ad..18cd22cdc5 100644 --- a/src/Framework/Core/Controls/GridViewDataSetExtensions.cs +++ b/src/Framework/Core/Controls/GridViewDataSetExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading.Tasks; namespace DotVVM.Framework.Controls { @@ -64,5 +65,20 @@ public static void GoToPageAndRefresh(this IPageableGridViewDataSet( + this GenericGridViewDataSet dataSet, + Func, Task>> loadMethod) + where TFilteringOptions : IFilteringOptions + where TSortingOptions : ISortingOptions + where TPagingOptions : IPagingOptions + where TRowInsertOptions : IRowInsertOptions + where TRowEditOptions : IRowEditOptions + { + var options = dataSet.GetOptions(); + var result = await loadMethod(options); + dataSet.ApplyResult(result); + } + } } diff --git a/src/Framework/Core/Controls/GridViewDataSetResult.cs b/src/Framework/Core/Controls/GridViewDataSetResult.cs new file mode 100644 index 0000000000..0dac85f9f8 --- /dev/null +++ b/src/Framework/Core/Controls/GridViewDataSetResult.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace DotVVM.Framework.Controls +{ + public class GridViewDataSetResult + where TFilteringOptions : IFilteringOptions + where TSortingOptions : ISortingOptions + where TPagingOptions : IPagingOptions + { + public GridViewDataSetResult(IReadOnlyList items, TFilteringOptions? filteringOptions = default, TSortingOptions? sortingOptions = default, TPagingOptions? pagingOptions = default) + { + Items = items; + FilteringOptions = filteringOptions; + SortingOptions = sortingOptions; + PagingOptions = pagingOptions; + } + + public GridViewDataSetResult(IReadOnlyList items, GridViewDataSetOptions options) + { + Items = items; + FilteringOptions = options.FilteringOptions; + SortingOptions = options.SortingOptions; + PagingOptions = options.PagingOptions; + } + + public IReadOnlyList Items { get; } + + public TFilteringOptions FilteringOptions { get; } + + public TSortingOptions SortingOptions { get; } + + public TPagingOptions PagingOptions { get; } + } +} diff --git a/src/Framework/Core/Controls/Options/GridViewDataSetOptions.cs b/src/Framework/Core/Controls/Options/GridViewDataSetOptions.cs index 1f47c50572..dfd6824380 100644 --- a/src/Framework/Core/Controls/Options/GridViewDataSetOptions.cs +++ b/src/Framework/Core/Controls/Options/GridViewDataSetOptions.cs @@ -5,11 +5,11 @@ public class GridViewDataSetOptions diff --git a/src/Framework/Framework/Controls/GridViewDataSetResult.cs b/src/Framework/Framework/Controls/GridViewDataSetResult.cs deleted file mode 100644 index ab4ff1fcee..0000000000 --- a/src/Framework/Framework/Controls/GridViewDataSetResult.cs +++ /dev/null @@ -1,26 +0,0 @@ -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/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj index 1a44ff70ef..3f552bb05d 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -104,6 +104,7 @@ + diff --git a/src/Samples/Common/Services/GitHubService.cs b/src/Samples/Common/Services/GitHubService.cs new file mode 100644 index 0000000000..76ce82e6a5 --- /dev/null +++ b/src/Samples/Common/Services/GitHubService.cs @@ -0,0 +1,55 @@ +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DotVVM.Samples.BasicSamples; + +namespace DotVVM.Samples.Common.Services; + +public class GitHubService +{ + + public async Task<(IssueDto[] items, string nextToken)> GetGitHubIssues(string currentToken) + { + var client = GetGitHubClient(); + + var response = await client.GetAsync(currentToken ?? "https://api.github.com/repos/riganti/dotvvm/issues"); + response.EnsureSuccessStatusCode(); + var items = await response.Content.ReadFromJsonAsync(); + + return (items, ParseNextToken(response)); + } + + private static HttpClient GetGitHubClient() + { + var client = new HttpClient(); + var token = SampleConfiguration.Instance.AppSettings[DotvvmStartup.GitHubTokenConfigName]; + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); + client.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28"); + client.DefaultRequestHeaders.Add("User-Agent", "tomasherceg"); + return client; + } + + private string ParseNextToken(HttpResponseMessage response) + { + var linkHeader = response.Headers.GetValues("Link").FirstOrDefault(); + if (string.IsNullOrWhiteSpace(linkHeader)) + { + return null; + } + + var match = Regex.Match(linkHeader, @"<([^>]+)>; rel=""next"""); + return match.Success ? match.Groups[1].Value : null; + } +} + +public class IssueDto +{ + public long Id { get; set; } + public long Number { get; set; } + public string Title { get; set; } + public string State { get; set; } +} diff --git a/src/Samples/Common/ViewModels/FeatureSamples/DataSet/GitHubApiStaticCommandsViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/DataSet/GitHubApiStaticCommandsViewModel.cs new file mode 100644 index 0000000000..4b057f7301 --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/DataSet/GitHubApiStaticCommandsViewModel.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.Controls; +using DotVVM.Framework.ViewModel; +using DotVVM.Framework.Hosting; +using DotVVM.Samples.Common.Services; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.DataSet +{ + public class GitHubApiStaticCommandsViewModel : DotvvmViewModelBase + { + public GenericGridViewDataSet Issues { get; set; } = new(new(), new(), new(), new(), new()); + public GenericGridViewDataSet Issues2 { get; set; } = new(new(), new(), new(), new(), new()); + + public override async Task PreRender() + { + if (!Context.IsPostBack) + { + await Issues.LoadAsync(LoadIssues); + await Issues2.LoadAsync(LoadIssues2); + } + + await base.PreRender(); + } + + [AllowStaticCommand] + public static async Task> LoadIssues(GridViewDataSetOptions options) + { + var gitHubService = new GitHubService(); + + var result = await gitHubService.GetGitHubIssues(options.PagingOptions.CurrentToken); + options.PagingOptions.NextPageToken = result.nextToken; + + return new(result.items, options); + } + + [AllowStaticCommand] + public static async Task> LoadIssues2(GridViewDataSetOptions options) + { + var gitHubService = new GitHubService(); + + var result = await gitHubService.GetGitHubIssues(options.PagingOptions.GetCurrentPageToken()); + options.PagingOptions.SaveNextPageToken(result.nextToken); + + return new(result.items, options); + } + } +} + diff --git a/src/Samples/Common/ViewModels/FeatureSamples/DataSet/GitHubApiViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/DataSet/GitHubApiViewModel.cs index 76a15807ab..df83342f4a 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/DataSet/GitHubApiViewModel.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/DataSet/GitHubApiViewModel.cs @@ -1,16 +1,11 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Net.Http.Json; using System.Text; -using System.Text.RegularExpressions; using System.Threading.Tasks; using DotVVM.Framework.Controls; using DotVVM.Framework.ViewModel; using DotVVM.Framework.Hosting; -using DotVVM.Samples.BasicSamples; +using DotVVM.Samples.Common.Services; namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.DataSet { @@ -22,65 +17,25 @@ public class GitHubApiViewModel : DotvvmViewModelBase public override async Task PreRender() { + var gitHubService = new GitHubService(); + if (Issues.IsRefreshRequired) { - var result = await GetGitHubIssues(Issues.PagingOptions.CurrentToken); + var result = await gitHubService.GetGitHubIssues(Issues.PagingOptions.CurrentToken); Issues.Items = result.items; Issues.PagingOptions.NextPageToken = result.nextToken; } if (Issues2.IsRefreshRequired) { - var result = await GetGitHubIssues(Issues2.PagingOptions.GetCurrentPageToken()); + var result = await gitHubService.GetGitHubIssues(Issues2.PagingOptions.GetCurrentPageToken()); Issues2.Items = result.items; Issues2.PagingOptions.SaveNextPageToken(result.nextToken); } await base.PreRender(); } - - private async Task<(IssueDto[] items, string nextToken)> GetGitHubIssues(string currentToken) - { - var client = GetGitHubClient(); - - var response = await client.GetAsync(currentToken ?? "https://api.github.com/repos/riganti/dotvvm/issues"); - response.EnsureSuccessStatusCode(); - var items = await response.Content.ReadFromJsonAsync(); - - return (items, ParseNextToken(response)); - } - - private static HttpClient GetGitHubClient() - { - var client = new HttpClient(); - var token = SampleConfiguration.Instance.AppSettings[DotvvmStartup.GitHubTokenConfigName]; - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); - client.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28"); - client.DefaultRequestHeaders.Add("User-Agent", "tomasherceg"); - return client; - } - - private string ParseNextToken(HttpResponseMessage response) - { - var linkHeader = response.Headers.GetValues("Link").FirstOrDefault(); - if (string.IsNullOrWhiteSpace(linkHeader)) - { - return null; - } - - var match = Regex.Match(linkHeader, @"<([^>]+)>; rel=""next"""); - return match.Success ? match.Groups[1].Value : null; - } } - public class IssueDto - { - public long Id { get; set; } - public long Number { get; set; } - public string Title { get; set; } - - public string State { get; set; } - } } diff --git a/src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApiStaticCommands.dothtml b/src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApiStaticCommands.dothtml new file mode 100644 index 0000000000..1d1bde0b73 --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApiStaticCommands.dothtml @@ -0,0 +1,42 @@ +@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.DataSet.GitHubApiStaticCommandsViewModel, DotVVM.Samples.Common + + + + + + + + + + +

        GitHub Issue Browser

        + +
        +
        +

        NextToken

        + + + + + + + +
        + +
        +

        NextTokenHistory

        + + + + + + + +
        +
        + + + + From 0fb573c546365d23ac4ca1b74eb5ee2773a0f646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 25 Feb 2024 18:31:30 +0100 Subject: [PATCH 32/60] DataPager: disable links when not clickable (on last page or first page) --- src/Framework/Framework/Controls/DataPager.cs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index 1d85221e90..62fda9d242 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -166,22 +166,24 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) pagerBindings = gridViewDataSetBindingProvider.GetDataPagerCommands(this.GetDataContextType().NotNull(), dataSetBinding, commandType); - var enabled = GetValueOrBinding(EnabledProperty)!; + var globalEnabled = GetValueOrBinding(EnabledProperty)!; ContentWrapper = CreateWrapperList(); Children.Add(ContentWrapper); if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataSetType)) { - GoToFirstPageButton = CreateNavigationButton("««", FirstPageTemplate, enabled, pagerBindings.GoToFirstPage!, context); - GoToFirstPageButton.CssClasses.Add(DisabledItemCssClass, new ValueOrBinding(pagerBindings.IsFirstPage.NotNull())); + var disabled = new ValueOrBinding(pagerBindings.IsFirstPage.NotNull()); + GoToFirstPageButton = CreateNavigationButton("««", FirstPageTemplate, globalEnabled.And(disabled.Negate()), pagerBindings.GoToFirstPage!, context); + GoToFirstPageButton.CssClasses.Add(DisabledItemCssClass, disabled); ContentWrapper.Children.Add(GoToFirstPageButton); } if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataSetType)) { - GoToPreviousPageButton = CreateNavigationButton("«", PreviousPageTemplate, enabled, pagerBindings.GoToPreviousPage!, context); - GoToPreviousPageButton.CssClasses.Add(DisabledItemCssClass, new ValueOrBinding(pagerBindings.IsFirstPage.NotNull())); + var disabled = new ValueOrBinding(pagerBindings.IsFirstPage.NotNull()); + GoToPreviousPageButton = CreateNavigationButton("«", PreviousPageTemplate, globalEnabled.And(disabled.Negate()), pagerBindings.GoToPreviousPage!, context); + GoToPreviousPageButton.CssClasses.Add(DisabledItemCssClass, disabled); ContentWrapper.Children.Add(GoToPreviousPageButton); } @@ -195,7 +197,7 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) var link = new LinkButton(); link.SetBinding(ButtonBase.ClickProperty, pagerBindings.GoToPage.NotNull()); link.SetBinding(ButtonBase.TextProperty, pagerBindings.PageNumberText.NotNull()); - if (!true.Equals(enabled)) link.SetValue(LinkButton.EnabledProperty, enabled); + if (!true.Equals(globalEnabled)) link.SetValue(LinkButton.EnabledProperty, globalEnabled); liTemplate.Children.Add(link); if (!this.RenderLinkForCurrentPage) { @@ -216,15 +218,17 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataSetType)) { - GoToNextPageButton = CreateNavigationButton("»", NextPageTemplate, enabled, pagerBindings.GoToNextPage!, context); - GoToNextPageButton.CssClasses.Add(DisabledItemCssClass, new ValueOrBinding(pagerBindings.IsLastPage.NotNull())); + var disabled = new ValueOrBinding(pagerBindings.IsLastPage.NotNull()); + GoToNextPageButton = CreateNavigationButton("»", NextPageTemplate, globalEnabled.And(disabled.Negate()), pagerBindings.GoToNextPage!, context); + GoToNextPageButton.CssClasses.Add(DisabledItemCssClass, disabled); ContentWrapper.Children.Add(GoToNextPageButton); } if (typeof(IPageableGridViewDataSet).IsAssignableFrom(dataSetType)) { - GoToLastPageButton = CreateNavigationButton("»»", LastPageTemplate, enabled, pagerBindings.GoToLastPage!, context); - GoToLastPageButton.CssClasses.Add(DisabledItemCssClass, new ValueOrBinding(pagerBindings.IsLastPage.NotNull())); + var disabled = new ValueOrBinding(pagerBindings.IsLastPage.NotNull()); + GoToLastPageButton = CreateNavigationButton("»»", LastPageTemplate, globalEnabled.And(disabled.Negate()), pagerBindings.GoToLastPage!, context); + GoToLastPageButton.CssClasses.Add(DisabledItemCssClass, disabled); ContentWrapper.Children.Add(GoToLastPageButton); } } From c9bacd8cd894cd580fa6c0a2b9bd2c8d18679324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 1 Mar 2024 12:44:00 +0100 Subject: [PATCH 33/60] Revert unwanted change in JavascriptTranslatableMethodCollection --- .../JavascriptTranslatableMethodCollection.cs | 13 +++++++------ .../DataPagerTests.CommandDataPager.html | 10 +++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index 0fe0d6d82d..ca1ed74589 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -81,6 +81,13 @@ public void AddMethodTranslator(Expression> methodCall, IJavascriptMe AddMethodTranslator(method, translator); } + public void AddMethodTranslator(Expression methodCall, IJavascriptMethodTranslator translator) + { + var method = (MethodInfo)MethodFindingHelper.GetMethodFromExpression(methodCall); + CheckNotAccidentalDefinition(method); + AddMethodTranslator(method, translator); + } + private void CheckNotAccidentalDefinition(MethodBase m) { if (m.DeclaringType == typeof(object)) @@ -104,12 +111,6 @@ 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() diff --git a/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html index 0b6f4a1cde..a061797212 100644 --- a/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html +++ b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html @@ -3,15 +3,15 @@ From 3324bf69cda6735c282ad00ea3497144866cac5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 26 Apr 2024 22:53:08 +0200 Subject: [PATCH 34/60] datasets: Data context fixes in AppendableDataPager + some tests --- .../Framework/Binding/BindingHelper.cs | 38 ++++++++++--- .../Binding/DataContextChangeAttribute.cs | 5 ++ .../Binding/HelperNamespace/DataPagerApi.cs | 4 +- .../ControlTree/ControlTreeResolverBase.cs | 15 ++++-- .../JavascriptTranslatableMethodCollection.cs | 3 +- .../Framework/Controls/AppendableDataPager.cs | 5 +- src/Framework/Framework/Controls/DataPager.cs | 2 - .../Controls/KnockoutBindingGroup.cs | 4 ++ src/Tests/ControlTests/DataPagerTests.cs | 54 ++++++++++++++++++- ...agerTests.StaticCommandApendablePager.html | 25 +++++++++ .../DataPagerTests.StaticCommandPager.html | 44 +++++++++++++++ ...alizationTests.SerializeDefaultConfig.json | 1 + 12 files changed, 179 insertions(+), 21 deletions(-) create mode 100644 src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandApendablePager.html create mode 100644 src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandPager.html diff --git a/src/Framework/Framework/Binding/BindingHelper.cs b/src/Framework/Framework/Binding/BindingHelper.cs index cc81bfaa84..57b4403e3e 100644 --- a/src/Framework/Framework/Binding/BindingHelper.cs +++ b/src/Framework/Framework/Binding/BindingHelper.cs @@ -400,7 +400,13 @@ public static TControl SetDataContextTypeFromDataSource(this TControl 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()); @@ -423,7 +429,13 @@ 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); @@ -431,33 +443,43 @@ public static DataContextStack GetDataContextType(this DotvvmProperty property, return DataContextStack.Create(childType, dataContextType, extensionParameters: extensionParameters.ToArray()); } - public static (Type? type, List extensionParameters) ApplyDataContextChange(DataContextStack dataContext, DataContextChangeAttribute[] attributes, ResolvedControl control, DotvvmProperty? property) + public static (Type? type, List extensionParameters, bool addLayer) ApplyDataContextChange(DataContextStack dataContext, DataContextChangeAttribute[] attributes, ResolvedControl control, DotvvmProperty? property) { var type = ResolvedTypeDescriptor.Create(dataContext.DataContextType); var extensionParameters = new List(); + 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 extensionParameters) ApplyDataContextChange(DataContextStack dataContextType, DataContextChangeAttribute[] attributes, DotvvmBindableObject obj, DotvvmProperty property) + private static (Type? childType, List extensionParameters, bool addLayer) ApplyDataContextChange(DataContextStack dataContextType, DataContextChangeAttribute[] attributes, DotvvmBindableObject obj, DotvvmProperty property) { Type? type = dataContextType.DataContextType; var extensionParameters = new List(); + 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); } /// diff --git a/src/Framework/Framework/Binding/DataContextChangeAttribute.cs b/src/Framework/Framework/Binding/DataContextChangeAttribute.cs index 582074f67b..3c07521828 100644 --- a/src/Framework/Framework/Binding/DataContextChangeAttribute.cs +++ b/src/Framework/Framework/Binding/DataContextChangeAttribute.cs @@ -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 { @@ -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 GetExtensionParameters(ITypeDescriptor dataContext) => Enumerable.Empty(); diff --git a/src/Framework/Framework/Binding/HelperNamespace/DataPagerApi.cs b/src/Framework/Framework/Binding/HelperNamespace/DataPagerApi.cs index 4c538ffba1..34e56873e2 100644 --- a/src/Framework/Framework/Binding/HelperNamespace/DataPagerApi.cs +++ b/src/Framework/Framework/Binding/HelperNamespace/DataPagerApi.cs @@ -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; @@ -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)); } @@ -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 GetExtensionParameters(ITypeDescriptor dataContext) { return new BindingExtensionParameter[] { diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs index ae1474660f..59a8a02cf2 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs @@ -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) @@ -759,17 +760,23 @@ protected virtual bool IsControlProperty(IPropertyDescriptor property) } } - public static (ITypeDescriptor? type, List extensionParameters) ApplyContextChange(IDataContextStack dataContext, DataContextChangeAttribute[] attributes, IAbstractControl control, IPropertyDescriptor? property) + public static (ITypeDescriptor? type, List extensionParameters, bool addLayer) ApplyContextChange(IDataContextStack dataContext, DataContextChangeAttribute[] attributes, IAbstractControl control, IPropertyDescriptor? property) { var type = dataContext.DataContextType; var extensionParameters = new List(); + 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); } diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index ca1ed74589..dcd9148b4e 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -837,7 +837,6 @@ JsExpression wrapInRound(JsExpression a) => 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), @@ -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 => diff --git a/src/Framework/Framework/Controls/AppendableDataPager.cs b/src/Framework/Framework/Controls/AppendableDataPager.cs index f246d4f6ef..9cc8ba1459 100644 --- a/src/Framework/Framework/Controls/AppendableDataPager.cs +++ b/src/Framework/Framework/Controls/AppendableDataPager.cs @@ -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) diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index 62fda9d242..d4efa37e87 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -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(pagerBindings.IsActivePage.NotNull())); var link = new LinkButton(); link.SetBinding(ButtonBase.ClickProperty, pagerBindings.GoToPage.NotNull()); diff --git a/src/Framework/Framework/Controls/KnockoutBindingGroup.cs b/src/Framework/Framework/Controls/KnockoutBindingGroup.cs index be71e3db6e..644cd3ee72 100644 --- a/src/Framework/Framework/Controls/KnockoutBindingGroup.cs +++ b/src/Framework/Framework/Controls/KnockoutBindingGroup.cs @@ -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; diff --git a/src/Tests/ControlTests/DataPagerTests.cs b/src/Tests/ControlTests/DataPagerTests.cs index b353b5f14d..eded24ff8b 100644 --- a/src/Tests/ControlTests/DataPagerTests.cs +++ b/src/Tests/ControlTests/DataPagerTests.cs @@ -16,6 +16,7 @@ using DotVVM.Framework.Binding; using FastExpressionCompiler; using DotVVM.Framework.Binding.Expressions; +using System.Reflection.Metadata; namespace DotVVM.Framework.Tests.ControlTests { @@ -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), """ - " + """ ); var commandExpressions = r.Commands @@ -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), """ + + """ + ); + check.CheckString(r.FormattedHtml, fileExtension: "html"); + } + + [TestMethod] + public async Task StaticCommandApendablePager() + { + var r = await cth.RunPage(typeof(GridViewModel), """ + + +
        + +
        +
        + end +
        + """ + ); + + check.CheckString(r.FormattedHtml, fileExtension: "html"); + + var commandExpressions = r.Commands + .Select(c => (c.control, c.command, str: c.command.GetProperty().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 Customers { get; set; } = new GridViewDataSet() @@ -77,6 +121,12 @@ public class CustomerData [Required] public string Name { get; set; } } + + [AllowStaticCommand] + public static GridViewDataSetResult LoadCustomers(GridViewDataSetOptions request) + { + throw new NotImplementedException(); + } } } } diff --git a/src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandApendablePager.html b/src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandApendablePager.html new file mode 100644 index 0000000000..0dec861046 --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandApendablePager.html @@ -0,0 +1,25 @@ + + + +
        + + +
        + +
        + +
        + end +
        +
        + + diff --git a/src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandPager.html b/src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandPager.html new file mode 100644 index 0000000000..cc1446269e --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandPager.html @@ -0,0 +1,44 @@ + + + +
          +
        • + «« +
        • +
        • + « +
        • + +
        • + + + + + + +
        • + +
        • + » +
        • +
        • + »» +
        • +
        + + diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index ae2804203e..cd07e69fb8 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -384,6 +384,7 @@ { "$type": "DotVVM.Framework.Binding.HelperNamespace.DataPagerApi+AddParameterDataContextChangeAttribute, DotVVM.Framework", "Name": "_dataPager", + "NestDataContext": false, "TypeId": "DotVVM.Framework.Binding.HelperNamespace.DataPagerApi+AddParameterDataContextChangeAttribute, DotVVM.Framework" } ], From 766cbb19fe864ea667b7da41acc1a7ee824cc8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 27 Apr 2024 14:28:32 +0200 Subject: [PATCH 35/60] Added tests to GridViewStaticCommand page --- .../GridView/GridViewStaticCommand.dothtml | 27 ++- .../Tests/Tests/Control/GridViewTests.cs | 229 +++++++++++++++++- 2 files changed, 235 insertions(+), 21 deletions(-) diff --git a/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml b/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml index 4b26faa7dc..95da72b52b 100644 --- a/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml +++ b/src/Samples/Common/Views/ControlSamples/GridView/GridViewStaticCommand.dothtml @@ -17,7 +17,8 @@

        Standard data set

        + LoadData="{staticCommand: RootViewModel.LoadStandard}" + data-ui="standard-grid"> @@ -25,11 +26,13 @@ - +

        NextToken paging options

        + LoadData="{staticCommand: RootViewModel.LoadToken}" + data-ui="next-grid"> @@ -37,11 +40,13 @@ - +

        NextTokenHistory data set

        + LoadData="{staticCommand: RootViewModel.LoadTokenHistory}" + data-ui="next-history-grid"> @@ -49,11 +54,13 @@ - +

        MultiSort data set

        + LoadData="{staticCommand: RootViewModel.LoadMultiSort}" + data-ui="multi-sort-grid"> @@ -61,8 +68,10 @@ - - + +
      • {{value: SortExpression}} {{value: SortDescending ? "DESC" : "ASC"}}
      • diff --git a/src/Samples/Tests/Tests/Control/GridViewTests.cs b/src/Samples/Tests/Tests/Control/GridViewTests.cs index d7a367d2b7..9f694f2285 100644 --- a/src/Samples/Tests/Tests/Control/GridViewTests.cs +++ b/src/Samples/Tests/Tests/Control/GridViewTests.cs @@ -1,6 +1,8 @@ using System.Linq; +using System.Text.RegularExpressions; using DotVVM.Samples.Tests.Base; using DotVVM.Testing.Abstractions; +using OpenQA.Selenium; using Riganti.Selenium.Core; using Riganti.Selenium.Core.Abstractions; using Riganti.Selenium.DotVVM; @@ -72,22 +74,225 @@ public void Control_GridView_GridViewInlineEditingValidation() } [Fact] - public void Control_GridView_GridViewStaticCommand() + public void Control_GridView_GridViewStaticCommand_Standard() { RunInAllBrowsers(browser => { browser.NavigateToUrl(SamplesRouteUrls.ControlSamples_GridView_GridViewStaticCommand); - //check rows - browser.FindElements("table tbody tr").ThrowIfDifferentCountThan(5); - //check first row Id - AssertUI.InnerTextEquals(browser.First("table tbody tr td span"), "1"); - //cal static command for delete row - browser.First("table tbody tr input[type=button]").Click(); - browser.WaitForPostback(); - //check rows again - browser.FindElements("table tbody tr").ThrowIfDifferentCountThan(4); - //check first row Id - AssertUI.InnerTextEquals(browser.First("table tbody tr td span"), "2"); + // Standard + var grid = browser.Single("standard-grid", SelectByDataUi); + var pager = browser.Single("standard-pager", SelectByDataUi); + + // verify initial data + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(10); + AssertUI.TextEquals(grid.First("tbody tr td"), "1"); + + // sort by name + grid.ElementAt("thead th", 1).Single("a").Click(); + AssertUI.TextEquals(grid.First("tbody tr td"), "9"); + + // sort by birth date + grid.ElementAt("thead th", 2).Single("a").Click(); + AssertUI.TextEquals(grid.First("tbody tr td"), "4"); + + // click on page 2 + pager.ElementAt("li", 3).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(2); + AssertUI.TextEquals(grid.First("tbody tr td"), "11"); + + // click on page 1 + pager.ElementAt("li", 2).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(10); + AssertUI.TextEquals(grid.First("tbody tr td"), "4"); + + // click on next button + pager.ElementAt("li", 4).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(2); + AssertUI.TextEquals(grid.First("tbody tr td"), "11"); + + // click on previous page + pager.ElementAt("li", 1).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(10); + AssertUI.TextEquals(grid.First("tbody tr td"), "4"); + + // click on last button + pager.ElementAt("li", 5).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(2); + AssertUI.TextEquals(grid.First("tbody tr td"), "11"); + + // click on first page + pager.ElementAt("li", 0).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(10); + AssertUI.TextEquals(grid.First("tbody tr td"), "4"); + }); + } + + [Fact] + public void Control_GridView_GridViewStaticCommand_Next() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.ControlSamples_GridView_GridViewStaticCommand); + + // NextToken + var grid = browser.Single("next-grid", SelectByDataUi); + var pager = browser.Single("next-pager", SelectByDataUi); + + // verify initial data + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(3); + AssertUI.TextEquals(grid.First("tbody tr td"), "1"); + AssertUI.HasAttribute(pager.ElementAt("li", 0).Single("a"), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 1).Single("a"), "disabled"); + + // click on next button + pager.ElementAt("li", 1).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(3); + AssertUI.TextEquals(grid.First("tbody tr td"), "4"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 0).Single("a"), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 1).Single("a"), "disabled"); + + // click on first page + pager.ElementAt("li", 0).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(3); + AssertUI.TextEquals(grid.First("tbody tr td"), "1"); + AssertUI.HasAttribute(pager.ElementAt("li", 0).Single("a"), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 1).Single("a"), "disabled"); + + // click on next button + pager.ElementAt("li", 1).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(3); + AssertUI.TextEquals(grid.First("tbody tr td"), "4"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 0), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 1), "disabled"); + + // click on next button + pager.ElementAt("li", 1).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(3); + AssertUI.TextEquals(grid.First("tbody tr td"), "7"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 0).Single("a"), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 1).Single("a"), "disabled"); + + // click on next button + pager.ElementAt("li", 1).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(3); + AssertUI.TextEquals(grid.First("tbody tr td"), "10"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 0).Single("a"), "disabled"); + AssertUI.HasAttribute(pager.ElementAt("li", 1).Single("a"), "disabled"); + }); + } + + [Fact] + public void Control_GridView_GridViewStaticCommand_NextHistory() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.ControlSamples_GridView_GridViewStaticCommand); + + // NextTokenHistory + var grid = browser.Single("next-history-grid", SelectByDataUi); + var pager = browser.Single("next-history-pager", SelectByDataUi); + + // verify initial data + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(3); + pager.FindElements("li").ThrowIfDifferentCountThan(5); + AssertUI.TextEquals(grid.First("tbody tr td"), "1"); + AssertUI.HasAttribute(pager.ElementAt("li", 0).Single("a"), "disabled"); + AssertUI.HasAttribute(pager.ElementAt("li", 1).Single("a"), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 4).Single("a"), "disabled"); + + // click on next button + pager.ElementAt("li", 4).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(3); + pager.FindElements("li").ThrowIfDifferentCountThan(6); + AssertUI.TextEquals(grid.First("tbody tr td"), "4"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 0).Single("a"), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 1).Single("a"), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 5).Single("a"), "disabled"); + + // click on first page + pager.ElementAt("li", 0).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(3); + pager.FindElements("li").ThrowIfDifferentCountThan(6); + AssertUI.TextEquals(grid.First("tbody tr td"), "1"); + AssertUI.HasAttribute(pager.ElementAt("li", 0).Single("a"), "disabled"); + AssertUI.HasAttribute(pager.ElementAt("li", 1).Single("a"), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 5).Single("a"), "disabled"); + + // click on page 2 + pager.ElementAt("li", 3).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(3); + pager.FindElements("li").ThrowIfDifferentCountThan(6); + AssertUI.TextEquals(grid.First("tbody tr td"), "4"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 0).Single("a"), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 1).Single("a"), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 5).Single("a"), "disabled"); + + // click on page 3 + pager.ElementAt("li", 4).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(3); + pager.FindElements("li").ThrowIfDifferentCountThan(7); + AssertUI.TextEquals(grid.First("tbody tr td"), "7"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 0).Single("a"), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 1).Single("a"), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 6).Single("a"), "disabled"); + + // click on previous button + pager.ElementAt("li", 1).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(3); + pager.FindElements("li").ThrowIfDifferentCountThan(7); + AssertUI.TextEquals(grid.First("tbody tr td"), "4"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 0).Single("a"), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 1).Single("a"), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 6).Single("a"), "disabled"); + + // click on page 4 + pager.ElementAt("li", 5).Single("a").Click(); + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(3); + pager.FindElements("li").ThrowIfDifferentCountThan(7); + AssertUI.TextEquals(grid.First("tbody tr td"), "10"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 0).Single("a"), "disabled"); + AssertUI.HasNotAttribute(pager.ElementAt("li", 1).Single("a"), "disabled"); + AssertUI.HasAttribute(pager.ElementAt("li", 6).Single("a"), "disabled"); + }); + } + + [Fact] + public void Control_GridView_GridViewStaticCommand_MultiSort() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.ControlSamples_GridView_GridViewStaticCommand); + + // MultiSort + var grid = browser.Single("multi-sort-grid", SelectByDataUi); + var criteria = browser.Single("multi-sort-criteria", SelectByDataUi); + + // verify initial data + grid.FindElements("tbody tr").ThrowIfDifferentCountThan(12); + AssertUI.TextEquals(grid.First("tbody tr td"), "1"); + AssertUI.TextEquals(criteria, ""); + + // sort by name + grid.ElementAt("thead th", 1).Single("a").Click(); + AssertUI.TextEquals(grid.First("tbody tr td"), "9"); + AssertUI.TextEquals(criteria, "Name ASC"); + + // add sort by message received + grid.ElementAt("thead th", 3).Single("a").Click(); + AssertUI.TextEquals(grid.First("tbody tr td"), "6"); + AssertUI.Text(criteria, t => Regex.Replace(t, "\\s+", " ") == "MessageReceived ASC Name ASC"); + + // reverse sort by message received + grid.ElementAt("thead th", 3).Single("a").Click(); + AssertUI.TextEquals(grid.First("tbody tr td"), "9"); + AssertUI.Text(criteria, t => Regex.Replace(t, "\\s+", " ") == "MessageReceived DESC Name ASC"); + + // add sort by birth date + grid.ElementAt("thead th", 2).Single("a").Click(); + AssertUI.TextEquals(grid.First("tbody tr td"), "4"); + AssertUI.Text(criteria, t => Regex.Replace(t, "\\s+", " ") == "BirthDate ASC MessageReceived DESC Name ASC"); + + // add sort by customer id + grid.ElementAt("thead th", 0).Single("a").Click(); + AssertUI.TextEquals(grid.First("tbody tr td"), "1"); + AssertUI.Text(criteria, t => Regex.Replace(t, "\\s+", " ") == "CustomerId ASC BirthDate ASC MessageReceived DESC"); }); } From c50bb49da34b6ba13b214bfbab5654388bd37424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 27 Apr 2024 14:37:09 +0200 Subject: [PATCH 36/60] datasets: return DataPager extensibility point, add ISingleRowInsertOptions --- .../Core/Controls/Options/IRowInsertOptions.cs | 10 ++++++++++ .../Core/Controls/Options/RowInsertOptions.cs | 2 +- src/Framework/Framework/Binding/BindingHelper.cs | 3 +++ src/Framework/Framework/Controls/DataPager.cs | 9 +++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Framework/Core/Controls/Options/IRowInsertOptions.cs b/src/Framework/Core/Controls/Options/IRowInsertOptions.cs index 6a9dae4791..15159aa48e 100644 --- a/src/Framework/Core/Controls/Options/IRowInsertOptions.cs +++ b/src/Framework/Core/Controls/Options/IRowInsertOptions.cs @@ -6,4 +6,14 @@ public interface IRowInsertOptions { } + + + /// + /// which may contain one inserted row. + /// + public interface ISingleRowInsertOptions: IRowInsertOptions + where T: class + { + T? InsertedRow { get; } + } } diff --git a/src/Framework/Core/Controls/Options/RowInsertOptions.cs b/src/Framework/Core/Controls/Options/RowInsertOptions.cs index d2f771af7e..f245f06db1 100644 --- a/src/Framework/Core/Controls/Options/RowInsertOptions.cs +++ b/src/Framework/Core/Controls/Options/RowInsertOptions.cs @@ -4,7 +4,7 @@ /// Represents settings for row (item) insert feature. ///
        /// The type of inserted row. - public class RowInsertOptions : IRowInsertOptions + public class RowInsertOptions : IRowInsertOptions, ISingleRowInsertOptions where T : class, new() { /// diff --git a/src/Framework/Framework/Binding/BindingHelper.cs b/src/Framework/Framework/Binding/BindingHelper.cs index 57b4403e3e..f319609f2b 100644 --- a/src/Framework/Framework/Binding/BindingHelper.cs +++ b/src/Framework/Framework/Binding/BindingHelper.cs @@ -20,6 +20,7 @@ using DotVVM.Framework.Runtime; using FastExpressionCompiler; using System.Diagnostics; +using System.Reflection; namespace DotVVM.Framework.Binding { @@ -242,6 +243,8 @@ public static T GetCommandDelegate(this ICommandBinding binding, DotvvmBin if (action is Func command2) return command2(); var parameters = action.GetType().GetMethod("Invoke")!.GetParameters(); + if (parameters.Length != args.Length) + throw new TargetParameterCountException($"Parameter count mismatch: received {args.Length}, but expected {parameters.Length} ({parameters.Select(p => p.ParameterType.ToCode()).StringJoin(", ")})"); var evaluatedArgs = args.Zip(parameters, (a, p) => a(p.ParameterType)).ToArray(); return action.DynamicInvoke(evaluatedArgs); } diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index d4efa37e87..40d4f06571 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -176,6 +176,7 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) var disabled = new ValueOrBinding(pagerBindings.IsFirstPage.NotNull()); GoToFirstPageButton = CreateNavigationButton("««", FirstPageTemplate, globalEnabled.And(disabled.Negate()), pagerBindings.GoToFirstPage!, context); GoToFirstPageButton.CssClasses.Add(DisabledItemCssClass, disabled); + AddItemCssClass(GoToFirstPageButton, context); ContentWrapper.Children.Add(GoToFirstPageButton); } @@ -184,6 +185,7 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) var disabled = new ValueOrBinding(pagerBindings.IsFirstPage.NotNull()); GoToPreviousPageButton = CreateNavigationButton("«", PreviousPageTemplate, globalEnabled.And(disabled.Negate()), pagerBindings.GoToPreviousPage!, context); GoToPreviousPageButton.CssClasses.Add(DisabledItemCssClass, disabled); + AddItemCssClass(GoToPreviousPageButton, context); ContentWrapper.Children.Add(GoToPreviousPageButton); } @@ -205,6 +207,7 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) link.SetBinding(DotvvmControl.IncludeInPageProperty, pagerBindings.IsActivePage.Negate()); liTemplate.Children.Add(notLink); } + AddItemCssClass(liTemplate, context); NumberButtonsRepeater = new Repeater() { DataSource = pagerBindings.PageNumbers, RenderWrapperTag = false, @@ -219,6 +222,7 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) var disabled = new ValueOrBinding(pagerBindings.IsLastPage.NotNull()); GoToNextPageButton = CreateNavigationButton("»", NextPageTemplate, globalEnabled.And(disabled.Negate()), pagerBindings.GoToNextPage!, context); GoToNextPageButton.CssClasses.Add(DisabledItemCssClass, disabled); + AddItemCssClass(GoToNextPageButton, context); ContentWrapper.Children.Add(GoToNextPageButton); } @@ -227,6 +231,7 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) var disabled = new ValueOrBinding(pagerBindings.IsLastPage.NotNull()); GoToLastPageButton = CreateNavigationButton("»»", LastPageTemplate, globalEnabled.And(disabled.Negate()), pagerBindings.GoToLastPage!, context); GoToLastPageButton.CssClasses.Add(DisabledItemCssClass, disabled); + AddItemCssClass(GoToLastPageButton, context); ContentWrapper.Children.Add(GoToLastPageButton); } } @@ -270,6 +275,10 @@ protected virtual void SetButtonContent(Hosting.IDotvvmRequestContext context, L } } + protected virtual void AddItemCssClass(HtmlGenericControl item, IDotvvmRequestContext context) + { + } + protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context) { if (RenderOnServer) From 7ab28113b05252d658741f7fb6c21101cf241b69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 27 Apr 2024 16:04:22 +0200 Subject: [PATCH 37/60] AppendableDataPager fixed --- .../Binding/HelperNamespace/DataPagerApi.cs | 15 +++++ .../JavascriptTranslatableMethodCollection.cs | 5 +- .../Framework/Controls/AppendableDataPager.cs | 60 +++++++++++------- .../binding-handlers/appendable-data-pager.ts | 63 ++++++++++++------- .../Common/DotVVM.Samples.Common.csproj | 1 + .../AppendableDataPagerViewModel.cs | 1 + .../AppendableDataPager.dothtml | 3 + .../AppendableDataPagerAutoLoad.dothtml | 46 ++++++++++++++ 8 files changed, 146 insertions(+), 48 deletions(-) create mode 100644 src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPagerAutoLoad.dothtml diff --git a/src/Framework/Framework/Binding/HelperNamespace/DataPagerApi.cs b/src/Framework/Framework/Binding/HelperNamespace/DataPagerApi.cs index 34e56873e2..73bab36afb 100644 --- a/src/Framework/Framework/Binding/HelperNamespace/DataPagerApi.cs +++ b/src/Framework/Framework/Binding/HelperNamespace/DataPagerApi.cs @@ -4,6 +4,7 @@ using System.Linq.Expressions; using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Compilation.ControlTree.Resolved; +using DotVVM.Framework.Compilation.Javascript; using DotVVM.Framework.Compilation.Javascript.Ast; using DotVVM.Framework.Controls; @@ -13,7 +14,9 @@ public class DataPagerApi { public void Load() => throw new NotSupportedException("The _dataPager.Load method is not supported on the server, please use a staticCommand to invoke it."); + public bool IsLoading => false; + public bool CanLoadNextPage => true; public class DataPagerExtensionParameter : BindingExtensionParameter @@ -22,6 +25,18 @@ public DataPagerExtensionParameter(string identifier, bool inherit = true) : bas { } + internal static void Register(JavascriptTranslatableMethodCollection collection) + { + collection.AddMethodTranslator(() => default(DataPagerApi)!.Load(), new GenericMethodCompiler(args => + args[0].Member("$appendableDataPager").Member("loadNextPage").Invoke().WithAnnotation(new ResultIsPromiseAnnotation(e => e)))); + + collection.AddPropertyGetterTranslator(typeof(DataPagerApi), nameof(IsLoading), new GenericMethodCompiler(args => + args[0].Member("$appendableDataPager").Member("isLoading").WithAnnotation(ResultIsObservableAnnotation.Instance))); + + collection.AddPropertyGetterTranslator(typeof(DataPagerApi), nameof(CanLoadNextPage), new GenericMethodCompiler(args => + args[0].Member("$appendableDataPager").Member("canLoadNextPage").WithAnnotation(ResultIsObservableAnnotation.Instance))); + } + public override JsExpression GetJsTranslation(JsExpression dataContext) => dataContext; public override Expression GetServerEquivalent(Expression controlParameter) => diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index dcd9148b4e..575840b0c8 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -846,9 +846,8 @@ private void AddDataSetOptionsTranslations() ).WithAnnotation(new ResultIsPromiseAnnotation(e => e)))); // _dataPager.Load() - AddMethodTranslator(() => default(DataPagerApi)!.Load(), new GenericMethodCompiler(args => - args[0].Member("$gridViewDataSetHelper").Member("loadNextPage").Invoke().WithAnnotation(new ResultIsPromiseAnnotation(e => e)))); - + DataPagerApi.DataPagerExtensionParameter.Register(this); + // PagingOptions AddMethodTranslator(() => default(PagingOptions)!.GoToFirstPage(),new GenericMethodCompiler(args => new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("PagingOptions").Member("goToFirstPage") diff --git a/src/Framework/Framework/Controls/AppendableDataPager.cs b/src/Framework/Framework/Controls/AppendableDataPager.cs index 9cc8ba1459..e9dc0645c9 100644 --- a/src/Framework/Framework/Controls/AppendableDataPager.cs +++ b/src/Framework/Framework/Controls/AppendableDataPager.cs @@ -17,6 +17,7 @@ namespace DotVVM.Framework.Controls public class AppendableDataPager : HtmlGenericControl { private readonly GridViewDataSetBindingProvider gridViewDataSetBindingProvider; + private readonly BindingCompilationService bindingService; [MarkupOptions(AllowBinding = false, MappingMode = MappingMode.InnerElement)] [DataPagerApi.AddParameterDataContextChange("_dataPager")] @@ -28,6 +29,16 @@ public ITemplate? LoadTemplate public static readonly DotvvmProperty LoadTemplateProperty = DotvvmProperty.Register(c => c.LoadTemplate, null); + [MarkupOptions(AllowBinding = false, MappingMode = MappingMode.InnerElement)] + [DataPagerApi.AddParameterDataContextChange("_dataPager")] + public ITemplate? LoadingTemplate + { + get { return (ITemplate?)GetValue(LoadingTemplateProperty); } + set { SetValue(LoadingTemplateProperty, value); } + } + public static readonly DotvvmProperty LoadingTemplateProperty + = DotvvmProperty.Register(c => c.LoadingTemplate, null); + [MarkupOptions(AllowBinding = false, MappingMode = MappingMode.InnerElement)] public ITemplate? EndTemplate { @@ -59,9 +70,10 @@ public ICommandBinding? LoadData private DataPagerBindings? dataPagerCommands = null; - public AppendableDataPager(GridViewDataSetBindingProvider gridViewDataSetBindingProvider) : base("div") + public AppendableDataPager(GridViewDataSetBindingProvider gridViewDataSetBindingProvider, BindingCompilationService bindingService) : base("div") { this.gridViewDataSetBindingProvider = gridViewDataSetBindingProvider; + this.bindingService = bindingService; } protected internal override void OnLoad(IDotvvmRequestContext context) @@ -72,16 +84,30 @@ protected internal override void OnLoad(IDotvvmRequestContext context) if (LoadTemplate != null) { - var container = new PlaceHolder(); - container.SetDataContextType(LoadTemplateProperty.GetDataContextType(this)); + var templateDataContext = LoadTemplateProperty.GetDataContextType(this)!; + var container = new PlaceHolder() + .SetProperty(p => p.IncludeInPage, bindingService.Cache.CreateValueBinding("_dataPager.CanLoadNextPage", templateDataContext)); + container.SetDataContextType(templateDataContext); + Children.Add(container); + LoadTemplate.BuildContent(context, container); + } + + if (LoadingTemplate != null) + { + var templateDataContext = LoadingTemplateProperty.GetDataContextType(this)!; + var container = new PlaceHolder() + .SetProperty(p => p.IncludeInPage, bindingService.Cache.CreateValueBinding("_dataPager.IsLoading", templateDataContext)); + container.SetDataContextType(templateDataContext); Children.Add(container); + + LoadingTemplate.BuildContent(context, container); } if (EndTemplate != null) { - var container = new HtmlGenericControl("div") - .SetProperty(p => p.Visible, dataPagerCommands.IsLastPage); + var container = new PlaceHolder() + .SetProperty(p => p.IncludeInPage, dataPagerCommands.IsLastPage); Children.Add(container); EndTemplate.BuildContent(context, container); @@ -101,24 +127,12 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest 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 - { - 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()); - } - + var binding = new KnockoutBindingGroup(); + binding.Add("dataSet", dataSetBinding); + binding.Add("loadNextPage", loadNextPage); + binding.Add("autoLoadWhenInViewport", LoadTemplate is null ? "true" : "false"); + writer.AddKnockoutDataBind("dotvvm-appendable-data-pager", binding); + base.AddAttributesToRender(writer, context); } 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 index 9b409ae41a..9a76cfd05f 100644 --- a/src/Framework/Framework/Resources/Scripts/binding-handlers/appendable-data-pager.ts +++ b/src/Framework/Framework/Resources/Scripts/binding-handlers/appendable-data-pager.ts @@ -8,39 +8,58 @@ type AppendableDataPagerBinding = { export default { 'dotvvm-appendable-data-pager': { - init: (element: HTMLInputElement, valueAccessor: () => AppendableDataPagerBinding, allBindingsAccessor: KnockoutAllBindingsAccessor) => { + init: (element: HTMLInputElement, valueAccessor: () => AppendableDataPagerBinding, allBindings?: any, viewModel?: any, bindingContext?: KnockoutBindingContext) => { const binding = valueAccessor(); + + const isLoading = ko.observable(false); + const canLoadNextPage = ko.computed(() => !isLoading() && valueAccessor().dataSet?.PagingOptions()?.IsLastPage() === false); + + // prepare the context with $appendableDataPager object + const state = { + loadNextPage: async () => { + try { + isLoading(true); + + await binding.loadNextPage(); + } + finally { + isLoading(false); + } + }, + canLoadNextPage, + isLoading: ko.computed(() => isLoading()) + }; + + // set up intersection observer 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 = 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]; - } - finally { - isLoading = false; - } + if (entry?.isIntersecting) { + if (!canLoadNextPage()) return; + + // load the next page + await state.loadNextPage(); + + // when the loading was finished, check whether we need to load another page + await new Promise(r => window.setTimeout(r, 500)); + observer.unobserve(element); + observer.observe(element); } + }, { + rootMargin: "20px" }); observer.observe(element); ko.utils.domNodeDisposal.addDisposeCallback(element, () => observer.disconnect()); } + + // extend the context + const innerBindingContext = bindingContext!.extend({ + $appendableDataPager: state + }); + ko.applyBindingsToDescendants(innerBindingContext, element); + return { controlsDescendantBindings: true }; // do not apply binding again } } } diff --git a/src/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj index 3f552bb05d..ca21111814 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -34,6 +34,7 @@ + diff --git a/src/Samples/Common/ViewModels/ControlSamples/AppendableDataPager/AppendableDataPagerViewModel.cs b/src/Samples/Common/ViewModels/ControlSamples/AppendableDataPager/AppendableDataPagerViewModel.cs index b37a7ce8c9..cbb16add08 100644 --- a/src/Samples/Common/ViewModels/ControlSamples/AppendableDataPager/AppendableDataPagerViewModel.cs +++ b/src/Samples/Common/ViewModels/ControlSamples/AppendableDataPager/AppendableDataPagerViewModel.cs @@ -48,6 +48,7 @@ public override Task PreRender() [AllowStaticCommand] public static async Task> LoadNextPage(GridViewDataSetOptions options) { + await Task.Delay(2000); var dataSet = new GridViewDataSet(); dataSet.ApplyOptions(options); dataSet.LoadFromQueryable(GetData()); diff --git a/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPager.dothtml b/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPager.dothtml index 11c27628be..972e42a994 100644 --- a/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPager.dothtml +++ b/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPager.dothtml @@ -23,6 +23,9 @@ + + Your data are on the way... + You reached to the end of the Earth. Now you shall see the 🐢🐢🐢🐢 and 🐘. diff --git a/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPagerAutoLoad.dothtml b/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPagerAutoLoad.dothtml new file mode 100644 index 0000000000..b03fb4db4c --- /dev/null +++ b/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPagerAutoLoad.dothtml @@ -0,0 +1,46 @@ +@viewModel DotVVM.Samples.Common.ViewModels.ControlSamples.AppendableDataPager.AppendableDataPagerViewModel, DotVVM.Samples.Common + + + + + + + + + + + +
        +

        Customer {{value: CustomerId}}

        +

        Name: {{value: Name}}

        +

        Birth date: {{value: BirthDate.ToString("d")}}

        +

        + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed et enim tristique, facilisis risus ut, mattis arcu. Pellentesque vitae egestas est, vitae placerat arcu. Aenean quis ipsum lacinia, sollicitudin ex nec, maximus mauris. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nulla imperdiet semper risus, at varius sem tristique vel. Vivamus fringilla euismod nulla. Aliquam feugiat accumsan aliquam. Vivamus et dolor ac augue congue vestibulum. Pellentesque gravida dignissim nibh, at rhoncus leo euismod sit amet. Donec consectetur, turpis sed mattis luctus, nisi diam laoreet libero, eu tincidunt dolor justo quis sem. Nunc pulvinar eget sem ac tincidunt. Sed mattis enim non euismod feugiat. Nullam sed ipsum faucibus, finibus elit ac, eleifend nibh. +

        +

        + Phasellus vel tortor ac diam volutpat lacinia. Nulla maximus, mauris non hendrerit pulvinar, ante ex suscipit orci, et aliquam lorem ipsum at tortor. Nulla tempus ante accumsan libero consequat, ac pellentesque massa malesuada. In convallis tortor ac purus tristique luctus. Maecenas id purus eget magna consequat porta a id sapien. Proin rhoncus eros efficitur consectetur feugiat. Praesent bibendum lorem et eros tempus, elementum sagittis tortor commodo. Proin elementum mi leo, sed mollis mauris luctus a. Etiam a arcu sit amet sem dignissim bibendum vel et velit. Cras pulvinar bibendum venenatis. Vivamus quis urna lacus. Proin scelerisque tempus erat quis dictum. Integer posuere facilisis sem id condimentum. Aliquam tortor lorem, finibus rhoncus consectetur porttitor, molestie a odio. Quisque aliquam commodo neque, ut aliquet quam vestibulum nec. +

        +

        + Nullam vel neque nisi. Ut mattis arcu interdum sodales commodo. Curabitur convallis, lectus nec vehicula placerat, ipsum tellus interdum lectus, ut pellentesque orci lorem sed elit. Praesent mi enim, dictum sit amet eros quis, aliquam sodales lorem. Integer sagittis orci eget mollis sodales. Nunc vel euismod libero. Aenean consectetur vel lorem in dapibus. Nam volutpat aliquet urna et pretium. +

        +

        + Aliquam dolor dui, tincidunt at sem eget, placerat scelerisque neque. Phasellus eros risus, luctus a libero nec, blandit iaculis magna. Aenean turpis justo, venenatis vel tortor vel, placerat cursus massa. Sed malesuada metus sit amet ante aliquet rhoncus. Nullam ut sodales velit. Donec accumsan tempor magna, eu mattis lectus suscipit vel. Nam est nunc, porta ac egestas et, commodo a dolor. Quisque vel ante non tellus pellentesque laoreet et non massa. Pellentesque tincidunt facilisis neque eu porttitor. Aliquam interdum maximus orci. Vivamus faucibus tincidunt mi ac vehicula. In nec dolor magna. Nam nec ligula enim. Morbi aliquam accumsan lacus a ornare. +

        +

        + Sed at gravida purus. Morbi quis porttitor diam. Donec imperdiet mi et dolor faucibus, nec placerat tellus placerat. Donec mollis magna a tortor elementum, vitae ultrices mauris pellentesque. Maecenas consequat interdum risus in pulvinar. Cras metus ligula, pharetra eget tincidunt sed, commodo eu mi. Morbi mattis odio molestie, faucibus arcu eget, faucibus ante. Nullam maximus ultrices lorem nec fermentum. Suspendisse laoreet venenatis velit a consectetur. In consectetur ligula leo, at suscipit turpis posuere tristique. Integer vel porta odio. Phasellus efficitur sed felis ut molestie. Vivamus finibus nisl at turpis iaculis, nec molestie nisi vestibulum. Sed vel turpis eleifend, egestas eros varius, congue enim. +

        +
        +
        + + + + Your data are on the way... + + + You reached to the end of the Earth. Now you shall see the 🐢🐢🐢🐢 and 🐘. + + + + + From ca5fa35566ee856777f25d545d21b5cf8d45f72d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 27 Apr 2024 17:27:17 +0200 Subject: [PATCH 38/60] Added tests for AppendableDataPager --- .../AppendableDataPager.dothtml | 4 +- .../AppendableDataPagerAutoLoad.dothtml | 6 +- .../Abstractions/SamplesRouteUrls.designer.cs | 4 ++ .../Tests/Control/AppendableDataPagerTests.cs | 71 +++++++++++++++++++ 4 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 src/Samples/Tests/Tests/Control/AppendableDataPagerTests.cs diff --git a/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPager.dothtml b/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPager.dothtml index 972e42a994..862c3d7711 100644 --- a/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPager.dothtml +++ b/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPager.dothtml @@ -24,10 +24,10 @@ - Your data are on the way... + Your data are on the way... - You reached to the end of the Earth. Now you shall see the 🐢🐢🐢🐢 and 🐘. + You reached to the end of the Earth. Now you shall see the 🐢🐢🐢🐢 and 🐘. diff --git a/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPagerAutoLoad.dothtml b/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPagerAutoLoad.dothtml index b03fb4db4c..60404006ff 100644 --- a/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPagerAutoLoad.dothtml +++ b/src/Samples/Common/Views/ControlSamples/AppendableDataPager/AppendableDataPagerAutoLoad.dothtml @@ -10,7 +10,7 @@ -
        +

        Customer {{value: CustomerId}}

        Name: {{value: Name}}

        Birth date: {{value: BirthDate.ToString("d")}}

        @@ -35,10 +35,10 @@ - Your data are on the way... + Your data are on the way... - You reached to the end of the Earth. Now you shall see the 🐢🐢🐢🐢 and 🐘. + You reached to the end of the Earth. Now you shall see the 🐢🐢🐢🐢 and 🐘. diff --git a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs index 6b287d9ae9..19e8632046 100644 --- a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs +++ b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs @@ -41,6 +41,8 @@ public partial class SamplesRouteUrls public const string ComplexSamples_TaskList_TaskListAsyncCommands = "ComplexSamples/TaskList/TaskListAsyncCommands"; public const string ComplexSamples_ViewModelDependencyInjection_Sample = "ComplexSamples/ViewModelDependencyInjection/Sample"; public const string ComplexSamples_ViewModelPopulate_ViewModelPopulate = "ComplexSamples/ViewModelPopulate/ViewModelPopulate"; + public const string ControlSamples_AppendableDataPager_AppendableDataPager = "ControlSamples/AppendableDataPager/AppendableDataPager"; + public const string ControlSamples_AppendableDataPager_AppendableDataPagerAutoLoad = "ControlSamples/AppendableDataPager/AppendableDataPagerAutoLoad"; public const string ControlSamples_AuthenticatedView_AuthenticatedViewTest = "ControlSamples/AuthenticatedView/AuthenticatedViewTest"; public const string ControlSamples_Button_Button = "ControlSamples/Button/Button"; public const string ControlSamples_Button_ButtonEnabled = "ControlSamples/Button/ButtonEnabled"; @@ -244,6 +246,8 @@ public partial class SamplesRouteUrls public const string FeatureSamples_CustomPrimitiveTypes_TextBox = "FeatureSamples/CustomPrimitiveTypes/TextBox"; public const string FeatureSamples_CustomPrimitiveTypes_UsedInControls = "FeatureSamples/CustomPrimitiveTypes/UsedInControls"; public const string FeatureSamples_CustomResponseProperties_SimpleExceptionFilter = "FeatureSamples/CustomResponseProperties/SimpleExceptionFilter"; + public const string FeatureSamples_DataSet_GitHubApi = "FeatureSamples/DataSet/GitHubApi"; + public const string FeatureSamples_DataSet_GitHubApiStaticCommands = "FeatureSamples/DataSet/GitHubApiStaticCommands"; public const string FeatureSamples_DateTimeSerialization_DateTimeSerialization = "FeatureSamples/DateTimeSerialization/DateTimeSerialization"; public const string FeatureSamples_DependencyInjection_ViewModelScopedService = "FeatureSamples/DependencyInjection/ViewModelScopedService"; public const string FeatureSamples_Directives_ImportDirective = "FeatureSamples/Directives/ImportDirective"; diff --git a/src/Samples/Tests/Tests/Control/AppendableDataPagerTests.cs b/src/Samples/Tests/Tests/Control/AppendableDataPagerTests.cs new file mode 100644 index 0000000000..c40f5c3ce0 --- /dev/null +++ b/src/Samples/Tests/Tests/Control/AppendableDataPagerTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Samples.Tests.Base; +using DotVVM.Testing.Abstractions; +using Riganti.Selenium.Core; +using Xunit; +using Xunit.Abstractions; + +namespace DotVVM.Samples.Tests.Control +{ + public class AppendableDataPagerTests : AppSeleniumTest + { + public AppendableDataPagerTests(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Control_AppendableDataPager_AppendableDataPager() + { + RunInAllBrowsers(browser => + { + browser.NavigateToUrl(SamplesRouteUrls.ControlSamples_AppendableDataPager_AppendableDataPager); + + var table = browser.Single("table"); + table.FindElements("tbody tr").ThrowIfDifferentCountThan(3); + + // load more data + for (var i = 1; i < 4; i++) + { + browser.Single("input[type=button]").Click(); + browser.WaitFor(() => { + browser.FindElements(".loading").ThrowIfDifferentCountThan(1, WaitForOptions.Disabled); + }, 2000); + table.FindElements("tbody tr").ThrowIfDifferentCountThan((i + 1) * 3); + } + + // check we are at the end + browser.FindElements("input[type=button]").ThrowIfDifferentCountThan(0); + browser.FindElements(".loaded").ThrowIfDifferentCountThan(1); + }); + } + + [Fact] + public void Control_AppendableDataPager_AppendableDataPagerAutoLoad() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.ControlSamples_AppendableDataPager_AppendableDataPagerAutoLoad); + + browser.FindElements(".customer h1").ThrowIfDifferentCountThan(3); + browser.Wait(1000); + + // load more data by scrolling to the bottom + for (var i = 1; i < 4; i++) + { + browser.GetJavaScriptExecutor() + .ExecuteScript("window.scrollTo(0, document.body.scrollHeight - 10)"); + browser.WaitFor(() => { + browser.FindElements(".loading").ThrowIfDifferentCountThan(1, WaitForOptions.Disabled); + }, 2000); + browser.FindElements(".customer h1").ThrowIfDifferentCountThan((i + 1) * 3); + } + + // check we are at the end + browser.FindElements(".loaded").ThrowIfDifferentCountThan(1); + }); + } + } +} From 9a2be8eebd0ed044318c44e7422685996b39c83f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 27 Apr 2024 18:18:47 +0200 Subject: [PATCH 39/60] Added tests for GitHub API test samples --- src/Samples/Common/DotvvmStartup.cs | 1 - .../FeatureSamples/DataSet/GitHubApi.dothtml | 8 +- .../DataSet/GitHubApiStaticCommands.dothtml | 10 ++- .../Tests/Tests/Feature/DataSetTests.cs | 86 +++++++++++++++++++ 4 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 src/Samples/Tests/Tests/Feature/DataSetTests.cs diff --git a/src/Samples/Common/DotvvmStartup.cs b/src/Samples/Common/DotvvmStartup.cs index fc32374ecc..c82d11c777 100644 --- a/src/Samples/Common/DotvvmStartup.cs +++ b/src/Samples/Common/DotvvmStartup.cs @@ -76,7 +76,6 @@ public void Configure(DotvvmConfiguration config, string applicationPath) map.Property(nameof(SerializationViewModel.Value2)).Bind(Direction.ClientToServer); map.Property(nameof(SerializationViewModel.IgnoredProperty)).Ignore(); }); - // new GithubApiClient.GithubApiClient().Repos.GetIssues() config.RegisterApiGroup(typeof(Common.Api.Owin.TestWebApiClientOwin), "http://localhost:61453/", "Scripts/TestWebApiClientOwin.js", "_apiOwin"); config.RegisterApiClient(typeof(Common.Api.AspNetCore.TestWebApiClientAspNetCore), "http://localhost:50001/", "Scripts/TestWebApiClientAspNetCore.js", "_apiCore"); diff --git a/src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApi.dothtml b/src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApi.dothtml index e2bc62828a..1b2bccc7bb 100644 --- a/src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApi.dothtml +++ b/src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApi.dothtml @@ -14,8 +14,8 @@

        NextToken

        - - + + @@ -25,8 +25,8 @@

        NextTokenHistory

        - - + + diff --git a/src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApiStaticCommands.dothtml b/src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApiStaticCommands.dothtml index 1d1bde0b73..58c9d85499 100644 --- a/src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApiStaticCommands.dothtml +++ b/src/Samples/Common/Views/FeatureSamples/DataSet/GitHubApiStaticCommands.dothtml @@ -15,8 +15,9 @@

        NextToken

        - + LoadData="{staticCommand: RootViewModel.LoadIssues}" + data-ui="next-pager"/> + @@ -27,8 +28,9 @@

        NextTokenHistory

        - + LoadData="{staticCommand: RootViewModel.LoadIssues2}" + data-ui="next-history-pager"/> + diff --git a/src/Samples/Tests/Tests/Feature/DataSetTests.cs b/src/Samples/Tests/Tests/Feature/DataSetTests.cs new file mode 100644 index 0000000000..26c8c0e681 --- /dev/null +++ b/src/Samples/Tests/Tests/Feature/DataSetTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Samples.Tests.Base; +using DotVVM.Testing.Abstractions; +using Riganti.Selenium.Core; +using Xunit; +using Xunit.Abstractions; + +namespace DotVVM.Samples.Tests.Feature +{ + public class DataSetTests : AppSeleniumTest + { + public DataSetTests(ITestOutputHelper output) : base(output) + { + } + + [Theory] + [InlineData(SamplesRouteUrls.FeatureSamples_DataSet_GitHubApi)] + [InlineData(SamplesRouteUrls.FeatureSamples_DataSet_GitHubApiStaticCommands)] + public void Feature_DataSet_GitHubApi_Next(string url) + { + RunInAllBrowsers(browser => + { + browser.NavigateToUrl(url); + + var grid = browser.Single("next-grid", SelectByDataUi); + var pager = browser.Single("next-pager", SelectByDataUi); + + // get first issue on the first page + var issueId = grid.ElementAt("tbody tr td", 0).GetInnerText(); + + // go next + pager.ElementAt("li", 1).Single("a").Click().Wait(500); + AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId); + + // go to first page + pager.ElementAt("li", 0).Single("a").Click().Wait(500); + AssertUI.TextEquals(grid.ElementAt("tbody tr td", 0), issueId); + }); + } + + [Theory] + [InlineData(SamplesRouteUrls.FeatureSamples_DataSet_GitHubApi)] + [InlineData(SamplesRouteUrls.FeatureSamples_DataSet_GitHubApiStaticCommands)] + public void Feature_DataSet_GitHubApi_NextHistory(string url) + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(url); + + var grid = browser.Single("next-history-grid", SelectByDataUi); + var pager = browser.Single("next-history-pager", SelectByDataUi); + + // get first issue on the first page + var issueId1 = grid.ElementAt("tbody tr td", 0).GetInnerText(); + + // go to page 2 + pager.ElementAt("li", 3).Single("a").Click().Wait(500); + AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId1); + var issueId2 = grid.ElementAt("tbody tr td", 0).GetInnerText(); + + // go to next page + pager.ElementAt("li", 5).Single("a").Click().Wait(500); + AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId1); + AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId2); + var issueId3 = grid.ElementAt("tbody tr td", 0).GetInnerText(); + + // go to first page + pager.ElementAt("li", 0).Single("a").Click().Wait(500); + AssertUI.TextEquals(grid.ElementAt("tbody tr td", 0), issueId1); + + // go to page 4 + pager.ElementAt("li", 5).Single("a").Click().Wait(500); + AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId1); + AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId2); + AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId3); + + // go to previous page + pager.ElementAt("li", 1).Single("a").Click().Wait(500); + AssertUI.TextEquals(grid.ElementAt("tbody tr td", 0), issueId3); + }); + } + } +} From 390ba482d2b60fcb9364f57eb8289e1a46168998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 27 Apr 2024 22:02:32 +0200 Subject: [PATCH 40/60] datasets: Remove setters from IRowEditOptions --- src/Framework/Core/Controls/Options/IRowEditOptions.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Framework/Core/Controls/Options/IRowEditOptions.cs b/src/Framework/Core/Controls/Options/IRowEditOptions.cs index 9bd2ce83d6..9ae00d337a 100644 --- a/src/Framework/Core/Controls/Options/IRowEditOptions.cs +++ b/src/Framework/Core/Controls/Options/IRowEditOptions.cs @@ -8,12 +8,11 @@ public interface IRowEditOptions /// /// Gets or sets the name of a property that uniquely identifies a row. (row ID, primary key, etc.). The value may be left out if inline editing is not enabled. /// - string? PrimaryKeyPropertyName { get; set; } + string? PrimaryKeyPropertyName { get; } /// /// Gets or sets the value of a property for the row that is being edited. Null if nothing is edited. /// - object? EditRowId { get; set; } - + object? EditRowId { get; } } } From 97c89669d7efc397a2f1d40906b7182c531433d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 27 Apr 2024 22:03:13 +0200 Subject: [PATCH 41/60] datasets: Add NoSorting, NoPaging and NoRowEdit options --- .../Core/Controls/Options/NoFilteringOptions.cs | 3 ++- .../Core/Controls/Options/NoPagingOptions.cs | 10 ++++++++++ .../Core/Controls/Options/NoRowEditOptions.cs | 11 +++++++++++ .../Core/Controls/Options/NoRowInsertOptions.cs | 3 ++- .../Core/Controls/Options/NoSortingOptions.cs | 10 ++++++++++ 5 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 src/Framework/Core/Controls/Options/NoPagingOptions.cs create mode 100644 src/Framework/Core/Controls/Options/NoRowEditOptions.cs create mode 100644 src/Framework/Core/Controls/Options/NoSortingOptions.cs diff --git a/src/Framework/Core/Controls/Options/NoFilteringOptions.cs b/src/Framework/Core/Controls/Options/NoFilteringOptions.cs index f2c19667f1..50a72732d3 100644 --- a/src/Framework/Core/Controls/Options/NoFilteringOptions.cs +++ b/src/Framework/Core/Controls/Options/NoFilteringOptions.cs @@ -2,7 +2,8 @@ namespace DotVVM.Framework.Controls { - public class NoFilteringOptions : IFilteringOptions, IApplyToQueryable + /// Dataset with NoFilteringOptions does not support filtering. + public sealed class NoFilteringOptions : IFilteringOptions, IApplyToQueryable { public IQueryable ApplyToQueryable(IQueryable queryable) => queryable; } diff --git a/src/Framework/Core/Controls/Options/NoPagingOptions.cs b/src/Framework/Core/Controls/Options/NoPagingOptions.cs new file mode 100644 index 0000000000..658c2e7a34 --- /dev/null +++ b/src/Framework/Core/Controls/Options/NoPagingOptions.cs @@ -0,0 +1,10 @@ +using System.Linq; + +namespace DotVVM.Framework.Controls +{ + /// Dataset with NoPagingOptions does not support paging. + public sealed class NoPagingOptions : IPagingOptions, IApplyToQueryable + { + public IQueryable ApplyToQueryable(IQueryable queryable) => queryable; + } +} diff --git a/src/Framework/Core/Controls/Options/NoRowEditOptions.cs b/src/Framework/Core/Controls/Options/NoRowEditOptions.cs new file mode 100644 index 0000000000..77d705385b --- /dev/null +++ b/src/Framework/Core/Controls/Options/NoRowEditOptions.cs @@ -0,0 +1,11 @@ +namespace DotVVM.Framework.Controls +{ + + /// Dataset with NoRowEditOptions does not support the row edit feature. + public sealed class NoRowEditOptions : IRowEditOptions + { + public string? PrimaryKeyPropertyName => null; + + public object? EditRowId => null; + } +} diff --git a/src/Framework/Core/Controls/Options/NoRowInsertOptions.cs b/src/Framework/Core/Controls/Options/NoRowInsertOptions.cs index 35644e8289..b86da74231 100644 --- a/src/Framework/Core/Controls/Options/NoRowInsertOptions.cs +++ b/src/Framework/Core/Controls/Options/NoRowInsertOptions.cs @@ -1,6 +1,7 @@ namespace DotVVM.Framework.Controls { - public class NoRowInsertOptions : IRowInsertOptions + /// Dataset with NoRowInsertOptions does not support the row insertion feature. + public sealed class NoRowInsertOptions : IRowInsertOptions { } } diff --git a/src/Framework/Core/Controls/Options/NoSortingOptions.cs b/src/Framework/Core/Controls/Options/NoSortingOptions.cs new file mode 100644 index 0000000000..0ba3864090 --- /dev/null +++ b/src/Framework/Core/Controls/Options/NoSortingOptions.cs @@ -0,0 +1,10 @@ +using System.Linq; + +namespace DotVVM.Framework.Controls +{ + /// Dataset with NoSortingOptions does not support sorting. + public sealed class NoSortingOptions : ISortingOptions, IApplyToQueryable + { + public IQueryable ApplyToQueryable(IQueryable queryable) => queryable; + } +} From e88faa7da3402a51038cf58b6b63bb59f7869bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 27 Apr 2024 22:04:03 +0200 Subject: [PATCH 42/60] datasets: Make GridViewDataSetResult fields nullable (as the constructor arguments are) --- src/Framework/Core/Controls/GenericGridViewDataSet.cs | 10 +++++++--- src/Framework/Core/Controls/GridViewDataSet.cs | 10 ++++++++++ src/Framework/Core/Controls/GridViewDataSetResult.cs | 6 +++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Framework/Core/Controls/GenericGridViewDataSet.cs b/src/Framework/Core/Controls/GenericGridViewDataSet.cs index 6d0118e72d..58946cb9b3 100644 --- a/src/Framework/Core/Controls/GenericGridViewDataSet.cs +++ b/src/Framework/Core/Controls/GenericGridViewDataSet.cs @@ -125,12 +125,16 @@ public GridViewDataSetOptions Sets new items + filtering, sorting, paging options.
        public void ApplyResult(GridViewDataSetResult result) { Items = result.Items.ToList(); - FilteringOptions = result.FilteringOptions; - SortingOptions = result.SortingOptions; - PagingOptions = result.PagingOptions; + if (result.FilteringOptions is {}) + FilteringOptions = result.FilteringOptions; + if (result.SortingOptions is {}) + SortingOptions = result.SortingOptions; + if (result.PagingOptions is {}) + PagingOptions = result.PagingOptions; } } } diff --git a/src/Framework/Core/Controls/GridViewDataSet.cs b/src/Framework/Core/Controls/GridViewDataSet.cs index 666a3e674b..a5f34bb683 100644 --- a/src/Framework/Core/Controls/GridViewDataSet.cs +++ b/src/Framework/Core/Controls/GridViewDataSet.cs @@ -17,5 +17,15 @@ public GridViewDataSet() : base(new NoFilteringOptions(), new SortingOptions(), new PagingOptions(), new NoRowInsertOptions(), new RowEditOptions()) { } + + // return specialized dataset options + public new GridViewDataSetOptions GetOptions() + { + return new GridViewDataSetOptions { + FilteringOptions = FilteringOptions, + SortingOptions = SortingOptions, + PagingOptions = PagingOptions + }; + } } } diff --git a/src/Framework/Core/Controls/GridViewDataSetResult.cs b/src/Framework/Core/Controls/GridViewDataSetResult.cs index 0dac85f9f8..c5a3b9487f 100644 --- a/src/Framework/Core/Controls/GridViewDataSetResult.cs +++ b/src/Framework/Core/Controls/GridViewDataSetResult.cs @@ -25,10 +25,10 @@ public GridViewDataSetResult(IReadOnlyList items, GridViewDataSetOptions< public IReadOnlyList Items { get; } - public TFilteringOptions FilteringOptions { get; } + public TFilteringOptions? FilteringOptions { get; } - public TSortingOptions SortingOptions { get; } + public TSortingOptions? SortingOptions { get; } - public TPagingOptions PagingOptions { get; } + public TPagingOptions? PagingOptions { get; } } } From ad878e7ed2e540465a03c8d20a75f21a9153c862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 27 Apr 2024 22:05:03 +0200 Subject: [PATCH 43/60] datasets: add JS translation for getOptions --- .../JavascriptTranslatableMethodCollection.cs | 9 ++++ .../Resources/Scripts/dataset/loader.ts | 53 ++++++++++--------- .../Resources/Scripts/dataset/translations.ts | 18 +++---- 3 files changed, 45 insertions(+), 35 deletions(-) diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index 575840b0c8..c9516a745f 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -848,6 +848,15 @@ private void AddDataSetOptionsTranslations() // _dataPager.Load() DataPagerApi.DataPagerExtensionParameter.Register(this); + // GridViewDataSet + var getOptions = new GenericMethodCompiler((JsExpression[] args, MethodInfo m) => + new JsIdentifierExpression("dotvvm").Member("dataSet").Member("getOptions") + .Invoke(args[1].WithAnnotation(ShouldBeObservableAnnotation.Instance)) + .WithAnnotation(new ViewModelInfoAnnotation(m.ReturnType, containsObservables: false)) + ); + AddMethodTranslator(typeof(GenericGridViewDataSet<,,,,,>).GetMethod("GetOptions"), getOptions); + AddMethodTranslator(() => default(GridViewDataSet)!.GetOptions(), getOptions); + // PagingOptions AddMethodTranslator(() => default(PagingOptions)!.GoToFirstPage(),new GenericMethodCompiler(args => new JsIdentifierExpression("dotvvm").Member("dataSet").Member("translations").Member("PagingOptions").Member("goToFirstPage") diff --git a/src/Framework/Framework/Resources/Scripts/dataset/loader.ts b/src/Framework/Framework/Resources/Scripts/dataset/loader.ts index 6d7f2e307c..4413ec3e96 100644 --- a/src/Framework/Framework/Resources/Scripts/dataset/loader.ts +++ b/src/Framework/Framework/Resources/Scripts/dataset/loader.ts @@ -1,37 +1,40 @@ import { StateManager } from "../state-manager"; type GridViewDataSet = { - PagingOptions: DotvvmObservable, - SortingOptions: DotvvmObservable, - FilteringOptions: DotvvmObservable, - Items: DotvvmObservable, - IsRefreshRequired?: DotvvmObservable + PagingOptions: object, + SortingOptions: object, + FilteringOptions: object, + Items: any[], + IsRefreshRequired?: boolean }; type GridViewDataSetOptions = { - PagingOptions: any, - SortingOptions: any, - FilteringOptions: any + PagingOptions: object, + SortingOptions: object, + FilteringOptions: object }; type GridViewDataSetResult = { Items: any[], - PagingOptions: any, - SortingOptions: any, - FilteringOptions: any + PagingOptions: object, + SortingOptions: object, + FilteringOptions: object }; +export function getOptions(dataSetObservable: DotvvmObservable): GridViewDataSetOptions { + const dataSet = dataSetObservable.state + return structuredClone({ + FilteringOptions: dataSet.FilteringOptions, + SortingOptions: dataSet.SortingOptions, + PagingOptions: dataSet.PagingOptions + }) +} + 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 options: GridViewDataSetOptions = { - FilteringOptions: structuredClone(dataSet.FilteringOptions), - SortingOptions: structuredClone(dataSet.SortingOptions), - PagingOptions: structuredClone(dataSet.PagingOptions) - }; + const options = getOptions(dataSetObservable); transformOptions(options); const result = await loadData(options); @@ -43,17 +46,15 @@ export async function loadDataSet( export const postProcessors = { replace(dataSet: DotvvmObservable, result: GridViewDataSetResult) { - dataSet.patchState(result); + dataSet.updateState(ds => ({...ds, ...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] - }); + dataSet.updateState(ds => ({ + ...ds, + ...result, + Items: [...ds.Items, ...result.Items] + })); } }; diff --git a/src/Framework/Framework/Resources/Scripts/dataset/translations.ts b/src/Framework/Framework/Resources/Scripts/dataset/translations.ts index 0c6a07d654..0b4145a714 100644 --- a/src/Framework/Framework/Resources/Scripts/dataset/translations.ts +++ b/src/Framework/Framework/Resources/Scripts/dataset/translations.ts @@ -1,4 +1,6 @@ -type PagingOptions = { +import { getOptions } from "./loader"; + +type PagingOptions = { PageIndex: number, PagesCount: number }; @@ -29,6 +31,7 @@ type SortCriterion = { }; export const translations = { + getOptions, PagingOptions: { goToFirstPage(options: PagingOptions) { options.PageIndex = 0; @@ -100,10 +103,10 @@ export const translations = { } }, isColumnSortedAscending(options: SortingOptions, sortExpression: string) { - return options.SortExpression === sortExpression && !options.SortDescending; + return options && options.SortExpression === sortExpression && !options.SortDescending; }, isColumnSortedDescending(options: SortingOptions, sortExpression: string) { - return options.SortExpression === sortExpression && options.SortDescending; + return options && options.SortExpression === sortExpression && options.SortDescending; } }, MultiCriteriaSortingOptions: { @@ -125,16 +128,13 @@ export const translations = { options.Criteria.unshift({ SortExpression: sortExpression, SortDescending: false }); } - if (options.Criteria.length > options.MaxSortCriteriaCount) - { - options.Criteria.splice(options.MaxSortCriteriaCount, options.Criteria.length - options.MaxSortCriteriaCount); - } + options.Criteria.splice(options.MaxSortCriteriaCount); }, isColumnSortedAscending(options: MultiCriteriaSortingOptions, sortExpression: string) { - return options.Criteria.some(c => c.SortExpression === sortExpression && !c.SortDescending); + return options?.Criteria?.some(c => c.SortExpression === sortExpression && !c.SortDescending); }, isColumnSortedDescending(options: MultiCriteriaSortingOptions, sortExpression: string) { - return options.Criteria.some(c => c.SortExpression === sortExpression && c.SortDescending); + return options?.Criteria?.some(c => c.SortExpression === sortExpression && c.SortDescending); } } }; From 1295be132f2ddeb2281645e20b99574a063a673d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 27 Apr 2024 22:05:48 +0200 Subject: [PATCH 44/60] datasets: error handling - missing sorting capabilities --- .../Controls/Options/MultiCriteriaSortingOptions.cs | 3 +++ src/Framework/Core/Controls/Options/SortingOptions.cs | 3 +++ src/Framework/Framework/Controls/GridView.cs | 2 +- src/Framework/Framework/Controls/GridViewColumn.cs | 11 ++++++++--- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs b/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs index f91651fb66..7d71fae90f 100644 --- a/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs +++ b/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs @@ -24,6 +24,9 @@ public virtual IQueryable ApplyToQueryable(IQueryable queryable) public virtual void SetSortExpression(string? sortExpression) { + if (sortExpression is {} && !IsSortingAllowed(sortExpression)) + throw new ArgumentException($"Sorting by column '{sortExpression}' is not allowed."); + if (sortExpression == null) { Criteria.Clear(); diff --git a/src/Framework/Core/Controls/Options/SortingOptions.cs b/src/Framework/Core/Controls/Options/SortingOptions.cs index da2bd17df2..af52efdae0 100644 --- a/src/Framework/Core/Controls/Options/SortingOptions.cs +++ b/src/Framework/Core/Controls/Options/SortingOptions.cs @@ -32,6 +32,9 @@ public class SortingOptions : ISortingOptions, ISortingStateCapability, ISorting /// public virtual void SetSortExpression(string? sortExpression) { + if (sortExpression is {} && !IsSortingAllowed(sortExpression)) + throw new ArgumentException($"Sorting by column '{sortExpression}' is not allowed."); + if (sortExpression == null) { SortExpression = null; diff --git a/src/Framework/Framework/Controls/GridView.cs b/src/Framework/Framework/Controls/GridView.cs index cfebb59e3d..78e5bf9f3d 100644 --- a/src/Framework/Framework/Controls/GridView.cs +++ b/src/Framework/Framework/Controls/GridView.cs @@ -258,7 +258,7 @@ protected virtual void CreateHeaderRow(IDotvvmRequestContext context) SetCellAttributes(column, cell, true); var decoratedCell = Decorator.ApplyDecorators(cell, column.HeaderCellDecorators); headerRow.Children.Add(decoratedCell); - + column.CreateHeaderControls(context, this, gridViewCommands, sortCommandBindingOverride, cell, gridViewDataSet); if (FilterPlacement == GridViewFilterPlacement.HeaderRow) { diff --git a/src/Framework/Framework/Controls/GridViewColumn.cs b/src/Framework/Framework/Controls/GridViewColumn.cs index 5d7ddf1828..3e4de88bc3 100644 --- a/src/Framework/Framework/Controls/GridViewColumn.cs +++ b/src/Framework/Framework/Controls/GridViewColumn.cs @@ -207,6 +207,12 @@ public virtual void CreateHeaderControls(IDotvvmRequestContext context, GridView var sortExpression = GetSortExpression(); + if (string.IsNullOrEmpty(sortExpression)) + throw new DotvvmControlException(this, "The SortExpression property must be set when AllowSorting is true!"); + + if ((gridViewDataSet?.SortingOptions as ISortingSetSortExpressionCapability)?.IsSortingAllowed(sortExpression) == false) + throw new DotvvmControlException(this, $"The sort expression '{sortExpression}' is not allowed in the sorting options!"); + var linkButton = new LinkButton(); linkButton.SetValue(ButtonBase.TextProperty, GetValueRaw(HeaderTextProperty)); linkButton.ClickArguments = new object?[] { sortExpression }; @@ -236,10 +242,9 @@ public virtual void CreateFilterControls(IDotvvmRequestContext context, GridView private void SetSortedCssClass(HtmlGenericControl cell, ISortableGridViewDataSet? gridViewDataSet, GridViewCommands gridViewCommands) { - if (gridViewDataSet is ISortableGridViewDataSet sortableGridViewDataSet) + if (gridViewDataSet is ISortableGridViewDataSet sortableGridViewDataSet && + GetSortExpression() is {} sortExpression) { - var sortExpression = GetSortExpression(); - var cellAttributes = cell.Attributes; if (!RenderOnServer) { From 882b6e77b42be7750b7f580519e3366c36b2e197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 27 Apr 2024 22:10:48 +0200 Subject: [PATCH 45/60] datasets: more doccomments --- .../Core/Controls/Options/GridViewDataSetOptions.cs | 2 ++ src/Framework/Core/Controls/Options/IPagingOptions.cs | 3 +++ src/Framework/Core/Controls/Options/ISortingOptions.cs | 4 +++- .../Controls/Options/MultiCriteriaSortingOptions.cs | 10 ++++++++++ src/Framework/Core/Controls/Options/PagingOptions.cs | 8 ++++++-- src/Framework/Framework/Controls/GridViewCommands.cs | 10 +++++----- .../Controls/GridViewDataSetBindingProvider.cs | 1 + 7 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/Framework/Core/Controls/Options/GridViewDataSetOptions.cs b/src/Framework/Core/Controls/Options/GridViewDataSetOptions.cs index dfd6824380..57a4c20c99 100644 --- a/src/Framework/Core/Controls/Options/GridViewDataSetOptions.cs +++ b/src/Framework/Core/Controls/Options/GridViewDataSetOptions.cs @@ -1,5 +1,6 @@ namespace DotVVM.Framework.Controls { + /// Contains filtering, sorting, and paging options of a . public class GridViewDataSetOptions where TFilteringOptions : IFilteringOptions where TSortingOptions : ISortingOptions @@ -12,6 +13,7 @@ public class GridViewDataSetOptions Contains filtering, sorting, and paging options of a public class GridViewDataSetOptions : GridViewDataSetOptions { } diff --git a/src/Framework/Core/Controls/Options/IPagingOptions.cs b/src/Framework/Core/Controls/Options/IPagingOptions.cs index 0579227e03..bb836e29cf 100644 --- a/src/Framework/Core/Controls/Options/IPagingOptions.cs +++ b/src/Framework/Core/Controls/Options/IPagingOptions.cs @@ -5,6 +5,9 @@ namespace DotVVM.Framework.Controls /// /// Represents a marker interface for GridViewDataSet paging options. /// + /// + /// + /// public interface IPagingOptions { } diff --git a/src/Framework/Core/Controls/Options/ISortingOptions.cs b/src/Framework/Core/Controls/Options/ISortingOptions.cs index 108084ca8e..a2c48660cc 100644 --- a/src/Framework/Core/Controls/Options/ISortingOptions.cs +++ b/src/Framework/Core/Controls/Options/ISortingOptions.cs @@ -3,6 +3,9 @@ namespace DotVVM.Framework.Controls /// /// Represents a marker interface for sorting options. /// + /// + /// + /// public interface ISortingOptions { } @@ -37,7 +40,6 @@ public interface ISortingStateCapability : ISortingOptions /// Determines whether the column with specified sort expression is sorted in descending order. /// bool IsColumnSortedDescending(string? sortExpression); - } /// diff --git a/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs b/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs index 7d71fae90f..248d37275a 100644 --- a/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs +++ b/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs @@ -5,10 +5,17 @@ namespace DotVVM.Framework.Controls; +/// +/// Sorting options which supports sorting by multiple columns. +/// In the default implementation, the sorting is applied in the reverse order of clicks on the columns - i.e. the last clicked column is the primary sort criterion, the previous one is the secondary criterion, etc. +/// It behaves similarly to the standard , except that sorting is "stable". +/// Maximum number of sort criteria can be set by , and is 3 by default. +/// public class MultiCriteriaSortingOptions : ISortingOptions, ISortingStateCapability, ISortingSetSortExpressionCapability, IApplyToQueryable { public IList Criteria { get; set; } = new List(); + /// Maximum length of the list. When exceeded, the overhanging tail is discarded. public int MaxSortCriteriaCount { get; set; } = 3; public virtual IQueryable ApplyToQueryable(IQueryable queryable) @@ -20,6 +27,7 @@ public virtual IQueryable ApplyToQueryable(IQueryable queryable) return queryable; } + /// When overridden in derived class, it can disallow sorting by certain columns. public virtual bool IsSortingAllowed(string sortExpression) => true; public virtual void SetSortExpression(string? sortExpression) @@ -57,7 +65,9 @@ public virtual void SetSortExpression(string? sortExpression) } } + /// Returns if the specified column is sorted in ascending order in any of the public bool IsColumnSortedAscending(string? sortExpression) => Criteria.Any(c => c.SortExpression == sortExpression && !c.SortDescending); + /// Returns if the specified column is sorted in descending order in any of the public bool IsColumnSortedDescending(string? sortExpression) => Criteria.Any(c => c.SortExpression == sortExpression && c.SortDescending); } diff --git a/src/Framework/Core/Controls/Options/PagingOptions.cs b/src/Framework/Core/Controls/Options/PagingOptions.cs index 303de1491b..9a626f53fe 100644 --- a/src/Framework/Core/Controls/Options/PagingOptions.cs +++ b/src/Framework/Core/Controls/Options/PagingOptions.cs @@ -6,7 +6,7 @@ namespace DotVVM.Framework.Controls { /// - /// Represents settings for paging. + /// Represents settings for offset-based paging using and . /// public class PagingOptions : IPagingOptions, IPagingFirstPageCapability, IPagingLastPageCapability, IPagingPreviousPageCapability, IPagingNextPageCapability, IPagingPageIndexCapability, IPagingPageSizeCapability, IPagingTotalItemsCountCapability, IApplyToQueryable, IPagingOptionsLoadingPostProcessor { @@ -57,10 +57,13 @@ public int PagesCount /// public int TotalItemsCount { get; set; } + /// Sets PageIndex to zero. public void GoToFirstPage() => PageIndex = 0; + /// Sets PageIndex to the last page (PagesCount - 1). public void GoToLastPage() => PageIndex = PagesCount - 1; + /// Increments the page counter, if the next page exists. public void GoToNextPage() { if (PageIndex < PagesCount - 1) @@ -68,6 +71,7 @@ public void GoToNextPage() PageIndex++; } } + /// Decrements the page counter, unless PageIndex is already zero. public void GoToPreviousPage() { if (PageIndex > 0) @@ -75,7 +79,7 @@ public void GoToPreviousPage() PageIndex--; } } - + /// Sets page index to the . If the index overflows, PageIndex is set to the first/last page. public void GoToPage(int pageIndex) { if (PageIndex >= 0 && PageIndex < PagesCount) diff --git a/src/Framework/Framework/Controls/GridViewCommands.cs b/src/Framework/Framework/Controls/GridViewCommands.cs index d51a4d83e2..38e28081dc 100644 --- a/src/Framework/Framework/Controls/GridViewCommands.cs +++ b/src/Framework/Framework/Controls/GridViewCommands.cs @@ -4,18 +4,18 @@ namespace DotVVM.Framework.Controls { + /// Contains pre-created data bindings for the components. Instance can obtained from public class GridViewCommands { - - private readonly ConcurrentDictionary> isSortColumnAscending = new(); - private readonly ConcurrentDictionary> isSortColumnDescending = new(); + private readonly ConcurrentDictionary> isSortColumnAscending = new(); + private readonly ConcurrentDictionary> isSortColumnDescending = new(); public ICommandBinding? SetSortExpression { get; init; } internal IValueBinding>? IsColumnSortedAscending { get; init; } internal IValueBinding>? IsColumnSortedDescending { get; init; } - public IValueBinding? GetIsColumnSortedAscendingBinding(string? sortExpression) + public IValueBinding? GetIsColumnSortedAscendingBinding(string sortExpression) { if (IsColumnSortedAscending == null) { @@ -24,7 +24,7 @@ public class GridViewCommands return isSortColumnAscending.GetOrAdd(sortExpression, _ => IsColumnSortedAscending.Select(a => a(sortExpression))); } - public IValueBinding? GetIsColumnSortedDescendingBinding(string? sortExpression) + public IValueBinding? GetIsColumnSortedDescendingBinding(string sortExpression) { if (IsColumnSortedDescending == null) { diff --git a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs index fc1e770bb4..2651526d69 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -17,6 +17,7 @@ namespace DotVVM.Framework.Controls; +/// Creates data bindings for GridView, DataPager and related components. public class GridViewDataSetBindingProvider { private readonly BindingCompilationService service; From 24fc65869dc8909ca68dbcdba03689ae0242cf68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 27 Apr 2024 22:11:10 +0200 Subject: [PATCH 46/60] datasets: error handling - multiple (ambiguous) interface implementations --- .../Framework/Controls/GridViewDataSetBindingProvider.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs index 2651526d69..e9da10f422 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -361,6 +361,7 @@ private static Type GetOptionsConcreteType(Type dataSetConcre var interfaces = dataSetConcreteType.GetInterfaces() .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == genericInterface) + .Distinct() .ToList(); if (interfaces.Count < 1) { @@ -368,7 +369,7 @@ private static Type GetOptionsConcreteType(Type dataSetConcre } else if (interfaces.Count > 1) { - throw new ArgumentException($"The {dataSetConcreteType} implements multiple interfaces where {genericInterface.Name}. Only one implementation is allowed."); + throw new ArgumentException($"The {dataSetConcreteType} implements multiple interfaces where {genericInterface.Name} ({interfaces.Select(i => i.ToCode()).StringJoin(", ")}). Only one implementation is allowed."); } var pagingOptionsConcreteType = interfaces[0].GetGenericArguments()[0]; From 4fbe3cb9fcaa7eb5445d654b8a258be4a14eab18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 27 Apr 2024 22:17:33 +0200 Subject: [PATCH 47/60] datasets: accept new test outputs --- src/Tests/ControlTests/DataPagerTests.cs | 15 --------------- ...ataPagerTests.StaticCommandApendablePager.html | 14 +++++++------- ...SerializationTests.SerializeDefaultConfig.json | 13 +++++++++++++ 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/Tests/ControlTests/DataPagerTests.cs b/src/Tests/ControlTests/DataPagerTests.cs index eded24ff8b..da1cba7fbb 100644 --- a/src/Tests/ControlTests/DataPagerTests.cs +++ b/src/Tests/ControlTests/DataPagerTests.cs @@ -78,21 +78,6 @@ public async Task StaticCommandApendablePager() ); check.CheckString(r.FormattedHtml, fileExtension: "html"); - - var commandExpressions = r.Commands - .Select(c => (c.control, c.command, str: c.command.GetProperty().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 diff --git a/src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandApendablePager.html b/src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandApendablePager.html index 0dec861046..8a9290a44f 100644 --- a/src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandApendablePager.html +++ b/src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandApendablePager.html @@ -1,25 +1,25 @@ -
        (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("WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkNvbnRyb2xUZXN0cy5EYXRhUGFnZXJUZXN0cytHcmlkVmlld01vZGVsLCBEb3RWVk0uRnJhbWV3b3JrLlRlc3RzIiwiTG9hZEN1c3RvbWVycyIsW10sMSxudWxsLCJBQT09Il0=", [args[0]], options),$element,[],args,$context))), dotvvm.dataSet.postProcessors.append); },$element,[],args,$context)), -postProcessor: dotvvm.dataSet.postProcessors.append +autoLoadWhenInViewport: false }"> - +
        + + + end -
        - end -
        diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index cd07e69fb8..ecc1ad6424 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -378,6 +378,19 @@ "required": true, "onlyBindings": true }, + "LoadingTemplate": { + "type": "DotVVM.Framework.Controls.ITemplate, DotVVM.Framework", + "dataContextChange": [ + { + "$type": "DotVVM.Framework.Binding.HelperNamespace.DataPagerApi+AddParameterDataContextChangeAttribute, DotVVM.Framework", + "Name": "_dataPager", + "NestDataContext": false, + "TypeId": "DotVVM.Framework.Binding.HelperNamespace.DataPagerApi+AddParameterDataContextChangeAttribute, DotVVM.Framework" + } + ], + "mappingMode": "InnerElement", + "onlyHardcoded": true + }, "LoadTemplate": { "type": "DotVVM.Framework.Controls.ITemplate, DotVVM.Framework", "dataContextChange": [ From 15ffc4c900d3a133b46c9c7369b8a507ac59ea6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Miku=C5=A1?= Date: Mon, 13 May 2024 17:56:33 +0200 Subject: [PATCH 48/60] Data pager extensibility improvements - to make it possible to implement `aria-labels` classes on links, and reader only spans I implemented extensibility points --- src/Framework/Framework/Controls/DataPager.cs | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index 40d4f06571..f706f7a4fd 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -192,22 +192,9 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) if (pagerBindings.PageNumbers is {}) { // number fields - var liTemplate = new HtmlGenericControl("li"); - liTemplate.CssClasses.Add(ActiveItemCssClass, new ValueOrBinding(pagerBindings.IsActivePage.NotNull())); - var link = new LinkButton(); - link.SetBinding(ButtonBase.ClickProperty, pagerBindings.GoToPage.NotNull()); - link.SetBinding(ButtonBase.TextProperty, pagerBindings.PageNumberText.NotNull()); - if (!true.Equals(globalEnabled)) link.SetValue(LinkButton.EnabledProperty, globalEnabled); - liTemplate.Children.Add(link); - if (!this.RenderLinkForCurrentPage) - { - var notLink = new Literal(pagerBindings.PageNumberText); - notLink.RenderSpanElement = true; - notLink.SetBinding(DotvvmControl.IncludeInPageProperty, pagerBindings.IsActivePage); - link.SetBinding(DotvvmControl.IncludeInPageProperty, pagerBindings.IsActivePage.Negate()); - liTemplate.Children.Add(notLink); - } + var liTemplate = CreatePageNumberButton(globalEnabled, pagerBindings, context); AddItemCssClass(liTemplate, context); + NumberButtonsRepeater = new Repeater() { DataSource = pagerBindings.PageNumbers, RenderWrapperTag = false, @@ -251,19 +238,40 @@ protected virtual HtmlGenericControl CreateWrapperList() return list; } + protected virtual HtmlGenericControl CreatePageNumberButton(ValueOrBinding globalEnabled, DataPagerBindings pagerBindings, IDotvvmRequestContext context) + { + var liTemplate = new HtmlGenericControl("li"); + liTemplate.CssClasses.Add(ActiveItemCssClass, new ValueOrBinding(pagerBindings.NotNull().IsActivePage.NotNull())); + var link = new LinkButton(); + link.SetBinding(ButtonBase.ClickProperty, pagerBindings.NotNull().GoToPage.NotNull()); + SetPageNumberButtonContent(link, pagerBindings, context); + if (!RenderLinkForCurrentPage) link.SetBinding(IncludeInPageProperty, pagerBindings.IsActivePage.NotNull().Negate()); + if (!true.Equals(globalEnabled)) link.SetValue(ButtonBase.EnabledProperty, globalEnabled); + liTemplate.Children.Add(link); + + if (!RenderLinkForCurrentPage) + { + var notLink = new Literal(); + SetPageNumberSpanContent(notLink, pagerBindings, context); + notLink.RenderSpanElement = true; + notLink.SetBinding(IncludeInPageProperty, pagerBindings.IsActivePage); + liTemplate.Children.Add(notLink); + } + return liTemplate; + } + protected virtual HtmlGenericControl CreateNavigationButton(string defaultText, ITemplate? userDefinedContentTemplate, object enabledValue, ICommandBinding clickCommandBindingExpression,IDotvvmRequestContext context) { var li = new HtmlGenericControl("li"); var link = new LinkButton(); - SetButtonContent(context, link, defaultText, userDefinedContentTemplate); + SetNavigationButtonContent(context, link, defaultText, userDefinedContentTemplate); link.SetBinding(ButtonBase.ClickProperty, clickCommandBindingExpression); - if (!true.Equals(enabledValue)) - link.SetValue(LinkButton.EnabledProperty, enabledValue); + if (!true.Equals(enabledValue)) link.SetValue(ButtonBase.EnabledProperty, enabledValue); li.Children.Add(link); return li; } - protected virtual void SetButtonContent(Hosting.IDotvvmRequestContext context, LinkButton button, string text, ITemplate? contentTemplate) + protected virtual void SetNavigationButtonContent(IDotvvmRequestContext context, LinkButton button, string text, ITemplate? contentTemplate) { if (contentTemplate != null) { @@ -275,6 +283,16 @@ protected virtual void SetButtonContent(Hosting.IDotvvmRequestContext context, L } } + protected virtual void SetPageNumberSpanContent(Literal notLink, DataPagerBindings pagerBindings, IDotvvmRequestContext context) + { + notLink.SetBinding(Literal.TextProperty, pagerBindings.PageNumberText.NotNull()); + } + + protected virtual void SetPageNumberButtonContent(LinkButton link, DataPagerBindings pagerBindings, IDotvvmRequestContext context) + { + link.SetBinding(ButtonBase.TextProperty, pagerBindings.PageNumberText.NotNull()); + } + protected virtual void AddItemCssClass(HtmlGenericControl item, IDotvvmRequestContext context) { } From 921b4138300fe9eb11065f11e74601e67f290c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 1 Jun 2024 18:07:53 +0200 Subject: [PATCH 49/60] GridView: fix SortChanged precedence --- src/Framework/Framework/Controls/GridView.cs | 5 ++++ .../Framework/Controls/GridViewColumn.cs | 2 +- src/Tests/ControlTests/GridViewTests.cs | 15 +++++++++++ ...dViewTests.SortedChangedStaticCommand.html | 27 +++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 src/Tests/ControlTests/testoutputs/GridViewTests.SortedChangedStaticCommand.html diff --git a/src/Framework/Framework/Controls/GridView.cs b/src/Framework/Framework/Controls/GridView.cs index 78e5bf9f3d..813e06f045 100644 --- a/src/Framework/Framework/Controls/GridView.cs +++ b/src/Framework/Framework/Controls/GridView.cs @@ -296,6 +296,11 @@ protected virtual void CreateHeaderRow(IDotvvmRequestContext context) protected virtual ICommandBinding BuildDefaultSortCommandBinding() { + if (GetValueRaw(SortChangedProperty) is IStaticCommandBinding staticCommandBinding) + { + return staticCommandBinding; + } + var dataContextStack = this.GetDataContextType()!; return new CommandBindingExpression(bindingCompilationService.WithoutInitialization(), new object[] { diff --git a/src/Framework/Framework/Controls/GridViewColumn.cs b/src/Framework/Framework/Controls/GridViewColumn.cs index 3e4de88bc3..9a48f10af9 100644 --- a/src/Framework/Framework/Controls/GridViewColumn.cs +++ b/src/Framework/Framework/Controls/GridViewColumn.cs @@ -199,7 +199,7 @@ public virtual void CreateHeaderControls(IDotvvmRequestContext context, GridView if (AllowSorting) { - var sortCommandBinding = gridViewCommands.SetSortExpression ?? sortCommandBindingOverride; + var sortCommandBinding = sortCommandBindingOverride ?? gridViewCommands.SetSortExpression; if (sortCommandBinding == null) { throw new DotvvmControlException(this, "Cannot use column sorting where no sort command is specified. Either put IGridViewDataSet in the DataSource property of the GridView, or set the SortChanged command on the GridView to implement custom sorting logic!"); diff --git a/src/Tests/ControlTests/GridViewTests.cs b/src/Tests/ControlTests/GridViewTests.cs index 219b029874..7f2b980ccd 100644 --- a/src/Tests/ControlTests/GridViewTests.cs +++ b/src/Tests/ControlTests/GridViewTests.cs @@ -154,6 +154,21 @@ vvv enabled customer vvv check.CheckString(r.FormattedHtml, fileExtension: "html"); } + + [TestMethod] + public async Task SortedChangedStaticCommand() + { + var r = await cth.RunPage(typeof(BasicTestViewModel), """ + _js.Invoke("resort", dir)}> + + + + + """, directives: "@js dotvvm.internal"); + + check.CheckString(r.FormattedHtml, fileExtension: "html"); + } + public class BasicTestViewModel: DotvvmViewModelBase { [Bind(Name = "int")] diff --git a/src/Tests/ControlTests/testoutputs/GridViewTests.SortedChangedStaticCommand.html b/src/Tests/ControlTests/testoutputs/GridViewTests.SortedChangedStaticCommand.html new file mode 100644 index 0000000000..92ac2cbd9a --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/GridViewTests.SortedChangedStaticCommand.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + +
        + Name +
        + +
        + + + From cdfb4ed0b6f2be8a46d10813bae59efcb3a12243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 1 Jun 2024 18:10:35 +0200 Subject: [PATCH 50/60] GridViewDataSet: simplify inheritance with C# default constructors --- src/Directory.Build.props | 2 +- .../Core/Controls/GridViewDataSet.cs | 9 ++----- .../GridViewStaticCommandViewModel.cs | 27 +++++++------------ 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 56b7b42208..cc0bba6d99 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -20,7 +20,7 @@ - 11.0 + 12.0 $(NoWarn);CS1591;CS1573 true diff --git a/src/Framework/Core/Controls/GridViewDataSet.cs b/src/Framework/Core/Controls/GridViewDataSet.cs index a5f34bb683..fc730b6de0 100644 --- a/src/Framework/Core/Controls/GridViewDataSet.cs +++ b/src/Framework/Core/Controls/GridViewDataSet.cs @@ -10,14 +10,9 @@ namespace DotVVM.Framework.Controls /// Represents a collection of items with paging, sorting and row edit capabilities. /// /// The type of the elements in the collection. - public class GridViewDataSet - : GenericGridViewDataSet + public class GridViewDataSet() + : GenericGridViewDataSet(new(), new(), new(), new(), new()) { - public GridViewDataSet() - : base(new NoFilteringOptions(), new SortingOptions(), new PagingOptions(), new NoRowInsertOptions(), new RowEditOptions()) - { - } - // return specialized dataset options public new GridViewDataSetOptions GetOptions() { diff --git a/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs b/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs index 12a4e43ea6..a41ce1b149 100644 --- a/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs +++ b/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewStaticCommandViewModel.cs @@ -90,12 +90,9 @@ public static async Task LoadMultiSort(GridViewDataSet return dataSet; } - public class NextTokenGridViewDataSet : GenericGridViewDataSet, RowEditOptions> - { - public NextTokenGridViewDataSet() : base(new NoFilteringOptions(), new SortingOptions(), new CustomerDataNextTokenPagingOptions(), new RowInsertOptions(), new RowEditOptions()) - { - } - } + public class NextTokenGridViewDataSet() : GenericGridViewDataSet, RowEditOptions>( + new(), new(), new(), new(), new() + ); public class CustomerDataNextTokenPagingOptions : NextTokenPagingOptions, IApplyToQueryable, IPagingOptionsLoadingPostProcessor { @@ -128,12 +125,9 @@ public void ProcessLoadedItems(IQueryable filteredQueryable, IList item } } - public class NextTokenHistoryGridViewDataSet : GenericGridViewDataSet, RowEditOptions> - { - public NextTokenHistoryGridViewDataSet() : base(new NoFilteringOptions(), new SortingOptions(), new CustomerDataNextTokenHistoryPagingOptions(), new RowInsertOptions(), new RowEditOptions()) - { - } - } + public class NextTokenHistoryGridViewDataSet() : GenericGridViewDataSet, RowEditOptions>( + new(), new(), new(), new(), new() + ); public class CustomerDataNextTokenHistoryPagingOptions : NextTokenHistoryPagingOptions, IApplyToQueryable, IPagingOptionsLoadingPostProcessor { @@ -173,11 +167,8 @@ public void ProcessLoadedItems(IQueryable filteredQueryable, IList item } } - public class MultiSortGridViewDataSet : GenericGridViewDataSet, RowEditOptions> - { - public MultiSortGridViewDataSet() : base(new NoFilteringOptions(), new MultiCriteriaSortingOptions(), new PagingOptions(), new RowInsertOptions(), new RowEditOptions()) - { - } - } + public class MultiSortGridViewDataSet() : GenericGridViewDataSet, RowEditOptions>( + new(), new(), new(), new(), new() + ); } } From 309cc5c8d89d80a39342003495f1b85430220053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 1 Jun 2024 18:43:33 +0200 Subject: [PATCH 51/60] GridViewDataSet: make IBaseGridViewDataSet.Items behave reasonably Normally, we return the inner mutable list, if it is IList. If it isn't we return a ReadOnlyCollection wrapper, to throw an exception instead of mutating the copy --- src/Framework/Core/Controls/GenericGridViewDataSet.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Framework/Core/Controls/GenericGridViewDataSet.cs b/src/Framework/Core/Controls/GenericGridViewDataSet.cs index 58946cb9b3..bb5867b845 100644 --- a/src/Framework/Core/Controls/GenericGridViewDataSet.cs +++ b/src/Framework/Core/Controls/GenericGridViewDataSet.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; namespace DotVVM.Framework.Controls @@ -62,7 +63,7 @@ public class GenericGridViewDataSet< public TRowEditOptions RowEditOptions { get; set; } - IList IBaseGridViewDataSet.Items => Items is List list ? list : Items.ToList(); + IList IBaseGridViewDataSet.Items => Items is IList list ? list : new ReadOnlyCollection(Items); IFilteringOptions IFilterableGridViewDataSet.FilteringOptions => this.FilteringOptions; From b2a59a97082cf137db2510107c9cdb0c2c903d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 1 Jun 2024 18:43:51 +0200 Subject: [PATCH 52/60] Button: Move ClickArguments handling to the base class --- src/Framework/Framework/Controls/Button.cs | 8 ++------ src/Framework/Framework/Controls/ButtonBase.cs | 12 ++++++++++++ src/Framework/Framework/Controls/LinkButton.cs | 8 ++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Framework/Framework/Controls/Button.cs b/src/Framework/Framework/Controls/Button.cs index 6cfe979170..7f1c7d15ad 100644 --- a/src/Framework/Framework/Controls/Button.cs +++ b/src/Framework/Framework/Controls/Button.cs @@ -128,13 +128,9 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest base.AddAttributesToRender(writer, context); - var clickBinding = GetCommandBinding(ClickProperty); - if (clickBinding != null) + if (CreateClickScript() is {} clickScript) { - writer.AddAttribute("onclick", KnockoutHelper.GenerateClientPostBackScript( - nameof(Click), clickBinding, this, - new PostbackScriptOptions(commandArgs: BindingHelper.GetParametrizedCommandArgs(this, ClickArguments))), - append: true, appendSeparator: ";"); + writer.AddAttribute("onclick", clickScript, append: true, appendSeparator: ";"); } } diff --git a/src/Framework/Framework/Controls/ButtonBase.cs b/src/Framework/Framework/Controls/ButtonBase.cs index 7a15382d27..03bd417bec 100644 --- a/src/Framework/Framework/Controls/ButtonBase.cs +++ b/src/Framework/Framework/Controls/ButtonBase.cs @@ -79,6 +79,17 @@ public ButtonBase(string tagName) : base(tagName) { } + /// Creates the contents of `onclick` attribute. + protected virtual string? CreateClickScript() + { + var clickBinding = GetCommandBinding(ClickProperty); + if (clickBinding is null) + return null; + + return KnockoutHelper.GenerateClientPostBackScript( + nameof(Click), clickBinding, this, + new PostbackScriptOptions(commandArgs: BindingHelper.GetParametrizedCommandArgs(this, ClickArguments))); + } /// /// Adds all attributes that should be added to the control begin tag. @@ -107,5 +118,6 @@ public bool ValidateCommand(DotvvmProperty targetProperty) } return false; } + } } diff --git a/src/Framework/Framework/Controls/LinkButton.cs b/src/Framework/Framework/Controls/LinkButton.cs index f96bacb6bc..a15087a6e6 100644 --- a/src/Framework/Framework/Controls/LinkButton.cs +++ b/src/Framework/Framework/Controls/LinkButton.cs @@ -33,13 +33,9 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest base.AddAttributesToRender(writer, context); - var clickBinding = GetCommandBinding(ClickProperty); - if (clickBinding != null) + if (CreateClickScript() is {} clickScript) { - writer.AddAttribute("onclick", KnockoutHelper.GenerateClientPostBackScript( - nameof(Click), clickBinding, this, - new PostbackScriptOptions(commandArgs: BindingHelper.GetParametrizedCommandArgs(this, ClickArguments))), - append: true, appendSeparator: ";"); + writer.AddAttribute("onclick", clickScript, append: true, appendSeparator: ";"); } } From 30b93b59fe026539738625eb769607e639af56d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 1 Jun 2024 18:44:56 +0200 Subject: [PATCH 53/60] DataPager: Allow setting both Visible and HideWhenOnlyOnePage --- src/Framework/Framework/Controls/DataPager.cs | 14 +++++++++++--- src/Tests/ControlTests/DataPagerTests.cs | 13 +++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index f706f7a4fd..176cc391c4 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -230,14 +230,22 @@ protected virtual HtmlGenericControl CreateWrapperList() // If Visible property was set to something, it would be overwritten by this if (HideWhenOnlyOnePage && pagerBindings?.HasMoreThanOnePage is {} hasMoreThanOnePage) { - if (IsPropertySet(VisibleProperty)) - throw new Exception("Visible can't be set on a DataPager when HideWhenOnlyOnePage is true. You can wrap it in an element that hide that or set HideWhenOnlyOnePage to false"); - list.SetProperty(HtmlGenericControl.VisibleProperty, hasMoreThanOnePage); + list.SetProperty( + HtmlGenericControl.VisibleProperty, + new ValueOrBinding(hasMoreThanOnePage).And(GetValueOrBinding(VisibleProperty)) + ); + } + else + { + list.SetProperty(HtmlGenericControl.VisibleProperty, GetValueOrBinding(VisibleProperty)); } + return list; } + protected override void AddVisibleAttributeOrBinding(in RenderState r, IHtmlWriter writer) { } // handled by the wrapper list + protected virtual HtmlGenericControl CreatePageNumberButton(ValueOrBinding globalEnabled, DataPagerBindings pagerBindings, IDotvvmRequestContext context) { var liTemplate = new HtmlGenericControl("li"); diff --git a/src/Tests/ControlTests/DataPagerTests.cs b/src/Tests/ControlTests/DataPagerTests.cs index da1cba7fbb..385f3a3a9a 100644 --- a/src/Tests/ControlTests/DataPagerTests.cs +++ b/src/Tests/ControlTests/DataPagerTests.cs @@ -80,6 +80,17 @@ public async Task StaticCommandApendablePager() check.CheckString(r.FormattedHtml, fileExtension: "html"); } + [TestMethod] + public async Task HideableDataPager() + { + var r = await cth.RunPage(typeof(GridViewModel), """ + + """ + ); + + XAssert.Equal("visible: Customers()?.PagingOptions()?.PagesCount() > 1 && BooleanProperty", r.Html.QuerySelector("ul").GetAttribute("data-bind")); + } + public class GridViewModel: DotvvmViewModelBase { public GridViewDataSet Customers { get; set; } = new GridViewDataSet() @@ -90,6 +101,8 @@ public class GridViewModel: DotvvmViewModelBase }, }; + public bool BooleanProperty { get; set; } = true; + public override async Task PreRender() { if (Customers.IsRefreshRequired) From d94ca61481df19cf1aa13523bf96839708f70845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 1 Jun 2024 23:07:15 +0200 Subject: [PATCH 54/60] DataPager: fix UI tests DataPager now marks unavailable pages as `disabled` --- .../Tests/Tests/Control/DataPagerTests.cs | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/Samples/Tests/Tests/Control/DataPagerTests.cs b/src/Samples/Tests/Tests/Control/DataPagerTests.cs index a03ebe029e..aadf996cbb 100644 --- a/src/Samples/Tests/Tests/Control/DataPagerTests.cs +++ b/src/Samples/Tests/Tests/Control/DataPagerTests.cs @@ -1,4 +1,4 @@ - + using System.Collections.Generic; using System.Linq; using DotVVM.Samples.Tests.Base; @@ -53,21 +53,23 @@ public void Control_DataPager_DataPager_ActiveCssClass() RunInAllBrowsers(browser => { browser.NavigateToUrl(SamplesRouteUrls.ControlSamples_DataPager_DataPager); - // the first li should be visible because it contains text, the second with the link should be hidden var pageIndex1 = browser.First("#pager1").ElementAt("li", 2); AssertUI.NotContainsElement(pageIndex1, "a"); AssertUI.HasClass(pageIndex1, "active"); AssertUI.IsDisplayed(pageIndex1); + AssertUI.TextEquals(pageIndex1, "1"); var pageIndex2 = browser.First("#pager1").ElementAt("li", 3); AssertUI.ContainsElement(pageIndex2, "a"); - AssertUI.IsNotDisplayed(pageIndex2); + AssertUI.HasNotClass(pageIndex2, "active"); + AssertUI.TextEquals(pageIndex2, "»"); // the first li should note be there because only hyperlinks are rendered var pageIndex3 = browser.First("#pager3").ElementAt("li", 2); AssertUI.ContainsElement(pageIndex3, "a"); AssertUI.HasClass(pageIndex3, "active"); AssertUI.IsDisplayed(pageIndex3); + AssertUI.TextEquals(pageIndex3, "1"); }); } @@ -78,14 +80,20 @@ public void Control_DataPager_DataPager_DisabledAttribute() RunInAllBrowsers(browser => { browser.NavigateToUrl(SamplesRouteUrls.ControlSamples_DataPager_DataPager); + browser.Single("populate-button", this.SelectByDataUi).Click(); + // the first ul should not be disabled - AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 0), "disabled"); - AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 1), "disabled"); - AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 2), "disabled"); - AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 3), "disabled"); - AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 4), "disabled"); + AssertUI.HasNotClass(browser.Single("#pager1"), "disabled"); + AssertUI.HasAttribute(browser.Single("#pager1").ElementAt("li a", 0), "disabled"); // first + AssertUI.HasAttribute(browser.Single("#pager1").ElementAt("li a", 1), "disabled"); // prev + AssertUI.NotContainsElement(browser.Single("#pager1").ElementAt("li", 2), "a"); // 1 (current) + AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 2), "disabled"); // 2 + AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 3), "disabled"); // 3 + AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 4), "disabled"); // 4 + AssertUI.HasNotAttribute(browser.Single("#pager1").Last("li a"), "disabled"); // last page // the forth ul should be disabled + AssertUI.HasClass(browser.Single("#pager4"), "disabled"); AssertUI.HasAttribute(browser.Single("#pager4").ElementAt("li a", 0), "disabled"); AssertUI.HasAttribute(browser.Single("#pager4").ElementAt("li a", 1), "disabled"); AssertUI.HasAttribute(browser.Single("#pager4").ElementAt("li a", 2), "disabled"); @@ -94,6 +102,7 @@ public void Control_DataPager_DataPager_DisabledAttribute() // verify element is disabled after click browser.Single("#enableCheckbox input[type=checkbox]").Click(); + AssertUI.HasClass(browser.Single("#pager1"), "disabled"); AssertUI.HasAttribute(browser.Single("#pager1").ElementAt("li a", 0), "disabled"); AssertUI.HasAttribute(browser.Single("#pager1").ElementAt("li a", 1), "disabled"); AssertUI.HasAttribute(browser.Single("#pager1").ElementAt("li a", 2), "disabled"); @@ -102,11 +111,20 @@ public void Control_DataPager_DataPager_DisabledAttribute() // verify element is not disabled after another click browser.Single("#enableCheckbox input[type=checkbox]").Click(); - AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 0), "disabled"); - AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 1), "disabled"); + AssertUI.HasNotClass(browser.Single("#pager1"), "disabled"); + AssertUI.HasAttribute(browser.Single("#pager1").ElementAt("li a", 0), "disabled"); + AssertUI.HasAttribute(browser.Single("#pager1").ElementAt("li a", 1), "disabled"); AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 2), "disabled"); AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 3), "disabled"); AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 4), "disabled"); + + // go to page 2 + browser.Single("#pager1").ElementAt("li a", 2).Click(); + AssertUI.TextEquals(browser.Single("#pager1").Single("li.active"), "2"); + AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 0), "disabled"); + AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 1), "disabled"); + AssertUI.HasNotAttribute(browser.Single("#pager1").ElementAt("li a", 2), "disabled"); + AssertUI.HasNotAttribute(browser.Single("#pager1").Last("li a"), "disabled"); }); } From e71881a5b9180d47dedd548fce160e70acea61b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 1 Jun 2024 23:25:16 +0200 Subject: [PATCH 55/60] Button: unit test ClickArguments --- src/Framework/Testing/BindingTestHelper.cs | 19 ++++++++++++++++++ .../Runtime/DotvvmControlRenderedHtmlTests.cs | 20 +++++++++++++++++++ ...sCommand-Button_ClickArgumentsCommand.html | 1 + 3 files changed, 40 insertions(+) create mode 100644 src/Tests/Runtime/testoutputs/DotvvmControlRenderedHtmlTests.Button_ClickArgumentsCommand-Button_ClickArgumentsCommand.html diff --git a/src/Framework/Testing/BindingTestHelper.cs b/src/Framework/Testing/BindingTestHelper.cs index bf67a3d1e4..6c3c4f275d 100644 --- a/src/Framework/Testing/BindingTestHelper.cs +++ b/src/Framework/Testing/BindingTestHelper.cs @@ -152,6 +152,25 @@ public StaticCommandBindingExpression StaticCommand(string expression, DataConte }); } + /// Creates a command binding by parsing the specified expression. + /// Hierarchy of data contexts. First element is _root, last element is _this. + /// If specified, an implicit conversion into this type will be applied in the expression. + public CommandBindingExpression Command(string expression, Type[] contexts, Type? expectedType = null) => + Command(expression, CreateDataContext(contexts), expectedType); + + /// Creates a command binding by parsing the specified expression. + /// If specified, an implicit conversion into this type will be applied in the expression. + public CommandBindingExpression Command(string expression, DataContextStack context, Type? expectedType = null) + { + expectedType ??= typeof(Command); + return new CommandBindingExpression(BindingService, new object[] { + context, + new OriginalStringBindingProperty(expression), + BindingParserOptions.Value.AddImports(context.NamespaceImports).AddImports(Configuration.Markup.ImportedNamespaces), + new ExpectedTypeBindingProperty(expectedType) + }); + } + /// Creates a value binding by parsing the specified expression. The expression will be implicitly converted to /// Hierarchy of data contexts. First element is _root, last element is _this. /// Convert the result to this type instead of converting it to . The type must be assignable to T. diff --git a/src/Tests/Runtime/DotvvmControlRenderedHtmlTests.cs b/src/Tests/Runtime/DotvvmControlRenderedHtmlTests.cs index 1803241eb4..f23705aec7 100644 --- a/src/Tests/Runtime/DotvvmControlRenderedHtmlTests.cs +++ b/src/Tests/Runtime/DotvvmControlRenderedHtmlTests.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Threading.Tasks; +using CheckTestOutput; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Compilation.ControlTree; @@ -18,6 +20,8 @@ namespace DotVVM.Framework.Tests.Runtime [TestClass] public class DotvvmControlRenderedHtmlTests : DotvvmControlTestBase { + static readonly BindingTestHelper bindingHelper = new BindingTestHelper(); + readonly OutputChecker outputChecker = new OutputChecker("testoutputs"); [TestMethod] public void GridViewTextColumn_RenderedHtmlTest_ServerRendering() { @@ -211,6 +215,22 @@ public void Literal_DateTimeToBrowserLocalTime_RenderOnServer() }); } + [TestMethod] + public void Button_ClickArgumentsCommand() + { + var vm = new LiteralDateTimeViewModel(); + var command = bindingHelper.Command("null", [ typeof(LiteralDateTimeViewModel) ], typeof(Func)); + var button = new Button("text", command) { + ClickArguments = new object[] { + bindingHelper.ValueBinding("DateTime", [ typeof(LiteralDateTimeViewModel) ]), + 1 + } + }; + + var html = InvokeLifecycleAndRender(button, CreateContext(vm)); + outputChecker.CheckString(html, "Button_ClickArgumentsCommand", fileExtension: "html"); + } + public class OrderedDataBindTextBox : TextBox { diff --git a/src/Tests/Runtime/testoutputs/DotvvmControlRenderedHtmlTests.Button_ClickArgumentsCommand-Button_ClickArgumentsCommand.html b/src/Tests/Runtime/testoutputs/DotvvmControlRenderedHtmlTests.Button_ClickArgumentsCommand-Button_ClickArgumentsCommand.html new file mode 100644 index 0000000000..b9b51c393d --- /dev/null +++ b/src/Tests/Runtime/testoutputs/DotvvmControlRenderedHtmlTests.Button_ClickArgumentsCommand-Button_ClickArgumentsCommand.html @@ -0,0 +1 @@ + From be4e3a4e44d7a2ca2c9083df189bb4c21c1b9cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Thu, 20 Jun 2024 17:35:30 +0200 Subject: [PATCH 56/60] DataSets: review adjustments * rename GridViewCommands -> GridViewBindings * add some doccomments * moved SortCriterion to MultiCriteriaSortingOptions file --- .../Controls/IRefreshableGridViewDataSet.cs | 2 +- .../Core/Controls/Options/ISortingOptions.cs | 16 ------------ .../Options/MultiCriteriaSortingOptions.cs | 16 ++++++++++++ .../Framework/Controls/AppendableDataPager.cs | 25 +++++++++++++------ src/Framework/Framework/Controls/DataPager.cs | 6 ++++- .../Framework/Controls/DataPagerCommands.cs | 1 + src/Framework/Framework/Controls/GridView.cs | 14 +++++------ ...ridViewCommands.cs => GridViewBindings.cs} | 8 +++--- .../Framework/Controls/GridViewColumn.cs | 4 +-- .../GridViewDataSetBindingProvider.cs | 14 ++++++----- .../Controls/GridViewDataSetCommandType.cs | 8 +++++- src/Tests/ViewModel/GridViewDataSetTests.cs | 6 ++--- 12 files changed, 72 insertions(+), 48 deletions(-) rename src/Framework/Framework/Controls/{GridViewCommands.cs => GridViewBindings.cs} (76%) diff --git a/src/Framework/Core/Controls/IRefreshableGridViewDataSet.cs b/src/Framework/Core/Controls/IRefreshableGridViewDataSet.cs index bcd0fcf935..346185ceab 100644 --- a/src/Framework/Core/Controls/IRefreshableGridViewDataSet.cs +++ b/src/Framework/Core/Controls/IRefreshableGridViewDataSet.cs @@ -12,7 +12,7 @@ public interface IRefreshableGridViewDataSet : IBaseGridViewDataSet bool IsRefreshRequired { get; set; } /// - /// Requests to reload data into the . + /// Sets the flag to true. /// void RequestRefresh(); } diff --git a/src/Framework/Core/Controls/Options/ISortingOptions.cs b/src/Framework/Core/Controls/Options/ISortingOptions.cs index a2c48660cc..8b27b40161 100644 --- a/src/Framework/Core/Controls/Options/ISortingOptions.cs +++ b/src/Framework/Core/Controls/Options/ISortingOptions.cs @@ -41,20 +41,4 @@ public interface ISortingStateCapability : ISortingOptions /// bool IsColumnSortedDescending(string? sortExpression); } - - /// - /// Represents a sort criterion. - /// - public sealed record SortCriterion - { - /// - /// Gets or sets whether the sort order should be descending. - /// - public bool SortDescending { get; set; } - - /// - /// Gets or sets the name of the property that is used for sorting. Null means the grid should not be sorted. - /// - public string? SortExpression { get; set; } - } } diff --git a/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs b/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs index 248d37275a..6d9c1e82e1 100644 --- a/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs +++ b/src/Framework/Core/Controls/Options/MultiCriteriaSortingOptions.cs @@ -71,3 +71,19 @@ public virtual void SetSortExpression(string? sortExpression) /// Returns if the specified column is sorted in descending order in any of the public bool IsColumnSortedDescending(string? sortExpression) => Criteria.Any(c => c.SortExpression == sortExpression && c.SortDescending); } + +/// +/// Represents a sort criterion. +/// +public sealed record SortCriterion +{ + /// + /// Gets or sets whether the sort order should be descending. + /// + public bool SortDescending { get; set; } + + /// + /// Gets or sets the name of the property that is used for sorting. Null means the grid should not be sorted. May contain chained properties separated by dots. + /// + public string? SortExpression { get; set; } +} diff --git a/src/Framework/Framework/Controls/AppendableDataPager.cs b/src/Framework/Framework/Controls/AppendableDataPager.cs index e9dc0645c9..a246dbed9f 100644 --- a/src/Framework/Framework/Controls/AppendableDataPager.cs +++ b/src/Framework/Framework/Controls/AppendableDataPager.cs @@ -14,11 +14,16 @@ 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 + public class AppendableDataPager : HtmlGenericControl { private readonly GridViewDataSetBindingProvider gridViewDataSetBindingProvider; private readonly BindingCompilationService bindingService; + /// + /// Template displayed when more pages exist (not ) + /// The template should contain a button triggering the loading of more data, it may use the {staticCommand: _dataPager.Load()} binding to invoke the function. + /// When this template isn't set, the pager will automatically load the next page when it becomes visible on screen. + /// [MarkupOptions(AllowBinding = false, MappingMode = MappingMode.InnerElement)] [DataPagerApi.AddParameterDataContextChange("_dataPager")] public ITemplate? LoadTemplate @@ -29,6 +34,7 @@ public ITemplate? LoadTemplate public static readonly DotvvmProperty LoadTemplateProperty = DotvvmProperty.Register(c => c.LoadTemplate, null); + /// Template displayed when the next page is being loaded [MarkupOptions(AllowBinding = false, MappingMode = MappingMode.InnerElement)] [DataPagerApi.AddParameterDataContextChange("_dataPager")] public ITemplate? LoadingTemplate @@ -39,6 +45,7 @@ public ITemplate? LoadingTemplate public static readonly DotvvmProperty LoadingTemplateProperty = DotvvmProperty.Register(c => c.LoadingTemplate, null); + /// Template displayed when we are at the last page. [MarkupOptions(AllowBinding = false, MappingMode = MappingMode.InnerElement)] public ITemplate? EndTemplate { @@ -48,6 +55,7 @@ public ITemplate? EndTemplate public static readonly DotvvmProperty EndTemplateProperty = DotvvmProperty.Register(c => c.EndTemplate, null); + /// The data source GridViewDataSet (AppendableDataPager does not support plain collections) [MarkupOptions(Required = true, AllowHardCodedValue = false)] public IPageableGridViewDataSet DataSet { @@ -57,6 +65,11 @@ public IPageableGridViewDataSet DataSet public static readonly DotvvmProperty DataSetProperty = DotvvmProperty.Register(c => c.DataSet, null); + /// + /// Gets or sets the (static) command that will be used to load the next page. + /// It is recommended to use a staticCommand in AppendableDataPager. + /// The command accepts one argument of type and should return a new or . + /// [MarkupOptions(Required = true)] public ICommandBinding? LoadData { @@ -66,10 +79,8 @@ public ICommandBinding? LoadData public static readonly DotvvmProperty LoadDataProperty = DotvvmProperty.Register(nameof(LoadData)); - private DataPagerBindings? dataPagerCommands = null; - public AppendableDataPager(GridViewDataSetBindingProvider gridViewDataSetBindingProvider, BindingCompilationService bindingService) : base("div") { this.gridViewDataSetBindingProvider = gridViewDataSetBindingProvider; @@ -80,7 +91,7 @@ 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); + dataPagerCommands = gridViewDataSetBindingProvider.GetDataPagerBindings(this.GetDataContextType()!, dataSetBinding, commandType); if (LoadTemplate != null) { @@ -126,17 +137,17 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest p == GridViewDataSetBindingProvider.PostProcessorDelegate ? new CodeParameterAssignment("dotvvm.dataSet.postProcessors.append", OperatorPrecedence.Max) : default }); - + var binding = new KnockoutBindingGroup(); binding.Add("dataSet", dataSetBinding); binding.Add("loadNextPage", loadNextPage); binding.Add("autoLoadWhenInViewport", LoadTemplate is 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!"); + => GetValueBinding(DataSetProperty) ?? throw new DotvvmControlException(this, "The DataSet property of the dot:AppendableDataPager control must be set!"); } } diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index 176cc391c4..087dd0229f 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -126,6 +126,10 @@ public bool Enabled public static readonly DotvvmProperty EnabledProperty = DotvvmPropertyWithFallback.Register(nameof(Enabled), FormControls.EnabledProperty); + /// + /// Gets or sets the (static) command that will be triggered when the DataPager needs to load data (when navigating to different page). + /// The command accepts one argument of type and should return a new or . + /// public ICommandBinding? LoadData { get => (ICommandBinding?)GetValue(LoadDataProperty); @@ -164,7 +168,7 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) var commandType = LoadData is {} ? GridViewDataSetCommandType.LoadDataDelegate : GridViewDataSetCommandType.Default; - pagerBindings = gridViewDataSetBindingProvider.GetDataPagerCommands(this.GetDataContextType().NotNull(), dataSetBinding, commandType); + pagerBindings = gridViewDataSetBindingProvider.GetDataPagerBindings(this.GetDataContextType().NotNull(), dataSetBinding, commandType); var globalEnabled = GetValueOrBinding(EnabledProperty)!; diff --git a/src/Framework/Framework/Controls/DataPagerCommands.cs b/src/Framework/Framework/Controls/DataPagerCommands.cs index bd6c0163a7..7bf4f48147 100644 --- a/src/Framework/Framework/Controls/DataPagerCommands.cs +++ b/src/Framework/Framework/Controls/DataPagerCommands.cs @@ -4,6 +4,7 @@ namespace DotVVM.Framework.Controls { + /// Contains pre-created command and value bindings for the components. An instance can be obtained from public class DataPagerBindings { public ICommandBinding? GoToFirstPage { get; init; } diff --git a/src/Framework/Framework/Controls/GridView.cs b/src/Framework/Framework/Controls/GridView.cs index 813e06f045..bad12e48f3 100644 --- a/src/Framework/Framework/Controls/GridView.cs +++ b/src/Framework/Framework/Controls/GridView.cs @@ -167,8 +167,8 @@ public bool InlineEditing DotvvmProperty.Register(t => t.InlineEditing, false); /// - /// Gets or sets the command that will be triggered when the GridView needs to load data (e.g. when the sort order has changed). - /// The command accepts one argument of type GridViewDataSetOptions<TFilteringOptions, TSortingOptions, TPagingOptions> and should return the new GridViewDataSet. + /// Gets or sets the (static) command that will be triggered when the GridView needs to load data (e.g. when the sort order has changed). + /// The command accepts one argument of type and should return a new or . /// public ICommandBinding? LoadData { @@ -251,7 +251,7 @@ protected virtual void CreateHeaderRow(IDotvvmRequestContext context) var decoratedHeaderRow = Decorator.ApplyDecorators(headerRow, HeaderRowDecorators); head.Children.Add(decoratedHeaderRow); - var (gridViewCommands, sortCommandBindingOverride) = GetGridViewCommandsAndSortBinding(); + var (gridViewBindings, sortCommandBindingOverride) = GetGridViewCommandsAndSortBinding(); foreach (var column in Columns.NotNull("GridView.Columns must be set")) { var cell = new HtmlGenericControl("th"); @@ -259,7 +259,7 @@ protected virtual void CreateHeaderRow(IDotvvmRequestContext context) var decoratedCell = Decorator.ApplyDecorators(cell, column.HeaderCellDecorators); headerRow.Children.Add(decoratedCell); - column.CreateHeaderControls(context, this, gridViewCommands, sortCommandBindingOverride, cell, gridViewDataSet); + column.CreateHeaderControls(context, this, gridViewBindings, sortCommandBindingOverride, cell, gridViewDataSet); if (FilterPlacement == GridViewFilterPlacement.HeaderRow) { column.CreateFilterControls(context, this, cell, gridViewDataSet); @@ -280,7 +280,7 @@ protected virtual void CreateHeaderRow(IDotvvmRequestContext context) } } - private (GridViewCommands gridViewCommands, ICommandBinding? sortCommandBindingOverride) GetGridViewCommandsAndSortBinding() + private (GridViewBindings gridViewBindings, ICommandBinding? sortCommandBindingOverride) GetGridViewCommandsAndSortBinding() { if (SortChanged is { } && LoadData is { }) { @@ -289,9 +289,9 @@ protected virtual void CreateHeaderRow(IDotvvmRequestContext context) var dataContextStack = this.GetDataContextType()!; var commandType = LoadData is { } ? GridViewDataSetCommandType.LoadDataDelegate : GridViewDataSetCommandType.Default; - var gridViewCommands = gridViewDataSetBindingProvider.GetGridViewCommands(dataContextStack, GetDataSourceBinding(), commandType); + var gridViewBindings = gridViewDataSetBindingProvider.GetGridViewBindings(dataContextStack, GetDataSourceBinding(), commandType); - return (gridViewCommands, SortChanged is { } ? BuildDefaultSortCommandBinding() : null); + return (gridViewBindings, SortChanged is { } ? BuildDefaultSortCommandBinding() : null); } protected virtual ICommandBinding BuildDefaultSortCommandBinding() diff --git a/src/Framework/Framework/Controls/GridViewCommands.cs b/src/Framework/Framework/Controls/GridViewBindings.cs similarity index 76% rename from src/Framework/Framework/Controls/GridViewCommands.cs rename to src/Framework/Framework/Controls/GridViewBindings.cs index 38e28081dc..186352857e 100644 --- a/src/Framework/Framework/Controls/GridViewCommands.cs +++ b/src/Framework/Framework/Controls/GridViewBindings.cs @@ -4,11 +4,11 @@ namespace DotVVM.Framework.Controls { - /// Contains pre-created data bindings for the components. Instance can obtained from - public class GridViewCommands + /// Contains pre-created command and value bindings for the components. An instance can be obtained from + public class GridViewBindings { - private readonly ConcurrentDictionary> isSortColumnAscending = new(); - private readonly ConcurrentDictionary> isSortColumnDescending = new(); + private readonly ConcurrentDictionary> isSortColumnAscending = new(concurrencyLevel: 1, capacity: 16); + private readonly ConcurrentDictionary> isSortColumnDescending = new(concurrencyLevel: 1, capacity: 16); public ICommandBinding? SetSortExpression { get; init; } diff --git a/src/Framework/Framework/Controls/GridViewColumn.cs b/src/Framework/Framework/Controls/GridViewColumn.cs index 9a48f10af9..3a3560df6e 100644 --- a/src/Framework/Framework/Controls/GridViewColumn.cs +++ b/src/Framework/Framework/Controls/GridViewColumn.cs @@ -189,7 +189,7 @@ public virtual void CreateEditControls(IDotvvmRequestContext context, DotvvmCont EditTemplate.BuildContent(context, container); } - public virtual void CreateHeaderControls(IDotvvmRequestContext context, GridView gridView, GridViewCommands gridViewCommands, ICommandBinding? sortCommandBindingOverride, HtmlGenericControl cell, IGridViewDataSet? gridViewDataSet) + public virtual void CreateHeaderControls(IDotvvmRequestContext context, GridView gridView, GridViewBindings gridViewCommands, ICommandBinding? sortCommandBindingOverride, HtmlGenericControl cell, IGridViewDataSet? gridViewDataSet) { if (HeaderTemplate != null) { @@ -240,7 +240,7 @@ public virtual void CreateFilterControls(IDotvvmRequestContext context, GridView } } - private void SetSortedCssClass(HtmlGenericControl cell, ISortableGridViewDataSet? gridViewDataSet, GridViewCommands gridViewCommands) + private void SetSortedCssClass(HtmlGenericControl cell, ISortableGridViewDataSet? gridViewDataSet, GridViewBindings gridViewCommands) { if (gridViewDataSet is ISortableGridViewDataSet sortableGridViewDataSet && GetSortExpression() is {} sortExpression) diff --git a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs index e9da10f422..99b9490a5c 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -23,21 +23,23 @@ public class GridViewDataSetBindingProvider private readonly BindingCompilationService service; private readonly ConcurrentDictionary<(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType), DataPagerBindings> dataPagerCommands = new(); - private readonly ConcurrentDictionary<(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType), GridViewCommands> gridViewCommands = new(); + private readonly ConcurrentDictionary<(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType), GridViewBindings> gridViewCommands = new(); public GridViewDataSetBindingProvider(BindingCompilationService service) { this.service = service; } - public DataPagerBindings GetDataPagerCommands(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType) + /// Returns pre-created DataPager bindings for a given data context and data source (result is cached). + public DataPagerBindings GetDataPagerBindings(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType) { return dataPagerCommands.GetOrAdd((dataContextStack, dataSetBinding, commandType), x => GetDataPagerCommandsCore(x.dataContextStack, x.dataSetBinding, x.commandType)); } - public GridViewCommands GetGridViewCommands(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType) + /// Returns pre-created GridView bindings for a given data context and data source (result is cached). + public GridViewBindings GetGridViewBindings(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType) { - return gridViewCommands.GetOrAdd((dataContextStack, dataSetBinding, commandType), x => GetGridViewCommandsCore(x.dataContextStack, x.dataSetBinding, x.commandType)); + return gridViewCommands.GetOrAdd((dataContextStack, dataSetBinding, commandType), x => GetGridViewBindingsCore(x.dataContextStack, x.dataSetBinding, x.commandType)); } private DataPagerBindings GetDataPagerCommandsCore(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType) @@ -130,7 +132,7 @@ ParameterExpression CreateParameter(DataContextStack dataContextStack, string na }; } - private GridViewCommands GetGridViewCommandsCore(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType) + private GridViewBindings GetGridViewBindingsCore(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType) { var dataSetExpr = dataSetBinding.GetProperty().Expression; ICommandBinding? GetCommandOrNull(DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression) @@ -147,7 +149,7 @@ private GridViewCommands GetGridViewCommandsCore(DataContextStack dataContextSta } var setSortExpressionParam = Expression.Parameter(typeof(string), "_sortExpression"); - return new GridViewCommands() + return new GridViewBindings() { SetSortExpression = GetCommandOrNull>( dataContextStack, diff --git a/src/Framework/Framework/Controls/GridViewDataSetCommandType.cs b/src/Framework/Framework/Controls/GridViewDataSetCommandType.cs index 49fd31c317..54f3dd55d1 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetCommandType.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetCommandType.cs @@ -2,7 +2,13 @@ namespace DotVVM.Framework.Controls { public enum GridViewDataSetCommandType { + /// + /// The will create command bindings which set the sorting/paging options and let the user handle data loading in the PreRender phase. + /// Default, + /// + /// The commands returned by utilize the LoadData function provided in the JS parameter. + /// LoadDataDelegate } -} \ No newline at end of file +} diff --git a/src/Tests/ViewModel/GridViewDataSetTests.cs b/src/Tests/ViewModel/GridViewDataSetTests.cs index e812d8daea..d9994475e1 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.Default); + var commands = commandProvider.GetDataPagerBindings(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.Default); + var commands = commandProvider.GetGridViewBindings(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.LoadDataDelegate); + var commands = commandProvider.GetDataPagerBindings(dataContextStack, dataSetBinding, GridViewDataSetCommandType.LoadDataDelegate); var goToFirstPage = CompileBinding(commands.GoToFirstPage); Console.WriteLine(goToFirstPage); From 8c433d6080c823b4e2e68a4f814f49e2ad317db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20=C5=A0est=C3=A1k?= Date: Sun, 7 Jul 2024 21:18:45 +0100 Subject: [PATCH 57/60] Added authorization check for tests dependent on GitHub API --- .../Tests/Tests/Feature/DataSetTests.cs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Samples/Tests/Tests/Feature/DataSetTests.cs b/src/Samples/Tests/Tests/Feature/DataSetTests.cs index 26c8c0e681..681e9ae984 100644 --- a/src/Samples/Tests/Tests/Feature/DataSetTests.cs +++ b/src/Samples/Tests/Tests/Feature/DataSetTests.cs @@ -1,11 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Linq; using DotVVM.Samples.Tests.Base; using DotVVM.Testing.Abstractions; +using OpenQA.Selenium; using Riganti.Selenium.Core; +using Riganti.Selenium.Core.Abstractions; using Xunit; using Xunit.Abstractions; @@ -22,9 +20,9 @@ public DataSetTests(ITestOutputHelper output) : base(output) [InlineData(SamplesRouteUrls.FeatureSamples_DataSet_GitHubApiStaticCommands)] public void Feature_DataSet_GitHubApi_Next(string url) { - RunInAllBrowsers(browser => - { + RunInAllBrowsers(browser => { browser.NavigateToUrl(url); + VerifyPageIsNotThrowingAuthError(browser); var grid = browser.Single("next-grid", SelectByDataUi); var pager = browser.Single("next-pager", SelectByDataUi); @@ -49,6 +47,7 @@ public void Feature_DataSet_GitHubApi_NextHistory(string url) { RunInAllBrowsers(browser => { browser.NavigateToUrl(url); + VerifyPageIsNotThrowingAuthError(browser); var grid = browser.Single("next-history-grid", SelectByDataUi); var pager = browser.Single("next-history-pager", SelectByDataUi); @@ -82,5 +81,14 @@ public void Feature_DataSet_GitHubApi_NextHistory(string url) AssertUI.TextEquals(grid.ElementAt("tbody tr td", 0), issueId3); }); } + private void VerifyPageIsNotThrowingAuthError(IBrowserWrapper browser) + { + var elms = browser.FindElements(".exceptionMessage", By.CssSelector); + var errorMessage = elms.FirstOrDefault(); + + if (errorMessage is null) return; + AssertUI.Text(errorMessage, e => !e.Contains("401"), "GitHub Authentication Failed!", + waitForOptions: WaitForOptions.FromTimeout(1000)); + } } } From 6cab06d350cbbc64e63fc358ed6ea66018a29970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Fri, 12 Jul 2024 17:39:56 +0200 Subject: [PATCH 58/60] Updated GitHub action --- .github/uitest/action.yml | 2 ++ .github/uitest/uitest.ps1 | 13 +++++++++++- .../DotVVM.Samples.BasicSamples.Owin.csproj | 1 + .../Owin/Properties/launchSettings.json | 4 ++-- src/Samples/Owin/Web.config | 20 +++++++++++++++---- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/.github/uitest/action.yml b/.github/uitest/action.yml index bbd0d05023..4cc0217567 100644 --- a/.github/uitest/action.yml +++ b/.github/uitest/action.yml @@ -37,6 +37,7 @@ runs: shell: bash env: DOTVVM_SAMPLES_CONFIG_PROFILE: ${{ inputs.samples-config }} + GITHUB_TOKEN: ${{ inputs.github-token }} - if: ${{ runner.os == 'Windows' }} run: choco install dotnet-aspnetcoremodule-v2 -y @@ -53,6 +54,7 @@ runs: shell: pwsh env: DOTVVM_SAMPLES_CONFIG_PROFILE: ${{ inputs.samples-config }} + GITHUB_TOKEN: ${{ inputs.github-token }} # publish the result to github - uses: ./.github/test-report diff --git a/.github/uitest/uitest.ps1 b/.github/uitest/uitest.ps1 index 3a1b990223..579c4b8e62 100644 --- a/.github/uitest/uitest.ps1 +++ b/.github/uitest/uitest.ps1 @@ -110,6 +110,8 @@ function Start-Sample { [int][parameter(Position = 2)]$port ) Invoke-RequiredCmds "Start sample '$sampleName'" { + Reset-IISServerManager -Confirm:$false + Remove-IISSite -Confirm:$false -Name $sampleName -ErrorAction SilentlyContinue icacls "$root\artifacts\" /grant "IIS_IUSRS:(OI)(CI)F" @@ -117,7 +119,7 @@ function Start-Sample { New-IISSite -Name "$sampleName" ` -PhysicalPath "$path" ` -BindingInformation "*:${port}:" - + # ensure IIS created the site while ($true) { $state = (Get-IISSite -Name $sampleName).State @@ -131,6 +133,15 @@ function Start-Sample { throw "Site '${sampleName}' could not be started. State: '${state}'." } } + + # add or update environment variable to the application pool + $existingEnvVar = c:\windows\system32\inetsrv\appcmd.exe list config -section:system.applicationHost/applicationPools ` + | out-string ` + | select-xml -XPath "//add[@name='DefaultAppPool']/environmentVariables/add[@name='GITHUB_TOKEN']" + if ($existingEnvVar -ne $null) { + c:\windows\system32\inetsrv\appcmd.exe set config -section:system.applicationHost/applicationPools /-"[name='DefaultAppPool'].environmentVariables.[name='GITHUB_TOKEN']" /commit:apphost + } + c:\windows\system32\inetsrv\appcmd.exe set config -section:system.applicationHost/applicationPools /+"[name='DefaultAppPool'].environmentVariables.[name='GITHUB_TOKEN',value='$env:GITHUB_TOKEN']" /commit:apphost } } diff --git a/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj b/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj index 1b85ac7325..7a1472bcfa 100644 --- a/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj +++ b/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj @@ -39,5 +39,6 @@ + diff --git a/src/Samples/Owin/Properties/launchSettings.json b/src/Samples/Owin/Properties/launchSettings.json index 00ab196b05..5083b9d1b0 100644 --- a/src/Samples/Owin/Properties/launchSettings.json +++ b/src/Samples/Owin/Properties/launchSettings.json @@ -3,8 +3,8 @@ "DotVVM.Samples.BasicSamples.Owin": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "http://localhost:5407", - "applicationUrl": "http://localhost:5407", + "launchUrl": "http://localhost:65481/", + "applicationUrl": "http://localhost:65481/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Samples/Owin/Web.config b/src/Samples/Owin/Web.config index b5f4f6e328..e7ffd97b9e 100644 --- a/src/Samples/Owin/Web.config +++ b/src/Samples/Owin/Web.config @@ -25,13 +25,13 @@ - + - + @@ -49,9 +49,21 @@ - + + + + + + + + + + + + + - + \ No newline at end of file From 815700fe4eb69486db34c2f6cf484eb403d29022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 14 Jul 2024 10:17:57 +0200 Subject: [PATCH 59/60] Fixed stale elements in tests --- .../Tests/Tests/Feature/DataSetTests.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Samples/Tests/Tests/Feature/DataSetTests.cs b/src/Samples/Tests/Tests/Feature/DataSetTests.cs index 681e9ae984..dad3c3b894 100644 --- a/src/Samples/Tests/Tests/Feature/DataSetTests.cs +++ b/src/Samples/Tests/Tests/Feature/DataSetTests.cs @@ -31,11 +31,17 @@ public void Feature_DataSet_GitHubApi_Next(string url) var issueId = grid.ElementAt("tbody tr td", 0).GetInnerText(); // go next - pager.ElementAt("li", 1).Single("a").Click().Wait(500); + pager.ElementAt("li", 1).Single("a").Click().Wait(1000); + + grid = browser.Single("next-grid", SelectByDataUi); + pager = browser.Single("next-pager", SelectByDataUi); AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId); // go to first page - pager.ElementAt("li", 0).Single("a").Click().Wait(500); + pager.ElementAt("li", 0).Single("a").Click().Wait(1000); + + grid = browser.Single("next-grid", SelectByDataUi); + pager = browser.Single("next-pager", SelectByDataUi); AssertUI.TextEquals(grid.ElementAt("tbody tr td", 0), issueId); }); } @@ -57,27 +63,42 @@ public void Feature_DataSet_GitHubApi_NextHistory(string url) // go to page 2 pager.ElementAt("li", 3).Single("a").Click().Wait(500); + + grid = browser.Single("next-grid", SelectByDataUi); + pager = browser.Single("next-pager", SelectByDataUi); AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId1); var issueId2 = grid.ElementAt("tbody tr td", 0).GetInnerText(); // go to next page pager.ElementAt("li", 5).Single("a").Click().Wait(500); + + grid = browser.Single("next-grid", SelectByDataUi); + pager = browser.Single("next-pager", SelectByDataUi); AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId1); AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId2); var issueId3 = grid.ElementAt("tbody tr td", 0).GetInnerText(); // go to first page pager.ElementAt("li", 0).Single("a").Click().Wait(500); + + grid = browser.Single("next-grid", SelectByDataUi); + pager = browser.Single("next-pager", SelectByDataUi); AssertUI.TextEquals(grid.ElementAt("tbody tr td", 0), issueId1); // go to page 4 pager.ElementAt("li", 5).Single("a").Click().Wait(500); + + grid = browser.Single("next-grid", SelectByDataUi); + pager = browser.Single("next-pager", SelectByDataUi); AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId1); AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId2); AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId3); // go to previous page pager.ElementAt("li", 1).Single("a").Click().Wait(500); + + grid = browser.Single("next-grid", SelectByDataUi); + pager = browser.Single("next-pager", SelectByDataUi); AssertUI.TextEquals(grid.ElementAt("tbody tr td", 0), issueId3); }); } From d209e2adb764e4a1394aca6d3757c81a4e804227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 14 Jul 2024 21:55:57 +0200 Subject: [PATCH 60/60] Fixed GitHub tests --- .../Tests/Tests/Feature/DataSetTests.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Samples/Tests/Tests/Feature/DataSetTests.cs b/src/Samples/Tests/Tests/Feature/DataSetTests.cs index dad3c3b894..9273030ad0 100644 --- a/src/Samples/Tests/Tests/Feature/DataSetTests.cs +++ b/src/Samples/Tests/Tests/Feature/DataSetTests.cs @@ -64,16 +64,16 @@ public void Feature_DataSet_GitHubApi_NextHistory(string url) // go to page 2 pager.ElementAt("li", 3).Single("a").Click().Wait(500); - grid = browser.Single("next-grid", SelectByDataUi); - pager = browser.Single("next-pager", SelectByDataUi); + grid = browser.Single("next-history-grid", SelectByDataUi); + pager = browser.Single("next-history-pager", SelectByDataUi); AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId1); var issueId2 = grid.ElementAt("tbody tr td", 0).GetInnerText(); // go to next page pager.ElementAt("li", 5).Single("a").Click().Wait(500); - grid = browser.Single("next-grid", SelectByDataUi); - pager = browser.Single("next-pager", SelectByDataUi); + grid = browser.Single("next-history-grid", SelectByDataUi); + pager = browser.Single("next-history-pager", SelectByDataUi); AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId1); AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId2); var issueId3 = grid.ElementAt("tbody tr td", 0).GetInnerText(); @@ -81,15 +81,15 @@ public void Feature_DataSet_GitHubApi_NextHistory(string url) // go to first page pager.ElementAt("li", 0).Single("a").Click().Wait(500); - grid = browser.Single("next-grid", SelectByDataUi); - pager = browser.Single("next-pager", SelectByDataUi); + grid = browser.Single("next-history-grid", SelectByDataUi); + pager = browser.Single("next-history-pager", SelectByDataUi); AssertUI.TextEquals(grid.ElementAt("tbody tr td", 0), issueId1); // go to page 4 pager.ElementAt("li", 5).Single("a").Click().Wait(500); - grid = browser.Single("next-grid", SelectByDataUi); - pager = browser.Single("next-pager", SelectByDataUi); + grid = browser.Single("next-history-grid", SelectByDataUi); + pager = browser.Single("next-history-pager", SelectByDataUi); AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId1); AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId2); AssertUI.TextNotEquals(grid.ElementAt("tbody tr td", 0), issueId3); @@ -97,8 +97,8 @@ public void Feature_DataSet_GitHubApi_NextHistory(string url) // go to previous page pager.ElementAt("li", 1).Single("a").Click().Wait(500); - grid = browser.Single("next-grid", SelectByDataUi); - pager = browser.Single("next-pager", SelectByDataUi); + grid = browser.Single("next-history-grid", SelectByDataUi); + pager = browser.Single("next-history-pager", SelectByDataUi); AssertUI.TextEquals(grid.ElementAt("tbody tr td", 0), issueId3); }); }