Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic support for DataContext={resource: ...} #1392

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/DynamicData/DynamicData/DataContextStackHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ public static DataContextStack CreateChildStack(this DataContextStack dataContex
dataContextStack.BindingPropertyResolvers);
}
}
}
}
2 changes: 1 addition & 1 deletion src/Framework/Core/Controls/GenericGridViewDataSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class GenericGridViewDataSet<
/// <summary>
/// Gets or sets the items for the current page.
/// </summary>
public IList<T> Items { get; set; } = new List<T>();
public virtual IList<T> Items { get; set; } = new List<T>();

/// <summary>
/// Gets or sets whether the data should be refreshed. This property is set to true automatically
Expand Down
27 changes: 27 additions & 0 deletions src/Framework/Core/Controls/ServerSideGridViewDataSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using DotVVM.Framework.ViewModel;

namespace DotVVM.Framework.Controls
{
/// <summary>
/// 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).
/// </summary>
/// <typeparam name="T">The type of the elements in the collection.</typeparam>
public class ServerSideGridViewDataSet<T>()
: GenericGridViewDataSet<T, NoFilteringOptions, SortingOptions, PagingOptions, NoRowInsertOptions, NoRowEditOptions>(new(), new(), new(), new(), new())
{

[Bind(Direction.None)]
public override IList<T> 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
};
}
}
}
52 changes: 39 additions & 13 deletions src/Framework/Framework/Binding/BindingHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -361,10 +380,12 @@ public static Func<TParam, TResult> Cache<TParam, TResult>(this Func<TParam, TRe
return f => 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<ThisBindingProperty>().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<ThisBindingProperty>().binding;
}

private static readonly ConditionalWeakTable<Expression, BindingParameterAnnotation> _expressionAnnotations =
Expand Down Expand Up @@ -403,7 +424,7 @@ public static TControl SetDataContextTypeFromDataSource<TControl>(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)
{
Expand All @@ -412,7 +433,7 @@ public static TControl SetDataContextTypeFromDataSource<TControl>(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);
}

/// <summary> Return the expected data context type for this property. Returns null if the type is unknown. </summary>
Expand All @@ -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)
{
Expand All @@ -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<BindingExtensionParameter> extensionParameters, bool addLayer) ApplyDataContextChange(DataContextStack dataContext, DataContextChangeAttribute[] attributes, ResolvedControl control, DotvvmProperty? property)
public static (Type? type, List<BindingExtensionParameter> extensionParameters, bool addLayer, bool serverOnly) ApplyDataContextChange(DataContextStack dataContext, DataContextChangeAttribute[] attributes, ResolvedControl control, DotvvmProperty? property)
{
var type = ResolvedTypeDescriptor.Create(dataContext.DataContextType);
var extensionParameters = new List<BindingExtensionParameter>();
var addLayer = false;
var serverOnly = dataContext.ServerSideOnly;

foreach (var attribute in attributes.OrderBy(a => a.Order))
{
if (type == null) break;
Expand All @@ -459,17 +482,19 @@ public static (Type? type, List<BindingExtensionParameter> 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<BindingExtensionParameter> extensionParameters, bool addLayer) ApplyDataContextChange(DataContextStack dataContextType, DataContextChangeAttribute[] attributes, DotvvmBindableObject obj, DotvvmProperty property)
private static (Type? childType, List<BindingExtensionParameter> extensionParameters, bool addLayer, bool serverOnly) ApplyDataContextChange(DataContextStack dataContextType, DataContextChangeAttribute[] attributes, DotvvmBindableObject obj, DotvvmProperty property)
{
Type? type = dataContextType.DataContextType;
var extensionParameters = new List<BindingExtensionParameter>();
var addLayer = false;
var serverOnly = dataContextType.ServerSideOnly;

foreach (var attribute in attributes.OrderBy(a => a.Order))
{
Expand All @@ -479,10 +504,11 @@ private static (Type? childType, List<BindingExtensionParameter> 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);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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;
}
Expand All @@ -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<string> 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);

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
5 changes: 5 additions & 0 deletions src/Framework/Framework/Binding/DataContextChangeAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BindingExtensionParameter> GetExtensionParameters(ITypeDescriptor dataContext) => Enumerable.Empty<BindingExtensionParameter>();

/// <summary> Gets if the nested data context is available only on server or also client-side, controls the <see cref="DataContextStack.ServerSideOnly" /> property. `null` means that it should be inherited from the parent context. </summary>
public virtual bool? IsServerSideOnly(IDataContextStack controlContextStack, IAbstractControl control, IPropertyDescriptor? property = null) => null;
/// <summary> Gets if the nested data context is available only on server or also client-side, controls the <see cref="DataContextStack.ServerSideOnly" /> property. `null` means that it should be inherited from the parent context. </summary>
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<string> PropertyDependsOn => Enumerable.Empty<string>();
}
Expand Down
12 changes: 8 additions & 4 deletions src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ public ValueBindingExpression<TResult> CreateValueBinding<TResult>(string code,
new ValueBindingExpression<TResult>(compilationService, new object?[] {
dataContext,
new OriginalStringBindingProperty(code),
parserOptions
parserOptions,
new ExpectedTypeBindingProperty(typeof(TResult))
}));
}

Expand All @@ -78,7 +79,8 @@ public ResourceBindingExpression<TResult> CreateResourceBinding<TResult>(string
new ResourceBindingExpression<TResult>(compilationService, new object?[] {
dataContext,
new OriginalStringBindingProperty(code),
parserOptions
parserOptions,
new ExpectedTypeBindingProperty(typeof(TResult))
}));
}

Expand All @@ -100,7 +102,8 @@ public CommandBindingExpression<TResult> CreateCommand<TResult>(string code, Dat
new CommandBindingExpression<TResult>(compilationService, new object?[] {
dataContext,
new OriginalStringBindingProperty(code),
parserOptions
parserOptions,
new ExpectedTypeBindingProperty(typeof(TResult))
}));
}

Expand All @@ -122,7 +125,8 @@ public StaticCommandBindingExpression<TResult> CreateStaticCommand<TResult>(stri
new StaticCommandBindingExpression<TResult>(compilationService, new object?[] {
dataContext,
new OriginalStringBindingProperty(code),
parserOptions
parserOptions,
new ExpectedTypeBindingProperty(typeof(TResult))
}));
}

Expand Down
Loading
Loading