From 07ad81fe6e6ede6a10224226d409cc71841e2133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 13 Jan 2024 19:48:12 +0100 Subject: [PATCH 01/13] JS translation: Dictionary.GetValueOrDefault --- .../Resolved/DirectiveCompilationService.cs | 16 +++-------- .../JavascriptTranslatableMethodCollection.cs | 14 ++++++++++ .../Scripts/translations/dictionaryHelper.ts | 8 ++++-- .../Framework/Utils/ReflectionUtils.cs | 27 +++++++++++++------ .../Binding/JavascriptCompilationTests.cs | 12 +++++++++ 5 files changed, 55 insertions(+), 22 deletions(-) diff --git a/src/Framework/Framework/Compilation/ControlTree/Resolved/DirectiveCompilationService.cs b/src/Framework/Framework/Compilation/ControlTree/Resolved/DirectiveCompilationService.cs index b602c75505..3bc17d053c 100644 --- a/src/Framework/Framework/Compilation/ControlTree/Resolved/DirectiveCompilationService.cs +++ b/src/Framework/Framework/Compilation/ControlTree/Resolved/DirectiveCompilationService.cs @@ -6,6 +6,7 @@ using DotVVM.Framework.Compilation.Binding; using System.Collections.Immutable; using DotVVM.Framework.Configuration; +using DotVVM.Framework.Utils; namespace DotVVM.Framework.Compilation.ControlTree.Resolved { @@ -59,7 +60,7 @@ public DirectiveCompilationService(CompiledAssemblyCache compiledAssemblyCache, public object? ResolvePropertyInitializer(DothtmlDirectiveNode directive, Type propertyType, BindingParserNode? initializer, ImmutableList imports) { - if (initializer == null) { return CreateDefaultValue(propertyType); } + if (initializer == null) { return ReflectionUtils.GetDefaultValue(propertyType); } var registry = RegisterImports(TypeRegistry.DirectivesDefault(compiledAssemblyCache), imports); @@ -75,25 +76,16 @@ public DirectiveCompilationService(CompiledAssemblyCache compiledAssemblyCache, var lambda = Expression.Lambda>(Expression.Block(Expression.Convert(TypeConversion.EnsureImplicitConversion(initializerExpression, propertyType), typeof(object)))); var lambdaDelegate = lambda.Compile(true); - return lambdaDelegate.Invoke() ?? CreateDefaultValue(propertyType); + return lambdaDelegate.Invoke() ?? ReflectionUtils.GetDefaultValue(propertyType); } catch (Exception ex) { directive.AddError("Could not initialize property value."); directive.AddError(ex.Message); - return CreateDefaultValue(propertyType); + return ReflectionUtils.GetDefaultValue(propertyType); } } - private object? CreateDefaultValue(Type? type) - { - if (type != null && type.IsValueType) - { - return Activator.CreateInstance(type); - } - return null; - } - private Expression? CompileDirectiveExpression(DothtmlDirectiveNode directive, BindingParserNode expressionSyntax, ImmutableList imports) { TypeRegistry registry; diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index 96f9f1e2a2..d53d1636ce 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -681,6 +681,20 @@ private void AddDefaultDictionaryTranslations() AddMethodTranslator(() => default(IReadOnlyDictionary)!.ContainsKey(null!), containsKey); AddMethodTranslator(() => default(IDictionary)!.Remove(null!), new GenericMethodCompiler(args => new JsIdentifierExpression("dotvvm").Member("translations").Member("dictionary").Member("remove").Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance), args[1]))); + + var getValueOrDefault = new GenericMethodCompiler((JsExpression[] args, MethodInfo method) => { + var defaultValue = + args.Length > 3 ? args[3] : + new JsLiteral(ReflectionUtils.GetDefaultValue(method.GetGenericArguments().Last())); + return new JsIdentifierExpression("dotvvm").Member("translations").Member("dictionary").Member("getItem").Invoke(args[1], args[2], defaultValue); + }); +#if DotNetCore + AddMethodTranslator(() => default(IReadOnlyDictionary)!.GetValueOrDefault(null!), getValueOrDefault); + AddMethodTranslator(() => default(IReadOnlyDictionary)!.GetValueOrDefault(null!, null), getValueOrDefault); +#endif + AddMethodTranslator(() => default(IImmutableDictionary)!.GetValueOrDefault(null!), getValueOrDefault); + AddMethodTranslator(() => default(IImmutableDictionary)!.GetValueOrDefault(null!, null), getValueOrDefault); + AddMethodTranslator(() => FunctionalExtensions.GetValueOrDefault(default(IReadOnlyDictionary)!, null!, null!, false), getValueOrDefault); } private bool IsDictionary(Type type) => diff --git a/src/Framework/Framework/Resources/Scripts/translations/dictionaryHelper.ts b/src/Framework/Framework/Resources/Scripts/translations/dictionaryHelper.ts index bc06aafd58..d19e7671b7 100644 --- a/src/Framework/Framework/Resources/Scripts/translations/dictionaryHelper.ts +++ b/src/Framework/Framework/Resources/Scripts/translations/dictionaryHelper.ts @@ -8,10 +8,14 @@ export function containsKey(dictionary: Dictionary, iden return getKeyValueIndex(dictionary, identifier) !== null; } -export function getItem(dictionary: Dictionary, identifier: Key): Value { +export function getItem(dictionary: Dictionary, identifier: Key, defaultValue?: Value): Value { const index = getKeyValueIndex(dictionary, identifier); if (index === null) { - throw Error("Provided key \"" + identifier + "\" is not present in the dictionary!"); + if (defaultValue !== undefined) { + return defaultValue; + } else { + throw Error("Provided key \"" + identifier + "\" is not present in the dictionary!"); + } } return ko.unwrap(ko.unwrap(dictionary[index]).Value); diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index bf5e8ea0ef..847090e336 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -27,6 +27,7 @@ using DotVVM.Framework.Routing; using DotVVM.Framework.ViewModel; using System.Diagnostics; +using System.Runtime.CompilerServices; namespace DotVVM.Framework.Utils { @@ -135,14 +136,7 @@ public static bool IsAssignableToGenericType(this Type givenType, Type genericTy // handle null values if (value == null) { - if (type == typeof(bool)) - return BoxingUtils.False; - else if (type == typeof(int)) - return BoxingUtils.Zero; - else if (type.IsValueType) - return Activator.CreateInstance(type); - else - return null; + return GetDefaultValue(type); } if (type.IsInstanceOfType(value)) return value; @@ -460,6 +454,23 @@ public static Type MakeNullableType(this Type type) return type.IsValueType && Nullable.GetUnderlyingType(type) == null && type != typeof(void) ? typeof(Nullable<>).MakeGenericType(type) : type; } + /// Returns the equivalent of default(T) in C#, null for reference and Nullable<T>, zeroed object for structs. + public static object? GetDefaultValue(Type type) + { + if (!type.IsValueType) + return null; + if (type.IsNullable()) + return null; + + if (type == typeof(bool)) + return BoxingUtils.False; + else if (type == typeof(int)) + return BoxingUtils.Zero; + // see https://github.com/dotnet/runtime/issues/90697 + // notably we can't use Activator.CreateInstance, because C# now allows default constructors in structs + return FormatterServices.GetUninitializedObject(type); + } + public static Type UnwrapTaskType(this Type type) { if (type.IsGenericType && typeof(Task<>).IsAssignableFrom(type.GetGenericTypeDefinition())) diff --git a/src/Tests/Binding/JavascriptCompilationTests.cs b/src/Tests/Binding/JavascriptCompilationTests.cs index 1c323b8337..561e1f7921 100644 --- a/src/Tests/Binding/JavascriptCompilationTests.cs +++ b/src/Tests/Binding/JavascriptCompilationTests.cs @@ -538,6 +538,18 @@ public void JsTranslator_ReadOnlyDictionaryIndexer_Get() Assert.AreEqual("dotvvm.translations.dictionary.getItem(ReadOnlyDictionary(),1)", result); } + [DataTestMethod] + [DataRow("Dictionary")] + [DataRow("ReadOnlyDictionary")] + public void JsTranslator_Dictionary_GetValueOrDefault(string property) + { + var imports = new NamespaceImport[] { new("System.Collections.Generic"), new("DotVVM.Framework.Utils") }; + var result = CompileBinding($"{property}.GetValueOrDefault(1)", imports, typeof(TestViewModel5)); + Assert.AreEqual($"dotvvm.translations.dictionary.getItem({property}(),1,0)", result); + var result2 = CompileBinding($"{property}.GetValueOrDefault(1, 1024)", imports, typeof(TestViewModel5)); + Assert.AreEqual($"dotvvm.translations.dictionary.getItem({property}(),1,1024)", result2); + } + [TestMethod] public void JsTranslator_DictionaryIndexer_Set() { From cb8e6752905dbf0d24b15f12a2ff5bf54a979746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 14 Jan 2024 21:05:59 +0100 Subject: [PATCH 02/13] Fixed bug in referencing master page from embedded resource (#1754) * Fixed issue with master page defines as embedded resource * UI test added --- .../Hosting/EmbeddedMarkupFileLoader.cs | 2 +- .../Common/DotVVM.Samples.Common.csproj | 4 ++++ .../EmbeddedResourceMasterPageViewModel.cs | 16 ++++++++++++++++ ...eWithEmbeddedResourceMasterPageViewModel.cs | 16 ++++++++++++++++ .../EmbeddedResourceMasterPage.dotmaster | 18 ++++++++++++++++++ .../PageWithEmbeddedResourceMasterPage.dothtml | 9 +++++++++ .../Abstractions/SamplesRouteUrls.designer.cs | 1 + .../Feature/EmbeddedResourceControlsTests.cs | 10 ++++++++++ 8 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/EmbeddedResourceControls/EmbeddedResourceMasterPageViewModel.cs create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/EmbeddedResourceControls/PageWithEmbeddedResourceMasterPageViewModel.cs create mode 100644 src/Samples/Common/Views/FeatureSamples/EmbeddedResourceControls/EmbeddedResourceMasterPage.dotmaster create mode 100644 src/Samples/Common/Views/FeatureSamples/EmbeddedResourceControls/PageWithEmbeddedResourceMasterPage.dothtml diff --git a/src/Framework/Framework/Hosting/EmbeddedMarkupFileLoader.cs b/src/Framework/Framework/Hosting/EmbeddedMarkupFileLoader.cs index 2abaeb0017..c424c37c7a 100644 --- a/src/Framework/Framework/Hosting/EmbeddedMarkupFileLoader.cs +++ b/src/Framework/Framework/Hosting/EmbeddedMarkupFileLoader.cs @@ -50,7 +50,7 @@ public class EmbeddedMarkupFileLoader : IMarkupFileLoader //load the file using (Stream stream = assembly.GetManifestResourceStream(resourceName)!) using (StreamReader sr = new StreamReader(stream)) - return new MarkupFile(resourceName, resourceName, sr.ReadToEnd()); + return new MarkupFile(virtualPath, virtualPath, sr.ReadToEnd()); } /// diff --git a/src/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj index 1bad72d894..09852a5a42 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -20,6 +20,7 @@ + @@ -98,6 +99,8 @@ + + @@ -193,6 +196,7 @@ + diff --git a/src/Samples/Common/ViewModels/FeatureSamples/EmbeddedResourceControls/EmbeddedResourceMasterPageViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/EmbeddedResourceControls/EmbeddedResourceMasterPageViewModel.cs new file mode 100644 index 0000000000..f1f72fa895 --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/EmbeddedResourceControls/EmbeddedResourceMasterPageViewModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.ViewModel; +using DotVVM.Framework.Hosting; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.EmbeddedResourceControls +{ + public class EmbeddedResourceMasterPageViewModel : DotvvmViewModelBase + { + + } +} + diff --git a/src/Samples/Common/ViewModels/FeatureSamples/EmbeddedResourceControls/PageWithEmbeddedResourceMasterPageViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/EmbeddedResourceControls/PageWithEmbeddedResourceMasterPageViewModel.cs new file mode 100644 index 0000000000..aa13b42bc3 --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/EmbeddedResourceControls/PageWithEmbeddedResourceMasterPageViewModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.ViewModel; +using DotVVM.Framework.Hosting; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.EmbeddedResourceControls +{ + public class PageWithEmbeddedResourceMasterPageViewModel : EmbeddedResourceMasterPageViewModel + { + + } +} + diff --git a/src/Samples/Common/Views/FeatureSamples/EmbeddedResourceControls/EmbeddedResourceMasterPage.dotmaster b/src/Samples/Common/Views/FeatureSamples/EmbeddedResourceControls/EmbeddedResourceMasterPage.dotmaster new file mode 100644 index 0000000000..5479abf84f --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/EmbeddedResourceControls/EmbeddedResourceMasterPage.dotmaster @@ -0,0 +1,18 @@ +@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.EmbeddedResourceControls.EmbeddedResourceMasterPageViewModel, DotVVM.Samples.Common + + + + + + + + + +

Embedded resource master page

+ + + + + + + diff --git a/src/Samples/Common/Views/FeatureSamples/EmbeddedResourceControls/PageWithEmbeddedResourceMasterPage.dothtml b/src/Samples/Common/Views/FeatureSamples/EmbeddedResourceControls/PageWithEmbeddedResourceMasterPage.dothtml new file mode 100644 index 0000000000..b0d2c18b4c --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/EmbeddedResourceControls/PageWithEmbeddedResourceMasterPage.dothtml @@ -0,0 +1,9 @@ +@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.EmbeddedResourceControls.PageWithEmbeddedResourceMasterPageViewModel, DotVVM.Samples.Common +@masterPage embedded://DotVVM.Samples.Common/Views.FeatureSamples.EmbeddedResourceControls.EmbeddedResourceMasterPage.dotmaster + + + +

Success

+ +
+ diff --git a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs index 013a4cb75e..fe2388d721 100644 --- a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs +++ b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs @@ -248,6 +248,7 @@ public partial class SamplesRouteUrls public const string FeatureSamples_Directives_ImportDirectiveInvalid = "FeatureSamples/Directives/ImportDirectiveInvalid"; public const string FeatureSamples_Directives_ViewModelMissingAssembly = "FeatureSamples/Directives/ViewModelMissingAssembly"; public const string FeatureSamples_EmbeddedResourceControls_EmbeddedResourceControls = "FeatureSamples/EmbeddedResourceControls/EmbeddedResourceControls"; + public const string FeatureSamples_EmbeddedResourceControls_PageWithEmbeddedResourceMasterPage = "FeatureSamples/EmbeddedResourceControls/PageWithEmbeddedResourceMasterPage"; public const string FeatureSamples_Formatting_AutoResourceInclusion = "FeatureSamples/Formatting/AutoResourceInclusion"; public const string FeatureSamples_Formatting_Formatting = "FeatureSamples/Formatting/Formatting"; public const string FeatureSamples_Formatting_ToStringGlobalFunctionBug = "FeatureSamples/Formatting/ToStringGlobalFunctionBug"; diff --git a/src/Samples/Tests/Tests/Feature/EmbeddedResourceControlsTests.cs b/src/Samples/Tests/Tests/Feature/EmbeddedResourceControlsTests.cs index 68957c23dc..a7c07f53da 100644 --- a/src/Samples/Tests/Tests/Feature/EmbeddedResourceControlsTests.cs +++ b/src/Samples/Tests/Tests/Feature/EmbeddedResourceControlsTests.cs @@ -25,5 +25,15 @@ public void Feature_EmbeddedResourceControls_EmbeddedResourceControls() AssertUI.Attribute(browser.First("input[type=button]"), "value", "This is text"); }); } + + [Fact] + public void Feature_EmbeddedResourceControls_PageWithEmbeddedResourceMasterPage() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_EmbeddedResourceControls_PageWithEmbeddedResourceMasterPage); + + AssertUI.TextEquals(browser.Single("p"), "Success"); + }); + } } } From 484e8157143f065c31f190090c68cba6eb41b5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Tue, 16 Jan 2024 22:22:58 +0100 Subject: [PATCH 03/13] Fix tokenizer error when empty directive is used Fixes the problem reported in https://forum.dotvvm.com/t/nowrappertag-syntax/101/2 --- .../Dothtml/Tokenizer/DothtmlTokenizer.cs | 18 +++---- .../DothtmlTokenizerDirectivesTests.cs | 51 +++++++++++-------- .../ViewModelDirectiveTest.cs | 10 ++++ 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/src/Framework/Framework/Compilation/Parser/Dothtml/Tokenizer/DothtmlTokenizer.cs b/src/Framework/Framework/Compilation/Parser/Dothtml/Tokenizer/DothtmlTokenizer.cs index 59230978f0..1f44e55897 100644 --- a/src/Framework/Framework/Compilation/Parser/Dothtml/Tokenizer/DothtmlTokenizer.cs +++ b/src/Framework/Framework/Compilation/Parser/Dothtml/Tokenizer/DothtmlTokenizer.cs @@ -145,6 +145,14 @@ private void ReadDirective() } SkipWhitespace(false); + if (Peek() is '\r' or '\n' or NullChar) + { + // empty value + CreateToken(DothtmlTokenType.DirectiveValue); + SkipWhitespace(); + return; + } + // whitespace if (LastToken!.Type != DothtmlTokenType.WhiteSpace) { @@ -152,15 +160,7 @@ private void ReadDirective() } // directive value - if (Peek() == '\r' || Peek() == '\n' || Peek() == NullChar) - { - CreateToken(DothtmlTokenType.DirectiveValue, errorProvider: t => CreateTokenError(t, DothtmlTokenType.DirectiveStart, DothtmlTokenizerErrors.DirectiveValueExpected)); - SkipWhitespace(); - } - else - { - ReadTextUntilNewLine(DothtmlTokenType.DirectiveValue); - } + ReadTextUntilNewLine(DothtmlTokenType.DirectiveValue); } /// diff --git a/src/Tests/Parser/Dothtml/DothtmlTokenizerDirectivesTests.cs b/src/Tests/Parser/Dothtml/DothtmlTokenizerDirectivesTests.cs index 1f69bc0952..b4505b9304 100644 --- a/src/Tests/Parser/Dothtml/DothtmlTokenizerDirectivesTests.cs +++ b/src/Tests/Parser/Dothtml/DothtmlTokenizerDirectivesTests.cs @@ -95,19 +95,15 @@ public void DothtmlTokenizer_DirectiveParsing_Invalid_OnlyAtSymbol() Assert.AreEqual(0, tokenizer.Tokens[i].Length); Assert.AreEqual(DothtmlTokenType.DirectiveName, tokenizer.Tokens[i++].Type); - Assert.IsTrue(tokenizer.Tokens[i].HasError); - Assert.AreEqual(0, tokenizer.Tokens[i].Length); - Assert.AreEqual(DothtmlTokenType.WhiteSpace, tokenizer.Tokens[i++].Type); - - Assert.IsTrue(tokenizer.Tokens[i].HasError); + Assert.IsFalse(tokenizer.Tokens[i].HasError); Assert.AreEqual(0, tokenizer.Tokens[i].Length); Assert.AreEqual(DothtmlTokenType.DirectiveValue, tokenizer.Tokens[i++].Type); } [TestMethod] - public void DothtmlTokenizer_DirectiveParsing_Invalid_AtSymbol_DirectiveName() + public void DothtmlTokenizer_DirectiveParsing_Invalid_ValueWithoutSpace() { - var input = @"@viewmodel"; + var input = @"@viewmodel=something"; // parse var tokenizer = new DothtmlTokenizer(); @@ -124,7 +120,28 @@ public void DothtmlTokenizer_DirectiveParsing_Invalid_AtSymbol_DirectiveName() Assert.AreEqual(0, tokenizer.Tokens[i].Length); Assert.AreEqual(DothtmlTokenType.WhiteSpace, tokenizer.Tokens[i++].Type); - Assert.IsTrue(tokenizer.Tokens[i].HasError); + Assert.AreEqual("=something", tokenizer.Tokens[i].Text); + Assert.IsFalse(tokenizer.Tokens[i].HasError); + Assert.AreEqual(DothtmlTokenType.DirectiveValue, tokenizer.Tokens[i++].Type); + } + + [TestMethod] + public void DothtmlTokenizer_DirectiveParsing_Valid_EmptyDirective() + { + var input = @"@viewmodel"; + + // parse + var tokenizer = new DothtmlTokenizer(); + tokenizer.Tokenize(input); + CheckForErrors(tokenizer, input.Length); + + var i = 0; + Assert.AreEqual(DothtmlTokenType.DirectiveStart, tokenizer.Tokens[i++].Type); + + Assert.AreEqual("viewmodel", tokenizer.Tokens[i].Text); + Assert.AreEqual(DothtmlTokenType.DirectiveName, tokenizer.Tokens[i++].Type); + + Assert.IsFalse(tokenizer.Tokens[i].HasError); Assert.AreEqual(0, tokenizer.Tokens[i].Length); Assert.AreEqual(DothtmlTokenType.DirectiveValue, tokenizer.Tokens[i++].Type); } @@ -170,11 +187,7 @@ public void DothtmlTokenizer_DirectiveParsing_Invalid_AtSymbol_NewLine_Content() Assert.AreEqual(0, tokenizer.Tokens[i].Length); Assert.AreEqual(DothtmlTokenType.DirectiveName, tokenizer.Tokens[i++].Type); - Assert.IsTrue(tokenizer.Tokens[i].HasError); - Assert.AreEqual(0, tokenizer.Tokens[i].Length); - Assert.AreEqual(DothtmlTokenType.WhiteSpace, tokenizer.Tokens[i++].Type); - - Assert.IsTrue(tokenizer.Tokens[i].HasError); + Assert.IsFalse(tokenizer.Tokens[i].HasError); Assert.AreEqual(0, tokenizer.Tokens[i].Length); Assert.AreEqual(DothtmlTokenType.DirectiveValue, tokenizer.Tokens[i++].Type); @@ -184,7 +197,7 @@ public void DothtmlTokenizer_DirectiveParsing_Invalid_AtSymbol_NewLine_Content() } [TestMethod] - public void DothtmlTokenizer_DirectiveParsing_Invalid_AtSymbol_DirectiveName_NewLine_Content() + public void DothtmlTokenizer_DirectiveParsing_DirectiveName_EmptyValue_NewLine_Content() { var input = "@viewmodel\ntest"; @@ -199,11 +212,7 @@ public void DothtmlTokenizer_DirectiveParsing_Invalid_AtSymbol_DirectiveName_New Assert.AreEqual("viewmodel", tokenizer.Tokens[i].Text); Assert.AreEqual(DothtmlTokenType.DirectiveName, tokenizer.Tokens[i++].Type); - Assert.IsTrue(tokenizer.Tokens[i].HasError); - Assert.AreEqual(0, tokenizer.Tokens[i].Length); - Assert.AreEqual(DothtmlTokenType.WhiteSpace, tokenizer.Tokens[i++].Type); - - Assert.IsTrue(tokenizer.Tokens[i].HasError); + Assert.IsFalse(tokenizer.Tokens[i].HasError); Assert.AreEqual(0, tokenizer.Tokens[i].Length); Assert.AreEqual(DothtmlTokenType.DirectiveValue, tokenizer.Tokens[i++].Type); @@ -233,7 +242,7 @@ public void DothtmlTokenizer_DirectiveParsing_Invalid_AtSymbol_DirectiveName_Spa Assert.AreEqual(2, tokenizer.Tokens[i].Length); Assert.AreEqual(DothtmlTokenType.WhiteSpace, tokenizer.Tokens[i++].Type); - Assert.IsTrue(tokenizer.Tokens[i].HasError); + Assert.IsFalse(tokenizer.Tokens[i].HasError); Assert.AreEqual(0, tokenizer.Tokens[i].Length); Assert.AreEqual(DothtmlTokenType.DirectiveValue, tokenizer.Tokens[i++].Type); @@ -265,7 +274,7 @@ public void DothtmlTokenizer_DirectiveParsing_Invalid_AtSymbol_Space_NewLine_Con Assert.AreEqual(2, tokenizer.Tokens[i].Length); Assert.AreEqual(DothtmlTokenType.WhiteSpace, tokenizer.Tokens[i++].Type); - Assert.IsTrue(tokenizer.Tokens[i].HasError); + Assert.IsFalse(tokenizer.Tokens[i].HasError); Assert.AreEqual(0, tokenizer.Tokens[i].Length); Assert.AreEqual(DothtmlTokenType.DirectiveValue, tokenizer.Tokens[i++].Type); diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/ViewModelDirectiveTest.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/ViewModelDirectiveTest.cs index 78eefccd23..d5c21c8322 100644 --- a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/ViewModelDirectiveTest.cs +++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/ViewModelDirectiveTest.cs @@ -31,6 +31,16 @@ public void ResolvedTree_UnknownViewModelType() Assert.IsTrue(directiveNode.NodeErrors.First().Contains("Could not resolve type")); } + [TestMethod] + public void ResolvedTree_EmptyViewModelType() + { + var root = ParseSource(@"@viewModel +"); + + var directiveNode = ((DothtmlRootNode)root.DothtmlNode).Directives.First(); + Assert.IsTrue(directiveNode.HasNodeErrors); + } + [TestMethod] public void ResolvedTree_ViewModel_GenericType() { From ef634fa125209f045da07f65beba7674caf1c10b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Thu, 18 Jan 2024 10:57:06 +0100 Subject: [PATCH 04/13] Fix data context path in InvalidCommandInvocationException --- src/Framework/Framework/Runtime/Commands/EventValidator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Framework/Framework/Runtime/Commands/EventValidator.cs b/src/Framework/Framework/Runtime/Commands/EventValidator.cs index d0cbfe1ba7..81320d3761 100644 --- a/src/Framework/Framework/Runtime/Commands/EventValidator.cs +++ b/src/Framework/Framework/Runtime/Commands/EventValidator.cs @@ -154,7 +154,7 @@ private FindBindingResult FindCommandBinding(string[] path, string commandId, else if (candidateBindings.All(b => !b.Key.DataContextPathMatch)) { // nothing in the specified data context path - errorMessage = $"Invalid command invocation - Nothing was found inside DataContext '{path}'. Please check if ViewModel is populated."; + errorMessage = $"Invalid command invocation - Nothing was found inside DataContext '{string.Join("/", path)}'. Please check if ViewModel is populated."; } else { From 01de186ce6ab28afc83afe4a23da516c28be7b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 19 Jan 2024 14:31:44 +0100 Subject: [PATCH 05/13] Remove unicode from GridView_RowDecorators_AddChildren --- src/Tests/ControlTests/GridViewTests.cs | 4 ++-- .../GridViewTests.GridView_RowDecorators_AddChildren.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Tests/ControlTests/GridViewTests.cs b/src/Tests/ControlTests/GridViewTests.cs index 263fa71c4e..fd81ba3b9b 100644 --- a/src/Tests/ControlTests/GridViewTests.cs +++ b/src/Tests/ControlTests/GridViewTests.cs @@ -131,14 +131,14 @@ public async Task GridView_RowDecorators_AddChildren() - ˯˯˯ enabled customer ˯˯˯ + vvv enabled customer vvv - ˄˄˄ enabled customer ˄˄˄ + ^^^ enabled customer ^^^ diff --git a/src/Tests/ControlTests/testoutputs/GridViewTests.GridView_RowDecorators_AddChildren.html b/src/Tests/ControlTests/testoutputs/GridViewTests.GridView_RowDecorators_AddChildren.html index 5c0b03eb36..75e9ff09ae 100644 --- a/src/Tests/ControlTests/testoutputs/GridViewTests.GridView_RowDecorators_AddChildren.html +++ b/src/Tests/ControlTests/testoutputs/GridViewTests.GridView_RowDecorators_AddChildren.html @@ -16,7 +16,7 @@ - ˯˯˯ enabled customer ˯˯˯ + vvv enabled customer vvv @@ -29,7 +29,7 @@ - ˄˄˄ enabled customer ˄˄˄ + ^^^ enabled customer ^^^ From 2c81c2e079f6e6a193225b3ceaaf3c9b5819495d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Fri, 19 Jan 2024 14:33:46 +0100 Subject: [PATCH 06/13] Fixed CheckTestOutput package version --- src/Tests/DotVVM.Framework.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tests/DotVVM.Framework.Tests.csproj b/src/Tests/DotVVM.Framework.Tests.csproj index 4250b4ccd0..b37e5b2397 100644 --- a/src/Tests/DotVVM.Framework.Tests.csproj +++ b/src/Tests/DotVVM.Framework.Tests.csproj @@ -35,7 +35,7 @@ - + From e05f34fec166319893c4bda391359a12c2cf0e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Thu, 25 Jan 2024 10:04:09 +0100 Subject: [PATCH 07/13] Allow internal-only namespaces in `@import` directive --- .../Compilation/CompiledAssemblyCache.cs | 5 ++- .../ImportDirectiveTests.cs | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/Framework/Framework/Compilation/CompiledAssemblyCache.cs b/src/Framework/Framework/Compilation/CompiledAssemblyCache.cs index b8a990dce4..8aea8818ed 100644 --- a/src/Framework/Framework/Compilation/CompiledAssemblyCache.cs +++ b/src/Framework/Framework/Compilation/CompiledAssemblyCache.cs @@ -172,12 +172,13 @@ private HashSet GetAllNamespaces() foreach (var a in GetAllAssemblies()) { string? lastNs = null; // namespaces come in batches, usually, so no need to hash it everytime when a quick compare says it's the same as last time - foreach (var type in a.ExportedTypes) + foreach (var type in a.GetLoadableTypes()) { var ns = type.Namespace; if (ns is null || lastNs == ns) continue; - result.Add(ns); + result.Add(ns); + lastNs = ns; } } return result; diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/ImportDirectiveTests.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/ImportDirectiveTests.cs index acd28c81bf..d53853e6ac 100644 --- a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/ImportDirectiveTests.cs +++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/ImportDirectiveTests.cs @@ -5,6 +5,11 @@ using System; using System.Collections.Generic; +namespace DotVVM.Framework.Tests.Runtime.ControlTree.TestInternalOnlyNamespace +{ + class InternalClass { } +} + namespace DotVVM.Framework.Tests.Runtime.ControlTree { [TestClass] @@ -70,6 +75,46 @@ @viewModel object Assert.AreEqual(typeof(List).FullName, importDirective.Type.FullName); } + [TestMethod] + public void ResolvedTree_ImportDirective_NamespaceDoesNotExist() + { + var root = ParseSource(@" +@viewModel object +@import This.Namespace.Does.Not.Exist +"); + var importDirective = EnsureSingleResolvedImportDirective(root); + + Assert.IsTrue(importDirective.HasError); + Assert.AreEqual("This.Namespace.Does.Not.Exist is unknown type or namespace.", importDirective.DothtmlNode.NodeErrors.Single()); + } + [TestMethod] + public void ResolvedTree_ImportDirective_NamespaceDoesNotExist_Alias() + { + var root = ParseSource(@" +@viewModel object +@import testns = This.Namespace.Does.Not.Exist +"); + var importDirective = EnsureSingleResolvedImportDirective(root); + + Assert.IsTrue(importDirective.HasError); + Assert.AreEqual("This.Namespace.Does.Not.Exist is unknown type or namespace.", importDirective.DothtmlNode.NodeErrors.Single()); + } + + [TestMethod] + public void ResolvedTree_ImportDirective_InternalNamespace() + { + var root = ParseSource(@" +@viewModel object +@import DotVVM.Framework.Tests.Runtime.ControlTree.TestInternalOnlyNamespace +"); + var importDirective = EnsureSingleResolvedImportDirective(root); + + Assert.IsFalse(importDirective.HasError, message: importDirective.DothtmlNode.NodeErrors.SingleOrDefault()); + Assert.IsNull(importDirective.Type); + Assert.IsTrue(importDirective.IsNamespace); + } + + private static ResolvedImportDirective EnsureSingleResolvedImportDirective(ResolvedTreeRoot root) => root.Directives["import"] .Single() From 54676ae10a135b8dc2d33b497a573b17c7bb3675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Thu, 25 Jan 2024 15:53:58 +0100 Subject: [PATCH 08/13] binding compiler: fix invocation of generic method with default(T) parameter or params T[] --- .../Binding/MemberExpressionFactory.cs | 60 ++++++++++++++----- src/Tests/Binding/BindingCompilationTests.cs | 43 +++++++++++++ 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/src/Framework/Framework/Compilation/Binding/MemberExpressionFactory.cs b/src/Framework/Framework/Compilation/Binding/MemberExpressionFactory.cs index da6ac12ce7..149f658ccc 100644 --- a/src/Framework/Framework/Compilation/Binding/MemberExpressionFactory.cs +++ b/src/Framework/Framework/Compilation/Binding/MemberExpressionFactory.cs @@ -415,6 +415,28 @@ public MethodRecognitionResult(int automaticTypeArgCount, int castCount, Express public bool HasParamsAttribute { get; set; } } + private Expression GetDefaultValue(ParameterInfo parameter) + { + if (parameter.HasDefaultValue) + { + var value = parameter.DefaultValue; + if (value is null && parameter.ParameterType.IsValueType) + { + // null with struct type means `default(T)` + value = ReflectionUtils.GetDefaultValue(parameter.ParameterType); + } + return Expression.Constant(value, parameter.ParameterType); + } + else if (parameter.IsDefined(ParamArrayAttributeType)) + { + return Expression.NewArrayInit(parameter.ParameterType.GetElementType().NotNull()); + } + else + { + throw new Exception($"Internal error: parameter {parameter.Name} of method {parameter.Member.Name} does not have a default value."); + } + } + private MethodRecognitionResult? TryCallMethod(MethodInfo method, Type[]? typeArguments, Expression[] positionalArguments, IDictionary? namedArguments) { if (positionalArguments.Contains(null)) throw new ArgumentNullException("positionalArguments[]"); @@ -445,7 +467,7 @@ public MethodRecognitionResult(int automaticTypeArgCount, int castCount, Express if (typeArgs[genericArgumentPosition] == null) { // try to resolve from arguments - var argType = GetGenericParameterType(genericArguments[genericArgumentPosition], parameterTypes, args.Select(s => s.Type).ToArray()); + var argType = GetGenericParameterType(genericArguments[genericArgumentPosition], parameterTypes, args.Select(s => s?.Type).ToArray()); automaticTypeArgs++; if (argType != null) typeArgs[genericArgumentPosition] = argType; else return null; @@ -466,11 +488,15 @@ public MethodRecognitionResult(int automaticTypeArgCount, int castCount, Express } else if (typeArguments != null) return null; - // cast arguments + // cast arguments and fill defaults for (int i = 0; i < args.Length; i++) { + if (args[i] == null) + { + args[i] = GetDefaultValue(parameters[i]); + } Type elm; - if (args.Length == i + 1 && hasParamsArrayAttributes && !args[i].Type.IsArray) + if (args.Length == i + 1 && hasParamsArrayAttributes && !args[i]!.Type.IsArray) { elm = parameters[i].ParameterType.GetElementType().NotNull(); if (positionalArguments.Skip(i).Any(s => TypeConversion.ImplicitConversion(s, elm) is null)) @@ -482,7 +508,7 @@ public MethodRecognitionResult(int automaticTypeArgCount, int castCount, Express { elm = parameters[i].ParameterType; } - var casted = TypeConversion.ImplicitConversion(args[i], elm); + var casted = TypeConversion.ImplicitConversion(args[i]!, elm); if (casted == null) { return null; @@ -492,7 +518,7 @@ public MethodRecognitionResult(int automaticTypeArgCount, int castCount, Express castCount++; args[i] = casted; } - if (args.Length == i + 1 && hasParamsArrayAttributes && !args[i].Type.IsArray) + if (args.Length == i + 1 && hasParamsArrayAttributes && !args[i]!.Type.IsArray) { var converted = positionalArguments.Skip(i) .Select(a => TypeConversion.EnsureImplicitConversion(a, elm)) @@ -505,13 +531,13 @@ public MethodRecognitionResult(int automaticTypeArgCount, int castCount, Express automaticTypeArgCount: automaticTypeArgs, castCount: castCount, method: method, - arguments: args, + arguments: args!, paramsArrayCount: positionalArguments.Length - args.Length, hasParamsAttribute: hasParamsArrayAttributes, isExtension: false ); } - private static bool TryPrepareArguments(ParameterInfo[] parameters, Expression[] positionalArguments, IDictionary? namedArguments, [MaybeNullWhen(false)] out Expression[] arguments, out int castCount) + private static bool TryPrepareArguments(ParameterInfo[] parameters, Expression[] positionalArguments, IDictionary? namedArguments, [MaybeNullWhen(false)] out Expression?[] arguments, out int castCount) { castCount = 0; arguments = null; @@ -522,7 +548,7 @@ private static bool TryPrepareArguments(ParameterInfo[] parameters, Expression[] if (!hasParamsArrayAttribute && parameters.Length < positionalArguments.Length) return false; - arguments = new Expression[parameters.Length]; + arguments = new Expression?[parameters.Length]; var copyItemsCount = !hasParamsArrayAttribute ? positionalArguments.Length : parameters.Length; if (hasParamsArrayAttribute && parameters.Length > positionalArguments.Length) @@ -530,8 +556,9 @@ private static bool TryPrepareArguments(ParameterInfo[] parameters, Expression[] var parameter = parameters.Last(); var elementType = parameter.ParameterType.GetElementType().NotNull(); - // User specified no arguments for the `params` array, we need to create an empty array - arguments[arguments.Length - 1] = Expression.NewArrayInit(elementType); + // User specified no arguments for the `params` array => use default value + // created later by the GetDefaultValue, after we know the generic arguments + arguments[arguments.Length - 1] = null; // Last argument was just generated => do not copy addedArguments++; @@ -561,7 +588,7 @@ private static bool TryPrepareArguments(ParameterInfo[] parameters, Expression[] else if (parameters[i].HasDefaultValue) { castCount++; - arguments[i] = Expression.Constant(parameters[i].DefaultValue, parameters[i].ParameterType); + arguments[i] = null; } else if (parameters[i].IsDefined(ParamArrayAttributeType)) { @@ -577,29 +604,30 @@ private static bool TryPrepareArguments(ParameterInfo[] parameters, Expression[] return true; } - private Type? GetGenericParameterType(Type genericArg, Type[] searchedGenericTypes, Type[] expressionTypes) + private Type? GetGenericParameterType(Type genericArg, Type[] searchedGenericTypes, Type?[] expressionTypes) { for (var i = 0; i < searchedGenericTypes.Length; i++) { if (expressionTypes.Length <= i) return null; + var expression = expressionTypes[i]; + if (expression == null) continue; var sgt = searchedGenericTypes[i]; if (sgt == genericArg) { - return expressionTypes[i]; + return expression; } if (sgt.IsArray) { var elementType = sgt.GetElementType(); - var expressionElementType = expressionTypes[i].GetElementType(); + var expressionElementType = expression.GetElementType(); if (elementType == genericArg) return expressionElementType; else - return GetGenericParameterType(genericArg, searchedGenericTypes[i].GetGenericArguments(), expressionTypes[i].GetGenericArguments()); + return GetGenericParameterType(genericArg, searchedGenericTypes[i].GetGenericArguments(), expression.GetGenericArguments()); } else if (sgt.IsGenericType) { Type[]? genericArguments = null; - var expression = expressionTypes[i]; if (expression.IsArray) { diff --git a/src/Tests/Binding/BindingCompilationTests.cs b/src/Tests/Binding/BindingCompilationTests.cs index 8ad41f60e9..81ff02e6f4 100755 --- a/src/Tests/Binding/BindingCompilationTests.cs +++ b/src/Tests/Binding/BindingCompilationTests.cs @@ -1055,6 +1055,39 @@ public void BindingCompiler_DelegateFromMethodGroup() Assert.AreEqual(42, result(42)); } + [DataTestMethod] + [DataRow("100", typeof(int))] + [DataRow("'aa'", null)] + [DataRow("NullableDateOnly", null)] + [DataRow("DateOnly", typeof(DateOnly))] + public void BindingCompiler_GenericMethod_DefaultArgument(string expression, Type resultType) + { + var result = ExecuteBinding($"_this.GenericDefault({expression})", new [] { new TestViewModel() }); + if (resultType == null) + { + Assert.IsNull(result); + } + else + { + Assert.AreEqual(resultType, result.GetType(), message: $"_this.GenericDefault({expression}) returned {result} of type {result?.GetType().FullName ?? "null"}"); + Assert.AreEqual(ReflectionUtils.GetDefaultValue(resultType), result); + } + } + + [TestMethod] + public void BindingCompiler_GenericMethod_ParamsEmpty() + { + var result = ExecuteBinding("_this.GenericParams()", new [] { new TestViewModel() }); + Assert.AreEqual((0, 0), result); + } + + [TestMethod] + public void BindingCompiler_GenericMethod_Params() + { + var result = ExecuteBinding("_this.GenericParams(10, 20, 30)", new [] { new TestViewModel() }); + Assert.AreEqual((10, 3), result); + } + [TestMethod] public void BindingCompiler_ComparisonOperators() { @@ -1356,6 +1389,16 @@ public async Task GetStringPropAsync() public string MethodWithOverloads(string i) => i; public string MethodWithOverloads(DateTime i) => i.ToString(); public int MethodWithOverloads(int a, int b) => a + b; + + public T GenericDefault(T something, T somethingElse = default) + { + return somethingElse; + } + + public (T, int) GenericParams(params T[] something) + { + return (something.FirstOrDefault(), something.Length); + } } From 1b44fcef0dbd08e47ab9857092a0e2fdfb540e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 26 Jan 2024 11:45:50 +0100 Subject: [PATCH 09/13] compilation page: fix controls with IDotvvmRequestContext injection --- .../DotvvmViewCompilationService.cs | 7 +- .../Testing/TestDotvvmRequestContext.cs | 10 ++ .../ViewCompilationFakeRequestContext.cs | 11 ++ .../Runtime/ViewCompilationServiceTests.cs | 141 ++++++++++++++++++ 4 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 src/Framework/Framework/Testing/ViewCompilationFakeRequestContext.cs create mode 100644 src/Tests/Runtime/ViewCompilationServiceTests.cs diff --git a/src/Framework/Framework/Compilation/DotvvmViewCompilationService.cs b/src/Framework/Framework/Compilation/DotvvmViewCompilationService.cs index a88597dbe0..c62eb4d96a 100644 --- a/src/Framework/Framework/Compilation/DotvvmViewCompilationService.cs +++ b/src/Framework/Framework/Compilation/DotvvmViewCompilationService.cs @@ -10,6 +10,8 @@ using DotVVM.Framework.Controls.Infrastructure; using Microsoft.Extensions.DependencyInjection; using DotVVM.Framework.Utils; +using DotVVM.Framework.Hosting; +using DotVVM.Framework.Testing; namespace DotVVM.Framework.Compilation { @@ -160,8 +162,9 @@ public bool BuildView(DotHtmlFileInfo file, bool forceRecompile, out DotHtmlFile var pageBuilder = controlBuilderFactory.GetControlBuilder(file.VirtualPath); - using var scopedServiceProvider = dotvvmConfiguration.ServiceProvider.CreateScope(); // dependencies that are configured as scoped cannot be resolved from root service provider - var compiledControl = pageBuilder.builder.Value.BuildControl(controlBuilderFactory, scopedServiceProvider.ServiceProvider); + using var scopedServices = dotvvmConfiguration.ServiceProvider.CreateScope(); // dependencies that are configured as scoped cannot be resolved from root service provider + scopedServices.ServiceProvider.GetRequiredService().Context = new ViewCompilationFakeRequestContext(scopedServices.ServiceProvider); + var compiledControl = pageBuilder.builder.Value.BuildControl(controlBuilderFactory, scopedServices.ServiceProvider); if (pageBuilder.descriptor.MasterPage is { FileName: {} masterPagePath }) { diff --git a/src/Framework/Framework/Testing/TestDotvvmRequestContext.cs b/src/Framework/Framework/Testing/TestDotvvmRequestContext.cs index 7cea6e316c..1ad27706e5 100644 --- a/src/Framework/Framework/Testing/TestDotvvmRequestContext.cs +++ b/src/Framework/Framework/Testing/TestDotvvmRequestContext.cs @@ -11,6 +11,7 @@ using DotVVM.Framework.ResourceManagement; using DotVVM.Framework.Routing; using DotVVM.Framework.Runtime.Tracing; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json.Linq; namespace DotVVM.Framework.Testing @@ -53,5 +54,14 @@ public IServiceProvider Services } public CustomResponsePropertiesManager CustomResponseProperties { get; } = new CustomResponsePropertiesManager(); + + + public TestDotvvmRequestContext() { } + public TestDotvvmRequestContext(IServiceProvider services) + { + this.Services = services; + this.Configuration = services.GetService(); + this.ResourceManager = services.GetService(); + } } } diff --git a/src/Framework/Framework/Testing/ViewCompilationFakeRequestContext.cs b/src/Framework/Framework/Testing/ViewCompilationFakeRequestContext.cs new file mode 100644 index 0000000000..05b815a23d --- /dev/null +++ b/src/Framework/Framework/Testing/ViewCompilationFakeRequestContext.cs @@ -0,0 +1,11 @@ +using System; + +namespace DotVVM.Framework.Testing +{ + class ViewCompilationFakeRequestContext : TestDotvvmRequestContext + { + public ViewCompilationFakeRequestContext(IServiceProvider services): base(services) + { + } + } +} diff --git a/src/Tests/Runtime/ViewCompilationServiceTests.cs b/src/Tests/Runtime/ViewCompilationServiceTests.cs new file mode 100644 index 0000000000..2f52f5b9d4 --- /dev/null +++ b/src/Tests/Runtime/ViewCompilationServiceTests.cs @@ -0,0 +1,141 @@ +using System; +using System.Linq; +using DotVVM.Framework.Compilation; +using DotVVM.Framework.Configuration; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Hosting; +using DotVVM.Framework.Testing; +using Microsoft.AspNetCore.Authorization.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotVVM.Framework.Tests.Runtime +{ + [TestClass] + public class ViewCompilationServiceTests + { + static readonly FakeMarkupFileLoader fileLoader; + static readonly DotvvmConfiguration config; + static readonly DotvvmViewCompilationService service; + static ViewCompilationServiceTests() + { + fileLoader = new FakeMarkupFileLoader(); + config = DotvvmTestHelper.CreateConfiguration(s => { + s.AddSingleton(fileLoader); + }); + config.Markup.AddCodeControls("test", exampleControl: typeof(ControlWithContextInjection)); + + config.RouteTable.Add("WithContextInjection", "WithContextInjection", "WithContextInjection.dothtml", null); + fileLoader.MarkupFiles["WithContextInjection.dothtml"] = """ + @viewModel object + + """; + + config.RouteTable.Add("WithUnresolvableDependency", "WithUnresolvableDependency", "WithUnresolvableDependency.dothtml", null); + fileLoader.MarkupFiles["WithUnresolvableDependency.dothtml"] = """ + @viewModel object + + """; + + + config.RouteTable.Add("WithError", "WithError", "WithError.dothtml", null); + fileLoader.MarkupFiles["WithError.dothtml"] = """ + @viewModel object + + """; + + config.RouteTable.Add("WithMasterPage", "WithMasterPage", "WithMasterPage.dothtml", null); + fileLoader.MarkupFiles["WithMasterPage.dothtml"] = """ + @viewModel object + @masterPage MasterPage.dothtml + test + """; + fileLoader.MarkupFiles["MasterPage.dothtml"] = """ + @viewModel object + + + + + + """; + + config.RouteTable.Add("NonCompilable", "NonCompilable", null, presenterFactory: _ => throw null); + + config.Freeze(); + + service = (DotvvmViewCompilationService)config.ServiceProvider.GetRequiredService(); + } + [TestMethod] + public void RequestContextInjection() + { + var route = service.GetRoutes().First(r => r.RouteName == "WithContextInjection"); + service.BuildView(route, out _); + Assert.IsNull(route.Exception); + Assert.AreEqual(CompilationState.CompletedSuccessfully, route.Status); + } + [TestMethod] + public void InjectionUnresolvableDependency() + { + var route = service.GetRoutes().First(r => r.RouteName == "WithUnresolvableDependency"); + service.BuildView(route, out _); + Assert.AreEqual(CompilationState.CompilationFailed, route.Status); + Assert.AreEqual("Unable to resolve service for type 'DotVVM.Framework.Tests.Runtime.ControlWithUnresolvableDependency+ThisServiceIsntRegistered' while attempting to activate 'DotVVM.Framework.Tests.Runtime.ControlWithUnresolvableDependency'.", route.Exception); + Assert.IsNotNull(route.Exception); + } + [TestMethod] + public void ErrorInMarkup() + { + var route = service.GetRoutes().First(r => r.RouteName == "WithError"); + 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); + } + + [TestMethod] + public void MasterPage() + { + var route = service.GetRoutes().First(r => r.RouteName == "WithMasterPage"); + service.BuildView(route, out var masterPage); + Assert.IsNull(route.Exception); + Assert.AreEqual(CompilationState.CompletedSuccessfully, route.Status); + Assert.IsNotNull(masterPage); + Assert.AreEqual(masterPage, service.GetMasterPages().FirstOrDefault(m => m.VirtualPath == "MasterPage.dothtml")); + Assert.AreEqual(CompilationState.None, masterPage.Status); + service.BuildView(masterPage, out _); + Assert.AreEqual(CompilationState.CompletedSuccessfully, masterPage.Status); + } + + [TestMethod] + public void NonCompilable() + { + var route = service.GetRoutes().First(r => r.RouteName == "NonCompilable"); + Assert.AreEqual(CompilationState.NonCompilable, route.Status); + service.BuildView(route, out _); + Assert.AreEqual(CompilationState.NonCompilable, route.Status); + Assert.IsNull(route.Exception); + } + } + + public class ControlWithContextInjection: DotvvmControl + { + public ControlWithContextInjection(IDotvvmRequestContext context) + { + if (context == null) + throw new ArgumentNullException(nameof(context)); + } + } + + public class ControlWithUnresolvableDependency: DotvvmControl + { + public ControlWithUnresolvableDependency(ThisServiceIsntRegistered dependency) + { + if (dependency == null) + throw new ArgumentNullException(nameof(dependency)); + } + + public class ThisServiceIsntRegistered + { + } + } +} From 4194d11e7aba5ef1465b0ec309d59b4c6ac4ca4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 27 Jan 2024 11:15:06 +0100 Subject: [PATCH 10/13] Disable Linq.Expression preferInterpretation for bindings The interpreter is broken, it causes problems in #1742. The bug is reported in dotnet, without any response yet: https://github.com/dotnet/runtime/issues/96385 --- .../Framework/Binding/IExpressionToDelegateCompiler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Framework/Framework/Binding/IExpressionToDelegateCompiler.cs b/src/Framework/Framework/Binding/IExpressionToDelegateCompiler.cs index f51f245d82..badab03e1e 100644 --- a/src/Framework/Framework/Binding/IExpressionToDelegateCompiler.cs +++ b/src/Framework/Framework/Binding/IExpressionToDelegateCompiler.cs @@ -20,7 +20,8 @@ public DefaultExpressionToDelegateCompiler(DotvvmConfiguration config) interpret = config.Debug; } public Delegate Compile(LambdaExpression expression) => - interpret ? expression.Compile(preferInterpretation: interpret) : + // the interpreter is broken: https://github.com/dotnet/runtime/issues/96385 + // interpret ? expression.Compile(preferInterpretation: interpret) : expression.Compile(); // TODO: use FastExpressionCompiler // we can't do that atm since it still has some bugs, when these are fixed we should use that for all bindings From a0a11f858907ddd449c55f2d243eedf50d4f375c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 31 Jan 2024 12:07:43 +0100 Subject: [PATCH 11/13] Warning when DotvvmView is used as control This issue occurs when a markup control is declared in a file with dothtml extension and without `@baseType` nor `@property` directives. It usually works, but causes issues with `@js` directive and we want to deprecate it in the future. --- .../ControlTree/ControlTreeResolverBase.cs | 4 +++ .../CompilationWarningsTests.cs | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs index bd1eae2fca..c435c02a7a 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs @@ -259,6 +259,10 @@ private IAbstractControl ProcessObjectElement(DothtmlElementNode element, IDataC 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!"); } + if (controlMetadata.VirtualPath is {} && controlMetadata.Type.IsAssignableTo(ResolvedTypeDescriptor.Create(typeof(DotvvmView)))) + { + element.TagNameNode.AddWarning($"The markup control <{element.FullTagName}> has a baseType DotvvmView. Please make sure that the control file has .dotcontrol file extension. This will work, but causes unexpected issues, for example @js directive will not work in this control."); + } var control = treeBuilder.BuildControl(controlMetadata, element, dataContext); control.ConstructorParameters = constructorParameters; diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/CompilationWarningsTests.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/CompilationWarningsTests.cs index cb512ab84e..a3c7c50f07 100644 --- a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/CompilationWarningsTests.cs +++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/CompilationWarningsTests.cs @@ -4,7 +4,11 @@ using DotVVM.Framework.Utils; using DotVVM.Framework.Controls; using DotVVM.Framework.Compilation.Parser.Dothtml.Parser; +using DotVVM.Framework.Testing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; +using DotVVM.Framework.Hosting; +using DotVVM.Framework.Controls.Infrastructure; namespace DotVVM.Framework.Tests.Runtime.ControlTree @@ -100,6 +104,30 @@ @viewModel System.DateTime Assert.AreEqual(1, literal.DothtmlNode.NodeWarnings.Count()); Assert.AreEqual("Evaluation of method \"ToBrowserLocalTime\" on server-side may yield unexpected results.", literal.DothtmlNode.NodeWarnings.First()); } + + [TestMethod] + public void DefaultViewCompiler_DotvvmView_Used_As_Control_Warning() + { + var files = new FakeMarkupFileLoader(); + files.MarkupFiles["TestControl.dothtml"] = """ + @viewModel object + test + """; + var config = DotvvmTestHelper.CreateConfiguration(s => { + s.AddSingleton(files); + }); + config.Markup.AddMarkupControl("cc", "TestControl", "TestControl.dothtml"); + + var markup = DotvvmTestHelper.ParseResolvedTree(""" + @viewModel object + + """, configuration: config); + var control = markup.Content.SelectRecursively(c => c.Content).Single(c => c.Properties.ContainsKey(Styles.TagProperty)); + Assert.AreEqual(typeof(DotvvmView), control.Metadata.Type); + var element = (DothtmlElementNode)control.DothtmlNode; + XAssert.Contains("The markup control has a baseType DotvvmView", element.TagNameNode.NodeWarnings.First()); + Assert.AreEqual(1, element.TagNameNode.NodeWarnings.Count()); + } } } From 39a13fd2e026024968c1c5daa968866256b04e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 31 Jan 2024 12:18:25 +0100 Subject: [PATCH 12/13] ViewCompilationServiceTests: remove non-deterministic assert --- src/Tests/Runtime/ViewCompilationServiceTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tests/Runtime/ViewCompilationServiceTests.cs b/src/Tests/Runtime/ViewCompilationServiceTests.cs index 2f52f5b9d4..5e7a26b5b7 100644 --- a/src/Tests/Runtime/ViewCompilationServiceTests.cs +++ b/src/Tests/Runtime/ViewCompilationServiceTests.cs @@ -5,7 +5,6 @@ using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; using DotVVM.Framework.Testing; -using Microsoft.AspNetCore.Authorization.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -101,7 +100,7 @@ public void MasterPage() Assert.AreEqual(CompilationState.CompletedSuccessfully, route.Status); Assert.IsNotNull(masterPage); Assert.AreEqual(masterPage, service.GetMasterPages().FirstOrDefault(m => m.VirtualPath == "MasterPage.dothtml")); - Assert.AreEqual(CompilationState.None, masterPage.Status); + // Assert.AreEqual(CompilationState.None, masterPage.Status); // it's not deterministic, because the master page is built asynchronously after the view asks for its viewmodel type service.BuildView(masterPage, out _); Assert.AreEqual(CompilationState.CompletedSuccessfully, masterPage.Status); } From 71ee2f4f4d839163a8455cfc953bbf85668bc774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Thu, 8 Feb 2024 12:14:01 +0100 Subject: [PATCH 13/13] AddTemplateDecorator: add Template suffix to properties --- .../Controls/AddTemplateDecorator.cs | 24 +++++++++---------- src/Tests/ControlTests/GridViewTests.cs | 8 +++---- ...alizationTests.SerializeDefaultConfig.json | 4 ++-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Framework/Framework/Controls/AddTemplateDecorator.cs b/src/Framework/Framework/Controls/AddTemplateDecorator.cs index a10948ee81..7dc7a1b8af 100644 --- a/src/Framework/Framework/Controls/AddTemplateDecorator.cs +++ b/src/Framework/Framework/Controls/AddTemplateDecorator.cs @@ -9,28 +9,28 @@ public class AddTemplateDecorator: Decorator { /// Template is rendered after the decorated control. [MarkupOptions(MappingMode = MappingMode.InnerElement)] - public ITemplate After + public ITemplate AfterTemplate { - get => (ITemplate)GetValue(AfterProperty)!; - set => SetValue(AfterProperty, value); + get => (ITemplate)GetValue(AfterTemplateProperty)!; + set => SetValue(AfterTemplateProperty, value); } - public static readonly DotvvmProperty AfterProperty = - DotvvmProperty.Register(nameof(After)); + public static readonly DotvvmProperty AfterTemplateProperty = + DotvvmProperty.Register(nameof(AfterTemplate)); /// Template is rendered before the decorated control. [MarkupOptions(MappingMode = MappingMode.InnerElement)] - public ITemplate Before + public ITemplate BeforeTemplate { - get => (ITemplate)GetValue(BeforeProperty)!; - set => SetValue(BeforeProperty, value); + get => (ITemplate)GetValue(BeforeTemplateProperty)!; + set => SetValue(BeforeTemplateProperty, value); } - public static readonly DotvvmProperty BeforeProperty = - DotvvmProperty.Register(nameof(Before)); + public static readonly DotvvmProperty BeforeTemplateProperty = + DotvvmProperty.Register(nameof(BeforeTemplate)); protected internal override void OnInit(IDotvvmRequestContext context) { - var after = this.After; - var before = this.Before; + var after = this.AfterTemplate; + var before = this.BeforeTemplate; if (after is {}) { diff --git a/src/Tests/ControlTests/GridViewTests.cs b/src/Tests/ControlTests/GridViewTests.cs index fd81ba3b9b..24d860e8d1 100644 --- a/src/Tests/ControlTests/GridViewTests.cs +++ b/src/Tests/ControlTests/GridViewTests.cs @@ -128,20 +128,20 @@ public async Task GridView_RowDecorators_AddChildren() - + vvv enabled customer vvv - - + + ^^^ enabled customer ^^^ - + diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index 999f2624a0..c595e0f70a 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -351,11 +351,11 @@ } }, "DotVVM.Framework.Controls.AddTemplateDecorator": { - "After": { + "AfterTemplate": { "type": "DotVVM.Framework.Controls.ITemplate, DotVVM.Framework", "mappingMode": "InnerElement" }, - "Before": { + "BeforeTemplate": { "type": "DotVVM.Framework.Controls.ITemplate, DotVVM.Framework", "mappingMode": "InnerElement" }