Mephistopheles Mephistopheles - 21 days ago 6
Android Question

MvvmCross Android converter causes cursor to jump

we have a Problem with converters in MvvmCross in Connection with EditText controls in Android:

In our app, the user inserts user data. We have to do some calculation with this data within the converter, and then write the data in our viewmodel.

This works, as long as the user does not revert his entry.
That means, if he uses the back key, the value is correctly edited, until he reaches the last decimal before "." (for example: 55.99, when he reaches the "55.9").
The ".9" will be removed correctly, but the curosor jumps bevor the remaining "55".
How can we resolve this annoying behaviour?

Viewmodel extract:

private Nullable mdValue1 = null;

public Nullable<decimal> Value1
{
get { return mdValue1; }
set
{
SetProperty(ref mdValue1, value);
}
}

private Nullable<decimal> mdValue2;

public Nullable<decimal> Value2
{
get { return mdValue2; }
set
{

SetProperty(ref mdValue2, value, nameof(Value2));

}
}


Converter (simplified):

public class DecimalToStringValueConverter : MvxValueConverter<Nullable<decimal>, string>
{
protected override string Convert(Nullable<decimal> poValue, Type poTargetType, object poParameter, CultureInfo poCulture)
{
if (!poValue.HasValue)
{
return null;
}

return poValue.Value.ToString();
}

protected override Nullable<decimal> ConvertBack(string value, Type targetType, object parameter, CultureInfo culture)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}

return decimal.Parse(value);
}
}


Configuration

Android Version: 4.4/5.1/7

Platform: Xamarin

Answer

It seems what's happening is when the point is removed and converted to a decimal, the resulting change in the value in the ViewModel is then different to the EditText causing the ViewModel to set the value of the EditText.

Example: User enters 59.9 then backspaces 9. This leaves the value in the EditText as 59. which gets parsed as a decimal to 59 to the ViewModel. As 59. is not equal to 59 the ViewModel will update the value in the EditText to 59 this is what cause the cursor to jump to the start.

One quick way to resolve this is to create a custom binding that makes sure that the cursor is always placed at the end of the EditText upon removing the last decimal place. This can be done using SetSelection which positions the cursor in the SetValueImpl method.

public class DecimalEditTextTargetBinding : MvxConvertingTargetBinding
{
    protected EditText EditTextControl => Target as EditText;

    private IDisposable _subscription;

    public DecimalEditTextTargetBinding(EditText target) : base(target)
    {
        if (target == null)
            MvxBindingTrace.Error($"Error - EditText is null in {nameof(DecimalEditTextTargetBinding)}");
    }

    public override Type TargetType => typeof(string);
    public override MvxBindingMode DefaultMode => MvxBindingMode.TwoWay;

    protected override void SetValueImpl(object target, object value)
    {
        ((TextView)target).Text = (string)value;
        EditTextControl.SetSelection(EditTextControl.Text?.Length ?? 0);
    }

    public override void SubscribeToEvents()
    {
        if (EditTextControl == null)
            return;

        _subscription = EditTextControl.WeakSubscribe<TextView, AfterTextChangedEventArgs>(
            nameof(EditTextControl.AfterTextChanged),
            EditTextOnAfterTextChanged);
    }

    private void EditTextOnAfterTextChanged(object sender, AfterTextChangedEventArgs e)
    {
        FireValueChanged(EditTextControl.Text);
    }

    protected override void Dispose(bool isDisposing)
    {
        if (isDisposing)
        {
            _subscription?.Dispose();
            _subscription = null;
        }
        base.Dispose(isDisposing);
    }
}

And then register the custom binding in your Setup.cs:

protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
{
    base.FillTargetFactories(registry);
    registry.RegisterCustomBindingFactory<EditText>("DecimalText", inputField => new DecimalEditTextTargetBinding(inputField));
}

XML usage:

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:inputType="numberDecimal|numberSigned"
    local:MvxBind="DecimalText DecimalToString(Value1)" />

Example of Binding for pre MvvmCross 4.4.0:

public class DecimalEditTextTargetBinding : MvxConvertingTargetBinding
{
    private bool _subscribed;

    public DecimalEditTextTargetBinding(EditText target) : base(target)
    {
        if (target == null)
            MvxBindingTrace.Error($"Error - EditText is null in {nameof(DecimalEditTextTargetBinding)}");
    }

    protected EditText EditTextControl => Target as EditText;

    public override Type TargetType => typeof(string);

    public override MvxBindingMode DefaultMode => MvxBindingMode.TwoWay;

    protected override void SetValueImpl(object target, object value)
    {
        ((TextView)target).Text = (string)value;
        EditTextControl.SetSelection(EditTextControl.Text?.Length ?? 0);
    }

    public override void SubscribeToEvents()
    {
        if (EditTextControl == null)
            return;

        EditTextControl.AfterTextChanged += EditTextOnAfterTextChanged;
        _subscribed = true;
    }

    private void EditTextOnAfterTextChanged(object sender, AfterTextChangedEventArgs e)
    {
        FireValueChanged(EditTextControl.Text);
    }

    protected override void Dispose(bool isDisposing)
    {
        if (isDisposing && EditTextControl != null && _subscribed)
        {
            EditTextControl.AfterTextChanged -= EditTextOnAfterTextChanged;
            _subscribed = false;
        }

        base.Dispose(isDisposing);
    }
}