rfreytag
rfreytag

Reputation: 1097

PropertyChanged event handler always null in Win Phone 8 Application

When I click the 'Add Some Thing' button on this simple Win Phone 8 application (built with VS 2012 Pro - its what I have), nothing happens. Why?

The repo of this example code is on bitbucket.org at: TestItemsControlInWinPhone8App

The MainPage.xaml contains:

<phone:PhoneApplicationPage
x:Class="TestItemsControlInWinPhone8App.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="Portrait" Orientation="Portrait"
shell:SystemTray.IsVisible="True">

<!--LayoutRoot is the root grid where all page content is placed-->
<Grid x:Name="LayoutRoot" Background="Transparent">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <!--TitlePanel contains the name of the application and page title-->
    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
        <TextBlock Text="MY APPLICATION" Style="{StaticResource PhoneTextNormalStyle}" Margin="12,0"/>
        <TextBlock Text="page name" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <Button x:Name="AddSomeThing"
                    Content="Add Some Thing"
                    Grid.Row="0"
                    Click="AddSomeThing_Click"/>
            <ItemsControl x:Name="LotsOfThingsItemsControl"
                          Grid.Row="1"
                          ItemsSource="{Binding Mode=OneWay}"
                          FontSize="{StaticResource PhoneFontSizeSmall}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Grid Height="Auto" Width="Auto"
                          VerticalAlignment="Center"
                          HorizontalAlignment="Center"
                          Background="Orange">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition Height="Auto"/>
                            </Grid.RowDefinitions>
                            <TextBlock Grid.Row="0" Text="{Binding Path=Id, Mode=OneWay}"/>
                            <TextBlock Grid.Row="1"
                                       Text="------------------------"/>
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </Grid>
    </StackPanel>

    <!--ContentPanel - place additional content here-->
    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">

    </Grid>
</Grid>

</phone:PhoneApplicationPage>

Please note that ItemsControl ItemsSource="{Binding Path=Things} has also been tried as just plain ItemsControl ItemsSource="{Binding}".

Both have the same result; the first five Thing objects display and clicking the "Add Some Thing" button adds another Thing to LotsOfThings.

The contents of the DataTemplate being a TextBlock do, in fact, display the first 5 Thing objects.

But clicking the button does NOT update the display, which persists in displaying only the original 5 Thing objects.

The code behind (MainPage.xaml.cs) reads:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using     TestItemsControlInWinPhone8App.Resources;

namespace TestItemsControlInWinPhone8App
{
public partial class MainPage : PhoneApplicationPage
{
    // Constructor
    public MainPage()
    {
        InitializeComponent();

        this.DataContext = new LotsOfThings(5);
    }

    private void AddSomeThing_Click(object sender, RoutedEventArgs e)
    {
        LotsOfThings lot = this.DataContext as LotsOfThings;

        lot.Add(new Thing());
    }
}
}

Note that in the Page constructor this.DataContext = new LotsOfThings(5); works and when the Page first displays, 5 Thing objects are displayed.

To be clear, what doesn't work is that a latter call of the AddSomeThing_click() button handler will add another Thing to LotsOfThings but only the original 5 Thing objects display; nothing more, even if there are many more Thing objects present on LotsOfThings according to the debugger.

What I notice using the debugger is that whenever OnPropertyChanged(...) is called handler is null. That is clearly important but I have no idea why that is happening having at this point followed all the remedial help I can find searching the web.

Why?

Contents of Thing.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TestItemsControlInWinPhone8App
{
public class Thing : INotifyPropertyChanged
{
    private string _Id = Guid.NewGuid().ToString();
    public string Id
    {
        get
        {
            return _Id;
        }
        set { }
    }

    #region Constructor
    public Thing()
    {
        this.OnPropertyChanged( "Id");
    }
    #endregion

    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string pPropertyName)
    {
        System.ComponentModel.PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(pPropertyName));
        }
    }
    #endregion
}
}

Contents of LotsOfThings.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TestItemsControlInWinPhone8App
{
class LotsOfThings : INotifyPropertyChanged, IList<Thing>
{
    private List<Thing> _things = new List<Thing>();
    public List<Thing> Things
    {
        get {
            return _things;
        }
        set { }
    }

    public LotsOfThings( int pNumberOfThings)
    {
        for( int x = 0; x < pNumberOfThings; x++){
            this.Add( new Thing());
        }
        OnPropertyChanged("Things");
    }

    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string pName)
    {
        System.ComponentModel.PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(pName));
        }
    }
    #endregion

    #region IList<T> methods
    public int IndexOf(Thing item)
    {
        return _things.IndexOf(item);
    }

    public void Insert(int index, Thing item)
    {
        _things.Insert(index, item);
    }

    public void RemoveAt(int index)
    {
        _things.RemoveAt(index);
    }

    public Thing this[int index]
    {
        get
        {
            return _things[index];
        }
        set
        {
            _things[index] = value;
        }
    }

    public void Add(Thing item)
    {
        _things.Add(item);

        OnPropertyChanged("Things");
    }

    public void Clear()
    {
        _things.Clear();
    }

    public bool Contains(Thing item)
    {
        return _things.Contains(item);
    }

    public void CopyTo(Thing[] array, int arrayIndex)
    {
        _things.CopyTo(array, arrayIndex);
    }

    public int Count
    {
        get { return _things.Count; }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }

    public bool Remove(Thing item)
    {
        return _things.Remove(item);
    }

    public IEnumerator<Thing> GetEnumerator()
    {
        return _things.GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return _things.GetEnumerator();
    }
    #endregion
}
}

If you need to just download the application or look at it with a better interface you can find it here: TestItemsControlInWinPhone8App

Thank you.

PS. I have read and, I think, followed all the advice I can find on Stackoverflow and elsewhere on the net about null handlers passed into OnPropertyChanged() methods and usage of ItemsControl that I can find.

Upvotes: 0

Views: 601

Answers (2)

rfreytag
rfreytag

Reputation: 1097

Ed Plunkett put me on the right path but the answer was a little more complicated so I'm laying out what I have learned in this answer.

First, I could have used ObservableCollection<T> - Ed is right. But I would have lost some of the IList<T> features I wanted. Besides I was trying to follow the practice in XAML Deep Dive ... (min 40-49) and they did not use ObservableCollection<T>.

Turns out I had mistakenly used INotifyPropertyChanged instead of INotifyCollectionChanged. The second interface has a slightly more complicated handler documented in the answer to this Stackoverflow question about calling OnCollectionChanged.

My research before asking this question found a bunch of ways to also get a null event handler. One was to call the handler with a misspelled property name (e.g. OnPropertyChanged("thing") when you should use OnPropertyChanged("Thing") because that is what the property is actually called - assuming you are dealing with properties and not collections. Another way to get a null event handler was to not bind the right object to the right content or container control. Here, take a look at "stack overflow C# why is my handler null".

And to finally put a stake in the heart of this problem I did a little research in the direction of Ed's hint that I become more familiar with the difference between List<T>, ObservableCollection<T> and INotifyPropertyChanged and found that excellent page.

I hope that helps. And thanks Ed; all my up-votes to you.

P.S. I have updated the repository of my test code to have the fix and git tag-ged the original version as broken and the fixed version as fixed.

Upvotes: 1

_things needs to be ObservableCollection<Thing>. List<T> doesn't implement INotifyCollectionChanged, and hence doesn't raise notifications when its contents change. ObservableCollection<Thing> does, which will enable the UI to know when it needs to add items to the list.

The simple, easy, standard way of doing things is to expose an ObservableCollection as a property. If you replace the whole collection with a new one, raise PropertyChanged("Things"); when items are added/removed, the ObservableCollection will raise appropriate events without your needing to do anything. Experienced WPF folks reading your code will know what they're looking at.

To get it working the way you had in mind, you would have to call OnPropertyChanged("Things") in the methods that alter the Things collection; I haven't tested that, but I think it ought to work (the reason it might not is that the actual collection object returned by Things has not changed; the control might see that and choose not to update; but as I said I haven't tested that). Then you could bind Things to an ItemsSource on a control and maybe it ought to work. But then you could have other classes altering Things, because it's public. It would be a mess to try to chase down all the loose ends. Much easier to use ObservableCollection.

If you want to bind LotsOfThings itself to ItemsSource, you'll have to implement INotifyCollectionChanged on LotsOfThings, which gets to be a real hassle to rewrite all that by hand, and I'm not sure what it buys you. You could just make LotsOfThings a subclass of ObservableCollection<Thing> -- that starts you off with a complete and bulletproof INotifyCollectionChanged implementation, for free.

Upvotes: 3

Related Questions