Skip to content

Commit

Permalink
Add support for IEnumerable<T> in control properties
Browse files Browse the repository at this point in the history
Also improves the error messages when property type is not suported +
a test that it works automatically (without MarkupOption)
in CompositeControl.
  • Loading branch information
exyi committed Aug 5, 2022
1 parent ac2a239 commit 458cbda
Show file tree
Hide file tree
Showing 11 changed files with 98 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public ControlPropertyBindingDataContextChangeAttribute(string propertyName, int
return dataContext;
}

throw new Exception($"Property '{PropertyName}' is required on '{control.Metadata.Type.Name}'.");
throw new Exception($"Property '{PropertyName}' is required on '{control.Metadata.Type.CSharpName}'.");
}

public override Type? GetChildDataContextType(Type dataContext, DataContextStack controlContextStack, DotvvmBindableObject control, DotvvmProperty? property = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public static IPropertyDescriptor GetHtmlAttributeDescriptor(this IControlResolv
public static IPropertyDescriptor GetPropertyGroupMember(this IControlResolverMetadata metadata, string prefix, string name)
{
var group = metadata.PropertyGroups.FirstOrDefault(f => f.Prefix == prefix).PropertyGroup;
if (group == null) throw new NotSupportedException($"Control { metadata.Type.Name } does not support property group with prefix '{prefix}'.");
if (group == null) throw new NotSupportedException($"Control { metadata.Type.CSharpName } does not support property group with prefix '{prefix}'.");
return group.GetDotvvmProperty(name);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ private IAbstractControl ProcessObjectElement(DothtmlElementNode element, IDataC
if (controlMetadata.DataContextConstraint != null && dataContext != null && !controlMetadata.DataContextConstraint.IsAssignableFrom(dataContext.DataContextType))
{
((DothtmlNode?)dataContextAttribute ?? element)
.AddError($"The control '{controlMetadata.Type.Name}' requires a DataContext of type '{controlMetadata.DataContextConstraint.FullName}'!");
.AddError($"The control '{controlMetadata.Type.CSharpName}' requires a DataContext of type '{controlMetadata.DataContextConstraint.CSharpFullName}'!");
}

ProcessAttributeProperties(control, element.Attributes.Where(a => a.AttributeName != "DataContext").ToArray(), dataContext!);
Expand All @@ -247,7 +247,7 @@ private IAbstractControl ProcessObjectElement(DothtmlElementNode element, IDataC
var unknownContent = control.Content.Where(c => !c.Metadata.Type.IsAssignableTo(new ResolvedTypeDescriptor(typeof(DotvvmControl))));
foreach (var unknownControl in unknownContent)
{
unknownControl.DothtmlNode!.AddError($"The control '{ unknownControl.Metadata.Type.FullName }' does not inherit from DotvvmControl and thus cannot be used in content.");
unknownControl.DothtmlNode!.AddError($"The control '{ unknownControl.Metadata.Type.CSharpName }' does not inherit from DotvvmControl and thus cannot be used in content.");
}

return control;
Expand Down Expand Up @@ -327,7 +327,7 @@ private void ProcessAttribute(IPropertyDescriptor property, DothtmlAttributeNode
{
if (!treeBuilder.AddProperty(control, treeBuilder.BuildPropertyValue(property, (property as DotVVM.Framework.Binding.DotvvmProperty)?.DefaultValue, attribute), out var error)) attribute.AddError(error);
}
else attribute.AddError($"The attribute '{property.Name}' on the control '{control.Metadata.Type.FullName}' must have a value!");
else attribute.AddError($"The attribute '{property.Name}' on the control '{control.Metadata.Type.CSharpName}' must have a value!");
}
else if (attribute.ValueNode is DothtmlValueBindingNode valueBindingNode)
{
Expand Down Expand Up @@ -422,7 +422,7 @@ public void ProcessControlContent(IAbstractControl control, IEnumerable<DothtmlN
control.Metadata.Type.IsAssignableTo(new ResolvedTypeDescriptor
(typeof(CompositeControl))) ?
" CompositeControls don't allow content by default and Content or ContentTemplate property is missing on this control." : "";
item.AddError($"Content not allowed inside {control.Metadata.Type.Name}.{compositeControlHelp}");
item.AddError($"Content not allowed inside {control.Metadata.Type.CSharpName}.{compositeControlHelp}");
}
}
}
Expand Down Expand Up @@ -460,7 +460,7 @@ IEnumerable<IAbstractControl> filterByType(ITypeDescriptor type, IEnumerable<IAb
c => {
// empty nodes are only filtered, non-empty nodes cause errors
if (c.DothtmlNode.IsNotEmpty())
c.DothtmlNode.AddError($"Control type {c.Metadata.Type.FullName} can't be used in collection of type {type.FullName}.");
c.DothtmlNode.AddError($"Control type {c.Metadata.Type.CSharpFullName} can't be used in collection of type {type.CSharpFullName}.");
});

// resolve data context
Expand All @@ -473,18 +473,6 @@ IEnumerable<IAbstractControl> filterByType(ITypeDescriptor type, IEnumerable<IAb
// template
return treeBuilder.BuildPropertyTemplate(property, ProcessTemplate(control, elementContent, dataContext), propertyWrapperElement);
}
else if (IsCollectionProperty(property))
{
var collectionType = GetCollectionType(property);
// collection of elements
var collection = elementContent.Select(childObject => ProcessNode(control, childObject, control.Metadata, dataContext)!);
if (collectionType != null)
{
collection = filterByType(collectionType, collection);
}

return treeBuilder.BuildPropertyControlCollection(property, collection.ToArray(), propertyWrapperElement);
}
else if (property.PropertyType.IsEqualTo(new ResolvedTypeDescriptor(typeof(string))))
{
// string property
Expand Down Expand Up @@ -514,9 +502,29 @@ IEnumerable<IAbstractControl> filterByType(ITypeDescriptor type, IEnumerable<IAb
return treeBuilder.BuildPropertyControl(property, null, propertyWrapperElement);
}
}
else if (IsCollectionProperty(property))
{
var collectionType = GetCollectionType(property);

// collection of elements
var collection = elementContent.Select(childObject => ProcessNode(control, childObject, control.Metadata, dataContext)!);
if (collectionType is null)
{
control.DothtmlNode!.AddError($"The property '{property.FullName}' is a collection, but the collection type could not be determined.");
}
else
{
if (!collectionType.IsAssignableTo(ResolvedTypeDescriptor.Create(typeof(IDotvvmObjectLike))))
control.DothtmlNode!.AddError($"The property '{property.FullName}' of type '{property.PropertyType.CSharpName}' cannot be used as an inner element. It is not a collection of DotvvmControl or IDotvvmObjectLike.");

collection = filterByType(collectionType, collection);
}

return treeBuilder.BuildPropertyControlCollection(property, collection.ToArray(), propertyWrapperElement);
}
else
{
control.DothtmlNode!.AddError($"The property '{property.FullName}' is not supported!");
control.DothtmlNode!.AddError($"The property '{property.FullName}' cannot be used as an inner element. The type '{property.PropertyType.CSharpName}' is not supported.");
return treeBuilder.BuildPropertyValue(property, null, propertyWrapperElement);
}
}
Expand Down Expand Up @@ -559,7 +567,7 @@ private IEnumerable<TNode> FilterNodes<TNode>(IEnumerable<DothtmlNode> nodes, IP

protected virtual bool IsCollectionProperty(IPropertyDescriptor property)
{
return property.PropertyType.IsAssignableTo(new ResolvedTypeDescriptor(typeof(ICollection)));
return property.PropertyType.IsAssignableTo(new ResolvedTypeDescriptor(typeof(IEnumerable)));
}

protected virtual ITypeDescriptor? GetCollectionType(IPropertyDescriptor property)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ public interface ITypeDescriptor
string? Assembly { get; }

string FullName { get; }
/// <summary> Returns type name with generic arguments in the C# style. Does not include namespaces. </summary>
string CSharpName { get; }
/// <summary> Returns type name including namespace with generic arguments in the C# style. </summary>
string CSharpFullName { get; }

bool IsAssignableTo(ITypeDescriptor typeDescriptor);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Reflection;
using DotVVM.Framework.Controls;
using DotVVM.Framework.Utils;
using FastExpressionCompiler;

namespace DotVVM.Framework.Compilation.ControlTree.Resolved
{
Expand All @@ -29,6 +30,9 @@ public ResolvedTypeDescriptor(Type type)

public string FullName => Type.FullName ?? (string.IsNullOrEmpty(Namespace) ? Name : (Namespace + "." + Name));

public string CSharpName => Type.ToCode(stripNamespace: true);
public string CSharpFullName => Type.ToCode();

public bool IsAssignableTo(ITypeDescriptor typeDescriptor)
{
return ToSystemType(typeDescriptor).IsAssignableFrom(Type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static IEnumerable<ControlUsageError> ValidateDefaultRules(IAbstractContr
if (missingProperties.Any())
{
yield return new ControlUsageError(
$"The control '{ control.Metadata.Type.FullName }' is missing required properties: { string.Join(", ", missingProperties.Select(p => "'" + p.Name + "'")) }.",
$"The control '{ control.Metadata.Type.CSharpName }' is missing required properties: { string.Join(", ", missingProperties.Select(p => "'" + p.Name + "'")) }.",
control.DothtmlNode
);
}
Expand All @@ -38,7 +38,7 @@ public static IEnumerable<ControlUsageError> ValidateDefaultRules(IAbstractContr
foreach (var unknownControl in unknownContent)
{
yield return new ControlUsageError(
$"The control '{ unknownControl.Metadata.Type.FullName }' does not inherit from DotvvmControl and thus cannot be used in content.",
$"The control '{ unknownControl.Metadata.Type.CSharpName }' does not inherit from DotvvmControl and thus cannot be used in content.",
control.DothtmlNode
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,36 +280,33 @@ public void EmitAddDirective(string controlName, string name, string value)

public ParameterExpression EmitEnsureCollectionInitialized(string parentName, DotvvmProperty property)
{
//if([parentName].GetValue(property) == null)
//{
// [parentName].SetValue(property, new [property.PropertyType]());
//}
// var collection = new [property.PropertyType]();
// [parentName].SetValue(property, collection);
// return collection;

var parentParameter = GetParameterOrVariable(parentName);

var getPropertyValue = Expression.Call(
parentParameter,
"GetValue",
emptyTypeArguments,
/*property:*/ EmitValue(property),
/*inherit:*/ EmitValue(true /*default*/ ));
var collectionType =
property.PropertyType.IsClass ? property.PropertyType :
property.PropertyType.IsGenericType ?
typeof(List<>).MakeGenericType(property.PropertyType.GetGenericArguments()[0]) :

throw new Exception($"Can not create collection {property.PropertyType.ToCode(stripNamespace: true)} for property {property.FullName}");

var ifCondition = Expression.Equal(getPropertyValue, Expression.Constant(null));
var collection = EmitCreateObject(collectionType);

var statement = Expression.Call(
parentParameter,
"SetValue",
emptyTypeArguments,
/*property*/ EmitValue(property),
/*value*/ EmitCreateObject(property.PropertyType));
/*value*/ collection);

var ifStatement = Expression.IfThen(ifCondition, statement);

EmitStatement(ifStatement);
EmitStatement(statement);

//var c = ([property.PropertyType])[parentName].GetValue(property);

return EmitCreateVariable(Expression.Convert(getPropertyValue, property.PropertyType));
return collection;
}

/// <summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Framework/Framework/Controls/TextOrContentCapability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,10 @@ public IEnumerable<DotvvmControl> ToControls()
else
return Content.NotNull();
}

public ITemplate ToTemplate()
{
return new CloneTemplate();
}
}
}
32 changes: 32 additions & 0 deletions src/Tests/ControlTests/CompositeControlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
using CheckTestOutput;
using DotVVM.Framework.Binding;
using DotVVM.Framework.Binding.Expressions;
using DotVVM.Framework.Compilation;
using DotVVM.Framework.Compilation.Styles;
using DotVVM.Framework.Controls;
using DotVVM.Framework.Controls.Infrastructure;
using DotVVM.Framework.Testing;
using DotVVM.Framework.Tests.Binding;
using DotVVM.Framework.Tests.Runtime;
using DotVVM.Framework.ViewModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;

Expand Down Expand Up @@ -210,6 +212,27 @@ public async Task ControlWithMultipleEnumClasses()
check.CheckString(r.FormattedHtml, fileExtension: "html");
}

[TestMethod]
public async Task ControlWithCollection()
{
var r = await cth.RunPage(typeof(BasicTestViewModel), @"
<cc:ControlWithCollectionProperty> <Repeaters> <dot:Repeater DataSource={value: List}> xx </dot:Repeater> </Repeaters> </cc:ControlWithCollectionProperty>
");

StringAssert.Contains(r.FormattedHtml, "1");
}

[TestMethod]
public async Task ControlWithCollection_WrongType()
{
var e = await Assert.ThrowsExceptionAsync<DotvvmCompilationException>(() =>
cth.RunPage(typeof(BasicTestViewModel), @"
<cc:ControlWithCollectionProperty> <Repeaters> <bazmek /> </Repeaters> </cc:ControlWithCollectionProperty>
"));

Assert.AreEqual("Control type DotVVM.Framework.Controls.HtmlGenericControl can't be used in collection of type DotVVM.Framework.Controls.Repeater.", e.Message);
}

public class BasicTestViewModel: DotvvmViewModelBase
{
[Bind(Name = "int")]
Expand Down Expand Up @@ -479,4 +502,13 @@ public enum EnumForCssClasses
[EnumMember(Value = "class-d")]
D
}
public class ControlWithCollectionProperty: CompositeControl
{
public static DotvvmControl GetContents(
IEnumerable<Repeater> repeaters
)
{
return new Literal(repeaters.Count().ToString());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
"Name": "ConfigurationSerializationTests",
"Namespace": "DotVVM.Framework.Tests.Runtime",
"Assembly": "DotVVM.Framework.Tests, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da",
"FullName": "DotVVM.Framework.Tests.Runtime.ConfigurationSerializationTests"
"FullName": "DotVVM.Framework.Tests.Runtime.ConfigurationSerializationTests",
"CSharpName": "ConfigurationSerializationTests",
"CSharpFullName": "DotVVM.Framework.Tests.Runtime.ConfigurationSerializationTests"
},
"Inherit": true
},
Expand All @@ -61,7 +63,9 @@
"Name": "Func`1",
"Namespace": "System",
"Assembly": "CoreLibrary",
"FullName": "System.Func`1[[System.Collections.Generic.IEnumerable`1[[System.Lazy`1[[System.IServiceProvider, ComponentLibrary]], CoreLibrary]], CoreLibrary]]"
"FullName": "System.Func`1[[System.Collections.Generic.IEnumerable`1[[System.Lazy`1[[System.IServiceProvider, ComponentLibrary]], CoreLibrary]], CoreLibrary]]",
"CSharpName": "Func<IEnumerable<Lazy<IServiceProvider>>>",
"CSharpFullName": "System.Func<System.Collections.Generic.IEnumerable<System.Lazy<System.IServiceProvider>>>"
},
"Inherit": true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
"Name": "TestApiClient",
"Namespace": "DotVVM.Framework.Tests.Binding",
"Assembly": "DotVVM.Framework.Tests, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da",
"FullName": "DotVVM.Framework.Tests.Binding.TestApiClient"
"FullName": "DotVVM.Framework.Tests.Binding.TestApiClient",
"CSharpName": "TestApiClient",
"CSharpFullName": "DotVVM.Framework.Tests.Binding.TestApiClient"
},
"Inherit": true
}
Expand Down

0 comments on commit 458cbda

Please sign in to comment.