How do I bind multiple Labels to one object that was deserialized from an API Request?

BaileyBrewBaileyBrew Member ✭✭

Okay, so here's the deal. I'm new to Xamarin Forms - and I've only been writing code for about a year (Java for Android). I'm working on a project and using Xamarin Forms Shell, but I'm having trouble getting my MVVM working correctly.

When I load my DetailsPage I set up the Binding Context with my ViewModel:

public ItemDetailsPage()
{
    InitializeComponent();
    BindingContext = viewModel = new ItemDetailsViewModel();
}

In my ViewModel, I've created my ObservableCollection based on my POCO, and I have an async command requesting the data from the API. And through my Debug messages, I know I'm getting COMPLETE/OK responses when it loads (takes no more than 700ms). This is followed by the json deserialization like so:

string authCode = Application.Current.Properties["auth_token"] as string;
var restClient = new RestClient(base_api_uri).UseSerializer(() => new JsonNetSerializer());
restClient.AddDefaultHeader("Content-type", "application/json");
var restRequest = new RestRequest("/1/items/complete/{itemNumber}", Method.POST);
restRequest.AddHeader("Authorization", authCode);
restRequest.AddParameter("itemNumber", "1525252", ParameterType.UrlSegment);
restRequest.AddJsonBody(itemSearch);
var cancellationTokenSource = new CancellationTokenSource();
var itemResults = await restClient.ExecuteTaskAsync(restRequest, cancellationTokenSource.Token);
Debug.WriteLine("REST RESPONSE: " + itemResults.ResponseStatus + " " + itemResults.StatusCode);
ItemCompleteDetail allItemDeets =  JsonConvert.DeserializeObject<ItemCompleteDetail>(itemResults.Content);
return allItemDeets;

Yet in my view, even though I have the Label associated with the value in my object, nothing ever displays. I'm sure I'm just missing something obvious, since I'm still wet-behind-the-ears with Xamarin, but does anyone have any ideas to help me?

Best Answers

Answers

  • BaileyBrewBaileyBrew Member ✭✭

    @JohnHardman I think that's what I'm missing. I'm familiar with using ItemSource and the ObservableCollection for populating a ListView in the same fashion, but because I'm trying to update labels, I need to add a reference for each and every field, correct?

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭
    edited May 17

    @JohnHardman said:
    @BaileyBrew

    You'll need to post the ViewModel code that sets the public property that the View binds to.

    I'm betting that this
    ItemCompleteDetail allItemDeets = JsonConvert.DeserializeObject<ItemCompleteDetail>(itemResults.Content);
    So... Is allItemDeets an object at inherits from BindableObject so bindinging hears the change? (or implimnets InotifyPropertyChanged?)
    Like John said: Can we see that class?

    I need to add a reference for each and every field, correct?

    Yes, if I understand you right.

    <Label Text = "{binding allItemDeets.Name"/>
    <Label Text = "{binding allItemDeets.HairColor}"/>
    <!-- etc -->
    

    Tutorials if you want to work them or reference them
    http://redpillxamarin.com/2018/03/12/2018-101-vs2017-new-solution/

  • BaileyBrewBaileyBrew Member ✭✭

    @ClintStLaurent & @JohnHardman

    allItemDeets is the de-serialized return from the API Call. It's the JSON results in an object I can manipulate.

    public class ItemCompleteDetail
    {
        public string ItemNumber { get; set; }
        public string Description { get; set; }
        public string Pack { get; set; }
        public string Size { get; set; }
        public string RetailUnits { get; set; }
        public string VendorNumber { get; set; }
        public string VendorName { get; set; }
        ... ... //And so on - it's a big POCO
    

    As for my ViewModel, this is what I'm looking at currently:

    class ItemDetailsViewModel : BaseViewModel
    {
        public ObservableCollection<ItemCompleteDetail> Items { get; set; }
        public Command LoadItemsCommand { get; set; }
    
        public ItemDetailsViewModel()
        {
            Title = "Item Details";
            Items = new ObservableCollection<ItemCompleteDetail>();
            LoadItemsCommand = new Command(async () => await ExecuteLoadItemDetailsCommand());
        }
    
        async Task ExecuteLoadItemDetailsCommand()
        {
            if (IsBusy)
                return;
    
            IsBusy = true;
    
            try
            {
                Items.Clear();
                ItemCompleteDetail item = await getDetailsRequestAsync();
                Items.Add(item);
                Debug.WriteLine("Items Size Is: " + Items.Count);
    
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
            }
            finally
            {
                IsBusy = false;
            }
        }
    
        private async System.Threading.Tasks.Task<ItemCompleteDetail> getDetailsRequestAsync()
        {
            var itemSearch = new ItemSearchRequest
            {
                WarehouseNumber = "330",
                CustomerNumber = "999989",
                LocationNumber = "476"
            };
            string authCode = Application.Current.Properties["auth_token"] as string;
            var restClient = new RestClient(base_api_uri)
                 .UseSerializer(() => new JsonNetSerializer());
            restClient.AddDefaultHeader("Content-type", "application/json");
            var restRequest = new RestRequest("/1/items/complete/{itemNumber}", Method.POST);
            restRequest.AddHeader("Authorization", authCode);
            restRequest.AddParameter("itemNumber", "1525252", ParameterType.UrlSegment);
            restRequest.AddJsonBody(itemSearch);
            var cancellationTokenSource = new CancellationTokenSource();
            var itemResults = await restClient.ExecuteTaskAsync(restRequest, cancellationTokenSource.Token);
            Debug.WriteLine("REST RESPONSE: " + itemResults.ResponseStatus + " " + itemResults.StatusCode);
            ItemCompleteDetail allItemDeets = 
                JsonConvert.DeserializeObject<ItemCompleteDetail>(itemResults.Content);
    
            return allItemDeets;
        }
    
        internal class ItemSearchRequest
        {
            public string WarehouseNumber { get; set; }
            public string CustomerNumber { get; set; }
            public string LocationNumber { get; set; }
        }
    }
    

    Currently, this inherits from a BaseViewModel, but I'm thinking I can't do that anymore:

    public class BaseViewModel : INotifyPropertyChanged
    {
        bool isBusy = false;
        public bool IsBusy
        {
            get { return isBusy; }
            set { SetProperty(ref isBusy, value); }
        }
    
        string title = string.Empty;
        public string Title
        {
            get { return title; }
            set { SetProperty(ref title, value); }
        }
    
        protected bool SetProperty<T>(ref T backingStore, T value,
            [CallerMemberName]string propertyName = "",
            Action onChanged = null)
        {
            if (EqualityComparer<T>.Default.Equals(backingStore, value))
                return false;
    
            backingStore = value;
            onChanged?.Invoke();
            OnPropertyChanged(propertyName);
            return true;
        }
    
        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            var changed = PropertyChanged;
            if (changed == null)
                return;
    
            changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion
    }
    

    So what I'm wondering is if I need to create declarations like below in my ViewModel:

    string title = string.Empty;
    public string Title
    {
        get { return title; }
        set { SetProperty(ref title, value); }
    }
    

    Or is there some way to Bind to the ObservableCollection that is in the ViewModel?

  • BaileyBrewBaileyBrew Member ✭✭

    Also @ClintStLaurent in my XAML page, I do have the Binding declared like your example:

    <Label Text="{Binding ItemNumber}" Margin="10" FontSize="20" FontAttributes="Bold" TextColor="Black" />
    <Label Text="{Binding ItemDescription}" Margin="10" FontSize="20" FontAttributes="Bold" />
    
  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭

    You're the second person I've seen doing this ref stuff in properties.
    set { SetProperty(ref isBusy, value); }
    I don't what source people are starting to follow, but it seems like a pattern is emerging where this syntax is followed by "its not working" posts here on the forum.

    public string ItemNumber { get; set; }
    No notification of value change. Let me guess, you're going to say the base class comes from GrailUI and that's supposed to do it automatically by type. That's what I got from the other person using that weird ref syntax.

    <Label Text="{Binding ItemNumber}"

    ItemNumber of ... what? Do you have that as a property on the ViewModel, that then has to be set when the model is set?
    I'm starting to think you have a lot of objects along the path between getting the JSON and setting the object the XAML is binded to... Get the object... copy it here... then copy it here... then copy the individual values to the VIewModel... or soemthing like that. I'm just wondering if the main issue(s) might be
    1 - The base class
    2 - The syntax of the properties
    3 - A convoluted path from update to final binded object causing the update notification chain to break. IE: Just more steps/object than required.

  • BaileyBrewBaileyBrew Member ✭✭
    @ClintStLaurent

    Nearly all the code comes from the new Shell class in Xamarin.Forms. I'm not sure what GrailUI is, but if it's using Shell then that's probably why.
  • AdamMeaneyAdamMeaney USMember ✭✭✭✭✭
    protected bool SetProperty<T>(ref T backingStore, T value,
            [CallerMemberName]string propertyName = "",
            Action onChanged = null)
        {
            if (EqualityComparer<T>.Default.Equals(backingStore, value))
                return false;
    
            backingStore = value;
            onChanged?.Invoke();
            OnPropertyChanged(propertyName);
            return true;
        }
    
    

    is the SetProperty code in this case. Looks relatively the same as every other SetProperty I have seen. Shouldn't be an issue the way it is written.

    As an FYI, none of the properties in your View Model are using this SetProperty, so you will need:

    string title = string.Empty;
    public string Title
    {
        get { return title; }
        set { SetProperty(ref title, value); }
    }
    

    for all your properties.

    In this case, that shouldn't have caused you issues as you are actually bound and using INotifyCollectionChanged events from your ObservableCollection hopefully in your UI, and never replacing the collection, just changing the amount of objects in it.

    Could we get a little more Xaml? Maybe the ListView, the DataTemplate, and the part in your CS where you assign the binding context?

  • BaileyBrewBaileyBrew Member ✭✭
    @AdamMeaney - I dont have all the SetProperties. I missed that part.

    But I think the other thing I'm missing an understanding of what to do is in the XAML because I'm not populating this data to a ListView. I'm trying to attach values to a custom built layout [mostly Label but a couple Button and Entry fields].

    I get the DataTemplate piece for a ListView, but how would I achieve the same concept with views in Stack and Grid Layouts?
  • AdamMeaneyAdamMeaney USMember ✭✭✭✭✭
    Depends on what you want to do with it. You have a list of items, that should be displayed in a listview or other control that turns a list of objects into things.

    If this call actually only brings one object, Bind to properties of the ViewModel set by that object or to an object implementing INotifyPropertyChanged that you have an instance of in a property with setproperty setup on the VM.

    if you post the simplified object from the call and a rough layout, we can point you towards what piece of it you are missing.
  • BaileyBrewBaileyBrew Member ✭✭
    @AdamMeaney my POCO is big... which is part of my problem I'd imagine (17 unique strings, 4 nested POCOs, and 3 Lists)

    public class ItemCompleteDetail
    {
    public string ItemNumber { get; set; }
    public string Description { get; set; }
    public string Pack { get; set; }
    public string Size { get; set; }
    public string RetailUnits { get; set; }
    public string VendorNumber { get; set; }
    public string VendorName { get; set; }
    public string PackageUpc { get; set; }
    public string ProductSpecsLink { get; set; }
    public string ProductImageLink { get; set; }
    public string Weight { get; set; }
    public string Cube { get; set; }
    public string CaseItemNumber { get; set; }
    public string HalfCaseItemNumber { get; set; }
    public string ServiceItemNumber { get; set; }
    public string SubstituteItemNumber { get; set; }
    public string SubstituteItemDescription { get; set; }
    public CustomerInformation CustomerInformation { get; set; }
    public OtherAttributes OtherAttributes { get; set; }
    public OtherUpcCodes OtherUpcCodes { get; set; }
    public PricingInformation PricingInformation { get; set; }
    public List WarehouseAvailability { get; set; }
    public List ShipperComponents { get; set; }
    public List RelatedItems { get; set; }
    }


    The API is built in a way to get me all details from one call, thus the large POCO. The more I think through this, the more I'm assuming parsing async into a local DB would be a better route, then pulling the data from the DB instance instead of trying to update as I'm parsing the details from the API.

    In the attached image. The Blue is a straight string [some coming from the nested POCOs]. The Orange is bool value to determine visibility. The purple points to a ListView populated from one of the Lists.

    Hopefully, that gives you a better idea of what I've been asked to populate to.
  • BaileyBrewBaileyBrew Member ✭✭
    That's correct. My web call is returning all the data in my POCO. So ultimately I'm going to have a ton of extra code to write into my ViewModel [which is fine].

    Thanks for your help @AdamMeaney - I'm not at my computer either anymore.

    Maybe I can pick your brain on Monday if you're around when I'm back in the office too and in front of the code.
  • BaileyBrewBaileyBrew Member ✭✭

    @AdamMeaney - I'm hoping you're around - but I'm still having some issue getting my data to populate on-screen.

    I rewrote my ViewModel class and included the title format from above. I added 4, just to test the process (and I still don't see any details):

    string itemNumber = string.Empty;
        public string ItemNumber
        {
            get { return itemNumber; }
            set { SetProperty(ref itemNumber, value); }
        }
    
        string description = string.Empty;
        public string Description
        {
            get { return description; }
            set { SetProperty(ref description, value); }
        }
    
        string pack = string.Empty;
        public string Pack
        {
            get { return pack; }
            set { SetProperty(ref pack, value); }
        }
    
        string size = string.Empty;
        public string Size
        {
            get { return size; }
            set { SetProperty(ref size, value); }
        }
    

    I've got the BindingContext set on my xaml.cs with:

    BindingContext = viewModel = new ItemDetailsViewModel();
    

    And then in my xaml, I've got binding set up:

    <Label Text="{Binding ItemNumber}" ... />
    

    Yet, no matter what I tweak, I can't get data to show up, but based on my Debug logs I know that my data is being populated .... but it's like I can't get it to actually interpret the data from the ObservableCollection object that's being created from my async API call.

  • BaileyBrewBaileyBrew Member ✭✭

    So I played around some more and I'm finding that it's as if my OnPropertyChanged isn't being invoked. I can hard code a string in place of and that displays:

    string itemNumber = "1234567";
    public string ItemNumber 
    {
        get { return itemNumber; }
        set { SetProperty(ref itemNumber, value); }
    }
    

    It's like my ObservableCollection isn't passing the values to the ViewModel so the OnPropertyChanged isn't being triggered...

  • AdamMeaneyAdamMeaney USMember ✭✭✭✭✭
        public class ItemCompleteDetail
        {
            public string ItemNumber { get; set; }
        }
    
        public class ItemDetailsViewModel : BaseViewModel
        {
            // Option One:
            string itemNumber = string.Empty;
            public string ItemNumber
            {
                get { return itemNumber; }
                set { SetProperty(ref itemNumber, value); }
            }
    
            string description = string.Empty;
            public string Description
            {
                get { return description; }
                set { SetProperty(ref description, value); }
            }
    
            string pack = string.Empty;
            public string Pack
            {
                get { return pack; }
                set { SetProperty(ref pack, value); }
            }
    
            string size = string.Empty;
            public string Size
            {
                get { return size; }
                set { SetProperty(ref size, value); }
            }
    
    
            //Option 2
    
            private ItemCompleteDetail _detail = new ItemCompleteDetail();
    
            public string ItemNumber
            {
                get => _detail.ItemNumber;
                set
                {
                    _detail.ItemNumber = value;
                    NotifyPropertyChanged();
                }
            }
    
            public async void GetData()
            {
                // Do json call to get the single ItemDetail
                var detail = new ItemCompleteDetail()
                {
                    ItemNumber = "12345"
                };
    
    
                // Option 1
                ItemNumber = detail.ItemNumber;
    
                // Option 2
    
                _detail = detail;
                NotifyDetailChanged();
            }
    
            private void NotifyDetailChanged()
            {
                NotifyPropertyChanged(namoef(ItemNumber));
                // More notifies for the other props
            }
        }
    

    This is how I was imagining it. You don't have a list of data, you have a single object. Drop the observable collection. It doesn't help you in this situation.

    Obviously copy over the rest of what you want, or use the detail as the backing store and call OnPropertyChanged for each property when you swap it out.

Sign In or Register to comment.