diff --git a/.github/setup/action.yml b/.github/setup/action.yml index dfcacd9594..f7a284e7d9 100644 --- a/.github/setup/action.yml +++ b/.github/setup/action.yml @@ -37,6 +37,10 @@ runs: - if: ${{ runner.os == 'Windows' }} run: choco install dotnetcore-3.1-windowshosting -y shell: pwsh + + # install workloads + - run: dotnet workload install wasm-tools + shell: pwsh # restore packages - if: ${{ runner.os == 'Windows' }} diff --git a/.github/uitest/uitest.ps1 b/.github/uitest/uitest.ps1 index d54c0846f7..04a53bcd9c 100644 --- a/.github/uitest/uitest.ps1 +++ b/.github/uitest/uitest.ps1 @@ -216,6 +216,8 @@ try { "$testDir", ` "--configuration", ` "$config", ` + "--filter", ` + "Category!=aspnetcore-only", ` "--no-restore", ` "--logger", ` "trx;LogFileName=$TrxName", ` diff --git a/src/DotVVM.sln b/src/DotVVM.sln index 3542ef1634..dd4357ba49 100644 --- a/src/DotVVM.sln +++ b/src/DotVVM.sln @@ -123,6 +123,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Framework.Controls.D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Framework.Controls.DynamicData", "DynamicData\DynamicData\DotVVM.Framework.Controls.DynamicData.csproj", "{9E19A537-E1B2-4D1E-A904-D99D4222474F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotVVM.Samples.BasicSamples.CSharpClient", "Samples\CSharpClient\DotVVM.Samples.BasicSamples.CSharpClient.csproj", "{A51F80E0-DA25-476C-BBF5-FCF79E6C67D4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotVVM.Framework.Interop.DotnetWasm", "Framework\Interop.DotnetWasm\DotVVM.Framework.Interop.DotnetWasm.csproj", "{D8898963-091F-4398-AFC6-CDB4ED6A98A2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -697,6 +701,30 @@ Global {9E19A537-E1B2-4D1E-A904-D99D4222474F}.Release|x64.Build.0 = Release|Any CPU {9E19A537-E1B2-4D1E-A904-D99D4222474F}.Release|x86.ActiveCfg = Release|Any CPU {9E19A537-E1B2-4D1E-A904-D99D4222474F}.Release|x86.Build.0 = Release|Any CPU + {A51F80E0-DA25-476C-BBF5-FCF79E6C67D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A51F80E0-DA25-476C-BBF5-FCF79E6C67D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A51F80E0-DA25-476C-BBF5-FCF79E6C67D4}.Debug|x64.ActiveCfg = Debug|Any CPU + {A51F80E0-DA25-476C-BBF5-FCF79E6C67D4}.Debug|x64.Build.0 = Debug|Any CPU + {A51F80E0-DA25-476C-BBF5-FCF79E6C67D4}.Debug|x86.ActiveCfg = Debug|Any CPU + {A51F80E0-DA25-476C-BBF5-FCF79E6C67D4}.Debug|x86.Build.0 = Debug|Any CPU + {A51F80E0-DA25-476C-BBF5-FCF79E6C67D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A51F80E0-DA25-476C-BBF5-FCF79E6C67D4}.Release|Any CPU.Build.0 = Release|Any CPU + {A51F80E0-DA25-476C-BBF5-FCF79E6C67D4}.Release|x64.ActiveCfg = Release|Any CPU + {A51F80E0-DA25-476C-BBF5-FCF79E6C67D4}.Release|x64.Build.0 = Release|Any CPU + {A51F80E0-DA25-476C-BBF5-FCF79E6C67D4}.Release|x86.ActiveCfg = Release|Any CPU + {A51F80E0-DA25-476C-BBF5-FCF79E6C67D4}.Release|x86.Build.0 = Release|Any CPU + {D8898963-091F-4398-AFC6-CDB4ED6A98A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8898963-091F-4398-AFC6-CDB4ED6A98A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8898963-091F-4398-AFC6-CDB4ED6A98A2}.Debug|x64.ActiveCfg = Debug|Any CPU + {D8898963-091F-4398-AFC6-CDB4ED6A98A2}.Debug|x64.Build.0 = Debug|Any CPU + {D8898963-091F-4398-AFC6-CDB4ED6A98A2}.Debug|x86.ActiveCfg = Debug|Any CPU + {D8898963-091F-4398-AFC6-CDB4ED6A98A2}.Debug|x86.Build.0 = Debug|Any CPU + {D8898963-091F-4398-AFC6-CDB4ED6A98A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8898963-091F-4398-AFC6-CDB4ED6A98A2}.Release|Any CPU.Build.0 = Release|Any CPU + {D8898963-091F-4398-AFC6-CDB4ED6A98A2}.Release|x64.ActiveCfg = Release|Any CPU + {D8898963-091F-4398-AFC6-CDB4ED6A98A2}.Release|x64.Build.0 = Release|Any CPU + {D8898963-091F-4398-AFC6-CDB4ED6A98A2}.Release|x86.ActiveCfg = Release|Any CPU + {D8898963-091F-4398-AFC6-CDB4ED6A98A2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -753,6 +781,8 @@ Global {DB0AB0C3-DA5E-4B5A-9CD4-036D37B50AED} = {E57EE0B8-30FC-4702-B310-FB82C19D7473} {3209E1B1-88BB-4A95-B234-950E89EFCEE0} = {CF90322D-63BC-4047-BFEA-EE87E45020AF} {9E19A537-E1B2-4D1E-A904-D99D4222474F} = {CF90322D-63BC-4047-BFEA-EE87E45020AF} + {A51F80E0-DA25-476C-BBF5-FCF79E6C67D4} = {DC6E006E-EE9D-481D-B94C-8A53331BCBC1} + {D8898963-091F-4398-AFC6-CDB4ED6A98A2} = {F211156C-FEE6-464C-A7A7-317D16DD3D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {61F8A195-365E-47B1-A6F2-CD3534E918F8} diff --git a/src/Framework/Framework/Binding/ViewModuleReferenceInfo.cs b/src/Framework/Framework/Binding/ViewModuleReferenceInfo.cs index 924f37f743..9129fef56b 100644 --- a/src/Framework/Framework/Binding/ViewModuleReferenceInfo.cs +++ b/src/Framework/Framework/Binding/ViewModuleReferenceInfo.cs @@ -17,14 +17,15 @@ namespace DotVVM.Framework.Binding [HandleAsImmutableObjectInDotvvmProperty] public sealed class ViewModuleReferenceInfo { - public string[] ReferencedModules { get; } + public ViewModuleReferencedModule[] ReferencedModules { get; } + /// The modules are referenced under an Id to the dotvvm client-side runtime. The same ID must be used in the invocation from the _js literal. public string ViewId { get; } /// Whether control id should be used instead of ViewId to identify the modules. public bool IsMarkupControl { get; } - public ViewModuleReferenceInfo(string viewId, string[] referencedModules, bool isMarkupControl) + public ViewModuleReferenceInfo(string viewId, ViewModuleReferencedModule[] referencedModules, bool isMarkupControl) { this.ViewId = viewId; this.IsMarkupControl = isMarkupControl; @@ -46,7 +47,7 @@ public ViewModuleReferenceInfo(string viewId, string[] referencedModules, bool i internal (ViewModuleImportResource importResource, ViewModuleInitResource initResource) BuildResources(IDotvvmResourceRepository allResources) { var dependencies = ReferencedModules.SelectMany((moduleResourceName, index) => { - var moduleResource = allResources.FindResource(moduleResourceName); + var moduleResource = allResources.FindResource(moduleResourceName.ModuleName); if (moduleResource is null) throw new Exception($"Cannot find resource named '{moduleResourceName}' referenced by the @js directive!"); if (!(moduleResource is ScriptModuleResource)) @@ -63,8 +64,10 @@ public ViewModuleReferenceInfo(string viewId, string[] referencedModules, bool i private string GenerateModuleBatchUniqueId() { using var sha = SHA256.Create(); - return Convert.ToBase64String(sha.ComputeHash(Encoding.Unicode.GetBytes(string.Join("\0", this.ReferencedModules)))) + return Convert.ToBase64String(sha.ComputeHash(Encoding.Unicode.GetBytes(string.Join("\0", this.ReferencedModules.Select(r => r.ModuleName + "\0" + (r.InitArguments != null ? string.Join("\0", r.InitArguments) : "")))))) .Replace("/", "_").Replace("+", "-").Replace("=", ""); } } + + public record ViewModuleReferencedModule(string ModuleName, string[]? InitArguments = null); } diff --git a/src/Framework/Framework/Compilation/Binding/DotnetViewModuleMethodTranslator.cs b/src/Framework/Framework/Compilation/Binding/DotnetViewModuleMethodTranslator.cs new file mode 100644 index 0000000000..cb237247a3 --- /dev/null +++ b/src/Framework/Framework/Compilation/Binding/DotnetViewModuleMethodTranslator.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Transactions; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Compilation.ControlTree; +using DotVVM.Framework.Compilation.Javascript; +using DotVVM.Framework.Compilation.Javascript.Ast; +using DotVVM.Framework.Utils; + +namespace DotVVM.Framework.Compilation.Binding +{ + public class DotnetViewModuleMethodTranslator : IJavascriptMethodTranslator + { + public JsExpression? TryTranslateCall(LazyTranslatedExpression? context, LazyTranslatedExpression[] arguments, MethodInfo method) + { + // ignore static methods + if (context == null) + { + return null; + } + + // check whether we have the annotation - otherwise the type is not used in the _dotnet context and will not be translated + var annotation = context.OriginalExpression.GetParameterAnnotation(); + if (annotation is null || annotation.ExtensionParameter is not DotnetExtensionParameter extensionParameter) + { + return null; + } + + // check that the method is callable + if (!method.IsPublic) + { + throw new DotvvmCompilationException($"Cannot call non-public method {method.DeclaringType!.FullName}.{method.Name} on a @dotnet module!"); + } + if (method.IsAbstract) + { + throw new DotvvmCompilationException($"Cannot call abstract method {method.DeclaringType!.FullName}.{method.Name} on a @dotnet module!"); + } + if (method.IsGenericMethod || method.IsGenericMethodDefinition) + { + throw new DotvvmCompilationException($"Cannot call generic method {method.DeclaringType!.FullName}.{method.Name} on a @dotnet module!"); + } + + // check that there are not more overloads + var allOverloads = context.NotNull().OriginalExpression.Type + .GetMethods() + .Where(m => m.Name == method.Name && m.IsPublic); + if (allOverloads.Count() > 1) + { + throw new DotvvmCompilationException($"There are multiple methods named {method.Name} on a @dotnet module {context.OriginalExpression.Type}! Overloads are not supported on @dotnet modules."); + } + + // translate the method + var viewIdOrElementExpr = extensionParameter.IsMarkupControl ? new JsSymbolicParameter(JavascriptTranslator.CurrentElementParameter) : (JsExpression)new JsLiteral(extensionParameter.Id); + + return new JsIdentifierExpression("dotvvm").Member("viewModules").Member("call") + .Invoke( + viewIdOrElementExpr, + new JsLiteral("dotnetWasmInvoke"), + new JsArrayExpression( + new[] { new JsLiteral(method.Name) } + .Concat(arguments.Select(a => a.JsExpression())) + .ToArray()), + new JsLiteral(true) + ) + .WithAnnotation(new ResultIsPromiseAnnotation(e => e)); + } + + } +} diff --git a/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs b/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs index 87fd7619c7..a35d642094 100644 --- a/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs +++ b/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs @@ -211,6 +211,52 @@ public ViewModuleAnnotation(string id, bool isMarkupControl) } } + public class DotnetExtensionParameter : BindingExtensionParameter + { + public string Id { get; } + public bool IsMarkupControl { get; } + public ITypeDescriptor Type { get; } + + public DotnetExtensionParameter(string id, bool isMarkupControl, ITypeDescriptor type) : base("_dotnet", type, true) + { + this.Id = id; + this.IsMarkupControl = isMarkupControl; + this.Type = type; + } + + public override Expression GetServerEquivalent(Expression controlParameter) + { + var type = ResolvedTypeDescriptor.ToSystemType(this.Type); + var constructors = type.GetConstructors(BindingFlags.Public); + if (constructors.Length != 1 || constructors[0].GetParameters().Length != 1) + { + throw new DotvvmCompilationException($"The type {type} referenced in the @dotnet directive must have exactly one public constructor with one parameter of IViewModuleContext!"); + } + // TODO: check parameter type + return Expression.New(constructors[0], Expression.Constant(null, typeof(object))) + .AddParameterAnnotation(new BindingParameterAnnotation(extensionParameter: this)); + } + + public override JsExpression GetJsTranslation(JsExpression dataContext) + { + return new JsIdentifierExpression("dotvvm").Member("viewModules") + .WithAnnotation(new ViewModuleAnnotation(Id, IsMarkupControl, ResolvedTypeDescriptor.ToSystemType(this.Type))); + } + + public class ViewModuleAnnotation + { + public ViewModuleAnnotation(string id, bool isMarkupControl, Type type) + { + Id = id; + IsMarkupControl = isMarkupControl; + Type = type; + } + public string Id { get; } + public bool IsMarkupControl { get; } + public Type Type { get; } + } + } + public class CurrentUserExtensionParameter : BindingExtensionParameter { public CurrentUserExtensionParameter() : base("_user", new ResolvedTypeDescriptor(typeof(ClaimsPrincipal)), true) diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs index aa9d9cae7a..e789e23e15 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs @@ -56,16 +56,18 @@ public virtual IAbstractTreeRoot ResolveTree(DothtmlRootNode root, string fileNa new BindingPageInfoExtensionParameter(), new BindingApiExtensionParameter() }.Concat(directiveMetadata.InjectedServices) - .Concat(directiveMetadata.ViewModuleResult is null ? new BindingExtensionParameter[0] : new[] { directiveMetadata.ViewModuleResult.ExtensionParameter }).ToArray()); + .Concat(directiveMetadata.ViewModuleResult is null ? new BindingExtensionParameter[0] : new[] { directiveMetadata.ViewModuleResult.ExtensionParameter }) + .Concat(directiveMetadata.DotnetViewModuleResult is null ? new BindingExtensionParameter[0] : new[] { directiveMetadata.DotnetViewModuleResult.ExtensionParameter }).ToArray()); var view = treeBuilder.BuildTreeRoot(this, viewMetadata, root, dataContextTypeStack, directiveMetadata.Directives, directiveMetadata.MasterPage); view.FileName = fileName; - if (directiveMetadata.ViewModuleResult is { }) + if (directiveMetadata.ViewModuleResult is { } || directiveMetadata.DotnetViewModuleResult is { }) { + var reference = BuildViewModuleReferenceInfo(directiveMetadata); treeBuilder.AddProperty( view, - treeBuilder.BuildPropertyValue(Internal.ReferencedViewModuleInfoProperty, directiveMetadata.ViewModuleResult.Reference, null), + treeBuilder.BuildPropertyValue(Internal.ReferencedViewModuleInfoProperty, reference, null), out _ ); } @@ -73,7 +75,21 @@ out _ ResolveRootContent(root, view, viewMetadata); return view; - } + } + + private static ViewModuleReferenceInfo BuildViewModuleReferenceInfo(MarkupPageMetadata directiveMetadata) + { + var firstReference = directiveMetadata.ViewModuleResult?.Reference ?? directiveMetadata.DotnetViewModuleResult?.Reference; + var reference = new ViewModuleReferenceInfo( + firstReference!.ViewId, + Enumerable.Concat( + directiveMetadata.ViewModuleResult?.Reference.ReferencedModules ?? Enumerable.Empty(), + directiveMetadata.DotnetViewModuleResult?.Reference.ReferencedModules ?? Enumerable.Empty() + ).ToArray(), + firstReference.IsMarkupControl + ); + return reference; + } /// /// Resolves the content of the root node. diff --git a/src/Framework/Framework/Compilation/ControlTree/IAbstractDotnetViewModuleDirective.cs b/src/Framework/Framework/Compilation/ControlTree/IAbstractDotnetViewModuleDirective.cs new file mode 100644 index 0000000000..920d5f6ff3 --- /dev/null +++ b/src/Framework/Framework/Compilation/ControlTree/IAbstractDotnetViewModuleDirective.cs @@ -0,0 +1,7 @@ +namespace DotVVM.Framework.Compilation.ControlTree; + +public interface IAbstractDotnetViewModuleDirective : IAbstractDirective +{ + /// Full type name of the module specified + ITypeDescriptor? ModuleType { get; } +} diff --git a/src/Framework/Framework/Compilation/ControlTree/IAbstractTreeBuilder.cs b/src/Framework/Framework/Compilation/ControlTree/IAbstractTreeBuilder.cs index af6509e636..abb366ffa2 100644 --- a/src/Framework/Framework/Compilation/ControlTree/IAbstractTreeBuilder.cs +++ b/src/Framework/Framework/Compilation/ControlTree/IAbstractTreeBuilder.cs @@ -28,6 +28,7 @@ public interface IAbstractTreeBuilder IAbstractBaseTypeDirective BuildBaseTypeDirective(DothtmlDirectiveNode directive, BindingParserNode nameSyntax, ImmutableList imports); IAbstractViewModuleDirective BuildViewModuleDirective(DothtmlDirectiveNode directiveNode, string modulePath, string resourceName); + IAbstractDotnetViewModuleDirective BuildDotnetViewModuleDirective(DothtmlDirectiveNode directiveNode, BindingParserNode moduleType, ImmutableList imports); IAbstractPropertyDeclarationDirective BuildPropertyDeclarationDirective(DothtmlDirectiveNode directive, TypeReferenceBindingParserNode typeSyntax, SimpleNameBindingParserNode nameSyntax, BindingParserNode? initializer, IList resolvedAttributes, BindingParserNode valueSyntaxRoot, ImmutableList imports); IAbstractDirectiveAttributeReference BuildPropertyDeclarationAttributeReference(DothtmlDirectiveNode directiveNode, IdentifierNameBindingParserNode propertyNameSyntax, ActualTypeReferenceBindingParserNode typeSyntax, LiteralExpressionBindingParserNode initializer, ImmutableList imports); IAbstractPropertyBinding BuildPropertyBinding(IPropertyDescriptor property, IAbstractBinding binding, DothtmlAttributeNode? sourceAttributeNode); diff --git a/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedDotnetViewModuleDirective.cs b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedDotnetViewModuleDirective.cs new file mode 100644 index 0000000000..68e2098b1b --- /dev/null +++ b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedDotnetViewModuleDirective.cs @@ -0,0 +1,18 @@ +using System.Collections.Immutable; +using DotVVM.Framework.Compilation.Parser.Binding.Parser; +using DotVVM.Framework.Compilation.Parser.Dothtml.Parser; + +namespace DotVVM.Framework.Compilation.ControlTree.Resolved; + +/// Represents the @dotnet directive - import .NET WASM module on the client side +public class ResolvedDotnetViewModuleDirective : ResolvedDirective, IAbstractDotnetViewModuleDirective +{ + /// Full .NET type of the module + public ITypeDescriptor? ModuleType { get; } + + public ResolvedDotnetViewModuleDirective(DirectiveCompilationService directiveCompilationService, DothtmlDirectiveNode node, BindingParserNode typeName, ImmutableList imports) + : base(node) + { + ModuleType = directiveCompilationService.ResolveType(node, typeName, imports); + } +} diff --git a/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedTreeBuilder.cs b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedTreeBuilder.cs index e0ea0beadf..6ae53bbe83 100644 --- a/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedTreeBuilder.cs +++ b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedTreeBuilder.cs @@ -97,6 +97,9 @@ public IAbstractBaseTypeDirective BuildBaseTypeDirective(DothtmlDirectiveNode di public IAbstractViewModuleDirective BuildViewModuleDirective(DothtmlDirectiveNode directiveNode, string modulePath, string resourceName) => new ResolvedViewModuleDirective(directiveNode, modulePath, resourceName); + public IAbstractDotnetViewModuleDirective BuildDotnetViewModuleDirective(DothtmlDirectiveNode directiveNode, BindingParserNode typeName, ImmutableList imports) => + new ResolvedDotnetViewModuleDirective(directiveService, directiveNode, typeName, imports); + public IAbstractPropertyDeclarationDirective BuildPropertyDeclarationDirective( DothtmlDirectiveNode directive, TypeReferenceBindingParserNode typeSyntax, diff --git a/src/Framework/Framework/Compilation/Directives/DotnetViewModuleCompilationResult.cs b/src/Framework/Framework/Compilation/Directives/DotnetViewModuleCompilationResult.cs new file mode 100644 index 0000000000..68c417de80 --- /dev/null +++ b/src/Framework/Framework/Compilation/Directives/DotnetViewModuleCompilationResult.cs @@ -0,0 +1,6 @@ +using DotVVM.Framework.Binding; +using DotVVM.Framework.Compilation.ControlTree; + +namespace DotVVM.Framework.Compilation.Directives; + +public record DotnetViewModuleCompilationResult(DotnetExtensionParameter ExtensionParameter, ViewModuleReferenceInfo Reference); diff --git a/src/Framework/Framework/Compilation/Directives/DotnetViewModuleDirectiveCompiler.cs b/src/Framework/Framework/Compilation/Directives/DotnetViewModuleDirectiveCompiler.cs new file mode 100644 index 0000000000..4496997c2c --- /dev/null +++ b/src/Framework/Framework/Compilation/Directives/DotnetViewModuleDirectiveCompiler.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Compilation.ControlTree; +using DotVVM.Framework.Compilation.ControlTree.Resolved; +using DotVVM.Framework.Compilation.Parser; +using DotVVM.Framework.Compilation.Parser.Dothtml.Parser; +using DotVVM.Framework.Compilation.ViewCompiler; +using DotVVM.Framework.ResourceManagement; + +namespace DotVVM.Framework.Compilation.Directives; + +public class DotnetViewModuleDirectiveCompiler : DirectiveCompiler +{ + private readonly IAbstractControlBuilderDescriptor? masterPage; + private readonly bool isMarkupControl; + private readonly ImmutableList imports; + + public DotnetViewModuleDirectiveCompiler(IReadOnlyDictionary> directiveNodesByName, IAbstractTreeBuilder treeBuilder, IAbstractControlBuilderDescriptor? masterPage, bool isMarkupControl, ImmutableList imports) + : base(directiveNodesByName, treeBuilder) + { + this.masterPage = masterPage; + this.isMarkupControl = isMarkupControl; + this.imports = imports; + } + + public override string DirectiveName => ParserConstants.CsharpViewModuleDirective; + + protected override DotnetViewModuleCompilationResult? CreateArtefact(IReadOnlyList resolvedDirectives) + { + var id = AssignViewModuleId(masterPage); + return ResolveImportedViewModules(resolvedDirectives, id); + } + + private DotnetViewModuleCompilationResult? ResolveImportedViewModules(IReadOnlyList moduleDirectives, string id) + { + if (moduleDirectives.Count == 0) + { + return null; + } + + if (moduleDirectives.Count > 1) + { + moduleDirectives[1].DothtmlNode!.AddError("There can be only one @dotnet directive in the page!"); + return null; + } + + var x = moduleDirectives[0]; + if (x.ModuleType == null) + { + moduleDirectives[0].DothtmlNode!.AddError($"The type {moduleDirectives[0].Value} was not found. Make sure the namespace and assembly name is correct."); + return null; + } + + var info = new ViewModuleReferenceInfo( + id, + new[] { new ViewModuleReferencedModule(ResourceConstants.DotvvmDotnetWasmInteropResourceName, new[] { x.ModuleType.FullName + ", "+ x.ModuleType.Assembly }) }, + isMarkupControl); + + return new DotnetViewModuleCompilationResult(new DotnetExtensionParameter(id, isMarkupControl, moduleDirectives[0].ModuleType!), info); + } + + protected virtual string AssignViewModuleId(IAbstractControlBuilderDescriptor? masterPage) + { + var numberOfMasterPages = 0; + while (masterPage != null) + { + masterPage = masterPage.MasterPage; + numberOfMasterPages += 1; + } + return "p" + numberOfMasterPages; + } + + protected override IAbstractDotnetViewModuleDirective Resolve(DothtmlDirectiveNode directiveNode) => + TreeBuilder.BuildDotnetViewModuleDirective(directiveNode, ParseDirective(directiveNode, p => p.ReadDirectiveTypeName()), imports); +} diff --git a/src/Framework/Framework/Compilation/Directives/MarkupDirectiveCompilerPipeline.cs b/src/Framework/Framework/Compilation/Directives/MarkupDirectiveCompilerPipeline.cs index 2475e84115..92b044dfc3 100644 --- a/src/Framework/Framework/Compilation/Directives/MarkupDirectiveCompilerPipeline.cs +++ b/src/Framework/Framework/Compilation/Directives/MarkupDirectiveCompilerPipeline.cs @@ -62,6 +62,15 @@ public MarkupPageMetadata Compile(DothtmlRootNode dothtmlRoot, string fileName) var viewModuleResult = viewModuleDirectiveCompiler.Compile(); resolvedDirectives.AddIfAny(viewModuleDirectiveCompiler.DirectiveName, viewModuleResult.Directives); + var dotnetViewModuleDirectiveCompiler = new DotnetViewModuleDirectiveCompiler( + directivesByName, + treeBuilder, + masterPage, + !baseType.IsEqualTo(ResolvedTypeDescriptor.Create(typeof(DotvvmView))), + imports); + var dotnetViewModuleResult = dotnetViewModuleDirectiveCompiler.Compile(); + resolvedDirectives.AddIfAny(dotnetViewModuleDirectiveCompiler.DirectiveName, dotnetViewModuleResult.Directives); + var propertyDirectiveCompiler = new PropertyDeclarationDirectiveCompiler(directivesByName, treeBuilder, baseType, imports); var propertyResult = propertyDirectiveCompiler.Compile(); resolvedDirectives.AddIfAny(propertyDirectiveCompiler.DirectiveName, propertyResult.Directives); @@ -84,6 +93,7 @@ public MarkupPageMetadata Compile(DothtmlRootNode dothtmlRoot, string fileName) baseType, viewModelType.TypeDescriptor, viewModuleResult.Artefact, + dotnetViewModuleResult.Artefact, propertyResult.Artefact); } } diff --git a/src/Framework/Framework/Compilation/Directives/MarkupPageMetadata.cs b/src/Framework/Framework/Compilation/Directives/MarkupPageMetadata.cs index 1908e89a10..16ca1aff70 100644 --- a/src/Framework/Framework/Compilation/Directives/MarkupPageMetadata.cs +++ b/src/Framework/Framework/Compilation/Directives/MarkupPageMetadata.cs @@ -14,5 +14,6 @@ public record MarkupPageMetadata( ITypeDescriptor BaseType, ITypeDescriptor? ViewModelType, ViewModuleCompilationResult? ViewModuleResult, + DotnetViewModuleCompilationResult? DotnetViewModuleResult, ImmutableList Properties); } diff --git a/src/Framework/Framework/Compilation/Directives/ViewModuleDirectiveCompiler.cs b/src/Framework/Framework/Compilation/Directives/ViewModuleDirectiveCompiler.cs index fa642a9a42..115cd00259 100644 --- a/src/Framework/Framework/Compilation/Directives/ViewModuleDirectiveCompiler.cs +++ b/src/Framework/Framework/Compilation/Directives/ViewModuleDirectiveCompiler.cs @@ -23,7 +23,7 @@ public ViewModuleDirectiveCompiler(IReadOnlyDictionary ParserConstants.ViewModuleDirective; + public override string DirectiveName => ParserConstants.JsViewModuleDirective; protected override ViewModuleCompilationResult? CreateArtefact(IReadOnlyList resolvedDirectives) { @@ -54,7 +54,7 @@ public ViewModuleDirectiveCompiler(IReadOnlyDictionary TreeBuilder.BuildViewModuleDirective(directiveNode, modulePath: directiveNode.Value, resourceName: directiveNode.Value); -} - + } } diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslator.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslator.cs index ab4145bb40..6df29a8ddb 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptTranslator.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptTranslator.cs @@ -8,6 +8,7 @@ using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Binding.HelperNamespace; +using DotVVM.Framework.Compilation.Binding; using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Compilation.Javascript.Ast; using DotVVM.Framework.Controls; @@ -248,6 +249,7 @@ public JavascriptTranslatorConfiguration() { Translators.Add(MethodCollection = new JavascriptTranslatableMethodCollection()); Translators.Add(new DelegateInvokeMethodTranslator()); + Translators.Add(new DotnetViewModuleMethodTranslator()); } public JsExpression? TryTranslateCall(LazyTranslatedExpression? context, LazyTranslatedExpression[] arguments, MethodInfo method) => diff --git a/src/Framework/Framework/Compilation/Parser/ParserConstants.cs b/src/Framework/Framework/Compilation/Parser/ParserConstants.cs index 145f5d54f4..91f23f9ce8 100644 --- a/src/Framework/Framework/Compilation/Parser/ParserConstants.cs +++ b/src/Framework/Framework/Compilation/Parser/ParserConstants.cs @@ -11,7 +11,8 @@ public class ParserConstants public const string WrapperTagNameDirective = "wrapperTag"; public const string NoWrapperTagNameDirective = "noWrapperTag"; public const string ServiceInjectDirective = "service"; - public const string ViewModuleDirective = "js"; + public const string JsViewModuleDirective = "js"; + public const string CsharpViewModuleDirective = "dotnet"; public const string ValueBinding = "value"; public const string CommandBinding = "command"; diff --git a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs index a3ca0cf583..2c803fec79 100644 --- a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs @@ -385,6 +385,13 @@ private static void RegisterResources(DotvvmConfiguration configuration) typeof(DotvvmConfiguration).Assembly, "DotVVM.Framework.Resources.Styles.DotVVM.Internal.css")); + configuration.Resources.RegisterScript(ResourceConstants.DotvvmDotnetWasmInteropResourceName, + new EmbeddedResourceLocation( + typeof(DotvvmConfiguration).Assembly, + "DotVVM.Framework.obj.javascript.dotvvmStaticResources.dotnetWasmViewModule.js", + debugName: "DotVVM.Framework.obj.javascript.dotvvmStaticResources.dotnetWasmViewModule.js"), + module: true); + RegisterGlobalizeResources(configuration); } diff --git a/src/Framework/Framework/Controls/DotvvmMarkupControl.cs b/src/Framework/Framework/Controls/DotvvmMarkupControl.cs index a03388ce18..72be03e492 100644 --- a/src/Framework/Framework/Controls/DotvvmMarkupControl.cs +++ b/src/Framework/Framework/Controls/DotvvmMarkupControl.cs @@ -126,7 +126,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest { var settings = DefaultSerializerSettingsProvider.Instance.GetSettingsCopy(); settings.StringEscapeHandling = StringEscapeHandling.EscapeHtml; - var binding = $"{{ modules: {JsonConvert.SerializeObject(viewModule.ReferencedModules, settings)} }}"; + var binding = JsonConvert.SerializeObject(viewModule.ReferencedModules.Select(m => new { module = m.ModuleName, args = m.InitArguments }), settings); if (RendersHtmlTag) writer.AddKnockoutDataBind("dotvvm-with-view-modules", binding); else diff --git a/src/Framework/Framework/Controls/Internal.cs b/src/Framework/Framework/Controls/Internal.cs index 3dcdde8f8d..9c6f7170ad 100644 --- a/src/Framework/Framework/Controls/Internal.cs +++ b/src/Framework/Framework/Controls/Internal.cs @@ -61,7 +61,7 @@ public class Internal public static DotvvmProperty ReferencedViewModuleInfoProperty = DotvvmProperty.Register(() => ReferencedViewModuleInfoProperty); - + public static DotvvmProperty UsedPropertiesInfoProperty = DotvvmProperty.Register(() => UsedPropertiesInfoProperty); diff --git a/src/Framework/Framework/Controls/NamedCommand.cs b/src/Framework/Framework/Controls/NamedCommand.cs index a95d464c18..0d7ef92113 100644 --- a/src/Framework/Framework/Controls/NamedCommand.cs +++ b/src/Framework/Framework/Controls/NamedCommand.cs @@ -77,7 +77,7 @@ public static IEnumerable ValidateUsage(ResolvedControl contr { if (!control.TreeRoot.TryGetProperty(Internal.ReferencedViewModuleInfoProperty, out var _)) { - yield return new ControlUsageError("The NamedCommand control can be used only in pages or controls that have the @js directive."); + yield return new ControlUsageError("The NamedCommand control can be used only in pages or controls that have the @js or @csharp directive."); } } diff --git a/src/Framework/Framework/DotVVM.Framework.csproj b/src/Framework/Framework/DotVVM.Framework.csproj index 6b0bd5a75c..7bc66f08d6 100644 --- a/src/Framework/Framework/DotVVM.Framework.csproj +++ b/src/Framework/Framework/DotVVM.Framework.csproj @@ -30,6 +30,7 @@ + @@ -120,6 +121,10 @@ + + + + diff --git a/src/Framework/Framework/ResourceManagement/ResourceConstants.cs b/src/Framework/Framework/ResourceManagement/ResourceConstants.cs index bc8761a801..8519aedb84 100644 --- a/src/Framework/Framework/ResourceManagement/ResourceConstants.cs +++ b/src/Framework/Framework/ResourceManagement/ResourceConstants.cs @@ -18,5 +18,6 @@ public class ResourceConstants public const string DotvvmFileUploadCssResourceName = "dotvvm.fileUpload-css"; public const string DotvvmInternalCssResourceName = "dotvvm.internal-css"; + public const string DotvvmDotnetWasmInteropResourceName = "dotvvm.interop.dotnet-wasm"; } } diff --git a/src/Framework/Framework/ResourceManagement/ViewModuleImportResource.cs b/src/Framework/Framework/ResourceManagement/ViewModuleImportResource.cs index 6e2235e696..12b14f18f1 100644 --- a/src/Framework/Framework/ResourceManagement/ViewModuleImportResource.cs +++ b/src/Framework/Framework/ResourceManagement/ViewModuleImportResource.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using DotVVM.Framework.Binding; using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; using Newtonsoft.Json; @@ -15,7 +16,7 @@ public class ViewModuleImportResource : IResource { public ResourceRenderPosition RenderPosition => ResourceRenderPosition.Anywhere; - public string[] ReferencedModules { get; } + public ViewModuleReferencedModule[] ReferencedModules { get; } public string[] Dependencies { get; } @@ -23,13 +24,13 @@ public class ViewModuleImportResource : IResource private string registrationScript; - public ViewModuleImportResource(string[] referencedModules, string name, string[] dependencies) + public ViewModuleImportResource(ViewModuleReferencedModule[] referencedModules, string name, string[] dependencies) { - this.ReferencedModules = referencedModules.ToArray(); + this.ReferencedModules = referencedModules; this.ResourceName = name; this.Dependencies = new string[] { "dotvvm" }.Concat(dependencies).ToArray(); - this.registrationScript = $"dotvvm.viewModules.registerMany({{{string.Join(", ", this.ReferencedModules.Select((m, i) => JsonConvert.ToString(m, '\'', StringEscapeHandling.EscapeHtml) + ": m" + i))}}});"; + this.registrationScript = $"dotvvm.viewModules.registerMany({{{string.Join(", ", this.ReferencedModules.Select((m, i) => JsonConvert.ToString(m.ModuleName, '\'', StringEscapeHandling.EscapeHtml) + ": m" + i))}}});"; } public void Render(IHtmlWriter writer, IDotvvmRequestContext context, string resourceName) @@ -39,13 +40,13 @@ public void Render(IHtmlWriter writer, IDotvvmRequestContext context, string res int i = 0; foreach (var r in this.ReferencedModules) { - var resource = context.ResourceManager.FindResource(r); + var resource = context.ResourceManager.FindResource(r.ModuleName); if (resource is null) throw new Exception($"Resource {r} does not exist."); if (!(resource is ILinkResource linkResource)) throw new Exception($"Resource {r} is not a LinkResource."); - var location = linkResource.GetLocations().FirstOrDefault()?.GetUrl(context, r); + var location = linkResource.GetLocations().FirstOrDefault()?.GetUrl(context, r.ModuleName); if (location is null) throw new Exception($"Could not get location of resource {r}"); diff --git a/src/Framework/Framework/ResourceManagement/ViewModuleInitResource.cs b/src/Framework/Framework/ResourceManagement/ViewModuleInitResource.cs index bcb2965f2b..efe54c04d4 100644 --- a/src/Framework/Framework/ResourceManagement/ViewModuleInitResource.cs +++ b/src/Framework/Framework/ResourceManagement/ViewModuleInitResource.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using DotVVM.Framework.Binding; using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; using Newtonsoft.Json; @@ -15,21 +16,34 @@ public class ViewModuleInitResource : IResource public ResourceRenderPosition RenderPosition => ResourceRenderPosition.Anywhere; - public string[] ReferencedModules { get; } + public ViewModuleReferencedModule[] ReferencedModules { get; } public string[] Dependencies { get; } public string ResourceName { get; } - private string registrationScript; + private readonly string registrationScript; - public ViewModuleInitResource(string[] referencedModules, string name, string viewId, string[] dependencies) + public ViewModuleInitResource(ViewModuleReferencedModule[] referencedModules, string name, string viewId, string[] dependencies) { this.ResourceName = name; this.ReferencedModules = referencedModules.ToArray(); this.Dependencies = dependencies; - this.registrationScript = string.Join("\r\n", this.ReferencedModules.Select(m => $"dotvvm.viewModules.init({KnockoutHelper.MakeStringLiteral(m)}, {KnockoutHelper.MakeStringLiteral(viewId)}, document.body);")); + this.registrationScript = string.Join("\r\n", this.ReferencedModules.Select(m => + { + var args = new List() + { + KnockoutHelper.MakeStringLiteral(m.ModuleName), + KnockoutHelper.MakeStringLiteral(viewId), + "document.body" + }; + if (m.InitArguments != null) + { + args.Add($"[{string.Join(", ", m.InitArguments.Select(a => KnockoutHelper.MakeStringLiteral(a)))}]"); + } + return $"dotvvm.viewModules.init({string.Join(", ", args)});"; + })); } public void Render(IHtmlWriter writer, IDotvvmRequestContext context, string resourceName) diff --git a/src/Framework/Framework/Resources/Scripts/_standalone/dotnetWasmViewModule.ts b/src/Framework/Framework/Resources/Scripts/_standalone/dotnetWasmViewModule.ts new file mode 100644 index 0000000000..943c68dbf3 --- /dev/null +++ b/src/Framework/Framework/Resources/Scripts/_standalone/dotnetWasmViewModule.ts @@ -0,0 +1,76 @@ +// This module is not a part of the DotVVM bundle, it is distributed separately at /dotvvmResource/dotnetWasmInterop.js + +// @ts-ignore +import { dotnet } from "./dotnet.js"; +declare var dotvvm: any; + +let interop: any; +async function initDotnet() { + const { setModuleImports, getAssemblyExports, getConfig } = await dotnet.withDiagnosticTracing(true).create(); + + setModuleImports("dotvvmResource/dotvvm--interop--dotnet-wasm/dotvvm--interop--dotnet-wasm", { + callNamedCommand: async (typeName: string, instanceName: string, commandName: string, args: string[]) => { + const viewIdOrElement = instanceMap[instanceName]; + const argValues = args.map(a => JSON.parse(a)); + const result = await dotvvm.viewModules.call(viewIdOrElement, "dotnetWasmCallNamedCommand", [commandName, ...argValues], true); + return JSON.stringify(result || null); + }, + getViewModelSnapshot: () => { + return JSON.stringify(dotvvm.state); + }, + patchViewModel: (patchJson: string) => { + dotvvm.patchState(JSON.parse(patchJson)); + } + }); + + const config = getConfig(); + const exports = await getAssemblyExports("DotVVM.Framework.Interop.DotnetWasm"); + + interop = exports.DotVVM.Framework.Interop.DotnetWasm.DotnetWasmInterop; +} +const initPromise: Promise = initDotnet(); + +let instanceCounter = 0; +const instanceMap: { [id: string]: string | HTMLElement } = {}; + +class DotnetWasmModule { + private readonly moduleType: string; + private readonly moduleInstanceId: string; + + constructor(public readonly context: any) { + this.moduleType = this.context.instanceArgs![0]; + this.moduleInstanceId = "dotnet-wasm-" + (instanceCounter++); + instanceMap[this.moduleInstanceId] = context.viewIdOrElement; + this.init(); + } + + private async init() { + await initPromise; + interop.CreateViewModuleInstance(this.moduleType, this.moduleInstanceId); + } + + async dotnetWasmInvoke(method: string, ...args: any[]) { + const argValues = args.map(a => JSON.stringify(a)); + + await initPromise; + const result = await interop.CallViewModuleCommand(this.moduleType, this.moduleInstanceId, method, argValues); + + return JSON.parse(result); + } + + async dotnetWasmCallNamedCommand(name: string, ...args: any[]) { + const command = this.context.namedCommands[name]; + if (!command) { + throw `NamedCommand control with name '${name}' not found.`; + } + return await command(...args); + } + + async $dispose() { + await initPromise; + interop.DisposeViewModuleInstance(this.moduleType, this.moduleInstanceId); + delete instanceMap[this.moduleInstanceId]; + } +} + +export default (context: any) => new DotnetWasmModule(context); diff --git a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts index b9096d3777..1cf3945da6 100644 --- a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts +++ b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts @@ -1,4 +1,4 @@ -import { initCore, getViewModel, getViewModelObservable, initBindings, getCulture, getState, getStateManager } from "./dotvvm-base" +import { initCore, getViewModel, getViewModelObservable, initBindings, getCulture, getState, getStateManager, getVirtualDirectory } from "./dotvvm-base" import * as events from './events' import * as spa from "./spa/spa" import * as validation from './validation/validation' @@ -107,6 +107,7 @@ const dotvvmExports = { call: viewModuleManager.callViewModuleCommand, registerMany: viewModuleManager.registerViewModules }, + get virtualDirectory() { return getVirtualDirectory() }, resourceLoader: { notifyModuleLoaded }, diff --git a/src/Framework/Framework/Resources/Scripts/viewModules/viewModuleManager.ts b/src/Framework/Framework/Resources/Scripts/viewModules/viewModuleManager.ts index 01bff8b926..49e4ea92b7 100644 --- a/src/Framework/Framework/Resources/Scripts/viewModules/viewModuleManager.ts +++ b/src/Framework/Framework/Resources/Scripts/viewModules/viewModuleManager.ts @@ -28,7 +28,7 @@ export function registerViewModules(modules: { [name: string]: any }) { } } -export function initViewModule(name: string, viewIdOrElement: string | HTMLElement, rootElement: HTMLElement): ModuleContext { +export function initViewModule(name: string, viewIdOrElement: string | HTMLElement, rootElement: HTMLElement, instanceArgs?: string[]): ModuleContext { if (compileConstants.debug && rootElement == null) { throw new Error("rootElement has to have a value"); } const handler = ensureModuleHandler(name); @@ -49,11 +49,14 @@ export function initViewModule(name: string, viewIdOrElement: string | HTMLEleme const context = new ModuleContext( name, [rootElement], - elementContext && elementContext.$control ? { ...elementContext.$control } : {} + elementContext && elementContext.$control ? { ...elementContext.$control } : {}, + viewIdOrElement, + instanceArgs ); const moduleInstance = createModuleInstance(handler.module.default, context); context.module = moduleInstance; Object.freeze(context); + context.instanceArgs && Object.freeze(context.instanceArgs); if (typeof viewIdOrElement === "string") { handler.contexts[viewIdOrElement] = context; @@ -254,13 +257,15 @@ function mapCommandResult(result: any) { } export class ModuleContext { - private readonly namedCommands: { [name: string]: (...args: any[]) => Promise } = {}; + public readonly namedCommands: { [name: string]: (...args: any[]) => Promise } = {}; public module: any; constructor( public readonly moduleName: string, public readonly elements: HTMLElement[], - public readonly properties: { [name: string]: any }) { + public readonly properties: { [name: string]: any }, + public readonly viewIdOrElement: string | HTMLElement, + public readonly instanceArgs?: string[]) { } public registerNamedCommand = (name: string, command: (...args: any[]) => Promise) => { diff --git a/src/Framework/Framework/package.json b/src/Framework/Framework/package.json index da5367c90c..17a0ac1784 100644 --- a/src/Framework/Framework/package.json +++ b/src/Framework/Framework/package.json @@ -16,8 +16,9 @@ "typescript": "4.7.4" }, "scripts": { - "build": "node ./build.js", - "build-stats": "PRINT_STATS=true node ./build.js", + "build": "node ./build.js && npm run build-dotnet-wasm-module", + "build-dotnet-wasm-module": "tsc Resources/Scripts/_standalone/dotnetWasmViewModule.ts --outDir ./obj/javascript/dotvvmStaticResources --module es2020 --target es2020 --noResolve --typeRoots _", + "build-stats": "PRINT_STATS=true npm run build", "build-development": "rollup -c && npm run tsc-types", "build-rollup": "npm run build-production && npm run build-development", "build-production": "rollup -c --environment BUILD:production", diff --git a/src/Framework/Interop.DotnetWasm/DotVVM.Framework.Interop.DotnetWasm.csproj b/src/Framework/Interop.DotnetWasm/DotVVM.Framework.Interop.DotnetWasm.csproj new file mode 100644 index 0000000000..68b30011cb --- /dev/null +++ b/src/Framework/Interop.DotnetWasm/DotVVM.Framework.Interop.DotnetWasm.csproj @@ -0,0 +1,15 @@ + + + + net7.0 + enable + enable + 11 + true + + + + + + + diff --git a/src/Framework/Interop.DotnetWasm/DotnetWasmInterop.cs b/src/Framework/Interop.DotnetWasm/DotnetWasmInterop.cs new file mode 100644 index 0000000000..a5083395cd --- /dev/null +++ b/src/Framework/Interop.DotnetWasm/DotnetWasmInterop.cs @@ -0,0 +1,80 @@ +using System.Reflection; +using System.Runtime.InteropServices.JavaScript; +using DotVVM.Framework.Utils; + +namespace DotVVM.Framework.Interop.DotnetWasm; + +internal static partial class DotnetWasmInterop +{ + private static Dictionary instances = new(); + private static DotvvmClientSerializer serializer = new(); + + [JSExport] + internal static void CreateViewModuleInstance(string typeName, string instanceName) + { + var type = Type.GetType(typeName, true)!; + var context = new ViewModuleContext(typeName, instanceName, serializer); + + var constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, new[] { typeof(IViewModuleContext) }); + if (constructor == null) + { + throw new Exception($"The type {type} referenced in the @dotnet directive must have one public constructor accepting a parameter of type {typeof(IViewModuleContext)}."); + } + var instance = constructor.Invoke(new object[] { context }); + instances.Add(new ViewModuleInstanceKey(typeName, instanceName), instance); + } + + [JSExport] + internal static async Task CallViewModuleCommand(string typeName, string instanceName, string methodName, string[] args) + { + var instance = GetInstance(typeName, instanceName); + var method = instance.GetType().GetMethod(methodName); + if (method == null) + { + throw new Exception($"The method {methodName} was not found!"); + } + + var parameters = method.GetParameters(); + var argValues = args + .Select((json, index) => serializer.Deserialize(parameters[index].ParameterType, json)) + .ToArray(); + + try + { + var result = method.Invoke(instance, argValues); + if (result is Task taskResult) + { + await taskResult; + result = TaskUtils.GetResult(taskResult); + } + return serializer.Serialize(result); + } + catch (TargetInvocationException ex) + { + throw ex.InnerException!; + } + } + + [JSImport("callNamedCommand", "dotvvmResource/dotvvm--interop--dotnet-wasm/dotvvm--interop--dotnet-wasm")] + internal static partial Task CallNamedCommand(string typeName, string instanceName, string commandName, string[] args); + + [JSImport("getViewModelSnapshot", "dotvvmResource/dotvvm--interop--dotnet-wasm/dotvvm--interop--dotnet-wasm")] + internal static partial string GetViewModelSnapshot(); + + [JSImport("patchViewModel", "dotvvmResource/dotvvm--interop--dotnet-wasm/dotvvm--interop--dotnet-wasm")] + internal static partial void PatchViewModelSnapshot(string patchJson); + + [JSExport] + internal static void DisposeViewModuleInstance(string typeName, string instanceName) + { + instances.Remove(new ViewModuleInstanceKey(typeName, instanceName)); + } + + private static object GetInstance(string typeName, string instanceName) + { + return instances[new ViewModuleInstanceKey(typeName, instanceName)]; + } + + record ViewModuleInstanceKey(string TypeName, string InstanceName); + +} diff --git a/src/Framework/Interop.DotnetWasm/DotvvmClientSerializer.cs b/src/Framework/Interop.DotnetWasm/DotvvmClientSerializer.cs new file mode 100644 index 0000000000..a6b5d08e9f --- /dev/null +++ b/src/Framework/Interop.DotnetWasm/DotvvmClientSerializer.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DotVVM.Framework.Interop.DotnetWasm; + +public class DotvvmClientSerializer +{ + private readonly JsonSerializerOptions options; + + public DotvvmClientSerializer() + { + this.options = GetDefaultOptions(); + } + + private JsonSerializerOptions GetDefaultOptions() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonStringEnumConverter()); + return options; + } + + public string Serialize(object? value) + { + return JsonSerializer.Serialize(value, options); + } + + public object? Deserialize(Type type, string json) + { + return JsonSerializer.Deserialize(json, type, options); + } +} diff --git a/src/Framework/Interop.DotnetWasm/IViewModuleContext.cs b/src/Framework/Interop.DotnetWasm/IViewModuleContext.cs new file mode 100644 index 0000000000..a7ed13eb4e --- /dev/null +++ b/src/Framework/Interop.DotnetWasm/IViewModuleContext.cs @@ -0,0 +1,13 @@ +namespace DotVVM.Framework.Interop.DotnetWasm; + +public interface IViewModuleContext +{ + T GetViewModelSnapshot(); + + void PatchViewModel(object data); + + Task InvokeNamedCommandAsync(string commandName, params object[] args); + + Task InvokeNamedCommandAsync(string commandName, params object[] args); + +} diff --git a/src/Framework/Interop.DotnetWasm/ViewModuleContext.cs b/src/Framework/Interop.DotnetWasm/ViewModuleContext.cs new file mode 100644 index 0000000000..b7c37cad7f --- /dev/null +++ b/src/Framework/Interop.DotnetWasm/ViewModuleContext.cs @@ -0,0 +1,37 @@ +namespace DotVVM.Framework.Interop.DotnetWasm +{ + public class ViewModuleContext : IViewModuleContext + { + private readonly string typeName; + private readonly string instanceName; + private readonly DotvvmClientSerializer serializer; + + public T GetViewModelSnapshot() + { + var json = DotnetWasmInterop.GetViewModelSnapshot(); + return (T)serializer.Deserialize(typeof(T), json)!; + } + + public void PatchViewModel(object data) + { + var json = serializer.Serialize(data); + DotnetWasmInterop.PatchViewModelSnapshot(json); + } + + public Task InvokeNamedCommandAsync(string commandName, params object[] args) => InvokeNamedCommandAsync(commandName, args); + + public async Task InvokeNamedCommandAsync(string commandName, params object[] args) + { + var argValues = args.Select(serializer.Serialize).ToArray(); + var json = await DotnetWasmInterop.CallNamedCommand(typeName, instanceName, commandName, argValues); + return (T?)serializer.Deserialize(typeof(T), json); + } + + public ViewModuleContext(string typeName, string instanceName, DotvvmClientSerializer serializer) + { + this.typeName = typeName; + this.instanceName = instanceName; + this.serializer = serializer; + } + } +} diff --git a/src/Samples/ApplicationInsights.Owin/Web.config b/src/Samples/ApplicationInsights.Owin/Web.config index caad38616d..a7b2698ee1 100644 --- a/src/Samples/ApplicationInsights.Owin/Web.config +++ b/src/Samples/ApplicationInsights.Owin/Web.config @@ -59,8 +59,8 @@ - - + + diff --git a/src/Samples/AspNetCoreLatest/DotVVM.Samples.BasicSamples.AspNetCoreLatest.csproj b/src/Samples/AspNetCoreLatest/DotVVM.Samples.BasicSamples.AspNetCoreLatest.csproj index 2b5baa8657..31b5e308b2 100644 --- a/src/Samples/AspNetCoreLatest/DotVVM.Samples.BasicSamples.AspNetCoreLatest.csproj +++ b/src/Samples/AspNetCoreLatest/DotVVM.Samples.BasicSamples.AspNetCoreLatest.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Samples/AspNetCoreLatest/Properties/launchSettings.json b/src/Samples/AspNetCoreLatest/Properties/launchSettings.json index 5cf93a20d2..07be086aca 100644 --- a/src/Samples/AspNetCoreLatest/Properties/launchSettings.json +++ b/src/Samples/AspNetCoreLatest/Properties/launchSettings.json @@ -11,6 +11,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -18,6 +19,7 @@ "DotVVM.Samples.BasicSamples": { "commandName": "Project", "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "launchUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/src/Samples/AspNetCoreLatest/Startup.cs b/src/Samples/AspNetCoreLatest/Startup.cs index cf0affbee1..a537f04972 100644 --- a/src/Samples/AspNetCoreLatest/Startup.cs +++ b/src/Samples/AspNetCoreLatest/Startup.cs @@ -12,7 +12,9 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; namespace DotVVM.Samples.BasicSamples @@ -88,6 +90,19 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF app.UseEndpoints(endpoints => { endpoints.MapHealthChecks("/health"); }); + + var wasmOutputPath = Path.GetFullPath(Path.Combine(env.ContentRootPath, "../CSharpClient/bin/Debug/net7.0/browser-wasm/AppBundle")); + var contentTypeProvider = new FileExtensionContentTypeProvider(); + contentTypeProvider.Mappings.Add(".dll", "application/octet-stream"); + contentTypeProvider.Mappings.Add(".symbols", "application/octet-stream"); + contentTypeProvider.Mappings.Add(".blat", "application/octet-stream"); + contentTypeProvider.Mappings.Add(".dat", "application/octet-stream"); + app.UseStaticFiles(new StaticFileOptions() + { + RequestPath = new PathString("/dotvvmResource/dotvvm--interop--dotnet-wasm"), + FileProvider = new PhysicalFileProvider(wasmOutputPath), + ContentTypeProvider = contentTypeProvider + }); app.UseStaticFiles(); app.UseEndpoints(endpoints => { diff --git a/src/Samples/CSharpClient/DotVVM.Samples.BasicSamples.CSharpClient.csproj b/src/Samples/CSharpClient/DotVVM.Samples.BasicSamples.CSharpClient.csproj new file mode 100644 index 0000000000..4bcd18ddbb --- /dev/null +++ b/src/Samples/CSharpClient/DotVVM.Samples.BasicSamples.CSharpClient.csproj @@ -0,0 +1,15 @@ + + + + net7.0 + browser-wasm + Exe + main.js + enable + + + + + + + diff --git a/src/Samples/CSharpClient/Program.cs b/src/Samples/CSharpClient/Program.cs new file mode 100644 index 0000000000..cf1d2bc8f9 --- /dev/null +++ b/src/Samples/CSharpClient/Program.cs @@ -0,0 +1,7 @@ +using System; +using System.Runtime.InteropServices.JavaScript; + +if (!OperatingSystem.IsBrowser()) +{ + throw new PlatformNotSupportedException("This application is expected to run on browser platform."); +} diff --git a/src/Samples/CSharpClient/TestCsharpModule.cs b/src/Samples/CSharpClient/TestCsharpModule.cs new file mode 100644 index 0000000000..1e22d375a7 --- /dev/null +++ b/src/Samples/CSharpClient/TestCsharpModule.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using DotVVM.Framework.Interop.DotnetWasm; + +namespace DotVVM.Samples.BasicSamples.CSharpClient; + +public class TestCsharpModule +{ + private readonly IViewModuleContext context; + + public TestCsharpModule(IViewModuleContext context) + { + this.context = context; + } + + public void Hello() + { + Console.WriteLine("Hello world"); + } + + public int TestViewModelAccess() + { + var vm = context.GetViewModelSnapshot(); + return vm.Value; + } + + public void PatchViewModel(int newValue) + { + context.PatchViewModel(new { Value = newValue }); + } + + public async Task CallNamedCommand(int value) + { + await context.InvokeNamedCommandAsync("TestCommand", value); + } + +} + +public class TestViewModelShadow +{ + public int Value { get; set; } +} diff --git a/src/Samples/CSharpClient/TypeMarshallingModule.cs b/src/Samples/CSharpClient/TypeMarshallingModule.cs new file mode 100644 index 0000000000..cd2a95e44d --- /dev/null +++ b/src/Samples/CSharpClient/TypeMarshallingModule.cs @@ -0,0 +1,82 @@ +using System; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using DotVVM.Framework.Interop.DotnetWasm; + +namespace DotVVM.Samples.BasicSamples.CSharpClient +{ + public class TypeMarshallingModule + { + public byte MarshallByte(byte a) => (byte)(a * 2); + public byte? MarshallNullableByte(byte? a) => (byte?)(a * 2); + public sbyte MarshallSByte(sbyte a) => (sbyte)(a * 2); + public sbyte? MarshallNullableSByte(sbyte? a) => (sbyte?)(a * 2); + public short MarshallShort(short a) => (short)(a * 2); + public short? MarshallNullableShort(short? a) => (short?)(a * 2); + public ushort MarshallUShort(ushort a) => (ushort)(a * 2); + public ushort? MarshallNullableUShort(ushort? a) => (ushort?)(a * 2); + public int MarshallInt(int a) => a * 2; + public int? MarshallNullableInt(int? a) => a * 2; + public uint MarshallUInt(uint a) => a * 2; + public uint? MarshallNullableUInt(uint? a) => a * 2; + public long MarshallLong(long a) => a * 2; + public long? MarshallNullableLong(long? a) => a * 2; + public ulong MarshallULong(ulong a) => a * 2; + public ulong? MarshallNullableULong(ulong? a) => a * 2; + public float MarshallFloat(float a) => a * 2; + public float? MarshallNullableFloat(float? a) => a * 2; + public double MarshallDouble(double a) => a * 2; + public double? MarshallNullableDouble(double? a) => a * 2; + public decimal MarshallDecimal(decimal a) => a * 2; + public decimal? MarshallNullableDecimal(decimal? a) => a * 2; + public DateTime MarshallDateTime(DateTime a) => a.AddDays(1); + public DateTime? MarshallNullableDateTime(DateTime? a) => a?.AddDays(1); + public DateOnly MarshallDateOnly(DateOnly a) => a.AddDays(1); + public DateOnly? MarshallNullableDateOnly(DateOnly? a) => a?.AddDays(1); + public TimeOnly MarshallTimeOnly(TimeOnly a) => a.AddHours(1); + public TimeOnly? MarshallNullableTimeOnly(TimeOnly? a) => a?.AddHours(1); + public TimeSpan MarshallTimeSpan(TimeSpan a) => a + TimeSpan.FromHours(1); + public TimeSpan? MarshallNullableTimeSpan(TimeSpan? a) => a + TimeSpan.FromHours(1); + public char MarshallChar(char a) => char.ToUpper(a); + public char? MarshallNullableChar(char? a) => a == null ? null : char.ToUpper(a.Value); + public Guid MarshallGuid(Guid a) => new Guid("C286C18D-ECD8-47E0-BFC6-6CE709C5D498"); + public Guid? MarshallNullableGuid(Guid? a) => a == null ? null : new Guid("C286C18D-ECD8-47E0-BFC6-6CE709C5D498"); + public string MarshallString(string a) => a.ToUpper(); + public ChildEnum MarshallEnum(ChildEnum a) => (ChildEnum)(4 - a); + public ChildEnum? MarshallNullableEnum(ChildEnum? a) => (ChildEnum?)(4 - a); + public ChildObject MarshallObject(ChildObject child) => new ChildObject() { Int = child.Int + 1, String = child.String.ToUpper() }; + public ChildRecord MarshallRecord(ChildRecord child) => new ChildRecord(child.Int + 1, child.String.ToUpper()); + public ChildObject[] MarshallObjectArray(ChildObject[] a) => a.Reverse().ToArray(); + public ChildRecord[] MarshallRecordArray(ChildRecord[] a) => a.Reverse().ToArray(); + public int[] MarshallIntArray(int[] a) => a.Reverse().ToArray(); + public double?[] MarshallNullableDoubleArray(double?[] a) => a.Reverse().ToArray(); + public string[] MarshallStringArray(string[] a) => a.Reverse().ToArray(); + public async Task MarshallTask(ChildRecord[] a) + { + await Task.Delay(1000); + return a.Reverse().ToArray(); + } + public Exception MarshallException() => throw new Exception("Test exception"); + + public TypeMarshallingModule(IViewModuleContext context) + { + } + } + + public class ChildObject + { + public int Int { get; set; } + + public string String { get; set; } = null!; + } + + public record ChildRecord(int Int, string String); + + public enum ChildEnum + { + One = 1, + Two = 2, + Three = 3 + } +} diff --git a/src/Samples/CSharpClient/main.js b/src/Samples/CSharpClient/main.js new file mode 100644 index 0000000000..9d6be11879 --- /dev/null +++ b/src/Samples/CSharpClient/main.js @@ -0,0 +1,48 @@ +import { dotnet } from './dotnet.js' + +const is_browser = typeof window != "undefined"; +if (!is_browser) throw new Error(`Expected to be running in a browser`); + +const { setModuleImports, getAssemblyExports, getConfig } = await dotnet.withDiagnosticTracing(true).create(); + +setModuleImports("/wasm/main.js", { + ready: () => { + console.log("DotVVM interop is ready."); + }, + callNamedCommand: (typeName, instanceName, commandName, args) => { + console.log(typeName, instanceName, commandName, args); + }, + getViewModelSnapshot: () => { + return JSON.stringify(dotvvm.state); + }, + patchViewModel: (patchJson) => { + dotvvm.patchState(JSON.parse(patchJson)); + } +}); + +const config = getConfig(); +const exports = await getAssemblyExports("DotVVM.Framework.Interop.DotnetWasm"); + +await dotnet.run(); + +const interop = exports.DotVVM.Framework.Interop.DotnetWasm.DotnetWasmInterop; + +const type = "DotVVM.Samples.BasicSamples.CSharpClient.TestCsharpModule, DotVVM.Samples.BasicSamples.CSharpClient"; + +interop.CreateViewModuleInstance(type, "p0", ["TestCommand"]); +console.log("Created"); + +interop.CallViewModuleCommand(type, "p0", "Hello", []); +console.log("Command called"); + +var viewModelValue = interop.CallViewModuleCommand(type, "p0", "TestViewModelAccess", []); +console.log("View model value: " + viewModelValue); + +interop.CallViewModuleCommand(type, "p0", "PatchViewModel", ["15"]); +console.log("New view model value: " + dotvvm.state.Value); + +var commandResult = interop.CallViewModuleCommand(type, "p0", "CallNamedCommand", ["30"]); +console.log("Command result: " + commandResult); + +interop.DisposeViewModuleInstance(type, "p0"); +console.log("Disposed"); diff --git a/src/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj index 29e4e1977a..1f9380ef68 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -1,6 +1,6 @@  - $(DefaultTargetFrameworks) + $(DefaultTargetFrameworks);net7.0 DotVVM.Samples.Common @@ -84,6 +84,8 @@ + + @@ -214,7 +216,14 @@ TRACE;DEBUG - + + CSHARP_CLIENT + + + + + + diff --git a/src/Samples/Common/TextFileDiagnosticsInformationSender.cs b/src/Samples/Common/TextFileDiagnosticsInformationSender.cs index 549cf03941..a0ab48a2ac 100644 --- a/src/Samples/Common/TextFileDiagnosticsInformationSender.cs +++ b/src/Samples/Common/TextFileDiagnosticsInformationSender.cs @@ -39,7 +39,7 @@ public Task SendInformationAsync(DiagnosticsInformation information) File.AppendAllText(logFilePath, messages); } - return TaskUtils.GetCompletedTask(); + return Task.CompletedTask; } private string FormatUnwrittenMessages(DiagnosticsInformation information) diff --git a/src/Samples/Common/ViewModels/ComplexSamples/ButtonInMarkupControl/EnabledViewModel.cs b/src/Samples/Common/ViewModels/ComplexSamples/ButtonInMarkupControl/EnabledViewModel.cs index d2ce250296..88df17babb 100644 --- a/src/Samples/Common/ViewModels/ComplexSamples/ButtonInMarkupControl/EnabledViewModel.cs +++ b/src/Samples/Common/ViewModels/ComplexSamples/ButtonInMarkupControl/EnabledViewModel.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using DotVVM.Framework.Utils; namespace DotVVM.Samples.BasicSamples.ViewModels.ComplexSamples.ButtonInMarkupControl { @@ -16,7 +15,7 @@ public class EnabledViewModel public Task Flip() { Enabled = !Enabled; - return TaskUtils.GetCompletedTask(); + return Task.CompletedTask; } public class TestDto diff --git a/src/Samples/Common/ViewModels/FeatureSamples/ActionFilterErrorHandling/ActionFilterErrorHandlingViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/ActionFilterErrorHandling/ActionFilterErrorHandlingViewModel.cs index 655a11c9e8..1930d950b0 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/ActionFilterErrorHandling/ActionFilterErrorHandlingViewModel.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/ActionFilterErrorHandling/ActionFilterErrorHandlingViewModel.cs @@ -29,7 +29,7 @@ protected override Task OnCommandExceptionAsync(IDotvvmRequestContext context, A { ((ActionFilterErrorHandlingViewModel)context.ViewModel).Result = "error was handled"; context.IsCommandExceptionHandled = true; - return TaskUtils.GetCompletedTask(); + return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/src/Samples/Common/ViewModels/FeatureSamples/ActionFilterErrorHandling/ActionFilterPageErrorHandlingViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/ActionFilterErrorHandling/ActionFilterPageErrorHandlingViewModel.cs index bb74eaff91..d128678292 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/ActionFilterErrorHandling/ActionFilterPageErrorHandlingViewModel.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/ActionFilterErrorHandling/ActionFilterPageErrorHandlingViewModel.cs @@ -22,7 +22,7 @@ protected override Task OnPageExceptionAsync(IDotvvmRequestContext context, Exce { context.IsPageExceptionHandled = true; context.RedirectToUrl("/error500"); - return TaskUtils.GetCompletedTask(); + return Task.CompletedTask; } } } diff --git a/src/Samples/Common/ViewModels/FeatureSamples/ChildViewModelInvokeMethods/NastyChildViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/ChildViewModelInvokeMethods/NastyChildViewModel.cs index 1884ab6e2c..4f9cc0847f 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/ChildViewModelInvokeMethods/NastyChildViewModel.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/ChildViewModelInvokeMethods/NastyChildViewModel.cs @@ -13,19 +13,19 @@ public class NastyChildViewModel : DotvvmViewModelBase public override Task Init() { InitCount++; - return TaskUtils.GetCompletedTask(); + return Task.CompletedTask; } public override Task Load() { LoadCount++; - return TaskUtils.GetCompletedTask(); + return Task.CompletedTask; } public override Task PreRender() { PreRenderCount++; - return TaskUtils.GetCompletedTask(); + return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CsharpClient/CSharpClientViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/CsharpClient/CSharpClientViewModel.cs new file mode 100644 index 0000000000..6f70d03ea5 --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/CsharpClient/CSharpClientViewModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.ViewModel; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CsharpClient +{ + public class CSharpClientViewModel : DotvvmViewModelBase + { + public int Value { get; set; } = 1; + + public int? ReadResult { get; set; } + + public string LastConsole { get; set; } + } +} + diff --git a/src/Samples/Common/ViewModels/FeatureSamples/CsharpClient/MarshallingViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/CsharpClient/MarshallingViewModel.cs new file mode 100644 index 0000000000..f67794b9ef --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/CsharpClient/MarshallingViewModel.cs @@ -0,0 +1,62 @@ +#if CSHARP_CLIENT +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.ViewModel; +using DotVVM.Samples.BasicSamples.CSharpClient; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CsharpClient +{ + public class MarshallingViewModel : DotvvmViewModelBase + { + + public byte ByteValue { get; set; } = 0; + public byte? NullableByteValue { get; set; } = null; + public sbyte SByteValue { get; set; } = 1; + public sbyte? NullableSByteValue { get; set; } = 2; + public short ShortValue { get; set; } = 3; + public short? NullableShortValue { get; set; } = 4; + public ushort UShortValue { get; set; } = 5; + public ushort? NullableUShortValue { get; set; } = 6; + public int IntValue { get; set; } = 7; + public int? NullableIntValue { get; set; } = 8; + public uint UIntValue { get; set; } = 9; + public uint? NullableUIntValue { get; set; } = 10; + public long LongValue { get; set; } = 11; + public long? NullableLongValue { get; set; } = 12; + public ulong ULongValue { get; set; } = 13; + public ulong? NullableULongValue { get; set; } = 14; + public float FloatValue { get; set; } = 1.23f; + public float? NullableFloatValue { get; set; } = null; + public double DoubleValue { get; set; } = 4.5678; + public double? NullableDoubleValue { get; set; } = 9999; + public decimal DecimalValue { get; set; } = 1000000m; + public decimal? NullableDecimalValue { get; set; } = 1000001m; + public DateTime DateTimeValue { get; set; } = new DateTime(2020, 1, 2, 3, 4, 5); + public DateTime? NullableDateTimeValue { get; set; } = null; + public DateOnly DateOnlyValue { get; set; } = new DateOnly(2020, 10, 11); + public DateOnly? NullableDateOnlyValue { get; set; } = new DateOnly(2020, 11, 12); + public TimeOnly TimeOnlyValue { get; set; } = new TimeOnly(6, 0, 5); + public TimeOnly? NullableTimeOnlyValue { get; set; } = null; + public TimeSpan TimeSpanValue { get; set; } = new TimeSpan(2, 3, 4, 5); + public TimeSpan? NullableTimeSpanValue { get; set; } = null; + public char CharValue { get; set; } = 'b'; + public char? NullableCharValue { get; set; } = null; + public Guid GuidValue { get; set; } = new Guid("2EF427B2-889C-42A6-B6C8-781839A46825"); + public Guid? NullableGuidValue { get; set; } = null; + public string StringValue { get; set; } = "bababa"; + public ChildEnum EnumValue { get; set; } = ChildEnum.One; + public ChildEnum? NullableEnumValue { get; set; } = null; + public ChildObject ObjectValue { get; set; } = new ChildObject() { Int = 1, String = "abc" }; + public ChildRecord RecordValue { get; set; } = new ChildRecord(2, "def"); + public ChildObject[] ObjectArrayValue { get; set; } = new[] { new ChildObject() { Int = 1, String = "abc" }, new ChildObject() { Int = 3, String = "ghi" } }; + public ChildRecord[] RecordArrayValue { get; set; } = new[] { new ChildRecord(2, "def"), new ChildRecord(4, "jkl") }; + public int[] IntArrayValue { get; set; } = new[] { 2, 5 }; + public double?[] NullableDoubleArrayValue { get; set; } = new[] { 3.0, (double?)null }; + public string[] StringArrayValue { get; set; } = new[] { "abc", "def" }; + public ChildRecord[] TaskValue { get; set; } = new[] { new ChildRecord(6, "mno"), new ChildRecord(8, "pqr") }; + } +} +#endif diff --git a/src/Samples/Common/Views/FeatureSamples/CsharpClient/CSharpClient.dothtml b/src/Samples/Common/Views/FeatureSamples/CsharpClient/CSharpClient.dothtml new file mode 100644 index 0000000000..24151979d8 --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/CsharpClient/CSharpClient.dothtml @@ -0,0 +1,48 @@ +@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.CsharpClient.CSharpClientViewModel, DotVVM.Samples.Common +@dotnet DotVVM.Samples.BasicSamples.CSharpClient.TestCsharpModule, DotVVM.Samples.BasicSamples.CSharpClient + + + + + + + + + + +

+ Value: +

+ +

+ +

+ +

+ + {{value: ReadResult}} +

+ +

+ +

+ +

+ +

+ + +

+ Last console entry: {{value: LastConsole}} +

+ + + + + diff --git a/src/Samples/Common/Views/FeatureSamples/CsharpClient/Marshalling.dothtml b/src/Samples/Common/Views/FeatureSamples/CsharpClient/Marshalling.dothtml new file mode 100644 index 0000000000..422bedb1c0 --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/CsharpClient/Marshalling.dothtml @@ -0,0 +1,197 @@ +@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.CsharpClient.MarshallingViewModel, DotVVM.Samples.Common +@dotnet DotVVM.Samples.BasicSamples.CSharpClient.TypeMarshallingModule, DotVVM.Samples.BasicSamples.CSharpClient + + + + + + + + +

+ + {{value: ByteValue}} +

+

+ + {{value: NullableByteValue}} +

+

+ + {{value: SByteValue}} +

+

+ + {{value: NullableSByteValue}} +

+

+ + {{value: ShortValue}} +

+

+ + {{value: NullableShortValue}} +

+

+ + {{value: UShortValue}} +

+

+ + {{value: NullableUShortValue}} +

+

+ + {{value: IntValue}} +

+

+ + {{value: NullableIntValue}} +

+

+ + {{value: UIntValue}} +

+

+ + {{value: NullableUIntValue}} +

+

+ + {{value: LongValue}} +

+

+ + {{value: NullableLongValue}} +

+

+ + {{value: ULongValue}} +

+

+ + {{value: NullableULongValue}} +

+

+ + {{value: FloatValue}} +

+

+ + {{value: NullableFloatValue}} +

+

+ + {{value: DoubleValue}} +

+

+ + {{value: NullableDoubleValue}} +

+

+ + {{value: DecimalValue}} +

+

+ + {{value: NullableDecimalValue}} +

+

+ + {{value: DateTimeValue}} +

+

+ + {{value: NullableDateTimeValue}} +

+

+ + {{value: DateOnlyValue}} +

+

+ + {{value: NullableDateOnlyValue}} +

+

+ + {{value: TimeOnlyValue}} +

+

+ + {{value: NullableTimeOnlyValue}} +

+

+ + {{value: TimeSpanValue}} +

+

+ + {{value: NullableTimeSpanValue}} +

+

+ + {{value: CharValue}} +

+

+ + {{value: NullableCharValue}} +

+

+ + {{value: GuidValue}} +

+

+ + {{value: NullableGuidValue}} +

+

+ + {{value: StringValue}} +

+

+ + {{value: EnumValue}} +

+

+ + {{value: NullableEnumValue}} +

+

+ + {{value: ObjectValue.Int}}, {{value: ObjectValue.String}} +

+

+ + {{value: RecordValue.Int}}, {{value: RecordValue.String}} +

+

+ + {{value: ObjectArrayValue[0].Int}}, {{value: ObjectArrayValue[0].String}}; {{value: ObjectArrayValue[1].Int}}, {{value: ObjectArrayValue[1].String}} +

+

+ + {{value: RecordArrayValue[0].Int}}, {{value: RecordArrayValue[0].String}}; {{value: RecordArrayValue[1].Int}}, {{value: RecordArrayValue[1].String}} +

+

+ + {{value: IntArrayValue[0]}}; {{value: IntArrayValue[1]}} +

+

+ + {{value: NullableDoubleArrayValue[0]}}; {{value: NullableDoubleArrayValue[1]}} +

+

+ + {{value: StringArrayValue[0]}}; {{value: StringArrayValue[1]}} +

+

+ + {{value: TaskValue[0].Int}}, {{value: TaskValue[0].String}}; {{value: TaskValue[1].Int}}, {{value: TaskValue[1].String}} +

+

+ +

+ + + + diff --git a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs index 936bcaa3d2..8908d72a3f 100644 --- a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs +++ b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs @@ -227,6 +227,8 @@ public partial class SamplesRouteUrls public const string FeatureSamples_CompositeControls_BasicSample = "FeatureSamples/CompositeControls/BasicSample"; public const string FeatureSamples_CompositeControls_ControlPropertyNamingConflict = "FeatureSamples/CompositeControls/ControlPropertyNamingConflict"; public const string FeatureSamples_ConditionalCssClasses_ConditionalCssClasses = "FeatureSamples/ConditionalCssClasses/ConditionalCssClasses"; + public const string FeatureSamples_CsharpClient_CSharpClient = "FeatureSamples/CsharpClient/CSharpClient"; + public const string FeatureSamples_CsharpClient_Marshalling = "FeatureSamples/CsharpClient/Marshalling"; public const string FeatureSamples_CustomResponseProperties_SimpleExceptionFilter = "FeatureSamples/CustomResponseProperties/SimpleExceptionFilter"; public const string FeatureSamples_DateTimeSerialization_DateTimeSerialization = "FeatureSamples/DateTimeSerialization/DateTimeSerialization"; public const string FeatureSamples_DependencyInjection_ViewModelScopedService = "FeatureSamples/DependencyInjection/ViewModelScopedService"; diff --git a/src/Samples/Tests/Tests/Feature/CsharpClientTests.cs b/src/Samples/Tests/Tests/Feature/CsharpClientTests.cs new file mode 100644 index 0000000000..fbfee7aaba --- /dev/null +++ b/src/Samples/Tests/Tests/Feature/CsharpClientTests.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Samples.Tests.Base; +using DotVVM.Testing.Abstractions; +using Riganti.Selenium.Core; +using Xunit; +using Xunit.Abstractions; + +namespace DotVVM.Samples.Tests.Feature +{ + public class CsharpClientTests : AppSeleniumTest + { + [Fact] + [Trait("Category", "aspnetcore-only")] + public void Feature_CsharpClient_CSharpClient() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_CsharpClient_CSharpClient); + + var value = browser.Single("value", SelectByDataUi); + AssertUI.Value(value, "1"); + + // check console access + browser.Single("hello", SelectByDataUi).Click(); + AssertUI.TextEquals(browser.Single("console", SelectByDataUi), "Hello world"); + + // read VM + browser.Single("read-vm", SelectByDataUi).Click(); + AssertUI.TextEquals(browser.Single("read-vm-result", SelectByDataUi), "1"); + + // patch VM + browser.Single("patch-vm", SelectByDataUi).Click(); + AssertUI.TextEquals(value, "30"); + browser.Single("read-vm", SelectByDataUi).Click(); + AssertUI.TextEquals(browser.Single("read-vm-result", SelectByDataUi), "30"); + + // call command + browser.Single("named-command", SelectByDataUi).Click(); + AssertUI.TextEquals(value, "60"); + }); + } + + [Theory] + [InlineData("MarshallByte", "0")] + [InlineData("MarshallNullableByte", "")] + [InlineData("MarshallSByte", "2")] + [InlineData("MarshallNullableSByte", "4")] + [InlineData("MarshallShort", "6")] + [InlineData("MarshallNullableShort", "8")] + [InlineData("MarshallUShort", "10")] + [InlineData("MarshallNullableUShort", "12")] + [InlineData("MarshallInt", "14")] + [InlineData("MarshallNullableInt", "16")] + [InlineData("MarshallUInt", "18")] + [InlineData("MarshallNullableUInt", "20")] + [InlineData("MarshallLong", "22")] + [InlineData("MarshallNullableLong", "24")] + [InlineData("MarshallULong", "26")] + [InlineData("MarshallNullableULong", "28")] + [InlineData("MarshallFloat", "2.46")] + [InlineData("MarshallNullableFloat", "")] + [InlineData("MarshallDouble", "9.1356")] + [InlineData("MarshallNullableDouble", "19998")] + [InlineData("MarshallDecimal", "2000000")] + [InlineData("MarshallNullableDecimal", "2000002")] + [InlineData("MarshallDateTime", "1/3/2020 3:04:05 AM")] + [InlineData("MarshallNullableDateTime", "")] + [InlineData("MarshallDateOnly", "Monday, October 12, 2020")] + [InlineData("MarshallNullableDateOnly", "Friday, November 13, 2020")] + [InlineData("MarshallTimeOnly", "7:00:05 AM")] + [InlineData("MarshallNullableTimeOnly", "")] + [InlineData("MarshallTimeSpan", "2.04:04:05")] + [InlineData("MarshallNullableTimeSpan", "")] + [InlineData("MarshallChar", "B")] + [InlineData("MarshallNullableChar", "")] + [InlineData("MarshallGuid", "c286c18d-ecd8-47e0-bfc6-6ce709c5d498")] + [InlineData("MarshallNullableGuid", "")] + [InlineData("MarshallString", "BABABA")] + [InlineData("MarshallEnum", "Three")] + [InlineData("MarshallNullableEnum", "")] + [InlineData("MarshallObject", "2, ABC")] + [InlineData("MarshallRecord", "3, DEF")] + [InlineData("MarshallObjectArray", "3, ghi; 1, abc")] + [InlineData("MarshallRecordArray", "4, jkl; 2, def")] + [InlineData("MarshallIntArray", "5; 2")] + [InlineData("MarshallNullableDoubleArray", "; 3")] + [InlineData("MarshallStringArray", "def; abc")] + [InlineData("MarshallTask", "8, pqr; 6, mno")] + [Trait("Category", "aspnetcore-only")] + public void Feature_CsharpClient_Marshalling(string section, string expected) + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_CsharpClient_Marshalling); + + var p = browser.Single(section, SelectByDataUi); + p.Single("input[type=button]").Click(); + AssertUI.TextEquals(p.Single("span"), expected); + }); + } + + public CsharpClientTests(ITestOutputHelper output) : base(output) + { + } + } +} diff --git a/src/Tests/ControlTests/ViewModulesServerSideTests.cs b/src/Tests/ControlTests/ViewModulesServerSideTests.cs index 1b7616967e..26732e3dc2 100644 --- a/src/Tests/ControlTests/ViewModulesServerSideTests.cs +++ b/src/Tests/ControlTests/ViewModulesServerSideTests.cs @@ -41,14 +41,14 @@ public async Task NamedCommandWithoutViewModule_StaticCommand() { var r = await Assert.ThrowsExceptionAsync(() => cth.RunPage(typeof(object), @" ")); - Assert.AreEqual("Validation error in NamedCommand at line 7: The NamedCommand control can be used only in pages or controls that have the @js directive.", r.Message); + Assert.AreEqual("Validation error in NamedCommand at line 7: The NamedCommand control can be used only in pages or controls that have the @js or @csharp directive.", r.Message); } [TestMethod] public async Task NamedCommandWithoutViewModule_Command() { var r = await Assert.ThrowsExceptionAsync(() => cth.RunPage(typeof(object), @" ")); - Assert.AreEqual("Validation error in NamedCommand at line 7: The NamedCommand control can be used only in pages or controls that have the @js directive.", r.Message); + Assert.AreEqual("Validation error in NamedCommand at line 7: The NamedCommand control can be used only in pages or controls that have the @js or @csharp directive.", r.Message); } [TestMethod] diff --git a/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_InternalProperty.html b/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_InternalProperty.html index e84fa989d5..bf16e52e70 100644 --- a/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_InternalProperty.html +++ b/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_InternalProperty.html @@ -1,7 +1,7 @@ -
+
diff --git a/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_JsInvoke.html b/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_JsInvoke.html index 960a66701e..4a9d2eeb0e 100644 --- a/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_JsInvoke.html +++ b/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_JsInvoke.html @@ -1,7 +1,7 @@ -
+
diff --git a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html index 46df5cdf5a..92f9879ed3 100644 --- a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html +++ b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html @@ -11,10 +11,10 @@ - + - + @@ -29,8 +29,8 @@ "dotvvm.internal", "dotvvm", "dotvvm.debug", - "viewModule.import.6DLlOTYMqGV5yPAJoz5k_moKDBgEmZ3_HLLQ2zKDo74", - "viewModule.init.6DLlOTYMqGV5yPAJoz5k_moKDBgEmZ3_HLLQ2zKDo74" + "viewModule.import.txdc3tmh_O-C0Z6NbwaMDcdUBct4MPc2ESN2w2mvtZU", + "viewModule.init.txdc3tmh_O-C0Z6NbwaMDcdUBct4MPc2ESN2w2mvtZU" ], "typeMetadata": {} }"> diff --git a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html index c897ea466f..31b07e946b 100644 --- a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html +++ b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html @@ -8,26 +8,26 @@ - + - + - + -
+
-
+
@@ -49,10 +49,10 @@ "knockout", "dotvvm.internal", "dotvvm", - "viewModule.import.lIquTPFe3a42GboPePTmYZ4Xc1S4ok-JXmZud9HUybE", + "viewModule.import.-X0xRoPX49kw-adwrbsYQEJD_niSRyAgf2xH3URtlgw", "dotvvm.debug", - "viewModule.import.HIj399FcjVwbyaIvDKFctn7Ghr0UMajY-haUgZypcHs", - "viewModule.init.HIj399FcjVwbyaIvDKFctn7Ghr0UMajY-haUgZypcHs" + "viewModule.import.Zi0iKhaMCabp2tHzbqfe2sm6goFucgC1ZwGOP_Pli5k", + "viewModule.init.Zi0iKhaMCabp2tHzbqfe2sm6goFucgC1ZwGOP_Pli5k" ], "typeMetadata": { "RPyCWRg9lOhkRJyH": { diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index 37417c9dd9..2090a52421 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -68,6 +68,17 @@ "knockout" ], "RenderPosition": "Anywhere" + }, + "dotvvm.interop.dotnet-wasm": { + "Defer": true, + "Location": { + "$type": "DotVVM.Framework.ResourceManagement.EmbeddedResourceLocation, DotVVM.Framework", + "Assembly": "DotVVM.Framework, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da", + "Name": "DotVVM.Framework.obj.javascript.dotvvmStaticResources.dotnetWasmViewModule.js", + "DebugName": "DotVVM.Framework.obj.javascript.dotvvmStaticResources.dotnetWasmViewModule.js" + }, + "MimeType": "text/javascript", + "RenderPosition": "Anywhere" } }, "scripts": {