Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calling .NET WebAssembly code from views #1526

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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' }}
Expand Down
2 changes: 2 additions & 0 deletions .github/uitest/uitest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ try {
"$testDir", `
"--configuration", `
"$config", `
"--filter", `
"Category!=aspnetcore-only", `
"--no-restore", `
"--logger", `
"trx;LogFileName=$TrxName", `
Expand Down
30 changes: 30 additions & 0 deletions src/DotVVM.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
11 changes: 7 additions & 4 deletions src/Framework/Framework/Binding/ViewModuleReferenceInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ namespace DotVVM.Framework.Binding
[HandleAsImmutableObjectInDotvvmProperty]
public sealed class ViewModuleReferenceInfo
{
public string[] ReferencedModules { get; }
public ViewModuleReferencedModule[] ReferencedModules { get; }

/// <summary>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.</summary>
public string ViewId { get; }

/// <summary> Whether control id should be used instead of ViewId to identify the modules. </summary>
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;
Expand All @@ -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))
Expand All @@ -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);
}
Original file line number Diff line number Diff line change
@@ -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));
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return Expression.New(constructors[0], Expression.Constant(null, typeof(object)))
return Expression.New(constructors[0], Expression.Constant(null, typeof(IViewModuleContext)))

It won't compile with object.

However, I'm thinking if we want to allow invoking the methods server-side. I guess it makes sense, but passing null into the constructor will just make it fail on the unexpected null reference. Maybe only allow it if the type also has parameterless constructor?

.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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,40 @@ 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 _
);
}

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<ViewModuleReferencedModule>(),
directiveMetadata.DotnetViewModuleResult?.Reference.ReferencedModules ?? Enumerable.Empty<ViewModuleReferencedModule>()
).ToArray(),
firstReference.IsMarkupControl
);
return reference;
}

/// <summary>
/// Resolves the content of the root node.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace DotVVM.Framework.Compilation.ControlTree;

public interface IAbstractDotnetViewModuleDirective : IAbstractDirective
{
/// <summary>Full type name of the module specified</summary>
ITypeDescriptor? ModuleType { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public interface IAbstractTreeBuilder
IAbstractBaseTypeDirective BuildBaseTypeDirective(DothtmlDirectiveNode directive, BindingParserNode nameSyntax, ImmutableList<NamespaceImport> imports);

IAbstractViewModuleDirective BuildViewModuleDirective(DothtmlDirectiveNode directiveNode, string modulePath, string resourceName);
IAbstractDotnetViewModuleDirective BuildDotnetViewModuleDirective(DothtmlDirectiveNode directiveNode, BindingParserNode moduleType, ImmutableList<NamespaceImport> imports);
IAbstractPropertyDeclarationDirective BuildPropertyDeclarationDirective(DothtmlDirectiveNode directive, TypeReferenceBindingParserNode typeSyntax, SimpleNameBindingParserNode nameSyntax, BindingParserNode? initializer, IList<IAbstractDirectiveAttributeReference> resolvedAttributes, BindingParserNode valueSyntaxRoot, ImmutableList<NamespaceImport> imports);
IAbstractDirectiveAttributeReference BuildPropertyDeclarationAttributeReference(DothtmlDirectiveNode directiveNode, IdentifierNameBindingParserNode propertyNameSyntax, ActualTypeReferenceBindingParserNode typeSyntax, LiteralExpressionBindingParserNode initializer, ImmutableList<NamespaceImport> imports);
IAbstractPropertyBinding BuildPropertyBinding(IPropertyDescriptor property, IAbstractBinding binding, DothtmlAttributeNode? sourceAttributeNode);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary> Represents the @dotnet directive - import .NET WASM module on the client side </summary>
public class ResolvedDotnetViewModuleDirective : ResolvedDirective, IAbstractDotnetViewModuleDirective
{
/// <summary>Full .NET type of the module</summary>
public ITypeDescriptor? ModuleType { get; }

public ResolvedDotnetViewModuleDirective(DirectiveCompilationService directiveCompilationService, DothtmlDirectiveNode node, BindingParserNode typeName, ImmutableList<NamespaceImport> imports)
: base(node)
{
ModuleType = directiveCompilationService.ResolveType(node, typeName, imports);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<NamespaceImport> imports) =>
new ResolvedDotnetViewModuleDirective(directiveService, directiveNode, typeName, imports);

public IAbstractPropertyDeclarationDirective BuildPropertyDeclarationDirective(
DothtmlDirectiveNode directive,
TypeReferenceBindingParserNode typeSyntax,
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Loading