如何在F#中在多个模块之间共享单个记录实例?

huangapple go评论59阅读模式
英文:

How to share a single record instance between multiple modules in F#?

问题

我正在使用WPF/C#来创建我的UI,连接到一个F#后端。我正在从C#代码传递多个委托来创建多个窗口,然后传递给我的主要F#程序模块。

如果我有一个用于保存函数委托的单个记录类型(在使用invoke之前); 我如何从多个不同的F#模块中访问此记录?

举个例子,假设我在顶级文件中声明了这个记录:

type Windows = {
    createReportMenu: Func<Window> 
    createProgressNote: Func<Window>
    createPatientRegistrationWindow: Func<Window>
}

然后将这些委托传递给我的主程序:

Program.main(MainWindow, () => new ReportMenu(), () => new ProgressNoteWindow2(), () => new PatientRegistrationWindow() )

如何创建记录Windows的单个实例,并在多个不同的F#模块之间共享它?(有点像全局记录 如何在F#中在多个模块之间共享单个记录实例? )。

感谢任何帮助或建议。

英文:

I am using WPF/C# for my UI to a F# backend. I am passing multiple delegates to create several windows from the C# code to my main F# program module.

If I had a single record type to hold the function delegates (prior to using invoke); how do I access this record from multiple different F# modules?

As an example, lets say I have this record declared in a top file:

type Windows = {
        createReportMenu: Func&lt;Window&gt; 
        createProgressNote: Func&lt;Window&gt;
        createPatientRegistrationWindow: Func&lt;Window&gt;
    }

And these delegates passed to my main program:

Program.main(MainWindow, () =&gt; new ReportMenu(), () =&gt; new ProgressNoteWindow2(), () =&gt; new PatientRegistrationWindow() )

How do you create a single instance of the record Windows and share it among multiple different F# modules? (Kind of like a Global record 如何在F#中在多个模块之间共享单个记录实例? ).

Thanks for any help or suggestions.

答案1

得分: 1

我已翻译您提供的文本,以下是翻译好的部分:

在我的 Elmish.WPF 应用程序中,我已经做了非常类似的事情。这样做的原因是,否则传递所有窗口创建函数将变得越来越难以忍受,而且是一项高风险的活动。

我将分享我的源代码的部分,用于执行这个操作。如果详情不足,请告诉我,我将继续编辑,直到满足您的需求。

以下内容在我的 Elmish.WPF 程序中使用,但我认为它也可以在其他架构中使用,例如纯粹的代码后端。

在 F# 中,我在一个地方声明了一个名为 WindowCreationService 的类型。这是我保存从 F# 创建特定窗口的所有函数定义的地方。

nullWindowCreationService 是一个在 Elmish.WPF 的设计时模型中使用的虚拟对象,只是为了编译通过,但它不会被调用。如果您不需要它,可以省略。

type WindowCreationService(skriverValg, snDlg, resDlg, ordDlg, veksleDlg) =
    member val CreateSkriverValgWindow : unit -> Window = skriverValg
    member val CreateSerieNrDialog : unit -> Window = snDlg
    member val CreateReseptAktiveringDialog : unit -> Window = resDlg
    member val CreateOrdreRedigerDialog : unit -> Window = ordDlg
    member val CreateVeksleDlg : unit -> Window = veksleDlg
static member nullWindowCreationService =
        WindowCreationService(
            (fun () -> failwith "Never called"),
            (fun () -> failwith "Never called"),
            (fun () -> failwith "Never called"),
            (fun () -> failwith "Never called"),
            (fun () -> failwith "Never called")
        )

C# WPF 程序使用了包含程序逻辑的 F# 库。假设程序名为 MyProgram,F# 库名为 MyProgram.Models,其中包含一个名为 Program 的模块,其中包含一个名为 main 的函数。

C# 程序将在某个时候将控制权移交给 F#。这是在 C# 端完成的相关部分。如果使用代码后端,则不一定要在 OnActivated 中执行此调用。oneShot 只是为了确保仅在第一次调用 OnActivated 时执行对 main 的调用。

public partial class App : Application
{
    bool oneShot = false;

    ...

    protected override void OnActivated(EventArgs e)
    {
        base.OnActivated(e);
        if (!oneShot)
        {
            oneShot = true;
            Models.Program.main(mainWindow: MainWindow,
                createSkriverWin: () => new Views.SkriverValg(),
                createSerieNrDialog: () => new Views.SerieNrDialog(),
                createReseptAktiveringDialog: () => new Views.ReseptAktiveringDialog(),
                createOrdreRedigeringDialog: () => new Views.OrdreRedigerDialog(),
                createVeksleCfgDlg: () => new Views.VeksleCfgDlg()
            );
        }
    }
}

在 F# 模块 Program 中的 main 函数如下所示,除了一些特定于我的应用程序的行之外,它们的缺失应该不相关。匹配 klientIni.LogElmishWpf 只是为了让我从设置中选择是否启用日志记录,这只在 Elmish.WPF 应用程序中才相关。

let main (mainWindow: Window)
        (createSkriverWin: Func<Window>)
        (createSerieNrDialog: Func<Window>)
        (createReseptAktiveringDialog: Func<Window>)
        (createOrdreRedigeringDialog: Func<Window>)
        (createVeksleCfgDlg: Func<Window>)
    =
    let createOwnedWindow (theCreate: Func<Window>) =
        let w = theCreate.Invoke()
        w.Owner <- mainWindow
        w
    let windowCreationService = WindowCreationService(
        skriverValg=(fun () -> createOwnedWindow createSkriverWin),
        snDlg=(fun () -> createOwnedWindow createSerieNrDialog),
        resDlg=(fun () -> createOwnedWindow createReseptAktiveringDialog),
        ordDlg=(fun () -> createOwnedWindow createOrdreRedigeringDialog),
        veksleDlg=(fun () -> createOwnedWindow createVeksleCfgDlg)
    )
    let init = fun () -> MainWindow.init ()
    let bindings () = MainWindow.bindings windowCreationService
    WpfProgram.mkProgram init MainWindow.update bindings
    |> (fun program ->
        match klientIni.LogElmishWpf with
        | true -> WpfProgram.withLogger (TdDesktop.SerilogRouter.getSeriLoggerFactory ()) program
        | false -> program
        )
    |> WpfProgram.startElmishLoop mainWindow

mainWindow(类型为 Window)之所以从 C# 带过来,是因为我希望所有窗口都将 mainWindow 作为它们的所有者。这将使得这些窗口在创建时应该在它们的所有者窗口中居中显示,这很好。

如果您正好使用 Elmish.WPF,您可能还想了解接下来会发生什么。在上面,您可以看到 windowCreationService 的使用。

接下来,这是用于声明 MainWindow 的函数绑定的方式,以及那些绑定如何使用 WindowCreationService。借助函数和柯里化的帮助,我们将 wcs 添加到这些绑定中。

let bindings (wcs: WindowCreationService) : Binding<Model, Msg> list =
    [
    ...
        "ProduksjonPane" |> Binding.subModelOpt ((fun m -> m.ProduksjonPane), snd, ProduksjonPaneMsg, (fun () -> ProduksjonPane.bindings wcs))
        "OrdrePane" |> Binding.subModelOpt ((fun m -> m.OrdrePane), snd, OrdrePaneMsg, (fun () -> OrdrePane.bindings wcs))
        "ResepterPane" |> Binding.subModelOpt ((fun m -> m.ResepterPane), snd, ResepterPaneMsg, (fun () -> ResepterPane.bindings wcs))
    ...
    ]

因此,MainWindow 的绑定函数将 wcs 传递给其他

英文:

I have done something very similar in my Elmish.WPF applications. The reason for doing this is that it otherwise becomes an increasingly intolerable nightmare and high-risk activity to manage the passing around of all the window creation functions.

I will share the parts of my source that do this. Let me know if it's not enough detail, and I'll keep editing until it's good enough.

The following is used in one of my Elmish.WPF programs, but I guess it should be usable also in other architectures, e.g. plain code-behind.

In F# I have declared a type WindowCreationService in one place. This is where I keep all the function definitions for creating specific windows from F#.

The nullWindowCreationService is a kind of dummy for use with the design time model in Elmish.WPF, just so that it compiles, but it won't be called. You can leave this out if you don't need it for that kind of use.

type WindowCreationService(skriverValg, snDlg, resDlg, ordDlg, veksleDlg) =
    member val CreateSkriverValgWindow : unit -&gt; Window = skriverValg
    member val CreateSerieNrDialog : unit -&gt; Window = snDlg
    member val CreateReseptAktiveringDialog : unit -&gt; Window = resDlg
    member val CreateOrdreRedigerDialog : unit -&gt; Window = ordDlg
    member val CreateVeksleDlg : unit -&gt; Window = veksleDlg
    static member nullWindowCreationService =
        WindowCreationService(
            (fun () -&gt; failwith &quot;Never called&quot;),
            (fun () -&gt; failwith &quot;Never called&quot;),
            (fun () -&gt; failwith &quot;Never called&quot;),
            (fun () -&gt; failwith &quot;Never called&quot;),
            (fun () -&gt; failwith &quot;Never called&quot;)
            )

The C# WPF program uses the F# library with the program's logic. Let's say the program is named MyProgram, and the F# library MyProgram.Models, and it contains a module named Program with a function named main.

The C# program will at some point hand over control to F#. This is the relevant part of how that is done on the C# side. If you use code-behind, then this call is not necessarily done in OnActivated. The oneShot is just there to make sure the call to main is only done once - the first time OnActivated is called.

public partial class App : Application
{
    bool oneShot = false;

    ...

    protected override void OnActivated(EventArgs e)
    {
        base.OnActivated(e);
        if (!oneShot)
        {
            oneShot = true;
            Models.Program.main(mainWindow:MainWindow,
                createSkriverWin:() =&gt; new Views.SkriverValg(),
                createSerieNrDialog:() =&gt; new Views.SerieNrDialog(),
                createReseptAktiveringDialog:() =&gt; new Views.ReseptAktiveringDialog(),
                createOrdreRedigeringDialog:() =&gt; new Views.OrdreRedigerDialog(),
                createVeksleCfgDlg:() =&gt; new Views.VeksleCfgDlg()
                );
        }
    }

And the main function in module Program in F# looks like this, except for some lines that are specific to my application, and their absence should be irrelevant. That match on klientIni.LogElmishWpf is only there so that I can choose from a setting whether to engage logging or not, which also is only relevant in an Elmish.WPF application.

let main (mainWindow: Window)
        (createSkriverWin: Func&lt;Window&gt;)
        (createSerieNrDialog: Func&lt;Window&gt;)
        (createReseptAktiveringDialog: Func&lt;Window&gt;)
        (createOrdreRedigeringDialog: Func&lt;Window&gt;)
        (createVeksleCfgDlg: Func&lt;Window&gt;)
        =
    let createOwnedWindow (theCreate: Func&lt;Window&gt;) =
        let w = theCreate.Invoke()
        w.Owner &lt;- mainWindow
        w
    let windowCreationService = WindowCreationService(
        skriverValg=(fun () -&gt; createOwnedWindow createSkriverWin),
        snDlg=(fun () -&gt; createOwnedWindow createSerieNrDialog),
        resDlg=(fun () -&gt; createOwnedWindow createReseptAktiveringDialog),
        ordDlg=(fun () -&gt; createOwnedWindow createOrdreRedigeringDialog),
        veksleDlg=(fun () -&gt; createOwnedWindow createVeksleCfgDlg)
        )
    let init = fun () -&gt; MainWindow.init ()
    let bindings () = MainWindow.bindings windowCreationService
    WpfProgram.mkProgram init MainWindow.update bindings
    |&gt; (fun program -&gt;
        match klientIni.LogElmishWpf with
        | true -&gt; WpfProgram.withLogger (TdDesktop.SerilogRouter.getSeriLoggerFactory ()) program
        | false -&gt; program
        )
    |&gt; WpfProgram.startElmishLoop mainWindow

The reason mainWindow (of type Window) was brought over from C#, is that I want all windows to have mainWindow as their owner. That will make it possible that these windows should be centered in their owner window upon creation, which is nice.

If you happen to use Elmish.WPF, you'll likely want to know what happens from here on as well. You see above where the windowCreationService is used.

Next, this is how the function bindings for the MainWindow is declared, and how the bindings there use the WindowCreationService. With the help of a fun and currying, we tag on wcs to these bindings.

let bindings (wcs: WindowCreationService) : Binding&lt;Model, Msg&gt; list =
    [
    ...
        &quot;ProduksjonPane&quot; |&gt; Binding.subModelOpt ((fun m -&gt; m.ProduksjonPane), snd, ProduksjonPaneMsg, (fun () -&gt; ProduksjonPane.bindings wcs))
        &quot;OrdrePane&quot; |&gt; Binding.subModelOpt ((fun m -&gt; m.OrdrePane), snd, OrdrePaneMsg, (fun () -&gt; OrdrePane.bindings wcs))
        &quot;ResepterPane&quot; |&gt; Binding.subModelOpt ((fun m -&gt; m.ResepterPane), snd, ResepterPaneMsg, (fun () -&gt; ResepterPane.bindings wcs))
    ...
    ]

So the bindings function for the MainWindow will pass wcs on to other bindings functions, which again will pass it on to other bindings functions lower down in the hierarchy, as deep as necessary.

If we look at just one of these - the ProduksjonPane bindings - pat of it looks like this.

let bindings (wcs: WindowCreationService) =
    [
        ...
        &quot;SerieNrDialog&quot; |&gt; Binding.subModelWin((fun m -&gt; m.SerieNrDialog |&gt; WindowState.ofOption),
            snd, SerieNrDialogMsg, SerieNrDialog.bindings, wcs.CreateSerieNrDialog,
            onCloseRequested=(SerieNrDialogMsg SerieNrDialog.Msg.Cancel), isModal=true)
        ...
    ]

Here we finally got to see actual use of one of the window creation functions. The wcs.CreateSerieNrDialog is handed over to the Binding.subModelWin of Elmish.WPF, which will use it to create a window at the proper time.

答案2

得分: 1

这是一种替代性和简单得多的解决方法,正如在我的第一个答案的评论线程中讨论的那样。

在 F# 项目中,我在所有需要访问在此声明和初始化的 wcs 的模块之前拥有此模块。AutoOpen 用于使不需要在其他地方打开或限定以访问 wcs。名为 createWindowCreationService 的函数封装了从 C# WPF 程序中需要执行的逻辑,因此我们不必在 C# 中编写此逻辑。

特别注意,mainWindow 和从不调用的虚拟函数使用 null 声明和初始化 wcs,这也发生在设计时间,当不调用 createWindowCreationService 时。如果 wcs 只是被赋予了 null,那么这将在设计时间引发异常。这是因为绑定引用了 wcs.CreateProduktVelgerWindow,并且绑定在设计时间用于在设计器中显示窗口。现在,设计时间不是通过 wcs 创建窗口,而只是尝试获取 wcs.CreateProduktVelgerWindow,所以不能为 null。

在 C# WPF 项目中,在 App.xaml.cs 中,有这段初始化 wcs 的代码。createWindowCreationService 在主函数之前被调用,这样 wcs 在绑定尝试访问它之前被重新初始化。

应该很容易根据需要添加更多的窗口创建函数。只需查看其他答案以掌握操作方法。

我使用了命名参数,因为随着函数列表的增长,这是一个优势。

英文:

This is an alternative and much simpler way to solve it, as discussed in the comment thread of my first answer.

In the F# project, I have this module before all modules that need access to the wcs declared and initialized here. AutoOpen is used so that there's no need to open or qualify elsewhere to get access to wcs. The function named createWindowCreationService wraps the logic needed to be done from the C# WPF program, so we don't have to fiddle with writing this in C#.

Note in particular that the wcs is declared and initialized with null for the mainWindow and a dummy function that is never called, and this also happens at design time when no call to createWindowCreationService is made. If wcs had just been assigned null, then that would provoke an exception at design time. This is because the bindings refer to wcs.CreateProduktVelgerWindow, and the bindings are used at design time in order to show the window in the designer. Now, the design time is not trying to create a window through wcs, but just trying to get the wcs.CreateProduktVelgerWindow, so has to go through wcs, so therefore this must not be null.

module [&lt;AutoOpen&gt;] TdPakke.Models.Services

open System
open System.Windows

type [&lt;AllowNullLiteral&gt;] WindowCreationService(mainWindow: Window,
        produktVelger: Func&lt;Window&gt;) =

    let createOwnedWindow (theCreate: Func&lt;Window&gt;) =
        let w = theCreate.Invoke()
        w.Owner &lt;- mainWindow
        w

    member val CreateProduktVelgerWindow : unit -&gt; Window =
        fun () -&gt; createOwnedWindow produktVelger

let mutable wcs: WindowCreationService =
    WindowCreationService(null,
        (fun () -&gt; failwith &quot;Never called&quot;)
    )

let createWindowCreationService(mainWindow: Window,
        produktVelger: Func&lt;Window&gt;) =
    wcs &lt;- WindowCreationService(mainWindow,
    produktVelger=produktVelger
    );

In the C# WPF project, in the App.xaml.cs, there's this code that initializes the wcs. The createWindowCreationService is called before main, so that wcs is reinitialized before the bindings try to access it.

private void StartElmish(object sender, EventArgs e)
{
    this.Activated -= StartElmish;
    Models.Services.createWindowCreationService(mainWindow:MainWindow,
        produktVelger: () =&gt; new ProduktVelger()
        );
    Models.Program.main(mainWindow:MainWindow);
}

It should be easy enough to add more window creation functions as needed. Just look at the other answer to get the hang of it.

I have used named parameters because that's an advantage as the list of functions grows.

huangapple
  • 本文由 发表于 2023年5月14日 11:37:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/76245710.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定