WebSharper - How to expose dynamically mapped stra

2020-07-27 05:44发布

问题:

I am at the process of learning WebSharper, and I am struggling with making some of my logic work on the client-side.

I have a few server-side objects with inheritance hierarchy, which I need to expose to the client-side. They deal with generating different parts of the page as doc fragments -- but on the client.

[<JavaScriptExport>]
type [<AbstractClass>] A() = 
    abstract member Doc: Map<string, A> -> Doc
    ...

[<JavaScriptExport>]
type [<AbstractClass>] B() =
    inherit A()

[<JavaScriptExport>]
type C() =
    inherit B()

The post is updated, see history for the older version.

In my server-side code I have a map associating each object with a given name (as in the strategy design pattern):

let mutable objectMap : Map<string, A> = Map.empty

At some point, the map gets filled with data. This happens once only in the application initialization phase, but is a result of the server-side backend logic.

Now, I need to use those objects on the client-side only, like in the overly-simplified snippet below:

[<JavaScriptExport>]
type C() =
   inherit B() // or inherit A()
   override this.Doc map =
       div [] [ map.["innerDoc"].Doc(map)

On the server-side end I would have:

module ServerSide =
     let Main ctx endpoint ... =
         // this is the mapping that is being generated on the server, derived somehow from the `objectMap ` above:
         let serverMap : Map<string, something> = ...

         let document =
             [
                 div [ on.afterRender(fun _ -> ClientCode.fromServerMap(serverMap));][client <@ ClientCode.getDoc("someDoc") @>]
             ] |> Doc.Concat
         Page.Content document 

The ClientCode module would be something that gets compiled to JS and would look like this:

[<JavaScript>]
moduel ClientCode =
    let _map : Var<Map<string, A>> = Var.Create <| Map.empty

    let fromServerMap (serverMap : something) =
        let clientMap : Map<string, A> = // TODO: get the information from server map
         _map.Set clientMap

    let getDoc (docName : string) =
        _map.View.Map(fun m -> m.[docName].Doc(m))
        |> Doc.EmbedView

So far I've found out that simply returning the map via an Rpc during the afterRender would not work -- either generic JS objects are being returned, or I am receiving a serialization error. Looks like this is the expected behavior for the WebSharper remoting and clinet-server communication.

I know I could just implement my ClientModule.obtainObject by hardCoding the A instances inside my map and it does work if I do so, but I need to avoid that part. The module I am developing does not have to know the exact mapping or implementation of the types inheriting from A (like B and C for example), nor what names they have been associated with.

What other approaches I need to use to pass the information from the server-side object map to the client? Maybe use something like Quotation.Expr<A> in my code?

Update 1: I do not necessarily need to instantiate the objects on the server. Maybe there is a way to send the mapping information to the client and let it do the instantiation somehow?

Update 2: Here is a github repo with a simple representation of what I have got working so far

Update 3: An alternative approach would be to keep on the server a mappping that would use the name of my object type instead of an instance of it (Map<string, string>). Now if my client code sees ClientAode.C of whatever the full type name is, is it possible to invoke the default constructor of that type entirely from JavaScript?

回答1:

Here is another take

In this case I create a dictionary called types that gives each class a unique identifier based on the file and line number. The server and client versions are slightly different. The server version uses the type name as the key while the client uses the file & line number as a key (Client.fs):

    let types = new System.Collections.Generic.Dictionary<string, string * A>()

    let registerType line (a:'a) =
        if IsClient 
        then types.Add(line                  , (line, a :> A) )     
        else types.Add(typedefof<'a>.FullName, (line, a :> A) )

    registerType (__SOURCE_FILE__ + __LINE__) <| C()
    registerType (__SOURCE_FILE__ + __LINE__) <| D()

    let fixType v =
        match types.TryGetValue v with
        | false, _         -> C() :> A
        | true , (line, a) -> a

    let fixMap (m:Map<string, string>) =
        m |> Seq.map (fun kvp -> kvp.Key, fixType kvp.Value) |> Map


[<JavaScript>]
module Client =

    let getDoc (m:Map<string, string>) (docName : string) =
        let m = ClientCode.fixMap m
        m.[docName].Doc(m)

On the server side I changed the _map that was Map<string,ClientCode.A> to Map<string, string>. The client does the same thing but in reverse.

The dictionary types acts literally as a dictionary for both the server and the client to translate back and forth between unique name and actual object.

(Site.fs):

[< JavaScript false >]
module Site =
    open WebSharper.UI.Html

    let HomePage _map ctx =
        Templating.Main ctx EndPoint.Home "Home" [
            Doc.ClientSide <@  Client.getDoc _map "C" @>
        ]

    let mutable _map : Map<string, string> = Map.empty

    let addMapping<'T> name = 
        match ClientCode.types.TryGetValue (typedefof<'T>.FullName) with
        | false,_         -> printfn "Could not map %s to type %s. It is not registered" name (typedefof<'T>.FullName)
        | true ,(line, a) -> 
        _map <- _map |> Map.add name line

    addMapping<ClientCode.C> "C"
    addMapping<ClientCode.D> "D"


    [<Website>]
    let Main =
        Application.MultiPage (fun ctx endpoint ->
            match endpoint with
            | EndPoint.Home -> HomePage _map ctx
        )