diff --git a/src/AutoUI/Core/PropertyHandlers/FormEditors/EnumComboBoxFormEditorProvider.cs b/src/AutoUI/Core/PropertyHandlers/FormEditors/EnumComboBoxFormEditorProvider.cs index c8d86ff239..3516459bd8 100644 --- a/src/AutoUI/Core/PropertyHandlers/FormEditors/EnumComboBoxFormEditorProvider.cs +++ b/src/AutoUI/Core/PropertyHandlers/FormEditors/EnumComboBoxFormEditorProvider.cs @@ -42,7 +42,7 @@ public override DotvvmControl CreateControl(PropertyDisplayMetadata property, Au .SetCapability(props.Html) .SetProperty(c => c.Enabled, props.Enabled) .SetProperty(c => c.SelectionChanged, props.Changed) - .SetProperty(c => c.SelectedValue, (IValueBinding)context.CreateValueBinding(property)); + .SetProperty(c => c.SelectedValue, props.Property); if (isNullable) { diff --git a/src/AutoUI/Core/PropertyHandlers/GridColumns/CheckBoxGridColumnProvider.cs b/src/AutoUI/Core/PropertyHandlers/GridColumns/CheckBoxGridColumnProvider.cs index e8b0772665..1d2b46e7a5 100644 --- a/src/AutoUI/Core/PropertyHandlers/GridColumns/CheckBoxGridColumnProvider.cs +++ b/src/AutoUI/Core/PropertyHandlers/GridColumns/CheckBoxGridColumnProvider.cs @@ -15,7 +15,7 @@ public override bool CanHandleProperty(PropertyDisplayMetadata property, AutoUIC protected override GridViewColumn CreateColumnCore(PropertyDisplayMetadata property, AutoGridViewColumn.Props props, AutoUIContext context) { var column = new GridViewCheckBoxColumn(); - column.SetBinding(GridViewCheckBoxColumn.ValueBindingProperty, context.CreateValueBinding(property)); + column.SetBinding(GridViewCheckBoxColumn.ValueBindingProperty, props.Property); return column; } } 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/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/BindingHelper.cs b/src/Framework/Framework/Binding/BindingHelper.cs index 9e851eb2e2..fd2fb2264d 100644 --- a/src/Framework/Framework/Binding/BindingHelper.cs +++ b/src/Framework/Framework/Binding/BindingHelper.cs @@ -90,10 +90,29 @@ internal static (int stepsUp, DotvvmBindableObject target) FindDataContextTarget var changes = 0; foreach (var a in control.GetAllAncestors(includingThis: true)) { - if (bindingContext.Equals(a.GetDataContextType(inherit: false))) + var ancestorContext = a.GetDataContextType(inherit: false); + if (bindingContext.Equals(ancestorContext)) return (changes, a); - if (a.properties.Contains(DotvvmBindableObject.DataContextProperty)) changes++; + // count only client-side data contexts (DataContext={resource:} is skipped in JS) + if (a.properties.TryGet(DotvvmBindableObject.DataContextProperty, out var ancestorRuntimeContext)) + { + if (a.properties.TryGet(Internal.IsServerOnlyDataContextProperty, out var isServerOnly) && isServerOnly != null) + { + if (isServerOnly is false) + changes++; + } + else + { + // we only count bindings which are not value bindings as client-side + // scalar values (null or otherwise) are used by Repeater to the collection elements + // (since this logic got inlined into many other ItemsControls and is painful to debug, let's not change it) + if (ancestorRuntimeContext is null || + ancestorRuntimeContext is IValueBinding || + ancestorRuntimeContext is not IBinding) + changes++; + } + } } // try to get the real objects, to see which is wrong @@ -361,10 +380,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 = @@ -403,7 +424,7 @@ public static TControl SetDataContextTypeFromDataSource(this TControl return dataContextType; } - var (childType, extensionParameters, addLayer) = ApplyDataContextChange(dataContextType, property.DataContextChangeAttributes, obj, property); + var (childType, extensionParameters, addLayer, serverOnly) = ApplyDataContextChange(dataContextType, property.DataContextChangeAttributes, obj, property); if (!addLayer) { @@ -412,7 +433,7 @@ public static TControl SetDataContextTypeFromDataSource(this TControl } 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()); + else return DataContextStack.Create(childType, dataContextType, extensionParameters: extensionParameters.ToArray(), serverSideOnly: serverOnly); } /// Return the expected data context type for this property. Returns null if the type is unknown. @@ -432,7 +453,7 @@ public static DataContextStack GetDataContextType(this DotvvmProperty property, return dataContextType; } - var (childType, extensionParameters, addLayer) = ApplyDataContextChange(dataContextType, property.DataContextChangeAttributes, obj, property); + var (childType, extensionParameters, addLayer, serverOnly) = ApplyDataContextChange(dataContextType, property.DataContextChangeAttributes, obj, property); if (!addLayer) { @@ -443,14 +464,16 @@ public static DataContextStack GetDataContextType(this DotvvmProperty property, if (childType is null) childType = typeof(UnknownTypeSentinel); - return DataContextStack.Create(childType, dataContextType, extensionParameters: extensionParameters.ToArray()); + return DataContextStack.Create(childType, dataContextType, extensionParameters: extensionParameters.ToArray(), serverSideOnly: serverOnly); } - public static (Type? type, List extensionParameters, bool addLayer) ApplyDataContextChange(DataContextStack dataContext, DataContextChangeAttribute[] attributes, ResolvedControl control, DotvvmProperty? property) + public static (Type? type, List extensionParameters, bool addLayer, bool serverOnly) ApplyDataContextChange(DataContextStack dataContext, DataContextChangeAttribute[] attributes, ResolvedControl control, DotvvmProperty? property) { var type = ResolvedTypeDescriptor.Create(dataContext.DataContextType); var extensionParameters = new List(); var addLayer = false; + var serverOnly = dataContext.ServerSideOnly; + foreach (var attribute in attributes.OrderBy(a => a.Order)) { if (type == null) break; @@ -459,17 +482,19 @@ public static (Type? type, List extensionParameters, { addLayer = true; type = attribute.GetChildDataContextType(type, dataContext, control, property); + serverOnly = attribute.IsServerSideOnly(dataContext, control, property) ?? serverOnly; } } - return (ResolvedTypeDescriptor.ToSystemType(type), extensionParameters, addLayer); + return (ResolvedTypeDescriptor.ToSystemType(type), extensionParameters, addLayer, serverOnly); } - private static (Type? childType, List extensionParameters, bool addLayer) ApplyDataContextChange(DataContextStack dataContextType, DataContextChangeAttribute[] attributes, DotvvmBindableObject obj, DotvvmProperty property) + private static (Type? childType, List extensionParameters, bool addLayer, bool serverOnly) ApplyDataContextChange(DataContextStack dataContextType, DataContextChangeAttribute[] attributes, DotvvmBindableObject obj, DotvvmProperty property) { Type? type = dataContextType.DataContextType; var extensionParameters = new List(); var addLayer = false; + var serverOnly = dataContextType.ServerSideOnly; foreach (var attribute in attributes.OrderBy(a => a.Order)) { @@ -479,10 +504,11 @@ private static (Type? childType, List extensionParame { addLayer = true; type = attribute.GetChildDataContextType(type, dataContextType, obj, property); + serverOnly = attribute.IsServerSideOnly(dataContextType, obj, property) ?? serverOnly; } } - return (type, extensionParameters, addLayer); + return (type, extensionParameters, addLayer, serverOnly); } /// 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/ConstantDataContextChangeAttribute.cs b/src/Framework/Framework/Binding/ConstantDataContextChangeAttribute.cs index b0b82d8d3e..fe69276cb7 100644 --- a/src/Framework/Framework/Binding/ConstantDataContextChangeAttribute.cs +++ b/src/Framework/Framework/Binding/ConstantDataContextChangeAttribute.cs @@ -15,11 +15,13 @@ public class ConstantDataContextChangeAttribute : DataContextChangeAttribute public Type Type { get; } public override int Order { get; } + public bool? ServerSideOnly { get; } - public ConstantDataContextChangeAttribute(Type type, int order = 0) + public ConstantDataContextChangeAttribute(Type type, int order = 0, bool? serverSideOnly = null) { Type = type; Order = order; + ServerSideOnly = serverSideOnly; } public override ITypeDescriptor? GetChildDataContextType(ITypeDescriptor dataContext, IDataContextStack controlContextStack, IAbstractControl control, IPropertyDescriptor? property = null) @@ -31,5 +33,10 @@ public ConstantDataContextChangeAttribute(Type type, int order = 0) { return Type; } + + public override bool? IsServerSideOnly(IDataContextStack controlContextStack, IAbstractControl control, IPropertyDescriptor? property = null) => + ServerSideOnly; + public override bool? IsServerSideOnly(DataContextStack controlContextStack, DotvvmBindableObject control, DotvvmProperty? property = null) => + ServerSideOnly; } } diff --git a/src/Framework/Framework/Binding/ControlPropertyBindingDataContextChangeAttribute.cs b/src/Framework/Framework/Binding/ControlPropertyBindingDataContextChangeAttribute.cs index be4ca4606b..405d7ebfe3 100644 --- a/src/Framework/Framework/Binding/ControlPropertyBindingDataContextChangeAttribute.cs +++ b/src/Framework/Framework/Binding/ControlPropertyBindingDataContextChangeAttribute.cs @@ -7,6 +7,8 @@ using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Controls; +using DotVVM.Framework.Utils; +using FastExpressionCompiler; namespace DotVVM.Framework.Binding { @@ -65,17 +67,16 @@ void ThrowDataContextMismatch(IAbstractBinding binding) public override Type? GetChildDataContextType(Type dataContext, DataContextStack controlContextStack, DotvvmBindableObject control, DotvvmProperty? property = null) { var controlType = control.GetType(); - var controlPropertyField = controlType.GetField($"{PropertyName}Property", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy); - var controlProperty = (DotvvmProperty?)controlPropertyField?.GetValue(null); + var controlProperty = DotvvmProperty.ResolveProperty(controlType, PropertyName); if (controlProperty == null) { - throw new Exception($"The property '{PropertyName}' was not found on control '{controlType}'!"); + throw new Exception($"The property '{PropertyName}' was not found on control '{controlType.ToCode()}'!"); } if (control.properties.Contains(controlProperty)) { - return control.GetValueBinding(controlProperty) is IValueBinding valueBinding + return control.GetBinding(controlProperty) is IStaticValueBinding valueBinding ? valueBinding.ResultType : dataContext; } @@ -85,9 +86,32 @@ void ThrowDataContextMismatch(IAbstractBinding binding) return dataContext; } - throw new Exception($"Property '{PropertyName}' is required on '{controlType.Name}'."); + throw new Exception($"Property '{PropertyName}' is required on '{controlType.ToCode()}'."); } public override IEnumerable PropertyDependsOn => new[] { PropertyName }; + + public override bool? IsServerSideOnly(DataContextStack controlContextStack, DotvvmBindableObject control, DotvvmProperty? property = null) + { + var controlProperty = DotvvmProperty.ResolveProperty(control.GetType(), PropertyName); + if (controlProperty is null) + return null; + + var binding = control.GetBinding(controlProperty); + return binding is not IValueBinding or null; + } + + public override bool? IsServerSideOnly(IDataContextStack controlContextStack, IAbstractControl control, IPropertyDescriptor? property = null) + { + if (!control.Metadata.TryGetProperty(PropertyName, out var controlProperty)) + return null; + if (!control.TryGetProperty(controlProperty, out var setter)) + return null; + + return + setter is IAbstractPropertyBinding { Binding.BindingType: var bindingType } && + !typeof(IValueBinding).IsAssignableFrom(bindingType); + + } } } 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/Binding/DataContextChangeAttribute.cs b/src/Framework/Framework/Binding/DataContextChangeAttribute.cs index 3c07521828..1c009f1d7c 100644 --- a/src/Framework/Framework/Binding/DataContextChangeAttribute.cs +++ b/src/Framework/Framework/Binding/DataContextChangeAttribute.cs @@ -34,6 +34,11 @@ public abstract class DataContextChangeAttribute : Attribute /// Gets the extension parameters that should be made available to the bindings inside. public virtual IEnumerable GetExtensionParameters(ITypeDescriptor dataContext) => Enumerable.Empty(); + /// Gets if the nested data context is available only on server or also client-side, controls the property. `null` means that it should be inherited from the parent context. + public virtual bool? IsServerSideOnly(IDataContextStack controlContextStack, IAbstractControl control, IPropertyDescriptor? property = null) => null; + /// Gets if the nested data context is available only on server or also client-side, controls the property. `null` means that it should be inherited from the parent context. + public virtual bool? IsServerSideOnly(DataContextStack controlContextStack, DotvvmBindableObject control, DotvvmProperty? property = null) => null; + /// Gets a list of attributes that need to be resolved before this attribute is invoked. public virtual IEnumerable PropertyDependsOn => Enumerable.Empty(); } 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 e29eb171f0..694801823f 100644 --- a/src/Framework/Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs +++ b/src/Framework/Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs @@ -174,12 +174,20 @@ expression is JsNewExpression || public ResultTypeBindingProperty GetResultType(ParsedExpressionBindingProperty expression) => new ResultTypeBindingProperty(expression.Expression.Type); - public ExpectedTypeBindingProperty GetExpectedType(AssignedPropertyBindingProperty? property = null) + public ExpectedTypeBindingProperty? GetExpectedType(AssignedPropertyBindingProperty? property = null) { var prop = property?.DotvvmProperty; - if (prop == null) return new ExpectedTypeBindingProperty(typeof(object)); + if (prop == null) return null; - return new ExpectedTypeBindingProperty(prop.IsBindingProperty ? (prop.PropertyType.GenericTypeArguments.SingleOrDefault() ?? typeof(object)) : prop.PropertyType); + if (prop.IsBindingProperty) + { + if (prop.PropertyType.GenericTypeArguments.SingleOrDefault() is {} type) + return new ExpectedTypeBindingProperty(type); + else + return null; + } + + return new ExpectedTypeBindingProperty(prop.PropertyType); } public BindingCompilationRequirementsAttribute GetAdditionalResolversFromProperty(AssignedPropertyBindingProperty property) @@ -264,9 +272,9 @@ public NegatedBindingExpression NegateBinding(ParsedExpressionBindingProperty e, (Expression)Expression.Not(e.Expression) )); } - public ExpectedAsStringBindingExpression ExpectAsStringBinding(ParsedExpressionBindingProperty e, ExpectedTypeBindingProperty expectedType, IBinding binding) + public ExpectedAsStringBindingExpression ExpectAsStringBinding(ParsedExpressionBindingProperty e, IBinding binding, ExpectedTypeBindingProperty? expectedType = null) { - if (expectedType.Type == typeof(string)) + if (expectedType is {} && expectedType.Type == typeof(string)) return new(binding); return new(binding.DeriveBinding(new ExpectedTypeBindingProperty(typeof(string)), e)); @@ -425,11 +433,12 @@ public ThisBindingProperty GetThisBinding(IBinding binding, DataContextStack sta return new ThisBindingProperty(thisBinding); } - public CollectionElementDataContextBindingProperty GetCollectionElementDataContext(DataContextStack dataContext, ResultTypeBindingProperty resultType) + public CollectionElementDataContextBindingProperty GetCollectionElementDataContext(DataContextStack dataContext, ResultTypeBindingProperty resultType, IBinding binding) { return new CollectionElementDataContextBindingProperty(DataContextStack.CreateCollectionElement( ReflectionUtils.GetEnumerableType(resultType.Type).NotNull(), - dataContext + parent: dataContext, + serverSideOnly: binding is not IValueBinding )); } diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs index 59fd861451..d10f168069 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs @@ -14,6 +14,8 @@ using DotVVM.Framework.Compilation.Binding; using DotVVM.Framework.Compilation.ViewCompiler; using DotVVM.Framework.Configuration; +using DotVVM.Framework.Binding.Expressions; +using FastExpressionCompiler; namespace DotVVM.Framework.Compilation.ControlTree { @@ -284,13 +286,18 @@ private IAbstractControl ProcessObjectElement(DothtmlElementNode element, IDataC if (control.TryGetProperty(DotvvmBindableObject.DataContextProperty, out var dataContextProperty) && dataContextProperty is IAbstractPropertyBinding { Binding: var dataContextBinding }) { + var serverOnly = !typeof(IValueBinding).IsAssignableFrom(dataContextBinding.BindingType); if (dataContextBinding?.ResultType != null) { - dataContext = CreateDataContextTypeStack(dataContextBinding.ResultType, parentDataContextStack: dataContext); + dataContext = CreateDataContextTypeStack( + dataContextBinding.ResultType, + parentDataContextStack: dataContext, + serverSideOnly: serverOnly + ); } else { - dataContext = CreateDataContextTypeStack(null, dataContext); + dataContext = CreateDataContextTypeStack(null, dataContext, serverSideOnly: serverOnly); } control.DataContextTypeStack = dataContext; } @@ -401,12 +408,19 @@ 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 var textValue = (DothtmlValueTextNode)attribute.ValueNode; try @@ -842,11 +856,11 @@ protected virtual bool IsControlProperty(IPropertyDescriptor property) try { - var (type, extensionParameters, addLayer) = ApplyContextChange(dataContext, attributes, control, property); + var (type, extensionParameters, addLayer, serverOnly) = 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()); + else return CreateDataContextTypeStack(type, parentDataContextStack: dataContext, extensionParameters: extensionParameters.ToArray(), serverSideOnly: serverOnly); } catch (Exception exception) { @@ -857,8 +871,9 @@ protected virtual bool IsControlProperty(IPropertyDescriptor property) } } - public static (ITypeDescriptor? type, List extensionParameters, bool addLayer) ApplyContextChange(IDataContextStack dataContext, DataContextChangeAttribute[] attributes, IAbstractControl control, IPropertyDescriptor? property) + public static (ITypeDescriptor? type, List extensionParameters, bool addLayer, bool serverOnly) ApplyContextChange(IDataContextStack dataContext, DataContextChangeAttribute[] attributes, IAbstractControl control, IPropertyDescriptor? property) { + var serverOnly = dataContext.ServerSideOnly; var type = dataContext.DataContextType; var extensionParameters = new List(); var addLayer = false; @@ -870,10 +885,10 @@ public static (ITypeDescriptor? type, List extensionP { addLayer = true; type = attribute.GetChildDataContextType(type, dataContext, control, property); + serverOnly = attribute.IsServerSideOnly(dataContext, control, property) ?? serverOnly; } - } - return (type, extensionParameters, addLayer); + return (type, extensionParameters, addLayer, serverOnly); } @@ -885,7 +900,7 @@ public static (ITypeDescriptor? type, List extensionP /// /// Creates the data context type stack object. /// - protected abstract IDataContextStack CreateDataContextTypeStack(ITypeDescriptor? viewModelType, IDataContextStack? parentDataContextStack = null, IReadOnlyList? imports = null, IReadOnlyList? extensionParameters = null); + protected abstract IDataContextStack CreateDataContextTypeStack(ITypeDescriptor? viewModelType, IDataContextStack? parentDataContextStack = null, IReadOnlyList? imports = null, IReadOnlyList? extensionParameters = null, bool serverSideOnly = false); /// /// Converts the value to the property type. diff --git a/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs b/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs index f43a04f963..63bf1031d7 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs @@ -26,20 +26,33 @@ public sealed class DataContextStack : IDataContextStack public ImmutableArray ExtensionParameters { get; } /// Extension property resolvers added by data context change attributes. public ImmutableArray BindingPropertyResolvers { get; } + /// When true, this data context is not available client-side, because `DataContext={resource: ...}` was used in the markup. Only resource and command bindings can use this data context. + public bool ServerSideOnly { get; } private readonly int hashCode; - private DataContextStack(Type type, DataContextStack? parent = null, IReadOnlyList? imports = null, IReadOnlyList? extensionParameters = null, - IReadOnlyList? bindingPropertyResolvers = null) + IReadOnlyList? bindingPropertyResolvers = null, + bool serverSideOnly = false) { Parent = parent; DataContextType = type; NamespaceImports = imports?.ToImmutableArray() ?? parent?.NamespaceImports ?? ImmutableArray.Empty; ExtensionParameters = extensionParameters?.ToImmutableArray() ?? ImmutableArray.Empty; 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(); } @@ -107,6 +120,21 @@ public IEnumerable Parents() } } + public bool IsAncestorOf(DataContextStack x) + { + var c = x.Parent; + while (c != null) + { + if (this.hashCode == c.hashCode) + { + if (this.Equals(c)) + return true; + } + c = c.Parent; + } + return false; + } + ITypeDescriptor IDataContextStack.DataContextType => new ResolvedTypeDescriptor(DataContextType); IDataContextStack? IDataContextStack.Parent => Parent; @@ -144,9 +172,7 @@ int ComputeHashCode() hashCode += parameter.GetHashCode(); } - hashCode = (hashCode * 397) ^ (Parent?.GetHashCode() ?? 0); - hashCode = (hashCode * 13) ^ (DataContextType?.FullName?.GetHashCode() ?? 0); - return hashCode; + return (hashCode, Parent, DataContextType?.FullName, ServerSideOnly).GetHashCode(); } } @@ -154,6 +180,7 @@ public override string ToString() { string?[] features = new [] { $"type={this.DataContextType.ToCode()}", + this.ServerSideOnly ? "server-side-only" : null, this.NamespaceImports.Any() ? "imports=[" + string.Join(", ", this.NamespaceImports) + "]" : null, this.ExtensionParameters.Any() ? "ext=[" + string.Join(", ", this.ExtensionParameters.Select(e => e.Identifier + ": " + e.ParameterType.CSharpName)) + "]" : null, this.BindingPropertyResolvers.Any() ? "resolvers=[" + string.Join(", ", this.BindingPropertyResolvers.Select(s => s.Method)) + "]" : null, @@ -168,9 +195,10 @@ public static DataContextStack Create(Type type, DataContextStack? parent = null, IReadOnlyList? imports = null, IReadOnlyList? extensionParameters = null, - IReadOnlyList? bindingPropertyResolvers = null) + IReadOnlyList? bindingPropertyResolvers = null, + bool serverSideOnly = false) { - var dcs = new DataContextStack(type, parent, imports, extensionParameters, bindingPropertyResolvers); + var dcs = new DataContextStack(type, parent, imports, extensionParameters, bindingPropertyResolvers, serverSideOnly); return dcs;// internCache.GetValue(dcs, _ => dcs); } @@ -180,7 +208,8 @@ public static DataContextStack CreateCollectionElement(Type elementType, DataContextStack? parent = null, IReadOnlyList? imports = null, IReadOnlyList? extensionParameters = null, - IReadOnlyList? bindingPropertyResolvers = null) + IReadOnlyList? bindingPropertyResolvers = null, + bool serverSideOnly = false) { var indexParameters = new CollectionElementDataContextChangeAttribute(0).GetExtensionParameters(new ResolvedTypeDescriptor(elementType.MakeArrayType())); extensionParameters = extensionParameters is null ? indexParameters.ToArray() : extensionParameters.Concat(indexParameters).ToArray(); @@ -188,7 +217,8 @@ public static DataContextStack CreateCollectionElement(Type elementType, elementType, parent, imports: imports, extensionParameters: extensionParameters, - bindingPropertyResolvers: bindingPropertyResolvers + bindingPropertyResolvers: bindingPropertyResolvers, + serverSideOnly: serverSideOnly ); } } diff --git a/src/Framework/Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs b/src/Framework/Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs index fa19c3b7ef..6004562389 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs @@ -41,13 +41,15 @@ protected override IControlType CreateControlType(ITypeDescriptor wrapperType, s return new ControlType(ResolvedTypeDescriptor.ToSystemType(wrapperType), virtualPath: virtualPath); } - protected override IDataContextStack CreateDataContextTypeStack(ITypeDescriptor? viewModelType, IDataContextStack? parentDataContextStack = null, IReadOnlyList? namespaceImports = null, IReadOnlyList? extensionParameters = null) + protected override IDataContextStack CreateDataContextTypeStack(ITypeDescriptor? viewModelType, IDataContextStack? parentDataContextStack = null, IReadOnlyList? namespaceImports = null, IReadOnlyList? extensionParameters = null, bool serverSideOnly = false) { return DataContextStack.Create( ResolvedTypeDescriptor.ToSystemType(viewModelType) ?? typeof(UnknownTypeSentinel), parentDataContextStack as DataContextStack, - namespaceImports, extensionParameters); + namespaceImports, + extensionParameters, + serverSideOnly: serverSideOnly); } protected override IAbstractBinding CompileBinding(DothtmlBindingNode node, BindingParserOptions bindingOptions, IDataContextStack context, IPropertyDescriptor property) diff --git a/src/Framework/Framework/Compilation/ControlTree/IDataContextStack.cs b/src/Framework/Framework/Compilation/ControlTree/IDataContextStack.cs index 3b5f951fab..bb23f31e0c 100644 --- a/src/Framework/Framework/Compilation/ControlTree/IDataContextStack.cs +++ b/src/Framework/Framework/Compilation/ControlTree/IDataContextStack.cs @@ -14,5 +14,7 @@ public interface IDataContextStack ImmutableArray ExtensionParameters { get; } IEnumerable<(int dataContextLevel, BindingExtensionParameter parameter)> GetCurrentExtensionParameters(); + bool ServerSideOnly { get; } + } } diff --git a/src/Framework/Framework/Compilation/ControlTree/UsedPropertiesFindingVisitor.cs b/src/Framework/Framework/Compilation/ControlTree/UsedPropertiesFindingVisitor.cs index aaddedaba7..e90ffa26cf 100644 --- a/src/Framework/Framework/Compilation/ControlTree/UsedPropertiesFindingVisitor.cs +++ b/src/Framework/Framework/Compilation/ControlTree/UsedPropertiesFindingVisitor.cs @@ -41,7 +41,7 @@ protected override Expression VisitMember(MemberExpression node) protected override Expression VisitParameter(ParameterExpression node) { - if (node.GetParameterAnnotation() is { DataContext: { Parent: null } } annotation) + if (node.GetParameterAnnotation() is { DataContext: { Parent: null }, ExtensionParameter: null } annotation) UsesViewModel = true; return base.VisitParameter(node); } diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslationVisitor.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslationVisitor.cs index 39199180ed..292e888475 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslationVisitor.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslationVisitor.cs @@ -14,6 +14,7 @@ using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel; using DotVVM.Framework.ViewModel.Serialization; +using FastExpressionCompiler; namespace DotVVM.Framework.Compilation.Javascript { @@ -27,7 +28,12 @@ public class JavascriptTranslationVisitor public bool WriteUnknownParameters { get; set; } = true; public JavascriptTranslationVisitor(DataContextStack dataContext, IJavascriptMethodTranslator translator) { - this.ContextMap = dataContext.EnumerableItems().Select((a, i) => (a, i)).ToDictionary(a => a.Item1, a => a.Item2); + this.ContextMap = + dataContext + .EnumerableItems() + .Where(c => !c.ServerSideOnly) // server-side only data contexts are not present at all client-side, so we need to skip them before assigning indices + .Select((a, i) => (a, i)) + .ToDictionary(a => a.a, a => a.i); this.DataContext = dataContext; this.Translator = translator; } @@ -196,6 +202,9 @@ public static JsExpression BuildIndexer(JsExpression target, JsExpression index, public JsExpression TranslateParameter(Expression expression, BindingParameterAnnotation annotation) { + if (annotation.DataContext is { ServerSideOnly: true }) + throw new NotSupportedException($"{expression} of type {annotation.DataContext.DataContextType.ToCode(stripNamespace: true)} cannot be translated to JavaScript, it can only be used in resource and command bindings. This is most likely because the data context is bound to a resource binding."); + JsExpression getDataContext(int parentContexts) { JsExpression context = new JsSymbolicParameter(JavascriptTranslator.KnockoutContextParameter); 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/DataItemContainer.cs b/src/Framework/Framework/Controls/DataItemContainer.cs index 6ba0c7bd61..e56ba90f6e 100644 --- a/src/Framework/Framework/Controls/DataItemContainer.cs +++ b/src/Framework/Framework/Controls/DataItemContainer.cs @@ -56,16 +56,17 @@ public int? DataItemIndex set { this.index = value; SetValue(Internal.UniqueIDProperty, value?.ToString()); } } + public bool RenderItemBinding { get; set; } = true; protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext context) { var maybeIndex = DataItemIndex; - if (maybeIndex is int index) + if (RenderItemBinding && maybeIndex is int index) writer.WriteKnockoutDataBindComment("dotvvm-SSR-item", index.ToString()); base.RenderControl(writer, context); - if (maybeIndex is int) + if (RenderItemBinding && maybeIndex is int) writer.WriteKnockoutDataBindEndComment(); } } 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/DotvvmControlProperties.cs b/src/Framework/Framework/Controls/DotvvmControlProperties.cs index 468cb79e57..5a254b4b10 100644 --- a/src/Framework/Framework/Controls/DotvvmControlProperties.cs +++ b/src/Framework/Framework/Controls/DotvvmControlProperties.cs @@ -388,9 +388,11 @@ public DotvvmPropertyDictionary(DotvvmBindableObject control) public object? this[DotvvmProperty key] { get => control.properties.GetOrThrow(key); set => control.properties.Set(key, value); } - public ICollection Keys => throw new NotImplementedException(); + public KeysCollection Keys => new KeysCollection(control); + ICollection IDictionary.Keys => Keys; - public ICollection Values => throw new NotImplementedException(); + public ValuesCollection Values => new ValuesCollection(control); + ICollection IDictionary.Values => Values; public int Count => control.properties.Count(); public bool IsReadOnly => false; @@ -442,5 +444,72 @@ public bool TryGetValue(DotvvmProperty key, out object? value) => IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); IEnumerator> IEnumerable>.GetEnumerator() => this.GetEnumerator(); + + + public readonly struct KeysCollection : ICollection, IReadOnlyCollection + { + private readonly DotvvmBindableObject control; + + public KeysCollection(DotvvmBindableObject control) { this.control = control; } + public int Count => control.properties.Count(); + + public bool IsReadOnly => true; + public void Add(DotvvmProperty item) => throw new NotSupportedException("Adding a property without value doesn't make sense"); + public void Clear() => throw new NotSupportedException("Explicitly use control.Properties.Clear() instead."); + public bool Contains(DotvvmProperty item) => control.properties.Contains(item); + public void CopyTo(DotvvmProperty[] array, int arrayIndex) + { + foreach (var x in control.properties) + { + array[arrayIndex++] = x.Key; + } + } + public IEnumerator GetEnumerator() + { + foreach (var x in control.properties) + { + yield return x.Key; + } + } + public bool Remove(DotvvmProperty item) => throw new NotSupportedException("Explicitly use control.Properties.Remove() instead."); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + public struct ValuesCollection : ICollection + { + private readonly DotvvmBindableObject control; + + public ValuesCollection(DotvvmBindableObject control) { this.control = control; } + public int Count => control.properties.Count(); + + public bool IsReadOnly => true; + public void Add(object? item) => throw new NotSupportedException("Adding a value without property doesn't make sense"); + public void Clear() => throw new NotSupportedException("Explicitly use control.Properties.Clear() instead."); + public bool Contains(object? item) + { + foreach (var x in control.properties) + { + if (Object.Equals(x.Value, item)) + return true; + } + return false; + } + public void CopyTo(object?[] array, int arrayIndex) + { + foreach (var x in control.properties) + { + array[arrayIndex++] = x.Value; + } + } + public IEnumerator GetEnumerator() + { + foreach (var x in control.properties) + { + yield return x.Value; + } + } + public bool Remove(object? item) => throw new NotSupportedException("Explicitly use control.Properties.Remove() instead."); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } } diff --git a/src/Framework/Framework/Controls/DotvvmMarkupControl.cs b/src/Framework/Framework/Controls/DotvvmMarkupControl.cs index 87537938db..8ec7004639 100644 --- a/src/Framework/Framework/Controls/DotvvmMarkupControl.cs +++ b/src/Framework/Framework/Controls/DotvvmMarkupControl.cs @@ -74,6 +74,17 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest { var properties = new KnockoutBindingGroup(); var usedProperties = GetValue(Internal.UsedPropertiesInfoProperty); + + if (usedProperties?.UsesViewModelClientSide == true) + { + // check that the markup control isn't used in resource-binding only data context + var dataContext = this.GetDataContextType(); + if (dataContext?.ServerSideOnly == true) + { + throw new DotvvmControlException(this, $"Markup control '{GetType().Name}' cannot be used in resource-binding only data context, because it uses value bindings on the data context."); + } + } + foreach (var p in usedProperties?.ClientSideUsedProperties ?? GetDeclaredProperties()) { if (p.DeclaringType.IsAssignableFrom(typeof(DotvvmMarkupControl))) diff --git a/src/Framework/Framework/Controls/EmptyData.cs b/src/Framework/Framework/Controls/EmptyData.cs index dbfabeec80..cebda2ebd2 100644 --- a/src/Framework/Framework/Controls/EmptyData.cs +++ b/src/Framework/Framework/Controls/EmptyData.cs @@ -49,14 +49,15 @@ public EmptyData() protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext context) { + var dataSourceBinding = GetValueBinding(DataSourceProperty); TagName = WrapperTagName; - // if RenderOnServer && DataSource is not empty then don't render anything - if (!RenderOnServer || GetIEnumerableFromDataSource()?.GetEnumerator()?.MoveNext() != true) + // if DataSource is resource binding && DataSource is not empty then don't render anything + if (dataSourceBinding is {} || GetIEnumerableFromDataSource()?.GetEnumerator()?.MoveNext() != true) { - if (!RenderOnServer) + if (dataSourceBinding is {}) { - var visibleBinding = GetBinding(DataSourceProperty) - .NotNull("DataSource property must contain a binding") + var visibleBinding = + dataSourceBinding .GetProperty().Binding .GetProperty().Binding .GetProperty().Binding diff --git a/src/Framework/Framework/Controls/GridView.cs b/src/Framework/Framework/Controls/GridView.cs index cc8b82367c..3665a64d96 100644 --- a/src/Framework/Framework/Controls/GridView.cs +++ b/src/Framework/Framework/Controls/GridView.cs @@ -202,6 +202,7 @@ private void DataBind(IDotvvmRequestContext context) head = null; var dataSourceBinding = GetDataSourceBinding(); + var serverOnly = dataSourceBinding is not IValueBinding; var dataSource = DataSource; CreateHeaderRow(context); @@ -212,9 +213,10 @@ 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.IsServerOnlyDataContextProperty, serverOnly); placeholder.SetValue(Internal.PathFragmentProperty, GetPathFragmentExpression() + "/[" + index + "]"); placeholder.ID = index.ToString(); Children.Add(placeholder); @@ -331,15 +333,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 { @@ -471,19 +467,22 @@ protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext head?.Render(writer, context); // render body - var foreachBinding = GetForeachDataBindExpression().GetKnockoutBindingExpression(this); - 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; @@ -519,8 +518,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); @@ -528,7 +529,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); } @@ -542,7 +544,7 @@ protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext c { base.RenderEndTag(writer, context); - if (!ShowHeaderWhenNoData) + if (!ShowHeaderWhenNoData && GetForeachDataBindExpression() is IValueBinding) { writer.WriteKnockoutDataBindEndComment(); } @@ -552,20 +554,23 @@ 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 = JsonSerializer.Serialize(mapping, new JsonSerializerOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); - - var helperBinding = new KnockoutBindingGroup(); - helperBinding.Add("dataSet", GetDataSourceBinding().GetKnockoutBindingExpression(this, unwrapped: true)); - helperBinding.Add("mapping", mappingJson); - if (this.LoadData is { } loadData) + if (GetBinding(DataSourceProperty) is IValueBinding dataBinding) { - var loadDataExpression = KnockoutHelper.GenerateClientPostbackLambda("LoadData", loadData, this, new PostbackScriptOptions(elementAccessor: "$element", koContext: CodeParameterAssignment.FromIdentifier("$context"))); - helperBinding.Add("loadDataSet", loadDataExpression); + var itemType = ReflectionUtils.GetEnumerableType(GetDataSourceBinding().ResultType); + var userColumnMappingService = context.Services.GetRequiredService(); + var mapping = userColumnMappingService.GetMapping(itemType!); + var mappingJson = JsonSerializer.Serialize(mapping, new JsonSerializerOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + + var helperBinding = new KnockoutBindingGroup(); + helperBinding.Add("dataSet", dataBinding.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()); } - writer.AddKnockoutDataBind("dotvvm-gridviewdataset", helperBinding.ToString()); base.AddAttributesToRender(writer, context); } diff --git a/src/Framework/Framework/Controls/GridViewBindings.cs b/src/Framework/Framework/Controls/GridViewBindings.cs index 186352857e..a9cafe2c39 100644 --- a/src/Framework/Framework/Controls/GridViewBindings.cs +++ b/src/Framework/Framework/Controls/GridViewBindings.cs @@ -7,15 +7,15 @@ namespace DotVVM.Framework.Controls /// Contains pre-created command and value bindings for the components. An instance can be obtained from public class GridViewBindings { - private readonly ConcurrentDictionary> isSortColumnAscending = new(concurrencyLevel: 1, capacity: 16); - private readonly ConcurrentDictionary> isSortColumnDescending = new(concurrencyLevel: 1, capacity: 16); + private readonly ConcurrentDictionary> isSortColumnAscending = new(concurrencyLevel: 1, capacity: 16); + private readonly ConcurrentDictionary> isSortColumnDescending = new(concurrencyLevel: 1, capacity: 16); public ICommandBinding? SetSortExpression { get; init; } - internal IValueBinding>? IsColumnSortedAscending { get; init; } - internal IValueBinding>? IsColumnSortedDescending { get; init; } + internal IStaticValueBinding>? IsColumnSortedAscending { get; init; } + internal IStaticValueBinding>? IsColumnSortedDescending { get; init; } - public IValueBinding? GetIsColumnSortedAscendingBinding(string sortExpression) + public IStaticValueBinding? GetIsColumnSortedAscendingBinding(string sortExpression) { if (IsColumnSortedAscending == null) { @@ -24,7 +24,7 @@ public class GridViewBindings return isSortColumnAscending.GetOrAdd(sortExpression, _ => IsColumnSortedAscending.Select(a => a(sortExpression))); } - public IValueBinding? GetIsColumnSortedDescendingBinding(string sortExpression) + public IStaticValueBinding? GetIsColumnSortedDescendingBinding(string sortExpression) { if (IsColumnSortedDescending == null) { diff --git a/src/Framework/Framework/Controls/GridViewCheckBoxColumn.cs b/src/Framework/Framework/Controls/GridViewCheckBoxColumn.cs index c67a92f6b3..e42a47b638 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 99b9490a5c..36e994668f 100644 --- a/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs +++ b/src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs @@ -22,8 +22,8 @@ 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), GridViewBindings> gridViewCommands = 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,19 +31,20 @@ 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)); } /// 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) + public GridViewBindings GetGridViewBindings(DataContextStack dataContextStack, IStaticValueBinding dataSetBinding, GridViewDataSetCommandType commandType) { 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,32 +109,35 @@ 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) }; } - private GridViewBindings GetGridViewBindingsCore(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType) + private GridViewBindings GetGridViewBindingsCore(DataContextStack dataContextStack, IStaticValueBinding dataSetBinding, GridViewDataSetCommandType commandType) { + var isServerOnly = dataSetBinding is not IValueBinding; var dataSetExpr = dataSetBinding.GetProperty().Expression; ICommandBinding? GetCommandOrNull(DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression) { @@ -141,10 +145,10 @@ private GridViewBindings GetGridViewBindingsCore(DataContextStack dataContextSta ? CreateCommandBinding(commandType, dataSetExpr, dataContextStack, methodName, arguments, transformExpression) : null; } - IValueBinding? GetValueBindingOrNull(DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression) + IStaticValueBinding? GetValueBindingOrNull(DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression) { return typeof(T).IsAssignableFrom(dataSetExpr.Type) - ? CreateValueBinding(dataSetExpr, dataContextStack, methodName, arguments, transformExpression) + ? CreateValueBinding(dataSetExpr, dataContextStack, methodName, arguments, transformExpression, isServerOnly) : null; } @@ -172,7 +176,7 @@ private GridViewBindings GetGridViewBindingsCore(DataContextStack dataContextSta }; } - private IValueBinding CreateValueBinding(Expression dataSet, DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression = null) + private IStaticValueBinding CreateValueBinding(Expression dataSet, DataContextStack dataContextStack, string methodName, Expression[] arguments, Func? transformExpression = null, bool isServerOnly = false) { // get concrete type from implementation of IXXXableGridViewDataSet var optionsConcreteType = GetOptionsConcreteType(dataSet.Type, out var optionsProperty); @@ -188,12 +192,19 @@ private IValueBinding CreateValueBinding(Ex expression = transformExpression(expression); } - return new ValueBindingExpression(service, - new object[] - { - new ParsedExpressionBindingProperty(expression), - dataContextStack - }); + 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, 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 2d1c23c65d..cf0b946285 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); /// Whether to automatically attach Validator.Value onto the TextBox or add a standalone Validator component. [MarkupOptions(AllowBinding = false)] @@ -76,13 +76,17 @@ 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(); CopyPropertyRaw(FormatStringProperty, literal, Literal.FormatStringProperty); CopyPropertyRaw(UITests.NameProperty, literal, UITests.NameProperty); - literal.SetBinding(Literal.TextProperty, ValueBinding); - Validator.Place(literal, container.Children, ValueBinding, ValidatorPlacement); + literal.SetBinding(Literal.TextProperty, binding); + Validator.Place(literal, container.Children, binding as IValueBinding, ValidatorPlacement); container.Children.Add(literal); } @@ -96,10 +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); - Validator.Place(textBox, container.Children, ValueBinding, ValidatorPlacement); + Validator.Place(textBox, container.Children, binding as IValueBinding, ValidatorPlacement); container.Children.Add(textBox); } } diff --git a/src/Framework/Framework/Controls/HierarchyRepeater.cs b/src/Framework/Framework/Controls/HierarchyRepeater.cs index c8201d01ae..2b7148be8b 100644 --- a/src/Framework/Framework/Controls/HierarchyRepeater.cs +++ b/src/Framework/Framework/Controls/HierarchyRepeater.cs @@ -7,8 +7,10 @@ using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Binding.Properties; +using DotVVM.Framework.Compilation.ControlTree.Resolved; using DotVVM.Framework.Compilation.Javascript; using DotVVM.Framework.Compilation.Javascript.Ast; +using DotVVM.Framework.Compilation.Validation; using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; using DotVVM.Framework.ResourceManagement; @@ -40,14 +42,14 @@ public HierarchyRepeater() : base("div") [ControlPropertyBindingDataContextChange(nameof(DataSource))] [BindingCompilationRequirements(new[] { typeof(DataSourceAccessBinding) }, new[] { typeof(DataSourceLengthBinding) })] [MarkupOptions(Required = true)] - public IValueBinding>? ItemChildrenBinding + public IStaticValueBinding>? ItemChildrenBinding { - get => (IValueBinding>?)GetValue(ItemChildrenBindingProperty); + get => (IStaticValueBinding>?)GetValue(ItemChildrenBindingProperty); set => SetValue(ItemChildrenBindingProperty, value); } public static readonly DotvvmProperty ItemChildrenBindingProperty - = DotvvmProperty.Register>?, HierarchyRepeater>(t => t.ItemChildrenBinding); + = DotvvmProperty.Register>?, HierarchyRepeater>(t => t.ItemChildrenBinding); /// /// Gets or sets the template for each HierarchyRepeater item. @@ -148,7 +150,7 @@ protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext c protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext context) { - if (RenderOnServer) + if (clientRootLevel is null) { foreach (var child in Children.Except(new[] { emptyDataContainer!, clientItemTemplate! })) { @@ -157,7 +159,7 @@ protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext } else { - clientRootLevel!.Render(writer, context); + clientRootLevel.Render(writer, context); } } @@ -167,12 +169,17 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat emptyDataContainer = null; clientItemTemplate = null; - if (DataSource is not null) + if (GetIEnumerableFromDataSource() is {} enumerable) { - CreateServerLevel(this.Children, context, GetIEnumerableFromDataSource()!); + CreateServerLevel(this.Children, + context, + enumerable, + parentPath: ImmutableArray.Empty, + foreachExpression: this.TryGetKnockoutForeachExpression() + ); } - if (renderClientTemplate) + if (renderClientTemplate && GetDataSourceBinding() is IValueBinding) { // whenever possible, we use the dotvvm deterministic ids, but if we are in a client-side template, // we'd get a binding... so we just generate a random Guid, not ideal but it will work. @@ -186,7 +193,7 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat Children.Add(clientRootLevel); clientRootLevel.Children.Add(new HierarchyRepeaterLevel { IsRoot = true, - ForeachExpression = GetForeachDataBindExpression().GetKnockoutBindingExpression(this), + ForeachExpression = this.TryGetKnockoutForeachExpression().NotNull(), ItemTemplateId = clientItemTemplateId, }); } @@ -201,19 +208,9 @@ private DotvvmControl CreateServerLevel( IList c, IDotvvmRequestContext context, IEnumerable items, - ImmutableArray parentPath = default, - string? foreachExpression = default) + ImmutableArray parentPath, + string? foreachExpression) { - if (parentPath.IsDefault) - { - parentPath = ImmutableArray.Empty; - } - - foreachExpression ??= ((IValueBinding)GetDataSourceBinding() - .GetProperty() - .Binding) - .GetKnockoutBindingExpression(this); - var dataContextLevelWrapper = new HierarchyRepeaterLevel { ForeachExpression = foreachExpression }; @@ -229,7 +226,7 @@ private DotvvmControl CreateServerLevel( var index = 0; foreach (var item in items) { - CreateServerItem(levelWrapper.Children, context, item, parentPath, index); + CreateServerItem(levelWrapper.Children, context, item, parentPath, index, foreachExpression is null); index++; } return dataContextLevelWrapper; @@ -240,14 +237,16 @@ private DotvvmControl CreateServerItem( IDotvvmRequestContext context, object item, ImmutableArray parentPath, - int index) + int index, + bool serverOnly) { var itemWrapper = ItemWrapperCapability.GetWrapper(); c.Add(itemWrapper); - var dataItem = new DataItemContainer { DataItemIndex = index }; + var dataItem = new DataItemContainer { DataItemIndex = index, RenderItemBinding = !serverOnly }; dataItem.SetValue(Internal.UniqueIDProperty, index.ToString() + "L"); // must be different from sibling HierarchyRepeaterLevel - itemWrapper.Children.Add(dataItem); dataItem.SetDataContextTypeFromDataSource(GetDataSourceBinding()); + dataItem.SetValue(Internal.IsServerOnlyDataContextProperty, serverOnly); + itemWrapper.Children.Add(dataItem); // NB: the placeholder is needed because during data context resolution DataItemContainers are looked up // only among parents var placeholder = new PlaceHolder { DataContext = item }; @@ -259,6 +258,7 @@ private DotvvmControl CreateServerItem( $"{GetPathFragmentExpression()}{parentSegment}/[{index}]"); placeholder.SetValue(Internal.UniqueIDProperty, "item"); placeholder.SetDataContextTypeFromDataSource(GetDataSourceBinding()); // DataContext type has to be duplicated on the placeholder, because BindingHelper.FindDataContextTarget (in v4.1) + placeholder.SetValue(Internal.IsServerOnlyDataContextProperty, serverOnly); dataItem.Children.Add(placeholder); ItemTemplate.BuildContent(context, placeholder); @@ -266,7 +266,8 @@ private DotvvmControl CreateServerItem( var itemChildren = GetItemChildren(item); if (itemChildren.Any()) { - var foreachExpression = ((IValueBinding)ItemChildrenBinding! + var foreachExpression = serverOnly ? null : ((IValueBinding)ItemChildrenBinding + .NotNull("ItemChildrenBinding property is required") .GetProperty() .Binding) .GetParametrizedKnockoutExpression(dataItem) @@ -364,6 +365,36 @@ private IEnumerable GetItemChildren(object item) return ItemChildrenBinding!.Evaluate(tempContainer) ?? Enumerable.Empty(); } + [ControlUsageValidator] + public static IEnumerable ValidateUsage(ResolvedControl control) + { + if (!control.TryGetProperty(DataSourceProperty, out var dataSource)) + { + yield return new("DataSource is required on HierarchyRepeater"); + yield break; + } + if (dataSource is not ResolvedPropertyBinding { Binding: var dataSourceBinding }) + { + yield return new("HierarchyRepeater.DataSource must be a binding"); + yield break; + } + if (!control.TryGetProperty(ItemChildrenBindingProperty, out var itemChildren) || + itemChildren is not ResolvedPropertyBinding { Binding: var itemChildrenBinding }) + { + yield break; + } + + if (dataSourceBinding.ParserOptions.BindingType != itemChildrenBinding.ParserOptions.BindingType) + { + yield return new( + "HierarchyRepeater.DataSource and HierarchyRepeater.ItemChildrenBinding must have the same binding type, use `value` or `resource` binding for both properties.", + dataSourceBinding.DothtmlNode, + itemChildrenBinding.DothtmlNode + ); + } + } + + /// /// An internal control for a level of the that renders /// the appropriate foreach binding. diff --git a/src/Framework/Framework/Controls/Internal.cs b/src/Framework/Framework/Controls/Internal.cs index 3dcdde8f8d..4d9a5c4547 100644 --- a/src/Framework/Framework/Controls/Internal.cs +++ b/src/Framework/Framework/Controls/Internal.cs @@ -35,6 +35,10 @@ public class Internal public static readonly DotvvmProperty PathFragmentProperty = DotvvmProperty.Register(() => PathFragmentProperty); + /// Assume that the DataContext is a resource binding, and level won't be present client-side + public static readonly DotvvmProperty IsServerOnlyDataContextProperty = + DotvvmProperty.Register(() => IsServerOnlyDataContextProperty, defaultValue: false, isValueInherited: false); + /// /// Gets compile-time DataContextStack /// diff --git a/src/Framework/Framework/Controls/ItemsControl.cs b/src/Framework/Framework/Controls/ItemsControl.cs index 2e89073dd3..ae4e0ebaee 100644 --- a/src/Framework/Framework/Controls/ItemsControl.cs +++ b/src/Framework/Framework/Controls/ItemsControl.cs @@ -14,6 +14,8 @@ using System.Linq.Expressions; using DotVVM.Framework.Hosting; using Microsoft.Extensions.DependencyInjection; +using FastExpressionCompiler; +using DotVVM.Framework.Compilation; namespace DotVVM.Framework.Controls { @@ -55,29 +57,50 @@ public ItemsControl(string tagName) : base(tagName, false) /// /// Gets the data source binding. /// - protected IValueBinding GetDataSourceBinding() + protected IStaticValueBinding GetDataSourceBinding() { - var binding = GetValueBinding(DataSourceProperty); - if (binding == null) + var binding = GetBinding(DataSourceProperty); + if (binding is null) { throw new DotvvmControlException(this, $"The DataSource property of the '{GetType().Name}' control must be set!"); } - return binding; + if (binding is not IStaticValueBinding resourceBinding) + throw new BindingHelper.BindingNotSupportedException(binding) { RelatedControl = this }; + return resourceBinding; } protected IValueBinding GetItemBinding() { - return (IValueBinding)GetForeachDataBindExpression().GetProperty().Binding; + return GetForeachDataBindExpression().GetProperty().Binding as IValueBinding ?? + throw new DotvvmControlException(this, $"The Item property of the '{GetType().Name}' control must be set to a value binding!"); } public IEnumerable? GetIEnumerableFromDataSource() => (IEnumerable?)GetForeachDataBindExpression().Evaluate(this); - protected IValueBinding GetForeachDataBindExpression() => - (IValueBinding)GetDataSourceBinding().GetProperty().Binding; + protected IStaticValueBinding GetForeachDataBindExpression() => + (IStaticValueBinding)GetDataSourceBinding().GetProperty().Binding; - protected string GetPathFragmentExpression() => - GetDataSourceBinding().GetKnockoutBindingExpression(this); + protected string? TryGetKnockoutForeachExpression(bool unwrapped = false) => + (GetForeachDataBindExpression() as IValueBinding)?.GetKnockoutBindingExpression(this, unwrapped); + + protected string GetPathFragmentExpression() + { + var binding = GetDataSourceBinding(); + var stringified = + binding.GetProperty(ErrorHandlingMode.ReturnNull)?.Code.Trim() ?? + binding.GetProperty(ErrorHandlingMode.ReturnNull)?.Code.FormatKnockoutScript(this, binding) ?? + binding.GetProperty(ErrorHandlingMode.ReturnNull)?.Expression.ToCSharpString(); + + if (stringified is null) + throw new DotvvmControlException(this, $"Can't create path fragment from binding {binding}, it does not have OriginalString, ParsedExpression, nor KnockoutExpression property."); + + return stringified; + } + + /// Returns data context which is expected in the ItemTemplate + protected DataContextStack GetChildDataContext() => + GetDataSourceBinding().GetProperty().DataContext; [ApplyControlStyle] public static void OnCompilation(ResolvedControl control, BindingCompilationService bindingService) @@ -87,9 +110,10 @@ public static void OnCompilation(ResolvedControl control, BindingCompilationServ if (!(dataSourceProperty is ResolvedPropertyBinding dataSourceBinding)) return; var dataContext = dataSourceBinding.Binding.Binding.GetProperty().DataContext; + var bindingType = dataContext.ServerSideOnly ? BindingParserOptions.Resource : BindingParserOptions.Value; control.SetProperty(new ResolvedPropertyBinding(Internal.CurrentIndexBindingProperty, - new ResolvedBinding(bindingService, new Compilation.BindingParserOptions(typeof(ValueBindingExpression)), dataContext, + new ResolvedBinding(bindingService, bindingType, dataContext, parsedExpression: CreateIndexBindingExpression(dataContext)))); } @@ -108,7 +132,7 @@ protected IBinding GetIndexBinding(IDotvvmRequestContext context) { // slower path: create the _index binding at runtime var bindingService = context.Services.GetRequiredService(); - var dataContext = GetDataSourceBinding().GetProperty().DataContext; + var dataContext = GetChildDataContext(); return bindingService.Cache.CreateCachedBinding("_index", new object[] { dataContext }, () => new ValueBindingExpression(bindingService, new object?[] { dataContext, diff --git a/src/Framework/Framework/Controls/Literal.cs b/src/Framework/Framework/Controls/Literal.cs index 813ed32d6a..c2d69ea937 100644 --- a/src/Framework/Framework/Controls/Literal.cs +++ b/src/Framework/Framework/Controls/Literal.cs @@ -97,7 +97,7 @@ bool isFormattedType(Type? type) => type != null && (type == typeof(float) || type == typeof(double) || type == typeof(decimal) || type == typeof(DateTime) || type == typeof(DateOnly) || type == typeof(TimeOnly) || isFormattedType(Nullable.GetUnderlyingType(type))); - bool isFormattedTypeOrObj(Type? type) => type == typeof(object) || isFormattedType(type); + bool isFormattedTypeOrObj(Type? type) => type is null || type == typeof(object) || isFormattedType(type); return isFormattedType(binding?.ResultType) && isFormattedTypeOrObj(binding?.GetProperty(ErrorHandlingMode.ReturnNull)?.Type); } diff --git a/src/Framework/Framework/Controls/Repeater.cs b/src/Framework/Framework/Controls/Repeater.cs index 988f8bbf2a..f675e0638a 100644 --- a/src/Framework/Framework/Controls/Repeater.cs +++ b/src/Framework/Framework/Controls/Repeater.cs @@ -148,16 +148,19 @@ protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext { TagName = WrapperTagName; - var (bindingName, bindingValue) = RenderOnServer ? - ("dotvvm-SSR-foreach", GetServerSideForeachBindingGroup()) : - GetForeachKnockoutBindingGroup(context); - if (RenderWrapperTag) + if (GetValueBinding(DataSourceProperty) is {}) { - writer.AddKnockoutDataBind(bindingName, bindingValue); - } - else - { - writer.WriteKnockoutDataBindComment(bindingName, bindingValue.ToString()); + var (bindingName, bindingValue) = RenderOnServer ? + ("dotvvm-SSR-foreach", GetServerSideForeachBindingGroup()) : + GetForeachKnockoutBindingGroup(context); + if (RenderWrapperTag) + { + writer.AddKnockoutDataBind(bindingName, bindingValue); + } + else + { + writer.WriteKnockoutDataBindComment(bindingName, bindingValue.ToString()); + } } if (RenderWrapperTag) @@ -168,7 +171,7 @@ protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext private KnockoutBindingGroup GetServerSideForeachBindingGroup() => new KnockoutBindingGroup { - { "data", GetForeachDataBindExpression().GetKnockoutBindingExpression(this) } + { "data", TryGetKnockoutForeachExpression().NotNull() } }; private (string bindingName, KnockoutBindingGroup bindingValue) GetForeachKnockoutBindingGroup(IDotvvmRequestContext context) @@ -177,7 +180,7 @@ private KnockoutBindingGroup GetServerSideForeachBindingGroup() => var value = new KnockoutBindingGroup(); - var javascriptDataSourceExpression = GetForeachDataBindExpression().GetKnockoutBindingExpression(this); + var javascriptDataSourceExpression = TryGetKnockoutForeachExpression().NotNull(); value.Add( useTemplate ? "foreach" : "data", javascriptDataSourceExpression); @@ -207,7 +210,6 @@ private KnockoutBindingGroup GetServerSideForeachBindingGroup() => /// protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext context) { - Debug.Assert((clientSideTemplate == null) == this.RenderOnServer); if (clientSideTemplate == null) { Debug.Assert(clientSeparator == null); @@ -226,7 +228,7 @@ protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext c { base.RenderEndTag(writer, context); } - else + else if (GetValueBinding(DataSourceProperty) is {}) { writer.WriteKnockoutDataBindEndComment(); } @@ -234,11 +236,10 @@ protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext c emptyDataContainer?.Render(writer, context); } - private DotvvmControl CreateEmptyItem(IDotvvmRequestContext context) + private DotvvmControl CreateEmptyItem(IDotvvmRequestContext context, IStaticValueBinding dataSourceBinding) { if (emptyDataContainer == null) { - var dataSourceBinding = GetDataSourceBinding(); emptyDataContainer = new EmptyData(); emptyDataContainer.SetValue(EmptyData.RenderWrapperTagProperty, GetValueRaw(RenderWrapperTagProperty)); emptyDataContainer.SetValue(EmptyData.WrapperTagNameProperty, GetValueRaw(WrapperTagNameProperty)); @@ -251,13 +252,13 @@ private DotvvmControl CreateEmptyItem(IDotvvmRequestContext context) } private readonly Dictionary childrenCache = new(ReferenceEqualityComparer.Instance); - private DotvvmControl AddItem(IList c, IDotvvmRequestContext context, object? item = null, int? index = null, bool allowMemoizationRetrieve = false, bool allowMemoizationStore = false) + private DotvvmControl AddItem(IList c, IDotvvmRequestContext context, object? item = null, int? index = null, bool serverOnly = false, bool allowMemoizationRetrieve = false, bool allowMemoizationStore = false) { if (allowMemoizationRetrieve && item != null && childrenCache.TryGetValue(item, out var container2) && container2.Parent == null) { Debug.Assert(item == container2.GetValueRaw(DataContextProperty)); c.Add(container2); - SetUpServerItem(context, item, (int)index!, container2); + SetUpServerItem(context, item, (int)index!, serverOnly, container2); return container2; } @@ -266,11 +267,12 @@ private DotvvmControl AddItem(IList c, IDotvvmRequestContext cont c.Add(container); if (item == null && index == null) { + Debug.Assert(!serverOnly); SetUpClientItem(context, container); } else { - SetUpServerItem(context, item!, (int)index!, container); + SetUpServerItem(context, item!, (int)index!, serverOnly, container); } ItemTemplate.BuildContent(context, container); @@ -309,17 +311,21 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat clientSeparator = null; clientSideTemplate = null; - if (DataSource != null) + var dataSource = GetIEnumerableFromDataSource(); + var dataSourceBinding = GetDataSourceBinding(); + var serverOnly = dataSourceBinding is not IValueBinding; + + if (dataSource != null) { var index = 0; var isCommand = context.RequestType == DotvvmRequestType.Command; // on GET request we are not initializing the Repeater twice - foreach (var item in GetIEnumerableFromDataSource()!) + foreach (var item in dataSource) { if (SeparatorTemplate != null && index > 0) { AddSeparator(Children, context); } - AddItem(Children, context, item, index, + AddItem(Children, context, item, index, serverOnly, allowMemoizationRetrieve: isCommand && !memoizeReferences, allowMemoizationStore: memoizeReferences ); @@ -327,7 +333,7 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat } } - if (renderClientTemplate) + if (renderClientTemplate && !serverOnly) { if (SeparatorTemplate != null) { @@ -339,7 +345,7 @@ private void SetChildren(IDotvvmRequestContext context, bool renderClientTemplat if (EmptyDataTemplate != null) { - CreateEmptyItem(context); + CreateEmptyItem(context, dataSourceBinding); } } @@ -350,10 +356,12 @@ private void SetUpClientItem(IDotvvmRequestContext context, DataItemContainer co container.SetValue(Internal.ClientIDFragmentProperty, this.GetIndexBinding(context)); } - private void SetUpServerItem(IDotvvmRequestContext context, object item, int index, DataItemContainer container) + private void SetUpServerItem(IDotvvmRequestContext context, object item, int index, bool serverOnly, DataItemContainer container) { container.DataItemIndex = index; container.DataContext = item; + container.RenderItemBinding = !serverOnly; + container.SetValue(Internal.IsServerOnlyDataContextProperty, serverOnly); container.SetValue(Internal.PathFragmentProperty, GetPathFragmentExpression() + "/[" + index + "]"); container.ID = index.ToString(); } 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/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs index 54bb4a235f..baa539e194 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs @@ -503,7 +503,13 @@ public ActionInfo ResolveCommand(IDotvvmRequestContext context, DotvvmView view) var target = view.FindControlByUniqueId(controlUniqueId); if (target == null) { - throw new Exception(string.Format("The control with ID '{0}' was not found!", controlUniqueId)); + var markupControls = + view.GetAllDescendants() + .OfType() + .Where(c => c.GetAllDescendants(cc => cc is not DotvvmMarkupControl) + .Any(cc => cc.Properties.Values + .Any(value => value is Binding.Expressions.CommandBindingExpression cb && cb.BindingId == command))); + throw new Exception($"The control with ID '{controlUniqueId}' was not found! Existing markup controls with this command are: {string.Join(", ", markupControls.Select(c => c.GetDotvvmUniqueId().ToString()).OrderBy(s => s, StringComparer.Ordinal))}"); } return commandResolver.GetFunction(target, view, context, path!, command, args); } 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/ViewModels/DefaultViewModel.cs b/src/Samples/Common/ViewModels/DefaultViewModel.cs index f4eeed8ed1..7ce4e37ce0 100644 --- a/src/Samples/Common/ViewModels/DefaultViewModel.cs +++ b/src/Samples/Common/ViewModels/DefaultViewModel.cs @@ -6,12 +6,15 @@ using System.Threading.Tasks; using DotVVM.Framework.Hosting; using DotVVM.Framework.Routing; +using DotVVM.Framework.ViewModel; namespace DotVVM.Samples.BasicSamples.ViewModels { public class DefaultViewModel : SamplesViewModel { public string Title { get; set; } + + [Bind(Direction.None)] public List Routes { get; set; } public override Task Init() diff --git a/src/Samples/Common/Views/ComplexSamples/TaskList/ServerRenderedTaskList.dothtml b/src/Samples/Common/Views/ComplexSamples/TaskList/ServerRenderedTaskList.dothtml index 897da3a383..5abda739e3 100644 --- a/src/Samples/Common/Views/ComplexSamples/TaskList/ServerRenderedTaskList.dothtml +++ b/src/Samples/Common/Views/ComplexSamples/TaskList/ServerRenderedTaskList.dothtml @@ -9,29 +9,34 @@ color: maroon; text-decoration: line-through; } + .alt-row { + background-color: #FFFFBB; + } -

Task List

+
Add Task -

Title:

-

+

Title:

+

+
+

 

- - - + + + @@ -39,4 +44,4 @@ - \ No newline at end of file + 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/Common/Views/Default.dothtml b/src/Samples/Common/Views/Default.dothtml index 1d0f175650..879f5ec664 100644 --- a/src/Samples/Common/Views/Default.dothtml +++ b/src/Samples/Common/Views/Default.dothtml @@ -2,8 +2,8 @@ @masterPage Views/Samples.dotmaster - -
  • {{value: RouteName}}
  • + +
  • {{resource: RouteName}}
  • -
    Number of Tests: {{value: Routes.Count}}
    +
    Number of Tests: {{resource: Routes.Count}}
    diff --git a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs index fb434046a7..bd65692afc 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_GridViewPagingSortingServerSide = "ControlSamples/GridView/GridViewPagingSortingServerSide"; 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/Complex/TaskListTests.cs b/src/Samples/Tests/Tests/Complex/TaskListTests.cs index 2e1776e050..5dc77ac2e4 100644 --- a/src/Samples/Tests/Tests/Complex/TaskListTests.cs +++ b/src/Samples/Tests/Tests/Complex/TaskListTests.cs @@ -51,7 +51,7 @@ public void Complex_TaskList_ServerRenderedTaskList() //add task browser.SendKeys("input[type=text]", "DotVVM"); - browser.Click("input[type=button]"); + browser.Click("input[type=submit]"); browser.FindElements(".table tr").ThrowIfDifferentCountThan(4); diff --git a/src/Samples/Tests/Tests/Control/GridViewTests.cs b/src/Samples/Tests/Tests/Control/GridViewTests.cs index 906c5a9396..3faf455677 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_GridViewPagingSortingServerSide)] [SampleReference(nameof(SamplesRouteUrls.ControlSamples_GridView_GridViewPagingSorting))] [SampleReference(nameof(SamplesRouteUrls.ControlSamples_GridView_GridViewServerRender))] + [SampleReference(nameof(SamplesRouteUrls.ControlSamples_GridView_GridViewPagingSortingServerSide))] 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/MarkupControlTests.cs b/src/Tests/ControlTests/MarkupControlTests.cs index ceac1d62e7..094cc55df2 100644 --- a/src/Tests/ControlTests/MarkupControlTests.cs +++ b/src/Tests/ControlTests/MarkupControlTests.cs @@ -27,13 +27,16 @@ public class MarkupControlTests _ = Repeater.RenderAsNamedTemplateProperty; config.Resources.RegisterScriptModuleUrl("somemodule", "http://localhost:99999/somemodule.js", null); config.Markup.AddMarkupControl("cc", "CustomControl", "CustomControl.dotcontrol"); - config.Markup.AddMarkupControl("cc", "CustomControlWithCommand", "CustomControlWithCommand.dotcontrol"); + config.Markup.AddMarkupControl("cc", "CustomControlWithStaticCommand", "CustomControlWithStaticCommand.dotcontrol"); + config.Markup.AddMarkupControl("cc", "CustomControlWithCommand_value", "CustomControlWithCommand_value.dotcontrol"); + config.Markup.AddMarkupControl("cc", "CustomControlWithCommand_resource", "CustomControlWithCommand_resource.dotcontrol"); config.Markup.AddMarkupControl("cc", "CustomControlWithProperty", "CustomControlWithProperty.dotcontrol"); config.Markup.AddMarkupControl("cc", "CustomControlWithInvalidVM", "CustomControlWithInvalidVM.dotcontrol"); config.Markup.AddMarkupControl("cc", "CustomControlWithInternalProperty", "CustomControlWithInternalProperty.dotcontrol"); config.Markup.AddMarkupControl("cc", "CustomControlWithResourceProperty", "CustomControlWithResourceProperty.dotcontrol"); config.Markup.AddMarkupControl("cc", "CustomControlWithJsInvoke", "CustomControlWithJsInvoke.dotcontrol"); config.Markup.AddMarkupControl("cc", "DataContextChangeControl", "DataContextChangeControl.dotcontrol"); + config.Markup.AddMarkupControl("cc", "MarkupControl_ValueInResource_Error", "MarkupControl_ValueInResource_Error.dotcontrol"); config.Styles.Register().SetProperty(r => r.RenderAsNamedTemplate, false, StyleOverrideOptions.Ignore); }, services: s => { s.Services.AddSingleton(); @@ -71,14 +74,14 @@ public async Task MarkupControl_PassingStaticCommand() { var r = await cth.RunPage(typeof(BasicTestViewModel), @" - + - + ", directives: $"@service s = {typeof(TestService)}", markupFiles: new Dictionary { - ["CustomControlWithCommand.dotcontrol"] = @" + ["CustomControlWithStaticCommand.dotcontrol"] = @" @viewModel int @baseType DotVVM.Framework.Tests.ControlTests.CustomControlWithCommand @wrapperTag div @@ -89,6 +92,61 @@ @wrapperTag div check.CheckString(r.FormattedHtml, fileExtension: "html"); } + [TestMethod] + [DataRow("value")] + [DataRow("resource")] + public async Task MarkupControl_CommandInResourceRepeater(string innerPropertyBinding) + { + var r = await cth.RunPage(typeof(BasicTestViewModel), $$""" + + + + + + """, + directives: $"@service s = {typeof(TestService)}", + fileName: $"MarkupControl_CommandInResourceRepeater_{innerPropertyBinding}", + markupFiles: new Dictionary { + [$"CustomControlWithCommand_{innerPropertyBinding}.dotcontrol"] = $$""" + @viewModel int + @baseType DotVVM.Framework.Tests.ControlTests.CustomControlWithCommand + @wrapperTag div + + """ + } + ); + + check.CheckString(r.FormattedHtml, fileExtension: "html", checkName: innerPropertyBinding); + } + + [TestMethod] + public async Task MarkupControl_ValueInResource_Error() + { + // currently, the error is only thrown at runtime + var exception = await Assert.ThrowsExceptionAsync(() => + cth.RunPage(typeof(BasicTestViewModel), @" + + + + + ", + directives: $"@service s = {typeof(TestService)}", + markupFiles: new Dictionary { + ["MarkupControl_ValueInResource_Error.dotcontrol"] = """ + @viewModel int + @wrapperTag div + {{value: $'Test-{_this}-1234'}} + """ + } + )); + + StringAssert.Contains(exception.Message, "cannot be used in resource-binding only data context, because it uses value bindings on the data context."); + + // is dothtml (not dotcontrol), otherwise it's impossible to find where is it being a problem + StringAssert.EndsWith("MarkupControl_ValueInResource_Error.dothtml", exception.FileName); + } + [TestMethod] public async Task MarkupControl_UpdateSource() { diff --git a/src/Tests/ControlTests/ResourceDataContextTests.cs b/src/Tests/ControlTests/ResourceDataContextTests.cs new file mode 100644 index 0000000000..dffc76b6d2 --- /dev/null +++ b/src/Tests/ControlTests/ResourceDataContextTests.cs @@ -0,0 +1,226 @@ +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 DotVVM.Framework.Compilation.Styles; + +namespace DotVVM.Framework.Tests.ControlTests +{ + [TestClass] + public class ResourceDataContextTests + { + static readonly ControlTestHelper cth = new ControlTestHelper(config: config => { + _ = Controls.Repeater.RenderAsNamedTemplateProperty; + config.Styles.Register().SetProperty(r => r.RenderAsNamedTemplate, false, StyleOverrideOptions.Ignore); + }); + OutputChecker check = new OutputChecker("testoutputs"); + + [TestMethod] + public async Task Error_ValueBindingUsage() + { + var e = await Assert.ThrowsExceptionAsync(() => cth.RunPage(typeof(TestViewModel), @" +
    + {{value: Name}} +
    + " + )); + + StringAssert.StartsWith(e.GetBaseException().Message, "_this of type ResourceDataContextTests.TestViewModel.CustomerData cannot be translated to JavaScript"); + } + + [TestMethod] + public async Task Error_StaticCommandBindingUsage() + { + var e = await Assert.ThrowsExceptionAsync(() => cth.RunPage(typeof(TestViewModel), @" +
    + +
    + " + )); + + StringAssert.StartsWith(e.GetBaseException().Message, "_this of type ResourceDataContextTests.TestViewModel.CustomerData cannot be translated to JavaScript, it can only be used in resource and command bindings."); + } + + [TestMethod] + public async Task BasicDataContext() + { + var r = await cth.RunPage(typeof(TestViewModel), @" +
    + {{resource: Name}} + + {{value: _parent.CommandData}} + + +
    + " + ); + + check.CheckString(r.FormattedHtml, fileExtension: "html"); + + await r.RunCommand("_root.TestMethod(Name)"); + + Assert.AreEqual((string)r.ViewModelJson["CommandData"], "Server o. Customer"); + } + + [TestMethod] + public async Task Repeater() + { + var r = await cth.RunPage(typeof(TestViewModel), @" + + + + This would be here if the Customers.Items were empty + + ------------------- + + {{resource: Name}} + + {{value: _parent.CommandData}} + + + + + This would be here if the Customers.Items were empty + + ------------------- + + {{resource: Name}} + + {{value: _parent.CommandData}} + + " + ); + + check.CheckString(r.FormattedHtml, fileExtension: "html"); + } + + [TestMethod] + public async Task RepeaterWithCommand() + { + var r = await cth.RunPage(typeof(TestViewModel), @" + + + + " + ); + + await r.RunCommand("_root.TestMethod(Name)", x => x is TestViewModel.CustomerData { Id: 1 }); + Assert.AreEqual((string)r.ViewModelJson["CommandData"], "One"); + + await r.RunCommand("_root.TestMethod(Name)", x => x is TestViewModel.CustomerData { Id: 2 }); + Assert.AreEqual((string)r.ViewModelJson["CommandData"], "Two"); + } + + [TestMethod] + public async Task DataContextRevert() + { + // revert client-side data context by DataContext={value: _root...} + var r = await cth.RunPage(typeof(TestViewModel), @" + + + {{resource: Name}} + + + {{value: _root.StringPrefix + _this}} + + + " + ); + + check.CheckString(r.FormattedHtml, fileExtension: "html"); + } + + [TestMethod] + public async Task HierarchyRepeater_SimpleTemplate() + { + var r = await cth.RunPage(typeof(TestViewModel), @" + + + + This would be here if the Customers.Items were empty + {{resource: Name}} + + + + + {{resource: Name}} + + " + ); + + 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 + { + public string NullableString { get; } = null; + + + [Bind(Direction.None)] + public CustomerData ServerOnlyCustomer { get; set; } = new CustomerData(100, "Server o. Customer", new()); + + public GridViewDataSet Customers { get; set; } = new GridViewDataSet() { + RowEditOptions = { + EditRowId = 1, + PrimaryKeyPropertyName = nameof(CustomerData.Id) + }, + Items = { + new CustomerData(1, "One", new()), + new CustomerData(2, "Two", new() { + new CustomerData(21, "first pyramid customer", new() { + new CustomerData(211, "second pyramid customer", new()) + }) + }) + } + }; + + public List FewStrings { get; set; } = new List() { "Hi!", "Ahoj!" }; + + public string StringPrefix { get; set; } = "..."; + + public UploadedFilesCollection Files { get; set; } = new UploadedFilesCollection(); + + public record CustomerData( + int Id, + [property: Required] + string Name, + // software for running MLM 😂 + List NextLevelCustomers + ); + + public string CommandData { get; set; } + + public void TestMethod(string data) + { + CommandData = data; + } + } + } +} diff --git a/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_CommandInResourceRepeater-resource.html b/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_CommandInResourceRepeater-resource.html new file mode 100644 index 0000000000..e4d9d601df --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_CommandInResourceRepeater-resource.html @@ -0,0 +1,19 @@ + + + + + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + + diff --git a/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_CommandInResourceRepeater-value.html b/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_CommandInResourceRepeater-value.html new file mode 100644 index 0000000000..aa33122eca --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_CommandInResourceRepeater-value.html @@ -0,0 +1,19 @@ + + + + + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + + diff --git a/src/Tests/ControlTests/testoutputs/ResourceDataContextTests.BasicDataContext.html b/src/Tests/ControlTests/testoutputs/ResourceDataContextTests.BasicDataContext.html new file mode 100644 index 0000000000..f11ab9270f --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/ResourceDataContextTests.BasicDataContext.html @@ -0,0 +1,10 @@ + + + +
    + Server o. Customer + + +
    + + diff --git a/src/Tests/ControlTests/testoutputs/ResourceDataContextTests.DataContextRevert.html b/src/Tests/ControlTests/testoutputs/ResourceDataContextTests.DataContextRevert.html new file mode 100644 index 0000000000..8ebd8c519f --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/ResourceDataContextTests.DataContextRevert.html @@ -0,0 +1,15 @@ + + + + One +
    + + +
    + Two +
    + + +
    + + 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 @@ + + + +
    {{value: Title}}
    {{resource: Title}} + Click={command: _parent.CompleteTask(TaskId)} + Visible={resource: !IsCompleted} />
    + + + + + + + + + + + + + + + + +
    + Id + + Name +
    + 1 + One
    + 2 + Two
    + + diff --git a/src/Tests/ControlTests/testoutputs/ResourceDataContextTests.HierarchyRepeater_SimpleTemplate.html b/src/Tests/ControlTests/testoutputs/ResourceDataContextTests.HierarchyRepeater_SimpleTemplate.html new file mode 100644 index 0000000000..230c44aafc --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/ResourceDataContextTests.HierarchyRepeater_SimpleTemplate.html @@ -0,0 +1,33 @@ + + + + + + One + Two + first pyramid customer + second pyramid customer + + +
    +
      +
    • + One +
    • +
    • + Two +
        +
      • + first pyramid customer +
          +
        • + second pyramid customer +
        • +
        +
      • +
      +
    • +
    +
    + + diff --git a/src/Tests/ControlTests/testoutputs/ResourceDataContextTests.Repeater.html b/src/Tests/ControlTests/testoutputs/ResourceDataContextTests.Repeater.html new file mode 100644 index 0000000000..f20094a32f --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/ResourceDataContextTests.Repeater.html @@ -0,0 +1,21 @@ + + + + + + One + + ------------------- + Two + + + +
    + One + + ------------------- + Two + +
    + + diff --git a/src/Tests/ControlTests/testoutputs/SimpleControlTests.EmptyData.html b/src/Tests/ControlTests/testoutputs/SimpleControlTests.EmptyData.html index 3a8739a7ed..60a76097b9 100644 --- a/src/Tests/ControlTests/testoutputs/SimpleControlTests.EmptyData.html +++ b/src/Tests/ControlTests/testoutputs/SimpleControlTests.EmptyData.html @@ -3,7 +3,7 @@ -
    +
    Data is empty
    @@ -11,7 +11,9 @@ Data is empty
    - +
    + Data is empty +
    Data is empty diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index 3127802255..bcb06f5f89 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 } @@ -965,7 +963,7 @@ "onlyHardcoded": true }, "ValueBinding": { - "type": "DotVVM.Framework.Binding.Expressions.IValueBinding, DotVVM.Framework", + "type": "DotVVM.Framework.Binding.Expressions.IStaticValueBinding, DotVVM.Framework", "required": true, "onlyBindings": true } @@ -977,7 +975,7 @@ "onlyHardcoded": true }, "ItemChildrenBinding": { - "type": "DotVVM.Framework.Binding.Expressions.IValueBinding`1[[System.Collections.Generic.IEnumerable`1[[System.Object, CoreLibrary]], CoreLibrary]], DotVVM.Framework", + "type": "DotVVM.Framework.Binding.Expressions.IStaticValueBinding`1[[System.Collections.Generic.IEnumerable`1[[System.Object, CoreLibrary]], CoreLibrary]], DotVVM.Framework", "dataContextChange": [ { "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute", @@ -1121,6 +1119,10 @@ "type": "System.Boolean", "defaultValue": false }, + "IsServerOnlyDataContext": { + "type": "System.Boolean", + "defaultValue": false + }, "IsSpaPage": { "type": "System.Boolean", "isValueInherited": 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.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