3

我正在尝试更新FsXaml 中的ProgressBar.Value。在 C# 中,我使用了下面提到的代码。我没有尝试在 F# 中实现 C# 方法,因为使用公共字段 ( myCaller ) 在我看来并不是一种功能方法(更不用说我不知道​​是否可以使用这个 C# F# 中的方法)。

//C# code
namespace Special_technical_dictionary_CSharp_4._011
    {
    //...some usings
    class ExcelData
        {
         //...some code    
        public void WritingIntoDat()
            {            
            //...some code
            using (bw = new BinaryWriter(new FileStream(...some params...)))
                {
                while ((currrowIndex < (lastrowIndex + 1)))
                    {
                    //...some code                   
                    Form1.myCaller.updateProgressBarValue(100 * currrowIndex);
                    currrowIndex += 1;
                    }
                bw.Close();
                }
            //...some code 
            }
        }
    }

namespace Special_technical_dictionary_CSharp_4._011
    {
    //...some usings
    public partial class Form1 : Form
        {
        //...some code
        public static Form1 myCaller;
        
        public Form1()
            {
            InitializeComponent();
            myCaller = this;
            }
        //...some code
        public void updateProgressBarValue(int valueV)           
            => progressBar.Value = (progressBar.Value == progressBar.Maximum) ? valueV : 0;
        //...some code
        }
    }

我的问题是:F#(FsXaml/code behind)中用于更新ProgressBar.Value的最佳(或至少是好的)功能方法是什么?

编辑1:

删除了不相关的代码和文本。那些对 Elmish.WPF 不感兴趣的人请等到出现与 FsXaml 相关的答案。

编辑2:

埃尔米什.WPF

我尝试使用 Bent Tranberg 的评论和答案以及他出色的示例代码来处理ProgressBar问题。我的改编适用于for-loop,但不适用于List.map(i)/iter(i),这是我实际上需要进度条的集合函数。这是简化的代码:

文件:MainWindow.fs

//F# code
module Elmish.MainWindow

type ProgressIndicator = Idle | InProgress of percent: int

type Model =
    {                     
        ProgressIndicatorLeft: ProgressIndicator
        ProgressIndicatorRight: ProgressIndicator
    }

let initialModel = 
    {
        ProgressIndicatorLeft = Idle 
        ProgressIndicatorRight = Idle         
    }

let init() = initialModel, Cmd.none
    
type Msg =   
    | UpdateStatusLeft of progress: int
    | WorkIsCompleteLeft 
    | UpdateStatusRight of progress: int
    | WorkIsCompleteRight 
    | TestButtonLeftEvent
    | TestButtonRightEvent    

 // FOR TESTING PURPOSES ONLY
let private longRunningOperationLeft dispatch = //simulating long running operation
    async
        {
            for i in 1..100 do 
                do! Async.Sleep 20
                dispatch (UpdateStatusLeft i) //THIS WORKS
            dispatch WorkIsCompleteLeft
        }  
    
 // FOR TESTING PURPOSES ONLY
let private longRunningOperationRight dispatch = //simulating long running operation    
    async  //NOT WORKING
        {
            [1..10000]    
            |> List.mapi(fun i item -> 
                                     [1..100] |> List.reduce (*) |> ignore 
                                     dispatch(UpdateStatusRight i)   
                         ) 
            dispatch WorkIsCompleteRight
        }   

let update (msg: Msg) (m: Model) : Model * Cmd<Msg> = 
    match msg with 
        | UpdateStatusLeft progress  -> { m with ProgressIndicatorLeft = InProgress progress; ProgressBackgroundLeft = Brushes.White }, Cmd.none                      
        | WorkIsCompleteLeft         -> { m with ProgressIndicatorLeft = Idle; ProgressBackgroundLeft = Brushes.LightSkyBlue }, Cmd.none                       
        | UpdateStatusRight progress -> { m with ProgressIndicatorRight = InProgress progress; ProgressBackgroundRight = Brushes.White }, Cmd.none                       
        | WorkIsCompleteRight        -> { m with ProgressIndicatorRight = Idle; ProgressBackgroundRight = Brushes.LightSkyBlue }, Cmd.none 
        | TestButtonLeftEvent        ->
                                      let incrementDelayedCmd (dispatch: Msg -> unit) : unit =  //THIS WORKS
                                          let delayedDispatch = longRunningOperationLeft dispatch                                                      
                                          Async.StartImmediate delayedDispatch
                                      { m with ProgressIndicatorLeft = InProgress 0 }, Cmd.ofSub incrementDelayedCmd           
        | TestButtonRightEvent       ->
                                      let incrementDelayedCmd (dispatch: Msg -> unit) : unit =  //NOT WORKING              
                                          let delayedDispatch = longRunningOperationRight dispatch
                                          Async.StartImmediate delayedDispatch
                                      { m with ProgressIndicatorRight = InProgress 0 }, Cmd.ofSub incrementDelayedCmd   
 
let bindings(): Binding<Model,Msg> list =
   [      
      "ProgressLeftBackg"    |> Binding.oneWay(fun m -> m.ProgressBackgroundLeft) 
      "ProgressRightBackg"   |> Binding.oneWay(fun m -> m.ProgressBackgroundRight) 
      "ProgressLeft"         |> Binding.oneWay(fun m -> match m.ProgressIndicatorLeft with Idle -> 0.0 | InProgress v -> float v)
      "ProgressRight"        |> Binding.oneWay(fun m -> match m.ProgressIndicatorRight with Idle -> 0.0 | InProgress v -> float v)       
      "TestButtonLeft"       |> Binding.cmdIf(TestButtonLeftEvent, fun m -> match m.ProgressIndicatorLeft with Idle -> true | _ -> false)
      "TestButtonRight"      |> Binding.cmdIf(TestButtonRightEvent, fun m -> match m.ProgressIndicatorRight with Idle -> true | _ -> false) 
   ]

即使将“i”索引与进度条值绑定对于 MainWindow 中的集合功能有效,它也不能解决问题。在现实生活中,用于处理进度条值的集合函数位于主窗口文件“上方”的其他文件中。像这样:

文件:MainLogicRight.fs

//F# code
module MainLogicRight

let textBoxString3 low high path = 

    //some code

    let myArray() =            
        Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)
        |> Option.ofObj
        |> optionToArraySort "..." "..."           
        |> Array.collect
                (fun item -> 
                            let arr = 
                                let p = prefix + "*"
                                Directory.EnumerateDirectories(item, p) 
                                |> Option.ofObj
                                |> optionToArraySort "..." "..."
                                |> Array.Parallel.mapi(fun i item -> 
                                                                let arr = Directory.EnumerateFiles(item, "*.jpg", SearchOption.TopDirectoryOnly)
                                                                          |> Option.ofObj   
                                                                          |> optionToArraySort "..." "..."                                                  
                                                                arr.Length
                                                      ) 
                            arr                    
                )    

我知道(可能)无法将 pb 值与非索引函数(例如Array.collect )绑定。但重要的是 - 如何将 pb 值与List/Array.mapi/iteri 在本例中为 Array.Parallel.mapi)中的“i”索引绑定?

编辑3:

根据 Bent 的最后一个答案,我现在不相关的文本和评论已被删除。一个基于答案的例子是here

4

1 回答 1

2

这个答案解释了在 Elmish.WPF 中,如何从异步中完成对用户界面的进度更新。

在 GitHub 上创建了一个示例来演示这个。该示例还演示了另一种调用异步函数和接收结果的方法。它还演示了如何使用 mkProgram 代替 mkSimple。该演示可用作 Elmish.WPF 应用程序的起始模板。

演示中的这个片段显示了从异步更新用户界面所涉及的基本代码。

这两种技术都基于Elmish Book中的代码。您会在那里找到很多在 Elmish.WPF 中也很有用的代码。

我没有尝试在此处更新进度条,仅更新状态文本框,但从中您将很容易弄清楚如何更新任何内容。

| UpdateStatusText statusText ->
    { m with StatusText = statusText }, Cmd.none
| RunWithProgress ->
    let incrementDelayedCmd (dispatch: Msg -> unit) : unit =
        let delayedDispatch = async {
            do! Async.Sleep 1000
            dispatch (UpdateStatusText "One")
            do! Async.Sleep 1000
            dispatch (UpdateStatusText "Two")
            do! Async.Sleep 1000
            dispatch (UpdateStatusText "Three")
            }
        Async.StartImmediate delayedDispatch
    { m with StatusText = "Started progress." }, Cmd.ofSub incrementDelayedCmd

更新:

我现在已经更新了 GitHub 上的演示项目,以便它演示来自异步的进度条(和状态文本)的更新。这些是基本部分的片段。

从异步发送的两条消息的声明。

| UpdateStatus of statusText:string * progress:int
| WorkIsComplete // This message could carry a result from the work done.

处理两条消息。

| UpdateStatus (statusText, progress) ->
    { m with StatusText = statusText; Progress = progress }, Cmd.none
| WorkIsComplete ->
    { m with StatusText = "Work was completed."; Progress = 0 }, Cmd.none
| RunWithProgress ->
    let incrementDelayedCmd (dispatch: Msg -> unit) : unit =
        let delayedDispatch = async {
            do! Async.Sleep 1000
            dispatch (UpdateStatus ("Early work", 30))
            do! Async.Sleep 1000
            dispatch (UpdateStatus ("Still working", 60))
            do! Async.Sleep 1000
            dispatch (UpdateStatus ("Late work", 90))
            do! Async.Sleep 1000
            dispatch WorkIsComplete
            }
        Async.StartImmediate delayedDispatch
    { m with StatusText = "Started progress." }, Cmd.ofSub incrementDelayedCmd

字段 Progress 被声明为 int。

    Progress: int

ProgressBar 的属性 Value 是一个浮点数,因此在绑定中需要强制转换为浮点数。

"Progress" |> Binding.oneWay (fun m -> float m.Progress)

当然,我们可以将模型中的 Progress 声明为浮点数,但我想借此机会指出,模型不必与组件属性的数据类型保持一致。我们当然可以在绑定中以任何我们想要的方式进行映射。

关于调度程序的最后一点说明。这可以通过 Cmd.ofSub 和 WkProgram.Subscribe 访问。可能会在另一个场合对此进行更多介绍,但现在请注意:使用调度程序发送消息是线程安全的。这意味着您也可以从在您的顶级异步函数中运行的异步函数,或者例如从计时器事件,或任何地方,向模型发送进度消息(或任何消息)。

最终更新:GitHub 上的演示现在比这里显示的要先进一些,但原理还是一样的,所以我不会费心更新这个答案中的源代码。任何对此感兴趣的人都可能需要完整的演示源,除非您已经熟悉 Elmish.WPF


问题的最后一部分,稍后添加,在这里得到回答。

在进行冗长和/或 CPU 密集型工作时,应按照longRunningOperationLeft以下函数所示进行。这也显示了其他不应该依赖于 GUI 的功能如何以这样一种方式编写,即可以将进度更新发送到 GUI。

下面longRunningOperationRight显示的是错误的方式,阻塞了 GUI。

我在异步和任务方面的专业知识不是很好,但我认为longRunningOperationLeft从 Elmish 调用的顶级异步函数(例如 )与 Elmish 循环在同一线程上运行,这就是为什么它们不应该被任何东西阻塞冗长或 CPU 密集型。相反,这种阻塞工作需要进入子计算(例如workToDo)。的作用longRunningOperationLeft是等待工作,而不是自己做工作,以免阻塞GUI。

我不知道 List.mapi 是否可以在其中进行异步操作。我怀疑不是。无论如何,我怀疑你的真实案例不需要。

Mira 更新:你是对的。在我的现实生活中不需要。在List/array.mapi中添加reportProgress i(就像在您的代码中一样)就足够了。

let private lengthyWork () =
    [1..20_000_000] |> List.reduce ( * ) |> ignore

let private workToDo reportProgress = async {
    reportProgress 0
    lengthyWork ()
    reportProgress 25
    lengthyWork ()
    reportProgress 50
    lengthyWork ()
    reportProgress 75
    lengthyWork ()
    reportProgress 100
    return 7
    }

// This is good.
let private longRunningOperationLeft dispatch = async {
    let reportProgress progress = dispatch (UpdateStatusLeft progress)
    let! hardWork = Async.StartChild (workToDo reportProgress)
    do! Async.Sleep 1000 // Can do some async work here too, while waiting for hardWork to finish.
    let! result = hardWork
    dispatch WorkIsCompleteLeft
    }

// This is not good. Blocking GUI.
let private longRunningOperationRight dispatch = async {
    dispatch (UpdateStatusRight 0)
    lengthyWork ()
    dispatch (UpdateStatusRight 25)
    lengthyWork ()
    dispatch (UpdateStatusRight 50)
    lengthyWork ()
    dispatch (UpdateStatusRight 75)
    lengthyWork ()
    dispatch (UpdateStatusRight 100)
    dispatch WorkIsCompleteRight
    }
于 2021-12-04T21:43:52.313 回答