Update Picker ItemSource and SelectedItem in ViewModel in MessaginCenter.Subscribe Callback error.

ZGuidoZGuido CHMember ✭✭

Hi,

I'm facing a problem during the update of a Picker ItemSource and SelectedItem in the ViewModel during a callback of a received message.

I'm using XF 2.5.0 and I've reproduced the error on emulator with Android 6 and also on a physical device with Android 7 (so actually on every device I have).

I've a modal main page with a picker binded with an observablecollection in the viewmodel. In the main page there is a button that when clicked push a new modal page that allows to create a new item that will replace the Picker ItemSource as unique member.

When the page for adding a new element disappear the picker appears with nothing selected; if I touch the picker the new element is visible.

If I put a breakpoint in the Setter of the ItemSelected I can see that is called twice: the first with the new item value, the second with null.

I've tried using different binding mode but it doesn't work.

This is the MainPage:

    <StackLayout>
        <Picker x:Name="PickerItems"
                ItemsSource="{Binding ItemsList}"
                ItemDisplayBinding="{Binding Description}"
                SelectedItem="{Binding ItemSelected, Mode=TwoWay}"
                HorizontalOptions="FillAndExpand" 
                TextColor="Black"
                Margin="0" />
        <Button Command="{Binding AddItemCommand}" Text="Add item" IsVisible="True"/>
    </StackLayout>

In the viewModel of the main page the command is managed this way:

    public ICommand AddItemCommand => new Command(async () => await AddItemCommandAsync());

        protected virtual async Task AddItemCommandAsync()
        {
            MessagingCenter.Subscribe<UpdateItemData>(this, "UpdateItem", async (model) =>
            {
                MessagingCenter.Unsubscribe<UpdateItemData>(this, "UpdateItem");

                List<PickerItem> list = new List<PickerItem>();
                list.Add(model.PickerItem);

                ItemsList = new ObservableCollection<PickerItem>(list);
                ItemSelected = ItemsList.First();
            });

            NewPickerItemViewModel viewModel = new NewPickerItemViewModel();
            NewItemPage newItemPage = new NewItemPage(viewModel);

            await Navigation.PushModalAsync(newItemPage);
        }

On AddItemCommand a new modal page is pushed on the navigation stack. This page is very simple:

        <StackLayout>

            <Entry TextColor="Black"
                   Text="{Binding Description}" />

            <Button Command="{Binding NewItemCommand}"
                    Text="Create" />

            <Button Command="{Binding BackCommand}"
                    Text="Back" />

        </StackLayout>

When NewItemCommand in the viewmodel is:

       public ICommand NewItemCommand => new Command(async () => await NewItemCommandAsync());

        private async Task NewItemCommandAsync()
        {
            UpdateItemData updateItemData = new UpdateItemData();
            PickerItem pickerItem = new PickerItem();

            pickerItem.Description = Description;
            pickerItem.Id = 0;

            updateItemData.PickerItem = pickerItem;

            MessagingCenter.Send(updateItemData, "UpdateItem");

            await Navigation.PopModalAsync();
        }

Do you have any suggestion?

Best regards

Andrea

Tagged:

Posts

  • AlessandroCaliaroAlessandroCaliaro ITMember ✭✭✭✭✭

    Hi Andrea.
    First, you should set Subscribe and Unsubscribe in OnAppearing / OnDisappearing events.. it's a good solution...

    Second. I have done some changes... take a look. You should continue to use ItemsList without reassign it with a new List... clear and add... clear and add. It should works

    Let me know...

  • ZGuidoZGuido CHMember ✭✭

    Thanks Alessandro.

    It works fine now.

    Best regards

  • ZGuidoZGuido CHMember ✭✭

    Hi Alessandro,

    I think I missing something about using Subscribe in OnAppearing and Unsubscribe in OnDisappearing.

    When I click the button to navigate to the second window the OnDisappearing on the main windows is called and so the Unsubscribe. Consequently the message sent by the second window is lost.

    Thanks

    Andrea

  • AlessandroCaliaroAlessandroCaliaro ITMember ✭✭✭✭✭
    I take a look in the evening
  • AlessandroCaliaroAlessandroCaliaro ITMember ✭✭✭✭✭
    edited February 2018

    I am drinking a coffee at Autogrill and I am taking a look to your code.
    I don't understand one thing. You are using Navigation but I don't see where you set the NavigationPage.
    Usually you should set the "root" page with a code like

    Application.Current.MainPage = new NavigationPage(new MainPage());
    
  • ZGuidoZGuido CHMember ✭✭

    Hi,

    I set the viewmodel property Navigation in the MainPage constructor;

            public MainPage(MainPageViewModel viewModel)
            {
                InitializeComponent();
                viewModel.Navigation = Navigation;
                BindingContext = viewModel;
            }
    

    So I don't any NavigationPage because I want to use modal page.

    public App ()
    {
        InitializeComponent();
        MainPageViewModel mainPageViewModel = new MainPageViewModel();
        MainPage = new XamarinFormPickerTest.MainPage(mainPageViewModel);
    }
    

    Thanks

  • AlessandroCaliaroAlessandroCaliaro ITMember ✭✭✭✭✭

    I think if you want to use Navigation you should have a NavigationPage as root. if you want to use Modal, you can PushModal or (if I remember) remove the navigationbar

  • ZGuidoZGuido CHMember ✭✭

    I use PushModal... but this method is in the Navigation class....

  • AlessandroCaliaroAlessandroCaliaro ITMember ✭✭✭✭✭
    Try setting your MainPage as NavigationPage
  • ZGuidoZGuido CHMember ✭✭

    I've done the same test replacing in App.xaml.cs

    MainPage = new XamarinFormPickerTest.MainPage(mainPageViewModel);

    with:

    MainPage = new NavigationPage(new XamarinFormPickerTest.MainPage(mainPageViewModel));

    But during the transition from page1 to page2 the OnDisappearing is called, and so the unsubscribe..

    It seems OnDisappearing is always called.

    Maybe the mistake is in the approach; I think I could use other 2 approach to get value from a modal page:

    1. Pass a callback function to the second page
    2. Maybe using TaskCompletionSource as explained forums.xamarin.com/discussion/65041/modal-return (cannot post link complete). This example actually is not complete so I'm not sure.

    Best regards

  • AlessandroCaliaroAlessandroCaliaro ITMember ✭✭✭✭✭

    I will take a look, but it's strange....

  • AlessandroCaliaroAlessandroCaliaro ITMember ✭✭✭✭✭

    Yes, you are right. I think another solution is to pass to NewItemPage the MainPage's ViewModel. Modify it and changes should reflect in MainPage

  • Chetan333Chetan333 Member ✭✭

    Hi Alessandro

    I am using custom datepicker it not working fine for me, could u help me the same.
    I want to bind the date in the format ("ddd MMM dd"), Hour,minute and AM/PM
    after the select the date it stored in oneoff the label.

    This is the PickerViewPage.xml



    <StackLayout.BindingContext>

    </StackLayout.BindingContext>

                    <pickerView:PickerView
                    WidthRequest="50"
                    ItemsSource="{Binding Hour}"
                        SelectedItem="{Binding CurrHour, Mode=TwoWay}"
                    />
                    <pickerView:PickerView
                    WidthRequest="50"
                    ItemsSource="{Binding Minute}"
                        SelectedItem="{Binding CurrMin, Mode=TwoWay}"
                    />
                    <pickerView:PickerView
                    WidthRequest="50"
                        ItemsSource="{Binding Format}"
                        SelectedItem="{Binding CurrZone, Mode=TwoWay}"
                    />
    
                </StackLayout>
    

    PickerView.cs
    public class PickerView : View
    {
    #region ItemsSource
    public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource),
    typeof(IEnumerable), typeof(PickerView), null);

        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }
        #endregion
    
        #region SelectedIndex
        public static readonly BindableProperty SelectedIndexProperty =
            BindableProperty.Create(nameof(SelectedIndex), typeof(int), typeof(PickerView), -1, BindingMode.TwoWay,
                coerceValue: CoerceSelectedIndex);
    
    
        public int SelectedIndex
        {
            get { return (int)GetValue(SelectedIndexProperty); }
            set { SetValue(SelectedIndexProperty, value); }
        }
    
        private static object CoerceSelectedIndex(BindableObject bindable, object value)
        {
            if (value == null)
            {
                return 0;
            }
            return value;
        }
        #endregion
    
        public static readonly BindableProperty SelectedItemProperty =
           BindableProperty.Create(nameof(SelectedItem), typeof(object), typeof(PickerView), null, BindingMode.TwoWay, propertyChanged: OnSelectedItemChanged);
        public object SelectedItem
        {
            get { return (object)GetValue(SelectedItemProperty); }
            set { SetValue(SelectedItemProperty, value); }
        }
        private static void OnSelectedItemChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var itemsView = (PickerView)bindable;
            if (newValue == oldValue && newValue != null)
            {
                return;
            }
        }
    

    }

    NumbersPickerViewModel.cs
    internal class NumbersPickerViewModel : INotifyPropertyChanged
    {
    public event PropertyChangedEventHandler PropertyChanged;
    private string[] _month;
    public string[] Month
    {
    get { return _month; }
    set
    {
    _month = value;
    OnPropertyChanged();
    }
    }
    private string[] _hour;
    public string[] Hour
    {
    get { return _hour; }
    set
    {
    _hour = value;
    OnPropertyChanged();
    }
    }
    private string[] _minute;
    public string[] Minute
    {
    get { return _minute; }
    set
    {
    _minute = value;
    OnPropertyChanged();
    }
    }
    private string[] _format;
    public string[] Format
    {
    get { return _format; }
    set
    {
    _format = value;
    OnPropertyChanged();
    }
    }
    private string _iCurrDate;
    public string CurrDate
    {
    get { return _iCurrDate; }
    set
    {
    _iCurrDate = value;
    OnPropertyChanged();
    }
    }
    private string _iCurrHour;
    public string CurrHour
    {
    get { return _iCurrHour; }
    set
    {
    _iCurrHour = value;
    OnPropertyChanged();
    }
    }
    private string _iCurrMin;
    public string CurrMin
    {
    get { return _iCurrMin; }
    set
    {
    _iCurrMin = value;
    OnPropertyChanged();
    }
    }
    private string _iCurrZone;
    public string CurrZone
    {
    get { return _iCurrZone; }
    set
    {
    _iCurrZone = value;
    OnPropertyChanged();
    }
    }
    public NumbersPickerViewModel()
    {
    var dates = new List();
    DateTime startdate = DateTime.Now.AddDays(-3);
    DateTime enddate = DateTime.Now.AddDays(3);
    DateTime currentdate = DateTime.Now;
    List hours = new List();
    List minutes = new List();
    for (DateTime dt = startdate; dt <= enddate; dt = dt.AddDays(1))
    {
    if (dt.Date == currentdate.Date)
    {
    dates.Add("Today");
    CurrHour = currentdate.ToString("hh");
    CurrMin = currentdate.ToString("mm");
    //CurrHour = int.Parse(currentdate.ToString("hh"));
    //CurrMin = int.Parse(currentdate.ToString("mm"));
    }
    else
    { dates.Add(dt.ToString("ddd MMM dd")); }

            }
            // CurrDate = (dates.Count)/2;
            CurrDate = "Today";
            Format = new string[] { "AM", "PM" };
            var zone = currentdate.ToString("tt");
            CurrZone = (zone == "PM") ? "PM" :"AM";
            Month = dates.ToArray();
            for (int a = 0; a < 12; a ++)
            {
                hours.Add(a.ToString("00"));
            }
            Hour = hours.ToArray();
            for (int b = 0; b < 60; b++)
            {
                minutes.Add(b.ToString("00"));
            }
            Minute = minutes.ToArray();
        }
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }
    

    }

    PickerViewRenderer.cs for Android

    public class PickerViewRenderer : ViewRenderer<PickerView, NumberPicker>
    {
    protected override void OnElementChanged(ElementChangedEventArgs e)
    {
    base.OnElementChanged(e);

            if (Control == null)
            {
                SetNativeControl(new NumberPicker(Context));
            }
            else
            {
                Control.ValueChanged -= Control_ValueChanged;
            }
    
            if (e.NewElement != null)
            {
                Control.ValueChanged += Control_ValueChanged;
    
                UpdateItemsSource();
                UpdateSelectedIndex();
            }
        }
    
        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);
    
            if (e.PropertyName == PickerView.ItemsSourceProperty.PropertyName)
            {
                UpdateItemsSource();
            }
            else if (e.PropertyName == PickerView.SelectedIndexProperty.PropertyName)
            {
                UpdateSelectedIndex();
            }
        }
    
        private void UpdateItemsSource()
        {
            var arr = new List<string>();
            if (Element.ItemsSource != null)
            {
                foreach (var item in Element.ItemsSource)
                {
                    arr.Add(item.ToString());
                }
    
            }
    
            if (arr.Count > 0)
            {
                int newMax = arr.Count - 1;
                if (newMax < Control.Value)
                {
                    Element.SelectedIndex = newMax;
                }
    
                var extend = Control.MaxValue <= newMax;
    
                if (extend)
                {
                    Control.SetDisplayedValues(arr.ToArray());
                }
    
                Control.MaxValue = newMax;
                Control.MinValue = 0;
    
                if (!extend)
                {
                    Control.SetDisplayedValues(arr.ToArray());
                }
            }
        }
        private void UpdateSelectedIndex()
        {
            if (Element.SelectedIndex < Control.MinValue || Element.SelectedIndex >= Control.MaxValue)
            {
                return;
            }
    
            Control.Value = Element.SelectedIndex;
        }
        void Control_ValueChanged(object sender, NumberPicker.ValueChangeEventArgs e)
        {
            Element.SelectedIndex = e.NewVal;
        }
    }
    
Sign In or Register to comment.