0
votes

I am having a requirement like a Month picker which is showing only month and year and not the date .How can we achieve the same for both IOS and Android platform using Xamarin forms ?

1

1 Answers

2
votes

You could implement it by using Custom Renderer

in Forms

create a custom View

using System;
using System.Collections.Generic;
using System.Text;
using Xamarin.Forms;

namespace App20
{
    public  class MonthYearPickerView :View
    {
      
        public static readonly BindableProperty FontSizeProperty = BindableProperty.Create(
            propertyName: nameof(FontSize),
            returnType: typeof(double),
            declaringType: typeof(MonthYearPickerView),
            defaultValue: (double)24,
            defaultBindingMode: BindingMode.TwoWay);

        [TypeConverter(typeof(FontSizeConverter))]
        public double FontSize
        {
            get => (double)GetValue(FontSizeProperty);
            set => SetValue(FontSizeProperty, value);
        }

       

        public static readonly BindableProperty TextColorProperty = BindableProperty.Create(
            propertyName: nameof(TextColor),
            returnType: typeof(Color),
            declaringType: typeof(MonthYearPickerView),
            defaultValue: Color.White,
            defaultBindingMode: BindingMode.TwoWay);

        public Color TextColor
        {
            get => (Color)GetValue(TextColorProperty);
            set => SetValue(TextColorProperty, value);
        }

     
        public static readonly BindableProperty InfiniteScrollProperty = BindableProperty.Create(
            propertyName: nameof(InfiniteScroll),
            returnType: typeof(bool),
            declaringType: typeof(MonthYearPickerView),
            defaultValue: true,
            defaultBindingMode: BindingMode.TwoWay);

        public bool InfiniteScroll
        {
            get => (bool)GetValue(InfiniteScrollProperty);
            set => SetValue(InfiniteScrollProperty, value);
        }

  

        public static readonly BindableProperty DateProperty = BindableProperty.Create(
            propertyName: nameof(Date),
            returnType: typeof(DateTime),
            declaringType: typeof(MonthYearPickerView),
            defaultValue: default,
            defaultBindingMode: BindingMode.TwoWay);

        public DateTime Date
        {
            get => (DateTime)GetValue(DateProperty);
            set => SetValue(DateProperty, value);
        }



        public static readonly BindableProperty MaxDateProperty = BindableProperty.Create(
            propertyName: nameof(MaxDate),
            returnType: typeof(DateTime?),
            declaringType: typeof(MonthYearPickerView),
            defaultValue: default,
            defaultBindingMode: BindingMode.TwoWay);

        public DateTime? MaxDate
        {
            get => (DateTime?)GetValue(MaxDateProperty);
            set => SetValue(MaxDateProperty, value);
        }



        public static readonly BindableProperty MinDateProperty = BindableProperty.Create(
            propertyName: nameof(MinDate),
            returnType: typeof(DateTime?),
            declaringType: typeof(MonthYearPickerView),
            defaultValue: default,
            defaultBindingMode: BindingMode.TwoWay);

        public DateTime? MinDate
        {
            get => (DateTime?)GetValue(MinDateProperty);
            set => SetValue(MinDateProperty, value);
        }

      
    }
}

in iOS

using System;
using App20;
using App20.iOS;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(MonthYearPickerView), typeof(MonthYearPickerRenderer))]
namespace App20.iOS
{
    public class MonthYearPickerRenderer : ViewRenderer<MonthYearPickerView, UITextField>
    {
        private DateTime _selectedDate;
        private UITextField _dateLabel;
        private PickerDateModel _pickerModel;

        protected override void OnElementChanged(ElementChangedEventArgs<MonthYearPickerView> e)
        {
            base.OnElementChanged(e);
            _dateLabel = new UITextField();
            _dateLabel.TextAlignment = UITextAlignment.Center;

            var dateToday = DateTime.Today;
            SetupPicker(new DateTime(dateToday.Year, dateToday.Month, 1));
            
            SetNativeControl(_dateLabel);

            Control.EditingChanged += ControlOnEditingChanged;
            Element.PropertyChanged += Element_PropertyChanged;
        }

        private void ControlOnEditingChanged(object sender, EventArgs e)
        {
            var currentDate = $"{Element.Date.Month:D2} | {Element.Date.Year}";
            if (_dateLabel.Text != currentDate)
            {
                _dateLabel.Text = currentDate;
            }
        }

        protected override void Dispose(bool disposing)
        {
            Element.PropertyChanged -= Element_PropertyChanged;
            base.Dispose(disposing);
        }

        private void SetupPicker(DateTime date)
        {
            var datePicker = new UIPickerView();
            _pickerModel = new PickerDateModel(datePicker, date, Element.MaxDate, Element.MinDate);
            datePicker.ShowSelectionIndicator = true;
            _selectedDate = date;
            _pickerModel.PickerChanged += (sender, e) =>
            {
                _selectedDate = e;
            };
            datePicker.Model = _pickerModel;
            _pickerModel.MaxDate = Element.MaxDate ?? DateTime.MaxValue;
            _pickerModel.MinDate = Element.MinDate ?? DateTime.MinValue;

            var toolbar = new UIToolbar
            {
                BarStyle = UIBarStyle.Default, 
                Translucent = true
            };
            toolbar.SizeToFit();

            var doneButton = new UIBarButtonItem("Done", UIBarButtonItemStyle.Done,
                (s, e) =>
                {
                    Element.Date = _selectedDate;
                    _dateLabel.Text = $"{Element.Date.Month:D2} | {Element.Date.Year}";
                    _dateLabel.ResignFirstResponder();
                });

            toolbar.SetItems(new[] { new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace), doneButton }, true);

            _dateLabel.InputView = datePicker;
            _dateLabel.Text = $"{Element.Date.Month:D2} | {Element.Date.Year}";
            _dateLabel.InputAccessoryView = toolbar;
            _dateLabel.TextColor = Element.TextColor.ToUIColor();
        }

        private void Element_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == MonthYearPickerView.MaxDateProperty.PropertyName)
            {
                _pickerModel.MaxDate = Element.MaxDate ?? DateTime.MinValue;
            }
            else if (e.PropertyName == MonthYearPickerView.MinDateProperty.PropertyName)
            {
                _pickerModel.MinDate = Element.MinDate ?? DateTime.MaxValue;
            }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using UIKit;

namespace App20.iOS
{
    public class PickerDateModel : UIPickerViewModel
    {
        public event EventHandler<DateTime> PickerChanged;

        #region Fields

        private readonly List<string> _mainNamesOfMonthSource;
        private readonly List<int> _mainYearsSource;
        private readonly UIPickerView _picker;
        private readonly int _numberOfComponents;
        private readonly int _minYear;
        private readonly int _maxYear;

        private List<int> _years;
        private List<string> _namesOfMonth;

        private DateTime _selectedDate;
        private DateTime _maxDate;
        private DateTime _minDate;

        #endregion Fields

        #region Constructors

        public PickerDateModel(UIPickerView datePicker, DateTime selectedDate, DateTime? maxDate, DateTime? minDate)
        {
            _mainNamesOfMonthSource = DateTimeFormatInfo.CurrentInfo?.MonthNames
                .Where(x => !string.IsNullOrWhiteSpace(x))
                .ToList();



            _maxDate = maxDate ?? DateTime.MaxValue;
            _minDate = minDate ?? DateTime.MinValue;

            _maxYear = _maxDate.Year;
            _minYear = _minDate.Year;

            _years = new List<int>();

            
            
            _picker = datePicker;
            _namesOfMonth = _mainNamesOfMonthSource;
            
            _numberOfComponents = 2;

            SelectedDate = selectedDate;
        }

        #endregion Constructors

        #region Properties

        public DateTime SelectedDate
        {
            get => _selectedDate;
            set
            {
                _selectedDate = value;
                ReloadSections();
                PickerChanged?.Invoke(this, value);
            }
        }


        public DateTime MaxDate
        {
            get => _maxDate;
            set
            {
                _maxDate = value;
                ReloadSections();
            }
        }

        public DateTime MinDate
        {
            get => _minDate;
            set
            {
                _minDate = value;
                ReloadSections();
            }
        }

        #endregion Properties

        #region Private Methods

        private void ReloadSections()
        {
            var selectedDate = SelectedDate == DateTime.MinValue
                ? DateTime.Today
                : SelectedDate;

            _years.Clear();
            for (int i = _minYear; i <= _maxYear; i++)
            {
                _years.Add(i);
            }

            _namesOfMonth = _mainNamesOfMonthSource;

            if (SelectedDate.Year == MinDate.Year)
            {
                _namesOfMonth = _mainNamesOfMonthSource.Skip(MinDate.Month - 1).ToList();
            }

            if (SelectedDate.Year == MaxDate.Year)
            {
                _namesOfMonth = _mainNamesOfMonthSource.Take(MaxDate.Month).ToList();
            }

            SetCarousels(selectedDate);
        }

        #endregion Private Methods

        #region Public Methods

        public void SetCarousels(DateTime dateTime)
        {
            if (_picker.NumberOfComponents != _numberOfComponents) return;

            var y = DateTimeFormatInfo.CurrentInfo?.GetMonthName(dateTime.Month);
            var x = _namesOfMonth.IndexOf(y);

            _picker.Select(x, 0, false);
            _picker.Select(_years.IndexOf(dateTime.Year), 1, false);

            _picker.ReloadComponent(0);
            _picker.ReloadComponent(1);
        }

        public override nint GetComponentCount(UIPickerView pickerView)
        {
            return _numberOfComponents;
        }

        public override nint GetRowsInComponent(UIPickerView pickerView, nint component)
        {



            if (component == 0)
            {
                return _namesOfMonth.Count;
            }
            else if (component == 1)
            {
                return _years.Count;
            }
            else
            {
                return 0;
            }

        }


        public override string GetTitle(UIPickerView pickerView, nint row, nint component)
        {
            

            if (component == 0)
            {
                return _namesOfMonth.Count==0 ? _namesOfMonth.First() : _namesOfMonth[(int)row];
            }
            else if (component == 1)
            {
                var list = _years;
                return _years.Count==0? _years.First().ToString() : _years[(int)row].ToString();
            }
            else
            {
                return row.ToString();
            }
           
        }
    
        public override void Selected(UIPickerView pickerView, nint row, nint component)
        {
            var month = GetMonthNumberByName(_namesOfMonth[(int)pickerView.SelectedRowInComponent(0)]);
            var year = _years[(int)pickerView.SelectedRowInComponent(1)];

            if (year == MinDate.Year)
            {
                month = month >= MinDate.Month ? month : MinDate.Month;
            }
            
            if (year == MaxDate.Year)
            {
                month = month <= MaxDate.Month ? month : MaxDate.Month;
            }
    
            SelectedDate = new DateTime(year, month, 1);
    
             ReloadSections();
            pickerView.ReloadAllComponents();

            int GetMonthNumberByName(string monthName) =>
                DateTime.ParseExact(monthName, "MMMM", CultureInfo.CurrentCulture).Month;
        }
        
        #endregion Public Methods
    }
}

in Android

in MainActivity

    public static MainActivity Instance { get; private set; }

    protected override void OnCreate(Bundle savedInstanceState)
    {
        TabLayoutResource = Resource.Layout.Tabbar;
        ToolbarResource = Resource.Layout.Toolbar;

        Instance = this;

        base.OnCreate(savedInstanceState);

        Xamarin.Essentials.Platform.Init(this, savedInstanceState);
        global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
        LoadApplication(new App());
    }
using Android.Content;
using Android.Support.V7.App;
using Android.Widget;
using App20;
using App20.Droid;

using System;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(MonthYearPickerView), typeof(MonthYearPickerRenderer))]
namespace App20.Droid
{
    public class MonthYearPickerRenderer : ViewRenderer<MonthYearPickerView, EditText>
    {
        private readonly Context _context;
        private MonthYearPickerDialog _monthYearPickerDialog;

        public MonthYearPickerRenderer(Context context) : base(context)
        {
            _context = context;
        }

        protected override void OnElementChanged(ElementChangedEventArgs<MonthYearPickerView> e)
        {
            base.OnElementChanged(e);

            CreateAndSetNativeControl();

            Control.KeyListener = null;
            Element.Focused += Element_Focused;
        }

        protected override void Dispose(bool disposing)
        {
            if (Control == null) return;

            Element.Focused -= Element_Focused;

            if (_monthYearPickerDialog != null)
            {
                _monthYearPickerDialog.OnDateTimeChanged -= OnDateTimeChanged;
                _monthYearPickerDialog.OnClosed -= OnClosed;
                _monthYearPickerDialog.Hide();
                _monthYearPickerDialog.Dispose();
                _monthYearPickerDialog = null;
            }

            base.Dispose(disposing);
        }

        #region Private Methods

        private void ShowDatePicker()
        {
            if (_monthYearPickerDialog == null)
            {
                _monthYearPickerDialog = new MonthYearPickerDialog();
                _monthYearPickerDialog.OnDateTimeChanged += OnDateTimeChanged;
                _monthYearPickerDialog.OnClosed += OnClosed;
            }
            _monthYearPickerDialog.Date = Element.Date;
            _monthYearPickerDialog.MinDate = FormatDateToMonthYear(Element.MinDate);
            _monthYearPickerDialog.MaxDate = FormatDateToMonthYear(Element.MaxDate);
            _monthYearPickerDialog.InfiniteScroll = Element.InfiniteScroll;

            var appcompatActivity = MainActivity.Instance;
            var mFragManager = appcompatActivity?.SupportFragmentManager;
            if (mFragManager != null)
            {
                _monthYearPickerDialog.Show(mFragManager, nameof(MonthYearPickerDialog));
            }
        }

        private void ClearPickerFocus()
        {
            ((IElementController)Element).SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
            Control.ClearFocus();
        }

        private DateTime? FormatDateToMonthYear(DateTime? dateTime) => 
            dateTime.HasValue ? (DateTime?) new DateTime(dateTime.Value.Year, dateTime.Value.Month, 1) : null;

        private void CreateAndSetNativeControl()
        {
            var tv = new EditText(_context);

            tv.SetTextColor(Element.TextColor.ToAndroid());
            tv.TextSize = (float)Element.FontSize;
            tv.Text = $"{Element.Date.Month:D2} | {Element.Date.Year}";
            tv.Gravity = Android.Views.GravityFlags.Center;
            tv.SetBackgroundColor(Element.BackgroundColor.ToAndroid());

            SetNativeControl(tv);
        }

        #endregion

        #region Event Handlers

        private void Element_Focused(object sender, FocusEventArgs e)
        {
            if (e.IsFocused)
            {
                ShowDatePicker();
            }
        }

        private void OnClosed(object sender, DateTime e)
        {
            ClearPickerFocus();
        }

        private void OnDateTimeChanged(object sender, DateTime e)
        {
            Element.Date = e;
            Control.Text = $"{Element.Date.Month:D2} | {Element.Date.Year}";
            ClearPickerFocus();
        }

        #endregion
    }
}
using Android.App;
using Android.OS;
using Android.Views;
using Android.Widget;
using System;
using System.Linq;


namespace App20.Droid
{
    public class MonthYearPickerDialog : Android.Support.V4.App.DialogFragment
    {
        public event EventHandler<DateTime> OnDateTimeChanged;
        public event EventHandler<DateTime> OnClosed;

        #region Private Fields

        private const int DefaultDay = 1;
        private const int MinNumberOfMonths = 1;
        private const int MaxNumberOfMonths = 12;
        private const int MinNumberOfYears = 1900;
        private const int MaxNumberOfYears = 2100;

        private NumberPicker _monthPicker;
        private NumberPicker _yearPicker;

        #endregion

        #region Public Properties
        
        public DateTime? MinDate { get; set; }
        public DateTime? MaxDate { get; set; }
        public DateTime? Date { get; set; }
        public bool InfiniteScroll { get; set; }

        #endregion

        public void Hide() => base.Dialog?.Hide();

        public override Dialog OnCreateDialog(Bundle savedInstanceState)
        {
            var builder = new AlertDialog.Builder(Activity);
            var inflater = Activity.LayoutInflater;

            var selectedDate = GetSelectedDate();

            var dialog = inflater.Inflate(Resource.Layout.date_picker_dialog, null);
            _monthPicker = (NumberPicker)dialog.FindViewById(Resource.Id.picker_month);
            _yearPicker = (NumberPicker)dialog.FindViewById(Resource.Id.picker_year);

            InitializeMonthPicker(selectedDate.Month);
            InitializeYearPicker(selectedDate.Year);
            SetMaxMinDate(MaxDate, MinDate);

            builder.SetView(dialog)
                .SetPositiveButton("Ok", (sender, e) =>
                {
                    selectedDate = new DateTime(_yearPicker.Value, _monthPicker.Value, DefaultDay);
                    OnDateTimeChanged?.Invoke(dialog, selectedDate);
                })
                .SetNegativeButton("Cancel", (sender, e) =>
                {
                    Dialog.Cancel();
                    OnClosed?.Invoke(dialog, selectedDate);
                });
            return builder.Create();
        }

        protected override void Dispose(bool disposing)
        {
            if (_yearPicker != null)
            {
                _yearPicker.ScrollChange -= YearPicker_ScrollChange;
                _yearPicker.Dispose();
                _yearPicker = null;
            }

            _monthPicker?.Dispose();
            _monthPicker = null;


            base.Dispose(disposing);
        }

        #region Private Methods

        private DateTime GetSelectedDate() => Date ?? DateTime.Now;

        private void InitializeYearPicker(int year)
        {
            _yearPicker.MinValue = MinNumberOfYears;
            _yearPicker.MaxValue = MaxNumberOfYears;
            _yearPicker.Value = year;
            _yearPicker.ScrollChange += YearPicker_ScrollChange;
            if (!InfiniteScroll)
            {
                _yearPicker.WrapSelectorWheel = false;
                _yearPicker.DescendantFocusability = DescendantFocusability.BlockDescendants;
            }
        }

        private void InitializeMonthPicker(int month)
        {
            _monthPicker.MinValue = MinNumberOfMonths;
            _monthPicker.MaxValue = MaxNumberOfMonths;
            _monthPicker.SetDisplayedValues(GetMonthNames());
            _monthPicker.Value = month;
            if (!InfiniteScroll)
            {
                _monthPicker.WrapSelectorWheel = false;
                _monthPicker.DescendantFocusability = DescendantFocusability.BlockDescendants;
            }
        }

        private void YearPicker_ScrollChange(object sender, View.ScrollChangeEventArgs e)
        {
            SetMaxMinDate(MaxDate, MinDate);
        }

        private void SetMaxMinDate(DateTime? maxDate, DateTime? minDate)
        {
            try
            {
                if (maxDate.HasValue)
                {
                    var maxYear = maxDate.Value.Year;
                    var maxMonth = maxDate.Value.Month;

                    if (_yearPicker.Value == maxYear)
                    {
                        _monthPicker.MaxValue = maxMonth;
                    }
                    else if (_monthPicker.MaxValue != MaxNumberOfMonths)
                    {
                        _monthPicker.MaxValue = MaxNumberOfMonths;
                    }

                    _yearPicker.MaxValue = maxYear;
                }

                if (minDate.HasValue)
                {
                    var minYear = minDate.Value.Year;
                    var minMonth = minDate.Value.Month;

                    if (_yearPicker.Value == minYear)
                    {
                        _monthPicker.MinValue = minMonth;
                    }
                    else if (_monthPicker.MinValue != MinNumberOfMonths)
                    {
                        _monthPicker.MinValue = MinNumberOfMonths;
                    }

                    _yearPicker.MinValue = minYear;
                }
                _monthPicker.SetDisplayedValues(GetMonthNames(_monthPicker.MinValue));
            }
            catch (Exception e)
            {
              
            }
        }

        private string[] GetMonthNames(int start = 1) => 
            System.Globalization.DateTimeFormatInfo.CurrentInfo?.MonthNames.Skip(start - 1).ToArray();

        #endregion

    }
}

create date_picker_dialog.xml in Resource ->layout

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">

  <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:orientation="horizontal">

    <NumberPicker
            android:id="@+id/picker_month"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="20dp"
            android:layout_marginRight="20dp">
    </NumberPicker>

    <NumberPicker
            android:id="@+id/picker_year"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
    </NumberPicker>
  </LinearLayout>
</LinearLayout>

Now you can reference it in Xaml

<StackLayout VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand">
        <local:MonthYearPickerView
            Date="06.15.2020"
            BackgroundColor="LightBlue"
           WidthRequest="150"
            MinDate="01.01.2020"
            MaxDate="12.31.2050"
            HorizontalOptions="CenterAndExpand"
            VerticalOptions="Center" />
    </StackLayout>

enter image description here