Skip to content
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

App crashes when entry bound to float value with fractional format #25728

Open
niza93 opened this issue Nov 7, 2024 · 15 comments
Open

App crashes when entry bound to float value with fractional format #25728

niza93 opened this issue Nov 7, 2024 · 15 comments
Labels
area-controls-entry Entry i/regression This issue described a confirmed regression on a currently supported version p/2 Work that is important, but is currently not scheduled for release platform/android 🤖 s/triaged Issue has been reviewed s/verified Verified / Reproducible Issue ready for Engineering Triage t/bug Something isn't working
Milestone

Comments

@niza93
Copy link

niza93 commented Nov 7, 2024

Description

When I bound Text property of Entry to float value with StringFormat='{0:F2}', app crashes when I change value with the following exception: Java.Lang.IllegalArgumentException: 'end should be < than charSequence length'.

I run my app on android 14 on Xiaomi 13.

Steps to Reproduce

  1. Downolad, build and run my reproduction project
    or
  2. Bound Entry.Text to float property with fractional string format, '{}{0:F2}' in my case.
  3. Run on a physical device (this issue doesn't occur on emulator).
  4. Remove all chars and enter any number.
  5. Get exception Java.Lang.IllegalArgumentException: 'end should be < than charSequence length'.

Link to public reproduction project repository

MauiStringFormatIssue.zip

Version with bug

9.0.0-rc.2.24503.2

Is this a regression from previous behavior?

Not sure, did not test other versions

Last version that worked well

No response

Affected platforms

Android

Affected platform versions

Android 14 UKQ1.230804.001

Did you find any workaround?

Do not use string format with float value

Relevant log output

13:21:35:821 [AndroidRuntime] FATAL EXCEPTION: main
13:21:35:821 [AndroidRuntime] Process: com.companyname.mauistringformatissue, PID: 5792
13:21:35:821 [AndroidRuntime] java.lang.IllegalArgumentException: end should be < than charSequence length
13:21:35:821 [AndroidRuntime] at androidx.core.util.Preconditions.checkArgument(Preconditions.java:51)
13:21:35:821 [AndroidRuntime] at androidx.emoji2.text.EmojiCompat.process(EmojiCompat.java:1127)
13:21:35:821 [AndroidRuntime] at androidx.emoji2.viewsintegration.EmojiTextWatcher.afterTextChanged(EmojiTextWatcher.java:99)
13:21:35:821 [AndroidRuntime] at android.widget.TextView.sendAfterTextChanged(TextView.java:12420)
13:21:35:821 [AndroidRuntime] at android.widget.TextView$ChangeWatcher.afterTextChanged(TextView.java:15930)
13:21:35:821 [AndroidRuntime] at android.text.SpannableStringBuilder.sendAfterTextChanged(SpannableStringBuilder.java:1278)
13:21:35:821 [AndroidRuntime] at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:578)
13:21:35:821 [AndroidRuntime] at androidx.emoji2.text.SpannableBuilder.replace(SpannableBuilder.java:308)
13:21:35:821 [AndroidRuntime] at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:508)
13:21:35:821 [AndroidRuntime] at androidx.emoji2.text.SpannableBuilder.replace(SpannableBuilder.java:298)
13:21:35:821 [AndroidRuntime] at androidx.emoji2.text.SpannableBuilder.replace(SpannableBuilder.java:48)
13:21:35:821 [AndroidRuntime] at android.text.method.NumberKeyListener.onKeyDown(NumberKeyListener.java:129)
13:21:35:821 [AndroidRuntime] at android.widget.TextView.doKeyDown(TextView.java:9566)
13:21:35:821 [AndroidRuntime] at android.widget.TextView.onKeyDown(TextView.java:9334)
13:21:35:821 [AndroidRuntime] at android.view.KeyEvent.dispatch(KeyEvent.java:2934)
13:21:35:821 [AndroidRuntime] at android.view.View.dispatchKeyEvent(View.java:15772)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at androidx.core.widget.NestedScrollView.dispatchKeyEvent(NestedScrollView.java:686)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1981)
13:21:35:821 [AndroidRuntime] at com.android.internal.policy.DecorView.superDispatchKeyEvent(DecorView.java:554)
13:21:35:821 [AndroidRuntime] at com.android.internal.policy.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1898)
13:21:35:821 [AndroidRuntime] at android.app.Activity.dispatchKeyEvent(Activity.java:4542)
13:21:35:821 [AndroidRuntime] at androidx.core.app.ComponentActivity.superDispatchKeyEvent(ComponentActivity.kt:103)
13:21:35:821 [AndroidRuntime] at androidx.core.view.KeyEventDispatcher.dispatchKeyEvent(KeyEventDispatcher.java:85)
13:21:35:821 [AndroidRuntime] at androidx.core.app.ComponentActivity.dispatchKeyEvent(ComponentActivity.kt:117)
13:21:35:821 [AndroidRuntime] at androidx.appcompat.app.AppCompatActivity.dispatchKeyEvent(AppCompatActivity.java:604)
13:21:35:821 [AndroidRuntime] at androidx.appcompat.view.WindowCallbackWrapper.dispatchKeyEvent(WindowCallbackWrapper.java:59)
13:21:35:821 [AndroidRuntime] at androidx.appcompat.app.AppCompatDelegateImpl$AppCompatWindowCallback.dispatchKeyEvent(AppCompatDelegateImpl.java:3397)
13:21:35:821 [AndroidRuntime] at com.android.internal.policy.DecorView.dispatchKeyEvent(DecorView.java:437)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent(ViewRootImpl.java:7717)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:7565)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:6953)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:7010)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:6976)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:7141)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:6984)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:7198)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:6957)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:7010)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:6976)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:6984)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:6957)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:10124)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:10075)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:10039)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl$ViewRootHandler.handleMessageImpl(ViewRootImpl.java:6709)
13:21:35:822 [AndroidRuntime] at android.view.ViewRootImpl$ViewRootHandler.handleMessage(ViewRootImpl.java:6566)
13:21:35:822 [AndroidRuntime] at android.os.Handler.dispatchMessage(Handler.java:106)
13:21:35:822 [AndroidRuntime] at android.os.Looper.loopOnce(Looper.java:224)
13:21:35:822 [AndroidRuntime] at android.os.Looper.loop(Looper.java:318)
13:21:35:822 [AndroidRuntime] at android.app.ActivityThread.main(ActivityThread.java:8777)
13:21:35:822 [AndroidRuntime] at java.lang.reflect.Method.invoke(Native Method)
13:21:35:822 [AndroidRuntime] at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:561)
13:21:35:822 [AndroidRuntime] at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1013)
13:21:36:154 Java.Lang.IllegalArgumentException: 'end should be < than charSequence length'

@niza93 niza93 added the t/bug Something isn't working label Nov 7, 2024
@niza93
Copy link
Author

niza93 commented Nov 7, 2024

This issue doesn't occur on 8.0.92, 8.0.93 and 9.0.0-rc.1.24453.9.
But this error occurs on these versions with package BarcodeScanning.Native.Maui 1.5.9.

@ninachen03 ninachen03 added s/verified Verified / Reproducible Issue ready for Engineering Triage s/triaged Issue has been reviewed i/regression This issue described a confirmed regression on a currently supported version labels Nov 8, 2024
@ninachen03
Copy link

This issue has been verified using Visual Studio 17.12.0 Preview 5, I can repro this issue at android Enmulator Pixel-7-api34 on maui control 9.0.0-rc.2.24503.2 but not repro on 9.0.0-rc.1.24453.9.
Image

But when I run this project on Google phone, I get below Exception they are not the same.
Image

@rmarinho rmarinho added the potential-regression This issue described a possible regression on a currently supported version., verification pending label Nov 8, 2024
@rmarinho rmarinho added this to the .NET 9 SR1 milestone Nov 8, 2024
@rmarinho rmarinho added the p/0 Work that we can't release without label Nov 8, 2024
@rmarinho rmarinho modified the milestones: .NET 9 SR1, .NET 9 SR1.1 Nov 8, 2024
@jsuarezruiz jsuarezruiz self-assigned this Nov 11, 2024
@jsuarezruiz
Copy link
Contributor

Have done more tests, and also happens with the Editor.

@jsuarezruiz
Copy link
Contributor

After doing some quick review the origin of the crash happens here


Because the values for the oldText and newText are incorrect.

@jsuarezruiz jsuarezruiz removed their assignment Nov 11, 2024
@Odaronil
Copy link

Version with bug:
Visual Studio 17.12.0, NET [9.0.0]
android 35.0.7/9.0.100 SDK 9.0.100, VS 17.12.35506.116

The problem is only on Android, everything works on iOS. When changing the cursor position in Entry, the following error occurs:

[InputEventSender] Exception dispatching finished signal for seq=12
[MessageQueue-JNI] Exception in MessageQueue callback: handleReceiveCallback
[MessageQueue-JNI] java.lang.IllegalArgumentException: end should be < than charSequence length
[MessageQueue-JNI] at androidx.core.util.Preconditions.checkArgument(Preconditions.java:51)
[MessageQueue-JNI] at androidx.emoji2.text.EmojiCompat.process(EmojiCompat.java:1127)
[MessageQueue-JNI] at androidx.emoji2.viewsintegration.EmojiTextWatcher.afterTextChanged(EmojiTextWatcher.java:99)
[MessageQueue-JNI] at android.widget.TextView.sendAfterTextChanged(TextView.java:12749)
[MessageQueue-JNI] at android.widget.TextView$ChangeWatcher.afterTextChanged(TextView.java:16341)
[MessageQueue-JNI] at android.text.SpannableStringBuilder.sendAfterTextChanged(SpannableStringBuilder.java:1278)
[MessageQueue-JNI] at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:578)
[MessageQueue-JNI] at androidx.emoji2.text.SpannableBuilder.replace(SpannableBuilder.java:308)
[MessageQueue-JNI] at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:508)
[MessageQueue-JNI] at androidx.emoji2.text.SpannableBuilder.replace(SpannableBuilder.java:298)
[MessageQueue-JNI] at androidx.emoji2.text.SpannableBuilder.replace(SpannableBuilder.java:48)
[MessageQueue-JNI] at android.text.method.NumberKeyListener.onKeyDown(NumberKeyListener.java:129)
[MessageQueue-JNI] at android.widget.TextView.doKeyDown(TextView.java:9818)
[MessageQueue-JNI] at android.widget.TextView.onKeyDown(TextView.java:9573)
[MessageQueue-JNI] at android.view.KeyEvent.dispatch(KeyEvent.java:3029)
[MessageQueue-JNI] at android.view.View.dispatchKeyEvent(View.java:16351)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at androidx.core.widget.NestedScrollView.dispatchKeyEvent(NestedScrollView.java:686)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[MessageQueue-JNI] at com.android.internal.policy.DecorView.superDispatchKeyEvent(DecorView.java:446)
[MessageQueue-JNI] at com.android.internal.policy.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1970)
[MessageQueue-JNI] at android.app.Activity.dispatchKeyEvent(Activity.java:4491)
[MessageQueue-JNI] at androidx.core.app.ComponentActivity.superDispatchKeyEvent(ComponentActivity.kt:103)
[MessageQueue-JNI] at androidx.core.view.KeyEventDispatcher.dispatchKeyEvent(KeyEventDispatcher.java:85)
[MessageQueue-JNI] at androidx.core.app.ComponentActivity.dispatchKeyEvent(ComponentActivity.kt:117)
[MessageQueue-JNI] at androidx.appcompat.app.AppCompatActivity.dispatchKeyEvent(AppCompatActivity.java:604)
[MessageQueue-JNI] at androidx.appcompat.view.WindowCallbackWrapper.dispatchKeyEvent(WindowCallbackWrapper.java:59)
[MessageQueue-JNI] at androidx.appcompat.app.AppCompatDelegateImpl$AppCompatWindowCallback.dispatchKeyEvent(AppCompatDelegateImpl.java:3397)
[MessageQueue-JNI] at com.android.internal.policy.DecorView.dispatchKeyEvent(DecorView.java:360)
[MessageQueue-JNI] at android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent(ViewRootImpl.java:7866)
[MessageQueue-JNI] at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:7706)
[MessageQueue-JNI] at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:7106)
[MessageQueue-JNI] at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:7163)
[MessageQueue-JNI] at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:7129)
[MessageQueue-JNI] at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:7295)
[MessageQueue-JNI] at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:7137)
[MessageQueue-JNI] at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:7352)
[MessageQueue-JNI] at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:7110)
[MessageQueue-JNI] at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:7163)
[MessageQueue-JNI] at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:7129)
[MessageQueue-JNI] at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:7137)
[MessageQueue-JNI] at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:7110)
[MessageQueue-JNI] at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:7163)
[MessageQueue-JNI] at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:7129)
[MessageQueue-JNI] at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:7328)
[MessageQueue-JNI] at android.view.ViewRootImpl$ImeInputStage.onFinishedInputEvent(ViewRootImpl.java:7554)
[MessageQueue-JNI] at android.view.inputmethod.InputMethodManager$PendingEvent.run(InputMethodManager.java:4871)
[MessageQueue-JNI] at android.view.inputmethod.InputMethodManager.invokeFinishedInputEventCallback(InputMethodManager.java:4262)
[MessageQueue-JNI] at android.view.inputmethod.InputMethodManager.finishedInputEvent(InputMethodManager.java:4253)
[MessageQueue-JNI] at android.view.inputmethod.InputMethodManager.-$$Nest$mfinishedInputEvent(Unknown Source:0)
[MessageQueue-JNI] at android.view.inputmethod.InputMethodManager$ImeInputEventSender.onInputEventFinished(InputMethodManager.java:4848)
[MessageQueue-JNI] at android.view.InputEventSender.dispatchInputEventFinished(InputEventSender.java:181)
[MessageQueue-JNI] at android.os.MessageQueue.nativePollOnce(Native Method)
[MessageQueue-JNI] at android.os.MessageQueue.next(MessageQueue.java:346)
[MessageQueue-JNI] at android.os.Looper.loopOnce(Looper.java:189)
[MessageQueue-JNI] at android.os.Looper.loop(Looper.java:317)
[MessageQueue-JNI] at android.app.ActivityThread.main(ActivityThread.java:8705)
[MessageQueue-JNI] at java.lang.reflect.Method.invoke(Native Method)
[MessageQueue-JNI] at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580)
[MessageQueue-JNI] at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:886)
[AndroidRuntime] Shutting down VM
[AndroidRuntime] FATAL EXCEPTION: main
[AndroidRuntime] Process: NAME, PID: 18652
[AndroidRuntime] java.lang.IllegalArgumentException: end should be < than charSequence length
[AndroidRuntime] at androidx.core.util.Preconditions.checkArgument(Preconditions.java:51)
[AndroidRuntime] at androidx.emoji2.text.EmojiCompat.process(EmojiCompat.java:1127)
[AndroidRuntime] at androidx.emoji2.viewsintegration.EmojiTextWatcher.afterTextChanged(EmojiTextWatcher.java:99)
[AndroidRuntime] at android.widget.TextView.sendAfterTextChanged(TextView.java:12749)
[AndroidRuntime] at android.widget.TextView$ChangeWatcher.afterTextChanged(TextView.java:16341)
[AndroidRuntime] at android.text.SpannableStringBuilder.sendAfterTextChanged(SpannableStringBuilder.java:1278)
[AndroidRuntime] at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:578)
[AndroidRuntime] at androidx.emoji2.text.SpannableBuilder.replace(SpannableBuilder.java:308)
[AndroidRuntime] at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:508)
[AndroidRuntime] at androidx.emoji2.text.SpannableBuilder.replace(SpannableBuilder.java:298)
[AndroidRuntime] at androidx.emoji2.text.SpannableBuilder.replace(SpannableBuilder.java:48)
[AndroidRuntime] at android.text.method.NumberKeyListener.onKeyDown(NumberKeyListener.java:129)
[AndroidRuntime] at android.widget.TextView.doKeyDown(TextView.java:9818)
[AndroidRuntime] at android.widget.TextView.onKeyDown(TextView.java:9573)
[AndroidRuntime] at android.view.KeyEvent.dispatch(KeyEvent.java:3029)
[AndroidRuntime] at android.view.View.dispatchKeyEvent(View.java:16351)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at androidx.core.widget.NestedScrollView.dispatchKeyEvent(NestedScrollView.java:686)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1962)
[AndroidRuntime] at com.android.internal.policy.DecorView.superDispatchKeyEvent(DecorView.java:446)
[AndroidRuntime] at com.android.internal.policy.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1970)
[AndroidRuntime] at android.app.Activity.dispatchKeyEvent(Activity.java:4491)
[AndroidRuntime] at androidx.core.app.ComponentActivity.superDispatchKeyEvent(ComponentActivity.kt:103)
[AndroidRuntime] at androidx.core.view.KeyEventDispatcher.dispatchKeyEvent(KeyEventDispatcher.java:85)
[AndroidRuntime] at androidx.core.app.ComponentActivity.dispatchKeyEvent(ComponentActivity.kt:117)
[AndroidRuntime] at androidx.appcompat.app.AppCompatActivity.dispatchKeyEvent(AppCompatActivity.java:604)
[AndroidRuntime] at androidx.appcompat.view.WindowCallbackWrapper.dispatchKeyEvent(WindowCallbackWrapper.java:59)
[AndroidRuntime] at androidx.appcompat.app.AppCompatDelegateImpl$AppCompatWindowCallback.dispatchKeyEvent(AppCompatDelegateImpl.java:3397)
[AndroidRuntime] at com.android.internal.policy.DecorView.dispatchKeyEvent(DecorView.java:360)
[AndroidRuntime] at android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent(ViewRootImpl.java:7866)
[AndroidRuntime] at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:7706)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:7106)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:7163)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:7129)
[AndroidRuntime] at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:7295)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:7137)
[AndroidRuntime] at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:7352)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:7110)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:7163)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:7129)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:7137)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:7110)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:7163)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:7129)
[AndroidRuntime] at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:7328)
[AndroidRuntime] at android.view.ViewRootImpl$ImeInputStage.onFinishedInputEvent(ViewRootImpl.java:7554)
[AndroidRuntime] at android.view.inputmethod.InputMethodManager$PendingEvent.run(InputMethodManager.java:4871)
[AndroidRuntime] at android.view.inputmethod.InputMethodManager.invokeFinishedInputEventCallback(InputMethodManager.java:4262)
[AndroidRuntime] at android.view.inputmethod.InputMethodManager.finishedInputEvent(InputMethodManager.java:4253)
[AndroidRuntime] at android.view.inputmethod.InputMethodManager.-$$Nest$mfinishedInputEvent(Unknown Source:0)
[AndroidRuntime] at android.view.inputmethod.InputMethodManager$ImeInputEventSender.onInputEventFinished(InputMethodManager.java:4848)
[AndroidRuntime] at android.view.InputEventSender.dispatchInputEventFinished(InputEventSender.java:181)
[AndroidRuntime] at android.os.MessageQueue.nativePollOnce(Native Method)
[AndroidRuntime] at android.os.MessageQueue.next(MessageQueue.java:346)
[AndroidRuntime] at android.os.Looper.loopOnce(Looper.java:189)
[AndroidRuntime] at android.os.Looper.loop(Looper.java:317)
[AndroidRuntime] at android.app.ActivityThread.main(ActivityThread.java:8705)
[AndroidRuntime] at java.lang.reflect.Method.invoke(Native Method)
[AndroidRuntime] at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580)
[AndroidRuntime] at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:886)
Java.Lang.IllegalArgumentException: 'end should be < than charSequence length'

@BioTurboNick
Copy link
Contributor

Just encountered this as well.

@utp087
Copy link

utp087 commented Nov 18, 2024

I have same problem on android. I using NET 9 NET MAUI - dotnet version 9.0.100. When clear all characters and after call entry.Text = "0"; application crash on android. On windows or iOS all OK.

public partial class EntryBehaviorPort : Behavior
{
protected override void OnAttachedTo(Entry bindable)
{
base.OnAttachedTo(bindable);
BindingContext = bindable.BindingContext;
bindable.TextChanged += OnChanged;
}
protected override void OnDetachingFrom(Entry bindable)
{
base.OnDetachingFrom(bindable);
BindingContext = null;
bindable.TextChanged -= OnChanged;
}
void OnChanged(object sender, TextChangedEventArgs args)
{
var entry = (Entry)sender;
// if (DeviceInfo.Platform != DevicePlatform.Android)
{
if (args.NewTextValue.IsEmpty())
{
entry.Text = "0"; // HERE PROBLEM
}
else if (args.NewTextValue.ToArray()[0] == '0' && args.NewTextValue.ToArray().Length > 1)
{
entry.Text = args.NewTextValue.Trim('0');
}
}
}
}

ERROR OUTPUT:
Java.Lang.IllegalArgumentException
Message=end should be < than charSequence length

ERROR LOG:

[AndroidRuntime] Shutting down VM
[AndroidRuntime] FATAL EXCEPTION: main
[AndroidRuntime] Process: eu.metel.iplog21, PID: 31591
[AndroidRuntime] java.lang.IllegalArgumentException: end should be < than charSequence length
[AndroidRuntime] at androidx.core.util.Preconditions.checkArgument(Preconditions.java:51)
[AndroidRuntime] at androidx.emoji2.text.EmojiCompat.process(EmojiCompat.java:1127)
[AndroidRuntime] at androidx.emoji2.viewsintegration.EmojiTextWatcher.afterTextChanged(EmojiTextWatcher.java:99)
[AndroidRuntime] at android.widget.TextView.sendAfterTextChanged(TextView.java:13061)
[AndroidRuntime] at android.widget.TextView$ChangeWatcher.afterTextChanged(TextView.java:16845)
[AndroidRuntime] at android.text.SpannableStringBuilder.sendAfterTextChanged(SpannableStringBuilder.java:1278)
[AndroidRuntime] at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:578)
[AndroidRuntime] at androidx.emoji2.text.SpannableBuilder.replace(SpannableBuilder.java:308)
[AndroidRuntime] at android.text.SpannableStringBuilder.delete(SpannableStringBuilder.java:231)
[AndroidRuntime] at androidx.emoji2.text.SpannableBuilder.delete(SpannableBuilder.java:330)
[AndroidRuntime] at androidx.emoji2.text.SpannableBuilder.delete(SpannableBuilder.java:48)
[AndroidRuntime] at android.text.method.BaseKeyListener.backspaceOrForwardDelete(BaseKeyListener.java:376)
[AndroidRuntime] at android.text.method.BaseKeyListener.backspace(BaseKeyListener.java:71)
[AndroidRuntime] at android.text.method.BaseKeyListener.onKeyDown(BaseKeyListener.java:502)
[AndroidRuntime] at android.text.method.NumberKeyListener.onKeyDown(NumberKeyListener.java:146)
[AndroidRuntime] at android.widget.TextView.doKeyDown(TextView.java:10086)
[AndroidRuntime] at android.widget.TextView.onKeyDown(TextView.java:9856)
[AndroidRuntime] at android.view.KeyEvent.dispatch(KeyEvent.java:3505)
[AndroidRuntime] at android.view.View.dispatchKeyEvent(View.java:16083)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:2003)
[AndroidRuntime] at com.android.internal.policy.DecorView.superDispatchKeyEvent(DecorView.java:782)
[AndroidRuntime] at com.android.internal.policy.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1957)
[AndroidRuntime] at android.app.Activity.dispatchKeyEvent(Activity.java:4523)
[AndroidRuntime] at androidx.core.app.ComponentActivity.superDispatchKeyEvent(ComponentActivity.kt:103)
[AndroidRuntime] at androidx.core.view.KeyEventDispatcher.dispatchKeyEvent(KeyEventDispatcher.java:85)
[AndroidRuntime] at androidx.core.app.ComponentActivity.dispatchKeyEvent(ComponentActivity.kt:117)
[AndroidRuntime] at androidx.appcompat.app.AppCompatActivity.dispatchKeyEvent(AppCompatActivity.java:604)
[AndroidRuntime] at androidx.appcompat.view.WindowCallbackWrapper.dispatchKeyEvent(WindowCallbackWrapper.java:59)
[AndroidRuntime] at androidx.appcompat.app.AppCompatDelegateImpl$AppCompatWindowCallback.dispatchKeyEvent(AppCompatDelegateImpl.java:3397)
[AndroidRuntime] at com.android.internal.policy.DecorView.dispatchKeyEvent(DecorView.java:696)
[AndroidRuntime] at android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent(ViewRootImpl.java:8559)
[AndroidRuntime] at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:8419)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:7752)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:7809)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:7775)
[AndroidRuntime] at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:7978)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:7783)
[AndroidRuntime] at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:8035)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:7756)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:7809)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:7775)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:7783)
[AndroidRuntime] at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:7756)
[AndroidRuntime] at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:11343)
[AndroidRuntime] at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:11212)
[AndroidRuntime] at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:11168)
[AndroidRuntime] at android.view.ViewRootImpl$ViewRootHandler.handleMessageImpl(ViewRootImpl.java:7364)
[AndroidRuntime] at android.view.ViewRootImpl$ViewRootHandler.handleMessage(ViewRootImpl.java:7236)
[AndroidRuntime] at android.os.Handler.dispatchMessage(Handler.java:106)
[AndroidRuntime] at android.os.Looper.loopOnce(Looper.java:230)
[AndroidRuntime] at android.os.Looper.loop(Looper.java:319)
[AndroidRuntime] at android.app.ActivityThread.main(ActivityThread.java:9063)
[AndroidRuntime] at java.lang.reflect.Method.invoke(Native Method)
[AndroidRuntime] at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:588)
[AndroidRuntime] at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1103)
Java.Lang.IllegalArgumentException: 'end should be < than charSequence length'

@PureWeen PureWeen added area-controls-entry Entry and removed potential-regression This issue described a possible regression on a currently supported version., verification pending labels Nov 18, 2024
@BioTurboNick
Copy link
Contributor

For some extra context, in my case, the issue is occurring when attached to a ValueConverter that clears the Entry when it is anything other than a parsable integer:

public class IntConverter :
    IValueConverter
{
    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        if (targetType != typeof(string))
            return new object();

        return value switch
        {
            int intValue => intValue.ToString(culture),
            _            => string.Empty
        };
    }

    public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        if (targetType != typeof(int))
            return new object();

        bool success = int.TryParse(value as string, out int result);

        return success ? result : 0;
    }
}

@jfversluis
Copy link
Member

jfversluis commented Nov 19, 2024

This has to do with this bug over at Google. From what I understand the cause is that the text length is different between different events in the EditText. Probably because the field in the binding will return a formatted value like 1.00 when you actually input a 1.

If I set the EditText.EmojiCompatEnabled = false; then the bug goes away. So either we

  • downgrade a dependency, I'm assuming this came in with an update of that
  • set EmojiCompatEnabled = false; but then people depending on that will see no emoji anymore
  • Find another solution that keeps both the setting of the text (and cursor position and selection state) intact as well as not hitting this bug.

@PureWeen PureWeen modified the milestones: .NET 9 SR1.1, .NET 9 SR2 Nov 22, 2024
@jfversluis
Copy link
Member

Reverting this commit makes the crash go away, so definitely introduced somewhere there.

@jfversluis
Copy link
Member

jfversluis commented Nov 26, 2024

There is no great solution for this right now unfortunately. A workaround is this, if you need it for Editor too, it should look the exact same but use EditorHandler:

#if ANDROID
  Microsoft.Maui.Handlers.EntryHandler.PlatformViewFactory = 
    (handler) =>
    {
       var editText = new AndroidX.AppCompat.Widget.AppCompatEditText(handler.Context);
       editText.EmojiCompatEnabled = false;

       return editText;
    };
#endif

However, we're using a AppCompatEditText here instead of the MauiAppCompatEditText that we're using, but this is still internal. I did open a PR to change that, but I'm not sure if we can bring that back to .NET 9 because it's a breaking change.

So potentially with this workaround you might hit something else, but I think it should be mostly OK.

Bumping down the priority a bit because we have some options to work with and I don't think a whole lot of people will be impacted by this. Hopefully it will be fixed on Google's side soon 🤞

@jfversluis jfversluis added p/2 Work that is important, but is currently not scheduled for release and removed p/0 Work that we can't release without labels Nov 26, 2024
@jfversluis jfversluis removed their assignment Nov 26, 2024
@jfversluis jfversluis modified the milestones: .NET 9 SR2, Backlog Nov 26, 2024
@jfversluis
Copy link
Member

Upon retrying it seems that a custom handler does work, not sure why I thought it didn't work earlier.

If you place this in your MauiProgram, it should also disable the emoji parsing.

// Entry
Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping("DisableEmojiCompat", (handler, view) =>
{
  handler.PlatformView.EmojiCompatEnabled = false;
});

// Editor
Microsoft.Maui.Handlers.EditorHandler.Mapper.AppendToMapping("DisableEmojiCompat", (handler, view) =>
{
  handler.PlatformView.EmojiCompatEnabled = false;
});

If you want to make it specific to a certain Entry/Editor you can check for that inside of the handler, for example:

// Entry
Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping("DisableEmojiCompat", (handler, view) =>
{
   if (view is NoEmojiEntry)
   {
      handler.PlatformView.EmojiCompatEnabled = false;
   }
});

More in detail is explained: https://learn.microsoft.com/dotnet/maui/user-interface/handlers/customize

Please let me know if that works for people!

@niza93
Copy link
Author

niza93 commented Nov 30, 2024

@jfversluis, I tried your workaround and it works! I put your code in protected override MauiApp CreateMauiApp() in MauiApplication. Exception that I mentioned above is not being thrown.

@Odaronil
Copy link

Odaronil commented Dec 12, 2024

A temporary solution:

        private void OnEntryTextChanged(object? sender, TextChangedEventArgs e)
        {
         // Your code
#if ANDROID
                var handler = entry.Handler as Microsoft.Maui.Handlers.EntryHandler;
                var editText = handler?.PlatformView as AndroidX.AppCompat.Widget.AppCompatEditText;
                if (editText != null)
                {
                    editText.EmojiCompatEnabled = false;
                    editText.SetTextKeepState(entry.Text);
                }
#endif
         // Your code
        }

The cursor position is now correct and there are no errors.

@rafageist
Copy link

rafageist commented Jan 7, 2025

This problem occurs when an Entry modifies its text indirectly during the model update. It is common with StringFormat or converters, or simply deleting and wanting "" to be "0", and generates inconsistencies in Android that can lead to crashes or infinite loops. If for example you change the Binding Mode to OneWayToSource, it happens less, but this would seem more like a patch than a solution.

sequenceDiagram
    participant User as User
    participant Entry as Entry (Text)
    participant ViewModel as ViewModel (Binding)

    User->>Entry: Types in the Entry "1"
    Entry->>ViewModel: Updates the ViewModel (Binding) "1" and 1
    ViewModel->>Entry: Modifies the text (format/conversion/empty to zero/...) "1.00"
    Entry->>User: Updated value visible in the UI, crash!

    Note over Entry,User: len("1.00") > len("1") 
    Note over Entry,User: end (3) should be < than charSequence length (1)
    Note over Entry,ViewModel: Bi-directional updates cause conflicts in Android
Loading

A practical solution is to update the model without modifying the control's text in real time, and apply formats only when focus is lost, or knowing when the update is not coming from the UI, or when the value itself has not really changed (1, 1.0, 1.00, 1.000, they are the same). This pattern avoids conflicts and improves the stability.

20250107-1817-24.8862181.mp4

Checking your example

In the example code I see this problem: how does the Set know that the update came from the UI or from some part of our code? So, when it comes from the UI, it returns to the UI

Important

While fixing the crash is important, it’s equally critical to emphasize that modifying the Text of an Entry during a binding update should be avoided altogether. This practice not only leads to crashes like the one described but can also result in infinite loops. For example, a converter that toggles between 1 and 1.00 creates continuous updates between the View and ViewModel.

To ensure stability, the Text should reflect what the user has written, and any formatting or transformation should be applied only after the user has finished interacting with the control, such as when the Entry loses focus. This approach prevents inconsistencies and ensures a smooth user experience.

Another solution is to use more sophisticated controls that enforce input constraints, such as input masks or controls designed to behave like a pocket calculator. Input masks guide the user by restricting input to a predefined format, preventing invalid data from being entered in the first place.

Helping

It might be better not to bind to a float, but to a string. A class like this EntryModel<T> can help with this challenge.

public interface IParsableFromString
{
	void Parse(string input);
}

public class EntryModel<T>: INotifyPropertyChanged, IEquatable<T>
{
	private T? val;
	private string input = string.Empty;
	public event PropertyChangedEventHandler? PropertyChanged;
	Func<T?, string> converter = (T? val) => val?.ToString() ?? string.Empty;
	Func<string, T?>? converterBack = null;

	public EntryModel(T? value = default, Func<T?, string>? formatter = null, Func<string, T?>? formatterBack = null)
	{
		if (formatter != null)
		{
			this.converter = formatter;
		}

		this.converterBack = formatterBack;

		SetValue(value, true);
	}

	protected virtual bool Set<X>(ref X oldValue, X newValue, [CallerMemberName] string? propertyName = null)
	{
		if (oldValue == null && newValue == null)
		{
			return false;
		}

		if ((oldValue == null && newValue != null) || oldValue?.Equals(newValue) == false)
		{
			oldValue = newValue;
			OnPropertyChanged(propertyName);
			return true;
		}

		return false;
	}

	protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
	{
		PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
	}
	
	public string Input
	{
		get => input;
		set 
		{
			if (UpdateValueFrom(value))
			{
				Set(ref input, value);
			}
		}
	}

	public static bool IsNumeric(object? obj)
	{
		if (obj == null)
		{
			return false;
		}
		return obj is sbyte
			|| obj is byte
			|| obj is short
			|| obj is ushort
			|| obj is int
			|| obj is uint
			|| obj is long
			|| obj is ulong
			|| obj is float
			|| obj is double
			|| obj is decimal;
	}

	public static object ConvertToNumeric(string? value, Type targetType, CultureInfo culture)
	{
		if (string.IsNullOrWhiteSpace(value))
		{
			return targetType switch
			{
				Type t when t == typeof(double) => 0.0,
				Type t when t == typeof(float) => 0.0f,
				Type t when t == typeof(decimal) => 0m,
				Type t when t == typeof(int) => 0,
				Type t when t == typeof(long) => 0L,
				Type t when t == typeof(short) => (short)0,
				Type t when t == typeof(ushort) => (ushort)0,
				Type t when t == typeof(uint) => 0u,
				Type t when t == typeof(ulong) => 0ul,
				_ => 0
			};
		}

		return targetType switch
		{
			Type t when t == typeof(double) => double.TryParse(value.Replace(',', '.'), NumberStyles.Any, culture, out var d) ? d : 0.0,
			Type t when t == typeof(float) => float.TryParse(value.Replace(',', '.'), NumberStyles.Any, culture, out var f) ? f : 0.0f,
			Type t when t == typeof(decimal) => decimal.TryParse(value.Replace(',', '.'), NumberStyles.Any, culture, out var m) ? m : 0m,
			Type t when t == typeof(int) => int.TryParse(value, NumberStyles.Any, culture, out var i) ? i : 0,
			Type t when t == typeof(long) => long.TryParse(value, NumberStyles.Any, culture, out var l) ? l : 0L,
			Type t when t == typeof(short) => short.TryParse(value, NumberStyles.Any, culture, out var s) ? s : (short)0,
			Type t when t == typeof(ushort) => ushort.TryParse(value, NumberStyles.Any, culture, out var us) ? us : (ushort)0,
			Type t when t == typeof(uint) => uint.TryParse(value, NumberStyles.Any, culture, out var ui) ? ui : 0u,
			Type t when t == typeof(ulong) => ulong.TryParse(value, NumberStyles.Any, culture, out var ul) ? ul : 0ul,
			_ => 0
		};
	}

	public bool UpdateValueFrom(string inputValue)
	{
		if (converterBack != null)
		{
			if (converterBack(inputValue) is T value)
			{
				return Set(ref val, value, propertyName: nameof(Value));
			}
		}

		if (val is IParsableFromString parsable)
		{
			parsable.Parse(inputValue);
			return Set(ref val, (T)parsable, propertyName: nameof(Value));
		}

		if (IsNumeric(val))
		{
			var value = ConvertToNumeric(inputValue, typeof(T), CultureInfo.InvariantCulture);
			return Set(ref val, (T)value, propertyName: nameof(Value));
		}

		return Set(ref val, (T)(object)inputValue, propertyName: nameof(Value));
	}

	public bool Equals(T? other)
	{
		return EqualityComparer<T>.Default.Equals(val, other);
	}

	public T? Value
	{
		get => val;
		set => SetValue(value);
	}

	private void SetValue(T? value, bool forceUpdateInput = false)
	{
		if (Set(ref val, value) || forceUpdateInput)
		{				
			Set(ref input, converter(value), propertyName: nameof(Input));				
		}
	}

	public void Unfocused(object sender, EventArgs args)
	{
		Set(ref input, converter(val), propertyName: nameof(Input));
	}
}

The EntryModel<T> class acts as a bridge between user input and the underlying model, ensuring robust data handling and synchronization between the UI and backend logic. The IParsableFromString interface plays a key role in enabling this by defining a standard method (Parse) for converting string input into specific types.

This class is ideal for managing inputs in complex applications, ensuring the integrity of data and enhancing the user experience by decoupling raw input from processed values.

So, in the view model:

private EntryModel<float> mFloatValue = new EntryModel<float>(
	value: 0.0f,
	formatter: (float v) => v.ToString("F2")
);

In the XAML:

<Entry x:Name="Level1"
             Keyboard="Numeric"
             SemanticProperties.HeadingLevel="Level1"
             Text="{Binding FloatValue.Input}" />

In the code behind:

Level1.Unfocused += vm.FloatValue.Unfocused;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-controls-entry Entry i/regression This issue described a confirmed regression on a currently supported version p/2 Work that is important, but is currently not scheduled for release platform/android 🤖 s/triaged Issue has been reviewed s/verified Verified / Reproducible Issue ready for Engineering Triage t/bug Something isn't working
Projects
Status: Todo
Development

No branches or pull requests