1

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?

Ivaylo Slavov
  • 8,839
  • 12
  • 65
  • 108
  • 1
    Out of the many recomendations for people that are starting with FP from OOP, one that really stands out is that of Do Not Use Inheritance. I do not know your specific case domain but probably it can be modeled better without hierarchies. Perhaps if you lay out the specifics of what you are trying to do instead of a generic desciption we could suggest a different approach. – AMieres Jan 12 '19 at 11:34
  • @AMieres, I will try to describe my case better. Nevertheless, so far I have no issue with inheritence if I work arround the case by creating the mapping entirely on the client, thus I have the feeling it won't be an issue. Following your advice on the former question of mine, I would like to know how to transfer the data I need on the client. – Ivaylo Slavov Jan 12 '19 at 12:06
  • 1
    Passing `Map` probably fails because the type `A` may not be serializable specially if it is a `class` object. Instead of passing an object try passing the information using pure F# types like Discriminated Unions, Record types, array of tuples, etc. without any `class` object members. Also if you have a link to your actual code in github (or just paste it here) it maybe easier to understand the issue. – AMieres Jan 13 '19 at 12:38
  • I think it would be. – AMieres Jan 13 '19 at 13:28
  • Hey, @AMieres. I have created a simple example here: https://github.com/ivaylo5ev/WebSharperPassStateToClient/blob/master/Client.fs -- the entire repo is dedicated to this case. You can see the `Client.initializeMapping` method does work for with the abstract classes. I need the information of the mapping to come from the server in some way. – Ivaylo Slavov Jan 14 '19 at 14:33

1 Answers1

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
        )
AMieres
  • 4,944
  • 1
  • 14
  • 19
  • Thanks, I will take a look into this solution. Basically, I see we have the `fixType ` function, which reinstantiates each server-side passed object on the client. It did not occur to me a type match would work on the client. I will try to send the objects via RPC and apply this function on them. Still, I have to know the exact types in my client code which I am trying to avoid..Do you think if I pass the type name from the server as a plain string I could instantiate it on the client? JS would not support reflection-like stuff, but still, if that is possible I would have my problem resolved. – Ivaylo Slavov Jan 16 '19 at 07:27
  • 1
    Notice how in my solution I pass `_map` as a parameter to `HomePage`. That will remove that error. I changed my answer to better reflect this. – AMieres Jan 16 '19 at 12:56
  • I tried using simply the type names but JS code does not know the name of the type. Somehow the type match does work so I used that. Since your types are simple enough that is not a problem. If they were more complex it may fail. – AMieres Jan 16 '19 at 12:59
  • Could not get it to work with maps, used array instead (looks better in JS debugger as well). Unbelievable, it works! Now I need to test it against the real project – Ivaylo Slavov Jan 16 '19 at 15:11
  • 1
    Yes, arrays are the simplest structure to serialize. Good luck! – AMieres Jan 16 '19 at 15:19
  • I tried this on my actual use case. Too bad, the instances come erased as `{}`. Do you know if I can instantiate a type from JS knowing its exact name? I can see in the compiled JS files the full name is retained as it is in. NET. I wonder if that could help me somehow... – Ivaylo Slavov Jan 16 '19 at 21:06
  • 1
    I updated with another version that gives a unique identifier to each type and uses that to instantiate the objects, – AMieres Jan 17 '19 at 06:27
  • Trying to wrap my head around thiis... When will `registerType ` be called from the client? In my case it is called on the server, thus the map will have the type name as a key, and the unique id/ instance as a value. Wouldn't the instance be erased again when asked for in the client? Or the F# compiler will replicate the registerType calls on the client somehow during the page init? – Ivaylo Slavov Jan 17 '19 at 06:53
  • 1
    That is right. The code actually runs both in the client and in the server. Notice the conditional `IsClient` that means in the client it runs differently than in the server. The client does not use the type name instead it uses only the file & line number. – AMieres Jan 17 '19 at 06:55
  • BTW You need to mark the module `ClientCode` with `[]` otherwise it will not run in the client. – AMieres Jan 17 '19 at 06:59
  • It has `[]` which should be fine I think – Ivaylo Slavov Jan 17 '19 at 07:03
  • That works too, but make sure it has it because you were adding it to each function instead, which is not necessary and not enough. – AMieres Jan 17 '19 at 07:05
  • One more thing -- the calls to `registerType` must be made from inside the `ClientCode`as well, right? – Ivaylo Slavov Jan 17 '19 at 07:06
  • 1
    Yes, from the client and the server. That happens automatically. – AMieres Jan 17 '19 at 07:07
  • 1
    The way it is defined now, if the same object is referenced more than once it is going to reuse the same object. That may not be what you want. If you want it to create a new object each time we need to change the definition of `registerType` to use a function instead of an object. – AMieres Jan 17 '19 at 07:11
  • My fears are that the `ClientCode` module is defined before the modules that provide the actual types to register. I think I might not be able to replicate a scenario where I would inject them properly, I mean, a place that would be compiled to JS as well. Anyway, you've been very hepful and responsive so far. I learned a lot about how WebSharper JS compilation work and it would be a shame not to reward you the bounty for all the efforts. Maybe I will use what I learned to make things work in my actual project -- at least I know what need to be done now, which is a good start. THANK YOU! – Ivaylo Slavov Jan 17 '19 at 07:14