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 10 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion src/DynamicData/DynamicData/DataContextStackHelper.cs
Expand Up @@ -40,4 +40,4 @@ public static DataContextStack CreateChildStack(this DataContextStack dataContex
dataContextStack.BindingPropertyResolvers);
}
}
}
}
80 changes: 53 additions & 27 deletions src/Framework/Framework/Binding/BindingHelper.cs
Expand Up @@ -87,12 +87,49 @@ internal static (int stepsUp, DotvvmBindableObject target) FindDataContextTarget
if (bindingContext == null || controlContext == null || controlContext.Equals(bindingContext)) return (0, control);

var changes = 0;
var lastAncestorContext = controlContext;
foreach (var a in control.GetAllAncestors(includingThis: true))
{
if (bindingContext.Equals(a.GetDataContextType(inherit: false)))
var ancestorContext = a.GetDataContextType(inherit: false);

if (ancestorContext is null)
continue;

if (!ancestorContext.ServerSideOnly &&
!ancestorContext.Equals(lastAncestorContext))
{
// only count changes which are visible client-side
// server-side context are not present in the client-side stack at all, so we need to skip them here

// don't count changes which only extend the data context, but don't nest it

var isNesting = ancestorContext.IsAncestorOf(lastAncestorContext);
if (isNesting)
{
changes++;
}
#if DEBUG
else if (!lastAncestorContext.DataContextType.IsAssignableFrom(ancestorContext.DataContextType))
{
// this should not happen - data context type should not randomly change without nesting.
// we change data context stack when we get into different compilation context - a markup control
// but that will be always the same viewmodel type (or supertype)

var previousAncestor = control.GetAllAncestors(includingThis: true).TakeWhile(aa => aa != a).LastOrDefault();
var config = (control.GetValue(Internal.RequestContextProperty) as Hosting.IDotvvmRequestContext)?.Configuration;
throw new DotvvmControlException(
previousAncestor ?? a,
$"DataContext type changed from '{lastAncestorContext.DataContextType.ToCode()}' to '{ancestorContext.DataContextType.ToCode()}' without nesting. " +
$"{previousAncestor?.DebugString(config)} has DataContext: {lastAncestorContext}, " +
$"{a.DebugString(config)} has DataContext: {ancestorContext}");
}
#endif
lastAncestorContext = ancestorContext;
}

if (bindingContext.Equals(ancestorContext))
return (changes, a);

if (a.properties.Contains(DotvvmBindableObject.DataContextProperty)) changes++;
}

// try to get the real objects, to see which is wrong
Expand Down Expand Up @@ -331,10 +368,12 @@ public static TBinding DeriveBinding<TBinding>(this TBinding binding, params obj
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 @@ -373,10 +412,10 @@ public static TExpression AddParameterAnnotation<TExpression>(this TExpression e
return dataContextType;
}

var (childType, extensionParameters) = ApplyDataContextChange(dataContextType, property.DataContextChangeAttributes, obj, property);
var (childType, extensionParameters, serverOnly) = ApplyDataContextChange(dataContextType, property.DataContextChangeAttributes, obj, property);

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 @@ -396,41 +435,28 @@ public static DataContextStack GetDataContextType(this DotvvmProperty property,
return dataContextType;
}

var (childType, extensionParameters) = ApplyDataContextChange(dataContextType, property.DataContextChangeAttributes, obj, property);

if (childType is null)
childType = typeof(UnknownTypeSentinel);

return DataContextStack.Create(childType, dataContextType, extensionParameters: extensionParameters.ToArray());
}
var (childTypeDescriptor, extensionParameters, serverOnly) = ControlTreeResolverBase.ApplyContextChange(dataContextType, property.DataContextChangeAttributes, obj, property);
var childType = ResolvedTypeDescriptor.ToSystemType(childTypeDescriptor) ?? typeof(UnknownTypeSentinel);

public static (Type? type, List<BindingExtensionParameter> extensionParameters) ApplyDataContextChange(DataContextStack dataContext, DataContextChangeAttribute[] attributes, ResolvedControl control, DotvvmProperty? property)
{
var type = ResolvedTypeDescriptor.Create(dataContext.DataContextType);
var extensionParameters = new List<BindingExtensionParameter>();
foreach (var attribute in attributes.OrderBy(a => a.Order))
{
if (type == null) break;
extensionParameters.AddRange(attribute.GetExtensionParameters(type));
type = attribute.GetChildDataContextType(type, dataContext, control, property);
}
return (ResolvedTypeDescriptor.ToSystemType(type), extensionParameters);
return DataContextStack.Create(childType, dataContextType, extensionParameters: extensionParameters.ToArray(), serverSideOnly: serverOnly);
}


private static (Type? childType, List<BindingExtensionParameter> extensionParameters) ApplyDataContextChange(DataContextStack dataContextType, DataContextChangeAttribute[] attributes, DotvvmBindableObject obj, DotvvmProperty property)
private static (Type? childType, List<BindingExtensionParameter> extensionParameters, bool serverOnly) ApplyDataContextChange(DataContextStack dataContextType, DataContextChangeAttribute[] attributes, DotvvmBindableObject obj, DotvvmProperty property)
{
Type? type = dataContextType.DataContextType;
var extensionParameters = new List<BindingExtensionParameter>();
var serverOnly = dataContextType.ServerSideOnly;

foreach (var attribute in attributes.OrderBy(a => a.Order))
{
if (type == null) break;
extensionParameters.AddRange(attribute.GetExtensionParameters(new ResolvedTypeDescriptor(type)));
type = attribute.GetChildDataContextType(type, dataContextType, obj, property);
serverOnly = attribute.IsServerSideOnly(dataContextType, obj, property) ?? serverOnly;
}

return (type, extensionParameters);
return (type, extensionParameters, serverOnly);
}

/// <summary>
Expand Down
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;
}
}
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);

}
}
}
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
Expand Up @@ -29,6 +29,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
Expand Up @@ -425,12 +425,13 @@ 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.Create(
ReflectionUtils.GetEnumerableType(resultType.Type).NotNull(),
parent: dataContext,
extensionParameters: new CollectionElementDataContextChangeAttribute(0).GetExtensionParameters(new ResolvedTypeDescriptor(dataContext.DataContextType)).ToArray()
extensionParameters: new CollectionElementDataContextChangeAttribute(0).GetExtensionParameters(new ResolvedTypeDescriptor(dataContext.DataContextType)).ToArray(),
serverSideOnly: binding is not IValueBinding
));
}

Expand Down