Forum Xamarin.Forms

ContentView bindings not working

zeb94zeb94 ITMember ✭✭

Hi everyone, I'm trying to create a custom control that allows me to show an horizontal list after having loaded the data from an online resource.
I'm still new to creating custom controls with property binding and this is quite frustrating to be honest :# .

In order to do what I want I derived a ContentView and this are its XAML an code-behind:

<?xml version="1.0" encoding="UTF-8"?>
<ContentView
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:ff="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
    x:Class="AppName.Controls.BrandsCarousel"
    x:Name="brands_carousel"
    HeightRequest="120">
    <ContentView.Content>
        <StackLayout BindingContext="{x:Reference brands_carousel}">
            <ActivityIndicator IsVisible="{Binding IsLoading}" IsRunning="{Binding IsLoading}" />
            <CollectionView IsVisible="{Binding IsNotLoading}" ItemsSource="{Binding Brands}">
                <CollectionView.ItemsLayout>
                    <GridItemsLayout Orientation="Horizontal" Span="1" HorizontalItemSpacing="8" />
                </CollectionView.ItemsLayout>
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <ff:CachedImage LoadingPlaceholder="tab_home" Source="{Binding Image}" WidthRequest="150" Aspect="AspectFit" />
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
        </StackLayout>
    </ContentView.Content>
</ContentView>


using System.Collections;
using System.Runtime.CompilerServices;
using Xamarin.Forms;

namespace AppName.Controls
{
    public partial class BrandsCarousel : ContentView
    {
        public static readonly BindableProperty BrandsProperty =
            BindableProperty.Create(nameof(Brands), typeof(IEnumerable), typeof(BrandsCarousel), null,
                                    propertyChanged: OnBrandsChanged, defaultBindingMode: BindingMode.OneWay);

        public static readonly BindableProperty IsLoadingProperty =
            BindableProperty.Create(nameof(IsLoading), typeof(bool), typeof(BrandsCarousel), null,
                                    propertyChanged: OnIsLoadingChanged, defaultBindingMode: BindingMode.OneWay);

        private static void OnBrandsChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var control = (BrandsCarousel)bindable;
            control.Brands = newValue as IEnumerable;
        }

        private static void OnIsLoadingChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var control = (BrandsCarousel)bindable;
            control.IsLoading = (bool)newValue;
        }

        public IEnumerable Brands
        {
            get => (IEnumerable)GetValue(BrandsProperty);
            set => SetValue(BrandsProperty, value);
        }

        public bool IsLoading
        {
            get => (bool)GetValue(IsLoadingProperty);
            set
            {
                SetValue(IsLoadingProperty, value);
                OnPropertyChanged(nameof(IsNotLoading));
            }
        }

        public bool IsNotLoading
        {
            get => !(bool)GetValue(IsLoadingProperty);
        }

        public BrandsCarousel()
        {
            InitializeComponent();
        }
    }
}

This is the view in which I'm including the custom control:

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:ff="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
    xmlns:controls="clr-namespace:AppName.Controls"
    xmlns:vm="clr-namespace:AppName.ViewModels"
    x:Class="AppName.Views.HomePage"
    Title="Home">
    <ContentPage.BindingContext>
        <vm:HomeViewModel />
    </ContentPage.BindingContext>
    <ContentPage.Content>
        <StackLayout Spacing="0">
            <Label Text="Our brands" StyleClass="SectionTitleLabel" />
            <controls:BrandsCarousel Brands="{Binding Brands}" IsLoading="{Binding LoadingBrands}" />
        </StackLayout>
    </ContentPage.Content>
</ContentPage>


using Xamarin.Forms;

namespace AppName.Views
{
    public partial class HomePage : ContentPage
    {
        public HomePage()
        {
            InitializeComponent();
        }
    }
}

And this is the view model associated with the view, responsible of loading the data from the online resource

using System.Collections.ObjectModel;
using System.Threading.Tasks;
using AppName.Models;
using AppName.Services;
using AsyncAwaitBestPractices.MVVM;

namespace AppName.ViewModels
{
    public class HomeViewModel : INotifyPropertyChanged
    {
        private ObservableCollection<Brand> _brands;
        public ObservableCollection<Brand> Brands
        {
            get => _brands;
            set => SetProperty(ref _brands, value);
        }

        public IAsyncCommand LoadBrandsCommand { get; set; }

        private bool _loadingBrands;
        public bool LoadingBrands
        {
            get => _loadingBrands;
            set => SetProperty(ref _loadingBrands, value);
        }

        public HomeViewModel()
        {
            LoadBrandsCommand = new AsyncCommand(LoadBrands, _ => !LoadingBrands);
            LoadBrandsCommand.Execute(null);
        }

        private async Task LoadBrands()
        {
            LoadingBrands = true;

            var brands = await BrandsDataStore.GetBrandsAsync();
            Brands = new ObservableCollection<Brand>(brands);

            LoadingBrands = false;
        }

        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
    }
}

The problem I'm facing is that the BrandsCarouse.Brands property gets correctly notified of the change happening inside of HomeViewModel.LoadBrands but the BrandsCarousel.IsLoading does not. What could be the reason?

Answers

  • ColeXColeX Member, Xamarin Team Xamurai
    edited December 2019

    We should set a default value while using Value types (int , float , bool ...) , do not set it as null at first.

    Correct way

       public static readonly BindableProperty IsLoadingProperty =
            BindableProperty.Create(nameof(IsLoading), typeof(bool), typeof(View1), true,
                                    propertyChanged: OnIsLoadingChanged, defaultBindingMode: BindingMode.OneWay);
    
  • zeb94zeb94 ITMember ✭✭

    @ColeX I've tried setting the default value but the behavior didn't change

  • ColeXColeX Member, Xamarin Team Xamurai

    What's your Xamarin.Forms version ?

    What platform did you test on ?

    Check my sample below

  • zeb94zeb94 ITMember ✭✭

    I tested it on both Android and iOS using the latest Xamarin.Forms version (4.4).
    I also tested your example and at first it looked like it was actually working, but in fact it's not. The reason your example seems to work is that you have changed the loading phase making it synchronous and the property IsLoading gets notified of a change when the binding context changes, which happens after the loading phase since it is synchronous.

    Changing the LoadBrands method in your example like as follows results in the exact same behavior I have.

    private void LoadBrands()
    {
        Device.BeginInvokeOnMainThread(async () =>
        {
            // Add delay so I'm sure the binding context changed event has been already fired
            await Task.Delay(5000);
    
            // This doesn't trigger View1.OnIsLoadingChanged
            LoadingBrands = true;
    
            Task.Delay(3000);
    
            // This doesn't trigger View1.OnIsLoadingChanged neither
            LoadingBrands = false;
        });
    }
    
  • ColeXColeX Member, Xamarin Team Xamurai
           // This doesn't trigger  View1.OnIsLoadingChanged because value does not change . 
        LoadingBrands = true;
    
        Task.Delay(3000);
    
        // This would trigger View1.OnIsLoadingChanged .
        LoadingBrands = false;
    

    I test on my side , it works fine .

  • zeb94zeb94 ITMember ✭✭
    edited December 2019

    Ok I tested again and it works. The reason it wasn't working is because I had changed the BindableProperty initialization setting true as default value on the Create method.

    What I see now is that only the first change will trigger OnIsLoadingChanged.
    This is an example of what I mean (assuming true as default value as in your example):

    private async Task LoadBrands()
    {
        // Won't trigger, the value is the same as the default
        LoadingBrands = true;
    
        await Task.Delay(3000);
    
        // Will trigger
        LoadingBrands = false;
    
        await Task.Delay(3000);
    
        // Won't trigger, no idea why since the value is changed...
        LoadingBrands = true;
    
        await Task.Delay(3000);
    
        // Again won't trigger...
        LoadingBrands = false;
    }
    

    I noticed that the same happens in reverse, meaning that settings false as default value the first LoadingBrands = true; will trigger the event and then no one will.

  • ColeXColeX Member, Xamarin Team Xamurai

    If you add Console.WriteLine("-------------"+ (bool)newValue); into method OnIsLoadingChanged , you can see it prints twice which means OnIsLoadingChanged method triggers twice .

Sign In or Register to comment.