diff --git a/src/Framework/Core/Controls/GenericGridViewDataSet.cs b/src/Framework/Core/Controls/GenericGridViewDataSet.cs index bb5867b845..041dd849ee 100644 --- a/src/Framework/Core/Controls/GenericGridViewDataSet.cs +++ b/src/Framework/Core/Controls/GenericGridViewDataSet.cs @@ -29,7 +29,7 @@ public class GenericGridViewDataSet< /// /// Gets or sets the items for the current page. /// - public IList Items { get; set; } = new List(); + public virtual IList Items { get; set; } = new List(); /// /// Gets or sets whether the data should be refreshed. This property is set to true automatically diff --git a/src/Framework/Core/Controls/ServerSideGridViewDataSet.cs b/src/Framework/Core/Controls/ServerSideGridViewDataSet.cs new file mode 100644 index 0000000000..8723ec5d42 --- /dev/null +++ b/src/Framework/Core/Controls/ServerSideGridViewDataSet.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using DotVVM.Framework.ViewModel; + +namespace DotVVM.Framework.Controls +{ + /// + /// Represents a collection of items with paging and sorting which keeps the Items collection server-side (Bind(Direction.None)) and only transfers the necessary metadata (page index, sort direction). + /// + /// The type of the elements in the collection. + public class ServerSideGridViewDataSet() + : GenericGridViewDataSet(new(), new(), new(), new(), new()) + { + + [Bind(Direction.None)] + public override IList Items { get => base.Items; set => base.Items = value; } + // return specialized dataset options + public new GridViewDataSetOptions GetOptions() + { + return new GridViewDataSetOptions { + FilteringOptions = FilteringOptions, + SortingOptions = SortingOptions, + PagingOptions = PagingOptions + }; + } + } +} diff --git a/src/Framework/Framework/Binding/BindingPropertyException.cs b/src/Framework/Framework/Binding/BindingPropertyException.cs index ba813c89f4..b29da4449f 100644 --- a/src/Framework/Framework/Binding/BindingPropertyException.cs +++ b/src/Framework/Framework/Binding/BindingPropertyException.cs @@ -38,7 +38,7 @@ static string GetMessage(IBinding binding, Type[] properties, string? message, E var introMsg = isRequiredProperty ? - $"Could not initialize binding as it is missing a required property {properties[0].Name}" : + $"Could not initialize binding '{binding}' as it is missing a required property {properties[0].Name}" : $"Unable to get property {properties[0].Name}"; var pathMsg = ""; diff --git a/src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs b/src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs index be11c6556a..d564a54476 100644 --- a/src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs +++ b/src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs @@ -56,7 +56,8 @@ public ValueBindingExpression CreateValueBinding(string code, new ValueBindingExpression(compilationService, new object?[] { dataContext, new OriginalStringBindingProperty(code), - parserOptions + parserOptions, + new ExpectedTypeBindingProperty(typeof(TResult)) })); } @@ -78,7 +79,8 @@ public ResourceBindingExpression CreateResourceBinding(string new ResourceBindingExpression(compilationService, new object?[] { dataContext, new OriginalStringBindingProperty(code), - parserOptions + parserOptions, + new ExpectedTypeBindingProperty(typeof(TResult)) })); } @@ -100,7 +102,8 @@ public CommandBindingExpression CreateCommand(string code, Dat new CommandBindingExpression(compilationService, new object?[] { dataContext, new OriginalStringBindingProperty(code), - parserOptions + parserOptions, + new ExpectedTypeBindingProperty(typeof(TResult)) })); } @@ -122,7 +125,8 @@ public StaticCommandBindingExpression CreateStaticCommand(stri new StaticCommandBindingExpression(compilationService, new object?[] { dataContext, new OriginalStringBindingProperty(code), - parserOptions + parserOptions, + new ExpectedTypeBindingProperty(typeof(TResult)) })); } diff --git a/src/Framework/Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs b/src/Framework/Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs index c37fbdd3eb..75c7da5ea8 100644 --- a/src/Framework/Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs +++ b/src/Framework/Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs @@ -430,7 +430,6 @@ public CollectionElementDataContextBindingProperty GetCollectionElementDataConte return new CollectionElementDataContextBindingProperty(DataContextStack.CreateCollectionElement( ReflectionUtils.GetEnumerableType(resultType.Type).NotNull(), parent: dataContext, - extensionParameters: new CollectionElementDataContextChangeAttribute(order: 0).GetExtensionParameters(new ResolvedTypeDescriptor(dataContext.DataContextType)).ToArray(), serverSideOnly: binding is not IValueBinding )); } diff --git a/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs b/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs index 8420ee3d5b..63bf1031d7 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs @@ -44,6 +44,16 @@ private DataContextStack(Type type, BindingPropertyResolvers = bindingPropertyResolvers?.ToImmutableArray() ?? ImmutableArray.Empty; ServerSideOnly = serverSideOnly; + if (ExtensionParameters.Length >= 2) + { + var set = new HashSet(StringComparer.Ordinal); + foreach (var p in ExtensionParameters) + { + if (!set.Add(p.Identifier)) + throw new Exception($"Extension parameter '{p.Identifier}' is defined multiple times in the data context stack {this}"); + } + } + hashCode = ComputeHashCode(); } diff --git a/src/Framework/Framework/Controls/CheckBox.cs b/src/Framework/Framework/Controls/CheckBox.cs index 0047b809c3..bc36fa34d5 100644 --- a/src/Framework/Framework/Controls/CheckBox.cs +++ b/src/Framework/Framework/Controls/CheckBox.cs @@ -30,7 +30,7 @@ public bool? Checked } public static readonly DotvvmProperty CheckedProperty = - DotvvmProperty.Register(t => t.Checked, false); + DotvvmProperty.Register(t => t.Checked, null); /// /// Gets or sets a collection of values of all checked checkboxes. Use this property in combination with the CheckedValue property. @@ -60,12 +60,14 @@ public bool DisableIndeterminate /// protected override void RenderInputTag(IHtmlWriter writer) { - if (HasValueBinding(CheckedProperty) && !HasValueBinding(CheckedItemsProperty)) + var checkedValue = GetValueRaw(CheckedProperty); + var checkedItemsValue = GetValueRaw(CheckedItemsProperty); + if (checkedValue is {} && checkedItemsValue is null) { // boolean mode - RenderCheckedProperty(writer); + RenderCheckedProperty(writer, checkedValue); } - else if (!HasValueBinding(CheckedProperty) && HasValueBinding(CheckedItemsProperty)) + else if (checkedValue is null && checkedItemsValue is {}) { // collection mode RenderCheckedItemsProperty(writer); @@ -121,18 +123,26 @@ protected virtual void RenderCheckedItemsBinding(IHtmlWriter writer) writer.AddKnockoutDataBind("dotvvm-checkedItems", checkedItemsBinding!.GetKnockoutBindingExpression(this)); } - protected virtual void RenderCheckedProperty(IHtmlWriter writer) + protected virtual void RenderCheckedProperty(IHtmlWriter writer, object? checkedValue) { - var checkedBinding = GetValueBinding(CheckedProperty); - - // dotvvm-CheckState sets elements to indeterminate state when checkedBinding is null, - // knockout's default checked binding does not do that - var bindingHandler = DisableIndeterminate ? "checked" : "dotvvm-CheckState"; - writer.AddKnockoutDataBind(bindingHandler, checkedBinding!, this); - - // Boolean mode can have prerendered `checked` attribute - if (RenderOnServer && true.Equals(GetValue(CheckedProperty))) - writer.AddAttribute("checked", null); + if (checkedValue is IValueBinding checkedBinding) + { + // dotvvm-CheckState sets elements to indeterminate state when checkedBinding is null, + // knockout's default checked binding does not do that + var bindingHandler = DisableIndeterminate ? "checked" : "dotvvm-CheckState"; + writer.AddKnockoutDataBind(bindingHandler, checkedBinding!, this); + + // Boolean mode can have prerendered `checked` attribute + if (RenderOnServer && KnockoutHelper.TryEvaluateValueBinding(this, checkedBinding) is true) + writer.AddAttribute("checked", null); + } + else + { + if ((bool?)EvalPropertyValue(CheckedProperty, checkedValue) is true) + { + writer.AddAttribute("checked", null); + } + } } diff --git a/src/Framework/Framework/Controls/DataPager.cs b/src/Framework/Framework/Controls/DataPager.cs index 087dd0229f..32e2d7781b 100644 --- a/src/Framework/Framework/Controls/DataPager.cs +++ b/src/Framework/Framework/Controls/DataPager.cs @@ -163,7 +163,7 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context) { Children.Clear(); - var dataSetBinding = GetValueBinding(DataSetProperty)!; + var dataSetBinding = GetDataSetBinding(); var dataSetType = dataSetBinding.ResultType; var commandType = LoadData is {} ? GridViewDataSetCommandType.LoadDataDelegate : GridViewDataSetCommandType.Default; @@ -318,8 +318,9 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest if (this.LoadData is {} loadData) { + var dataSetBinding = GetDataSetBinding() as IValueBinding ?? throw new DotvvmControlException(this, "The DataSet property must set to a value binding when LoadData command is used!"); var helperBinding = new KnockoutBindingGroup(); - helperBinding.Add("dataSet", GetDataSetBinding().GetKnockoutBindingExpression(this, unwrapped: true)); + helperBinding.Add("dataSet", dataSetBinding.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()); @@ -356,8 +357,7 @@ 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!"); + private IStaticValueBinding GetDataSetBinding() + => GetValueOrResourceBinding(DataSetProperty) ?? throw new DotvvmControlException(this, "The DataSet property of the dot:DataPager control must be set!"); } } diff --git a/src/Framework/Framework/Controls/DotvvmBindableObject.cs b/src/Framework/Framework/Controls/DotvvmBindableObject.cs index 055d438e61..4aae2967af 100644 --- a/src/Framework/Framework/Controls/DotvvmBindableObject.cs +++ b/src/Framework/Framework/Controls/DotvvmBindableObject.cs @@ -223,6 +223,22 @@ public void SetValueRaw(DotvvmProperty property, object? value) return binding as IValueBinding; } + /// + /// Gets the value binding set to a specified property. Returns null if the property is not a binding, throws if the binding some kind of command. + /// + public IStaticValueBinding? GetValueOrResourceBinding(DotvvmProperty property, bool inherit = true) + { + var binding = GetBinding(property, inherit); + if (binding is null) + return null; + if (binding is not IStaticValueBinding valueBinding) + { + // throw exception on incompatible binding types + throw new BindingHelper.BindingNotSupportedException(binding) { RelatedControl = this }; + } + return valueBinding; + } + /// Returns a Javascript (knockout) expression representing value or binding of this property. public ParametrizedCode GetJavascriptValue(DotvvmProperty property, bool inherit = true) => GetValueOrBinding(property, inherit).GetParametrizedJsExpression(this); diff --git a/src/Framework/Framework/Controls/GridView.cs b/src/Framework/Framework/Controls/GridView.cs index 3824748434..05d8024bb6 100644 --- a/src/Framework/Framework/Controls/GridView.cs +++ b/src/Framework/Framework/Controls/GridView.cs @@ -332,15 +332,9 @@ private static void SetCellAttributes(GridViewColumn column, HtmlGenericControl if (!isHeaderCell) { - var cssClassBinding = column.GetValueBinding(GridViewColumn.CssClassProperty); - if (cssClassBinding != null) - { - cellAttributes["class"] = cssClassBinding; - } - else if (!string.IsNullOrWhiteSpace(column.CssClass)) - { - cellAttributes["class"] = column.CssClass; - } + if (column.GetValueRaw(GridViewColumn.CssClassProperty) is {} cssClassBinding && + cssClassBinding is not "") + cellAttributes.Set("class", cssClassBinding); } else { diff --git a/src/Framework/Framework/Controls/GridViewCheckBoxColumn.cs b/src/Framework/Framework/Controls/GridViewCheckBoxColumn.cs index c67a92f6b3..81ba6dde4a 100644 --- a/src/Framework/Framework/Controls/GridViewCheckBoxColumn.cs +++ b/src/Framework/Framework/Controls/GridViewCheckBoxColumn.cs @@ -1,4 +1,5 @@ using DotVVM.Framework.Binding; +using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Binding.Properties; using DotVVM.Framework.Hosting; @@ -14,13 +15,13 @@ public class GridViewCheckBoxColumn : GridViewColumn /// Gets or sets a binding which retrieves the value to display from the current data item. /// [MarkupOptions(AllowHardCodedValue = false, Required = true)] - public bool ValueBinding + public IStaticValueBinding ValueBinding { - get { return (bool)GetValue(ValueBindingProperty)!; } + get { return (IStaticValueBinding)GetValueRaw(ValueBindingProperty)!; } set { SetValue(ValueBindingProperty, value); } } public static readonly DotvvmProperty ValueBindingProperty = - DotvvmProperty.Register(c => c.ValueBinding); + DotvvmProperty.Register, GridViewCheckBoxColumn>(c => c.ValueBinding); /// Whether to automatically attach Validator.Value onto the TextBox or add a standalone Validator component. public ValidatorPlacement ValidatorPlacement @@ -51,9 +52,9 @@ private void CreateControlsCore(DotvvmControl container, bool enabled) var checkBox = new CheckBox { Enabled = enabled }; CopyProperty(UITests.NameProperty, checkBox, UITests.NameProperty); - var valueBinding = GetValueBinding(ValueBindingProperty); - checkBox.SetBinding(CheckBox.CheckedProperty, valueBinding); - Validator.Place(checkBox, container.Children, valueBinding, ValidatorPlacement); + var binding = ValueBinding; + checkBox.SetBinding(CheckBox.CheckedProperty, binding); + Validator.Place(checkBox, container.Children, binding as IValueBinding, ValidatorPlacement); container.Children.Add(checkBox); } @@ -61,7 +62,7 @@ private void CreateControlsCore(DotvvmControl container, bool enabled) { if (string.IsNullOrEmpty(SortExpression)) { - return GetValueBinding(ValueBindingProperty)?.GetProperty()?.Code ?? + return GetBinding(ValueBindingProperty)?.GetProperty()?.Code ?? throw new DotvvmControlException(this, $"The 'ValueBinding' property must be set on the '{GetType()}' control!"); } else diff --git a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs index 3e58a82924..9c38e9b06f 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -22,7 +22,7 @@ public class GridViewDataSetBindingProvider { private readonly BindingCompilationService service; - private readonly ConcurrentDictionary<(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType), DataPagerBindings> dataPagerCommands = new(); + private readonly ConcurrentDictionary<(DataContextStack dataContextStack, IStaticValueBinding dataSetBinding, GridViewDataSetCommandType commandType), DataPagerBindings> dataPagerCommands = new(); private readonly ConcurrentDictionary<(DataContextStack dataContextStack, IStaticValueBinding dataSetBinding, GridViewDataSetCommandType commandType), GridViewBindings> gridViewCommands = new(); public GridViewDataSetBindingProvider(BindingCompilationService service) @@ -31,7 +31,7 @@ public GridViewDataSetBindingProvider(BindingCompilationService service) } /// 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) + public DataPagerBindings GetDataPagerBindings(DataContextStack dataContextStack, IStaticValueBinding dataSetBinding, GridViewDataSetCommandType commandType) { return dataPagerCommands.GetOrAdd((dataContextStack, dataSetBinding, commandType), x => GetDataPagerCommandsCore(x.dataContextStack, x.dataSetBinding, x.commandType)); } @@ -42,8 +42,9 @@ public GridViewBindings GetGridViewBindings(DataContextStack dataContextStack, I return gridViewCommands.GetOrAdd((dataContextStack, dataSetBinding, commandType), x => GetGridViewBindingsCore(x.dataContextStack, x.dataSetBinding, x.commandType)); } - private DataPagerBindings GetDataPagerCommandsCore(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType) + private DataPagerBindings GetDataPagerCommandsCore(DataContextStack dataContextStack, IStaticValueBinding dataSetBinding, GridViewDataSetCommandType commandType) { + var isServerOnly = dataSetBinding is not IValueBinding; var dataSetExpr = dataSetBinding.GetProperty().Expression; ICommandBinding? GetCommandOrNull(DataContextStack dataContextStack, string methodName, params Expression[] arguments) { @@ -70,7 +71,7 @@ ParameterExpression CreateParameter(DataContextStack dataContextStack, string na } var pageIndexDataContext = DataContextStack.CreateCollectionElement( - typeof(int), dataContextStack + typeof(int), dataContextStack, serverSideOnly: isServerOnly ); var isFirstPage = GetValueBindingOrNull, bool>(d => d.PagingOptions.IsFirstPage) ?? @@ -108,27 +109,29 @@ ParameterExpression CreateParameter(DataContextStack dataContextStack, string na IsActivePage = // _this == _parent.DataSet.PagingOptions.PageIndex typeof(IPageableGridViewDataSet).IsAssignableFrom(dataSetExpr.Type) - ? new ValueBindingExpression(service, new object[] { + ? ValueOrResourceBinding(isServerOnly, [ pageIndexDataContext, new ParsedExpressionBindingProperty(Expression.Equal( CreateParameter(pageIndexDataContext, "_thisIndex"), Expression.Property(Expression.Property(dataSetExpr, "PagingOptions"), "PageIndex") )), - }) + ]) : null, - PageNumberText = - service.Cache.CreateValueBinding("_this + 1", pageIndexDataContext), + PageNumberText = isServerOnly switch { + true => service.Cache.CreateResourceBinding("_this + 1", pageIndexDataContext), + false => service.Cache.CreateValueBinding("_this + 1", pageIndexDataContext) + }, HasMoreThanOnePage = GetValueBindingOrNull, bool>(d => d.PagingOptions.PagesCount > 1) ?? (isFirstPage != null && isLastPage != null ? - new ValueBindingExpression(service, new object[] { + ValueOrResourceBinding(isServerOnly, [ dataContextStack, new ParsedExpressionBindingProperty(Expression.Not(Expression.AndAlso( isFirstPage.GetProperty().Expression, isLastPage.GetProperty().Expression ))) - }) : null) + ]) : null) }; } @@ -189,22 +192,19 @@ private IStaticValueBinding CreateValueBinding(service, + return ValueOrResourceBinding(isServerOnly, [ new ParsedExpressionBindingProperty(expression), dataContextStack ]); - } + } + + private IStaticValueBinding ValueOrResourceBinding(bool isServerOnly, object?[] properties) + { + if (isServerOnly) + return new ResourceBindingExpression(service, properties); else - { - return new ValueBindingExpression(service, - [ - new ParsedExpressionBindingProperty(expression), - dataContextStack - ]); - } + return new ValueBindingExpression(service, properties); } private ICommandBinding CreateCommandBinding(GridViewDataSetCommandType commandType, Expression dataSet, DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression = null) diff --git a/src/Framework/Framework/Controls/GridViewTextColumn.cs b/src/Framework/Framework/Controls/GridViewTextColumn.cs index 7be558bdfd..cf0b946285 100644 --- a/src/Framework/Framework/Controls/GridViewTextColumn.cs +++ b/src/Framework/Framework/Controls/GridViewTextColumn.cs @@ -86,8 +86,7 @@ public override void CreateControls(IDotvvmRequestContext context, DotvvmControl CopyPropertyRaw(UITests.NameProperty, literal, UITests.NameProperty); literal.SetBinding(Literal.TextProperty, binding); - if (binding is IValueBinding v) - Validator.Place(literal, container.Children, v, ValidatorPlacement); + Validator.Place(literal, container.Children, binding as IValueBinding, ValidatorPlacement); container.Children.Add(literal); } @@ -101,11 +100,11 @@ public override void CreateEditControls(IDotvvmRequestContext context, DotvvmCon var textBox = new TextBox(); + var binding = ValueBinding; CopyPropertyRaw(FormatStringProperty, textBox, TextBox.FormatStringProperty); - textBox.SetBinding(TextBox.TextProperty, ValueBinding); + textBox.SetBinding(TextBox.TextProperty, binding); CopyPropertyRaw(ChangedBindingProperty, textBox, TextBox.ChangedProperty); - if (ValueBinding is IValueBinding v) - Validator.Place(textBox, container.Children, v, ValidatorPlacement); + Validator.Place(textBox, container.Children, binding as IValueBinding, ValidatorPlacement); container.Children.Add(textBox); } } diff --git a/src/Framework/Framework/Controls/Validator.cs b/src/Framework/Framework/Controls/Validator.cs index 3ef265a065..a074b58a05 100644 --- a/src/Framework/Framework/Controls/Validator.cs +++ b/src/Framework/Framework/Controls/Validator.cs @@ -69,6 +69,9 @@ public static void Place( IValueBinding? value, ValidatorPlacement placement) { + if (value is null) + return; + if (placement.HasFlag(ValidatorPlacement.AttachToControl)) { control.SetValue(ValueProperty, value!); } diff --git a/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs b/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs index 76ae01c8b9..8ca61d2596 100644 --- a/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs +++ b/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs @@ -66,7 +66,16 @@ private void CheckRenderedResources(IDotvvmRequestContext context) { throw new DotvvmControlException(control, "This control cannot use PostBack.Update=\"true\" because it has dynamic ID. This happens when the control is inside a Repeater or other data-bound control and the RenderSettings.Mode=\"Client\"."); } - yield return (clientId, w.ToString()); + var result = w.ToString(); + if (string.IsNullOrWhiteSpace(result)) + { + throw new DotvvmControlException(control, "The PostBack.Update=\"true\" property is set on this control, but the control does not render anything. "); + } + if (!result.Contains(clientId)) + { + throw new DotvvmControlException(control, "The PostBack.Update=\"true\" property is set on this control, but the control does not render the correct client ID."); + } + yield return (clientId, result); } } else diff --git a/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewPagingSortingServerSideViewModel.cs b/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewPagingSortingServerSideViewModel.cs new file mode 100644 index 0000000000..b04e402e66 --- /dev/null +++ b/src/Samples/Common/ViewModels/ControlSamples/GridView/GridViewPagingSortingServerSideViewModel.cs @@ -0,0 +1,121 @@ +using DotVVM.Framework.Binding; +using DotVVM.Framework.Controls; +using DotVVM.Framework.ViewModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + + +namespace DotVVM.Samples.BasicSamples.ViewModels.ControlSamples.GridView +{ + public class GridViewPagingSortingServerSideViewModel : DotvvmViewModelBase + { + private static IQueryable GetData() + { + return new[] + { + 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 = true}, + new CustomerData() { CustomerId = 4, Name = "Jim Hacker", BirthDate = DateTime.Parse("1912-11-04"), MessageReceived = true}, + new CustomerData() { CustomerId = 5, Name = "Joe E. Brown", BirthDate = DateTime.Parse("1947-09-05"), MessageReceived = false}, + new CustomerData() { CustomerId = 6, Name = "Jim Harris", BirthDate = DateTime.Parse("1956-07-06"), MessageReceived = false}, + new CustomerData() { CustomerId = 7, Name = "J. P. Morgan", BirthDate = DateTime.Parse("1969-05-07"), MessageReceived = false }, + new CustomerData() { CustomerId = 8, Name = "J. R. Ewing", BirthDate = DateTime.Parse("1987-03-08"), MessageReceived = false}, + new CustomerData() { CustomerId = 9, Name = "Jeremy Clarkson", BirthDate = DateTime.Parse("1994-04-09"), MessageReceived = false }, + new CustomerData() { CustomerId = 10, Name = "Jenny Green", BirthDate = DateTime.Parse("1947-02-10"), MessageReceived = false}, + new CustomerData() { CustomerId = 11, Name = "Joseph Blue", BirthDate = DateTime.Parse("1948-12-11"), MessageReceived = false}, + new CustomerData() { CustomerId = 12, Name = "Jack Daniels", BirthDate = DateTime.Parse("1968-10-12"), MessageReceived = true}, + new CustomerData() { CustomerId = 13, Name = "Jackie Chan", BirthDate = DateTime.Parse("1978-08-13"), MessageReceived = false}, + new CustomerData() { CustomerId = 14, Name = "Jasper", BirthDate = DateTime.Parse("1934-06-14"), MessageReceived = false}, + new CustomerData() { CustomerId = 15, Name = "Jumbo", BirthDate = DateTime.Parse("1965-06-15"), MessageReceived = false }, + new CustomerData() { CustomerId = 16, Name = "Junkie Doodle", BirthDate = DateTime.Parse("1977-05-16"), MessageReceived = false } + }.AsQueryable(); + } + + public ServerSideGridViewDataSet CustomersDataSet { get; set; } = new() { + PagingOptions = new PagingOptions() { + PageSize = 10 + } + }; + public ServerSideGridViewDataSet EmptyCustomersDataSet { get; set; } = new() { + PagingOptions = new PagingOptions() { + PageSize = 10 + } + }; + + [Bind(Direction.None)] + public string SelectedSortColumn { get; set; } + + [Bind(Direction.None)] + public List Customers { get; set; } + + [Bind(Direction.None)] + public List Null { get; set; } + + public ServerSideGridViewDataSet NullDataSet { get; set; } + + [Bind(Direction.None)] + public string CustomNameForName { get; set; } = "Name"; + + public override Task Load() + { + CustomersDataSet.RequestRefresh(); + LoadData(); + + return base.Load(); + } + + public override Task PreRender() + { + LoadData(); + + return base.PreRender(); + } + + private void LoadData() + { + if (CustomersDataSet.IsRefreshRequired) + CustomersDataSet.LoadFromQueryable(GetData()); + + if (SelectedSortColumn == "Name") + { + Customers = GetData().OrderBy(c => c.Name).ToList(); + } + else if (SelectedSortColumn == "BirthDate") + { + Customers = GetData().OrderBy(c => c.BirthDate).ToList(); + } + else + { + Customers = GetData().ToList(); + } + } + + public void TestCommand() + { + if (CustomersDataSet.SortingOptions.SortExpression == "BirthDate") + { + CustomersDataSet.SortingOptions.SortDescending = !CustomersDataSet.SortingOptions.SortDescending; + } + else + { + CustomersDataSet.PagingOptions.PageIndex = 0; + CustomersDataSet.SortingOptions.SortExpression = "BirthDate"; + CustomersDataSet.SortingOptions.SortDescending = false; + } + } + + public void SortCustomers(string column) + { + SelectedSortColumn = column; + } + + [AllowStaticCommand] + public List GetDataList() + { + return GetData().ToList(); + } + } +} diff --git a/src/Samples/Common/Views/ControlSamples/GridView/GridViewPagingSortingServerSide.dothtml b/src/Samples/Common/Views/ControlSamples/GridView/GridViewPagingSortingServerSide.dothtml new file mode 100644 index 0000000000..1e13150bd3 --- /dev/null +++ b/src/Samples/Common/Views/ControlSamples/GridView/GridViewPagingSortingServerSide.dothtml @@ -0,0 +1,85 @@ +@viewModel DotVVM.Samples.BasicSamples.ViewModels.ControlSamples.GridView.GridViewPagingSortingServerSideViewModel, DotVVM.Samples.Common + + + + Hello from DotVVM! + + + +
+

GridView with IGridViewDataSet

+ + + + + + + + Birth date + + + + + + + + + +

 

+

 

+

 

+ +

GridView with simple collection

+ + + + + + + +

Selected sort column: {{resource: SelectedSortColumn}}

+

 

+

 

+

 

+ +

GridView with null DataSource

+
+ + + + + + + +
+ +
+

EmptyData with data source

+ + This is not displayed because data is not empty + +

EmptyData with no data source

+ + This is displayed because data is empty + + + +
+ +

GridView with empty dataset and ShowHeaderWhenNoData true

+ + + + + + + + + +
+ + diff --git a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs index fb434046a7..8d4e6bf335 100644 --- a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs +++ b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs @@ -87,6 +87,7 @@ public partial class SamplesRouteUrls public const string ControlSamples_GridView_GridViewInlineEditingPrimaryKeyString = "ControlSamples/GridView/GridViewInlineEditingPrimaryKeyString"; public const string ControlSamples_GridView_GridViewInlineEditingValidation = "ControlSamples/GridView/GridViewInlineEditingValidation"; public const string ControlSamples_GridView_GridViewPagingSorting = "ControlSamples/GridView/GridViewPagingSorting"; + public const string ControlSamples_GridView_GridViewPagingSortingServerOnly = "ControlSamples/GridView/GridViewPagingSortingServerOnly"; public const string ControlSamples_GridView_GridViewRowDecorators = "ControlSamples/GridView/GridViewRowDecorators"; public const string ControlSamples_GridView_GridViewServerRender = "ControlSamples/GridView/GridViewServerRender"; public const string ControlSamples_GridView_GridViewSortChanged = "ControlSamples/GridView/GridViewSortChanged"; diff --git a/src/Samples/Tests/Tests/Control/GridViewTests.cs b/src/Samples/Tests/Tests/Control/GridViewTests.cs index 906c5a9396..f3838c83ae 100644 --- a/src/Samples/Tests/Tests/Control/GridViewTests.cs +++ b/src/Samples/Tests/Tests/Control/GridViewTests.cs @@ -510,8 +510,10 @@ public void Control_GridView_GridViewInlineEditing_PagingWhenEditing(string path [Theory] [InlineData(SamplesRouteUrls.ControlSamples_GridView_GridViewPagingSorting)] [InlineData(SamplesRouteUrls.ControlSamples_GridView_GridViewServerRender)] + [InlineData(SamplesRouteUrls.ControlSamples_GridView_GridViewPagingSortingServerOnly)] [SampleReference(nameof(SamplesRouteUrls.ControlSamples_GridView_GridViewPagingSorting))] [SampleReference(nameof(SamplesRouteUrls.ControlSamples_GridView_GridViewServerRender))] + [SampleReference(nameof(SamplesRouteUrls.ControlSamples_GridView_GridViewPagingSortingServerOnly))] public void Control_GridView_GridViewPagingSortingBase(string path) { RunInAllBrowsers(browser => { diff --git a/src/Tests/Binding/StaticCommandCompilationTests.cs b/src/Tests/Binding/StaticCommandCompilationTests.cs index e38b2e0092..664c898190 100644 --- a/src/Tests/Binding/StaticCommandCompilationTests.cs +++ b/src/Tests/Binding/StaticCommandCompilationTests.cs @@ -58,7 +58,6 @@ static DotvvmConfiguration MakeConfiguration() static StaticCommandCompilationTests() { var extensionParams = new BindingExtensionParameter[] { - new CurrentCollectionIndexExtensionParameter(), new CurrentCollectionIndexExtensionParameter(), new BindingPageInfoExtensionParameter(), new InjectedServiceExtensionParameter("injectedService", new ResolvedTypeDescriptor(typeof(TestService))) diff --git a/src/Tests/Binding/StaticCommandValidationPathTests.cs b/src/Tests/Binding/StaticCommandValidationPathTests.cs index fcb66579ae..455aa2d1a5 100644 --- a/src/Tests/Binding/StaticCommandValidationPathTests.cs +++ b/src/Tests/Binding/StaticCommandValidationPathTests.cs @@ -26,7 +26,6 @@ public class StaticCommandValidationPathTests { static readonly BindingTestHelper bindingHelper = new BindingTestHelper(defaultExtensionParameters: new BindingExtensionParameter[] { - new CurrentCollectionIndexExtensionParameter(), new CurrentCollectionIndexExtensionParameter(), new BindingPageInfoExtensionParameter(), new InjectedServiceExtensionParameter("s", new ResolvedTypeDescriptor(typeof(ValidatedService))) diff --git a/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html index a061797212..98b22e1a00 100644 --- a/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html +++ b/src/Tests/ControlTests/testoutputs/DataPagerTests.CommandDataPager.html @@ -11,10 +11,10 @@
  • - + - +
  • diff --git a/src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandPager.html b/src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandPager.html index 932afdb9f0..c40e8e9169 100644 --- a/src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandPager.html +++ b/src/Tests/ControlTests/testoutputs/DataPagerTests.StaticCommandPager.html @@ -17,13 +17,13 @@
  • - - +
  • diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index e183d7438d..0f29ce8476 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -468,7 +468,6 @@ "DotVVM.Framework.Controls.CheckBox": { "Checked": { "type": "System.Nullable`1[[System.Boolean, CoreLibrary]]", - "defaultValue": false, "onlyBindings": true }, "CheckedItems": { @@ -829,8 +828,7 @@ "defaultValue": "None" }, "ValueBinding": { - "type": "System.Boolean", - "defaultValue": false, + "type": "DotVVM.Framework.Binding.Expressions.IStaticValueBinding`1[[System.Nullable`1[[System.Boolean, CoreLibrary]], CoreLibrary]], DotVVM.Framework", "required": true, "onlyBindings": true } diff --git a/src/Tests/Runtime/testoutputs/RuntimeErrorTests.BindingErrorsShouldBeReasonable.txt b/src/Tests/Runtime/testoutputs/RuntimeErrorTests.BindingErrorsShouldBeReasonable.txt index c34d7a8757..242d8ee5b1 100644 --- a/src/Tests/Runtime/testoutputs/RuntimeErrorTests.BindingErrorsShouldBeReasonable.txt +++ b/src/Tests/Runtime/testoutputs/RuntimeErrorTests.BindingErrorsShouldBeReasonable.txt @@ -1,4 +1,4 @@ -BindingPropertyException occurred: Could not initialize binding as it is missing a required property KnockoutExpressionBindingProperty: Could not resolve identifier '_parent.LOL'. Binding: {value: _parent.LOL}. Property path: KnockoutExpressionBindingProperty, KnockoutJsExpressionBindingProperty, CastedExpressionBindingProperty. +BindingPropertyException occurred: Could not initialize binding '{value: _parent.LOL}' as it is missing a required property KnockoutExpressionBindingProperty: Could not resolve identifier '_parent.LOL'. Binding: {value: _parent.LOL}. Property path: KnockoutExpressionBindingProperty, KnockoutJsExpressionBindingProperty, CastedExpressionBindingProperty. at void DotVVM.Framework.Tests.Runtime.RuntimeErrorTests.BindingErrorsShouldBeReasonable()+() => { } BindingCompilationException occurred: Could not resolve identifier '_parent.LOL'. diff --git a/src/Tests/Runtime/testoutputs/RuntimeErrorTests.CantFindDataContextSpace.txt b/src/Tests/Runtime/testoutputs/RuntimeErrorTests.CantFindDataContextSpace.txt index 1178fad778..dd299534cb 100644 --- a/src/Tests/Runtime/testoutputs/RuntimeErrorTests.CantFindDataContextSpace.txt +++ b/src/Tests/Runtime/testoutputs/RuntimeErrorTests.CantFindDataContextSpace.txt @@ -1,2 +1,5 @@ -InvalidDataContextTypeException occurred: Could not find DataContext space of '{value: False}'. The DataContextType property of the binding does not correspond to DataContextType of the HtmlGenericControl nor any of its ancestors. Control's context is (type=string, par=[int]), binding's context is (type=string). Real data context types: . +InvalidDataContextTypeException occurred: Could not find DataContext space of '{value: False}'. The DataContextType property of the binding does not correspond to DataContextType of the HtmlGenericControl nor any of its ancestors. +Control's context is (type=string, par=[int]). +Binding's context is (type=string). +Real data context types: . at object DotVVM.Framework.Controls.DotvvmBindableObject.EvalPropertyValue(DotvvmProperty property, object value) diff --git a/src/Tests/Runtime/testoutputs/RuntimeErrorTests.InitResolverFailure.txt b/src/Tests/Runtime/testoutputs/RuntimeErrorTests.InitResolverFailure.txt index 28fc26ab19..a58918a45b 100644 --- a/src/Tests/Runtime/testoutputs/RuntimeErrorTests.InitResolverFailure.txt +++ b/src/Tests/Runtime/testoutputs/RuntimeErrorTests.InitResolverFailure.txt @@ -1,4 +1,4 @@ -BindingPropertyException occurred: Could not initialize binding as it is missing a required property KnockoutExpressionBindingProperty: Method object.ToString() -> string cannot be translated to Javascript. Binding: {value: value(DotVVM.Framework.Configuration.DotvvmConfiguration).ToString()}. +BindingPropertyException occurred: Could not initialize binding '{value: value(DotVVM.Framework.Configuration.DotvvmConfiguration).ToString()}' as it is missing a required property KnockoutExpressionBindingProperty: Method object.ToString() -> string cannot be translated to Javascript. Binding: {value: value(DotVVM.Framework.Configuration.DotvvmConfiguration).ToString()}. at void DotVVM.Framework.Tests.Runtime.RuntimeErrorTests.InitResolverFailure()+() => { } NotSupportedException occurred: Method object.ToString() -> string cannot be translated to Javascript