Thursday, March 04, 2021

F# and XAML and OpenSilver ctd.

Last time, we concluded that what we really want is a reimplementation of the Silverlight System.Windows.Application.LoadComponent, whereas what we have at the moment is generated C# code built from the XAML.

In practice, those generated InitializeComponent methods are a compile-time version of what the runtime component load would give us. This leads to a next step that takes us in the direction of what our ideal would be, and is in any case closer to the initial manual code-only approach.

Start with a new OpenSilver application, and add an F# netstandard2.0 library like last time. This time, however, make this assembly depend on the primary OpenSilver project, and the .Simulation and .Browser projects depend on the F# library. Add OpenSilver to the F# library project, as a nuget package, but this time there's no need to add any ExcludeAssets qualifier, nor to static link the F# core library.

The F# library now contains the Page (or UserControl) and Application classes that you would want to write, but their XAML goes into the C# project that it builds against. In the XAML project, remove everything in the initial classes after the // Enter construction logic here... comments. In the .Simulation project, point the debug parameters at the F# library, and in the .Browser project's RunApplication method, construct the F# Application type. This is important, otherwise nothing will show when you try to run the application!

We can now go one of two ways to invoke the InitializeComponent methods into the F# code.

First, we can treat the generated InitializeComponent code as almost the static utility LoadComponent method we would desire; in which case, in the F# library, the Application leeches from the corresponding generated code by

    member this.InitializeComponent() =
        let prototype = Inversion.App()
        this.Resources <- prototype.Resources

and the Page similarly

    member this.InitializeComponent() =
       let prototype = Inversion.MainPage()
       prototype.InitializeComponent()
       this.Content <- prototype.Content
       let hack = typeof.GetField("_nameScopeDictionary",
                                                System.Reflection.BindingFlags.Instance |||
                                                System.Reflection.BindingFlags.NonPublic)

       hack
       |> Option.ofObj
       |> Option.map ((fun f -> f.GetValue prototype) >> Option.ofObj)
       |> Option.flatten
       |> Option.map(fun o -> o :?> System.Collections.Generic.Dictionary<string, Object>)
       |> Option.iter (fun dict ->
             dict
             |> Seq.iter (fun kvp ->>this.RegisterName(kvp.Key, kvp.Value)))

takes the work from its pre-processed XAML, here using an over-engineered construction to get at the private name registration dictionary while avoiding any hint of NRE. These methods should then be called from the respective F# type constructors.

Alternatively, we remove the sealed qualifier from the generated Application, mark it and the MainPage class abstract, and let the F# types directly subclass the skeleton types in the C# library; this case needs no F# level InitializeComponent methods as the code we're interested in already gets called during the base class constructor call.

If this starts with the same application code as previous -- here my original Silverlight ephemeris clock -- we get the same visual appearance as with the previous approach, so no extra pictures in this post.

No comments :