Reputation: 1097
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
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
Reputation: 37059
_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