diff --git a/.github/setup/action.yml b/.github/setup/action.yml
index b348a7a5bf..8b786bb560 100644
--- a/.github/setup/action.yml
+++ b/.github/setup/action.yml
@@ -32,7 +32,7 @@ runs:
shell: bash
# .NET
- - uses: actions/setup-dotnet@v3
+ - uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
diff --git a/src/Analyzers/Analyzers.Tests/DotVVM.Analyzers.Tests.csproj b/src/Analyzers/Analyzers.Tests/DotVVM.Analyzers.Tests.csproj
index 18e58be589..a50b53b11f 100644
--- a/src/Analyzers/Analyzers.Tests/DotVVM.Analyzers.Tests.csproj
+++ b/src/Analyzers/Analyzers.Tests/DotVVM.Analyzers.Tests.csproj
@@ -17,8 +17,8 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/Analyzers/Analyzers.Tests/Serializability/ViewModelSerializabilityTest.cs b/src/Analyzers/Analyzers.Tests/Serializability/ViewModelSerializabilityTest.cs
index 5c1baae28a..5f1e6a4543 100644
--- a/src/Analyzers/Analyzers.Tests/Serializability/ViewModelSerializabilityTest.cs
+++ b/src/Analyzers/Analyzers.Tests/Serializability/ViewModelSerializabilityTest.cs
@@ -1,9 +1,5 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
+using System.Threading.Tasks;
using DotVVM.Analyzers.Serializability;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.Testing;
using Xunit;
using VerifyCS = DotVVM.Analyzers.Tests.CSharpAnalyzerVerifier<
@@ -14,7 +10,7 @@ namespace DotVVM.Analyzers.Tests.Serializability
public class ViewModelSerializabilityTest
{
[Fact]
- public async void Test_NotSerializableProperty_RegularClass()
+ public async Task Test_NotSerializableProperty_RegularClass()
{
var test = @"
using System;
@@ -33,7 +29,7 @@ public class RegularClass
}
[Fact]
- public async void Test_NotSerializableProperty_ViewModel()
+ public async Task Test_NotSerializableProperty_ViewModel()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using DotVVM.Framework.ViewModel;
@@ -54,7 +50,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_SerializableRecordProperty_ViewModel()
+ public async Task Test_SerializableRecordProperty_ViewModel()
{
var test = @"
using DotVVM.Framework.ViewModel;
@@ -81,7 +77,7 @@ internal static class IsExternalInit {}
}
[Fact]
- public async void Test_NotSerializableList_ViewModel()
+ public async Task Test_NotSerializableList_ViewModel()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using DotVVM.Framework.ViewModel;
@@ -103,7 +99,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_WarnAboutInterface_ViewModel()
+ public async Task Test_WarnAboutInterface_ViewModel()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using DotVVM.Framework.ViewModel;
@@ -124,7 +120,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_Primitives_AreSerializableAndSupported_ViewModel()
+ public async Task Test_Primitives_AreSerializableAndSupported_ViewModel()
{
var test = @"
using DotVVM.Framework.ViewModel;
@@ -155,7 +151,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_Enums_AreSerializableAndSupported_ViewModel()
+ public async Task Test_Enums_AreSerializableAndSupported_ViewModel()
{
var test = @"
using DotVVM.Framework.ViewModel;
@@ -180,7 +176,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_DotVVMFriendlyObjects_AreSerializableAndSupported_ViewModel()
+ public async Task Test_DotVVMFriendlyObjects_AreSerializableAndSupported_ViewModel()
{
var test = @"
using DotVVM.Framework.ViewModel;
@@ -205,7 +201,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_NullablePrimitives_AreSerializableAndSupported_ViewModel()
+ public async Task Test_NullablePrimitives_AreSerializableAndSupported_ViewModel()
{
var test = @"
using DotVVM.Framework.ViewModel;
@@ -236,7 +232,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_NullableStructs_AreSerializableAndSupported_ViewModel()
+ public async Task Test_NullableStructs_AreSerializableAndSupported_ViewModel()
{
var test = @"
using DotVVM.Framework.ViewModel;
@@ -258,7 +254,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_NullableReferenceTypes_AreSerializableAndSupported_ViewModel()
+ public async Task Test_NullableReferenceTypes_AreSerializableAndSupported_ViewModel()
{
var test = @"
#nullable enable
@@ -285,7 +281,7 @@ public class Test
}
[Fact]
- public async void Test_CommonTypesAreSerializableAndSupported_ViewModel()
+ public async Task Test_CommonTypesAreSerializableAndSupported_ViewModel()
{
var test = @"
using DotVVM.Framework.ViewModel;
@@ -309,7 +305,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_CollectionAreSerializableAndSupported_ViewModel()
+ public async Task Test_CollectionAreSerializableAndSupported_ViewModel()
{
var test = @"
using DotVVM.Framework.ViewModel;
@@ -330,7 +326,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_NoWarningsForEnumerables_ViewModel()
+ public async Task Test_NoWarningsForEnumerables_ViewModel()
{
var test = @"
using DotVVM.Framework.ViewModel;
@@ -352,7 +348,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_UserTypesAreSerializableAndSupported_ViewModel()
+ public async Task Test_UserTypesAreSerializableAndSupported_ViewModel()
{
var test = @"
using DotVVM.Framework.ViewModel;
@@ -381,7 +377,7 @@ public class UserType
}
[Fact]
- public async void Test_NotSupportedProperty_ViewModel()
+ public async Task Test_NotSupportedProperty_ViewModel()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using DotVVM.Framework.ViewModel;
@@ -402,7 +398,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_PublicFieldsInViewModel()
+ public async Task Test_PublicFieldsInViewModel()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using DotVVM.Framework.ViewModel;
@@ -421,7 +417,7 @@ public class DefaultViewModel : DotvvmViewModelBase
VerifyCS.Diagnostic(ViewModelSerializabilityAnalyzer.DoNotUseFieldsRule).WithLocation(0));
}
[Fact]
- public async void Test_ConstFieldsInViewModel()
+ public async Task Test_ConstFieldsInViewModel()
{
var text = @"
using DotVVM.Framework.ViewModel;
@@ -441,7 +437,7 @@ public class DefaultViewModel : DotvvmViewModelBase
[Fact]
- public async void Test_StaticPropertiesInViewModel()
+ public async Task Test_StaticPropertiesInViewModel()
{
var text = @"
using DotVVM.Framework.ViewModel;
@@ -463,7 +459,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_StaticFieldsInViewModel()
+ public async Task Test_StaticFieldsInViewModel()
{
var text = @"
using DotVVM.Framework.ViewModel;
@@ -485,7 +481,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_NonPublicFieldsInViewModel()
+ public async Task Test_NonPublicFieldsInViewModel()
{
var text = @"
using DotVVM.Framework.ViewModel;
@@ -505,7 +501,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_NonPublicPropertiesInViewModel()
+ public async Task Test_NonPublicPropertiesInViewModel()
{
var text = @"
using DotVVM.Framework.ViewModel;
@@ -525,7 +521,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_IgnoreNonSerializedMembers_BindDirectionNone_ViewModel()
+ public async Task Test_IgnoreNonSerializedMembers_BindDirectionNone_ViewModel()
{
var text = @"
using DotVVM.Framework.ViewModel;
@@ -552,7 +548,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_SelfReferencingTypes_GenericArgs_ViewModel()
+ public async Task Test_SelfReferencingTypes_GenericArgs_ViewModel()
{
var text = @"
using DotVVM.Framework.ViewModel;
@@ -571,7 +567,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_SelfReferencingTypes_Properties_ViewModel()
+ public async Task Test_SelfReferencingTypes_Properties_ViewModel()
{
var text = @"
using DotVVM.Framework.ViewModel;
@@ -591,7 +587,7 @@ public class DefaultViewModel : DotvvmViewModelBase
[Fact]
- public async void Test_WhiteListedDotvvmTypes_Properties_ViewModel()
+ public async Task Test_WhiteListedDotvvmTypes_Properties_ViewModel()
{
var text = @"
using DotVVM.Framework.Controls;
@@ -612,7 +608,7 @@ public class DefaultViewModel : DotvvmViewModelBase
}
[Fact]
- public async void Test_OverridenSerialization_OnProperty_ViewModel()
+ public async Task Test_OverridenSerialization_OnProperty_ViewModel()
{
var text = @"
using DotVVM.Framework.Controls;
@@ -640,7 +636,7 @@ public class NonSerializable
}
[Fact]
- public async void Test_OverridenSerialization_OnTypeDeclaration_ViewModel()
+ public async Task Test_OverridenSerialization_OnTypeDeclaration_ViewModel()
{
var text = @"
using DotVVM.Framework.Controls;
@@ -668,7 +664,7 @@ public class NonSerializable
}
[Fact]
- public async void Test_InnerTypesWithNonSerializableProperties_RegularClass()
+ public async Task Test_InnerTypesWithNonSerializableProperties_RegularClass()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using DotVVM.Framework.Controls;
@@ -695,7 +691,7 @@ public class Entry
}
[Fact]
- public async void Test_GenericReferenceType_Properties_ViewModel()
+ public async Task Test_GenericReferenceType_Properties_ViewModel()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using DotVVM.Framework.Controls;
@@ -724,7 +720,7 @@ public class WrappedValue
}
[Fact]
- public async void Test_GenericViewModelType_Properties_ViewModel()
+ public async Task Test_GenericViewModelType_Properties_ViewModel()
{
var text = @"
using DotVVM.Framework.Controls;
diff --git a/src/AutoUI/Core/PropertyHandlers/FormEditors/EnumComboBoxFormEditorProvider.cs b/src/AutoUI/Core/PropertyHandlers/FormEditors/EnumComboBoxFormEditorProvider.cs
index 7b4d6d1f1b..c8d86ff239 100644
--- a/src/AutoUI/Core/PropertyHandlers/FormEditors/EnumComboBoxFormEditorProvider.cs
+++ b/src/AutoUI/Core/PropertyHandlers/FormEditors/EnumComboBoxFormEditorProvider.cs
@@ -32,10 +32,11 @@ public override DotvvmControl CreateControl(PropertyDisplayMetadata property, Au
LocalizableString.CreateNullable(displayAttribute?.Name, displayAttribute?.ResourceType) ??
LocalizableString.Constant(name.Humanize());
var title = LocalizableString.CreateNullable(displayAttribute?.Description, displayAttribute?.ResourceType);
- return (name, displayName, title);
- })
- .Select(e => new SelectorItem(e.displayName.ToBinding(context.BindingService), new(Enum.Parse(enumType, e.name)))
- .AddAttribute("title", e.title?.ToBinding(context.BindingService)));
+ var enumJsString = ReflectionUtils.ToEnumString(enumType, name);
+
+ return new SelectorItem(displayName.ToBinding(context.BindingService), new(enumJsString))
+ .AddAttribute("title", title?.ToBinding(context.BindingService));
+ });
var control = new ComboBox()
.SetCapability(props.Html)
diff --git a/src/Framework/Framework/Binding/ValueOrBinding.cs b/src/Framework/Framework/Binding/ValueOrBinding.cs
index aa5db0bf3c..bf06d6d635 100644
--- a/src/Framework/Framework/Binding/ValueOrBinding.cs
+++ b/src/Framework/Framework/Binding/ValueOrBinding.cs
@@ -160,6 +160,15 @@ public TResult ProcessValueBinding(DotvvmBindableObject control, Func If this contains a `resource` binding, it is evaluated and its value placed in property. `value`, and all other bindings are untouched and remain in the property.
+ public ValueOrBinding EvaluateResourceBinding(DotvvmBindableObject control)
+ {
+ if (binding is null or IValueBinding or not IStaticValueBinding) return this;
+
+ var value = this.Evaluate(control);
+ return new ValueOrBinding(value);
+ }
+
public static explicit operator ValueOrBinding(T val) => new ValueOrBinding(val);
public const string EqualsDisabledReason = "Equals is disabled on ValueOrBinding as it may lead to unexpected behavior. Please use object.ReferenceEquals for reference comparison or evaluate the ValueOrBinding and compare the value. Or use IsNull/NotNull for nullchecks on bindings.";
diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs
index 396ba9ccc8..92dee9430c 100644
--- a/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs
+++ b/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs
@@ -200,6 +200,10 @@ public IControlResolverMetadata ResolveControl(IControlType controlType)
///
protected abstract IControlType FindMarkupControl(string file);
+ /// Returns a list of possible DotVVM controls.
+ /// Used only for smart error handling, the list isn't necessarily complete, but doesn't contain false positives.
+ public abstract IEnumerable<(string tagPrefix, string? tagName, IControlType type)> EnumerateControlTypes();
+
///
/// Gets the control metadata.
///
diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlTreeHelper.cs b/src/Framework/Framework/Compilation/ControlTree/ControlTreeHelper.cs
index 98b2d7b4b1..2840188ad1 100644
--- a/src/Framework/Framework/Compilation/ControlTree/ControlTreeHelper.cs
+++ b/src/Framework/Framework/Compilation/ControlTree/ControlTreeHelper.cs
@@ -1,4 +1,5 @@
-using System;
+using System;
+using System.Collections.Generic;
using System.Linq;
using DotVVM.Framework.Compilation.Parser.Dothtml.Parser;
@@ -7,7 +8,7 @@ namespace DotVVM.Framework.Compilation.ControlTree
public static class ControlTreeHelper
{
public static bool HasEmptyContent(this IAbstractControl control)
- => control.Content.All(c => !DothtmlNodeHelper.IsNotEmpty(c.DothtmlNode)); // allow only whitespace literals
+ => control.Content.All(c => DothtmlNodeHelper.IsEmpty(c.DothtmlNode)); // allow only whitespace literals
public static bool HasProperty(this IAbstractControl control, IPropertyDescriptor property)
{
@@ -27,6 +28,19 @@ public static bool HasPropertyValue(this IAbstractControl control, IPropertyDesc
return value;
}
+ public static Dictionary GetPropertyGroup(this IAbstractControl control, IPropertyGroupDescriptor group)
+ {
+ var result = new Dictionary();
+ foreach (var prop in control.Properties)
+ {
+ if (prop.Key is IGroupedPropertyDescriptor member && member.PropertyGroup == group)
+ {
+ result.Add(member.GroupMemberName, prop.Value);
+ }
+ }
+ return result;
+ }
+
public static IPropertyDescriptor GetHtmlAttributeDescriptor(this IControlResolverMetadata metadata, string name)
=> metadata.GetPropertyGroupMember("", name);
public static IPropertyDescriptor GetPropertyGroupMember(this IControlResolverMetadata metadata, string prefix, string name)
diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs
index ae1474660f..59d0507420 100644
--- a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs
+++ b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs
@@ -13,6 +13,7 @@
using DotVVM.Framework.Compilation.Directives;
using DotVVM.Framework.Compilation.Binding;
using DotVVM.Framework.Compilation.ViewCompiler;
+using DotVVM.Framework.Configuration;
namespace DotVVM.Framework.Compilation.ControlTree
{
@@ -24,6 +25,7 @@ public abstract class ControlTreeResolverBase : IControlTreeResolver
protected readonly IControlResolver controlResolver;
protected readonly IAbstractTreeBuilder treeBuilder;
private readonly IMarkupDirectiveCompilerPipeline markupDirectiveCompilerPipeline;
+ protected readonly DotvvmConfiguration configuration;
protected Lazy rawLiteralMetadata;
protected Lazy literalMetadata;
protected Lazy placeholderMetadata;
@@ -31,11 +33,12 @@ public abstract class ControlTreeResolverBase : IControlTreeResolver
///
/// Initializes a new instance of the class.
///
- public ControlTreeResolverBase(IControlResolver controlResolver, IAbstractTreeBuilder treeBuilder, IMarkupDirectiveCompilerPipeline markupDirectiveCompilerPipeline)
+ public ControlTreeResolverBase(IControlResolver controlResolver, IAbstractTreeBuilder treeBuilder, IMarkupDirectiveCompilerPipeline markupDirectiveCompilerPipeline, DotvvmConfiguration configuration)
{
this.controlResolver = controlResolver;
this.treeBuilder = treeBuilder;
this.markupDirectiveCompilerPipeline = markupDirectiveCompilerPipeline;
+ this.configuration = configuration;
rawLiteralMetadata = new Lazy(() => controlResolver.ResolveControl(new ResolvedTypeDescriptor(typeof(RawLiteral))));
literalMetadata = new Lazy(() => controlResolver.ResolveControl(new ResolvedTypeDescriptor(typeof(Literal))));
placeholderMetadata = new Lazy(() => controlResolver.ResolveControl(new ResolvedTypeDescriptor(typeof(PlaceHolder))));
@@ -258,7 +261,12 @@ private IAbstractControl ProcessObjectElement(DothtmlElementNode element, IDataC
{
controlMetadata = controlResolver.ResolveControl("", element.TagName, out constructorParameters).NotNull();
constructorParameters = new[] { element.FullTagName };
- element.AddError($"The control <{element.FullTagName}> could not be resolved! Make sure that the tagPrefix is registered in DotvvmConfiguration.Markup.Controls collection!");
+ var similarControls = FindSimilarControls(element.TagPrefix, element.TagName, controlBaseType: controlMetadata.Type);
+ var similarNameHelp = similarControls.Any() ? $" Did you mean {string.Join(", ", similarControls.Select(c => c.tagPrefix + ":" + c.name))}, or other DotVVM control?" : "";
+ var tagPrefixHelp = configuration.Markup.Controls.Any(c => string.Equals(c.TagPrefix, element.TagPrefix, StringComparison.OrdinalIgnoreCase))
+ ? ""
+ : $" {(similarNameHelp is "" ? "Make" : "Otherwise, make")} sure that the tagPrefix '{element.TagPrefix}' is registered in DotvvmConfiguration.Markup.Controls collection!";
+ element.TagNameNode.AddError($"The control <{element.FullTagName}> could not be resolved!{similarNameHelp}{tagPrefixHelp}");
}
if (controlMetadata.VirtualPath is {} && controlMetadata.Type.IsAssignableTo(ResolvedTypeDescriptor.Create(typeof(DotvvmView))))
{
@@ -288,7 +296,7 @@ private IAbstractControl ProcessObjectElement(DothtmlElementNode element, IDataC
}
if (controlMetadata.DataContextConstraint != null && dataContext != null && !controlMetadata.DataContextConstraint.IsAssignableFrom(dataContext.DataContextType))
{
- ((DothtmlNode?)dataContextAttribute ?? element)
+ ((DothtmlNode?)dataContextAttribute ?? element.TagNameNode)
.AddError($"The control '{controlMetadata.Type.CSharpName}' requires a DataContext of type '{controlMetadata.DataContextConstraint.CSharpFullName}'!");
}
@@ -476,7 +484,7 @@ private static void AddHtmlAttributeWarning(IAbstractControl control, DothtmlAtt
// Ignore SVG attributes (unless they also start with an uppercase letter)
- if ((allowFirstCharacterUppercase || !char.IsUpper(name[0])) && uppercaseHtmlAttributeList.Contains(name))
+ if ((allowFirstCharacterUppercase || !char.IsUpper(name, 0)) && uppercaseHtmlAttributeList.Contains(name))
return;
if (pGroup.Name.EndsWith("Attributes") &&
@@ -486,7 +494,8 @@ private static void AddHtmlAttributeWarning(IAbstractControl control, DothtmlAtt
var similarNameProperties =
control.Metadata.AllProperties
.Where(p => StringSimilarity.DamerauLevenshteinDistance(p.Name.ToLowerInvariant(), (prefix + name).ToLowerInvariant()) <= 2)
- .Select(p => p.Name)
+ // suggest the alias if the property is obsolete
+ .Select(p => p.ObsoleteAttribute is null && p is PropertyAliasAttribute alias ? alias.AliasedPropertyName : p.Name)
.ToArray();
var similarPropertyHelp =
similarNameProperties.Any() ? $" Did you mean {string.Join(", ", similarNameProperties)}, or another DotVVM property?" : " Did you intent to use a DotVVM property instead?";
@@ -496,17 +505,67 @@ private static void AddHtmlAttributeWarning(IAbstractControl control, DothtmlAtt
}
}
+ private (string tagPrefix, string name, IControlType)[] FindSimilarControls(string? tagPrefix, string elementName, ITypeDescriptor? controlBaseType = null, int threshold = 4, int limit = 5)
+ {
+ return (
+ from c in this.controlResolver.EnumerateControlTypes()
+ where controlBaseType is null || c.type.Type.IsAssignableTo(controlBaseType)
+ let prefixScore = tagPrefix is null ? 0 : StringSimilarity.DamerauLevenshteinDistance(c.tagPrefix, tagPrefix)
+ where prefixScore <= threshold
+ from controlName in Enumerable.Concat([ c.tagName ?? c.type.PrimaryName ], c.type.AlternativeNames)
+ let nameScore = StringSimilarity.DamerauLevenshteinDistance(elementName.ToLowerInvariant(), controlName.ToLowerInvariant())
+ where prefixScore + nameScore <= threshold
+ orderby (prefixScore + nameScore, controlName, c.tagPrefix) descending
+ select (c.tagPrefix, controlName, c.type)
+ ).Take(limit).ToArray();
+ }
+
+ private string? GetMissingElementPropertyDiagnostic(IAbstractControl parentControl, DothtmlElementNode element, ITypeDescriptor? allowedControlTypes = null)
+ {
+ // * warning: content allowed, but element has uppercase letter
+ // * error: content not allowed, no property matches
+ // * error: content allowed, HtmlGenericControl isn't allowed, no property matches
+ var isUppercase = char.IsUpper(element.TagName, 0);
+ var similarNameControls =
+ !parentControl.Metadata.IsContentAllowed && parentControl.Metadata.DefaultContentProperty is null
+ ? []
+ : FindSimilarControls(element.TagPrefix, element.TagName, allowedControlTypes);
+ var similarNameProperties =
+ parentControl.Metadata.AllProperties
+ .Where(p => p.MarkupOptions.MappingMode.HasFlag(MappingMode.InnerElement) &&
+ StringSimilarity.DamerauLevenshteinDistance(p.Name.ToLowerInvariant(), element.FullTagName.ToLowerInvariant()) <= 4)
+ .Select(p => p.Name)
+ .ToArray();
+ if (similarNameControls.Any() && similarNameProperties.Any())
+ {
+ return $" Did you mean property {string.Join(", ", similarNameProperties)}, or control {string.Join(", ", similarNameControls.Select(c => c.tagPrefix + ":" + c.name))}?";
+ }
+ else if (similarNameProperties.Any())
+ {
+ return $" Did you mean {string.Join(", ", similarNameProperties)}, or another DotVVM property?";
+ }
+ else if (similarNameControls.Any())
+ {
+ return $" Did you mean {string.Join(", ", similarNameControls.Select(c => c.tagPrefix + ":" + c.name))}, or another DotVVM control?";
+ }
+ else
+ {
+ return null;
+ }
+ }
+
///
/// Processes the content of the control node.
///
public void ProcessControlContent(IAbstractControl control, IEnumerable nodes)
{
+ var allowsContent = control.Metadata.IsContentAllowed || control.Metadata.DefaultContentProperty is {};
var content = new List();
bool properties = true;
foreach (var node in nodes)
{
var element = node as DothtmlElementNode;
- if (element != null && properties)
+ if (element is {} && properties)
{
var property = controlResolver.FindProperty(control.Metadata, element.TagName, MappingMode.InnerElement);
if (property != null && string.IsNullOrEmpty(element.TagPrefix) && property.MarkupOptions.MappingMode.HasFlag(MappingMode.InnerElement))
@@ -515,10 +574,33 @@ public void ProcessControlContent(IAbstractControl control, IEnumerable c.IsNotEmpty())}')).");
+ }
+ else
+ {
+ element.TagNameNode.AddWarning(
+ $"HTML element name '{element.TagName}' should not contain uppercase letters.{GetMissingElementPropertyDiagnostic(control, element)}"
+ );
+ }
+ }
+ }
}
}
if (control.Metadata.DefaultContentProperty is IPropertyDescriptor contentProperty)
{
// don't assign the property, when content is empty
- if (content.All(c => !c.IsNotEmpty()))
+ if (content.All(c => c.IsEmpty()))
return;
if (control.HasProperty(contentProperty))
@@ -551,21 +654,10 @@ public void ProcessControlContent(IAbstractControl control, IEnumerable filterByType(ITypeDescriptor type, IEnumerable c is object && c.Metadata.Type.IsAssignableTo(type),
c => {
- // empty nodes are only filtered, non-empty nodes cause errors
+ // empty nodes are only filtered out, non-empty nodes cause errors
if (c.DothtmlNode.IsNotEmpty())
- c.DothtmlNode.AddError($"Control type {c.Metadata.Type.CSharpFullName} can't be used in collection of type {type.CSharpFullName}.");
+ {
+ // when used without explicit property wrapper element, add information about available content properties
+ var propertyHelp = propertyWrapperElement is null && c.DothtmlNode is DothtmlElementNode element ? GetMissingElementPropertyDiagnostic(control, element, type) : null;
+ ((c.DothtmlNode as DothtmlElementNode)?.TagNameNode ?? c.DothtmlNode)
+ .AddError($"Control type {c.Metadata.Type.CSharpFullName} can't be used in a property of type {type.CSharpFullName}.{propertyHelp}");
+ }
});
// resolve data context
diff --git a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs
index 4736b7e1ec..7405493110 100644
--- a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs
+++ b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs
@@ -300,6 +300,47 @@ public override IControlResolverMetadata BuildControlMetadata(IControlType type)
return new ControlResolverMetadata((ControlType)type);
}
+ public override IEnumerable<(string tagPrefix, string? tagName, IControlType type)> EnumerateControlTypes()
+ {
+ var markupControls = new HashSet<(string, string)>(); // don't report MarkupControl with @baseType twice
+
+ foreach (var control in configuration.Markup.Controls)
+ {
+ if (!string.IsNullOrEmpty(control.Src))
+ {
+ markupControls.Add((control.TagPrefix!, control.TagName!));
+ IControlType? markupControl = null;
+ try
+ {
+ markupControl = FindMarkupControl(control.Src);
+ }
+ catch { } // ignore the error, we should not crash here
+ if (markupControl != null)
+ yield return (control.TagPrefix!, control.TagName, markupControl);
+ }
+ }
+
+ foreach (var assemblyGroup in configuration.Markup.Controls.Where(c => !string.IsNullOrEmpty(c.Assembly) && string.IsNullOrEmpty(c.Src)).GroupBy(c => c.Assembly!))
+ {
+ var assembly = compiledAssemblyCache.GetAssembly(assemblyGroup.Key);
+ if (assembly is null)
+ continue;
+
+ var namespaces = assemblyGroup.GroupBy(c => c.Namespace ?? "").ToDictionary(g => g.Key, g => g.First());
+ foreach (var type in assembly.GetLoadableTypes())
+ {
+ if (type.IsPublic && !type.IsAbstract &&
+ type.DeclaringType is null &&
+ typeof(DotvvmBindableObject).IsAssignableFrom(type) &&
+ namespaces.TryGetValue(type.Namespace ?? "", out var controlConfig))
+ {
+ if (!markupControls.Contains((controlConfig.TagPrefix!, type.Name)))
+ yield return (controlConfig.TagPrefix!, null, new ControlType(type));
+ }
+ }
+ }
+ }
+
protected override IPropertyDescriptor? FindGlobalPropertyOrGroup(string name, MappingMode requiredMode)
{
// try to find property
diff --git a/src/Framework/Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs b/src/Framework/Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs
index 9e5cd0f451..fa19c3b7ef 100644
--- a/src/Framework/Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs
+++ b/src/Framework/Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs
@@ -5,6 +5,7 @@
using DotVVM.Framework.Compilation.Directives;
using DotVVM.Framework.Compilation.Parser.Dothtml.Parser;
using DotVVM.Framework.Compilation.ViewCompiler;
+using DotVVM.Framework.Configuration;
using DotVVM.Framework.Utils;
namespace DotVVM.Framework.Compilation.ControlTree
@@ -23,8 +24,9 @@ public DefaultControlTreeResolver(
IControlResolver controlResolver,
IAbstractTreeBuilder treeBuilder,
IControlBuilderFactory controlBuilderFactory,
- IMarkupDirectiveCompilerPipeline direrectiveCompilerPipeline)
- : base(controlResolver, treeBuilder, direrectiveCompilerPipeline)
+ IMarkupDirectiveCompilerPipeline direrectiveCompilerPipeline,
+ DotvvmConfiguration configuration)
+ : base(controlResolver, treeBuilder, direrectiveCompilerPipeline, configuration)
{
this.controlBuilderFactory = controlBuilderFactory;
}
diff --git a/src/Framework/Framework/Compilation/ControlTree/IAbstractControl.cs b/src/Framework/Framework/Compilation/ControlTree/IAbstractControl.cs
index 5431488b32..7babee5d33 100644
--- a/src/Framework/Framework/Compilation/ControlTree/IAbstractControl.cs
+++ b/src/Framework/Framework/Compilation/ControlTree/IAbstractControl.cs
@@ -8,6 +8,7 @@ namespace DotVVM.Framework.Compilation.ControlTree
public interface IAbstractControl : IAbstractContentNode
{
IEnumerable PropertyNames { get; }
+ IEnumerable> Properties { get; }
bool TryGetProperty(IPropertyDescriptor property, [NotNullWhen(true)] out IAbstractPropertySetter? value);
diff --git a/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs b/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs
index a781632f9b..fb2d92c6ff 100644
--- a/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs
+++ b/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using DotVVM.Framework.Controls;
using DotVVM.Framework.Runtime;
@@ -26,6 +27,10 @@ public interface IControlResolver
///
IControlResolverMetadata ResolveControl(ITypeDescriptor controlType);
+ /// Returns a list of possible DotVVM controls.
+ /// Used only for smart error handling, the list isn't necessarily complete, but doesn't contain false positives.
+ IEnumerable<(string tagPrefix, string? tagName, IControlType type)> EnumerateControlTypes();
+
///
/// Resolves the binding type.
///
diff --git a/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedControl.cs b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedControl.cs
index 9f8cfcede7..135f70047b 100644
--- a/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedControl.cs
+++ b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedControl.cs
@@ -18,6 +18,10 @@ public class ResolvedControl : ResolvedContentNode, IAbstractControl
IEnumerable IAbstractControl.PropertyNames => Properties.Keys;
+ IEnumerable> IAbstractControl.Properties =>
+ Properties.Select(p => new KeyValuePair(p.Key, p.Value));
+
+
public ResolvedControl(ControlResolverMetadata metadata, DothtmlNode? node, DataContextStack dataContext)
: base(metadata, node, dataContext) { }
diff --git a/src/Framework/Framework/Compilation/ControlTree/ResolvedTreeHelpers.cs b/src/Framework/Framework/Compilation/ControlTree/ResolvedTreeHelpers.cs
index b6b62c21c9..582394faba 100644
--- a/src/Framework/Framework/Compilation/ControlTree/ResolvedTreeHelpers.cs
+++ b/src/Framework/Framework/Compilation/ControlTree/ResolvedTreeHelpers.cs
@@ -55,7 +55,7 @@ public static DataContextStack GetDataContextStack(this ResolvedPropertySetter s
public static bool IsOnlyWhitespace(this IAbstractControl control) =>
- control.Metadata.Type.IsEqualTo(ResolvedTypeDescriptor.Create(typeof(RawLiteral))) && control.DothtmlNode?.IsNotEmpty() == false;
+ control.Metadata.Type.IsEqualTo(ResolvedTypeDescriptor.Create(typeof(RawLiteral))) && control.DothtmlNode?.IsEmpty() == true;
public static bool HasOnlyWhiteSpaceContent(this IAbstractContentNode control) =>
control.Content.All(IsOnlyWhitespace);
diff --git a/src/Framework/Framework/Compilation/ControlType.cs b/src/Framework/Framework/Compilation/ControlType.cs
index 07bb601b8d..e8c4782309 100644
--- a/src/Framework/Framework/Compilation/ControlType.cs
+++ b/src/Framework/Framework/Compilation/ControlType.cs
@@ -1,7 +1,9 @@
using System;
using System.Diagnostics;
+using System.Reflection;
using DotVVM.Framework.Compilation.ControlTree;
using DotVVM.Framework.Compilation.ControlTree.Resolved;
+using DotVVM.Framework.Controls;
namespace DotVVM.Framework.Compilation
{
@@ -17,6 +19,10 @@ public sealed class ControlType : IControlType
ITypeDescriptor? IControlType.DataContextRequirement => ResolvedTypeDescriptor.Create(DataContextRequirement);
+ public string PrimaryName => GetControlNames(Type).primary;
+
+ public string[] AlternativeNames => GetControlNames(Type).alternative;
+
static void ValidateControlClass(Type control)
{
if (!control.IsPublic && !control.IsNestedPublic)
@@ -35,6 +41,19 @@ public ControlType(Type type, string? virtualPath = null, Type? dataContextRequi
DataContextRequirement = dataContextRequirement;
}
+ public static (string primary, string[] alternative) GetControlNames(Type controlType)
+ {
+ var attr = controlType.GetCustomAttribute();
+ if (attr is null)
+ {
+ return (controlType.Name, Array.Empty());
+ }
+ else
+ {
+ return (attr.PrimaryName ?? controlType.Name, attr.AlternativeNames ?? Array.Empty());
+ }
+ }
+
public override bool Equals(object? obj)
{
diff --git a/src/Framework/Framework/Compilation/IControlType.cs b/src/Framework/Framework/Compilation/IControlType.cs
index e7700133d4..af410279b7 100644
--- a/src/Framework/Framework/Compilation/IControlType.cs
+++ b/src/Framework/Framework/Compilation/IControlType.cs
@@ -11,5 +11,9 @@ public interface IControlType
ITypeDescriptor? DataContextRequirement { get; }
+ string PrimaryName { get; }
+
+ string[] AlternativeNames { get; }
+
}
}
diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DotHtmlCommentNode.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DotHtmlCommentNode.cs
index fadb6c1efd..4613325a8d 100644
--- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DotHtmlCommentNode.cs
+++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DotHtmlCommentNode.cs
@@ -39,5 +39,8 @@ public override IEnumerable EnumerateNodes()
{
return base.EnumerateNodes().Concat(EnumerateChildNodes().SelectMany(node => node.EnumerateNodes()));
}
+
+ public override string ToString() =>
+ IsServerSide ? $"<%-- {Value} --%>" : $"";
}
}
diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlAttributeNode.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlAttributeNode.cs
index 5a3ef2d6e5..8eb59fd916 100644
--- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlAttributeNode.cs
+++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlAttributeNode.cs
@@ -5,14 +5,14 @@
namespace DotVVM.Framework.Compilation.Parser.Dothtml.Parser
{
- [DebuggerDisplay("{debuggerDisplay,nq}{ValueNode}")]
public sealed class DothtmlAttributeNode : DothtmlNode
{
- #region debugger display
- [DebuggerBrowsable(DebuggerBrowsableState.Never)]
- private string debuggerDisplay =>
- AttributeFullName + (ValueNode == null ? "" : "=");
- #endregion
+ public override string ToString() =>
+ AttributeFullName + ValueNode switch {
+ null => "",
+ DothtmlValueBindingNode => $"={ValueNode}",
+ _ => $"=\"{ValueNode}\""
+ };
public string? AttributePrefix => AttributePrefixNode?.Text;
public string AttributeName => AttributeNameNode.Text;
diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlBindingNode.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlBindingNode.cs
index 7b782d508e..cec3939e70 100644
--- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlBindingNode.cs
+++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlBindingNode.cs
@@ -5,21 +5,12 @@
namespace DotVVM.Framework.Compilation.Parser.Dothtml.Parser
{
- [DebuggerDisplay("{debuggerDisplay,nq}")]
public class DothtmlBindingNode : DothtmlNode
{
-
- #region debugger display
- [DebuggerBrowsable(DebuggerBrowsableState.Never)]
- private string debuggerDisplay
+ public override string ToString()
{
- get
- {
- return "{" + Name + ": " + Value + "}";
- }
+ return "{" + Name + ": " + Value + "}";
}
- #endregion
-
public DothtmlBindingNode(DothtmlToken startToken, DothtmlToken endToken, DothtmlToken separatorToken, DothtmlNameNode nameNode, DothtmlValueTextNode valueNode)
{
diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlDirectiveNode.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlDirectiveNode.cs
index f23f498aac..e4fe1d432b 100644
--- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlDirectiveNode.cs
+++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlDirectiveNode.cs
@@ -39,5 +39,7 @@ public override IEnumerable EnumerateNodes()
{
return base.EnumerateNodes().Concat( EnumerateChildNodes().SelectMany(node => node.EnumerateNodes() ) );
}
+
+ public override string ToString() => $"@{Name} {Value}";
}
}
diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlElementNode.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlElementNode.cs
index 1240af5394..ba0f643736 100644
--- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlElementNode.cs
+++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlElementNode.cs
@@ -5,19 +5,12 @@
namespace DotVVM.Framework.Compilation.Parser.Dothtml.Parser
{
- [DebuggerDisplay("{debuggerDisplay,nq}")]
public sealed class DothtmlElementNode : DothtmlNodeWithContent
{
- #region debugger display
- [DebuggerBrowsable(DebuggerBrowsableState.Never)]
- private string debuggerDisplay
+ public override string ToString()
{
- get
- {
- return "<" + (IsClosingTag ? "/" : "") + FullTagName + (Attributes.Any() ? " ..." : "") + (IsSelfClosingTag ? " /" : "") + ">";
- }
+ return "<" + (IsClosingTag ? "/" : "") + FullTagName + (Attributes.Any() ? " ..." : "") + (IsSelfClosingTag ? " /" : "") + ">";
}
- #endregion
public string TagName => TagNameNode.Text;
public string? TagPrefix => TagPrefixNode?.Text;
diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlLiteralNode.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlLiteralNode.cs
index 95a957c87b..f468741ae7 100644
--- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlLiteralNode.cs
+++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlLiteralNode.cs
@@ -5,7 +5,6 @@
namespace DotVVM.Framework.Compilation.Parser.Dothtml.Parser
{
- [DebuggerDisplay("{Value}")]
public sealed class DothtmlLiteralNode : DothtmlNode
{
public DothtmlToken? MainValueToken { get; set; }
@@ -20,5 +19,7 @@ public override void Accept(IDothtmlSyntaxTreeVisitor visitor)
{
visitor.Visit(this);
}
+
+ public override string ToString() => Value;
}
}
diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlNodeHelper.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlNodeHelper.cs
index c8f79fe4e6..2319ffb117 100644
--- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlNodeHelper.cs
+++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlNodeHelper.cs
@@ -5,11 +5,13 @@ namespace DotVVM.Framework.Compilation.Parser.Dothtml.Parser
{
public static class DothtmlNodeHelper
{
- public static bool IsNotEmpty([NotNullWhen(true)] this DothtmlNode? node)
+ public static bool IsNotEmpty([NotNullWhen(true)] this DothtmlNode? node) =>
+ !IsEmpty(node);
+
+ public static bool IsEmpty([NotNullWhen(false)] this DothtmlNode? node)
{
- return node is object &&
- !(node is DotHtmlCommentNode) &&
- !(node is DothtmlLiteralNode literalNode && string.IsNullOrWhiteSpace(literalNode.Value));
+ return node is null or DotHtmlCommentNode ||
+ (node is DothtmlLiteralNode literalNode && string.IsNullOrWhiteSpace(literalNode.Value));
}
public static int GetContentStartPosition(this DothtmlElementNode node)
diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlValueBindingNode.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlValueBindingNode.cs
index 6084be6f5a..1256d4ec20 100644
--- a/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlValueBindingNode.cs
+++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Parser/DothtmlValueBindingNode.cs
@@ -39,5 +39,7 @@ public override IEnumerable EnumerateNodes()
{
return base.EnumerateNodes().Concat(EnumerateChildNodes().SelectMany(n=> n.EnumerateNodes()));
}
+
+ public override string ToString() => BindingNode.ToString();
}
}
diff --git a/src/Framework/Framework/Configuration/DefaultControlRegistrationStrategy.cs b/src/Framework/Framework/Configuration/DefaultControlRegistrationStrategy.cs
index 5ac3d1b336..4f37c56201 100644
--- a/src/Framework/Framework/Configuration/DefaultControlRegistrationStrategy.cs
+++ b/src/Framework/Framework/Configuration/DefaultControlRegistrationStrategy.cs
@@ -4,12 +4,13 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
+using DotVVM.Framework.Routing;
namespace DotVVM.Framework.Configuration
{
public class DefaultControlRegistrationStrategy : IControlRegistrationStrategy
{
- private DotvvmConfiguration configuration;
+ readonly DotvvmConfiguration configuration;
readonly string controlPrefix;
readonly string controlsDirectory;
readonly string filesFilter;
@@ -31,6 +32,10 @@ protected virtual IEnumerable ListFiles()
public IEnumerable GetControls()
=> ListFiles()
- .Select(f => new DotvvmControlConfiguration { Src = f, TagPrefix = GetControlPrefix(f), TagName = GetControlName(f) });
+ .Select(f => new DotvvmControlConfiguration {
+ Src = DefaultRouteStrategy.GetRelativePathBetween(configuration.ApplicationPhysicalPath, f),
+ TagPrefix = GetControlPrefix(f),
+ TagName = GetControlName(f)
+ });
}
}
diff --git a/src/Framework/Framework/Configuration/RestApiRegistrationHelpers.cs b/src/Framework/Framework/Configuration/RestApiRegistrationHelpers.cs
index 5254122e8d..2c2003a9a4 100644
--- a/src/Framework/Framework/Configuration/RestApiRegistrationHelpers.cs
+++ b/src/Framework/Framework/Configuration/RestApiRegistrationHelpers.cs
@@ -227,7 +227,7 @@ private static JsObjectExpression CreateHttpObj(string customFetchFunction)
private static void RegisterApiDependencies(DotvvmConfiguration configuration, string identifier, string jsApiClientFile, JsNode jsinitializer, ApiGroupDescriptor descriptor)
{
configuration.Resources.RegisterScript("apiClient" + identifier, new FileResourceLocation(jsApiClientFile));
- configuration.Resources.Register("apiInit" + identifier, new InlineScriptResource(defer: true, code: jsinitializer.FormatScript(niceMode: configuration.Debug)) { Dependencies = new[] { "dotvvm", "apiClient" + identifier } });
+ configuration.Resources.Register("apiInit" + identifier, new InlineScriptResource(module: true, code: jsinitializer.FormatScript(niceMode: configuration.Debug)) { Dependencies = new[] { "dotvvm", "apiClient" + identifier } });
configuration.Markup.DefaultExtensionParameters.Add(new ApiExtensionParameter(identifier, descriptor));
diff --git a/src/Framework/Framework/Controls/GridViewColumn.cs b/src/Framework/Framework/Controls/GridViewColumn.cs
index 5c0ab92a03..b3c368fecb 100644
--- a/src/Framework/Framework/Controls/GridViewColumn.cs
+++ b/src/Framework/Framework/Controls/GridViewColumn.cs
@@ -123,7 +123,7 @@ public static readonly DotvvmProperty WidthProperty
= DotvvmProperty.Register(c => c.Width, null);
[PopDataContextManipulation]
- [MarkupOptions(AllowHardCodedValue = false)]
+ [MarkupOptions]
public bool Visible
{
get { return (bool)GetValue(VisibleProperty)!; }
diff --git a/src/Framework/Framework/Controls/Infrastructure/BodyResourceLinks.cs b/src/Framework/Framework/Controls/Infrastructure/BodyResourceLinks.cs
index 877a117065..581b1cfe7f 100644
--- a/src/Framework/Framework/Controls/Infrastructure/BodyResourceLinks.cs
+++ b/src/Framework/Framework/Controls/Infrastructure/BodyResourceLinks.cs
@@ -41,8 +41,7 @@ protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext
{
initCode = $"ko.options.deferUpdates = true;\n{initCode}";
}
- new InlineScriptResource(initCode, defer: true)
- .Render(writer, context, "dotvvm-init-script");
+ InlineScriptResource.RenderScript(writer, initCode, defer: true, module: true);
var warnings = RenderWarnings(context);
if (warnings.Length > 0)
diff --git a/src/Framework/Framework/Controls/JsComponent.cs b/src/Framework/Framework/Controls/JsComponent.cs
index 215fa50ec9..5a1d4e0f2f 100644
--- a/src/Framework/Framework/Controls/JsComponent.cs
+++ b/src/Framework/Framework/Controls/JsComponent.cs
@@ -1,9 +1,14 @@
+using System;
+using System.Collections.Generic;
using System.Linq;
using DotVVM.Framework.Binding;
using DotVVM.Framework.Binding.Expressions;
+using DotVVM.Framework.Binding.Properties;
using DotVVM.Framework.Compilation.ControlTree;
using DotVVM.Framework.Compilation.ControlTree.Resolved;
+using DotVVM.Framework.Compilation.Parser.Dothtml.Parser;
using DotVVM.Framework.Compilation.Styles;
+using DotVVM.Framework.Compilation.Validation;
using DotVVM.Framework.Hosting;
using DotVVM.Framework.ResourceManagement;
using DotVVM.Framework.Utils;
@@ -11,9 +16,15 @@
namespace DotVVM.Framework.Controls
{
/// Control which initializes a client-side component.
+ ///
+ /// The client-side component is either exported from a view module referenced in the page's @js directive, or registered using the dotvvm.registerGlobalComponent method.
+ /// The module should export a $controls field with any number of named components, (TypeScript signature is $controls?: { [name:string]: DotvvmJsComponentFactory })
+ ///
+ ///
public class JsComponent : DotvvmControl
{
- /// If set to true, only globally registered JsComponents will be considered for rendering client-side.
+ /// If set to true, view modules are ignored and JsComponents registered using dotvvm.registerGlobalComponent will be considered for client-side rendering.
+ [MarkupOptions(AllowBinding = false)]
public bool Global
{
get { return (bool)GetValue(GlobalProperty)!; }
@@ -23,7 +34,7 @@ public bool Global
DotvvmProperty.Register(nameof(Global));
/// Name by which the client-side component was registered. The name is case sensitive.
- [MarkupOptions(Required = true)]
+ [MarkupOptions(Required = true, AllowBinding = false)]
public string Name
{
get { return (string)GetValue(NameProperty)!; }
@@ -33,6 +44,7 @@ public string Name
DotvvmProperty.Register(nameof(Name));
/// The JsComponent must have a wrapper HTML tag, this property configures which tag is used. By default, `div` is used.
+ [MarkupOptions(AllowBinding = false)]
public string WrapperTagName
{
get { return (string)GetValue(WrapperTagNameProperty)!; }
@@ -132,7 +144,8 @@ protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext
{ "props", props },
{ "templates", templates },
};
- if (GetValue(Internal.ReferencedViewModuleInfoProperty) is ViewModuleReferenceInfo viewModule)
+ if (GetValue(Internal.ReferencedViewModuleInfoProperty) is ViewModuleReferenceInfo viewModule &&
+ GetValue(GlobalProperty) is not true)
binding.Add("view", ViewModuleHelpers.GetViewIdJsExpression(viewModule, this));
writer.AddKnockoutDataBind("dotvvm-js-component", binding);
@@ -154,5 +167,34 @@ public static void AddReferencedViewModuleInfoProperty(ResolvedControl control)
control.ConstructorParameters = null;
}
}
+
+ [ControlUsageValidator]
+ public static IEnumerable ValidateUsage(ResolvedControl control)
+ {
+ if (!control.TreeRoot.HasProperty(Internal.ReferencedViewModuleInfoProperty) &&
+ control.GetProperty(GlobalProperty) is null or ResolvedPropertyValue { Value: false })
+ {
+ yield return new ControlUsageError(
+ $"This view does not have any view modules registered, only global JsComponent will work. Add the `Global` property to this component, to make the intent clear.",
+ DiagnosticSeverity.Warning,
+ (control.DothtmlNode as DothtmlElementNode)?.TagNameNode
+ );
+ }
+
+ var props = control.GetPropertyGroup(PropsGroupDescriptor);
+ var templates = control.GetPropertyGroup(TemplatesGroupDescriptor);
+
+ foreach (var name in props.Keys.Intersect(templates.Keys))
+ {
+ var templateElement = templates[name].DothtmlNode;
+ yield return new ControlUsageError(
+ $"JsComponent property and template must not share the same name ('{name}').",
+ DiagnosticSeverity.Error,
+ props[name].DothtmlNode,
+ (templateElement as DothtmlElementNode)?.TagNameNode ?? templateElement
+ );
+ }
+ }
+
}
}
diff --git a/src/Framework/Framework/Controls/SelectorItem.cs b/src/Framework/Framework/Controls/SelectorItem.cs
index 962e701b1e..800e85a665 100644
--- a/src/Framework/Framework/Controls/SelectorItem.cs
+++ b/src/Framework/Framework/Controls/SelectorItem.cs
@@ -51,7 +51,16 @@ public SelectorItem(ValueOrBinding text, ValueOrBinding