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 value) protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context) { - writer.AddAttribute("value", Value + ""); + var value = this.GetValueOrBinding(ValueProperty).EvaluateResourceBinding(this); + if (value.ValueOrDefault is string s) + { + writer.AddAttribute("value", s); + } + else + { + // anything else than string is better to pass as knockout value binding to avoid issues with `false != 'false'`, ... + writer.AddKnockoutDataBind("value", value.GetJsExpression(this)); + } base.AddAttributesToRender(writer, context); } diff --git a/src/Framework/Framework/Controls/TableUtils.cs b/src/Framework/Framework/Controls/TableUtils.cs index 5d5642f9fd..4e617154a1 100644 --- a/src/Framework/Framework/Controls/TableUtils.cs +++ b/src/Framework/Framework/Controls/TableUtils.cs @@ -1,4 +1,5 @@ using DotVVM.Framework.Binding; +using DotVVM.Framework.Binding.Expressions; using System; using System.Collections.Generic; using System.Linq; @@ -13,13 +14,12 @@ public class TableUtils /// Hides entire column in the table. Should be applied to the header. /// Does not check for correct usage, may give JS errors, check out the console if it does not work. /// - [MarkupOptions(AllowHardCodedValue = false)] + [MarkupOptions] public static readonly DotvvmProperty ColumnVisibleProperty = DelegateActionProperty.Register("ColumnVisible", (writer, context, property, control) => { - var binding = control.GetValueBinding(property); - if (binding != null) - writer.AddKnockoutDataBind("dotvvm-table-columnvisible", binding.GetKnockoutBindingExpression(control)); + var value = control.GetValueOrBinding(property); + writer.AddKnockoutDataBind("dotvvm-table-columnvisible", value.GetJsExpression(control)); }, defaultValue: true); } diff --git a/src/Framework/Framework/ResourceManagement/InlineScriptResource.cs b/src/Framework/Framework/ResourceManagement/InlineScriptResource.cs index b11fac4ef9..18673bea4c 100644 --- a/src/Framework/Framework/ResourceManagement/InlineScriptResource.cs +++ b/src/Framework/Framework/ResourceManagement/InlineScriptResource.cs @@ -20,7 +20,14 @@ public InlineScriptResource(ResourceRenderPosition renderPosition = ResourceRend } [JsonConstructor] - public InlineScriptResource(string code, ResourceRenderPosition renderPosition = ResourceRenderPosition.Body, bool defer = false) : base(renderPosition) + public InlineScriptResource(string code, ResourceRenderPosition renderPosition = ResourceRenderPosition.Body, bool defer = false, bool module = false) : base(renderPosition) + { + this.Code = code; + this.Module = module; + this.Defer = defer || module; + } + + public InlineScriptResource(string code, ResourceRenderPosition renderPosition, bool defer) : base(renderPosition) { this.Code = code; this.Defer = defer; @@ -57,6 +64,8 @@ public string Code /// If the script should be executed after the page loads (using the `defer` attribute). public bool Defer { get; } + /// If the script should be rendered as type='module'. Module=true implies Defer=true + public bool Module { get; } public bool ShouldSerializeCode() => code?.IsValueCreated == true; static bool InlineScriptContentGuard(string? code) @@ -72,21 +81,27 @@ static bool InlineScriptContentGuard(string? code) /// public override void Render(IHtmlWriter writer, IDotvvmRequestContext context, string resourceName) { - RenderScript(writer, Code, Defer); + RenderScript(writer, Code, Defer, Module); } /// Renders a <script> element with the content. - public static void RenderScript(IHtmlWriter writer, string code, bool defer) + public static void RenderScript(IHtmlWriter writer, string code, bool defer) => + RenderScript(writer, code, defer, module: false); + /// Renders a <script> element with the content. + public static void RenderScript(IHtmlWriter writer, string code, bool defer, bool module) { if (string.IsNullOrWhiteSpace(code)) return; var needBase64Hack = - defer || // browsers don't support `defer` attribute on inline script. We can overcome this limitation by using base64 data URI + (defer && !module) || // browsers don't support `defer` attribute on inline script. We can overcome this limitation by using base64 data URI InlineScriptContentGuard(code); // or, when the script is XSS-unsafe, we can do the same - if (defer) + if (defer && !module) writer.AddAttribute("defer", null); + if (module) + writer.AddAttribute("type", "module"); + if (needBase64Hack) RenderDataUriString(writer, code); else diff --git a/src/Framework/Framework/ResourceManagement/ResourceRepositoryExtensions.cs b/src/Framework/Framework/ResourceManagement/ResourceRepositoryExtensions.cs index 32d73ca84d..0abb6a4ff9 100644 --- a/src/Framework/Framework/ResourceManagement/ResourceRepositoryExtensions.cs +++ b/src/Framework/Framework/ResourceManagement/ResourceRepositoryExtensions.cs @@ -53,7 +53,9 @@ public static LinkResourceBase RegisterScript( string[]? dependencies = null, string? integrityHash = null) { - LinkResourceBase r = module ? new ScriptModuleResource(location, defer) : new ScriptResource(location, defer); + if (!defer && module) + throw new ArgumentException(" + + + + + + + + + +
+ + real body + +
+ + + + + + + + + + + + + diff --git a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html index 032e690caf..abb0be5986 100644 --- a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html +++ b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html @@ -5,7 +5,7 @@ - + @@ -39,6 +39,7 @@ "4n/9KyUdRKOKC0n3": {"type":"object","debugName":"object","properties":{}} } }"> - + diff --git a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html index 6c9ee52987..053bd6affb 100644 --- a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html +++ b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html @@ -5,7 +5,7 @@ - + @@ -60,6 +60,7 @@ "R+sz4Xh1JrRtZJ4S": {"type":"object","debugName":"ViewModulesServerSideTests.TestViewModel","properties":{"Collection":{"type":["String"]}}} } }"> - + diff --git a/src/Tests/DotVVM.Framework.Tests.csproj b/src/Tests/DotVVM.Framework.Tests.csproj index da7347b78f..a9e51aa0b8 100644 --- a/src/Tests/DotVVM.Framework.Tests.csproj +++ b/src/Tests/DotVVM.Framework.Tests.csproj @@ -59,7 +59,7 @@ - + diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/CompilationWarningsTests.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/CompilationWarningsTests.cs index a3c7c50f07..e75e4b1493 100644 --- a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/CompilationWarningsTests.cs +++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/CompilationWarningsTests.cs @@ -37,6 +37,32 @@ public void CompilationWarning_ValidationTargetPrimitiveType() Assert.AreEqual("BoolProp", warnings[0].tokens.Trim()); } + [TestMethod] + public void CompilationWarning_JsComponentNoModules() + { + var warnings = GetWarnings($$""" + @viewModel {{typeof(TestViewModel)}} + + """); + Assert.AreEqual(1, warnings.Length); + StringAssert.Contains(warnings[0].warning, "This view does not have any view modules registered"); + Assert.AreEqual("Test", warnings[0].tokens.Trim()); + } + + [TestMethod] + public void CompilationWarning_JsComponentFine() + { + XAssert.Empty(GetWarnings($$""" + @viewModel {{typeof(TestViewModel)}} + @js dotvvm.internal + + """)); + XAssert.Empty(GetWarnings($$""" + @viewModel {{typeof(TestViewModel)}} + + """)); + } + [DataTestMethod] [DataRow("TestViewModel2")] [DataRow("VMArray")] @@ -51,7 +77,7 @@ public void CompilationWarning_ValidationTargetPrimitiveType_Negative(string pro } [TestMethod] - public void DefaultViewCompiler_NonExistenPropertyWarning() + public void DefaultViewCompiler_NonExistentPropertyWarning() { var markup = $@" @viewModel System.Boolean @@ -72,7 +98,7 @@ @viewModel System.Boolean } [TestMethod] - public void DefaultViewCompiler_NonExistenPropertyWarning_PrefixedGroup() + public void DefaultViewCompiler_NonExistentPropertyWarning_PrefixedGroup() { var markup = $@" @viewModel System.Boolean @@ -90,6 +116,64 @@ @viewModel System.Boolean Assert.AreEqual("HTML attribute name 'IncludeInPage' should not contain uppercase letters. Did you intent to use a DotVVM property instead?", XAssert.Single(attribute2.AttributeNameNode.NodeWarnings)); } + [TestMethod] + public void DefaultViewCompiler_NonExistentPropertyWarning_InnerElement() + { + var markup = $@" +@viewModel bool + + empty + --- + + test + ---- + +"; + var repeater = ParseSource(markup) + .Content.SelectRecursively(c => c.Content) + .Single(c => c.Metadata.Type == typeof(Repeater)); + + var elementNode = (DothtmlElementNode)repeater.DothtmlNode; + var correctTemplateElement = elementNode.Content.OfType().Single(e => e.TagName == "EmptyDataTemplate"); + var mistakeTemplateElement = elementNode.Content.OfType().Single(e => e.TagName == "SepratrorTemplate"); + var mistakeTextBoxElement = elementNode.Content.OfType().Single(e => e.TagName == "TextBox"); + var lateTemplate = elementNode.Content.OfType().Single(e => e.TagName == "SeparatorTemplate"); + + XAssert.Empty(correctTemplateElement.TagNameNode.NodeWarnings); + Assert.AreEqual("HTML element name 'SepratrorTemplate' should not contain uppercase letters. Did you mean SeparatorTemplate, or another DotVVM property?", XAssert.Single(mistakeTemplateElement.TagNameNode.NodeWarnings)); + Assert.AreEqual("HTML element name 'TextBox' should not contain uppercase letters. Did you mean dot:CheckBox, dot:ListBox, dot:TextBox, or another DotVVM control?", XAssert.Single(mistakeTextBoxElement.TagNameNode.NodeWarnings)); + Assert.AreEqual("This element looks like an inner element property Repeater.SeparatorTemplate, but it isn't, because it is prefixed by other content ('')).", XAssert.Single(lateTemplate.TagNameNode.NodeWarnings)); + } + + [TestMethod] + public void DefaultViewCompiler_DisallowedContentControlType() + { + var markup = $@" +@viewModel System.Collections.Generic.IEnumerable + + empty + test + + +"; + var repeater = ParseSource(markup) + .Content.SelectRecursively(c => c.Content) + .Single(c => c.Metadata.Type == typeof(GridView)); + + var elementNode = (DothtmlElementNode)repeater.DothtmlNode; + var fine = elementNode.Content.OfType().Single(e => e.TagName == "GridViewTemplateColumn"); + var unallowedType = elementNode.Content.OfType().Single(e => e.TagName == "TextBox"); + var mistypedTemplate = elementNode.Content.OfType().Single(e => e.TagName == "EmtyDataTemplate"); + + XAssert.Empty(fine.TagNameNode.NodeWarnings); + XAssert.Empty(fine.TagNameNode.NodeErrors); + + Assert.AreEqual("Control type DotVVM.Framework.Controls.TextBox can't be used in a property of type DotVVM.Framework.Controls.GridViewColumn.", XAssert.Single(unallowedType.TagNameNode.NodeErrors)); + Assert.AreEqual("Control type DotVVM.Framework.Controls.HtmlGenericControl can't be used in a property of type DotVVM.Framework.Controls.GridViewColumn. Did you mean EmptyDataTemplate, or another DotVVM property?", XAssert.Single(mistypedTemplate.TagNameNode.NodeErrors)); + Assert.AreEqual("HTML element name 'EmtyDataTemplate' should not contain uppercase letters. Did you mean EmptyDataTemplate, or another DotVVM property?", XAssert.Single(mistypedTemplate.TagNameNode.NodeWarnings)); + } + + [TestMethod] public void DefaultViewCompiler_UnsupportedCallSite_ResourceBinding_Warning() { diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs index b24cf51373..dabdab4307 100644 --- a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs +++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs @@ -201,12 +201,39 @@ public void ResolvedTree_UnknownElement() Assert.AreEqual(typeof(HtmlGenericControl), control.Metadata.Type); Assert.AreEqual(1, control.ConstructorParameters.Length); Assert.AreEqual("dot:xxxButton", control.ConstructorParameters[0]); - Assert.IsTrue(control.DothtmlNode.HasNodeErrors); - Assert.IsTrue(control.DothtmlNode.NodeErrors.First().Contains("could not be resolved")); + var node = (control.DothtmlNode as DothtmlElementNode).TagNameNode; + Assert.AreEqual("The control could not be resolved! Did you mean dot:LinkButton, dot:Button, or other DotVVM control?", XAssert.Single(node.NodeErrors)); Assert.AreEqual(root, control.Parent); } + [TestMethod] + public void ResolvedTree_UnknownPrefix() + { + var root = ParseSource(@"@viewModel string +"); + + var control = root.Content.First(); + var node = (control.DothtmlNode as DothtmlElementNode).TagNameNode; + Assert.AreEqual("The control could not be resolved! Did you mean dot:Button, or other DotVVM control? Otherwise, make sure that the tagPrefix 'bp' is registered in DotvvmConfiguration.Markup.Controls collection!", XAssert.Single(node.NodeErrors)); + } + + + [TestMethod] + public void ResolvedTree_SimilarToMarkupControl() + { + var root = ParseSource(@"@viewModel string + + +"); + + var controls = root.Content.Where(c => !c.IsOnlyWhitespace()).ToArray(); + var node = (controls[0].DothtmlNode as DothtmlElementNode).TagNameNode; + Assert.AreEqual("The control could not be resolved! Did you mean cmc:ControlWithPropertyDirective, or other DotVVM control?", XAssert.Single(node.NodeErrors)); + node = (controls[1].DothtmlNode as DothtmlElementNode).TagNameNode; + Assert.AreEqual("The control could not be resolved! Did you mean cmc:ControlWithBaseType, or other DotVVM control?", XAssert.Single(node.NodeErrors)); + } + [TestMethod] public void ResolvedTree_ElementProperty() { diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTestsBase.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTestsBase.cs index 29c08ac877..f1998d7fb6 100644 --- a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTestsBase.cs +++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTestsBase.cs @@ -1,7 +1,11 @@ -using DotVVM.Framework.Compilation.ControlTree.Resolved; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Compilation.ControlTree.Resolved; using DotVVM.Framework.Configuration; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Hosting; using DotVVM.Framework.Testing; using DotVVM.Framework.Tests.Runtime.ControlTree.DefaultControlTreeResolver; +using Microsoft.Extensions.DependencyInjection; namespace DotVVM.Framework.Tests.Runtime.ControlTree { @@ -11,13 +15,43 @@ public abstract class DefaultControlTreeResolverTestsBase static DefaultControlTreeResolverTestsBase() { - configuration = DotvvmTestHelper.CreateConfiguration(); + var fakeMarkupFileLoader = new FakeMarkupFileLoader() { + MarkupFiles = { + ["ControlWithBaseType.dotcontrol"] = """ + @viewModel object + @baseType DotVVM.Framework.Tests.Runtime.ControlTree.DefaultControlTreeResolverTestsBase.TestMarkupControl1 + + {{value: Text}} + """, + ["ControlWithPropertyDirective.dotcontrol"] = """ + @viewModel object + @property string Text + + {{value: Text}} + """ + } + }; + configuration = DotvvmTestHelper.CreateConfiguration(s => { + s.AddSingleton(fakeMarkupFileLoader); + }); configuration.Markup.AddCodeControls("cc", typeof(ClassWithInnerElementProperty)); + configuration.Markup.AddMarkupControl("cmc", "ControlWithBaseType", "ControlWithBaseType.dotcontrol"); + configuration.Markup.AddMarkupControl("cmc", "ControlWithPropertyDirective", "ControlWithPropertyDirective.dotcontrol"); configuration.Freeze(); } protected ResolvedTreeRoot ParseSource(string markup, string fileName = "default.dothtml", bool checkErrors = false) => DotvvmTestHelper.ParseResolvedTree(markup, fileName, configuration, checkErrors); + public class TestMarkupControl1: DotvvmMarkupControl + { + public string Text + { + get { return (string)GetValue(TextProperty); } + set { SetValue(TextProperty, value); } + } + public static readonly DotvvmProperty TextProperty = + DotvvmProperty.Register(nameof(Text)); + } } } diff --git a/src/Tests/Runtime/ViewCompilationServiceTests.cs b/src/Tests/Runtime/ViewCompilationServiceTests.cs index 5e7a26b5b7..d07557fef0 100644 --- a/src/Tests/Runtime/ViewCompilationServiceTests.cs +++ b/src/Tests/Runtime/ViewCompilationServiceTests.cs @@ -88,7 +88,7 @@ public void ErrorInMarkup() service.BuildView(route, out _); Assert.AreEqual(CompilationState.CompilationFailed, route.Status); Assert.IsNotNull(route.Exception); - Assert.AreEqual("The control could not be resolved! Make sure that the tagPrefix is registered in DotvvmConfiguration.Markup.Controls collection!", route.Exception); + Assert.AreEqual("The control could not be resolved!", route.Exception); } [TestMethod] diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json index 1bb86526e1..4c929c467c 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json @@ -17,6 +17,7 @@ "apiInit_testApi": { "Code": "dotvvm.api._testApi=new DotVVM.Framework.Tests.Binding.TestApiClient(\"http://server/api\");", "Defer": true, + "Module": true, "Dependencies": [ "dotvvm", "apiClient_testApi" diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index 7547369705..2902ab93e0 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -25,6 +25,7 @@ "dotvvm": { "Code": "", "Defer": true, + "Module": false, "Dependencies": [ "dotvvm.internal" ], @@ -33,7 +34,7 @@ }, "DotVVM.Framework.ResourceManagement.ScriptModuleResource": { "dotvvm.internal": { - "Defer": true, + "Defer": false, "Location": { "Assembly": "DotVVM.Framework, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da", "Name": "DotVVM.Framework.obj.javascript.root_only.dotvvm-root.js", @@ -48,7 +49,7 @@ "RenderPosition": "Anywhere" }, "dotvvm.internal-spa": { - "Defer": true, + "Defer": false, "Location": { "Assembly": "DotVVM.Framework, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da", "Name": "DotVVM.Framework.obj.javascript.root_spa.dotvvm-root.js", @@ -860,8 +861,7 @@ "dataContextManipulation": { "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute" }, - "defaultValue": true, - "onlyBindings": true + "defaultValue": true }, "Width": { "type": "System.String", @@ -1090,7 +1090,8 @@ "DotVVM.Framework.Controls.JsComponent": { "Global": { "type": "System.Boolean", - "defaultValue": false + "defaultValue": false, + "onlyHardcoded": true }, "Html:ID": { "type": "System.String", @@ -1103,11 +1104,13 @@ }, "Name": { "type": "System.String", - "required": true + "required": true, + "onlyHardcoded": true }, "WrapperTagName": { "type": "System.String", - "defaultValue": "div" + "defaultValue": "div", + "onlyHardcoded": true } }, "DotVVM.Framework.Controls.Label": { @@ -1491,7 +1494,6 @@ "ColumnVisible": { "type": "System.Boolean", "defaultValue": true, - "onlyBindings": true, "isActive": true } }, diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json index 11eac35a4b..24b0e024c6 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json @@ -9,10 +9,12 @@ "r6": { "Code": "alert(1)", "Defer": false, + "Module": false, "RenderPosition": "Body" }, "r7": { "Defer": false, + "Module": false, "RenderPosition": "Body" } }, diff --git a/src/Tools/AppStartupInstabilityTester.py b/src/Tools/AppStartupInstabilityTester.py new file mode 100644 index 0000000000..80e6f77f32 --- /dev/null +++ b/src/Tools/AppStartupInstabilityTester.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +import subprocess, requests, os, time, argparse + +parser = argparse.ArgumentParser(description="Repeatedly starts the server and every time checks if some pages are working, use to find startup-time race condition bugs") +parser.add_argument("--port", type=int, default=16017, help="Port to run the server on") +parser.add_argument("--working-directory", type=str, default=".", help="Working directory to run the server in") +parser.add_argument("--server-path", type=str, default="bin/Debug/net8.0/DotVVM.Samples.BasicSamples.AspNetCoreLatest", help="Path to the server executable") +parser.add_argument("--environment", type=str, default="Development", help="Asp.Net Core environment (Development, Production)") +args = parser.parse_args() + +port = args.port + +def server_start() -> subprocess.Popen: + """Starts the server and returns the process object""" + server = subprocess.Popen([ + args.server_path, "--environment", args.environment, "--urls", f"http://localhost:{port}"], + cwd=args.working_directory, + ) + return server + +def req(path): + try: + response = requests.get(f"http://localhost:{port}{path}") + return response.status_code + except requests.exceptions.ConnectionError: + return None + +iteration = 0 +while True: + iteration += 1 + print(f"Starting iteration {iteration}") + server = server_start() + time.sleep(0.1) + while req("/") is None: + time.sleep(0.1) + + probes = [ + req("/"), + req("/FeatureSamples/LambdaExpressions/StaticCommands"), + req("/FeatureSamples/LambdaExpressions/ClientSideFiltering"), + req("/FeatureSamples/LambdaExpressions/LambdaExpressions") + ] + if set(probes) != {200}: + print(f"Iteration {iteration} failed: {probes}") + time.sleep(100000000) + + server.terminate() + server.wait() diff --git a/src/Tracing/ApplicationInsights.AspNetCore/OperationNameTelemetryInitializer.cs b/src/Tracing/ApplicationInsights.AspNetCore/OperationNameTelemetryInitializer.cs new file mode 100644 index 0000000000..bc39bc5823 --- /dev/null +++ b/src/Tracing/ApplicationInsights.AspNetCore/OperationNameTelemetryInitializer.cs @@ -0,0 +1,32 @@ +using DotVVM.Framework.Hosting; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.ApplicationInsights.Channel; +using Microsoft.AspNetCore.Http; + +namespace DotVVM.Tracing.ApplicationInsights.AspNetCore; + +public class OperationNameTelemetryInitializer : ITelemetryInitializer +{ + private readonly IHttpContextAccessor _accessor; + + public OperationNameTelemetryInitializer(IHttpContextAccessor accessor) + { + _accessor = accessor; + } + + public void Initialize(ITelemetry telemetry) + { + var context = _accessor.HttpContext; + var url = context?.GetDotvvmContext()?.Route?.Url; + if (url != null && telemetry is RequestTelemetry) + { + var method = context.Request.Method; + var operationName = $"{method} /{url}"; + + var requestTelemetry = telemetry as RequestTelemetry; + requestTelemetry.Name = operationName; + requestTelemetry.Context.Operation.Name = operationName; + } + } +} diff --git a/src/Tracing/ApplicationInsights.AspNetCore/TracingBuilderExtensions.cs b/src/Tracing/ApplicationInsights.AspNetCore/TracingBuilderExtensions.cs index bf370072cf..114564f26d 100644 --- a/src/Tracing/ApplicationInsights.AspNetCore/TracingBuilderExtensions.cs +++ b/src/Tracing/ApplicationInsights.AspNetCore/TracingBuilderExtensions.cs @@ -30,6 +30,7 @@ public static IDotvvmServiceCollection AddApplicationInsightsTracing(this IDotvv services.AddDotvvmApplicationInsights(); services.Services.AddApplicationInsightsTelemetryProcessor(); + services.Services.AddSingleton(); services.Services.TryAddSingleton(); services.Services.AddTransient, ApplicationInsightSetup>(); diff --git a/src/Tracing/ApplicationInsights.Owin/DotVVM.Tracing.ApplicationInsights.Owin.csproj b/src/Tracing/ApplicationInsights.Owin/DotVVM.Tracing.ApplicationInsights.Owin.csproj index 1769b858ca..aeb6701fb4 100644 --- a/src/Tracing/ApplicationInsights.Owin/DotVVM.Tracing.ApplicationInsights.Owin.csproj +++ b/src/Tracing/ApplicationInsights.Owin/DotVVM.Tracing.ApplicationInsights.Owin.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Tracing/ApplicationInsights.Owin/OperationNameTelemetryInitializer.cs b/src/Tracing/ApplicationInsights.Owin/OperationNameTelemetryInitializer.cs new file mode 100644 index 0000000000..d0cda14c33 --- /dev/null +++ b/src/Tracing/ApplicationInsights.Owin/OperationNameTelemetryInitializer.cs @@ -0,0 +1,25 @@ +using System.Web; +using DotVVM.Framework.Hosting; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.ApplicationInsights.Channel; + +namespace DotVVM.Tracing.ApplicationInsights.Owin; + +public class OperationNameTelemetryInitializer : ITelemetryInitializer +{ + public void Initialize(ITelemetry telemetry) + { + var context = HttpContext.Current; + var url = context?.GetOwinContext()?.GetDotvvmContext()?.Route?.Url; + if (url != null && telemetry is RequestTelemetry) + { + var method = context.Request.HttpMethod; + var operationName = $"{method} /{url}"; + + var requestTelemetry = telemetry as RequestTelemetry; + requestTelemetry.Name = operationName; + requestTelemetry.Context.Operation.Name = operationName; + } + } +} diff --git a/src/Tracing/ApplicationInsights.Owin/TracingBuilderExtensions.cs b/src/Tracing/ApplicationInsights.Owin/TracingBuilderExtensions.cs index 5941e8717e..582d2bde0e 100644 --- a/src/Tracing/ApplicationInsights.Owin/TracingBuilderExtensions.cs +++ b/src/Tracing/ApplicationInsights.Owin/TracingBuilderExtensions.cs @@ -18,6 +18,7 @@ public static class TracingBuilderExtensions public static IDotvvmServiceCollection AddApplicationInsightsTracing(this IDotvvmServiceCollection services) { TelemetryConfiguration.Active.TelemetryProcessorChainBuilder.Use(next => new RequestTelemetryFilter(next)).Build(); + TelemetryConfiguration.Active.TelemetryInitializers.Add(new OperationNameTelemetryInitializer()); services.Services.TryAddSingleton(); services.AddDotvvmApplicationInsights();