Custom TimePickerRenderer is not firing the events to update the TimeProperty

aismaniottoaismaniotto ✭✭Member ✭✭

I created a custom TimePicker and the renderers to android and iphone, with the objective to allow for that be nullable. As inspiration, was used a post of xamgirl (i am not allowed yet to post links)

But, for some reason, the event is not firing when the time is set, thats happenen only in android, and more specific, back in android 8.1.

On shared project:

public class NullableTimePicker : TimePicker
{
    public NullableTimePicker()
    {
        Time = DateTime.Now.TimeOfDay;
        NullableTime = null;

        Format = @"HH\:mm";
    }
    public string _originalFormat = null;

    public static readonly BindableProperty PlaceHolderProperty =
        BindableProperty.Create(nameof(PlaceHolder), typeof(string), typeof(NullableTimePicker), "  :  ");

    public string PlaceHolder
    {
        get { return (string)GetValue(PlaceHolderProperty); }
        set
        {
            SetValue(PlaceHolderProperty, value);
        }
    }


    public static readonly BindableProperty NullableTimeProperty =
    BindableProperty.Create(nameof(NullableTime), typeof(TimeSpan?), typeof(NullableTimePicker), null, defaultBindingMode: BindingMode.TwoWay);

    public TimeSpan? NullableTime
    {
        get { return (TimeSpan?)GetValue(NullableTimeProperty); }
        set { SetValue(NullableTimeProperty, value); UpdateTime(); }
    }

    private void UpdateTime()
    {
        if (NullableTime != null)
        {
            if (_originalFormat != null)
            {
                Format = _originalFormat;
            }
        }
        else
        {
            Format = PlaceHolder;
        }

    }
    protected override void OnBindingContextChanged()
    {
        base.OnBindingContextChanged();
        if (BindingContext != null)
        {
            _originalFormat = Format;
            UpdateTime();
        }
    }

    protected override void OnPropertyChanged(string propertyName = null)
    {
        base.OnPropertyChanged(propertyName);

        if (propertyName == TimeProperty.PropertyName ||
            (
                propertyName == IsFocusedProperty.PropertyName &&
                !IsFocused &&
                (Time == DateTime.Now.TimeOfDay)))
        {
            AssignValue();
        }

        if (propertyName == NullableTimeProperty.PropertyName && NullableTime.HasValue)
        {
            Time = NullableTime.Value;
            if (Time == DateTime.Now.TimeOfDay)
            {
                //this code was done because when date selected is the actual date the"DateProperty" does not raise  
                UpdateTime();
            }
        }
    }

    public void CleanTime()
    {
        NullableTime = null;
        UpdateTime();
    }
    public void AssignValue()
    {
        NullableTime = Time;
        UpdateTime();

    }
}

On Android project:

public class NullableTimePickerRenderer : ViewRenderer<NullableTimePicker, EditText>
    {
        public NullableTimePickerRenderer(Context context) : base(context)
        {
        }

        TimePickerDialog _dialog;
        protected override void OnElementChanged(ElementChangedEventArgs<NullableTimePicker> e)
        {
            base.OnElementChanged(e);

            this.SetNativeControl(new Android.Widget.EditText(Context));
            if (Control == null || e.NewElement == null)
                return;

            this.Control.Click += OnPickerClick;

            if (Element.NullableTime.HasValue)
                Control.Text = DateTime.Today.Add(Element.Time).ToString(Element.Format);
            else
                this.Control.Text = Element.PlaceHolder;

            this.Control.KeyListener = null;
            this.Control.FocusChange += OnPickerFocusChange;
            this.Control.Enabled = Element.IsEnabled;

        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == Xamarin.Forms.TimePicker.TimeProperty.PropertyName ||
                e.PropertyName == Xamarin.Forms.TimePicker.FormatProperty.PropertyName)
                SetTime(Element.Time);
        }

        void OnPickerFocusChange(object sender, Android.Views.View.FocusChangeEventArgs e)
        {
            if (e.HasFocus)
            {
                ShowTimePicker();
            }
        }

        protected override void Dispose(bool disposing)
        {
            if (Control != null)
            {
                this.Control.Click -= OnPickerClick;
                this.Control.FocusChange -= OnPickerFocusChange;

                if (_dialog != null)
                {
                    _dialog.Hide();
                    _dialog.Dispose();
                    _dialog = null;
                }
            }

            base.Dispose(disposing);
        }

        void OnPickerClick(object sender, EventArgs e)
        {
            ShowTimePicker();
        }

        void SetTime(TimeSpan time)
        {
            Control.Text = DateTime.Today.Add(time).ToString(Element.Format);
            Element.Time = time;
        }

        private void ShowTimePicker()
        {
            CreateTimePickerDialog(this.Element.Time.Hours, this.Element.Time.Minutes);
            _dialog.Show();
        }

        void CreateTimePickerDialog(int hours, int minutes)
        {
            NullableTimePicker view = Element;
            _dialog = new TimePickerDialog(Context, (o, e) =>
            {
                view.Time = new TimeSpan(hours: e.HourOfDay, minutes: e.Minute, seconds: 0);
                view.AssignValue();
                ((IElementController)view).SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
                Control.ClearFocus();

                _dialog = null;
            }, hours, minutes, true);

            _dialog.SetButton("ok", (sender, e) =>
            {
                SetTime(Element.Time);
                this.Element.Format = this.Element._originalFormat;
                this.Element.AssignValue();
            });

            _dialog.SetButton2("clear", (sender, e) =>
            {
                this.Element.CleanTime();
                Control.Text = this.Element.Format;
            });
        }
    }

On iOS project:

public class NullableTimePickerRenderer : TimePickerRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<TimePicker> e)
        {
            base.OnElementChanged(e);
            var timePicker = (UIDatePicker)Control.InputView;
            timePicker.Locale = new NSLocale("no_nb");

            if (e.NewElement != null && this.Control != null)
            {
                this.UpdateDoneButton();
                this.AddClearButton();
                this.Control.BorderStyle = UITextBorderStyle.Line;
                Control.Layer.BorderColor = UIColor.LightGray.CGColor;
                Control.Layer.BorderWidth = 1;

                if (Device.Idiom == TargetIdiom.Tablet)
                {
                    this.Control.Font = UIFont.SystemFontOfSize(25);
                }
            }

        }

        private void UpdateDoneButton()
        {
            var toolbar = (UIToolbar)Control.InputAccessoryView;
            var doneBtn = toolbar.Items[1];

            doneBtn.Clicked += (sender, args) =>
            {
                NullableTimePicker baseTimePicker = this.Element as NullableTimePicker;
                if (!baseTimePicker.NullableTime.HasValue)
                {
                    baseTimePicker.AssignValue();
                }
            };
        }

        private void AddClearButton()
        {
            var originalToolbar = this.Control.InputAccessoryView as UIToolbar;

            if (originalToolbar != null && originalToolbar.Items.Length <= 2)
            {
                var clearButton = new UIBarButtonItem("clear", UIBarButtonItemStyle.Plain, ((sender, ev) =>
                {
                    NullableTimePicker baseTimePicker = this.Element as NullableTimePicker;
                    this.Element.Unfocus();
                    this.Element.Time = DateTime.Now.TimeOfDay;
                    baseTimePicker.CleanTime();

                }));

                var newItems = new List<UIBarButtonItem>();
                foreach (var item in originalToolbar.Items)
                {
                    newItems.Add(item);
                }

                newItems.Insert(0, clearButton);

                originalToolbar.Items = newItems.ToArray();
                originalToolbar.SetNeedsDisplay();
            }
        }
    }

This code works fine on iOS and Android version 8.1 or higher, but in lower version, this just not fire the event to set time, setting always the default time.

I'm also provided a github repo with the code, maybe, make easily understand my problem. (aismaniotto/Nullable24hTimePicker)

Best Answer

  • aismaniottoaismaniotto ✭✭ ✭✭
    Accepted Answer

    Hi people. Thanks for your help. After more digging on code and debugging, I realize the problem was on OkButton implementation. On Android 8, apparently, the callback from the TimePickerDialog was called before the OkButton implementation e what was there, is ignored. On the older version, the OKButton implementation was called before and, for some reason, cancel the invocation of the callback.

    That way, removing the OkButton implementation, the problem was solved... remembering that on the working version, was ignored anyway. Eventually, I will commit to the repository, to be registered.

    Thanks a lot.

Answers

  • jezhjezh Xamurai Member, Xamarin Team Xamurai

    I have tested the code of the github repo you post on Android Emulator with android version 8.0 , it worked property which could fire the event to set time.

  • aismaniottoaismaniotto ✭✭ Member ✭✭

    Could test on Android 6? Can be on emulator.

  • JoeMankeJoeManke ✭✭✭✭✭ USMember ✭✭✭✭✭
    edited March 21

    Comments I left for myself in my NullableTimePicker renderer on Android:

    TimeSpan timeSpan = Element.Time ?? DateTime.Now.TimeOfDay;
    _dialog = new TimePickerDialog(Context, OnTimeSet, timeSpan.Hours, timeSpan.Minutes, false);
    
    // The OnTimeSet callback is the only way to get the time from the dialog.
    // On Lollipop and later, the callback is only called if the Positive button is clicked and does not have a handler. 
    // On earlier versions, the callback is called after the click handler regardless of which button is clicked. 
    if (Build.VERSION.SdkInt < BuildVersionCodes.Lollipop)
    {
        _dialog.SetButton((int)DialogButtonType.Positive, "Done", OnDialogButtonClicked);
    }
    
  • chetanrawatchetanrawat ✭✭✭ USMember ✭✭✭

    Hi @aismaniotto can you register your custom renderer? Like that

    [assembly: ExportRenderer(typeof(Xamarin.Forms.ProgressBar), typeof(CustomProgressBarRenderer))]

  • aismaniottoaismaniotto ✭✭ Member ✭✭

    @chetanrawat said:
    Hi @aismaniotto can you register your custom renderer? Like that

    [assembly: ExportRenderer(typeof(Xamarin.Forms.ProgressBar), typeof(CustomProgressBarRenderer))]

    Yes, I did that. Is possible to see on git repo. I put here just the logic code.

  • aismaniottoaismaniotto ✭✭ Member ✭✭
    Accepted Answer

    Hi people. Thanks for your help. After more digging on code and debugging, I realize the problem was on OkButton implementation. On Android 8, apparently, the callback from the TimePickerDialog was called before the OkButton implementation e what was there, is ignored. On the older version, the OKButton implementation was called before and, for some reason, cancel the invocation of the callback.

    That way, removing the OkButton implementation, the problem was solved... remembering that on the working version, was ignored anyway. Eventually, I will commit to the repository, to be registered.

    Thanks a lot.

Sign In or Register to comment.