Reminder: all the code for this application is in github
Going on with this series, let's see now the client/browser logic. There are a few things that the browser does without asking for the server intervention
UI interactivity: this is a background activity that we get for free thanks to two of the most popular frameworks: JQuery and Bootstrap. Our main page shows a navigation bar and an accordion which feature some dynamic behaviour; this is possible thanks to javascript logic running in the browser client process
Opening report: this is the foreground operation that occurs when the user clicks on one of the provided hyperlinks. This code is part of our application
Before looking at the relevant code, let's see what actions are performed by it
The user clicks on a report link: as a result a javascript function is invoked
This function begins sending an asynchronous AJAX request to the server, passing the wanted resource id (group id + report id)
The server receives the request and immediately retrieves the embed token for the required resource
The browser receives the embed token, so it has all the information needed to embed the resource in a page
The javascript function then continues by dynamically creating a new page and injecting the specific resource metadata
The browser scans the injected content and displays the new page with the embedded content
Client.fs and Remoting.fs
Now, so far I only mention javascript, yet in this post you'll see mainly F# code, this is because WebSharper includes a F# to javascript transpiler. You write your logic in F# and the WebSharper compiler translates it to javascript which is known by the browser.
[code language="fsharp"] [] module Client = open WebSharper.UI.Next open WebSharper.UI.Next.Html open WebSharper.JavaScript open WebSharper.JQuery open WebSharper.UI.Next.Client.Attr open WebSharper.UI.Next.Client.HtmlExtensions open Microsoft.PowerBI.Api.V2.Models open WebSharper.UI.Next.Client.HtmlExtensions
let private openReport name embedUrl reportId embedToken = let html = JQuery("#embedReportHtml").Text() .Replace("${{rplcTitle}}", "NCF.BPP - " + name) .Replace("${{rplcEmbedToken}}", embedToken) .Replace("${{rplcEmbedUrl}}", embedUrl) .Replace("${{rplcReportId}}", reportId)
let view = JS.Window.Open() view.Document.Write(html)
let private getEmbedToken gId rId = async { return! Server.getEmbedTokenAsync gId rId }
let reportClicked (e:Dom.Element) _ = async { let div = e.ParentElement let gId, rId, embedUrl = div.GetAttribute("data-groupId"), div.GetAttribute("data-reportId"), div.GetAttribute("data-embedUrl") JQuery("*").Css("cursor", "progress").Ignore let! token = getEmbedToken gId rId JQuery("*").Css("cursor", "default").Ignore match token with | Ok embedToken -> openReport e.TextContent embedUrl rId embedToken | Error message -> JS.Alert message } |> Async.Start [/code]
This F# code is translated to javascript and executed in the browser. But when? How does the reportClicked function get called? Here the part of (the already seen) Main.fs where the "magic" happens:
[code language="fsharp"] . module FrontEnd = open WebSharper.UI.Next.Html open State open WebSharper.UI.Next.Html.Tags
let private renderReport groupId (r:Report) : Doc = divAttr [ attr.``data-`` "groupId" groupId attr.``data-`` "reportId" r.id attr.``data-`` "embedUrl" r.embedUrl ] [ aAttr [attr.href "#"; on.click <@ Client.reportClicked @>] [text r.name] ] :> Doc . [/code]
The instruction on.click <@ Client.reportClicked @> is our call.
This is how WebSharper links together client and server: F# quotations. In rendering the HTML markup for the main page, the quotation becomes a javascript call.
Going back to reportClicked(), let's note a few things:
The full logic is inside an async block. As this operation could be long (it also include an AJAX roundtrip to the server), going async means that the browser UI is not locked during this operation, for a better user experience
the first two instructions retrieve the PBI resource (report in this case) metadata, which were embedded in the HTML markup by Main.fs as data- attributes
The AJAX call (let! token = getEmbedToken gId rId) calls the server passing the ids and expects the embed token back; before and after it, the mouse pointer is swapped to the waiting cursor to show the ongoing long call. Thanks to the asynchronous execution, though, the user is free to open new tabs and see different pages as the browser is not locked
getEmbedToken is defined in the Remoting.fs
[code language="fsharp"] module Server = [] let getEmbedTokenAsync groupId reportId = let token = State.getEmbedToken groupId reportId async {return token} [/code]
The browser AJAX call is routed by WebSharper to this function in the server. Which, in turn, goes to the State for the token. As we already saw, the State could answer immediately with a cached token or forward this request to PowerBI.fs which finally makes another remote call to the Power BI Cloud Services.
Either way, at the end a token (or an error) is returned to the browser. If the request resulted in an error, an alert box is displayed with the error message, otherwise another function is called, openReport, that I copy here for clarity
[code language="fsharp"] let private openReport name embedUrl reportId embedToken = let html = JQuery("#embedReportHtml").Text() .Replace("${{rplcTitle}}", "NCF.BPP - " + name) .Replace("${{rplcEmbedToken}}", embedToken) .Replace("${{rplcEmbedUrl}}", embedUrl) .Replace("${{rplcReportId}}", reportId)
let view = JS.Window.Open() view.Document.Write(html)
[/code]
This function does 4 things:
Retrieve some text from a named element in the current page (note how JQuery is called from F#)
Replace some placeholders in that text with the report metadata
Invoke the javascript library function window.open(), which is the standard way to tell to the browser (please open a new page)
Inject the text to the newly created window through another standard javascript library function (document.write())
This results in opening a new browser tab with a content directly injected by the main page, which is some html markup we are going to talk about in the next post.
Summary
With this post we get almost the complete picture of how our application works:
Main.fs, State.fs, PowerBI.fs represent the back-end business logic of our application. They maintain and update the data model and render it to the browser
Client.fs manages the user request to open a specific report and operates on the browser UI without requiring the server to render the embedding page at every user click
Remoting.fs is the only moment where the browser involves the server, to ask for the last piece of disposable metadata. It is also possible to optimize further this step by caching the embed token and expiration time browser side, hence no remote call would be performed on successive opening of the same report
Conclusion
In the next (and last) post we will see the last piece of application, the actual embedding page. This page is the only place where plain javascript has been written, without relying on WebSharper transpiling capabilities.
