Closed moonshxne closed 1 year ago
I'm glad you are enjoying it!
I think the App
is inaccessible due to file ordering.
But rather than trying to change the example, it would be better IMO to simple create our own Services
module:
namespace AvaloniaExample
open System
open Microsoft.Extensions.DependencyInjection
// Putting sample service here for convenience
type IFileService =
abstract member OpenFile: unit -> string option
type FileService() =
interface IFileService with
member this.OpenFile() = failwith "Not Implemented"
module Services =
let container : IServiceProvider =
let services = ServiceCollection()
services.AddSingleton<IFileService>(FileService()) |> ignore
services.BuildServiceProvider()
open AvaloniaExample
open AvaloniaExample.Services
open Microsoft.Extensions.DependencyInjection
// Somewhere in your VM
let fileProvider = container.GetService<IFileService>()
let filepath = fileProvider.OpenFile()
I'm not saying this is necessarily the best way to do it, but it is similar to the example you provided.
Hmm. The container under the Services module does seem like a clean idea, but the issue is, the FilesService requires a reference to an Avalonia Window in order to access those underlying StorageProvider APIs (which is why the example did the dependency injection inside of App.axaml.cs).
Ahh I see. In that case, you could just make it mutable and initialize it within App
:
namespace AvaloniaExample
open System
type IFileService =
abstract member OpenFile: unit -> string option
type FileService() =
interface IFileService with
member this.OpenFile() = failwith "Not Implemented"
module Services =
let mutable container : IServiceProvider = null
namespace AvaloniaExample
open Avalonia
open Avalonia.Markup.Xaml
open AvaloniaExample.Views
open Avalonia.Controls.ApplicationLifetimes
open Microsoft.Extensions.DependencyInjection
type App() =
inherit Application()
override this.Initialize() =
// Initialize Avalonia controls from NuGet packages:
let _ = typeof<Avalonia.Controls.DataGrid>
let services = ServiceCollection()
// Init file service here
services.AddSingleton<IFileService>(FileService()) |> ignore
Services.container <- services.BuildServiceProvider()
AvaloniaXamlLoader.Load(this)
override this.OnFrameworkInitializationCompleted() =
match this.ApplicationLifetime with
| :? IClassicDesktopStyleApplicationLifetime as desktop ->
let view = MainView()
desktop.MainWindow <- view
ViewModels.MainViewModel.vm.StartElmishLoop(view)
| _ ->
// leave this here for design view re-renders
()
base.OnFrameworkInitializationCompleted()
At some point I will look into a more integrated way to do DI, but this should work for now.
Slight correction -- you'd have to do it in the OnFrameInitializationCompleted
method, but yes! This definitely works for now. :)
So, for posterity:
namespace AvaloniaExample
open Avalonia.Controls
open System
type IFileService =
abstract member OpenFile: unit -> string option
type FileService(window: Window) =
interface IFileService with
member this.OpenFile() = window.StorageProvider.blah blah blah
module Services =
let mutable container : IServiceProvider = null
namespace AvaloniaExample
open Avalonia
open Avalonia.Markup.Xaml
open AvaloniaExample.Views
open Avalonia.Controls.ApplicationLifetimes
open Microsoft.Extensions.DependencyInjection
type App() =
inherit Application()
override this.Initialize() =
// Initialize Avalonia controls from NuGet packages:
let _ = typeof<Avalonia.Controls.DataGrid>
AvaloniaXamlLoader.Load(this)
override this.OnFrameworkInitializationCompleted() =
match this.ApplicationLifetime with
| :? IClassicDesktopStyleApplicationLifetime as desktop ->
let view = MainView()
desktop.MainWindow <- view
let services = ServiceCollection()
// Init file service here
services.AddSingleton<IFileService>(FileService(desktop.MainWindow)) |> ignore
Services.container <- services.BuildServiceProvider()
ViewModels.MainViewModel.vm.StartElmishLoop(view)
| _ ->
// leave this here for design view re-renders
()
base.OnFrameworkInitializationCompleted()
It's a bit ugly I think, but that's just the nature of DI. This works, and thank you so much for your help!
Another subtle improvement (move services init to services + static members):
namespace AvaloniaExample
open System
open Avalonia.Controls
open Microsoft.Extensions.DependencyInjection
type IFileService =
abstract member OpenFile: unit -> string option
type FileService(window: Window) =
interface IFileService with
member this.OpenFile() = Some "c:/files/file.txt" //window.StorageProvider.OpenFilePickerAsync
type Services() =
static let mutable container : IServiceProvider = null
static member Container
with get() = container
static member Init mainWindow =
let services = ServiceCollection()
services.AddSingleton<IFileService>(FileService(mainWindow)) |> ignore
container <- services.BuildServiceProvider()
static member Get<'Svc>() =
container.GetRequiredService<'Svc>()
override this.OnFrameworkInitializationCompleted() =
match this.ApplicationLifetime with
| :? IClassicDesktopStyleApplicationLifetime as desktop ->
let view = MainView()
desktop.MainWindow <- view
Services.Init view
ViewModels.MainViewModel.vm.StartElmishLoop(view)
| _ ->
// leave this here for design view re-renders
()
module AvaloniaExample.ViewModels.AboutViewModel
open Elmish.Avalonia
open Elmish
open Messaging
open AvaloniaExample
open Microsoft.Extensions.DependencyInjection
type Model =
{
Version: string
}
type Msg =
| Ok
let init () =
{
Version = "1.1"
}, Cmd.ofEffect (fun _ ->
let fileProvider = Services.Get<IFileService>()
let filepath = fileProvider.OpenFile()
printfn $"Filepath: {filepath}"
)
let update (msg: Msg) (model: Model) =
match msg with
| Ok ->
model, Cmd.ofEffect (fun _ -> bus.OnNext(GlobalMsg.GoHome))
let bindings () : Binding<Model, Msg> list = [
"Version" |> Binding.oneWay (fun m -> m.Version)
"Ok" |> Binding.cmd Ok
]
let designVM = ViewModel.designInstance (fst (init())) (bindings())
let vm = ElmishViewModel(AvaloniaProgram.mkProgram init update bindings)
Here's another variation, this time added to the update
function instead:
This passes in only the function signature(s) needed.
module AvaloniaExample.ViewModels.FilePickerViewModel
open Elmish.Avalonia
open Elmish
open Messaging
open AvaloniaExample
type Model =
{
FilePath: string option
}
type Msg =
| Ok
| PickFile
| SetFilePath of string option
let init () =
{
FilePath = None
}, Cmd.none
let update tryPickFile (msg: Msg) (model: Model) =
match msg with
| Ok ->
model, Cmd.ofEffect (fun _ -> bus.OnNext(GlobalMsg.GoHome))
| PickFile ->
model, Cmd.OfTask.perform tryPickFile () SetFilePath
| SetFilePath path ->
{ model with FilePath = path }, Cmd.none
let bindings () : Binding<Model, Msg> list = [
"Ok" |> Binding.cmd Ok
"FilePath" |> Binding.oneWay (fun m -> m.FilePath |> Option.defaultValue "Not Set")
"PickFile" |> Binding.cmd PickFile
]
let designVM =
ViewModel.designInstance (fst (init ())) (bindings())
let vm () =
let tryPickFile () =
let fileProvider = Services.Get<FileService>()
fileProvider.TryPickFile()
ElmishViewModel(AvaloniaProgram.mkProgram init (update tryPickFile) bindings)
This way, the DI / "service locator" stuff is isolated to the vm ()
function.
It could be better (or not) depending on the situation (how many side effect functions you need, what you need to test, whether you are writing tests at all, etc).
Testing the update
function will only require stubbing out the tryPickFile
function which is much easier than stubbing out an entire interface.
Added new page to sample for future reference:
Elmish.Avalonia has been a lovely and sorely needed abstraction on top of Avalonia!
But, since we aren't creating classes for our ViewModels anymore, what's the best way to access the StorageProvider for stuff like opening and saving files? For now, in each of the ViewModels that need this functionality, I've been doing
but the Avalonia docs say this is actually hacky and not recommended for real-world usage. 😅
I also tried to use dependency injection by creating a FilesService and initializing it in App.axaml.fs, but I couldn't access any
App
in my view model like they did in this example.How would you go about this?