From a9193f73e1b584a063e37e1d6a9b70ca00e7537e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Tue, 2 Aug 2022 23:25:00 +0200 Subject: [PATCH] DataContext resource binding: GridView support --- .../DynamicData/DataContextStackHelper.cs | 2 +- .../Framework/Binding/BindingHelper.cs | 8 ++-- ...lPropertyTypeDataContextChangeAttribute.cs | 2 +- .../ControlTree/ControlTreeResolverBase.cs | 11 ++++- src/Framework/Framework/Controls/GridView.cs | 48 +++++++++++-------- .../Framework/Controls/GridViewColumn.cs | 7 ++- .../Framework/Controls/GridViewTextColumn.cs | 18 ++++--- src/Tests/ControlTests/RepeaterTests.cs | 2 +- .../ControlTests/ResourceDataContextTests.cs | 13 +++++ .../ResourceDataContextTests.GridView.html | 31 ++++++++++++ ...alizationTests.SerializeDefaultConfig.json | 2 +- 11 files changed, 107 insertions(+), 37 deletions(-) create mode 100644 src/Tests/ControlTests/testoutputs/ResourceDataContextTests.GridView.html diff --git a/src/DynamicData/DynamicData/DataContextStackHelper.cs b/src/DynamicData/DynamicData/DataContextStackHelper.cs index cdaf98fe81..775c23ff31 100644 --- a/src/DynamicData/DynamicData/DataContextStackHelper.cs +++ b/src/DynamicData/DynamicData/DataContextStackHelper.cs @@ -40,4 +40,4 @@ public static DataContextStack CreateChildStack(this DataContextStack dataContex dataContextStack.BindingPropertyResolvers); } } -} \ No newline at end of file +} diff --git a/src/Framework/Framework/Binding/BindingHelper.cs b/src/Framework/Framework/Binding/BindingHelper.cs index 7438de9bcd..1710a6e5d0 100644 --- a/src/Framework/Framework/Binding/BindingHelper.cs +++ b/src/Framework/Framework/Binding/BindingHelper.cs @@ -323,10 +323,12 @@ public static Func Cache(this Func cache.GetOrAdd(f, func); } - public static IValueBinding GetThisBinding(this DotvvmBindableObject obj) + public static IStaticValueBinding GetThisBinding(this DotvvmBindableObject obj) { - var dataContext = obj.GetValueBinding(DotvvmBindableObject.DataContextProperty); - return (IValueBinding)dataContext!.GetProperty().binding; + var dataContext = (IStaticValueBinding?)obj.GetBinding(DotvvmBindableObject.DataContextProperty); + if (dataContext is null) + throw new InvalidOperationException("DataContext must be set to a binding to allow creation of a {value: _this} binding"); + return (IStaticValueBinding)dataContext!.GetProperty().binding; } private static readonly ConditionalWeakTable _expressionAnnotations = diff --git a/src/Framework/Framework/Binding/ControlPropertyTypeDataContextChangeAttribute.cs b/src/Framework/Framework/Binding/ControlPropertyTypeDataContextChangeAttribute.cs index f509c813ef..a94e472fd7 100644 --- a/src/Framework/Framework/Binding/ControlPropertyTypeDataContextChangeAttribute.cs +++ b/src/Framework/Framework/Binding/ControlPropertyTypeDataContextChangeAttribute.cs @@ -47,7 +47,7 @@ public ControlPropertyTypeDataContextChangeAttribute(string propertyName, int or throw new Exception($"The property '{PropertyName}' was not found on control '{controlType}'!"); } - if (control.properties.Contains(controlProperty) && control.GetValueBinding(controlProperty) is IValueBinding valueBinding) + if (control.properties.Contains(controlProperty) && control.GetBinding(controlProperty) is IStaticValueBinding valueBinding) { return valueBinding.ResultType; } diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs index 65533adc75..e39ccf3aa9 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs @@ -12,6 +12,7 @@ using System.Diagnostics.CodeAnalysis; using DotVVM.Framework.Compilation.Directives; using DotVVM.Framework.Binding.Expressions; +using FastExpressionCompiler; namespace DotVVM.Framework.Compilation.ControlTree { @@ -345,13 +346,21 @@ private void ProcessAttribute(IPropertyDescriptor property, DothtmlAttributeNode attribute.ValueNode.AddError($"The property '{ property.FullName }' cannot contain {bindingNode.Name} binding."); } var binding = ProcessBinding(bindingNode, dataContext, property); + if (property.IsBindingProperty) + { + // check that binding types are compatible + if (!property.PropertyType.IsAssignableFrom(ResolvedTypeDescriptor.Create(binding.BindingType))) + { + attribute.ValueNode.AddError($"The property '{property.FullName}' cannot contain a binding of type '{binding.BindingType}'!"); + } + } var bindingProperty = treeBuilder.BuildPropertyBinding(property, binding, attribute); if (!treeBuilder.AddProperty(control, bindingProperty, out var error)) attribute.AddError(error); } else { // hard-coded value in markup - if (!property.MarkupOptions.AllowHardCodedValue) + if (!property.MarkupOptions.AllowHardCodedValue || property.IsBindingProperty) { attribute.ValueNode.AddError($"The property '{ property.FullName }' cannot contain hard coded value."); } diff --git a/src/Framework/Framework/Controls/GridView.cs b/src/Framework/Framework/Controls/GridView.cs index 83df7b4e9f..56c3659673 100644 --- a/src/Framework/Framework/Controls/GridView.cs +++ b/src/Framework/Framework/Controls/GridView.cs @@ -177,6 +177,7 @@ private void DataBind(IDotvvmRequestContext context) head = null; var dataSourceBinding = GetDataSourceBinding(); + var serverOnly = dataSourceBinding is not IValueBinding; var dataSource = DataSource; var sortCommand = SortChanged; @@ -196,7 +197,7 @@ private void DataBind(IDotvvmRequestContext context) foreach (var item in GetIEnumerableFromDataSource()!) { // create row - var placeholder = new DataItemContainer { DataItemIndex = index }; + var placeholder = new DataItemContainer { DataItemIndex = index, RenderItemBinding = !serverOnly }; placeholder.SetDataContextTypeFromDataSource(dataSourceBinding); placeholder.DataContext = item; placeholder.SetValue(Internal.PathFragmentProperty, GetPathFragmentExpression() + "/[" + index + "]"); @@ -431,19 +432,22 @@ protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext head?.Render(writer, context); // render body - var foreachBinding = TryGetKnockoutForeachExpression().NotNull("GridView does not support DataSource={resource: ...} at this moment."); - if (RenderOnServer) + var foreachBinding = TryGetKnockoutForeachExpression(); + if (foreachBinding is {}) { - writer.AddKnockoutDataBind("dotvvm-SSR-foreach", "{data:" + foreachBinding + "}"); - } - else - { - writer.AddKnockoutForeachDataBind(foreachBinding); + if (RenderOnServer) + { + writer.AddKnockoutDataBind("dotvvm-SSR-foreach", "{data:" + foreachBinding + "}"); + } + else + { + writer.AddKnockoutForeachDataBind(foreachBinding); + } } writer.RenderBeginTag("tbody"); // render contents - if (RenderOnServer) + if (RenderOnServer || foreachBinding is null) { // render on server var index = 0; @@ -498,8 +502,10 @@ protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext { if (!ShowHeaderWhenNoData) { - writer.WriteKnockoutDataBindComment("if", - GetForeachDataBindExpression().GetProperty().Binding.CastTo().GetKnockoutBindingExpression(this)); + if (GetForeachDataBindExpression().GetProperty().Binding is IValueBinding conditionValueBinding) + { + writer.WriteKnockoutDataBindComment("if", conditionValueBinding.GetKnockoutBindingExpression(this)); + } } base.RenderBeginTag(writer, context); @@ -507,7 +513,8 @@ protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext context) { - if (RenderOnServer && numberOfRows == 0 && !ShowHeaderWhenNoData) + var ssr = RenderOnServer || GetForeachDataBindExpression() is not IValueBinding; + if (ssr && numberOfRows == 0 && !ShowHeaderWhenNoData) { emptyDataContainer?.Render(writer, context); } @@ -521,7 +528,7 @@ protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext c { base.RenderEndTag(writer, context); - if (!ShowHeaderWhenNoData) + if (!ShowHeaderWhenNoData && GetForeachDataBindExpression() is IValueBinding) { writer.WriteKnockoutDataBindEndComment(); } @@ -531,13 +538,16 @@ protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext c protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context) { - var itemType = ReflectionUtils.GetEnumerableType(GetDataSourceBinding().ResultType); - var userColumnMappingService = context.Services.GetRequiredService(); - var mapping = userColumnMappingService.GetMapping(itemType!); - var mappingJson = JsonConvert.SerializeObject(mapping); - var dataBinding = TryGetKnockoutForeachExpression(unwrapped: true).NotNull("GridView does not support DataSource={resource: ...} at this moment."); - writer.AddKnockoutDataBind("dotvvm-gridviewdataset", $"{{'mapping':{mappingJson},'dataSet':{dataBinding}}}"); + if (TryGetKnockoutForeachExpression(unwrapped: true) is {} dataBinding) + { + var itemType = ReflectionUtils.GetEnumerableType(GetDataSourceBinding().ResultType); + var userColumnMappingService = context.Services.GetRequiredService(); + var mapping = userColumnMappingService.GetMapping(itemType!); + var mappingJson = JsonConvert.SerializeObject(mapping); + + writer.AddKnockoutDataBind("dotvvm-gridviewdataset", $"{{'mapping':{mappingJson},'dataSet':{dataBinding}}}"); + } base.AddAttributesToRender(writer, context); } diff --git a/src/Framework/Framework/Controls/GridViewColumn.cs b/src/Framework/Framework/Controls/GridViewColumn.cs index cbb68c9e04..5cef3c769d 100644 --- a/src/Framework/Framework/Controls/GridViewColumn.cs +++ b/src/Framework/Framework/Controls/GridViewColumn.cs @@ -200,7 +200,7 @@ public virtual void CreateHeaderControls(IDotvvmRequestContext context, GridView var binding = new CommandBindingExpression(context.Services.GetRequiredService().WithoutInitialization(), h => sortCommand(sortExpression), bindingId); linkButton.SetBinding(ButtonBase.ClickProperty, binding); - SetSortedCssClass(cell, gridViewDataSet, gridView.GetValueBinding(GridView.DataSourceProperty)!); + SetSortedCssClass(cell, gridViewDataSet, (IStaticValueBinding)gridView.GetBinding(GridView.DataSourceProperty)!); } else { @@ -220,14 +220,13 @@ public virtual void CreateFilterControls(IDotvvmRequestContext context, GridView } } - private void SetSortedCssClass(HtmlGenericControl cell, ISortableGridViewDataSet? sortableGridViewDataSet, IValueBinding dataSourceBinding) + private void SetSortedCssClass(HtmlGenericControl cell, ISortableGridViewDataSet? sortableGridViewDataSet, IStaticValueBinding dataSourceBinding) { if (sortableGridViewDataSet != null) { var cellAttributes = cell.Attributes; - if (!RenderOnServer) + if (!RenderOnServer && (dataSourceBinding as IValueBinding)?.GetKnockoutBindingExpression(cell, unwrapped: true) is {} gridViewDataSetExpr) { - 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()}}"; } else if (sortableGridViewDataSet.SortingOptions.SortExpression == GetSortExpression()) diff --git a/src/Framework/Framework/Controls/GridViewTextColumn.cs b/src/Framework/Framework/Controls/GridViewTextColumn.cs index f574722bd7..5e930da209 100644 --- a/src/Framework/Framework/Controls/GridViewTextColumn.cs +++ b/src/Framework/Framework/Controls/GridViewTextColumn.cs @@ -43,13 +43,13 @@ public ICommandBinding? ChangedBinding /// Gets or sets a binding which retrieves the value to display from the current data item. /// [MarkupOptions(Required = true)] - public IValueBinding? ValueBinding + public IStaticValueBinding? ValueBinding { - get { return GetValueBinding(ValueBindingProperty); } + get { return (IStaticValueBinding?)GetBinding(ValueBindingProperty); } set { SetValue(ValueBindingProperty, value); } } public static readonly DotvvmProperty ValueBindingProperty = - DotvvmProperty.Register(c => c.ValueBinding); + DotvvmProperty.Register(c => c.ValueBinding); [MarkupOptions(AllowBinding = false)] public ValidatorPlacement ValidatorPlacement @@ -75,13 +75,18 @@ public static readonly DotvvmProperty ValidatorPlacementProperty public override void CreateControls(IDotvvmRequestContext context, DotvvmControl container) { + var binding = ValueBinding; + if (binding is null) + throw new DotvvmControlException(this, "The 'ValueBinding' property is required."); + var literal = new Literal(); literal.FormatString = FormatString; CopyProperty(UITests.NameProperty, literal, UITests.NameProperty); - literal.SetBinding(Literal.TextProperty, ValueBinding); - Validator.Place(literal, container.Children, ValueBinding, ValidatorPlacement); + literal.SetBinding(Literal.TextProperty, binding); + if (binding is IValueBinding v) + Validator.Place(literal, container.Children, v, ValidatorPlacement); container.Children.Add(literal); } @@ -92,7 +97,8 @@ public override void CreateEditControls(IDotvvmRequestContext context, DotvvmCon textBox.SetBinding(TextBox.TextProperty, ValueBinding); textBox.SetBinding(TextBox.ChangedProperty, ChangedBinding); - Validator.Place(textBox, container.Children, ValueBinding, ValidatorPlacement); + if (ValueBinding is IValueBinding v) + Validator.Place(textBox, container.Children, v, ValidatorPlacement); container.Children.Add(textBox); } } diff --git a/src/Tests/ControlTests/RepeaterTests.cs b/src/Tests/ControlTests/RepeaterTests.cs index 772441b41b..c359b3394e 100644 --- a/src/Tests/ControlTests/RepeaterTests.cs +++ b/src/Tests/ControlTests/RepeaterTests.cs @@ -105,7 +105,7 @@ public class RepeaterWrapper : CompositeControl { public static DotvvmControl GetContents( HtmlCapability htmlCapability, - [ControlPropertyTypeDataContextChange("DataSource"), CollectionElementDataContextChange(1)] + [ControlPropertyBindingDataContextChange("DataSource"), CollectionElementDataContextChange(1)] ITemplate itemTemplate, IValueBinding dataSource ) diff --git a/src/Tests/ControlTests/ResourceDataContextTests.cs b/src/Tests/ControlTests/ResourceDataContextTests.cs index 86163436d5..73e6616834 100644 --- a/src/Tests/ControlTests/ResourceDataContextTests.cs +++ b/src/Tests/ControlTests/ResourceDataContextTests.cs @@ -164,6 +164,19 @@ public async Task HierarchyRepeater_SimpleTemplate() check.CheckString(r.FormattedHtml, fileExtension: "html"); } + [TestMethod] + public async Task GridView() + { + var r = await cth.RunPage(typeof(TestViewModel), @" + + + + {{resource: Name}} + + "); + check.CheckString(r.FormattedHtml, fileExtension: "html"); + } + public class TestViewModel: DotvvmViewModelBase { diff --git a/src/Tests/ControlTests/testoutputs/ResourceDataContextTests.GridView.html b/src/Tests/ControlTests/testoutputs/ResourceDataContextTests.GridView.html new file mode 100644 index 0000000000..9ee6b330ed --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/ResourceDataContextTests.GridView.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + +
+ Id + + Name +
+ 1 + One
+ 2 + Two
+ + diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index d85105aceb..044c82fa76 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -605,7 +605,7 @@ "type": "DotVVM.Framework.Controls.ValidatorPlacement, DotVVM.Framework" }, "ValueBinding": { - "type": "DotVVM.Framework.Binding.Expressions.IValueBinding, DotVVM.Framework", + "type": "DotVVM.Framework.Binding.Expressions.IStaticValueBinding, DotVVM.Framework", "required": true } },