diff --git a/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs b/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs new file mode 100644 index 0000000000..a96458816d --- /dev/null +++ b/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs @@ -0,0 +1,95 @@ +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.Framework.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", 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/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..eb9c81c81b --- /dev/null +++ b/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs @@ -0,0 +1,62 @@ +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.Framework.CodeAnalysis.UnsupportedCallSiteAttribute"; + private const int callSiteTypeServerUnderlyingValue = 0; + + public static DiagnosticDescriptor DoNotInvokeMethodFromUnsupportedCallSite = new DiagnosticDescriptor( + 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; + + var reason = (string?)attribute.ConstructorArguments.Skip(1).First().Value; + context.ReportDiagnostic( + Diagnostic.Create( + DoNotInvokeMethodFromUnsupportedCallSite, + invocation.Syntax.GetLocation(), + invocation.TargetMethod.Name, + (reason != null) ? $"due to: \"{reason}\"" : string.Empty)); + } + }, 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..dbe0226b70 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 {1}. + /// + 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..b2cc65bf2c 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 {1} + + + Unsupported call site + Fields are not supported in viewmodels. Use properties to save state of viewmodels instead. 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/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/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/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); 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/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 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 diff --git a/src/Framework/Core/CodeAnalysis/CallSiteType.cs b/src/Framework/Core/CodeAnalysis/CallSiteType.cs new file mode 100644 index 0000000000..1809e68202 --- /dev/null +++ b/src/Framework/Core/CodeAnalysis/CallSiteType.cs @@ -0,0 +1,8 @@ +namespace DotVVM.Framework.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..9555082a34 --- /dev/null +++ b/src/Framework/Core/CodeAnalysis/UnsupportedCallSiteAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace DotVVM.Framework.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false)] + public class UnsupportedCallSiteAttribute : Attribute + { + public readonly CallSiteType Type; + public readonly string? Reason; + + public UnsupportedCallSiteAttribute(CallSiteType type, string? reason = null) + { + Type = type; + Reason = reason; + } + } +} 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 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..2f51f88980 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 be serialized and sent. Default is Both. + /// Set to None to disable serialization of the property. + /// This enumeration can be treated as 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..cc11591f26 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; } + /// 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), _ => { @@ -31,6 +32,7 @@ public DotvvmBindingCacheHelper(IDotvvmCacheAdapter cache, BindingCompilationSer }); } + /// 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/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..64c9410954 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 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. 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..5e50a872c7 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 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/HelperNamespace/DateTimeExtensions.cs b/src/Framework/Framework/Binding/HelperNamespace/DateTimeExtensions.cs index e4b73be990..43fca030a9 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.Framework.CodeAnalysis; namespace DotVVM.Framework.Binding.HelperNamespace { @@ -7,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. /// - [Obsolete("When evaluated on the server, no conversion is made as we don't know the browser timezone.")] + [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. /// - [Obsolete("When evaluated on the server, no conversion is made as we don't know the browser timezone.")] + [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; } 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..8be168dd0e 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, 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/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 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; } 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/ControlTree/UnsupportedCallSiteCheckingVisitor.cs b/src/Framework/Framework/Compilation/ControlTree/UnsupportedCallSiteCheckingVisitor.cs new file mode 100644 index 0000000000..e9e8e1640d --- /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.Framework.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) + { + if (node.Method.IsDefined(typeof(UnsupportedCallSiteAttribute))) + { + 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); + } + + return base.VisitMethodCall(node); + } + } + + public override void VisitBinding(ResolvedBinding binding) + { + base.VisitBinding(binding); + if (binding.Binding is not ResourceBindingExpression and not CommandBindingExpression) + 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/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/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/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; 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/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) diff --git a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts index b9096d3777..6e3f98028c 100644 --- a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts +++ b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts @@ -132,11 +132,16 @@ if (compileConstants.debug) { (dotvvmExports as any).debug = true } + + declare global { - const dotvvm: typeof dotvvmExports & {debug?: true, isSpaReady?: typeof isSpaReady, handleSpaNavigation?: typeof handleSpaNavigation}; + interface DotvvmGlobalExtensions {} + type DotvvmGlobal = DotvvmGlobalExtensions & typeof dotvvmExports & { debug?: true, isSpaReady?: typeof isSpaReady, handleSpaNavigation?: typeof handleSpaNavigation } + + const dotvvm: DotvvmGlobal; interface Window { - dotvvm: typeof dotvvmExports + dotvvm: DotvvmGlobal } } 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 +} 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/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) { 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]")); + }); + } } } 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..c7ca399f42 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,15 +13,28 @@ 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) { - 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 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()) diff --git a/src/Tests/ControlTests/ServerSideStyleTests.cs b/src/Tests/ControlTests/ServerSideStyleTests.cs index 5ecb5fbc38..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(); }); } @@ -271,6 +275,58 @@ public async Task CapabilityReads() check.CheckString(r.FormattedHtml, fileExtension: "html"); } + [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" })) + .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 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.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 +
+ + + +
+ + + 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 +
+ + 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() { 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; } + } } }