Binding Issue on child view in Xamarin

Viewed 425

I have a shared view for Add and Detail page. For some reason in the detail page, the view model won't binding to this child view (page come up blank as in NO populated value from the api service). Any ideas?

Debug this and there was a data coming from web api for both CategoryList as well as _activity.

How to debug this binding process?

ActivityView.xaml

<ContentView xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="AthlosifyMobileApp.Views.ActivityView">
    <StackLayout Spacing="12">
        <Entry x:Name="txtName" Text="{Binding Name}" HeightRequest="40" BackgroundColor="White" Placeholder="Name" HorizontalOptions="FillAndExpand"/>
        <Entry  x:Name="txtNoOfMinutes" Keyboard="Numeric"  Text="{Binding NoOfMinutes}" BackgroundColor="White" Placeholder="NoOfMinutes" HorizontalOptions="FillAndExpand"/>
        <Entry x:Name="txtDescription" Text="{Binding Description}" HeightRequest="40" BackgroundColor="White" Placeholder="Description" HorizontalOptions="FillAndExpand"/>
        <Picker ItemsSource="{Binding CategoryList}" ItemDisplayBinding="{Binding Name}" SelectedItem="{Binding SelectedCategory}"></Picker>
    </StackLayout>
</ContentView>

ActivityView.xaml.cs

namespace AthlosifyMobileApp.Views
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class ActivityView : ContentView
    {
        public ActivityView ()
        {
            InitializeComponent ();
        }
    }
}

ActivityDetailViewModel.cs

namespace AthlosifyMobileApp.ViewModels
{
    public class ActivityDetailViewModel : ActivityBaseViewModel
    {
        public ICommand DeleteCommand { get; private set; }
        public ICommand UpdateCommand { get; private set; }

        public ActivityDetailViewModel(INavigation navigation, int selectedActivityId)
        {
            _navigation = navigation;
            _activityValidator = new ActivityValidator();
            _activity = new Activity();
            _activity.Id = selectedActivityId;
            _apiService = new ApiService();

            //DeleteCommand = new Command(async () => await HandleDeleteActivity());
            UpdateCommand = new Command(async () => await UpdateActivity());

            FetchActivityDetail();
            FetchCategories();
        }


        async void FetchActivityDetail()
        {
            _activity = await _apiService.GetActivity(_activity.Id);
        }

        async void FetchCategories()
        {
            CategoryResult categoryResult = await _apiService.GetCategories();
            CategoryList = categoryResult.Results;
        }


        async Task UpdateActivity()
        {
            _activity.OwnerId = Preferences.Get(Constant.Setting_UserId, "");
            _activity.CategoryId = SelectedCategory.Id;
            _activity.CategoryName = SelectedCategory.Name;


            var validationResults = _activityValidator.Validate(_activity);

            if (validationResults.IsValid)
            {
                bool isUserAccept = await Application.Current.MainPage.DisplayAlert("Contact Details", "Update Contact Details", "OK", "Cancel");
                if (isUserAccept)
                {
                    var response = await _apiService.UpdateActivity(_activity.Id,_activity);
                    if (!response)
                    {
                        await Application.Current.MainPage.DisplayAlert("Add Activity", "Error", "Alright");
                    }
                    else
                    {
                        await _navigation.PushAsync(new ActivityListPage());
                    }
                    await _navigation.PopAsync();
                }
            }
            else
            {
                await Application.Current.MainPage.DisplayAlert("Add Contact", validationResults.Errors[0].ErrorMessage, "Ok");
            }
        }

        public async Task HandleDeleteActivity(int id)
        {
            var alert = await Application.Current.MainPage.DisplayAlert("Warning", "Do you want to delete this item?", "Yes", "Cancel");
            if (alert)
            {
                var response = await _apiService.DeleteActivity(id);
                if (!response)
                {
                    await Application.Current.MainPage.DisplayAlert("Error", "Something wrong", "Alright");
                }
                else
                {
                    await _navigation.PushAsync(new ActivityListPage());
                }
            }
        }


    }
}

ActivityBaseViewModel.cs

namespace AthlosifyMobileApp.ViewModels
{
    public class ActivityBaseViewModel : INotifyPropertyChanged
    {
        public Activity _activity;

        public INavigation _navigation;
        public IValidator _activityValidator;
        public ApiService _apiService;

        public string Name
        {
            get
            {
                return _activity.Name;
            }
            set
            {
                _activity.Name = value;
                NotifyPropertyChanged("Name");
            }
        }

        public string Description
        {
            get { return _activity.Description; }
            set
            {
                _activity.Description = value;
                NotifyPropertyChanged("Description");
            }
        }

        public int NoOfMinutes
        {
            get { return _activity.NoOfMinutes; }
            set
            {
                _activity.NoOfMinutes = value;
                NotifyPropertyChanged("NoOfMinutes");
            }
        }

        public int CategoryId
        {
            get { return _activity.CategoryId; }
            set
            {
                _activity.CategoryId = value;
                NotifyPropertyChanged("CategoryId");
            }
        }

        public string CategoryName
        {
            get { return _activity.CategoryName; }
            set
            {
                _activity.CategoryName = value;
                NotifyPropertyChanged("CategoryName");
            }
        }

        //List<Activity> _activityList;
        InfiniteScrollCollection<Activity> _activityList;

        //public List<Activity> ActivityList
        public InfiniteScrollCollection<Activity> ActivityList
        {
            get => _activityList;
            set
            {
                _activityList = value;
                NotifyPropertyChanged("ActivityList");
            }
        }

        List<Category> _categoryList;

        public List<Category> CategoryList
        {
            get { return _categoryList; }
            set
            {
                _categoryList = value;
                NotifyPropertyChanged("CategoryList");
            }
        }

        public Category SelectedCategory
        {
            get
            {
                return _activity.SelectedCategory;
            }
            set
            {
                _activity.SelectedCategory = value;
                NotifyPropertyChanged("SelectedCategory");
            }
        }

        #region INotifyPropertyChanged       
        public event PropertyChangedEventHandler PropertyChanged;
        protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion
    }
}

ActivityDetailPage.xaml

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:AthlosifyMobileApp.Views"
             x:Class="AthlosifyMobileApp.Views.ActivityDetailPage"   
             Title="Detail Activity">
    <ContentPage.ToolbarItems>
        <ToolbarItem Command="">
            <ToolbarItem.IconImageSource>
                <FontImageSource  Glyph="&#xf1c0;" FontFamily="{StaticResource MaterialFontFamily}"/>
            </ToolbarItem.IconImageSource>
        </ToolbarItem>
        <ToolbarItem Command="{Binding UpdateCommand}">
            <ToolbarItem.IconImageSource>
                <FontImageSource Size="30" Glyph="&#xf193;" FontFamily="{StaticResource MaterialFontFamily}"/>
            </ToolbarItem.IconImageSource>
        </ToolbarItem>
    </ContentPage.ToolbarItems>
    <ContentPage.Content>
        <StackLayout  Padding="20" Spacing="12">
            <local:ActivityView />
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

ActivityDetailPage.xaml.cs

namespace AthlosifyMobileApp.Views
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class ActivityDetailPage : ContentPage
    {
        public ActivityDetailPage(int activityId)
        {
            InitializeComponent ();
            BindingContext = new ActivityDetailViewModel(Navigation, activityId);
        }
    }
}
3 Answers

'I'm not sure, but apparently the page is changing the binding context of your view.

Set a breakpoint inside your OnBindingContextChanged overridden method of your view and debug it. If it is confirmed, instantiate your view model from your page instead.

Based on the code you shared, I think it's likely you're not seeing any data on the activity detail page because you are fetching the data via an async method that is not awaited (FetchActivityData). Btw, async void method should be avoided if possible. There is no way to catch/handle exceptions thrown from them.

It looks like you are not awaiting because you are calling from the constructor of your viewmodel. What actually happens here is that the constructor returns immediately, while FetchActivityDetail() and FetchCategories() continue to run in the background. The page is displayed, but there is no data yet, so you don't see anything displayed. Then, when FetchActivityDetail completes, it sets _activity, but that's a field, so no PropertyChanged events are fired, so the page doesn't know it needs to update.

Here are a few suggestions:

  1. Do not perform long-running processes (like fetching data) in constructors. Passing in the existing data (like your activityid), is generally ok, although it can make using dependency injection a bit harder, if you eventually want to do that.

  2. When navigating to a viewmodel that requires fetching data, I generally recommend waiting until the view/vm are displayed before making the api call. To do this, I have all my views call an OnAppearing method in my viewmodels. This is easily moved into a BasePage and BaseViewModel that everything inherits from. Then, you can do things like setting an IsBusy status (to trigger some UI like a spinner), and populate your data. It could look something like this:

    public override async Task OnAppearing()
    {
        await base.OnAppearing();
    
        try
        {
            IsBusy = true;
            await FetchActivityDetail(); 
            await FetchCategories(); 
        }
        catch (Exception ex)
        {
            //handle/display error
        }
        finally 
        {
            IsBusy = false;
        }
    }
    

Another option would be to make this a method that's called prior to navigation, but that would require creating the viewmodel first, which is a different navigation pattern than you're using here. There are some good examples out there of viewmodel-first navigation, but I won't go into that here.

  1. Ensure that when data is fetched, it sets properties that cause PropertyChanged events to fire, so the view bindings update. You can't just set a backing field.

According to your description, you want to bind custom view in Xamarin.Forms, I suggest you don't assign binding internally inside custom controls, use this:

<ContentView
x:Class="demo2.simplecontrol.View1"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<ContentView.Content>
    <StackLayout>
        <Entry x:Name="label1" />
        <Entry x:Name="label2" />
    </StackLayout>
</ContentView.Content>

public partial class View1 : ContentView
{
    public View1 ()
    {
        InitializeComponent ();          
    }

    public static readonly BindableProperty Label1Property= BindableProperty.Create(
                    nameof(Label1),
        typeof(string),
        typeof(View1),
        "",
        BindingMode.TwoWay,
        propertyChanged: (bindable, oldValue, newValue) =>
        {
            if (newValue != null && bindable is View1 control)
            {
                var actualNewValue = (string)newValue;
                control.label1.Text = actualNewValue;
            }
        });

    public string Label1 { get; set; }

    public static readonly BindableProperty Label2Property = BindableProperty.Create(
                    nameof(Label2),
        typeof(string),
        typeof(View1),
        "",
        BindingMode.TwoWay,
        propertyChanged: (bindable, oldValue, newValue) =>
        {
            if (newValue != null && bindable is View1 control)
            {
                var actualNewValue = (string)newValue;
                control.label2.Text = actualNewValue;
            }
        });

    public string Label2 { get; set; }
}

Then you can use this custom view in ContentPage.

<ContentPage
x:Class="demo2.simplecontrol.Page10"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:demo2.simplecontrol">
<ContentPage.Content>
    <StackLayout>
        <Label
            HorizontalOptions="CenterAndExpand"
            Text="Welcome to Xamarin.Forms!"
            VerticalOptions="CenterAndExpand" />
        <local:View1 Label1="{Binding text1}" Label2="{Binding text2}" />
    </StackLayout>
</ContentPage.Content>

public partial class Page10 : ContentPage, INotifyPropertyChanged
{
    private string _text1;
    public string text1
    {
        get { return _text1; }
        set
        {
            _text1 = value;
            RaisePropertyChanged("text1");
        }
    }

    private string _text2;
    public string text2
    {
        get { return _text2; }
        set
        {
            _text2 = value;
            RaisePropertyChanged("text2");
        }
    }
    public Page10 ()
    {
        InitializeComponent ();
        text1 = "test1";
        text2 = "test2";
        this.BindingContext = this;
    }


    public event PropertyChangedEventHandler PropertyChanged;

    public void RaisePropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Finally, you get data coming from web api for CategoryList, so you can add break point to check if having data.

Related