Receiving a NullReferenceException when setting ListView.ItemsSource from an OnAppearing event

JoshuaRussoJoshuaRusso USMember ✭✭
edited January 2016 in Xamarin.Forms

I'm creating a Forms app for our server monitoring system and want the dashboard page of alerts to refresh every time the user navigates to the page. I have the ListView.ItemSource set from a call in the OnAppearing event and everything works fine the first time the page is displayed. When I navigate away from the page and then back, I receive a NullReferenceException from the setting of the ItemSource. The dataset looks good, in fact it rarely changes in these instances because the alerts don't tend to change that quickly.

The page is using XAML to manage the display. This is the class that backs the XAML:

namespace MyApp.Pages
{
    public partial class DashboardPage : ContentPage
    {
        public DashboardPage ()
        {
            InitializeComponent ();
            DashboardList.ItemTapped += DashboardList_ItemTapped;
        }

        async void DashboardList_ItemTapped (object sender, ItemTappedEventArgs e)
        {
            var masterPage = (MasterPage)Xamarin.Forms.Application.Current.MainPage;
            var selectedItem = (DashboardObject)e.Item;
            masterPage.ShowObject (selectedItem.ObjectId);
        }

        public async Task RefreshNotifications()
        {
            DashboardList.ItemsSource = await DashboardService.GetDashboardItems();
        }

        protected async override void OnAppearing ()
        {
            base.OnAppearing ();
            await RefreshNotifications ();
        }
    }
}

and this is the XAML:

<?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:myApp="clr-namespace:MyApp;assembly=MyApp"
             xmlns:controls="clr-namespace:MyApp.Controls;assembly=MyApp"
             x:Class="MyApp.Pages.DashboardPage"
             BackgroundColor="{x:Static myApp:InnovativeColorHelper.GreyLight}">
    <ContentPage.Content>

        <ListView x:Name="DashboardList"            
            SeparatorVisibility="None"
            HasUnevenRows="true">

            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <ContentView
                            Padding="5,5,5,5"
                            HorizontalOptions="FillAndExpand"
                            VerticalOptions="StartAndExpand"
                            BackgroundColor="{x:Static myApp:InnovativeColorHelper.GreyLight}">
                            <Frame
                                HasShadow="true"
                                BackgroundColor="White">
                                <StackLayout
                                    VerticalOptions="StartAndExpand">
                                    <Label Text="{Binding OrganizationName}" 
                                           TextColor="Black"
                                           FontSize="Medium" />
                                    <Label Text="{Binding UnitName}" 
                                           TextColor="Black"
                                           FontSize="Medium" />
                                    <Label Text="{Binding Name}" 
                                           TextColor="Black"
                                           FontSize="Medium" />
                                    <Label Text="Item is critical" 
                                           IsVisible="{Binding IsCritical}"
                                           TextColor="Red"
                                           FontSize="Medium" />

                                    <controls:RepeaterView ItemsSource="{Binding Alerts}">
                                        <controls:RepeaterView.ItemTemplate>
                                            <DataTemplate>
                                                <ContentView>
                                                    <Frame HasShadow="true"
                                                           BackgroundColor="{Binding AgeColor}">
                                                        <StackLayout>
                                                            <Label Text="{Binding Text}"
                                                                   FontAttributes="Bold"
                                                                   FontSize="Medium" />
                                                            <Label Text="{Binding Started}" />
                                                        </StackLayout>
                                                    </Frame>
                                                </ContentView>
                                            </DataTemplate>
                                        </controls:RepeaterView.ItemTemplate>
                                    </controls:RepeaterView>
                                </StackLayout>
                            </Frame>
                        </ContentView>
                    </ViewCell>
                </DataTemplate> 
            </ListView.ItemTemplate>

        </ListView>
    </ContentPage.Content>
</ContentPage>

Here is the RepeaterView that I found a definition for here on the forums:

namespace MyApp.Controls
{
    using System;
    using System.Collections;
    using System.Collections.Specialized;
    using Xamarin.Forms;

    public delegate void RepeaterViewItemAddedEventHandler(object sender, RepeaterViewItemAddedEventArgs args);

    // in lieu of an actual Xamarin Forms ItemsControl, this is a heavily modified version of code from https://forums.xamarin.com/discussion/21635/xforms-needs-an-itemscontrol
    public class RepeaterView : StackLayout
    {
        public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create<RepeaterView, IEnumerable>(
            p => p.ItemsSource,
            null,
            BindingMode.OneWay,
            propertyChanged: ItemsChanged);

        public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create<RepeaterView, DataTemplate>(
            p => p.ItemTemplate,
            default(DataTemplate));

        public event RepeaterViewItemAddedEventHandler ItemCreated;

        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        public DataTemplate ItemTemplate
        {
            get { 
                return (DataTemplate)GetValue(ItemTemplateProperty); 
            }
            set { SetValue(ItemTemplateProperty, value); }
        }

        private static void ItemsChanged(BindableObject bindable, IEnumerable oldValue, IEnumerable newValue)
        {
            var control = (RepeaterView)bindable;
            var oldObservableCollection = oldValue as INotifyCollectionChanged;

            if (oldObservableCollection != null)
            {
                oldObservableCollection.CollectionChanged -= control.OnItemsSourceCollectionChanged;
            }

            var newObservableCollection = newValue as INotifyCollectionChanged;

            if (newObservableCollection != null)
            {
                newObservableCollection.CollectionChanged += control.OnItemsSourceCollectionChanged;
            }

            control.Children.Clear();

            if (newValue != null)
            {
                foreach (var item in newValue)
                {
                    var view = control.CreateChildViewFor(item);
                    control.Children.Add(view);
                    control.OnItemCreated(view);
                }
            }

            control.UpdateChildrenLayout();
            control.InvalidateLayout();
        }

        protected virtual void OnItemCreated(View view) =>
        this.ItemCreated?.Invoke(this, new RepeaterViewItemAddedEventArgs(view, view.BindingContext));

        private void OnItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            var invalidate = false;

            if (e.OldItems != null)
            {
                this.Children.RemoveAt(e.OldStartingIndex);
                invalidate = true;
            }

            if (e.NewItems != null)
            {
                for (var i = 0; i < e.NewItems.Count; ++i)
                {
                    var item = e.NewItems[i];
                    var view = this.CreateChildViewFor(item);

                    this.Children.Insert(i + e.NewStartingIndex, view);
                    OnItemCreated(view);
                }

                invalidate = true;
            }

            if (invalidate)
            {
                this.UpdateChildrenLayout();
                this.InvalidateLayout();
            }
        }

        private View CreateChildViewFor(object item)
        {
            this.ItemTemplate.SetValue(BindableObject.BindingContextProperty, item);
            return (View)this.ItemTemplate.CreateContent();
        }
    }

    public class RepeaterViewItemAddedEventArgs : EventArgs
    {
        private readonly View view;
        private readonly object model;

        public RepeaterViewItemAddedEventArgs(View view, object model)
        {
            this.view = view;
            this.model = model;
        }

        public View View => this.view;

        public object Model => this.model;
    }
}

[Side note: It's silly that Xamarin doesn't have a basic repeater in the list of Forms controls, to compliment the ListView which is a scrollable full screen only element.]

Posts

  • JoshuaRussoJoshuaRusso USMember ✭✭
    edited February 2016

    So it turns out that I'm running into the same problem as I had previously where the NullReferenceException is thrown on clearing of the StackLayout.Children collection. I'm not alone in having this problem.

    I'm currently solving the problem by creating a new page instance any time I want to refresh the page. This seems like a real waste of resources. Has anyone found alternate solutions?

Sign In or Register to comment.