Search code examples
xamarin.formsdata-bindingxamarin-community-toolkit.net-standard-2.1

Xamarin View freeze when raising OnPropertyChanged on value binded to Xamarin community toolkit BadgeView


I'm currently struggling with a weird behavior concerning Xamarin Community Toolkit BadgeView component.

The component is used in the TitleView of my page like this:

<TabbedPage>
<Shell.TitleView>
    <Grid ColumnDefinitions="6*,1*">
        <Image Source="logo" HorizontalOptions="Center" Margin="0,2,0,2"/>

        <StackLayout Grid.Column="1" Orientation="Horizontal">
            <Label x:Name="For testing only" Text="{Binding NotificationsNumber}" VerticalOptions="Center"/> 
            <StackLayout VerticalOptions="Center" HorizontalOptions="EndAndExpand">
                <StackLayout.GestureRecognizers>
                    <TapGestureRecognizer Command="{Binding OpenNotificationCommand}" NumberOfTapsRequired="1"/>
                </StackLayout.GestureRecognizers>
                <xct:BadgeView Text="{Binding NotificationsNumber}" BackgroundColor="#c1121f" TextColor="White" FontSize="Caption" AutoHide="True">
                    <Image>
                        <Image.Source>
                            <FontImageSource FontFamily="FASolid" Color="White" Size="Large" Glyph="{x:Static icons:FontAwesomeIcons.Bell}"/>
                        </Image.Source>
                    </Image>
                </xct:BadgeView>
            </StackLayout>
        </StackLayout>
    </Grid>
</Shell.TitleView>
    Page content
</TabbedPage

For testing i added the label above with x:Name="for testing only" with Text Bindable Property binded to my property and the value update well without any concern.

In my ViewModel the property NotificationsNumber is initialized in the method InitializeAsync called by the constructor of the viewModel:

public class HomeViewModel : INotifyPropertyChanged, ApiViewModelBase 
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private int _notificationsNumber = 0;

    public HomeViewModel(IApiClient client) : base(client)
    {
        OpenNotificationCommand = new Command(async () => await Shell.Current.GoToAsync($"{nameof(PlaceholderPage)}"));
        InitializeAsync();
    }

    public ICommand OpenNotificationCommand { get; }

    public int NotificationsNumber
    {
        get => _notificationsNumber;
        private set => SetProperty(ref _notificationsNumber, value);
    }

    private async void InitializeAsync()
    {
        await RunInSafeScope(async () =>
        {
            // API call made with an instance of custom Http client instance
            var notificationCountTask = HttpClient.GetWithRetryAsync<ValueResult<int>>(ApiRoutes.NOTIFICATION_COUNT);

            var htmlSource = new HtmlWebViewSource();
            await Task.WhenAll(notificationCountTask);

            // notificationCountTask.Result.Value return 2 and update NotificationsNumber Property 
            NotificationsNumber = notificationCountTask.Result.Value; 
        }, (ex) =>
        {
            if (ex is ApiRequestException exception && exception.StatusCode == System.Net.HttpStatusCode.Unauthorized)
                throw new Exception("Erreur", "Unauthorized");
            else
                throw new Exception("Erreur", "An internal error occured");
        });
    }

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

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        var changed = PropertyChanged;
        if (changed == null)
            return;

        changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected async Task RunInSafeScope(Func<Task> tryScope, Action<Exception> catchScope, Action? finallyScope = null)
    {
        try
        {
            await tryScope.Invoke();
        }
        catch (Exception ex)
        {
            catchScope.Invoke(ex);
        } 
        finally
        {
            finallyScope?.Invoke();
        }
    }
}

For the sack of clarity i simplified the ViewModel and displayed only methods or properties or instructions usefull for this context.

So What is happening here is when i call the InitializeAsync the api call is made successfully then i set the value of NotificationsNumber property. The SetProperty method is raised, backing field is updated then OnPropertyChanged is invoked then i go back in the getter returning the updated value for finally having no response after that the screen remain freezed like if it was a deadlock.

I precise in the InitializeAsync() method i instantiate other properties with exactly the same process and there is no problems at all, that's why i think the problem is coming from the BindableProperty of the BadgeView component making an infinite loop or something of this kind. I can't figure it out how to check if my assumptions are true, or test further.

Thanks in advance for your help!


Solution

  • Yes, it is the case as you said.

    And I have created a new issue about this problem, you can follow it up here: https://github.com/xamarin/XamarinCommunityToolkit/issues/1900.

    Thanks for your feedback and support for xamarin.

    Best Regards.