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

Add MaxLength limit to cursor position updates #15970

Closed
wants to merge 3 commits into from

Conversation

cat0363
Copy link
Contributor

@cat0363 cat0363 commented Jul 3, 2023

Description of Change

An exception will occur on Android and iOS if you bind a string that exceeds the value specified in the MaxLength property to the Text property of Entry and Editor. In this PR, we will add consideration of the MaxLength property to cursor position updates.

On iOS, calling the GetTextRange method inside the SetTextRange method of the TextInputExtensions class caused an ArgumentNullException.

[src\Core\src\Platform\iOS\TextInputExtensions.cs]

internal static void SetTextRange(this IUITextInput platformView, int start, int selectedTextLength)
{
    int end = start + selectedTextLength;

    // Let's be sure we have positive positions
    start = Math.Max(start, 0);
    end = Math.Max(end, 0);

    // Switch start and end positions if necessary
    start = Math.Min(start, end);
    end = Math.Max(start, end);

    var startPosition = platformView.GetPosition(platformView.BeginningOfDocument, start);
    var endPosition = platformView.GetPosition(platformView.BeginningOfDocument, end);
    platformView.SelectedTextRange = platformView.GetTextRange(startPosition, endPosition);
}

This is because the startPosition and endPosition passed to the GetTextRange method are null. This is because null is returned if you specify an index that exceeds MaxLength in the offset of the GetPosition method. This offset should be set considering MaxLength.

I fixed it as follows.

[src\Core\src\Platform\iOS\TextInputExtensions.cs]

internal static void SetTextRange(this IUITextInput platformView, int start, int selectedTextLength, int maxLength)
{
    int end = start + selectedTextLength;

    // Let's be sure we have positive positions
    start = Math.Max(start, 0);
    end = Math.Max(end, 0);

    // Switch start and end positions if necessary
    start = Math.Min(start, end);
    end = Math.Max(start, end);

    if (maxLength >= 0)
    {
        start = Math.Min(start, maxLength);
        end = Math.Min(end, maxLength);
    }

    var startPosition = platformView.GetPosition(platformView.BeginningOfDocument, start);
    var endPosition = platformView.GetPosition(platformView.BeginningOfDocument, end);
    platformView.SelectedTextRange = platformView.GetTextRange(startPosition, endPosition);
}

[src\Controls\src\Core\Platform\iOS\Extensions\TextExtensions.cs]

public static void UpdateText(this UITextView textView, InputView inputView)
{
    // Setting the text causes the cursor to be reset to the end of the UITextView.
    // So, let's set back the cursor to the last known position and calculate a new
    // position if needed when the text was modified by a Converter.
    var oldText = textView.Text ?? string.Empty;
    var newText = TextTransformUtilites.GetTransformedText(
        inputView?.Text,
        textView.SecureTextEntry ? TextTransform.Default : inputView.TextTransform
    );

    // Re-calculate the cursor offset position if the text was modified by a Converter.
    // but if the text is being set by code, let's just move the cursor to the end.
    var cursorOffset = newText.Length - oldText.Length;
    var cursorPosition = textView.IsFirstResponder ? textView.GetCursorPosition(cursorOffset) : newText.Length;

    if (oldText != newText)
        textView.Text = newText;

    textView.SetTextRange(cursorPosition, 0, inputView.MaxLength);
}

public static void UpdateText(this UITextField textField, InputView inputView)
{
    // Setting the text causes the cursor to be reset to the end of the UITextView.
    // So, let's set back the cursor to the last known position and calculate a new
    // position if needed when the text was modified by a Converter.
    var oldText = textField.Text ?? string.Empty;
    var newText = TextTransformUtilites.GetTransformedText(
        inputView?.Text,
        textField.SecureTextEntry ? TextTransform.Default : inputView.TextTransform
    );

    // Re-calculate the cursor offset position if the text was modified by a Converter.
    // but if the text is being set by code, let's just move the cursor to the end.
    var cursorOffset = newText.Length - oldText.Length;
    var cursorPosition = textField.IsEditing ? textField.GetCursorPosition(cursorOffset) : newText.Length;

    if (oldText != newText)
        textField.Text = newText;

    textField.SetTextRange(cursorPosition, 0, inputView.MaxLength);
}

On Android, calling the SetSelection method inside the UpdateText method of the EditTextExtensions class caused an IndexOutOfBoundsException.

[src\Controls\src\Core\Platform\Android\Extensions\EditTextExtensions.cs]

public static void UpdateText(this EditText editText, InputView inputView)
{
    (var oldText, var newText) = GetTexts(editText, inputView);

    if (oldText != newText)
    {
        editText.Text = newText;

        // When updating from xplat->plat, we set the selection (cursor) to the end of the text
        editText.SetSelection(newText.Length);
    }
}

This is because an index exceeding the MaxLength property was specified as an argument when the SetSelection method was called.

I fixed it as follows.

[src\Controls\src\Core\Platform\Android\Extensions\EditTextExtensions.cs]

public static void UpdateText(this EditText editText, InputView inputView)
{
    (var oldText, var newText) = GetTexts(editText, inputView);

    if (oldText != newText)
    {
        editText.Text = newText;

        // When updating from xplat->plat, we set the selection (cursor) to the end of the text
        int selectionLength = newText.Length;
        if (inputView.MaxLength >= 0)
        {
            selectionLength = Math.Min(selectionLength, inputView.MaxLength);
        }
        editText.SetSelection(selectionLength);
    }
}

On Windows, the exception is not thrown, but updating the cursor position requires a similar fix.

[src\Controls\src\Core\Platform\Windows\Extensions\TextBoxExtensions.cs]

public static void UpdateText(this TextBox platformControl, InputView inputView)
{
    var hasFocus = platformControl.FocusState != UI.Xaml.FocusState.Unfocused;
    var passwordBox = platformControl as MauiPasswordTextBox;
    var isPassword = passwordBox?.IsPassword ?? false;
    var textTransform = inputView?.TextTransform ?? TextTransform.None;

    // Setting the text causes the cursor to be reset to position zero.
    // So, let's retain the current cursor position and calculate a new cursor
    // position if the text was modified by a Converter.
    var oldText = platformControl.Text ?? string.Empty;
    var newText = TextTransformUtilites.GetTransformedText(
        inputView?.Text,
        isPassword ? TextTransform.None : textTransform
    );

    // Re-calculate the cursor offset position if the text was modified by a Converter.
    // but if the text is being set by code, let's just move the cursor to the end.
    var cursorOffset = newText.Length - oldText.Length;
    int cursorPosition = hasFocus ? platformControl.GetCursorPosition(cursorOffset) : newText.Length;

    if (inputView?.MaxLength >= 0)
    {
        cursorPosition = Math.Min(cursorPosition, inputView.MaxLength);
    }

    if (oldText != newText && passwordBox is not null)
        passwordBox.Password = newText;
    else if (oldText != newText)
        platformControl.Text = newText;

    platformControl.Select(cursorPosition, 0);
}

However, there is no problem with the Windows TextBox Select method even if you specify an Index that exceeds MaxLength, so there is no problem even if you do not include a guard condition.

In addition to the solution in PR #7371, the MaxLength property should be considered.
This PR targets iOS, Android, and Windows, which were targeted for fixes in PR #7371.

Issues Fixed

Fixes #15937

@ghost ghost added the community ✨ Community Contribution label Jul 3, 2023
@ghost
Copy link

ghost commented Jul 3, 2023

Hey there @cat0363! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@cat0363 cat0363 changed the title Added MaxLength limit to cursor position updates Add MaxLength limit to cursor position updates Jul 3, 2023
@jfversluis
Copy link
Member

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 2 pipeline(s).

@ghost ghost added the legacy-area-controls Label, Button, CheckBox, Slider, Stepper, Switch, Picker, Entry, Editor label Jul 3, 2023
@juniorsaraviao
Copy link

who should review this PR?

@waguilar
Copy link

waguilar commented Aug 7, 2023

I have the same issue :/
Any chance someone can review this PR, pretty please?

@rodrigojuarez
Copy link

+1

@samhouts samhouts added this to the Under Consideration milestone Aug 10, 2023
@cat0363 cat0363 requested a review from a team as a code owner August 25, 2023 07:24
@samhouts samhouts added the stale Indicates a stale issue/pr and will be closed soon label Sep 11, 2023
@jfversluis
Copy link
Member

jfversluis commented Nov 9, 2023

/rebase

@jfversluis
Copy link
Member

Hey @cat0363 I was just looking into this one, it seems like some work was done in this area. Could you confirm that this fix is still needed? And if you think it is, could you rebase it? Thanks!

@jfversluis jfversluis added the s/pr-needs-author-input PR needs an update from the author label Nov 9, 2023
@ghost
Copy link

ghost commented Nov 9, 2023

Hi @cat0363. We have added the "s/pr-needs-author-input" label to this issue, which indicates that we have an open question/action for you before we can take further action. This PRwill be closed automatically in 14 days if we do not hear back from you by then - please feel free to re-open it if you come back to this PR after that time.

@cat0363
Copy link
Contributor Author

cat0363 commented Nov 10, 2023

@jfversluis , Thank you for the good news.
I tested the latest version of main and confirmed that the issue was resolved on Android, iOS, and Windows platforms.
I was busy with my day job, so my reply was delayed.

@jfversluis
Copy link
Member

Hehehe no worries my friend, I think we should be apologising to you for the delay on this PR 😉

Thanks for your efforts here and confirming this is fixed! Looking forward to your future work on this project!

@jfversluis jfversluis closed this Nov 10, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Dec 11, 2023
@Eilon Eilon removed the legacy-area-controls Label, Button, CheckBox, Slider, Stepper, Switch, Picker, Entry, Editor label May 10, 2024
@samhouts samhouts removed this from the Under Consideration milestone Jul 1, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-controls-entry Entry community ✨ Community Contribution s/pr-needs-author-input PR needs an update from the author stale Indicates a stale issue/pr and will be closed soon
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[iOS] Issue when using BindableLayout and Entry/Editor
8 participants