Reputation: 934
Context: Bootstrapping an F# app that uses server sider blazor/ razor components BUT does not use razor to define components. Instead I use FsBoleros DSL.
Everything works as expected but there is still on .cshtml
file (_Host.cshtml
) that I'd like to replace with just (F#) code for a few reasons:
All my code will be written in F# - but if I need to change things/ add code to the host razor file I need to mix in C#. While this works it's not optimal and I'd rather just have F# code and no special razor file.
F# Projects depend on file ordering. The razor file behaves a bit strange in that regard (I cant just reorder it like other files in my project using IDE tooling).
So the goal is to get rid of the _Host.cshtml
file and replace it with just code.
I've already invested 2 days in trying to do this. There seems to be little to no content on how to replace a razor page with just code or how to call a tag helper from non razor code. A little guidance on how to approach this would be highly appreciated, otherwise I'll burn more time on this.
My current understanding of what I have to do is:
Render a html response that includes
Is this correct ?
If so:
Generating the needed html page is trivial except for the prerendered annotated component. Is calling a tag helper the right approach to achieving that (as I'm having some issues with that) ?
Other Component rendering classes (and interfaces) are internal so I suspect that I should not use them. I also assume that I have to use the tag helper in order to get the html correctly annotated.
note: It's not required to actually replace it with code that does exactly the same (eg. is a razor page) but rather does what is required to bootstrap blazor in a maintainable fashion (so no magic string plumbing).
(This is the _Host.cshtml
file to replace)
@page "/"
@namespace X.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Bolero Application</title>
<base href="~/">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
<link rel="stylesheet" href="css/index.css">
</head>
<body>
<h1>Blazor Host</h1>
<component type="typeof(App)" render-mode="ServerPrerendered"/>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
Upvotes: 1
Views: 1086
Reputation: 934
EDIT: This also seems to be possible using the IHtmlHelper
after opening the right namespaces.
task {
let htmlHelper = container.GetService<IHtmlHelper>()
(htmlHelper :?> IViewContextAware).Contextualize(ViewContext(HttpContext = httpContext))
// RenderComponentAsync is an extension from "Microsoft.AspNetCore.Mvc.Rendering"
let! componentHtmlContent = htmlHelper.RenderComponentAsync<App>(RenderMode.ServerPrerendered)
let componentHtml: string =
using (new StringWriter()) (fun writer ->
componentHtmlContent.WriteTo (writer, HtmlEncoder.Default)
writer.Flush()
writer.ToString()
)
}
Old answer is below:
The solution I've found works BUT involves calling methods on internal types that are not exposed by ASP.NET Core.
Module to call IComponentRenderer.RenderComponentAsync via reflection.
[<RequireQualifiedAccess>]
module ComponentRenderer =
open System
open System.Reflection
open System.Threading.Tasks
open Microsoft.AspNetCore.Html
open Microsoft.AspNetCore.Mvc.Rendering
let private ``reflected IComponentRenderer``: Type =
let assemblyName = "Microsoft.AspNetCore.Mvc.ViewFeatures"
let assembly = Assembly.Load assemblyName
let componentRendererInterface = assembly.GetType $"{assemblyName}.IComponentRenderer"
componentRendererInterface
let private getRef (serviceContainer: IServiceProvider) : obj =
serviceContainer.GetService ``reflected IComponentRenderer``
let private getMethodRef () : MethodInfo =
let flags = BindingFlags.Public ||| BindingFlags.Instance
let method = ``reflected IComponentRenderer``.GetMethod ("RenderComponentAsync", flags)
method
let callRenderComponentAsync
(serviceContainer: IServiceProvider,
viewContext: ViewContext,
componentType: Type,
renderMode: RenderMode,
parameters: obj) : Task<IHtmlContent> =
let renderer = getRef serviceContainer
let method = getMethodRef ()
let result = method.Invoke(renderer, [| viewContext; componentType; renderMode; parameters |])
result :?> Task<IHtmlContent>
Basically all that's required is a HTML response that contains:
There are multiple ways to get to that desired result, here is a simple example handler.
module HttpHandlers =
let htmlTemplate (comp: string) =
$"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Bolero Application</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
<link rel="stylesheet" href="css/index.css">
</head>
<body>
{comp}
<script src="_framework/blazor.server.js"></script>
</body>
</html>
"""
let index (container: IServiceProvider) (httpContext: HttpContext) : Task =
task {
let! componentHtmlContent =
ComponentRenderer.callRenderComponentAsync (
container,
ViewContext(HttpContext = httpContext),
typeof<App>,
RenderMode.ServerPrerendered,
null
)
let componentHtml: string =
using (new StringWriter()) (fun writer ->
componentHtmlContent.WriteTo (writer, HtmlEncoder.Default)
writer.Flush()
writer.ToString()
)
let! _ =
(htmlTemplate componentHtml)
|> Encoding.UTF8.GetBytes
|> ReadOnlyMemory
|> httpContext.Response.BodyWriter.WriteAsync
return ()
} :> _
The resulting html response looks should now look like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
<link rel="stylesheet" href="css/index.css">
</head>
<body>
<!--Blazor:{"sequence":0,"type":"server","prerenderId":"de4cefeb845c4241810b39a9fa6fb09d","descriptor":"CfDJ8B1PPZs/SbNIpyDpD4CgsZTZ9MSJjiAAdULVygxQGjm2QdNlb19sKvxXV\u002B0ZZh\u002Br43icwKyOdOw0RCK6auj2fziShHMZNiE/kNp0XnqJywdEpAAGuNROLgewx4NSSwKJ5lUUWq\u002BiuwKOnKNwQEJA1BAQ1B0IJbE6gKqHkKPMK3vYVjnB/jgpX01f2DS7djVlH\u002Bc/D8hr2jjuqt08527OrAPky7Fm71HejVDjEwZApZUj853dq3sDpmyNO2uWJaTRufSeBX1UISwofgBwDobZ8RBSVTfzMP8HPJ\u002BKBJxt\u002BNXueXpxcXXQwva9n5tqWKyFEahW4lOQFLrr3/Gvh9mRY1EExZapEiO/b5qHc9CtwgqDQN8U7fwtH2il8uPBs3Hsdg=="}-->
<div>
<p>Hello World and welcome to my app!</p>
</div>
<!--Blazor:{"prerenderId":"de4cefeb845c4241810b39a9fa6fb09d"}-->
<script src="_framework/blazor.server.js"></script>
</body>
</html>
Upvotes: 1
Reputation: 9029
Look at the generated code file _Host.cshtml.g.cs
and convert that to F#
I don't do F#, so can't comment on how difficult that will be, but reviewing the generated code should at least answer your questions about how to call taghelpers etc...
Upvotes: 0