I'm having a bit of an issue converting some Microsoft playwright code from C# to F#
Specifically this code: https://playwright.dev/dotnet/docs/navigations#multiple-navigations
// Running action in the callback of waitForNavigation prevents a race
// condition between clicking and waiting for a navigation.
await page.RunAndWaitForNavigationAsync(async () =>
{
// Triggers a navigation with a script redirect.
await page.ClickAsync("a");
}, new PageWaitForNavigationOptions
{
UrlString = "**/login"
});
My F# code is a little separated and specific to my requirements - but here is the attempt so far ( which doesn't work )
let waitNavOptions = new PageWaitForNavigationOptions(UrlRegex=Regex("dashboard|login",RegexOptions.IgnoreCase))
do! Async.AwaitTask(page.Value.RunAndWaitForNavigationAsync(page.Value.ClickAsync("#xl-form-submit"),waitNavOptions))
let waitNavOptions = PageRunAndWaitForNavigationOptions(UrlRegex=Regex("dashboard|login",RegexOptions.IgnoreCase))
do!
page.Value.RunAndWaitForNavigationAsync(
(fun () -> page.Value.ClickAsync("#xl-form-submit")),
waitNavOptions)
|> Async.AwaitTask
|> Async.Ignore
There were a few things to fix here:
Changed PageWaitForNavigationOptions to PageRunAndWaitForNavigationOptions
Change the first method argument to a function returning a task, instead of just a task.
Ignore the Async result at the end so that do! is allowed
Related
I am new to Bolero. I need two separate layouts both with a separate menu system - one for "normal" web pages, the other for a CMS. That means using two different css files.
Is it possible to have two Elmish loops to achieve that? If not, what then? I have tried to utilise two Elmish loops, but the CMS loop does not work. Visual Studio signals no error, but the CMS page gets me back to the base as if there was something wrong with the routing (which certainly is).
See below for parts of my code, but you will probably need much more - the complete code is available on my GitHub - the CMS system is only simulated there, it is to be coded later.
member this.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
app
.UseAuthentication()
.UseRemoting()
.MapWhen(
(fun ctx -> ctx.Request.Path.Value.StartsWith "/rozcestnikCMS"),
(fun app ->
app
.UseBlazorFrameworkFiles()
.UseStaticFiles()
.UseRouting()
.UseEndpoints(fun endpoints ->
endpoints.MapBlazorHub() |> ignore
endpoints.MapFallbackToBolero(IndexCMS.page) |> ignore)
|> ignore
)
)
.UseBlazorFrameworkFiles()
.UseStaticFiles()
.UseRouting()
.UseEndpoints(fun endpoints ->
endpoints.MapBlazorHub() |> ignore
endpoints.MapFallbackToBolero(Index.page) |> ignore)
|> ignore
Index.page
let page = doctypeHtml {
head {
//some code
}
body {
div { attr.id "generalLayout"; rootComp<Client.Controller.MyApp> }
boleroScript
}
}
IndexCMS.page
let page = doctypeHtml {
head {
//some code
}
body {
div { attr.id "generalLayout"; rootComp<Client.ControllerCMS.MyCMSApp> }
boleroScript
}
}
Controller.fs
type MyApp() =
inherit ProgramComponent<Model, Message>()
override this.Program =
let remote : RemoteServices =
{
login = this.Remote<Login.RemoteService>()
}
let init _ = initModel, initCmd
let update message model = update remote message model
Program.mkProgram init update view
|> Program.withRouter router
ControllerCMS.fs
type MyCMSApp() =
inherit ProgramComponent<Model, Message>()
override this.Program =
let init _ =
initModel, Cmd.none
Program.mkProgram init update view
|> Program.withRouter router
EDIT 25-06-2022
In Blazor, the problem with different css styles can be dealt with this way. Is it possible to use the same approach in Bolero or not?
I created a default SAFE app as described here.
Removing redundant stuff, the server is this:
open Giraffe
open Saturn
let webApp = scope {
get "/api/init" (fun next ctx ->
task {
let number = 42
let! counter = task { return number }
return! Successful.OK counter next ctx
})
}
let app = application {
url ("http://0.0.0.0:8085/")
router webApp
memory_cache
use_static "../Client/public"
use_gzip
}
run app
Now, when running app, I see some logging in the console, basically incoming requests:
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET http://localhost:8085/api/init
How do I customize the logging? The docs are as scarce as possible, no examples. I need something simple, like logging "going to return 42...".
Or at least some links with cases.
You can pull the fully blown ILogger object from the context, ctx.
Open Microsoft.Extensions.Logging module and then you can do things like this:
let webApp = scope {
get "/api/init" (fun next ctx ->
task {
let logger = ctx.GetLogger();
let number = 42
logger.Log(LogLevel.Information, "Going to return " + number.ToString())
let! counter = task { return number }
return! Successful.OK counter next ctx
})
}
This will bring to your console:
info: object[0]
Going to return 42
I do not have any proper references. I found a similar thing at the Github of Giraffe server for which Saturn is basically a set of abstractions.
Logging configuration is built into v0.9 at least. I used the case below for myself to suppress most of the logging.
open Microsoft.Extensions.Logging
let app = application {
url ("http://0.0.0.0:8085/")
use_router webApp
logging (fun logger -> logger.SetMinimumLevel LogLevel.Critical |> ignore)
}
In my xaml I have a Button and a TextBlock
<TextBlock Text="{Binding AText}" FontSize="20"/>
<Button Content="Click" Command="{Binding MyCommand}" Grid.Row="1"/>
and in my ViewModel I have following Code
let aText = self.Factory.Backing(<# self.AText #>, "Initial Text")
let asyncTask x = async {
self.AText <- "Loading"
do! Async.Sleep(5000)
self.AText <- "Loaded"
}
member self.AText with get() = aText.Value and set(v) = aText.Value <- v
member self.MyCommand = self.Factory.CommandAsyncChecked(asyncTask, fun _ -> true)
When I click the button, It gets disabled and remains so until it finishes the asyncTask. I thought that setting canExecute as true will change the behavior but it did not!
How to control the behaviour of the button?
The Async command support in FSharp.ViewModule was designed specifically so that the commands disable while operating, in order to prevent them from occurring multiple times.
If you don't want that behavior, the simplest option is to just use a normal command, and start the async workflow manually:
let asyncTask () = async {
self.AText <- "Loading"
do! Async.Sleep(5000)
self.AText <- "Loaded"
} |> Async.Start
member self.MyCommand = self.Factory.Command asyncTask
The normal command behavior will always remain enabled.
I read the following code from the book Expert f#,
Why the function collectLinks embeds let! html = async { .... } in the outer async block? How about just flat it by removing the inner async?
Same question for the function waitForUrl in urlCollector which has a do! Async.StartChild (async {....}) |> Async.Ignore in an outer async block. How about flat it?
How is the implementation comparing with the one implemented with block queue? https://msdn.microsoft.com/en-us/library/vstudio/hh297096(v=vs.100).aspx Creating a block queue with 5, and en-queue the link to producer.
Code:
open System.Collections.Generic
open System.Net
open System.IO
open System.Threading
open System.Text.RegularExpressions
let limit = 50
let linkPat = "href=\s*\"[^\"h]*(http://[^&\"]*)\""
let getLinks (txt:string) =
[ for m in Regex.Matches(txt,linkPat) -> m.Groups.Item(1).Value ]
// A type that helps limit the number of active web requests
type RequestGate(n:int) =
let semaphore = new Semaphore(initialCount=n, maximumCount=n)
member x.AsyncAcquire(?timeout) =
async { let! ok = Async.AwaitWaitHandle(semaphore,
?millisecondsTimeout=timeout)
if ok then
return
{ new System.IDisposable with
member x.Dispose() =
semaphore.Release() |> ignore }
else
return! failwith "couldn't acquire a semaphore" }
// Gate the number of active web requests
let webRequestGate = RequestGate(5)
// Fetch the URL, and post the results to the urlCollector.
let collectLinks (url:string) =
async { // An Async web request with a global gate
let! html =
async { // Acquire an entry in the webRequestGate. Release
// it when 'holder' goes out of scope
use! holder = webRequestGate.AsyncAcquire()
let req = WebRequest.Create(url,Timeout=5)
// Wait for the WebResponse
use! response = req.AsyncGetResponse()
// Get the response stream
use reader = new StreamReader(response.GetResponseStream())
// Read the response stream (note: a synchronous read)
return reader.ReadToEnd() }
// Compute the links, synchronously
let links = getLinks html
// Report, synchronously
do printfn "finished reading %s, got %d links" url (List.length links)
// We're done
return links }
/// 'urlCollector' is a single agent that receives URLs as messages. It creates new
/// asynchronous tasks that post messages back to this object.
let urlCollector =
MailboxProcessor.Start(fun self ->
// This is the main state of the urlCollector
let rec waitForUrl (visited : Set<string>) =
async { // Check the limit
if visited.Count < limit then
// Wait for a URL...
let! url = self.Receive()
if not (visited.Contains(url)) then
// Start off a new task for the new url. Each collects
// links and posts them back to the urlCollector.
do! Async.StartChild
(async { let! links = collectLinks url
for link in links do
self.Post link }) |> Async.Ignore
// Recurse into the waiting state
return! waitForUrl(visited.Add(url)) }
// This is the initial state.
waitForUrl(Set.empty))
I can think of one reason why async code would call another async block, which is that it lets you dispose of resources earlier - when the nested block completes. To demonstrate this, here is a little helper that prints a message when Dispose is called:
let printOnDispose text =
{ new System.IDisposable with
member x.Dispose() = printfn "%s" text }
The following uses nested async to do something in a nested block and then cleanup the local resources used in the nested block. Then it sleeps some more and cleans up resources used in the outer block:
async {
use bye = printOnDispose "bye from outer block"
let! r = async {
use bye = printOnDispose "bye from nested block"
do! Async.Sleep(1000)
return 1 }
do! Async.Sleep(1000) }
|> Async.Start
Here, the "nested block" resources are disposed of after 1 second and the outer block resources are disposed of after 2 seconds.
There are other cases where nesting async is useful (like returning from an asynchronous block containing try .. with), but I don't think that applies here.
I usually write Windows services in C# but I'm giving it a go in F#. For a polling serivce, like this one, I ordinarily use a class I've written, which is similar to BackgroundWorker. It spawns a background thread and fires an OnWork method at regular intervals. (Complete code is here [github].)
Is there another, perhaps better or more idiomatic, way to do this in F#? It could be a better way to write the polling background worker class, or built-in alternatives to it.
EDIT
Here's what I came up, based on Joel's suggestion.
module Async =
open System.Diagnostics
let poll interval work =
let sw = Stopwatch()
let rec loop() =
async {
sw.Restart()
work()
sw.Stop()
let elapsed = int sw.ElapsedMilliseconds
if elapsed < interval then
do! Async.Sleep(interval - elapsed)
return! loop()
}
loop()
//Example
open System.Threading
let cts = new CancellationTokenSource()
Async.Start(Async.poll 2000 (fun () -> printfn "%A" DateTime.Now), cts.Token)
Thread.Sleep(TimeSpan.FromSeconds(10.0))
cts.Cancel()
The service using poll:
type MyService() =
inherit System.ServiceProcess.ServiceBase()
let mutable cts = new CancellationTokenSource()
let interval = 2000
override __.OnStart(_) =
let polling = Async.poll interval (fun () ->
//do work
)
Async.Start(polling, cts.Token)
override __.OnStop() =
cts.Cancel()
cts.Dispose()
cts <- new CancellationTokenSource()
override __.Dispose(disposing) =
if disposing then cts.Dispose()
base.Dispose(true)
I wish there was a way to avoid the mutable CancellationTokenSource, but alas.
I might be tempted to write a simple loop in an asynchronous workflow. You can use do! Async.Sleep interval to sleep in between polling - this has two advantages: you're not tying up a thread just to have it sit idle with Thread.Sleep, and the do! automatically checks for cancellation for you if you pass a CancellationToken into Async.Start.
Plus, if your polling operation involves network communication, you're already in an async workflow, making it trivial to make async network calls.