在 FsXaml 和 ElmishWPF 中更新 ProgressBar.Value
Updating ProgressBar.Value in FsXaml and ElmishWPF
我正在尝试在 FsXaml 中更新 ProgressBar.Value。在 C# 中,我使用了下面提到的代码。我没有尝试在 F# 中实现 C# 方法,因为使用 public 字段 (myCaller) 在我看来并不是一种功能性方法(更不用说我不知道是否可以在 F# 中使用这种 C# 方法。
//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# 中用于更新 ProgressBar.Value[= 的最佳(或至少是好的)功能方法是什么(FsXaml/code 落后) 56=]?
编辑 1:
不相关的代码和文本已删除。对 Elmish.WPF 不感兴趣的人请等待,直到出现与 FsXaml 相关的答案。
编辑2:
Elmish.WPF
我尝试使用 Bent Tranberg 的评论和回答以及 his excellent example code 来处理 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 中的收集函数有效,也无法解决问题。在现实生活中,用于处理进度条值的集合函数位于主 window 文件“上方”的其他文件中。像这样:
文件: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 中的“i”索引绑定。mapi/iteri ( Array.Parallel.mapi 在这种情况下) ?
编辑3:
根据Bent 的最后回答,删除了我现在无关的文本和评论。
基于答案的示例是 here.
此答案解释了在 Elmish.WPF 中如何从异步完成对用户界面的进度更新。
我创建了一个演示此功能的 example on 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 的 属性 值为浮点数,因此需要在绑定中转换为浮点数。
"Progress" |> Binding.oneWay (fun m -> float m.Progress)
当然我们可以在模型中将 Progress 声明为浮点数,但我想借此机会指出模型不必与组件属性的数据类型对齐。我们当然可以在绑定中以任何我们想要的方式映射。
关于调度程序的最后一点说明。这可以通过 Cmd.ofSub 访问,也可以通过 WkProgram.Subscribe 访问。也许在其他情况下会详细介绍这一点,但现在请注意:使用调度程序发送消息是线程安全的。这意味着您也可以从顶级异步函数中 运行 的异步函数向模型发送进度消息(或任何消息),或者例如来自计时器事件,或任何地方。
最终更新:GitHub 上的演示现在比此处显示的稍微高级一些,但原理仍然相同,因此我不会费心更新此答案中的源代码。任何对此感兴趣的人很可能无论如何都需要完整的演示源,除非你已经很了解 Elmish.WPF
问题的最后一部分,后来补充的,在这里回答。
当进行冗长的and/or CPU 密集型工作时,应按照下面的longRunningOperationLeft
功能所示进行操作。这也显示了其他地方的功能,不应该依赖于 GUI,可以以这样一种方式编写,即进度更新可以发送到 GUI。
下面显示的 longRunningOperationRight
做错了,阻塞了 GUI。
我在异步和任务方面的专业知识不是很好,但我认为从 Elmish 调用的顶级异步函数(例如 longRunningOperationLeft
)与 运行在同一个线程上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
}
我正在尝试在 FsXaml 中更新 ProgressBar.Value。在 C# 中,我使用了下面提到的代码。我没有尝试在 F# 中实现 C# 方法,因为使用 public 字段 (myCaller) 在我看来并不是一种功能性方法(更不用说我不知道是否可以在 F# 中使用这种 C# 方法。
//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# 中用于更新 ProgressBar.Value[= 的最佳(或至少是好的)功能方法是什么(FsXaml/code 落后) 56=]?
编辑 1:
不相关的代码和文本已删除。对 Elmish.WPF 不感兴趣的人请等待,直到出现与 FsXaml 相关的答案。
编辑2:
Elmish.WPF
我尝试使用 Bent Tranberg 的评论和回答以及 his excellent example code 来处理 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 中的收集函数有效,也无法解决问题。在现实生活中,用于处理进度条值的集合函数位于主 window 文件“上方”的其他文件中。像这样:
文件: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 中的“i”索引绑定。mapi/iteri ( Array.Parallel.mapi 在这种情况下) ?
编辑3:
根据Bent 的最后回答,删除了我现在无关的文本和评论。 基于答案的示例是 here.
此答案解释了在 Elmish.WPF 中如何从异步完成对用户界面的进度更新。
我创建了一个演示此功能的 example on 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 的 属性 值为浮点数,因此需要在绑定中转换为浮点数。
"Progress" |> Binding.oneWay (fun m -> float m.Progress)
当然我们可以在模型中将 Progress 声明为浮点数,但我想借此机会指出模型不必与组件属性的数据类型对齐。我们当然可以在绑定中以任何我们想要的方式映射。
关于调度程序的最后一点说明。这可以通过 Cmd.ofSub 访问,也可以通过 WkProgram.Subscribe 访问。也许在其他情况下会详细介绍这一点,但现在请注意:使用调度程序发送消息是线程安全的。这意味着您也可以从顶级异步函数中 运行 的异步函数向模型发送进度消息(或任何消息),或者例如来自计时器事件,或任何地方。
最终更新:GitHub 上的演示现在比此处显示的稍微高级一些,但原理仍然相同,因此我不会费心更新此答案中的源代码。任何对此感兴趣的人很可能无论如何都需要完整的演示源,除非你已经很了解 Elmish.WPF
问题的最后一部分,后来补充的,在这里回答。
当进行冗长的and/or CPU 密集型工作时,应按照下面的longRunningOperationLeft
功能所示进行操作。这也显示了其他地方的功能,不应该依赖于 GUI,可以以这样一种方式编写,即进度更新可以发送到 GUI。
下面显示的 longRunningOperationRight
做错了,阻塞了 GUI。
我在异步和任务方面的专业知识不是很好,但我认为从 Elmish 调用的顶级异步函数(例如 longRunningOperationLeft
)与 运行在同一个线程上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
}