From 42e56bbf4515191289b459bf92a78900538e9df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Mon, 9 Jan 2023 21:19:02 +0100 Subject: [PATCH 01/31] Removed `ObsoleteAttribute` It was a little misused. This API is not obsoleted at all. Furthermore, it complicates stuff for VSExtension as we can not easily filter warnings. Whenever bindings with this API gets compiled, false-positive warnings were emitted. --- .../Framework/Binding/HelperNamespace/DateTimeExtensions.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs b/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs index e4b73be990..95ecdc356e 100644 --- a/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs +++ b/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs @@ -9,14 +9,12 @@ public static class DateTimeExtensions /// Converts the date (assuming it is in UTC) to browser's local time. /// CAUTION: When evaluated on the server, no conversion is made as we don't know the browser timezone. /// - [Obsolete("When evaluated on the server, no conversion is made as we don't know the browser timezone.")] public static DateTime ToBrowserLocalTime(this DateTime value) => value; /// /// Converts the date (assuming it is in UTC) to browser's local time. /// CAUTION: When evaluated on the server, no conversion is made as we don't know the browser timezone. /// - [Obsolete("When evaluated on the server, no conversion is made as we don't know the browser timezone.")] public static DateTime? ToBrowserLocalTime(this DateTime? value) => value; } From 0003e3f2097024edc17d81e87b73af1d5b472acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Mon, 9 Jan 2023 21:23:14 +0100 Subject: [PATCH 02/31] Added `UnsupportedCallSiteAttribute` - our own metadata for analysis This attribute makes it that every callsite can get analyzed. Server-side invocations can be analyzed by our analyzers. Client-side invocations can be analyzed by VSExtension / framework's compiler pipeline. --- src/Framework/Core/CodeAnalysis/CallSiteType.cs | 8 ++++++++ .../CodeAnalysis/UnsupportedCallSiteAttribute.cs | 15 +++++++++++++++ .../Binding/HelperNamespace/DateTimeExtensions.cs | 3 +++ 3 files changed, 26 insertions(+) create mode 100644 src/Framework/Core/CodeAnalysis/CallSiteType.cs create mode 100644 src/Framework/Core/CodeAnalysis/UnsupportedCallSiteAttribute.cs diff --git a/src/Framework/Core/CodeAnalysis/CallSiteType.cs b/src/Framework/Core/CodeAnalysis/CallSiteType.cs new file mode 100644 index 0000000000..99fa00aed6 --- /dev/null +++ b/src/Framework/Core/CodeAnalysis/CallSiteType.cs @@ -0,0 +1,8 @@ +namespace DotVVM.Core.CodeAnalysis +{ + public enum CallSiteType + { + ServerSide, + ClientSide + } +} diff --git a/src/Framework/Core/CodeAnalysis/UnsupportedCallSiteAttribute.cs b/src/Framework/Core/CodeAnalysis/UnsupportedCallSiteAttribute.cs new file mode 100644 index 0000000000..075ff80f6c --- /dev/null +++ b/src/Framework/Core/CodeAnalysis/UnsupportedCallSiteAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace DotVVM.Core.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false)] + public class UnsupportedCallSiteAttribute : Attribute + { + public readonly CallSiteType Type; + + public UnsupportedCallSiteAttribute(CallSiteType type) + { + Type = type; + } + } +} diff --git a/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs b/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs index 95ecdc356e..32e257f5d4 100644 --- a/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs +++ b/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs @@ -1,4 +1,5 @@ using System; +using DotVVM.Core.CodeAnalysis; namespace DotVVM.Framework.Binding.HelperNamespace { @@ -9,12 +10,14 @@ public static class DateTimeExtensions /// Converts the date (assuming it is in UTC) to browser's local time. /// CAUTION: When evaluated on the server, no conversion is made as we don't know the browser timezone. /// + [UnsupportedCallSite(CallSiteType.ServerSide)] public static DateTime ToBrowserLocalTime(this DateTime value) => value; /// /// Converts the date (assuming it is in UTC) to browser's local time. /// CAUTION: When evaluated on the server, no conversion is made as we don't know the browser timezone. /// + [UnsupportedCallSite(CallSiteType.ServerSide)] public static DateTime? ToBrowserLocalTime(this DateTime? value) => value; } From 8501fe342d9805914a8e10122675ef8aa950c1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Mon, 9 Jan 2023 22:08:33 +0100 Subject: [PATCH 03/31] Added `UnsupportedCallSiteAttributeAnalyzer` --- .../UnsupportedCallSiteAttributeTests.cs | 66 +++++++++++++++++++ .../Analyzers/AnalyzerReleases.Unshipped.md | 1 + .../UnsupportedCallSiteAttributeAnalyzer.cs | 60 +++++++++++++++++ src/Analyzers/Analyzers/DiagnosticCategory.cs | 1 + .../Analyzers/DotvvmDiagnosticIds.cs | 1 + src/Analyzers/Analyzers/Resources.Designer.cs | 27 ++++++++ src/Analyzers/Analyzers/Resources.resx | 9 +++ 7 files changed, 165 insertions(+) create mode 100644 src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs create mode 100644 src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs diff --git a/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs b/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs new file mode 100644 index 0000000000..9d989f3f15 --- /dev/null +++ b/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using DotVVM.Analyzers.ApiUsage; +using Xunit; +using VerifyCS = DotVVM.Analyzers.Tests.CSharpAnalyzerVerifier< + DotVVM.Analyzers.ApiUsage.UnsupportedCallSiteAttributeAnalyzer>; + +namespace DotVVM.Analyzers.Tests.ApiUsage +{ + public class UnsupportedCallSiteAttributeTests + { + [Fact] + public async Task Test_NoDiagnostics_InvokeMethod_WithoutUnsupportedCallSiteAttribute() + { + var test = @" + using System; + using System.IO; + + namespace ConsoleApplication1 + { + public class RegularClass + { + public void Target() + { + + } + + public void CallSite() + { + Target(); + } + } + }"; + + await VerifyCS.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task Test_Warning_InvokeMethod_WithUnsupportedCallSiteAttribute() + { + await VerifyCS.VerifyAnalyzerAsync(@" + using System; + using System.IO; + using DotVVM.Core.CodeAnalysis; + + namespace ConsoleApplication1 + { + public class RegularClass + { + [UnsupportedCallSite(CallSiteType.ServerSide)] + public void Target() + { + + } + + public void CallSite() + { + {|#0:Target()|}; + } + } + }", + + VerifyCS.Diagnostic(UnsupportedCallSiteAttributeAnalyzer.DoNotInvokeMethodFromUnsupportedCallSite) + .WithLocation(0).WithArguments("Target")); + } + } +} diff --git a/src/Analyzers/Analyzers/AnalyzerReleases.Unshipped.md b/src/Analyzers/Analyzers/AnalyzerReleases.Unshipped.md index 282282bc8e..82a3bfce06 100644 --- a/src/Analyzers/Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Analyzers/Analyzers/AnalyzerReleases.Unshipped.md @@ -6,3 +6,4 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- DotVVM02 | Serializability | Warning | ViewModelSerializabilityAnalyzer DotVVM03 | Serializability | Warning | ViewModelSerializabilityAnalyzer +DotVVM04 | ApiUsage | Warning | UnsupportedCallSiteAttributeAnalyzer diff --git a/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs b/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs new file mode 100644 index 0000000000..872dd8e070 --- /dev/null +++ b/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs @@ -0,0 +1,60 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace DotVVM.Analyzers.ApiUsage +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UnsupportedCallSiteAttributeAnalyzer : DiagnosticAnalyzer + { + private static readonly LocalizableResourceString unsupportedCallSiteTitle = new(nameof(Resources.ApiUsage_UnsupportedCallSite_Title), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableResourceString unsupportedCallSiteMessage = new(nameof(Resources.ApiUsage_UnsupportedCallSite_Message), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableResourceString unsupportedCallSiteDescription = new(nameof(Resources.ApiUsage_UnsupportedCallSite_Description), Resources.ResourceManager, typeof(Resources)); + private const string unsupportedCallSiteAttributeMetadataName = "DotVVM.Core.CodeAnalysis.UnsupportedCallSiteAttribute"; + private const int callSiteTypeServerUnderlyingValue = 0; + + public static DiagnosticDescriptor DoNotInvokeMethodFromUnsupportedCallSite = new( + DotvvmDiagnosticIds.DoNotInvokeMethodFromUnsupportedCallSiteRuleId, + unsupportedCallSiteTitle, + unsupportedCallSiteMessage, + DiagnosticCategory.ApiUsage, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + unsupportedCallSiteDescription); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(DoNotInvokeMethodFromUnsupportedCallSite); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterOperationAction(context => + { + var unsupportedCallSiteAttribute = context.Compilation.GetTypeByMetadataName(unsupportedCallSiteAttributeMetadataName); + if (unsupportedCallSiteAttribute is null) + return; + + if (context.Operation is IInvocationOperation invocation) + { + var method = invocation.TargetMethod; + var attribute = method.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, unsupportedCallSiteAttribute)); + if (attribute is null || !attribute.ConstructorArguments.Any()) + return; + + if (attribute.ConstructorArguments.First().Value is not int callSiteType || callSiteTypeServerUnderlyingValue != callSiteType) + return; + + context.ReportDiagnostic( + Diagnostic.Create( + DoNotInvokeMethodFromUnsupportedCallSite, + invocation.Syntax.GetLocation(), + invocation.TargetMethod.Name)); + } + }, OperationKind.Invocation); + } + } +} diff --git a/src/Analyzers/Analyzers/DiagnosticCategory.cs b/src/Analyzers/Analyzers/DiagnosticCategory.cs index 971e43d384..60e78aaa99 100644 --- a/src/Analyzers/Analyzers/DiagnosticCategory.cs +++ b/src/Analyzers/Analyzers/DiagnosticCategory.cs @@ -8,5 +8,6 @@ internal static class DiagnosticCategory { public const string Serializability = nameof(Serializability); public const string StaticCommands = nameof(StaticCommands); + public const string ApiUsage = nameof(ApiUsage); } } diff --git a/src/Analyzers/Analyzers/DotvvmDiagnosticIds.cs b/src/Analyzers/Analyzers/DotvvmDiagnosticIds.cs index 9dd12ce1d0..ad409d4bf2 100644 --- a/src/Analyzers/Analyzers/DotvvmDiagnosticIds.cs +++ b/src/Analyzers/Analyzers/DotvvmDiagnosticIds.cs @@ -11,5 +11,6 @@ public static class DotvvmDiagnosticIds public const string UseSerializablePropertiesInViewModelRuleId = "DotVVM02"; public const string DoNotUseFieldsInViewModelRuleId = "DotVVM03"; + public const string DoNotInvokeMethodFromUnsupportedCallSiteRuleId = "DotVVM04"; } } diff --git a/src/Analyzers/Analyzers/Resources.Designer.cs b/src/Analyzers/Analyzers/Resources.Designer.cs index bc677f9d79..c7b5bdeca0 100644 --- a/src/Analyzers/Analyzers/Resources.Designer.cs +++ b/src/Analyzers/Analyzers/Resources.Designer.cs @@ -60,6 +60,33 @@ internal class Resources { } } + /// + /// Looks up a localized string similar to Method that declares that it should not be called on server is only meant to be invoked on client. + /// + internal static string ApiUsage_UnsupportedCallSite_Description { + get { + return ResourceManager.GetString("ApiUsage_UnsupportedCallSite_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Method '{0}' invocation is not supported on server. + /// + internal static string ApiUsage_UnsupportedCallSite_Message { + get { + return ResourceManager.GetString("ApiUsage_UnsupportedCallSite_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unsupported call site. + /// + internal static string ApiUsage_UnsupportedCallSite_Title { + get { + return ResourceManager.GetString("ApiUsage_UnsupportedCallSite_Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to Fields are not supported in viewmodels. Use properties to save state of viewmodels instead.. /// diff --git a/src/Analyzers/Analyzers/Resources.resx b/src/Analyzers/Analyzers/Resources.resx index 73e3cb946e..e84fae2b93 100644 --- a/src/Analyzers/Analyzers/Resources.resx +++ b/src/Analyzers/Analyzers/Resources.resx @@ -117,6 +117,15 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Method that declares that it should not be called on server is only meant to be invoked on client + + + Method '{0}' invocation is not supported on server + + + Unsupported call site + Fields are not supported in viewmodels. Use properties to save state of viewmodels instead. From 971dc4c11eed9cc54fe18179e8ae30900ad59793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Thu, 12 Jan 2023 18:32:30 +0100 Subject: [PATCH 04/31] Added compilation warning when unsupported method called from resource binding --- .../UnsupportedCallSiteCheckingVisitor.cs | 43 +++++++++++++++++++ .../DotVVMServiceCollectionExtensions.cs | 1 + .../DefaultControlTreeResolverTests.cs | 15 +++++++ 3 files changed, 59 insertions(+) create mode 100644 src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs diff --git a/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs b/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs new file mode 100644 index 0000000000..eecee90ebb --- /dev/null +++ b/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using DotVVM.Core.CodeAnalysis; +using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Compilation.ControlTree.Resolved; + +namespace DotVVM.Framework.Compilation.ControlTree +{ + public class UnsupportedCallSiteCheckingVisitor : ResolvedControlTreeVisitor + { + class ExpressionInspectingVisitor : ExpressionVisitor + { + public event Action? InvalidCallSiteDetected; + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + var callSiteAttr = node.Method.CustomAttributes.FirstOrDefault(a => a.AttributeType == typeof(UnsupportedCallSiteAttribute)); + if (callSiteAttr is not null && callSiteAttr.ConstructorArguments.Any()) + { + if (callSiteAttr.ConstructorArguments.First().Value is int type && type == (int)CallSiteType.ServerSide) + InvalidCallSiteDetected?.Invoke(node.Method); + } + + return base.VisitMethodCall(node); + } + } + + public override void VisitBinding(ResolvedBinding binding) + { + base.VisitBinding(binding); + if (binding.Binding is not ResourceBindingExpression) + return; + + var expressionVisitor = new ExpressionInspectingVisitor(); + expressionVisitor.InvalidCallSiteDetected += method => + binding.DothtmlNode?.AddWarning($"Evaluation of method \"{method.Name}\" on server-side may yield unexpected results."); + + expressionVisitor.Visit(binding.Expression); + } + } +} diff --git a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs index fb1c4deff2..2b3b97d0ac 100644 --- a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs +++ b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs @@ -120,6 +120,7 @@ public static IServiceCollection RegisterDotVVMServices(IServiceCollection servi o.TreeVisitors.Add(() => ActivatorUtilities.CreateInstance(s)); o.TreeVisitors.Add(() => new UsedPropertiesFindingVisitor()); o.TreeVisitors.Add(() => new LifecycleRequirementsAssigningVisitor()); + o.TreeVisitors.Add(() => new UnsupportedCallSiteCheckingVisitor()); }); services.TryAddSingleton(); diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs index 1a02ba0a97..3a4a97d750 100644 --- a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs +++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs @@ -723,6 +723,21 @@ @viewModel System.Boolean Assert.AreEqual("HTML attribute name 'Visble' should not contain uppercase letters. Did you mean Visible, or another DotVVM property?", attribute3.AttributeNameNode.NodeWarnings.Single()); } + [TestMethod] + public void DefaultViewCompiler_UnsupportedCallSite_ResourceBinding_Warning() + { + var markup = @" +@viewModel System.DateTime +{{resource: _this.ToBrowserLocalTime()}} +"; + var literal = ParseSource(markup) + .Content.SelectRecursively(c => c.Content) + .Single(c => c.Metadata.Type == typeof(Literal)); + + 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_DifferentControlPrimaryName() { From 0a6d2967fafe3eca1d459fdeeb3737201fe92da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Thu, 12 Jan 2023 18:39:12 +0100 Subject: [PATCH 05/31] Fixed description --- src/Analyzers/Analyzers/Resources.Designer.cs | 2 +- src/Analyzers/Analyzers/Resources.resx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Analyzers/Analyzers/Resources.Designer.cs b/src/Analyzers/Analyzers/Resources.Designer.cs index c7b5bdeca0..1a70e1f52d 100644 --- a/src/Analyzers/Analyzers/Resources.Designer.cs +++ b/src/Analyzers/Analyzers/Resources.Designer.cs @@ -61,7 +61,7 @@ internal class Resources { } /// - /// Looks up a localized string similar to Method that declares that it should not be called on server is only meant to be invoked on client. + /// Looks up a localized string similar to Method that declares that it should not be called on server is only meant to be invoked on client.. /// internal static string ApiUsage_UnsupportedCallSite_Description { get { diff --git a/src/Analyzers/Analyzers/Resources.resx b/src/Analyzers/Analyzers/Resources.resx index e84fae2b93..67d1d437b4 100644 --- a/src/Analyzers/Analyzers/Resources.resx +++ b/src/Analyzers/Analyzers/Resources.resx @@ -118,7 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Method that declares that it should not be called on server is only meant to be invoked on client + Method that declares that it should not be called on server is only meant to be invoked on client. Method '{0}' invocation is not supported on server From 32080ed48b687db6de420d0fb62a4640a14a8b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Mon, 16 Jan 2023 16:06:54 +0100 Subject: [PATCH 06/31] Fixed namespace inconsistencies --- .../ApiUsage/UnsupportedCallSiteAttributeTests.cs | 2 +- .../Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs | 2 +- src/Framework/Core/CodeAnalysis/CallSiteType.cs | 2 +- src/Framework/Core/CodeAnalysis/UnsupportedCallSiteAttribute.cs | 2 +- .../Framework/Binding/HelperNamespace/DateTimeExtensions.cs | 2 +- .../ControlTree/UnsupportedCallSiteCheckingVisitor.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs b/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs index 9d989f3f15..2fb5fad4d3 100644 --- a/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs +++ b/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs @@ -40,7 +40,7 @@ public async Task Test_Warning_InvokeMethod_WithUnsupportedCallSiteAttribute() await VerifyCS.VerifyAnalyzerAsync(@" using System; using System.IO; - using DotVVM.Core.CodeAnalysis; + using DotVVM.Framework.CodeAnalysis; namespace ConsoleApplication1 { diff --git a/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs b/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs index 872dd8e070..8529aafc10 100644 --- a/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs +++ b/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs @@ -12,7 +12,7 @@ public sealed class UnsupportedCallSiteAttributeAnalyzer : DiagnosticAnalyzer private static readonly LocalizableResourceString unsupportedCallSiteTitle = new(nameof(Resources.ApiUsage_UnsupportedCallSite_Title), Resources.ResourceManager, typeof(Resources)); private static readonly LocalizableResourceString unsupportedCallSiteMessage = new(nameof(Resources.ApiUsage_UnsupportedCallSite_Message), Resources.ResourceManager, typeof(Resources)); private static readonly LocalizableResourceString unsupportedCallSiteDescription = new(nameof(Resources.ApiUsage_UnsupportedCallSite_Description), Resources.ResourceManager, typeof(Resources)); - private const string unsupportedCallSiteAttributeMetadataName = "DotVVM.Core.CodeAnalysis.UnsupportedCallSiteAttribute"; + private const string unsupportedCallSiteAttributeMetadataName = "DotVVM.Framework.CodeAnalysis.UnsupportedCallSiteAttribute"; private const int callSiteTypeServerUnderlyingValue = 0; public static DiagnosticDescriptor DoNotInvokeMethodFromUnsupportedCallSite = new( diff --git a/src/Framework/Core/CodeAnalysis/CallSiteType.cs b/src/Framework/Core/CodeAnalysis/CallSiteType.cs index 99fa00aed6..1809e68202 100644 --- a/src/Framework/Core/CodeAnalysis/CallSiteType.cs +++ b/src/Framework/Core/CodeAnalysis/CallSiteType.cs @@ -1,4 +1,4 @@ -namespace DotVVM.Core.CodeAnalysis +namespace DotVVM.Framework.CodeAnalysis { public enum CallSiteType { diff --git a/src/Framework/Core/CodeAnalysis/UnsupportedCallSiteAttribute.cs b/src/Framework/Core/CodeAnalysis/UnsupportedCallSiteAttribute.cs index 075ff80f6c..2384f793c6 100644 --- a/src/Framework/Core/CodeAnalysis/UnsupportedCallSiteAttribute.cs +++ b/src/Framework/Core/CodeAnalysis/UnsupportedCallSiteAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace DotVVM.Core.CodeAnalysis +namespace DotVVM.Framework.CodeAnalysis { [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false)] public class UnsupportedCallSiteAttribute : Attribute diff --git a/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs b/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs index 32e257f5d4..cafb456b18 100644 --- a/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs +++ b/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs @@ -1,5 +1,5 @@ using System; -using DotVVM.Core.CodeAnalysis; +using DotVVM.Framework.CodeAnalysis; namespace DotVVM.Framework.Binding.HelperNamespace { diff --git a/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs b/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs index eecee90ebb..2211c16451 100644 --- a/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs +++ b/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using DotVVM.Core.CodeAnalysis; +using DotVVM.Framework.CodeAnalysis; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Compilation.ControlTree.Resolved; From 79d61cea768f54c4219dfcc6f8893bcccfe77213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Mon, 16 Jan 2023 16:09:11 +0100 Subject: [PATCH 07/31] Changed root namespace to `DotVVM.Framework` --- src/Framework/Core/DotVVM.Core.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Framework/Core/DotVVM.Core.csproj b/src/Framework/Core/DotVVM.Core.csproj index bafc9b085b..76ef2cabbe 100644 --- a/src/Framework/Core/DotVVM.Core.csproj +++ b/src/Framework/Core/DotVVM.Core.csproj @@ -10,6 +10,7 @@ This package contains base classes and interfaces of DotVVM that might be useful in a business layer. DotVVM is an open source ASP.NET-based framework which allows to build modern web apps without writing any JavaScript code. enable + DotVVM.Framework From bbc1eaaa52949ddf4fc513b42562344fbec30558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 7 Jan 2023 14:48:03 +0100 Subject: [PATCH 08/31] Version updated to 4.1.0 so the project can be easily referenced from the sources when other libraries depend on 4.1 --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a910dccbe7..37b0b0da36 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,7 @@ RIGANTI DotVVM is an open source ASP.NET-based framework which allows to build interactive web apps easily by using mostly C# and HTML. dotvvm;asp.net;mvvm;owin;dotnetcore - 4.0.0 + 4.1.0 package-icon.png git https://github.com/riganti/dotvvm.git From fc8fc2843c7932789f54baf94e5c543125b74e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 7 Jan 2023 14:48:27 +0100 Subject: [PATCH 09/31] MarkupFile constructor allows creating the file with specified content --- src/Framework/Framework/Hosting/MarkupFile.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Framework/Framework/Hosting/MarkupFile.cs b/src/Framework/Framework/Hosting/MarkupFile.cs index 8821a97fe9..8dc82c8d58 100644 --- a/src/Framework/Framework/Hosting/MarkupFile.cs +++ b/src/Framework/Framework/Hosting/MarkupFile.cs @@ -67,7 +67,7 @@ public MarkupFile(string fileName, string fullPath) }; } - internal MarkupFile(string fileName, string fullPath, string contents) + public MarkupFile(string fileName, string fullPath, string contents) { FileName = fileName; FullPath = fullPath; From 4072d80a876016ff79c7e615fb761002d5ba2f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sat, 7 Jan 2023 17:38:41 +0100 Subject: [PATCH 10/31] AutoUI - LocalizableString needs only BindingCompilationService, not the entire context --- src/AutoUI/Core/Controls/AutoFormBase.cs | 2 +- src/AutoUI/Core/Controls/AutoGridViewColumn.cs | 2 +- src/AutoUI/Core/Controls/BulmaForm.cs | 2 +- src/AutoUI/Core/Metadata/LocalizableString.cs | 4 ++-- .../PropertyHandlers/FormEditors/CheckBoxEditorProvider.cs | 2 +- .../FormEditors/EnumComboBoxFormEditorProvider.cs | 4 ++-- .../PropertyHandlers/FormEditors/TextBoxEditorProvider.cs | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/AutoUI/Core/Controls/AutoFormBase.cs b/src/AutoUI/Core/Controls/AutoFormBase.cs index 29c8cc82bf..a2fef87240 100644 --- a/src/AutoUI/Core/Controls/AutoFormBase.cs +++ b/src/AutoUI/Core/Controls/AutoFormBase.cs @@ -119,7 +119,7 @@ internal static PropertyDisplayMetadata[] GetPropertiesToDisplay(AutoUIContext c if (property.IsDefaultLabelAllowed) { - return new Label(id).AppendChildren(new Literal(property.GetDisplayName().ToBinding(autoUiContext))); + return new Label(id).AppendChildren(new Literal(property.GetDisplayName().ToBinding(autoUiContext.BindingService))); } return null; } diff --git a/src/AutoUI/Core/Controls/AutoGridViewColumn.cs b/src/AutoUI/Core/Controls/AutoGridViewColumn.cs index e6930be54c..0de383ec1f 100644 --- a/src/AutoUI/Core/Controls/AutoGridViewColumn.cs +++ b/src/AutoUI/Core/Controls/AutoGridViewColumn.cs @@ -45,7 +45,7 @@ public static GridViewColumn Replace(IStyleMatchContext col) if (props.HeaderTemplate is null && props.HeaderText is null) { - props = props with { HeaderText = propertyMetadata.GetDisplayName().ToBinding(context) }; + props = props with { HeaderText = propertyMetadata.GetDisplayName().ToBinding(context.BindingService) }; } var control = CreateColumn(context, props, propertyMetadata); diff --git a/src/AutoUI/Core/Controls/BulmaForm.cs b/src/AutoUI/Core/Controls/BulmaForm.cs index a76428200c..bf1a0dac35 100644 --- a/src/AutoUI/Core/Controls/BulmaForm.cs +++ b/src/AutoUI/Core/Controls/BulmaForm.cs @@ -39,7 +39,7 @@ public DotvvmControl GetContents(FieldProps props) } var help = property.Description is { } description - ? new HtmlGenericControl("div").AddCssClass("help").SetProperty(c => c.InnerText, description.ToBinding(context)!) + ? new HtmlGenericControl("div").AddCssClass("help").SetProperty(c => c.InnerText, description.ToBinding(context.BindingService)!) : null; var validator = new Validator() .AddCssClass("help is-danger") diff --git a/src/AutoUI/Core/Metadata/LocalizableString.cs b/src/AutoUI/Core/Metadata/LocalizableString.cs index 88c3f77ac7..bf35fecc8c 100644 --- a/src/AutoUI/Core/Metadata/LocalizableString.cs +++ b/src/AutoUI/Core/Metadata/LocalizableString.cs @@ -37,12 +37,12 @@ public static LocalizableString Create(string value, Type? resourceType) } } - public ValueOrBinding ToBinding(AutoUIContext context) + public ValueOrBinding ToBinding(BindingCompilationService bindingCompilationService) { if (IsLocalized) { var binding = new ResourceBindingExpression( - context.BindingService, + bindingCompilationService, new object[] { new ParsedExpressionBindingProperty( ExpressionUtils.Replace(() => this.Localize()) diff --git a/src/AutoUI/Core/PropertyHandlers/FormEditors/CheckBoxEditorProvider.cs b/src/AutoUI/Core/PropertyHandlers/FormEditors/CheckBoxEditorProvider.cs index 334616667f..00ea9f3343 100644 --- a/src/AutoUI/Core/PropertyHandlers/FormEditors/CheckBoxEditorProvider.cs +++ b/src/AutoUI/Core/PropertyHandlers/FormEditors/CheckBoxEditorProvider.cs @@ -22,7 +22,7 @@ public override DotvvmControl CreateControl(PropertyDisplayMetadata property, Au .AddCssClasses(ControlCssClass, property.Styles?.FormControlCssClass) .SetProperty(c => c.Changed, props.Changed) .SetProperty(c => c.Checked, props.Property) - .SetProperty(c => c.Text, property.GetDisplayName().ToBinding(context)) + .SetProperty(c => c.Text, property.GetDisplayName().ToBinding(context.BindingService)) .SetProperty(c => c.Enabled, props.Enabled); return checkBox; } diff --git a/src/AutoUI/Core/PropertyHandlers/FormEditors/EnumComboBoxFormEditorProvider.cs b/src/AutoUI/Core/PropertyHandlers/FormEditors/EnumComboBoxFormEditorProvider.cs index 5cfaa08379..7b4d6d1f1b 100644 --- a/src/AutoUI/Core/PropertyHandlers/FormEditors/EnumComboBoxFormEditorProvider.cs +++ b/src/AutoUI/Core/PropertyHandlers/FormEditors/EnumComboBoxFormEditorProvider.cs @@ -34,8 +34,8 @@ public override DotvvmControl CreateControl(PropertyDisplayMetadata property, Au var title = LocalizableString.CreateNullable(displayAttribute?.Description, displayAttribute?.ResourceType); return (name, displayName, title); }) - .Select(e => new SelectorItem(e.displayName.ToBinding(context), new(Enum.Parse(enumType, e.name))) - .AddAttribute("title", e.title?.ToBinding(context))); + .Select(e => new SelectorItem(e.displayName.ToBinding(context.BindingService), new(Enum.Parse(enumType, e.name))) + .AddAttribute("title", e.title?.ToBinding(context.BindingService))); var control = new ComboBox() .SetCapability(props.Html) diff --git a/src/AutoUI/Core/PropertyHandlers/FormEditors/TextBoxEditorProvider.cs b/src/AutoUI/Core/PropertyHandlers/FormEditors/TextBoxEditorProvider.cs index 3957930105..028dc3e2e7 100644 --- a/src/AutoUI/Core/PropertyHandlers/FormEditors/TextBoxEditorProvider.cs +++ b/src/AutoUI/Core/PropertyHandlers/FormEditors/TextBoxEditorProvider.cs @@ -34,8 +34,8 @@ public override DotvvmControl CreateControl(PropertyDisplayMetadata property, Au .SetCapability(props.Html) .AddCssClasses(ControlCssClass, property.Styles?.FormControlCssClass) .SetProperty(t => t.Text, props.Property) - .SetAttribute("placeholder", property.Placeholder?.ToBinding(context)) - .SetAttribute("title", property.Description?.ToBinding(context)) + .SetAttribute("placeholder", property.Placeholder?.ToBinding(context.BindingService)) + .SetAttribute("title", property.Description?.ToBinding(context.BindingService)) .SetProperty(t => t.FormatString, property.FormatString) .SetProperty(t => t.Enabled, props.Enabled) .SetProperty(t => t.Changed, props.Changed); From 8d2702a42766531ceb81e1b3c0fea283908e96be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Tue, 24 Jan 2023 13:15:26 +0100 Subject: [PATCH 11/31] rest api: use Set instead of array to keep track of elements --- .../Framework/Resources/Scripts/api/api.ts | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/Framework/Framework/Resources/Scripts/api/api.ts b/src/Framework/Framework/Resources/Scripts/api/api.ts index 4aee2b05a2..edc6b2af59 100644 --- a/src/Framework/Framework/Resources/Scripts/api/api.ts +++ b/src/Framework/Framework/Resources/Scripts/api/api.ts @@ -14,27 +14,24 @@ class CachedValue { _stateManager?: StateManager _isLoading?: boolean _promise?: PromiseLike - _elements: HTMLElement[] = [] + _elements: Set = new Set() constructor(public _cache: Cache) { } - registerElement(element: HTMLElement, sharingKeyValue: string) { - if (!this._elements.includes(element)) { - this._elements.push(element); + _registerElement(element: HTMLElement, sharingKeyValue: string) { + if (!this._elements.has(element)) { + this._elements.add(element); ko.utils.domNodeDisposal.addDisposeCallback(element, () => { - this.unregisterElement(element, sharingKeyValue); + this._unregisterElement(element, sharingKeyValue); }); } } - unregisterElement(element: HTMLElement, sharingKeyValue: string) { - const oldElementIndex = this._elements.indexOf(element); - if (oldElementIndex >= 0) { - this._elements.splice(oldElementIndex, 1); - } - if (!this._elements.length) { + _unregisterElement(element: HTMLElement, sharingKeyValue: string) { + this._elements.delete(element); + if (!this._elements.size) { delete this._cache[sharingKeyValue]; } } @@ -74,9 +71,9 @@ export function invoke( return } - cachedValue.registerElement(lifetimeElement, sharingKeyValue); + cachedValue._registerElement(lifetimeElement, sharingKeyValue); if (oldCached) { - oldCached.unregisterElement(lifetimeElement, oldKey); + oldCached._unregisterElement(lifetimeElement, oldKey); } if (cachedValue._stateManager == null) From 7bd5ed21cfa6a38e55994adb3d658ec9d168560c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Mon, 23 Jan 2023 11:48:40 +0100 Subject: [PATCH 12/31] Maybe fix flaky HierarchyRepeater UI test --- src/Samples/Tests/Tests/Control/HierarchyRepeaterTests.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Samples/Tests/Tests/Control/HierarchyRepeaterTests.cs b/src/Samples/Tests/Tests/Control/HierarchyRepeaterTests.cs index 6c1c2e5b58..ec89d62ecb 100644 --- a/src/Samples/Tests/Tests/Control/HierarchyRepeaterTests.cs +++ b/src/Samples/Tests/Tests/Control/HierarchyRepeaterTests.cs @@ -1,6 +1,7 @@ using System.Linq; using DotVVM.Samples.Tests.Base; using DotVVM.Testing.Abstractions; +using OpenQA.Selenium.Interactions; using Riganti.Selenium.Core; using Riganti.Selenium.Core.Abstractions; using Xunit; @@ -20,6 +21,7 @@ public void Control_HierarchyRepeater_Basic() { RunInAllBrowsers(browser => { browser.NavigateToUrl(SamplesRouteUrls.ControlSamples_HierarchyRepeater_Basic); + browser.Driver.Manage().Window.Maximize(); AssertUI.InnerTextEquals(browser.First("HR-Empty", SelectByDataUi), ""); AssertUI.InnerTextEquals(browser.First("HR-EmptyData", SelectByDataUi), "There are no nodes."); @@ -44,8 +46,8 @@ IElementWrapper getNode(string hr, params int[] index) AssertUI.InnerTextEquals(getNode("HR-Server", 0, 0).Single("input[type=button]"), "2"); AssertUI.InnerTextEquals(getNode("HR-Client", 0, 0).Single("input[type=button]"), "2"); - browser.Single("GlobalLabel", SelectByDataUi).ClearInputByKeyboard().SendKeys("lalala"); - getNode("HR-Client", 0, 0).Single("input[type=button]").Click(); + browser.Single("GlobalLabel", SelectByDataUi).ScrollTo().ClearInputByKeyboard().SendKeys("lalala"); + getNode("HR-Client", 0, 0).ScrollTo().Single("input[type=button]").Click(); AssertUI.Attribute(getNode("HR-Server", 0, 0).Single("input[type=button]"), "title", "lalala: -- 0"); }); } @@ -56,6 +58,7 @@ public void Control_HierarchyRepeater_WithMarkupControl() { RunInAllBrowsers(browser => { browser.NavigateToUrl(SamplesRouteUrls.ControlSamples_HierarchyRepeater_WithMarkupControl); + browser.Driver.Manage().Window.Maximize(); IElementWrapper getNode(string hr, params int[] index) { From d0b7716b757d753dfaeb4b5978647a4083a36f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 25 Jan 2023 10:47:26 +0100 Subject: [PATCH 13/31] Update the template to DotVVM 4.1 and net7 --- .../EmptyWeb/.template.config/template.json | 6 +++--- .../content/EmptyWeb/DotvvmApplication1.csproj | 9 +++------ src/Templates/content/EmptyWeb/DotvvmStartup.cs | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/Templates/content/EmptyWeb/.template.config/template.json b/src/Templates/content/EmptyWeb/.template.config/template.json index 51fe192cd0..656c64bea6 100644 --- a/src/Templates/content/EmptyWeb/.template.config/template.json +++ b/src/Templates/content/EmptyWeb/.template.config/template.json @@ -28,11 +28,11 @@ }, { "choice": "net7.0", - "description": "Target .NET 7 (preview, at the time of writing)" + "description": "Target .NET 7" } ], - "replaces": "net6.0", - "defaultValue": "net6.0" + "replaces": "net7.0", + "defaultValue": "net7.0" } }, "primaryOutputs": [ { "path": "DotvvmApplication1.csproj" } ] diff --git a/src/Templates/content/EmptyWeb/DotvvmApplication1.csproj b/src/Templates/content/EmptyWeb/DotvvmApplication1.csproj index 7870c3bcc2..991ccd9c3f 100644 --- a/src/Templates/content/EmptyWeb/DotvvmApplication1.csproj +++ b/src/Templates/content/EmptyWeb/DotvvmApplication1.csproj @@ -1,23 +1,20 @@ - net6.0 + net7.0 enable - - - - + - + diff --git a/src/Templates/content/EmptyWeb/DotvvmStartup.cs b/src/Templates/content/EmptyWeb/DotvvmStartup.cs index f3fa423654..4069998f1d 100644 --- a/src/Templates/content/EmptyWeb/DotvvmStartup.cs +++ b/src/Templates/content/EmptyWeb/DotvvmStartup.cs @@ -1,5 +1,6 @@ using DotVVM.Framework; using DotVVM.Framework.Configuration; +using DotVVM.Framework.Compilation; using DotVVM.Framework.Routing; using Microsoft.Extensions.DependencyInjection; @@ -12,6 +13,19 @@ public void Configure(DotvvmConfiguration config, string applicationPath) ConfigureRoutes(config, applicationPath); ConfigureControls(config, applicationPath); ConfigureResources(config, applicationPath); + + // https://www.dotvvm.com/docs/4.0/pages/concepts/configuration/explicit-assembly-loading + config.ExperimentalFeatures.ExplicitAssemblyLoading.Enable(); + + // Use this for command heavy applications + // - DotVVM will store the viewmodels on the server, and client will only have to send back diffs + // https://www.dotvvm.com/docs/4.0/pages/concepts/viewmodels/server-side-viewmodel-cache + // config.ExperimentalFeatures.ServerSideViewModelCache.EnableForAllRoutes(); + + // Use this if you are deploying to containers or slots + // - DotVVM will precompile all views before it appears as ready + // https://www.dotvvm.com/docs/4.0/pages/concepts/configuration/view-compilation-modes + // config.Markup.ViewCompilation.Mode = ViewCompilationMode.DuringApplicationStart; } private void ConfigureRoutes(DotvvmConfiguration config, string applicationPath) From c1007af5993858ad302b868ebcb93913dffa3a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 25 Jan 2023 18:40:08 +0100 Subject: [PATCH 14/31] template: move views to Pages folder This should prevent the VS extension from creating ViewModel class in a different folder --- src/Templates/content/EmptyWeb/DotvvmStartup.cs | 8 ++++---- .../{Views => Pages}/Default/DefaultViewModel.cs | 0 .../EmptyWeb/{Views => Pages}/Default/default.dothtml | 0 .../EmptyWeb/{Views => Pages}/Error/ErrorViewModel.cs | 9 +++++++-- .../EmptyWeb/{Views => Pages}/Error/error.dothtml | 0 5 files changed, 11 insertions(+), 6 deletions(-) rename src/Templates/content/EmptyWeb/{Views => Pages}/Default/DefaultViewModel.cs (100%) rename src/Templates/content/EmptyWeb/{Views => Pages}/Default/default.dothtml (100%) rename src/Templates/content/EmptyWeb/{Views => Pages}/Error/ErrorViewModel.cs (77%) rename src/Templates/content/EmptyWeb/{Views => Pages}/Error/error.dothtml (100%) diff --git a/src/Templates/content/EmptyWeb/DotvvmStartup.cs b/src/Templates/content/EmptyWeb/DotvvmStartup.cs index 4069998f1d..c7ca399f42 100644 --- a/src/Templates/content/EmptyWeb/DotvvmStartup.cs +++ b/src/Templates/content/EmptyWeb/DotvvmStartup.cs @@ -30,11 +30,11 @@ public void Configure(DotvvmConfiguration config, string applicationPath) private void ConfigureRoutes(DotvvmConfiguration config, string applicationPath) { - config.RouteTable.Add("Default", "", "Views/Default/default.dothtml"); - config.RouteTable.Add("Error", "error", "Views/Error/error.dothtml"); + config.RouteTable.Add("Default", "", "Pages/Default/default.dothtml"); + config.RouteTable.Add("Error", "error", "Pages/Error/error.dothtml"); - // Uncomment the following line to auto-register all dothtml files in the Views folder - // config.RouteTable.AutoDiscoverRoutes(new DefaultRouteStrategy(config)); + // Uncomment the following line to auto-register all dothtml files in the Pages folder + config.RouteTable.AutoDiscoverRoutes(new DefaultRouteStrategy(config)); } private void ConfigureControls(DotvvmConfiguration config, string applicationPath) diff --git a/src/Templates/content/EmptyWeb/Views/Default/DefaultViewModel.cs b/src/Templates/content/EmptyWeb/Pages/Default/DefaultViewModel.cs similarity index 100% rename from src/Templates/content/EmptyWeb/Views/Default/DefaultViewModel.cs rename to src/Templates/content/EmptyWeb/Pages/Default/DefaultViewModel.cs diff --git a/src/Templates/content/EmptyWeb/Views/Default/default.dothtml b/src/Templates/content/EmptyWeb/Pages/Default/default.dothtml similarity index 100% rename from src/Templates/content/EmptyWeb/Views/Default/default.dothtml rename to src/Templates/content/EmptyWeb/Pages/Default/default.dothtml diff --git a/src/Templates/content/EmptyWeb/Views/Error/ErrorViewModel.cs b/src/Templates/content/EmptyWeb/Pages/Error/ErrorViewModel.cs similarity index 77% rename from src/Templates/content/EmptyWeb/Views/Error/ErrorViewModel.cs rename to src/Templates/content/EmptyWeb/Pages/Error/ErrorViewModel.cs index 2ef050724d..fed88c7f92 100644 --- a/src/Templates/content/EmptyWeb/Views/Error/ErrorViewModel.cs +++ b/src/Templates/content/EmptyWeb/Pages/Error/ErrorViewModel.cs @@ -15,10 +15,10 @@ public class ErrorViewModel : DotvvmViewModelBase public string? RequestId { get; set; } [Bind(Direction.None)] - public string ExceptionType { get; set; } + public string? ExceptionType { get; set; } [Bind(Direction.None)] - public string RequestPath { get; set; } + public string? RequestPath { get; set; } public ErrorViewModel() @@ -29,6 +29,11 @@ public override Task Init() { var aspcontext = Context.GetAspNetCoreContext(); var exceptionInfo = aspcontext.Features.Get(); + if (exceptionInfo is null) + { + ExceptionType = "View called without IExceptionHandlerFeature"; + return base.Init(); + } ExceptionType = exceptionInfo.Error.GetType().Name; RequestId = aspcontext.TraceIdentifier; RequestPath = exceptionInfo.Path; diff --git a/src/Templates/content/EmptyWeb/Views/Error/error.dothtml b/src/Templates/content/EmptyWeb/Pages/Error/error.dothtml similarity index 100% rename from src/Templates/content/EmptyWeb/Views/Error/error.dothtml rename to src/Templates/content/EmptyWeb/Pages/Error/error.dothtml From 5c4602da89b18fe4c82053c408f84e3923b287f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 25 Jan 2023 18:40:33 +0100 Subject: [PATCH 15/31] template: switch to IWebHostEnvironment to silence some deprecation warning --- src/Templates/content/EmptyWeb/Startup.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Templates/content/EmptyWeb/Startup.cs b/src/Templates/content/EmptyWeb/Startup.cs index 2957f1fea2..ac26b3622f 100644 --- a/src/Templates/content/EmptyWeb/Startup.cs +++ b/src/Templates/content/EmptyWeb/Startup.cs @@ -4,9 +4,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace DotvvmApplication1; public class Startup @@ -23,7 +22,7 @@ public void ConfigureServices(IServiceCollection services) } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // Configure the HTTP request pipeline. if (!env.IsDevelopment()) From 6da4c6d42e8a521f80c81c8c1911fa533f9adc67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=A0t=C4=9Bp=C3=A1nek?= Date: Fri, 27 Jan 2023 13:39:03 +0100 Subject: [PATCH 16/31] Add the DotvvmGlobalExtensions interface --- src/Framework/Framework/Resources/Scripts/dotvvm-root.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts index b9096d3777..edc9d8976e 100644 --- a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts +++ b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts @@ -132,8 +132,12 @@ if (compileConstants.debug) { (dotvvmExports as any).debug = true } + + declare global { - const dotvvm: typeof dotvvmExports & {debug?: true, isSpaReady?: typeof isSpaReady, handleSpaNavigation?: typeof handleSpaNavigation}; + interface DotvvmGlobalExtensions {} + + const dotvvm: DotvvmGlobalExtensions & typeof dotvvmExports & {debug?: true, isSpaReady?: typeof isSpaReady, handleSpaNavigation?: typeof handleSpaNavigation}; interface Window { dotvvm: typeof dotvvmExports From cf2cb8008ba84ebbac1b649899216eb80657fa60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=A0t=C4=9Bp=C3=A1nek?= Date: Fri, 27 Jan 2023 13:49:11 +0100 Subject: [PATCH 17/31] Add one more reference to DotvvmGlobalExtensions --- src/Framework/Framework/Resources/Scripts/dotvvm-root.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts index edc9d8976e..558e247ee8 100644 --- a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts +++ b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts @@ -140,7 +140,7 @@ declare global { const dotvvm: DotvvmGlobalExtensions & typeof dotvvmExports & {debug?: true, isSpaReady?: typeof isSpaReady, handleSpaNavigation?: typeof handleSpaNavigation}; interface Window { - dotvvm: typeof dotvvmExports + dotvvm: DotvvmGlobalExtensions & typeof dotvvmExports } } From 91b0a4ca2f475cf178843ba7b0bcd2de6b5ca8a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 28 Jan 2023 21:07:26 +0100 Subject: [PATCH 18/31] Cleanup AutoUI[.Annotations] docs/comments * Removed ComboBoxSettingsAttribute, not used in AutoUI * doccomments for Selection * fixed tags of the packages * add readme to the AutoUI packages --- .../Annotations/ComboBoxSettingsAttribute.cs | 33 ------------------- .../DotVVM.AutoUI.Annotations.csproj | 2 +- src/AutoUI/Annotations/EnabledAttribute.cs | 2 +- src/AutoUI/Annotations/ISelectionProvider.cs | 2 ++ src/AutoUI/Annotations/README.md | 14 ++++++++ src/AutoUI/Annotations/Selection.cs | 20 +++++++++++ src/AutoUI/Annotations/SelectionAttribute.cs | 1 + src/AutoUI/Annotations/VisibleAttribute.cs | 2 +- src/AutoUI/Core/DotVVM.AutoUI.csproj | 8 +++-- src/AutoUI/README.md | 9 ++--- ...rk.Controls.DynamicData.Annotations.csproj | 2 +- ...tVVM.Framework.Controls.DynamicData.csproj | 2 +- 12 files changed, 50 insertions(+), 47 deletions(-) delete mode 100644 src/AutoUI/Annotations/ComboBoxSettingsAttribute.cs create mode 100644 src/AutoUI/Annotations/README.md diff --git a/src/AutoUI/Annotations/ComboBoxSettingsAttribute.cs b/src/AutoUI/Annotations/ComboBoxSettingsAttribute.cs deleted file mode 100644 index 39c4c5ef6d..0000000000 --- a/src/AutoUI/Annotations/ComboBoxSettingsAttribute.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace DotVVM.AutoUI.Annotations -{ - /// - /// Defines the settings for the ComboBox form editor provider. - /// - [AttributeUsage(AttributeTargets.Property)] - public class ComboBoxSettingsAttribute : Attribute - { - - /// - /// Gets or sets the name of the property to be displayed. - /// - public string DisplayMember { get; set; } - - /// - /// Gets or sets the name of the property to be used as selected value. - /// - public string ValueMember { get; set; } - - /// - /// Gets or sets the binding expression for the list of items. - /// - public string DataSourceBinding { get; set; } - - /// - /// Gets or sets the text on the empty item. If null or empty, the empty item will not be included. - /// - public string EmptyItemText { get; set; } - - } -} diff --git a/src/AutoUI/Annotations/DotVVM.AutoUI.Annotations.csproj b/src/AutoUI/Annotations/DotVVM.AutoUI.Annotations.csproj index c21c2772ad..d5c963410f 100644 --- a/src/AutoUI/Annotations/DotVVM.AutoUI.Annotations.csproj +++ b/src/AutoUI/Annotations/DotVVM.AutoUI.Annotations.csproj @@ -7,7 +7,7 @@ DotVVM.AutoUI.Annotations Annotation attributes for DotVVM AutoUI. https://www.dotvvm.com/docs/3.0/pages/community-add-ons/dotvvm-dynamic-data - ($PackageTags);autoui;annotations;metadata;ui generation + $(PackageTags);autoui;annotations;metadata;ui generation true diff --git a/src/AutoUI/Annotations/EnabledAttribute.cs b/src/AutoUI/Annotations/EnabledAttribute.cs index 9a0f912940..ef86f63f85 100644 --- a/src/AutoUI/Annotations/EnabledAttribute.cs +++ b/src/AutoUI/Annotations/EnabledAttribute.cs @@ -30,7 +30,7 @@ public class EnabledAttribute : Attribute, IConditionalFieldAttribute public string Roles { get; set; } /// - /// Gets or sets whether the field should be editable for authenticated or non-authenticated users, or null for both kinds (default behavior). + /// Gets or sets whether the field should be editable for authenticated or non-authenticated users, or for both (default behavior). /// public AuthenticationMode IsAuthenticated { get; set; } diff --git a/src/AutoUI/Annotations/ISelectionProvider.cs b/src/AutoUI/Annotations/ISelectionProvider.cs index 4c6c8d9e63..8b5bfc3c82 100644 --- a/src/AutoUI/Annotations/ISelectionProvider.cs +++ b/src/AutoUI/Annotations/ISelectionProvider.cs @@ -4,12 +4,14 @@ namespace DotVVM.AutoUI.Annotations; +/// The service providing items. Automatically used from the SelectionViewModel, unless Items are set explicitly. public interface ISelectionProvider { [AllowStaticCommand] Task> GetSelectorItems(); } +/// The service providing items. Automatically used from the SelectionViewModel, unless Items are set explicitly. public interface ISelectionProvider { [AllowStaticCommand] diff --git a/src/AutoUI/Annotations/README.md b/src/AutoUI/Annotations/README.md new file mode 100644 index 0000000000..7a95e41c2b --- /dev/null +++ b/src/AutoUI/Annotations/README.md @@ -0,0 +1,14 @@ +# Annotations for DotVVM Auto UI + +This package only contains annotations and some interfaces used to annotate classes for which [DotVVM.AutoUI](https://www.nuget.org/packages/DotVVM.AutoUI) can create forms and tables. + +DotVVM.AutoUI.Annotations only depends on DotVVM.Core, and is intended to be included in non-web projects. + +Attributes included: +* `VisibleAttribute`, `EnabledAttribute` +* `ComboBoxSettingsAttribute` +* `StyleAttribute` for hardcoding css classes +* `SelectionAttribute` for declaring selectable fields using components like RadioButton + +Base classes included: +* `Selection` for selectable items diff --git a/src/AutoUI/Annotations/Selection.cs b/src/AutoUI/Annotations/Selection.cs index ff97cfd24b..9a95e3a907 100644 --- a/src/AutoUI/Annotations/Selection.cs +++ b/src/AutoUI/Annotations/Selection.cs @@ -1,14 +1,34 @@ namespace DotVVM.AutoUI.Annotations; +/// +/// Base class for selectable items, please prefer to derive from +/// public abstract record Selection { + /// The label to display in the selector component public string DisplayName { get; set; } private protected abstract void SorryWeCannotAllowYouToInheritThisClass(); } +/// +/// Base class for selectable items. See also +/// +/// Type of the value (the identifier). The property labeled with [Selection(typeof(This))] will have to be of type +/// +/// public record ProductSelection : Selection<Guid>; +/// // and then ... +/// public class ProductSelectionProvider : ISelectionProvider<ProductSelection> +/// { +/// public Task<List<ProductSelection>> GetSelectorItems() => +/// Task.FromResult(new() { +/// new ProductSelection() { Value = new Guid("00000000-0000-0000-0000-000000000001"), DisplayName = "First product" }, +/// }); +/// } +/// public abstract record Selection : Selection { + /// The value identifying this selection item public TKey Value { get; set; } private protected override void SorryWeCannotAllowYouToInheritThisClass() => throw new System.NotImplementedException("Mischief managed."); diff --git a/src/AutoUI/Annotations/SelectionAttribute.cs b/src/AutoUI/Annotations/SelectionAttribute.cs index a2f6ff41d6..d37ff9ae00 100644 --- a/src/AutoUI/Annotations/SelectionAttribute.cs +++ b/src/AutoUI/Annotations/SelectionAttribute.cs @@ -8,6 +8,7 @@ namespace DotVVM.AutoUI.Annotations; [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class SelectionAttribute : System.Attribute { + /// The implementation public Type SelectionType { get; } public SelectionAttribute(Type selectionType) diff --git a/src/AutoUI/Annotations/VisibleAttribute.cs b/src/AutoUI/Annotations/VisibleAttribute.cs index 5e12ed48d8..fb11696502 100644 --- a/src/AutoUI/Annotations/VisibleAttribute.cs +++ b/src/AutoUI/Annotations/VisibleAttribute.cs @@ -30,7 +30,7 @@ public class VisibleAttribute : Attribute, IConditionalFieldAttribute public string Roles { get; set; } /// - /// Gets or sets whether the field should be visible for authenticated or non-authenticated users, or null for both kinds (default behavior). + /// Gets or sets whether the field should be visible for authenticated or non-authenticated users, or for both (default behavior). /// public AuthenticationMode IsAuthenticated { get; set; } diff --git a/src/AutoUI/Core/DotVVM.AutoUI.csproj b/src/AutoUI/Core/DotVVM.AutoUI.csproj index 5f9c724a80..8b865c411f 100644 --- a/src/AutoUI/Core/DotVVM.AutoUI.csproj +++ b/src/AutoUI/Core/DotVVM.AutoUI.csproj @@ -5,8 +5,10 @@ true true DotVVM.AutoUI + Annotation attributes for DotVVM AutoUI. https://www.dotvvm.com/docs/3.0/pages/community-add-ons/dotvvm-dynamic-data - ($PackageTags);autoui;annotations;metadata;ui generation + $(PackageTags);autoui;annotations;metadata;ui generation + README.md true enable @@ -16,10 +18,10 @@ + + - - diff --git a/src/AutoUI/README.md b/src/AutoUI/README.md index 9880391a8c..ddde3f8a8a 100644 --- a/src/AutoUI/README.md +++ b/src/AutoUI/README.md @@ -1,6 +1,6 @@ # DotVVM Auto UI -Automatically generated forms, tables and more from type metadata. +Automatically generated forms, tables and more from type metadata. ## Data Annotations @@ -30,8 +30,6 @@ It should be able to create reasonable UI from just the type information, for mo ### Example -```csharp - ```csharp public class EmployeeDTO { @@ -68,7 +66,6 @@ public class EmployeeDTO ### Configuration API - The metadata can be also controlled using a configuration API: ```csharp @@ -125,7 +122,7 @@ This will allow to provide UI metadata using the standard .NET Data Annotations When your view model class is decorated with data annotation attributes, you can auto-generate GridView columns. -DotVVM AutoUI brings the `auto:GridViewColumns` control, it is a special grid column which get's replaced by a separate column for each property. +DotVVM AutoUI brings the `auto:GridViewColumns` control, it is a special grid column which gets replaced by a separate column for each property. It can be used with the built-in `dot:GridView`, and also with the `GridView`s in DotVVM component packages ```html @@ -198,7 +195,7 @@ As with grid columns, there is a similar set of properties to customize the form If you want to layout the form into multiple parts, you can use the group names to render each group separately. If you specify the `GroupName` property, the `Form` will render only fields from this group. -``` +```html
diff --git a/src/DynamicData/Annotations/DotVVM.Framework.Controls.DynamicData.Annotations.csproj b/src/DynamicData/Annotations/DotVVM.Framework.Controls.DynamicData.Annotations.csproj index c811dcf952..7056d8efed 100644 --- a/src/DynamicData/Annotations/DotVVM.Framework.Controls.DynamicData.Annotations.csproj +++ b/src/DynamicData/Annotations/DotVVM.Framework.Controls.DynamicData.Annotations.csproj @@ -4,7 +4,7 @@ DotVVM.DynamicData.Annotations Annotation attributes for DotVVM Dynamic Data that provide additional features. https://www.dotvvm.com/docs/3.0/pages/community-add-ons/dotvvm-dynamic-data - ($PackageTags);dnx;dynamic data;annotations;metadata;ui generation + $(PackageTags);dnx;dynamic data;annotations;metadata;ui generation true diff --git a/src/DynamicData/DynamicData/DotVVM.Framework.Controls.DynamicData.csproj b/src/DynamicData/DynamicData/DotVVM.Framework.Controls.DynamicData.csproj index d940174647..517c18bfa7 100644 --- a/src/DynamicData/DynamicData/DotVVM.Framework.Controls.DynamicData.csproj +++ b/src/DynamicData/DynamicData/DotVVM.Framework.Controls.DynamicData.csproj @@ -3,7 +3,7 @@ $(DefaultTargetFrameworks) DotVVM.DynamicData https://www.dotvvm.com/docs/3.0/pages/community-add-ons/dotvvm-dynamic-data - ($PackageTags);dynamic data;annotations;metadata;ui generation + $(PackageTags);dynamic data;annotations;metadata;ui generation true From aa7549d5faa19d170d15fc07231fbe36c50dbc26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Tue, 31 Jan 2023 14:35:34 +0100 Subject: [PATCH 19/31] Include referencing assembly in "could not load" error This should help with debugging why is DotVVM trying to load some assembly, which shouldn't even be there --- .../ControlTree/DefaultControlResolver.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs index af61b2563b..6757086bba 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs @@ -164,12 +164,10 @@ private static void RegisterCapabilitiesFromInterfaces(Type type) private IEnumerable GetAllRelevantAssemblies(string dotvvmAssembly) { - #if DotNetCore var assemblies = compiledAssemblyCache.GetAllAssemblies() .Where(a => a.GetReferencedAssemblies().Any(r => r.Name == dotvvmAssembly)); #else - var loadedAssemblies = compiledAssemblyCache.GetAllAssemblies() .Where(a => a.GetReferencedAssemblies().Any(r => r.Name == dotvvmAssembly)); @@ -178,7 +176,16 @@ private IEnumerable GetAllRelevantAssemblies(string dotvvmAssembly) // ReflectionUtils.GetAllAssemblies() in netframework returns only assemblies which have already been loaded into // the current AppDomain, to return all assemblies we traverse recursively all referenced Assemblies var assemblies = loadedAssemblies - .SelectRecursively(a => a.GetReferencedAssemblies().Where(an => visitedAssemblies.Add(an.FullName)).Select(an => Assembly.Load(an))) + .SelectRecursively(a => a.GetReferencedAssemblies().Where(an => visitedAssemblies.Add(an.FullName)).Select(an => { + try + { + return Assembly.Load(an); + } + catch (Exception ex) + { + throw new Exception($"Unable to load assembly '{an.FullName}' referenced by '{a.FullName}'.", ex); + } + })) .Where(a => a.GetReferencedAssemblies().Any(r => r.Name == dotvvmAssembly)) .Distinct(); #endif From 6844348ce7f873dbc79e09e7f47ba60feef97b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 28 Jan 2023 14:48:49 +0100 Subject: [PATCH 20/31] Set the Content-Length header in resource middleware Otherwise, when we write to the stream, Asp.Net Core will using chunked encoding, which is slightly wasteful and might prevent nginx (in some settings) compressing the resource. --- .../Hosting/Middlewares/DotvvmLocalResourceMiddleware.cs | 3 +++ .../Hosting/Middlewares/DotvvmReturnedFileMiddleware.cs | 3 +++ src/Framework/Hosting.AspNetCore/Hosting/DotvvmHttpContext.cs | 2 +- src/Framework/Hosting.AspNetCore/Hosting/DotvvmHttpResponse.cs | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Framework/Framework/Hosting/Middlewares/DotvvmLocalResourceMiddleware.cs b/src/Framework/Framework/Hosting/Middlewares/DotvvmLocalResourceMiddleware.cs index a0167990dd..24ad666121 100644 --- a/src/Framework/Framework/Hosting/Middlewares/DotvvmLocalResourceMiddleware.cs +++ b/src/Framework/Framework/Hosting/Middlewares/DotvvmLocalResourceMiddleware.cs @@ -33,6 +33,9 @@ public async Task Handle(IDotvvmRequestContext request) request.HttpContext.Response.Headers.Add("Cache-Control", new[] { "no-cache, no-store, must-revalidate" }); using (var body = resource.LoadResource(request)) { + if (body.CanSeek) + request.HttpContext.Response.Headers["Content-Length"] = body.Length.ToString(); + await body.CopyToAsync(request.HttpContext.Response.Body); } return true; diff --git a/src/Framework/Framework/Hosting/Middlewares/DotvvmReturnedFileMiddleware.cs b/src/Framework/Framework/Hosting/Middlewares/DotvvmReturnedFileMiddleware.cs index 55bcc76685..743351ddf9 100644 --- a/src/Framework/Framework/Hosting/Middlewares/DotvvmReturnedFileMiddleware.cs +++ b/src/Framework/Framework/Hosting/Middlewares/DotvvmReturnedFileMiddleware.cs @@ -76,6 +76,9 @@ private async Task RenderReturnedFile(IHttpContext context, IReturnedFileStorage } } + if (stream.CanSeek) + context.Response.Headers["Content-Length"] = stream.Length.ToString(); + context.Response.StatusCode = (int)HttpStatusCode.OK; await stream.CopyToAsync(context.Response.Body); } diff --git a/src/Framework/Hosting.AspNetCore/Hosting/DotvvmHttpContext.cs b/src/Framework/Hosting.AspNetCore/Hosting/DotvvmHttpContext.cs index be1d4237c7..9e16802cd3 100644 --- a/src/Framework/Hosting.AspNetCore/Hosting/DotvvmHttpContext.cs +++ b/src/Framework/Hosting.AspNetCore/Hosting/DotvvmHttpContext.cs @@ -37,4 +37,4 @@ public T GetItem(string key) .Select(k => new KeyValuePair(k.Key.ToString(), k.Value))); } } -} \ No newline at end of file +} diff --git a/src/Framework/Hosting.AspNetCore/Hosting/DotvvmHttpResponse.cs b/src/Framework/Hosting.AspNetCore/Hosting/DotvvmHttpResponse.cs index b8abef17be..b35ea0eb3f 100644 --- a/src/Framework/Hosting.AspNetCore/Hosting/DotvvmHttpResponse.cs +++ b/src/Framework/Hosting.AspNetCore/Hosting/DotvvmHttpResponse.cs @@ -64,4 +64,4 @@ public Task WriteAsync(string text, CancellationToken token) return OriginalResponse.WriteAsync(text, token); } } -} \ No newline at end of file +} From 723c656f854549380872a2466f3449d33a857fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Wed, 1 Feb 2023 13:23:01 +0100 Subject: [PATCH 21/31] Fixed issue with commands not being analyzed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Together with resource bindings they are always evaluated on server Co-authored-by: Stanislav Lukeš --- .../ControlTree/UnsupportedCallSiteCheckingVisitor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs b/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs index 2211c16451..45116382a5 100644 --- a/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs +++ b/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs @@ -30,7 +30,7 @@ protected override Expression VisitMethodCall(MethodCallExpression node) public override void VisitBinding(ResolvedBinding binding) { base.VisitBinding(binding); - if (binding.Binding is not ResourceBindingExpression) + if (binding.Binding is not ResourceBindingExpression and not CommandBindingExpression) return; var expressionVisitor = new ExpressionInspectingVisitor(); From 8c06430716c57bf3e2889e335194d0ee31196714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Wed, 1 Feb 2023 13:41:43 +0100 Subject: [PATCH 22/31] Added optional reason argument to `UnsupportedCallSiteAttribute` --- .../UnsupportedCallSiteAttributeTests.cs | 31 ++++++++++++++++++- .../UnsupportedCallSiteAttributeAnalyzer.cs | 4 ++- src/Analyzers/Analyzers/Resources.Designer.cs | 2 +- src/Analyzers/Analyzers/Resources.resx | 2 +- .../UnsupportedCallSiteAttribute.cs | 4 ++- .../HelperNamespace/DateTimeExtensions.cs | 6 ++-- 6 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs b/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs index 2fb5fad4d3..a96458816d 100644 --- a/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs +++ b/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs @@ -60,7 +60,36 @@ public void CallSite() }", VerifyCS.Diagnostic(UnsupportedCallSiteAttributeAnalyzer.DoNotInvokeMethodFromUnsupportedCallSite) - .WithLocation(0).WithArguments("Target")); + .WithLocation(0).WithArguments("Target", string.Empty)); + } + + [Fact] + public async Task Test_Warning_InvokeMethod_WithUnsupportedCallSiteAttribute_WithReason() + { + await VerifyCS.VerifyAnalyzerAsync(@" + using System; + using System.IO; + using DotVVM.Framework.CodeAnalysis; + + namespace ConsoleApplication1 + { + public class RegularClass + { + [UnsupportedCallSite(CallSiteType.ServerSide, ""REASON"")] + public void Target() + { + + } + + public void CallSite() + { + {|#0:Target()|}; + } + } + }", + + VerifyCS.Diagnostic(UnsupportedCallSiteAttributeAnalyzer.DoNotInvokeMethodFromUnsupportedCallSite) + .WithLocation(0).WithArguments("Target", "due to: \"REASON\"")); } } } diff --git a/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs b/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs index 8529aafc10..3ba6fb3376 100644 --- a/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs +++ b/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs @@ -48,11 +48,13 @@ public override void Initialize(AnalysisContext context) if (attribute.ConstructorArguments.First().Value is not int callSiteType || callSiteTypeServerUnderlyingValue != callSiteType) return; + var reason = (string?)attribute.ConstructorArguments.Skip(1).First().Value; context.ReportDiagnostic( Diagnostic.Create( DoNotInvokeMethodFromUnsupportedCallSite, invocation.Syntax.GetLocation(), - invocation.TargetMethod.Name)); + invocation.TargetMethod.Name, + (reason != null) ? $"due to: \"{reason}\"" : string.Empty)); } }, OperationKind.Invocation); } diff --git a/src/Analyzers/Analyzers/Resources.Designer.cs b/src/Analyzers/Analyzers/Resources.Designer.cs index 1a70e1f52d..dbe0226b70 100644 --- a/src/Analyzers/Analyzers/Resources.Designer.cs +++ b/src/Analyzers/Analyzers/Resources.Designer.cs @@ -70,7 +70,7 @@ internal class Resources { } /// - /// Looks up a localized string similar to Method '{0}' invocation is not supported on server. + /// Looks up a localized string similar to Method '{0}' invocation is not supported on server {1}. /// internal static string ApiUsage_UnsupportedCallSite_Message { get { diff --git a/src/Analyzers/Analyzers/Resources.resx b/src/Analyzers/Analyzers/Resources.resx index 67d1d437b4..b2cc65bf2c 100644 --- a/src/Analyzers/Analyzers/Resources.resx +++ b/src/Analyzers/Analyzers/Resources.resx @@ -121,7 +121,7 @@ Method that declares that it should not be called on server is only meant to be invoked on client. - Method '{0}' invocation is not supported on server + Method '{0}' invocation is not supported on server {1} Unsupported call site diff --git a/src/Framework/Core/CodeAnalysis/UnsupportedCallSiteAttribute.cs b/src/Framework/Core/CodeAnalysis/UnsupportedCallSiteAttribute.cs index 2384f793c6..9555082a34 100644 --- a/src/Framework/Core/CodeAnalysis/UnsupportedCallSiteAttribute.cs +++ b/src/Framework/Core/CodeAnalysis/UnsupportedCallSiteAttribute.cs @@ -6,10 +6,12 @@ namespace DotVVM.Framework.CodeAnalysis public class UnsupportedCallSiteAttribute : Attribute { public readonly CallSiteType Type; + public readonly string? Reason; - public UnsupportedCallSiteAttribute(CallSiteType type) + public UnsupportedCallSiteAttribute(CallSiteType type, string? reason = null) { Type = type; + Reason = reason; } } } diff --git a/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs b/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs index cafb456b18..43fca030a9 100644 --- a/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs +++ b/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs @@ -8,16 +8,14 @@ public static class DateTimeExtensions /// /// Converts the date (assuming it is in UTC) to browser's local time. - /// CAUTION: When evaluated on the server, no conversion is made as we don't know the browser timezone. /// - [UnsupportedCallSite(CallSiteType.ServerSide)] + [UnsupportedCallSite(CallSiteType.ServerSide, "When evaluated on the server, no conversion is made as we don't know the browser timezone.")] public static DateTime ToBrowserLocalTime(this DateTime value) => value; /// /// Converts the date (assuming it is in UTC) to browser's local time. - /// CAUTION: When evaluated on the server, no conversion is made as we don't know the browser timezone. /// - [UnsupportedCallSite(CallSiteType.ServerSide)] + [UnsupportedCallSite(CallSiteType.ServerSide, "When evaluated on the server, no conversion is made as we don't know the browser timezone.")] public static DateTime? ToBrowserLocalTime(this DateTime? value) => value; } From 0b9055cf140812fd017b67c5cdae15f29229c726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Wed, 1 Feb 2023 13:48:37 +0100 Subject: [PATCH 23/31] Performance improved: faster attribute checking --- .../ControlTree/UnsupportedCallSiteCheckingVisitor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs b/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs index 45116382a5..e9e8e1640d 100644 --- a/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs +++ b/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs @@ -16,10 +16,10 @@ class ExpressionInspectingVisitor : ExpressionVisitor protected override Expression VisitMethodCall(MethodCallExpression node) { - var callSiteAttr = node.Method.CustomAttributes.FirstOrDefault(a => a.AttributeType == typeof(UnsupportedCallSiteAttribute)); - if (callSiteAttr is not null && callSiteAttr.ConstructorArguments.Any()) + if (node.Method.IsDefined(typeof(UnsupportedCallSiteAttribute))) { - if (callSiteAttr.ConstructorArguments.First().Value is int type && type == (int)CallSiteType.ServerSide) + var callSiteAttr = node.Method.CustomAttributes.FirstOrDefault(a => a.AttributeType == typeof(UnsupportedCallSiteAttribute))!; + if (callSiteAttr.ConstructorArguments.Any() && callSiteAttr.ConstructorArguments.First().Value is int type && type == (int)CallSiteType.ServerSide) InvalidCallSiteDetected?.Invoke(node.Method); } From ce5ac78dee16ef16c39bef9020346b2183377a2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Mon, 23 Jan 2023 13:42:07 +0100 Subject: [PATCH 24/31] styles: support controls in capabilities --- .../ControlTree/Resolved/ResolvedControl.cs | 22 +++++- .../Resolved/ResolvedPropertyCapability.cs | 7 +- .../ControlTree/ResolvedTreeHelpers.cs | 2 +- .../Styles/ResolvedControlHelper.cs | 69 ++++++++++++++++--- .../Styles/StyleMatchContextMethods.cs | 5 ++ .../ControlTests/ServerSideStyleTests.cs | 29 ++++++++ ...verSideStyleTests.BindableObjectReads.html | 50 ++++++++++++++ 7 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 src/Tests/ControlTests/testoutputs/ServerSideStyleTests.BindableObjectReads.html diff --git a/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedControl.cs b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedControl.cs index 0b3994860b..9f8cfcede7 100644 --- a/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedControl.cs +++ b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedControl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using DotVVM.Framework.Binding; @@ -57,6 +58,8 @@ public void SetProperty(ResolvedPropertySetter value, bool replace = false) public bool SetProperty(ResolvedPropertySetter value, bool replace, [NotNullWhen(false)] out string? error) { + if (value is ResolvedPropertyCapability capability) + return SetCapabilityProperty(capability, replace, out error); error = null; if (!Properties.TryGetValue(value.Property, out var oldValue) || replace) { @@ -82,8 +85,25 @@ public bool SetProperty(ResolvedPropertySetter value, bool replace, [NotNullWhen return true; } + bool SetCapabilityProperty(ResolvedPropertyCapability capability, bool replace, [NotNullWhen(false)] out string? error) + { + foreach (var v in capability.Values) + { + Debug.Assert(v.Value.Property == v.Key); + if (!SetProperty(v.Value, replace, out var innerError)) + { + error = $"{v.Key}: {innerError}"; + return false; + } + } + error = null; + return true; + } + public bool RemoveProperty(DotvvmProperty property) { + if (property is DotvvmCapabilityProperty capability) + throw new NotSupportedException("Cannot remove capability property, remove each of its properties manually."); if (Properties.TryGetValue(property, out _)) { Properties.Remove(property); @@ -124,7 +144,7 @@ public bool RemoveProperty(DotvvmProperty property) if (properties.Count == 0) return null; - return new ResolvedPropertyCapability(capability, properties); + return new ResolvedPropertyCapability(capability, properties) { Parent = this }; } public override void Accept(IResolvedControlTreeVisitor visitor) diff --git a/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedPropertyCapability.cs b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedPropertyCapability.cs index b2c4e1afc9..8eab78ab48 100644 --- a/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedPropertyCapability.cs +++ b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedPropertyCapability.cs @@ -43,7 +43,7 @@ public override void Accept(IResolvedControlTreeVisitor visitor) public override void AcceptChildren(IResolvedControlTreeVisitor visitor) { } - public object? ToCapabilityObject(bool throwExceptions = false) + public object? ToCapabilityObject(IServiceProvider? services, bool throwExceptions = false) { var capability = this.Property; @@ -69,7 +69,6 @@ public override void Accept(IResolvedControlTreeVisitor visitor) else return t.GetConstructor(new [] { elementType })!.Invoke(new [] { value }); } - // TODO: controls and templates if (throwExceptions) throw new NotSupportedException($"Can not convert {value} to {t}"); return null; @@ -79,7 +78,7 @@ public override void Accept(IResolvedControlTreeVisitor visitor) foreach (var (p, dotprop) in mapping) { if (this.Values.TryGetValue(dotprop, out var value)) - p.SetValue(obj, convertValue(value.GetValue(), p.PropertyType)); + p.SetValue(obj, convertValue(Styles.ResolvedControlHelper.ToRuntimeValue(value, services), p.PropertyType)); } if (capability.PropertyGroupMapping is not { Length: > 0 } groupMappingList) @@ -105,7 +104,7 @@ public override void Accept(IResolvedControlTreeVisitor visitor) { foreach (var p in properties) - dictionary.Add(p.GroupMemberName, convertValue(this.Values[p].GetValue(), dictionaryElementType)); + dictionary.Add(p.GroupMemberName, convertValue(Styles.ResolvedControlHelper.ToRuntimeValue(this.Values[p], services), dictionaryElementType)); } if (propertyOriginalValue is null) prop.SetValue(obj, dictionary); diff --git a/src/Framework/Framework/Compilation/ControlTree/ResolvedTreeHelpers.cs b/src/Framework/Framework/Compilation/ControlTree/ResolvedTreeHelpers.cs index ff681458fd..b6b62c21c9 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ResolvedTreeHelpers.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ResolvedTreeHelpers.cs @@ -33,7 +33,7 @@ public static class ResolvedTreeHelpers ResolvedPropertyTemplate value => value.Content, ResolvedPropertyControl value => value.Control, ResolvedPropertyControlCollection value => value.Controls, - ResolvedPropertyCapability value => value.ToCapabilityObject(throwExceptions: false), + ResolvedPropertyCapability value => value, _ => throw new NotSupportedException() }; diff --git a/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs b/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs index dd9d9527f8..243e5194a8 100644 --- a/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs +++ b/src/Framework/Framework/Compilation/Styles/ResolvedControlHelper.cs @@ -18,6 +18,7 @@ using DotVVM.Framework.Hosting; using DotVVM.Framework.Compilation.ViewCompiler; using DotVVM.Framework.Runtime; +using FastExpressionCompiler; namespace DotVVM.Framework.Compilation.Styles { @@ -149,7 +150,18 @@ public record PropertyTranslationException(DotvvmProperty property): public static ResolvedPropertySetter TranslateProperty(DotvvmProperty property, object? value, DataContextStack dataContext, DotvvmConfiguration? config) { - if (value is ResolvedPropertySetter resolvedSetter) + if (value is ResolvedPropertyCapability resolvedCapability) + { + if (resolvedCapability.Property == property && resolvedCapability.GetDataContextStack(null) == dataContext) + return resolvedCapability; + if (property is not DotvvmCapabilityProperty capabilityProperty) + throw new NotSupportedException($"Property {property.Name} must capability property."); + if (resolvedCapability.Property.PropertyType != capabilityProperty.PropertyType) + throw new NotSupportedException($"Property {property.Name} have type {resolvedCapability.Property.PropertyType.ToCode()}."); + + value = resolvedCapability.ToCapabilityObject(config?.ServiceProvider); + } + else if (value is ResolvedPropertySetter resolvedSetter) { value = resolvedSetter.GetValue(); } @@ -280,12 +292,36 @@ public static void SetContent(ResolvedControl control, ResolvedControl[] innerCo static DotvvmBindableObject ToLazyRuntimeControl(this ResolvedControl c, Type expectedType, IServiceProvider services) { if (expectedType == typeof(DotvvmControl)) - return new LazyRuntimeControl(c); + return new LazyRuntimeControl(c, services); else return ToRuntimeControl(c, services); } - static object? ToRuntimeValue(this ResolvedPropertySetter setter, IServiceProvider services) + static IEnumerable CreateEnumerable(Type type, IEnumerable items) + { + var elementType = ReflectionUtils.GetEnumerableType(type)!; + if (type.IsArray) + { + var array = Array.CreateInstance(elementType, items.Count()); + foreach (var (item, i) in items.Select((item, i) => (item, i))) + array.SetValue(item, i); + return (IEnumerable)array; + } + else + { + if (type.IsInterface) + { + // hope that List implements the interface + type = typeof(List<>).MakeGenericType(elementType); + } + var list = (System.Collections.IList)Activator.CreateInstance(type)!; + foreach (var i in items) + list.Add(i); + return (IEnumerable)list; + } + } + + public static object? ToRuntimeValue(this ResolvedPropertySetter setter, IServiceProvider? services) { if (setter is ResolvedPropertyValue valueSetter) return valueSetter.Value; @@ -295,16 +331,31 @@ static DotvvmBindableObject ToLazyRuntimeControl(this ResolvedControl c, Type ex var expectedType = setter.Property.PropertyType; if (setter is ResolvedPropertyControl controlSetter) + { + if (services is null) + throw new ArgumentNullException(nameof(services), "Cannot convert a control to a runtime value without a service provider."); + return controlSetter.Control?.ToLazyRuntimeControl(expectedType, services); + } else if (setter is ResolvedPropertyControlCollection controlCollectionSetter) { + if (services is null) + throw new ArgumentNullException(nameof(services), "Cannot convert a control to a runtime value without a service provider."); + var expectedControlType = ReflectionUtils.GetEnumerableType(expectedType)!; - return controlCollectionSetter.Controls.Select(c => c.ToLazyRuntimeControl(expectedControlType, services)).ToList(); + return CreateEnumerable( + expectedType, + controlCollectionSetter.Controls.Select(c => c.ToLazyRuntimeControl(expectedControlType, services)) + ); } else if (setter is ResolvedPropertyTemplate templateSetter) { return new ResolvedControlTemplate(templateSetter.Content.ToArray()); } + else if (setter is ResolvedPropertyCapability capability) + { + return capability.ToCapabilityObject(services); + } else throw new NotSupportedException($"Property setter {setter.GetType().Name} is not supported."); } @@ -343,12 +394,15 @@ public static DotvvmBindableObject ToRuntimeControl(this ResolvedControl c, ISer public sealed class LazyRuntimeControl: DotvvmControl { public ResolvedControl ResolvedControl { get; set; } + readonly IServiceProvider services; + private bool initialized = false; - public LazyRuntimeControl(ResolvedControl resolvedControl) + public LazyRuntimeControl(ResolvedControl resolvedControl, IServiceProvider services) { ResolvedControl = resolvedControl; LifecycleRequirements = ControlLifecycleRequirements.Init; + this.services = services; } void InitializeChildren(IDotvvmRequestContext? context) @@ -359,10 +413,9 @@ void InitializeChildren(IDotvvmRequestContext? context) { if (initialized) return; - if (context is null) - throw new InvalidOperationException("Internal.RequestContextProperty property is not set."); + var services = context?.Services ?? this.services; - Children.Add((DotvvmControl)ResolvedControl.ToRuntimeControl(context.Services)); + Children.Add((DotvvmControl)ResolvedControl.ToRuntimeControl(services)); initialized = true; } } diff --git a/src/Framework/Framework/Compilation/Styles/StyleMatchContextMethods.cs b/src/Framework/Framework/Compilation/Styles/StyleMatchContextMethods.cs index bd39ccf811..9586dbf6a9 100644 --- a/src/Framework/Framework/Compilation/Styles/StyleMatchContextMethods.cs +++ b/src/Framework/Framework/Compilation/Styles/StyleMatchContextMethods.cs @@ -249,8 +249,13 @@ public static T PropertyValue(this IStyleMatchContext c, DotvvmProperty prope if (c.Control.GetProperty(property) is {} s) { var value = s.GetValue(); + // If they ask for ResolvedControl, we should return it. Otherwise, try converting to runtime value (DotvvmControl or capability object) if (value is T or null) return (T?)value; + + var runtimeValue = s.ToRuntimeValue(c.Configuration.ServiceProvider); + if (runtimeValue is T) + return (T?)runtimeValue; } return GetDefault(property); } diff --git a/src/Tests/ControlTests/ServerSideStyleTests.cs b/src/Tests/ControlTests/ServerSideStyleTests.cs index 5ecb5fbc38..da7daaff1a 100644 --- a/src/Tests/ControlTests/ServerSideStyleTests.cs +++ b/src/Tests/ControlTests/ServerSideStyleTests.cs @@ -271,6 +271,35 @@ public async Task CapabilityReads() check.CheckString(r.FormattedHtml, fileExtension: "html"); } + [TestMethod] + public async Task BindableObjectReads() + { + var cth = createHelper(c => { + c.Styles.Register(c => + c.PropertyValue(x => x.Columns).Any(c => c is GridViewTextColumn { HeaderText: "Test" })) + .SetAttribute("data-headers", c => string.Join(" ; ", c.PropertyValue(c => c.Columns).Select(c => c.HeaderText ?? "?"))); + }); + + var r = await cth.RunPage(typeof(BasicTestViewModel), @" + + + + + + + + + + + + + + + "); + check.CheckString(r.FormattedHtml, fileExtension: "html"); + } + + [TestMethod] public async Task StyleBindingMapping() { diff --git a/src/Tests/ControlTests/testoutputs/ServerSideStyleTests.BindableObjectReads.html b/src/Tests/ControlTests/testoutputs/ServerSideStyleTests.BindableObjectReads.html new file mode 100644 index 0000000000..383b9dd9fb --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/ServerSideStyleTests.BindableObjectReads.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + +
+ Test +
+ +
+ + + + + + + + + + + + + + + + +
+ RealValue + + Test +
+ + + +
+ + + From f9d64132c541cc94bfb5f92fe63a7d34cd08d1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Tue, 24 Jan 2023 12:30:10 +0100 Subject: [PATCH 25/31] styles: Test reading ITemplate inside capability --- .../ControlTests/ServerSideStyleTests.cs | 27 +++++++++++++++++++ ...SideStyleTests.ContentCapabilityReads.html | 8 ++++++ 2 files changed, 35 insertions(+) create mode 100644 src/Tests/ControlTests/testoutputs/ServerSideStyleTests.ContentCapabilityReads.html diff --git a/src/Tests/ControlTests/ServerSideStyleTests.cs b/src/Tests/ControlTests/ServerSideStyleTests.cs index da7daaff1a..cfdc0d2221 100644 --- a/src/Tests/ControlTests/ServerSideStyleTests.cs +++ b/src/Tests/ControlTests/ServerSideStyleTests.cs @@ -13,6 +13,8 @@ using DotVVM.Framework.Compilation.Styles; using System.Linq; using DotVVM.Framework.Binding; +using DotVVM.AutoUI.Controls; +using DotVVM.AutoUI; namespace DotVVM.Framework.Tests.ControlTests { @@ -27,6 +29,8 @@ ControlTestHelper createHelper(Action c) _ = Repeater.RenderAsNamedTemplateProperty; config.Styles.Register().SetProperty(r => r.RenderAsNamedTemplate, false, StyleOverrideOptions.Ignore); c(config); + }, services: s => { + s.AddAutoUI(); }); } @@ -274,6 +278,7 @@ public async Task CapabilityReads() [TestMethod] public async Task BindableObjectReads() { + // this isn't the intended way to use styles (c.ControlProperty should be used instead), but we need it to work in some cases var cth = createHelper(c => { c.Styles.Register(c => c.PropertyValue(x => x.Columns).Any(c => c is GridViewTextColumn { HeaderText: "Test" })) @@ -299,6 +304,28 @@ public async Task BindableObjectReads() check.CheckString(r.FormattedHtml, fileExtension: "html"); } + [TestMethod] + public async Task ContentCapabilityReads() + { + var cth = createHelper(c => { + c.Styles.Register(c => c.PropertyValue(c => c.GetCapability())?.OverrideTemplate is not null) + .SetProperty( + c => c.GetCapability().OverrideTemplate, + c => new CloneTemplate( + new HtmlGenericControl("div").AddCssClass("template-wrapper") + .AppendChildren(new TemplateHost(c.PropertyValue(c => c.GetCapability()).OverrideTemplate)) + )); + }); + + var r = await cth.RunPage(typeof(BasicTestViewModel), @" + + + Test + + + "); + check.CheckString(r.FormattedHtml, fileExtension: "html"); + } [TestMethod] public async Task StyleBindingMapping() diff --git a/src/Tests/ControlTests/testoutputs/ServerSideStyleTests.ContentCapabilityReads.html b/src/Tests/ControlTests/testoutputs/ServerSideStyleTests.ContentCapabilityReads.html new file mode 100644 index 0000000000..77d4e6da13 --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/ServerSideStyleTests.ContentCapabilityReads.html @@ -0,0 +1,8 @@ + + + +
+ Test +
+ + From dc236c56f5f9e5b3f6da5627624c292cf11e4dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Fri, 27 Jan 2023 10:15:39 +0100 Subject: [PATCH 26/31] Added test for capabilities with templates used in style-precompiled controls --- .../ControlTree/ServerSideStyleTests.cs | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/src/Tests/Runtime/ControlTree/ServerSideStyleTests.cs b/src/Tests/Runtime/ControlTree/ServerSideStyleTests.cs index edb10ab9eb..cf1aafc91d 100644 --- a/src/Tests/Runtime/ControlTree/ServerSideStyleTests.cs +++ b/src/Tests/Runtime/ControlTree/ServerSideStyleTests.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; +using DotVVM.AutoUI; +using DotVVM.AutoUI.Controls; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Compilation.ControlTree; @@ -13,13 +16,18 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using DotVVM.Framework.Testing; using DotVVM.Framework.ResourceManagement; +using DotVVM.Framework.ViewModel; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json.Linq; namespace DotVVM.Framework.Tests.Runtime.ControlTree { [TestClass] public class ServerSideStyleTests { - DotvvmConfiguration config = DotvvmTestHelper.CreateConfiguration(); + private DotvvmConfiguration config = DotvvmTestHelper.CreateConfiguration(services => { + new DotvvmServiceCollection(services).AddAutoUI(); + }); public ServerSideStyleTests() { config.Styles @@ -39,9 +47,9 @@ public ServerSideStyleTests() .AppendControlProperty(PostBack.HandlersProperty, new ConfirmPostBackHandler("4")); } - ResolvedTreeRoot Parse(string markup, string fileName = "default.dothtml", bool checkErrors = true) => + ResolvedTreeRoot Parse(string markup, string fileName = "default.dothtml", bool checkErrors = true, string viewModelType = "System.Collections.Generic.List") => DotvvmTestHelper.ParseResolvedTree( - "@viewModel System.Collections.Generic.List\n" + markup, fileName, config, checkErrors); + "@viewModel " + viewModelType + "\n" + markup, fileName, config, checkErrors); [TestMethod] @@ -493,5 +501,51 @@ public void StylesRemove() var repeater = e.Content[0]; Assert.IsFalse(repeater.Properties.ContainsKey(Repeater.ItemTemplateProperty), "ItemTemplate should not be set"); } + + + [TestMethod] + public void TemplateInCapability() + { + var e = Parse(@" + + {{value: Name}} + +", viewModelType: "DotVVM.Framework.Tests.Runtime.ControlTree.BasicTestViewModel"); + Assert.AreEqual(1, e.Content.Count); + + var gridView = e.Content[0]; + var columns = gridView.GetProperty(GridView.ColumnsProperty)!.GetValue() as List; + Assert.AreEqual(3, columns!.Count); + + var idColumn = columns.Single(c => c.GetProperty(GridViewColumn.HeaderTextProperty) is ResolvedPropertyValue { Value: "Id" }); + Assert.AreEqual(typeof(GridViewTextColumn), idColumn.Metadata.Type); + Assert.IsNotNull(idColumn.GetProperty(GridViewTextColumn.ValueBindingProperty)); + + var nameColumn = columns.Single(c => c.GetProperty(GridViewColumn.HeaderTextProperty) is ResolvedPropertyValue { Value: "Name" }); + Assert.AreEqual(typeof(GridViewTemplateColumn), nameColumn.Metadata.Type); + Assert.IsNotNull(nameColumn.GetProperty(GridViewTemplateColumn.ContentTemplateProperty)); + + var enabledColumn = columns.Single(c => c.GetProperty(GridViewColumn.HeaderTextProperty) is ResolvedPropertyValue { Value: "Enabled" }); + Assert.AreEqual(typeof(GridViewCheckBoxColumn), enabledColumn.Metadata.Type); + Assert.IsNotNull(enabledColumn.GetProperty(GridViewCheckBoxColumn.ValueBindingProperty)); + } + } + + public class BasicTestViewModel : DotvvmViewModelBase + { + public GridViewDataSet Customers { get; set; } = new GridViewDataSet() { + Items = { + new CustomerData() { Id = 1, Name = "One" }, + new CustomerData() { Id = 2, Name = "Two" } + } + }; + + public class CustomerData + { + public int Id { get; set; } + [Required] + public string Name { get; set; } + public bool Enabled { get; set; } + } } } From 1c4a65f0494cee275f2ad5f250090dd365c4f89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Sun, 29 Jan 2023 12:07:35 +0100 Subject: [PATCH 27/31] Added UI test for auto:GridViewColumns which tests the issue --- .../Common/DotVVM.Samples.Common.csproj | 1 + .../AutoUI/AutoGridViewColumnsViewModel.cs | 50 +++++++++++++++++++ .../AutoUI/AutoGridViewColumns.dothtml | 23 +++++++++ .../Abstractions/SamplesRouteUrls.designer.cs | 1 + .../Tests/Tests/Feature/AutoUITests.cs | 22 ++++++++ 5 files changed, 97 insertions(+) create mode 100644 src/Samples/Common/ViewModels/FeatureSamples/AutoUI/AutoGridViewColumnsViewModel.cs create mode 100644 src/Samples/Common/Views/FeatureSamples/AutoUI/AutoGridViewColumns.dothtml diff --git a/src/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj index bb6db75a48..aba0d776da 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -83,6 +83,7 @@ + diff --git a/src/Samples/Common/ViewModels/FeatureSamples/AutoUI/AutoGridViewColumnsViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/AutoUI/AutoGridViewColumnsViewModel.cs new file mode 100644 index 0000000000..ef55aa97f0 --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/AutoUI/AutoGridViewColumnsViewModel.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.Controls; +using DotVVM.Framework.ViewModel; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.AutoUI +{ + public class AutoGridViewColumnsViewModel : DotvvmViewModelBase + { + + public GridViewDataSet Customers { get; set; } = new(); + + + public override Task PreRender() + { + if (Customers.IsRefreshRequired) + { + Customers.LoadFromQueryable(GetData()); + } + return base.PreRender(); + } + + private static IQueryable GetData() + { + return new[] + { + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 1, Name = "John Doe", BirthDate = DateTime.Parse("1976-04-01"), MessageReceived = false}, + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 2, Name = "John Deer", BirthDate = DateTime.Parse("1984-03-02"), MessageReceived = false }, + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 3, Name = "Johnny Walker", BirthDate = DateTime.Parse("1934-01-03"), MessageReceived = true}, + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 4, Name = "Jim Hacker", BirthDate = DateTime.Parse("1912-11-04"), MessageReceived = true}, + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 5, Name = "Joe E. Brown", BirthDate = DateTime.Parse("1947-09-05"), MessageReceived = false}, + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 6, Name = "Jim Harris", BirthDate = DateTime.Parse("1956-07-06"), MessageReceived = false}, + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 7, Name = "J. P. Morgan", BirthDate = DateTime.Parse("1969-05-07"), MessageReceived = false }, + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 8, Name = "J. R. Ewing", BirthDate = DateTime.Parse("1987-03-08"), MessageReceived = false}, + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 9, Name = "Jeremy Clarkson", BirthDate = DateTime.Parse("1994-04-09"), MessageReceived = false }, + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 10, Name = "Jenny Green", BirthDate = DateTime.Parse("1947-02-10"), MessageReceived = false}, + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 11, Name = "Joseph Blue", BirthDate = DateTime.Parse("1948-12-11"), MessageReceived = false}, + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 12, Name = "Jack Daniels", BirthDate = DateTime.Parse("1968-10-12"), MessageReceived = true}, + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 13, Name = "Jackie Chan", BirthDate = DateTime.Parse("1978-08-13"), MessageReceived = false}, + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 14, Name = "Jasper", BirthDate = DateTime.Parse("1934-06-14"), MessageReceived = false}, + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 15, Name = "Jumbo", BirthDate = DateTime.Parse("1965-06-15"), MessageReceived = false }, + new BasicSamples.ViewModels.ControlSamples.GridView.CustomerData() { CustomerId = 16, Name = "Junkie Doodle", BirthDate = DateTime.Parse("1977-05-16"), MessageReceived = false } + }.AsQueryable(); + } + } +} + diff --git a/src/Samples/Common/Views/FeatureSamples/AutoUI/AutoGridViewColumns.dothtml b/src/Samples/Common/Views/FeatureSamples/AutoUI/AutoGridViewColumns.dothtml new file mode 100644 index 0000000000..dc0d5acaca --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/AutoUI/AutoGridViewColumns.dothtml @@ -0,0 +1,23 @@ +@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.AutoUI.AutoGridViewColumnsViewModel, DotVVM.Samples.Common + + + + + + + + + + + + + +

{{value: Name}}

+
+
+
+ + + + + diff --git a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs index 9fbc18277f..bd3f19551d 100644 --- a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs +++ b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs @@ -209,6 +209,7 @@ public partial class SamplesRouteUrls public const string FeatureSamples_Attribute_ToStringConversion = "FeatureSamples/Attribute/ToStringConversion"; public const string FeatureSamples_AutoUI_AutoEditor = "FeatureSamples/AutoUI/AutoEditor"; public const string FeatureSamples_AutoUI_AutoForm = "FeatureSamples/AutoUI/AutoForm"; + public const string FeatureSamples_AutoUI_AutoGridViewColumns = "FeatureSamples/AutoUI/AutoGridViewColumns"; public const string FeatureSamples_BindableCssStyles_BindableCssStyles = "FeatureSamples/BindableCssStyles/BindableCssStyles"; public const string FeatureSamples_BindingContexts_BindingContext = "FeatureSamples/BindingContexts/BindingContext"; public const string FeatureSamples_BindingContexts_CollectionContext = "FeatureSamples/BindingContexts/CollectionContext"; diff --git a/src/Samples/Tests/Tests/Feature/AutoUITests.cs b/src/Samples/Tests/Tests/Feature/AutoUITests.cs index 6b511f6727..4879c55895 100644 --- a/src/Samples/Tests/Tests/Feature/AutoUITests.cs +++ b/src/Samples/Tests/Tests/Feature/AutoUITests.cs @@ -110,5 +110,27 @@ public void Feature_AutoUI_AutoForm() AssertUI.IsDisplayed(streetField.ParentElement.ParentElement.Single(".help")); }); } + + [Fact] + public void Feature_AutoUI_AutoGridViewColumns() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_AutoUI_AutoGridViewColumns); + + var headerCells = browser.FindElements("thead tr:first-child th") + .ThrowIfDifferentCountThan(4); + AssertUI.TextEquals(headerCells[0], "Customer id"); + AssertUI.TextEquals(headerCells[1], "Person or company name"); + AssertUI.TextEquals(headerCells[2], "Birth date"); + AssertUI.TextEquals(headerCells[3], "Message received"); + + var cells = browser.FindElements("tbody tr:first-child td") + .ThrowIfDifferentCountThan(4); + AssertUI.TextEquals(cells[0].Single("span"), "1"); + AssertUI.TextEquals(cells[1].Single("h2"), "John Doe"); + AssertUI.TextEquals(cells[2].Single("span"), "4/1/1976 12:00:00 AM"); + AssertUI.IsNotChecked(cells[3].Single("input[type=checkbox]")); + }); + } } } From eead0a73bde1e61625b57e061466a926525d119a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 4 Jan 2023 14:34:17 +0100 Subject: [PATCH 28/31] Add documentation comments to various binding-related classes Part is taken from #1392 which won't be merged for a while. --- .../ViewModel/AllowStaticCommandAttribute.cs | 5 +++++ src/Framework/Core/ViewModel/Direction.cs | 16 ++++++++++++++-- src/Framework/Core/ViewModel/ProtectMode.cs | 6 +++--- .../Framework/Binding/ActiveDotvvmProperty.cs | 1 + .../Binding/AttachedPropertyAttribute.cs | 2 ++ .../BindingCompilationOptionsAttribute.cs | 7 +++++++ .../Binding/BindingCompilationService.cs | 5 ++++- .../Framework/Binding/BindingHelper.cs | 4 +++- .../Framework/Binding/BindingPageInfo.cs | 3 +++ ...llectionElementDataContextChangeAttribute.cs | 1 + .../Binding/CompileTimeOnlyDotvvmProperty.cs | 2 +- .../ConstantDataContextChangeAttribute.cs | 1 + ...PropertyBindingDataContextChangeAttribute.cs | 1 + ...rolPropertyTypeDataContextChangeAttribute.cs | 1 + .../Framework/Binding/DelegateActionProperty.cs | 1 + .../Binding/DotvvmBindingCacheHelper.cs | 2 ++ .../Binding/Expressions/BindingExpression.cs | 3 +++ .../Framework/Binding/Expressions/IBinding.cs | 11 +++++++++-- .../StaticCommandBindingExpression.cs | 1 + .../Binding/HelperNamespace/ListExtensions.cs | 3 +++ .../Binding/VirtualPropertyGroupDictionary.cs | 17 +++++++++++++++++ .../Compilation/ControlTree/DataContextStack.cs | 6 +++++- .../ControlTree/DotvvmPropertyGroup.cs | 1 + 23 files changed, 89 insertions(+), 11 deletions(-) diff --git a/src/Framework/Core/ViewModel/AllowStaticCommandAttribute.cs b/src/Framework/Core/ViewModel/AllowStaticCommandAttribute.cs index 307da6592e..f09f0fb468 100644 --- a/src/Framework/Core/ViewModel/AllowStaticCommandAttribute.cs +++ b/src/Framework/Core/ViewModel/AllowStaticCommandAttribute.cs @@ -3,6 +3,11 @@ namespace DotVVM.Framework.ViewModel { + /// Allows DotVVM to call the method from staticCommand. + /// + /// This attribute must be used to prevent attackers from calling any method in your system. + /// While DotVVM signs the method names used staticCommand and it shouldn't be possible to execute any other method, + /// the attribute offers a decent protection against RCE in case the Asp.Net Core encryption keys are compromised. public class AllowStaticCommandAttribute : Attribute { } diff --git a/src/Framework/Core/ViewModel/Direction.cs b/src/Framework/Core/ViewModel/Direction.cs index c47a9a2922..f7e9ab041c 100644 --- a/src/Framework/Core/ViewModel/Direction.cs +++ b/src/Framework/Core/ViewModel/Direction.cs @@ -5,19 +5,31 @@ namespace DotVVM.Framework.ViewModel { /// - /// ServerToClient, ServerToClient on postback, ClientToServer, C2S iff in command path + /// Specifies on which requests should the property should serialized and sent. Default is Both. + /// Set to None to disable serialization of the property. + /// This enums is flags, the directions can be arbitrarily combined. /// [Flags] public enum Direction { + /// Never send this property to the client, it won't be allowed to use this property from value and staticCommand bindings. None = 0, + /// Sent to client on the initial GET request, but not sent again on postbacks ServerToClientFirstRequest = 1, + /// Property is updated on postbacks, but not sent on the first request (initially it will be set to null or default value of the primitive type) ServerToClientPostback = 2, + /// Sent from server to client, but not sent back. ServerToClient = ServerToClientFirstRequest | ServerToClientPostback, + /// Complement to , not meant to be used on its own. ClientToServerNotInPostbackPath = 4, + /// Sent from client to server, but only if the current data context is this property. If the data context is a child object of this property, only that part of the object will be sent, all other properties are ignored. + /// To sent the initial value to client, use Direction.ServerToClientFirstRequest | Direction.ClientToServerInPostbackPath ClientToServerInPostbackPath = 8, + /// Sent back on postbacks. Initially the property will set to null or primitive default value. To send the initial value to client, use Direction.ServerToClientFirstRequest | Direction.ClientToServer ClientToServer = ClientToServerInPostbackPath | ClientToServerNotInPostbackPath, + /// Always sent to client, sent back only when the object is the current data context (see also ) IfInPostbackPath = ServerToClient | ClientToServerInPostbackPath, + /// Value is sent on each request. This is the default value. Both = 15, } -} \ No newline at end of file +} diff --git a/src/Framework/Core/ViewModel/ProtectMode.cs b/src/Framework/Core/ViewModel/ProtectMode.cs index 18c2c9a958..288ee41e6d 100644 --- a/src/Framework/Core/ViewModel/ProtectMode.cs +++ b/src/Framework/Core/ViewModel/ProtectMode.cs @@ -11,17 +11,17 @@ namespace DotVVM.Framework.ViewModel public enum ProtectMode { /// - /// The property value is sent to the client unencrypted and it is not signed. It can be modified on the client with no restrictions. + /// The property value is sent to the client unencrypted and it is not signed. It can be modified on the client with no restrictions. This is the default. /// None, /// - /// The property value is sent to the client unencrypted, but it is also signed. If it is modified on the client, the server will throw an exception during postback. + /// The property value is sent to the client in both unencrypted and encrypted form. On server, the encrypted value is read, so it cannot be modified on the client. /// SignData, /// - /// The property value is encrypted before it is sent to the client. + /// The property value is encrypted before it is sent to the client. Encrypted properties thus cannot be used in value bindings. /// EncryptData } diff --git a/src/Framework/Framework/Binding/ActiveDotvvmProperty.cs b/src/Framework/Framework/Binding/ActiveDotvvmProperty.cs index 719beda699..831b85d787 100644 --- a/src/Framework/Framework/Binding/ActiveDotvvmProperty.cs +++ b/src/Framework/Framework/Binding/ActiveDotvvmProperty.cs @@ -10,6 +10,7 @@ namespace DotVVM.Framework.Binding { + /// An abstract DotvvmProperty which contains code to be executed when the assigned control is being rendered. public abstract class ActiveDotvvmProperty : DotvvmProperty { public abstract void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context, DotvvmControl control); diff --git a/src/Framework/Framework/Binding/AttachedPropertyAttribute.cs b/src/Framework/Framework/Binding/AttachedPropertyAttribute.cs index 65ee39234e..a30427ab94 100644 --- a/src/Framework/Framework/Binding/AttachedPropertyAttribute.cs +++ b/src/Framework/Framework/Binding/AttachedPropertyAttribute.cs @@ -6,6 +6,8 @@ namespace DotVVM.Framework.Binding { + /// Used to mark DotvvmProperty which are used on other control than the declaring type. For example, Validation.Target is an attached property. + /// Note that DotVVM allows this for any DotvvmProperty, but this attribute instructs editor extension to include the property in autocompletion. [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] public class AttachedPropertyAttribute : Attribute { diff --git a/src/Framework/Framework/Binding/BindingCompilationOptionsAttribute.cs b/src/Framework/Framework/Binding/BindingCompilationOptionsAttribute.cs index d04de43a34..5a7e8eddc8 100644 --- a/src/Framework/Framework/Binding/BindingCompilationOptionsAttribute.cs +++ b/src/Framework/Framework/Binding/BindingCompilationOptionsAttribute.cs @@ -1,11 +1,18 @@ using System; using System.Collections.Generic; using System.Text; +using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Compilation.Binding; namespace DotVVM.Framework.Binding { + /// Allow to adjust how bindings are compiled. Can be placed on custom binding type (for example, see ) or on a dotvvm property public abstract class BindingCompilationOptionsAttribute : Attribute { + /// Returns a list of resolvers - functions which accept any set of existing binding properties and returns one new binding property. + /// It will be automatically invoked when the returned property is needed. + /// See for a list of default property resolvers - to adjust how the binding is compiled, you'll want to redefine one of the default resolvers. + /// See for example how to use this method. public abstract IEnumerable GetResolvers(); } } diff --git a/src/Framework/Framework/Binding/BindingCompilationService.cs b/src/Framework/Framework/Binding/BindingCompilationService.cs index d349303b5e..f5e2ee7ddd 100644 --- a/src/Framework/Framework/Binding/BindingCompilationService.cs +++ b/src/Framework/Framework/Binding/BindingCompilationService.cs @@ -26,10 +26,13 @@ public class BindingCompilationOptions public List TransformerClasses { get; set; } = new List(); } + /// A service used to create new bindings and compute binding properties. public class BindingCompilationService { private readonly IExpressionToDelegateCompiler expressionCompiler; private readonly Lazy noInitService; + + /// Utilities for caching bindings created at runtime public DotvvmBindingCacheHelper Cache { get; } public BindingCompilationService(IOptions options, IExpressionToDelegateCompiler expressionCompiler, IDotvvmCacheAdapter cache) @@ -123,7 +126,7 @@ public BindingCompilationRequirementsAttribute GetRequirements(IBinding binding, } /// - /// Resolves required and optional properties + /// Resolves required properties of the binding. If the binding contains it will be used to report errors instead of throwing an exception. /// public virtual void InitializeBinding(IBinding binding, IEnumerable? bindingRequirements = null) { diff --git a/src/Framework/Framework/Binding/BindingHelper.cs b/src/Framework/Framework/Binding/BindingHelper.cs index a7e7172b90..c646d716f7 100644 --- a/src/Framework/Framework/Binding/BindingHelper.cs +++ b/src/Framework/Framework/Binding/BindingHelper.cs @@ -24,8 +24,10 @@ namespace DotVVM.Framework.Binding { public static partial class BindingHelper { + /// Gets the binding property identified by the type. The result may be null, if is ReturnNul This method should always return the same result and should run fast (may rely on caching, so first call might not be that fast). [return: MaybeNull] - public static T GetProperty(this IBinding binding, ErrorHandlingMode errorMode = ErrorHandlingMode.ThrowException) => (T)binding.GetProperty(typeof(T), errorMode)!; + public static T GetProperty(this IBinding binding, ErrorHandlingMode errorMode) => (T)binding.GetProperty(typeof(T), errorMode)!; + /// Gets the binding property identified by the type. This method should always return the same result and should run fast (may rely on caching, so first call might not be that fast). public static T GetProperty(this IBinding binding) => GetProperty(binding, ErrorHandlingMode.ThrowException)!; [Obsolete] diff --git a/src/Framework/Framework/Binding/BindingPageInfo.cs b/src/Framework/Framework/Binding/BindingPageInfo.cs index f705bd4534..80d64dcca6 100644 --- a/src/Framework/Framework/Binding/BindingPageInfo.cs +++ b/src/Framework/Framework/Binding/BindingPageInfo.cs @@ -11,8 +11,11 @@ namespace DotVVM.Framework.Binding { public class BindingPageInfo { + /// Returns true if any command or staticCommand is currently running. Always returns false on the server. public bool IsPostbackRunning => false; + /// Returns true on server and false in JavaScript. public bool EvaluatingOnServer => true; + /// Returns false on server and true in JavaScript. public bool EvaluatingOnClient => false; internal static void RegisterJavascriptTranslations(JavascriptTranslatableMethodCollection methods) diff --git a/src/Framework/Framework/Binding/CollectionElementDataContextChangeAttribute.cs b/src/Framework/Framework/Binding/CollectionElementDataContextChangeAttribute.cs index 3dadaa94bd..7a53483fe2 100644 --- a/src/Framework/Framework/Binding/CollectionElementDataContextChangeAttribute.cs +++ b/src/Framework/Framework/Binding/CollectionElementDataContextChangeAttribute.cs @@ -10,6 +10,7 @@ namespace DotVVM.Framework.Binding { + /// Sets data context type to the element type of current data context. public class CollectionElementDataContextChangeAttribute : DataContextChangeAttribute { public override int Order { get; } diff --git a/src/Framework/Framework/Binding/CompileTimeOnlyDotvvmProperty.cs b/src/Framework/Framework/Binding/CompileTimeOnlyDotvvmProperty.cs index 744d391820..b982ee6569 100644 --- a/src/Framework/Framework/Binding/CompileTimeOnlyDotvvmProperty.cs +++ b/src/Framework/Framework/Binding/CompileTimeOnlyDotvvmProperty.cs @@ -7,7 +7,7 @@ namespace DotVVM.Framework.Binding { /// - /// The DotvvmProperty that fallbacks to another DotvvmProperty's value. + /// The DotvvmProperty that can only be used at compile time (in server-side styles or precompiled CompositeControls) /// public class CompileTimeOnlyDotvvmProperty : DotvvmProperty { diff --git a/src/Framework/Framework/Binding/ConstantDataContextChangeAttribute.cs b/src/Framework/Framework/Binding/ConstantDataContextChangeAttribute.cs index 63c4d4bebb..b0b82d8d3e 100644 --- a/src/Framework/Framework/Binding/ConstantDataContextChangeAttribute.cs +++ b/src/Framework/Framework/Binding/ConstantDataContextChangeAttribute.cs @@ -9,6 +9,7 @@ namespace DotVVM.Framework.Binding { + /// Changes the data context type to the type specified in the attribute constructor. public class ConstantDataContextChangeAttribute : DataContextChangeAttribute { public Type Type { get; } diff --git a/src/Framework/Framework/Binding/ControlPropertyBindingDataContextChangeAttribute.cs b/src/Framework/Framework/Binding/ControlPropertyBindingDataContextChangeAttribute.cs index b56d25d813..be4ca4606b 100644 --- a/src/Framework/Framework/Binding/ControlPropertyBindingDataContextChangeAttribute.cs +++ b/src/Framework/Framework/Binding/ControlPropertyBindingDataContextChangeAttribute.cs @@ -10,6 +10,7 @@ namespace DotVVM.Framework.Binding { + /// Sets data context type to the result type of binding the specified property. public class ControlPropertyBindingDataContextChangeAttribute : DataContextChangeAttribute { public string PropertyName { get; set; } diff --git a/src/Framework/Framework/Binding/ControlPropertyTypeDataContextChangeAttribute.cs b/src/Framework/Framework/Binding/ControlPropertyTypeDataContextChangeAttribute.cs index 050ee0294a..f509c813ef 100644 --- a/src/Framework/Framework/Binding/ControlPropertyTypeDataContextChangeAttribute.cs +++ b/src/Framework/Framework/Binding/ControlPropertyTypeDataContextChangeAttribute.cs @@ -8,6 +8,7 @@ namespace DotVVM.Framework.Binding { + [Obsolete("Use ControlPropertyBindingDataContextChangeAttribute instead.")] public class ControlPropertyTypeDataContextChangeAttribute : DataContextChangeAttribute { public string PropertyName { get; set; } diff --git a/src/Framework/Framework/Binding/DelegateActionProperty.cs b/src/Framework/Framework/Binding/DelegateActionProperty.cs index 591d2d5feb..81c1e23c86 100644 --- a/src/Framework/Framework/Binding/DelegateActionProperty.cs +++ b/src/Framework/Framework/Binding/DelegateActionProperty.cs @@ -10,6 +10,7 @@ namespace DotVVM.Framework.Binding { + /// DotvvmProperty which calls the function passed in the Register method, when the assigned control is being rendered. public sealed class DelegateActionProperty: ActiveDotvvmProperty { private Action func; diff --git a/src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs b/src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs index 25b9abd82f..c5c922d403 100644 --- a/src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs +++ b/src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs @@ -22,6 +22,7 @@ public DotvvmBindingCacheHelper(IDotvvmCacheAdapter cache, BindingCompilationSer this.compilationService = compilationService; } + /// Created a new binding using the , unless an existing cache entry is found. Entries are identified using the identifier and keys. By default, the cache is LRU with size=1000 public T CreateCachedBinding(string identifier, object?[] keys, Func factory) where T: IBinding { return this.cache.GetOrAdd(new CacheKey(typeof(T), identifier, keys), _ => { @@ -31,6 +32,7 @@ public DotvvmBindingCacheHelper(IDotvvmCacheAdapter cache, BindingCompilationSer }); } + /// Created a new binding of type with the specified properties, unless an existing cache entry is found. Entries are identified using the identifier and keys. By default, the cache is LRU with size=1000 public T CreateCachedBinding(string identifier, object[] keys, object[] properties) where T: IBinding { return CreateCachedBinding(identifier, keys, () => (T)BindingFactory.CreateBinding(this.compilationService, typeof(T), properties)); diff --git a/src/Framework/Framework/Binding/Expressions/BindingExpression.cs b/src/Framework/Framework/Binding/Expressions/BindingExpression.cs index 513d20d756..6d4972d6a7 100644 --- a/src/Framework/Framework/Binding/Expressions/BindingExpression.cs +++ b/src/Framework/Framework/Binding/Expressions/BindingExpression.cs @@ -18,6 +18,9 @@ namespace DotVVM.Framework.Binding.Expressions { + /// Represents a data-binding in DotVVM. + /// This is a base class for all bindings, BindingExpression in general does not guarantee that the binding will have any property. + /// This class only contains the glue code which automatically calls resolvers and caches the results when is invoked. [BindingCompilationRequirements(optional: new[] { typeof(BindingResolverCollection) })] [Newtonsoft.Json.JsonConverter(typeof(BindingDebugJsonConverter))] public abstract class BindingExpression : IBinding, ICloneableBinding diff --git a/src/Framework/Framework/Binding/Expressions/IBinding.cs b/src/Framework/Framework/Binding/Expressions/IBinding.cs index 7a5753fe80..efefc57681 100644 --- a/src/Framework/Framework/Binding/Expressions/IBinding.cs +++ b/src/Framework/Framework/Binding/Expressions/IBinding.cs @@ -4,26 +4,33 @@ namespace DotVVM.Framework.Binding.Expressions { + /// Controls what happens when the binding property does not exist on this binding or when it's resolver throws an exception. public enum ErrorHandlingMode { + /// Returns null. The null is returned even in case when resolver throws an exception, you can't distinguish between the "property does not exist", "resolver failed" states using this mode. ReturnNull, + /// Throws the exception. Always throws . In case the property is missing, message = "resolver not found". Otherwise, the exception will have the resolver error as InnerException. ThrowException, + /// Behaves similarly to ThrowException, but the exception is returned instead of being thrown. This is useful when you'd catch the exception immediately to avoid annoying debugger by throwing too many exceptions. ReturnException } + + /// General interface which all DotVVM data binding types must implement. This interface does not provide any specific binding properties, only the basic building blocks - that bindings are composed of binding properties (), should have a DataContext and may have resolvers. public interface IBinding { + /// Gets the binding property identified by the type. Returned object will always be of type , null, or Exception (this depends on the ). This method should always return the same result and should run fast (may rely on caching, so first call might not be that fast). object? GetProperty(Type type, ErrorHandlingMode errorMode = ErrorHandlingMode.ThrowException); + /// If the binding expects a specific data context, this property should return it. "Normal" binding coming from dothtml markup won't return null since they always depend on the data context. DataContextStack? DataContext { get; } BindingResolverCollection? GetAdditionalResolvers(); - //IDictionary Properties { get; } - //IList AdditionalServices { get; } } public interface ICloneableBinding: IBinding { + /// Returns a list of all properties which are already cached. Creating a new binding with these properties will produce the same binding. IEnumerable GetAllComputedProperties(); } } diff --git a/src/Framework/Framework/Binding/Expressions/StaticCommandBindingExpression.cs b/src/Framework/Framework/Binding/Expressions/StaticCommandBindingExpression.cs index 30d1f9dcd1..11f1ec994b 100644 --- a/src/Framework/Framework/Binding/Expressions/StaticCommandBindingExpression.cs +++ b/src/Framework/Framework/Binding/Expressions/StaticCommandBindingExpression.cs @@ -10,6 +10,7 @@ namespace DotVVM.Framework.Binding.Expressions { + /// The `{staticCommand: ...}` binding. It is a command, so should be used to handle events, but compared to `command`, it runs primarily client-side. Compared to `value` binding, `staticCommand`s are expected to have side effects and run asynchronously (the binding will return a Promise or a Task). [BindingCompilationRequirements( required: new[] { typeof(StaticCommandOptionsLambdaJavascriptProperty), /*typeof(BindingDelegate)*/ } )] diff --git a/src/Framework/Framework/Binding/HelperNamespace/ListExtensions.cs b/src/Framework/Framework/Binding/HelperNamespace/ListExtensions.cs index 44597c1a70..b647ff25ef 100644 --- a/src/Framework/Framework/Binding/HelperNamespace/ListExtensions.cs +++ b/src/Framework/Framework/Binding/HelperNamespace/ListExtensions.cs @@ -8,6 +8,7 @@ namespace DotVVM.Framework.Binding.HelperNamespace { public static class ListExtensions { + /// Updates all entries identified by using the . If none match, the is appended to the list. public static void AddOrUpdate(this List list, T element, Func matcher, Func updater) { var found = false; @@ -24,6 +25,7 @@ public static void AddOrUpdate(this List list, T element, Func mat list.Add(element); } + /// Removes the first entry identified by . public static void RemoveFirst(this List list, Func predicate) { for (var index = 0; index < list.Count; index++) @@ -36,6 +38,7 @@ public static void RemoveFirst(this List list, Func predicate) } } + /// Removes the last entry identified by . public static void RemoveLast(this List list, Func predicate) { for (var index = list.Count - 1; index >= 0; index--) diff --git a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs index 323e740f89..c5f9de39dc 100644 --- a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs +++ b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs @@ -12,6 +12,7 @@ namespace DotVVM.Framework.Binding { + /// Represents a dictionary of values of . public readonly struct VirtualPropertyGroupDictionary : IDictionary, IReadOnlyDictionary { private readonly DotvvmBindableObject control; @@ -38,6 +39,7 @@ public IEnumerable Keys } } + /// Lists all values. If any of the properties contains a binding, it will be automatically evaluated. public IEnumerable Values { get @@ -106,6 +108,7 @@ public bool Any() ICollection IDictionary.Values => Values.ToList(); + /// Gets or sets value of property identified by . If the property contains a binding, the getter will automatically evaluate it. public TValue this[string key] { get @@ -122,8 +125,11 @@ public bool Any() } } + /// Gets the value binding set to a specified property. Returns null if the property is not a binding, throws if the binding some kind of command. public IValueBinding? GetValueBinding(string key) => control.GetValueBinding(group.GetDotvvmProperty(key)); + /// Gets the binding set to a specified property. Returns null if the property is not set or if the value is not a binding. public IBinding? GetBinding(string key) => control.GetBinding(group.GetDotvvmProperty(key)); + /// Gets the value or a binding object for a specified property. public object? GetValueRaw(string key) { var p = group.GetDotvvmProperty(key); @@ -133,12 +139,15 @@ public bool Any() return p.DefaultValue!; } + /// Adds value or overwrites the property identified by . public void Set(string key, ValueOrBinding value) { control.properties.Set(group.GetDotvvmProperty(key), value.UnwrapToObject()); } + /// Adds value or overwrites the property identified by with the value. public void Set(string key, TValue value) => control.properties.Set(group.GetDotvvmProperty(key), value); + /// Adds binding or overwrites the property identified by with the binding. public void SetBinding(string key, IBinding binding) => control.properties.Set(group.GetDotvvmProperty(key), binding); @@ -156,6 +165,7 @@ private void AddOnConflict(GroupedDotvvmProperty property, object? value) control.properties.Set(property, mergedValue); } + /// Adds the property identified by . If the property is already set, it tries appending the value using the group's public void Add(string key, ValueOrBinding value) { var prop = group.GetDotvvmProperty(key); @@ -163,9 +173,12 @@ public void Add(string key, ValueOrBinding value) if (!control.properties.TryAdd(prop, val)) AddOnConflict(prop, val); } + + /// Adds the property identified by . If the property is already set, it tries appending the value using the group's public void Add(string key, TValue value) => this.Add(key, new ValueOrBinding(value)); + /// Adds the property identified by . If the property is already set, it tries appending the value using the group's public void AddBinding(string key, IBinding? binding) { Add(key, new ValueOrBinding(binding!)); @@ -222,6 +235,7 @@ public bool Remove(string key) return control.Properties.Remove(group.GetDotvvmProperty(key)); } + /// Tries getting value of property identified by . If the property contains a binding, it will be automatically evaluated. #pragma warning disable CS8767 public bool TryGetValue(string key, [MaybeNullWhen(false)] out TValue value) #pragma warning restore CS8767 @@ -239,6 +253,7 @@ public bool TryGetValue(string key, [MaybeNullWhen(false)] out TValue value) } } + /// Adds the property-value pair to the dictionary. If the property is already set, it tries appending the value using the group's public void Add(KeyValuePair item) { Add(item.Key, item.Value); @@ -303,6 +318,7 @@ public bool Remove(KeyValuePair item) return false; } + /// Enumerates all keys and values. If a property contains a binding, the it will be automatically evaluated. public IEnumerator> GetEnumerator() { foreach (var (p, value) in control.properties) @@ -316,6 +332,7 @@ public bool Remove(KeyValuePair item) } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + /// Enumerates all keys and values, without evaluating the bindings. public IEnumerable> RawValues { get diff --git a/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs b/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs index 2b71472435..a655cadc35 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DataContextStack.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; @@ -17,9 +17,13 @@ namespace DotVVM.Framework.Compilation.ControlTree public sealed class DataContextStack : IDataContextStack { public DataContextStack? Parent { get; } + /// Type of `_this` public Type DataContextType { get; } + /// Namespaces imported by data context change attributes. public ImmutableArray NamespaceImports { get; } + /// Extension parameters added by data context change attributes (for example _index, _collection). public ImmutableArray ExtensionParameters { get; } + /// Extension property resolvers added by data context change attributes. public ImmutableArray BindingPropertyResolvers { get; } private readonly int hashCode; diff --git a/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs b/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs index 03d5868330..2fa03d4137 100644 --- a/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs +++ b/src/Framework/Framework/Compilation/ControlTree/DotvvmPropertyGroup.cs @@ -12,6 +12,7 @@ namespace DotVVM.Framework.Compilation.ControlTree { + /// A set of DotvvmProperties identified by a common prefix. For example RouteLink.Params-XX or html attributes are property groups. public class DotvvmPropertyGroup : IPropertyGroupDescriptor { public FieldInfo? DescriptorField { get; } From 7d74406249caa1afb2eba1819dae60b5e8dfd4be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20Luke=C5=A1?= Date: Wed, 1 Feb 2023 17:37:31 +0000 Subject: [PATCH 29/31] doccomments: Apply grammar fixes from @acizmarik's code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andrej Čižmárik --- src/Framework/Core/ViewModel/Direction.cs | 4 ++-- src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs | 4 ++-- src/Framework/Framework/Binding/Expressions/IBinding.cs | 2 +- .../Binding/Expressions/StaticCommandBindingExpression.cs | 2 +- .../Framework/Binding/VirtualPropertyGroupDictionary.cs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Framework/Core/ViewModel/Direction.cs b/src/Framework/Core/ViewModel/Direction.cs index f7e9ab041c..2f51f88980 100644 --- a/src/Framework/Core/ViewModel/Direction.cs +++ b/src/Framework/Core/ViewModel/Direction.cs @@ -5,9 +5,9 @@ namespace DotVVM.Framework.ViewModel { /// - /// Specifies on which requests should the property should serialized and sent. Default is Both. + /// Specifies on which requests should the property be serialized and sent. Default is Both. /// Set to None to disable serialization of the property. - /// This enums is flags, the directions can be arbitrarily combined. + /// This enumeration can be treated as flags, the directions can be arbitrarily combined. /// [Flags] public enum Direction diff --git a/src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs b/src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs index c5c922d403..cc11591f26 100644 --- a/src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs +++ b/src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs @@ -22,7 +22,7 @@ public DotvvmBindingCacheHelper(IDotvvmCacheAdapter cache, BindingCompilationSer this.compilationService = compilationService; } - /// Created a new binding using the , unless an existing cache entry is found. Entries are identified using the identifier and keys. By default, the cache is LRU with size=1000 + /// Creates a new binding using the , unless an existing cache entry is found. Entries are identified using the identifier and keys. By default, the cache is LRU with size=1000 public T CreateCachedBinding(string identifier, object?[] keys, Func factory) where T: IBinding { return this.cache.GetOrAdd(new CacheKey(typeof(T), identifier, keys), _ => { @@ -32,7 +32,7 @@ public DotvvmBindingCacheHelper(IDotvvmCacheAdapter cache, BindingCompilationSer }); } - /// Created a new binding of type with the specified properties, unless an existing cache entry is found. Entries are identified using the identifier and keys. By default, the cache is LRU with size=1000 + /// Creates a new binding of type with the specified properties, unless an existing cache entry is found. Entries are identified using the identifier and keys. By default, the cache is LRU with size=1000 public T CreateCachedBinding(string identifier, object[] keys, object[] properties) where T: IBinding { return CreateCachedBinding(identifier, keys, () => (T)BindingFactory.CreateBinding(this.compilationService, typeof(T), properties)); diff --git a/src/Framework/Framework/Binding/Expressions/IBinding.cs b/src/Framework/Framework/Binding/Expressions/IBinding.cs index efefc57681..64c9410954 100644 --- a/src/Framework/Framework/Binding/Expressions/IBinding.cs +++ b/src/Framework/Framework/Binding/Expressions/IBinding.cs @@ -4,7 +4,7 @@ namespace DotVVM.Framework.Binding.Expressions { - /// Controls what happens when the binding property does not exist on this binding or when it's resolver throws an exception. + /// Controls what happens when the binding property does not exist on this binding or when its resolver throws an exception. public enum ErrorHandlingMode { /// Returns null. The null is returned even in case when resolver throws an exception, you can't distinguish between the "property does not exist", "resolver failed" states using this mode. diff --git a/src/Framework/Framework/Binding/Expressions/StaticCommandBindingExpression.cs b/src/Framework/Framework/Binding/Expressions/StaticCommandBindingExpression.cs index 11f1ec994b..5e50a872c7 100644 --- a/src/Framework/Framework/Binding/Expressions/StaticCommandBindingExpression.cs +++ b/src/Framework/Framework/Binding/Expressions/StaticCommandBindingExpression.cs @@ -10,7 +10,7 @@ namespace DotVVM.Framework.Binding.Expressions { - /// The `{staticCommand: ...}` binding. It is a command, so should be used to handle events, but compared to `command`, it runs primarily client-side. Compared to `value` binding, `staticCommand`s are expected to have side effects and run asynchronously (the binding will return a Promise or a Task). + /// The `{staticCommand: ...}` binding. It is a command that runs primarily client-side and is well-suited to handle events. Compared to `value` binding, `staticCommand`s are expected to have side effects and run asynchronously (the binding will return a Promise or a Task). [BindingCompilationRequirements( required: new[] { typeof(StaticCommandOptionsLambdaJavascriptProperty), /*typeof(BindingDelegate)*/ } )] diff --git a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs index c5f9de39dc..8be168dd0e 100644 --- a/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs +++ b/src/Framework/Framework/Binding/VirtualPropertyGroupDictionary.cs @@ -318,7 +318,7 @@ public bool Remove(KeyValuePair item) return false; } - /// Enumerates all keys and values. If a property contains a binding, the it will be automatically evaluated. + /// Enumerates all keys and values. If a property contains a binding, it will be automatically evaluated. public IEnumerator> GetEnumerator() { foreach (var (p, value) in control.properties) From a263c47d8d51339073a4a57853a0c7728415b72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 1 Feb 2023 18:42:56 +0100 Subject: [PATCH 30/31] `const dotvvm` and `window.dotvvm` should have the same type --- src/Framework/Framework/Resources/Scripts/dotvvm-root.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts index 558e247ee8..6e3f98028c 100644 --- a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts +++ b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts @@ -136,11 +136,12 @@ if (compileConstants.debug) { declare global { interface DotvvmGlobalExtensions {} + type DotvvmGlobal = DotvvmGlobalExtensions & typeof dotvvmExports & { debug?: true, isSpaReady?: typeof isSpaReady, handleSpaNavigation?: typeof handleSpaNavigation } - const dotvvm: DotvvmGlobalExtensions & typeof dotvvmExports & {debug?: true, isSpaReady?: typeof isSpaReady, handleSpaNavigation?: typeof handleSpaNavigation}; + const dotvvm: DotvvmGlobal; interface Window { - dotvvm: DotvvmGlobalExtensions & typeof dotvvmExports + dotvvm: DotvvmGlobal } } From aa88b4ec9c4242f4a343964173af4aa8f5e5e2a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Fri, 3 Feb 2023 12:48:29 +0100 Subject: [PATCH 31/31] Fixed false-positive warning See: https://github.com/dotnet/roslyn-analyzers/issues/5828 for more info --- .../Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs b/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs index 3ba6fb3376..eb9c81c81b 100644 --- a/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs +++ b/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs @@ -15,7 +15,7 @@ public sealed class UnsupportedCallSiteAttributeAnalyzer : DiagnosticAnalyzer private const string unsupportedCallSiteAttributeMetadataName = "DotVVM.Framework.CodeAnalysis.UnsupportedCallSiteAttribute"; private const int callSiteTypeServerUnderlyingValue = 0; - public static DiagnosticDescriptor DoNotInvokeMethodFromUnsupportedCallSite = new( + public static DiagnosticDescriptor DoNotInvokeMethodFromUnsupportedCallSite = new DiagnosticDescriptor( DotvvmDiagnosticIds.DoNotInvokeMethodFromUnsupportedCallSiteRuleId, unsupportedCallSiteTitle, unsupportedCallSiteMessage,