-
-
Notifications
You must be signed in to change notification settings - Fork 122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Infinite loop when messages are sent too quickly in succession #161
Comments
I have also been able to reproduce this issue. I would prefer to use the Having an event that fires when focus is lost on the control could be another possible solution. |
One way to explain this behavior is that EXF is too slow compared to Android firing the events, and can't keep in sync. When comes time to refresh the UI based on the model, EXF is too late and sets the wrong values.
On iOS, in a general manner, things are executed more quickly, allowing EXF to follow closely what's happening on the UI. This might explain the fact I did not reproduce the bug on iOS. One workaround could be a throttling mecanism that would wait a few milliseconds (for the TextChanged events to stop) before dispatching a message. let view (model: Model) dispatch =
View.ContentPage(
content = View.StackLayout(padding = 20.0, verticalOptions = LayoutOptions.Center,
children = [
View.Entry(text=model.Text, textChanged=(fun e ->
throttle dispatch (TextChanged e.NewTextValue))
)
])) |
Someone wrote an article on how to throttle the TextChanged event in F# on Xamarin.Forms |
I've updated the first comment with a workaround inspired by the article. |
@TimLariviere Do you think this should be de-bounced in EXF itself? |
@dsyme Yes, I think it's going to be a recurring scenario (might affect iOS too in some extreme cases) Ideally something like that: View.Entry(textChanged=(debounce 250 (fun e -> dispatch (TextChanged e)))) But this can be quite hard to implement:
|
@willsam100 since it is now possible to capture the control we can do this: View.Entry
(text = model.Name,
created = (fun entry ->
entry.Unfocused.Add(fun args ->
if model.Name <> entry.Text then dispatch (NameChanged entry.Text))))
Are there any downsides to this, apart from having to access the underlying control? Is it better not to compare the model and control values? The |
Seems like a good solution. Comparing the model and the control is ok for me.
This is one of the many forgotten properties/events that we're currently missing in Fabulous :) |
I've tried using focus on a couple of Android phones and it works ok for our app. Not had the chance to try it out on iOS yet, emulator or hardware. On the other hand textChanged breaks very quickly on the Android phones but is not a problem on the emulator. The sooner there is an accepted solution for that the better, but that's currently beyond my knowledge. I'll do a PR for the event as soon as I get the time. |
Further to the suggestion to use Unfocused, above, the model values are captured just once when the control is created which is not what was intended. I got round this by having a mutable module scope binding for the model which is captured by the created callback and is assigned to on each view() call. Is there a better way to do this? |
Ah yes indeed. let update msg model =
match msg with
| NameFocusLost text ->
if model.Name <> text then
{ model with Name = text }, (Cmd.ofMsg NameChanged)
else
model, Cmd.none
| NameChanged ->
// Do something on name changed
let view model dispatch =
View.Entry
(text = model.Name,
created = (fun entry ->
entry.Unfocused.Add(fun args -> dispatch (NameFocusLost entry.Text)))) |
Found another way to debounce that we could include in Fabulous. let debounce<'T> =
let memoizations = Dictionary<obj, CancellationTokenSource>(HashIdentity.Structural)
fun (timeout: int) (fn: 'T -> unit) value ->
let key = fn.GetType()
// Cancel previous debouncer
match memoizations.TryGetValue(key) with
| true, cts -> cts.Cancel()
| _ -> ()
// Create a new cancellation token and memoize it
let cts = new CancellationTokenSource()
memoizations.[key] <- cts
// Start a new debouncer
(async {
try
// Wait timeout to see if another event will cancel this one
do! Async.Sleep timeout
// If still not cancelled, then proceed to invoke the callback and discard the unused token
memoizations.Remove(key) |> ignore
fn value
with
| _ -> ()
})
|> (fun task -> Async.StartImmediate(task, cts.Token)) We could use it like that View.Entry(textChanged=(debounce 250 (fun e -> dispatch (TextChanged e)))) |
Hi. The latestest version of the decounce doesn't work. I get this loop on the phone and in the emulator, when I type to fast. I didn't check the version with the agent, yet. |
@DieselMeister I'm interested to know how you got it to break. Do you have an example or a repro? |
Hi. I try to setup a project in the next days. I had a basic login form only with username and password. Nothing mor. Sometimes the behavior appeared in the emulator and sometime on my Huawei P20. The Behavior ist, that when I enter something it stops after a couple of letters and than the last letter apears and disapears in an I think invinite loop. You are not able to enter some thing. |
Updated with workaround
Applies to: Android (maybe iOS)
Description:
To update the model along with what the user does, I listen to events like
TextChanged
.This works fine.
But under some circumstances, such event might trigger an infinite update-view loop which will freeze the app.
Based on what I do and the logs, I suspect EXF doesn't like TextChanged to trigger too quickly in a row.
I will update this issue when I find more about this.
iOS seems not to be affected.
Logs
In ElmishContacts, I searched for a contact "Hoya Booya" and then erased my search.
Repro:
https://github.com/TimLariviere/EXF_ReproInfiniteLoop
Workaround:
The issue only occurs when the UI updates quicker than EXF follows, so one way to prevent that is to throttle the
TextChanged
event and only send a single message when no moreTextChanged
are triggered for a given time.Here's an example with a 250ms timeout.
One important point: we need to "fix" the throttle function with
fixf
=>fixf throttle dispatch 250
.The throttle func start a new loop each time it is called, and without caching it with
fixf
, a new loop would be created each time the view refreshes.The text was updated successfully, but these errors were encountered: