Reputation: 109289
I have an ObservableCollection
of objects as follows:
public class UserDataViewModel
{
private ObservableCollection<CategoryItem> _data =
new ObservableCollection<CategoryItem>();
public ObservableCollection<CategoryItem> Data
{
get { return _data; }
private set { }
}
// Other methods to set Data
}
The CategoryItem
class is defined as:
public class CategoryItem : INotifyPropertyChanged
{
private string _name = null;
private ObservableCollection<EntryItem> _entries =
new ObservableCollection<EntryItem>();
public string Name
{
get { return _name; }
set {
if( value != _name ) {
_name = value;
NotifyPropertyChanged( "Name" );
}
}
}
public ObservableCollection<EntryItem> Entries
{
get { return _entries; }
set {
if( value != _entries ) {
_entries = value;
NotifyPropertyChanged( "Entries" );
}
}
}
// INotifyPropertyChanged code follows
}
The EntryItem
class is defined as:
public class EntryItem : INotifyPropertyChanged
{
private string _name = null;
public string Name
{
get { return _name; }
set {
if( value != _name ) {
_name = value;
NotifyPropertyChanged( "Name" );
}
}
}
// INotifyPropertyChanged code follows
}
I'm trying to bind this to a ListBox
. Each ListBoxItem
consists of 2 TextBlock
s. I want the first TextBlock
to display the EntryItem.Name
property and the second to display the CategoryItem.Name
property. Here's what I tried in XAML (without success):
<ListBox x:Name="MyListBox"
Margin="0,0,-12,0"
ItemsSource="{Binding Data}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,17">
<!--This should display EntryItem.Name-->
<TextBlock Text="{Binding Entries.Name}"
TextWrapping="Wrap"
Margin="12,0,0,0"
Style="{StaticResource PhoneTextExtraLargeStyle}" />
<!--This should display CategoryItem.Name-->
<TextBlock Text="{Binding Name}"
TextWrapping="Wrap"
Margin="12,-6,0,0"
Style="{StaticResource PhoneTextSubtleStyle}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
In the code-behind for this page I'm setting:
DataContext = App.ViewModel; // ViewModel is of type UserDataViewModel
I keep getting the binding error:
System.Windows.Data Error: BindingExpression path error: 'Name' property not found on 'System.Collections.ObjectModel.ObservableCollection`1[NestedCollection.ViewModels.EntryItem]' 'System.Collections.ObjectModel.ObservableCollection`1[NestedCollection.ViewModels.EntryItem]' (HashCode=123081170). BindingExpression: Path='Entries.Name' DataItem='NestedCollection.ViewModels.CategoryItem' (HashCode=121425257); target element is 'System.Windows.Controls.TextBlock' (Name=''); target property is 'Text' (type 'System.String')..
NestedCollection
is the name of this project and all of the above classes are in the NestedCollection.ViewModels
namespace.
Only the contents of the second TextBlock
are being displayed. How do I fix this?
Thanks for your help, this has been driving me nuts for a few hours now!
EDIT:
Suppose the Data
collection has 2 entries, "Credit Cards" and "Email Accounts" (these are Name
property of each CategoryItem
object in the collection. Say the first CategoryItem
has the EntryItem
objects "Visa", "Mastercard" and "American Express", and the second CategoryItem
object has the EntryItem
objects "GMail" and "Hotmail", then I want the ListBox
to display:
Visa
Credit Cards
Mastercard
Credit Cards
American Express
Credit Cards
GMail
Email Accounts
Hotmail
Email Accounts
I realize that the Entries
property of Data
does not have a Name
property, each entry within it does. Is there anyway to index into the Entries
in the XAML binding?
Upvotes: 3
Views: 6808
Reputation: 189535
Assumption 1: UserDataViewModel truely is a ViewModel
The term "ViewModel" on the end of class name it implies that the purpose of that class is to support a specific view. You would not expect such a view model to make it difficult for the view to which it is attached to do its job.
I would therefore suggest your "ViewModel" is messed up and needs redevelopment. Start with:-
public class EntryItem
{
public string Name {get; set;}
public CategoryItem Category {get; set;}
}
Your CategoryItem
doesn't need an entires collection property and your UserDataView
returns a flat collection of all EntryItem
objects. Binding is easy.
<TextBlock Text="{Binding Name}"
TextWrapping="Wrap"
Margin="12,0,0,0"
Style="{StaticResource PhoneTextExtraLargeStyle}" />
<TextBlock Text="{Binding Category.Name}"
TextWrapping="Wrap"
Margin="12,-6,0,0"
Style="{StaticResource PhoneTextSubtleStyle}" />
Assumption 2: UserDataViewModel isn't actually a ViewModel
Its possible that what you've called a view model is in fact just a model of data arranged in manner matching its storage or general usage. This would account for why it doesn't match the actual view's requirements.
I would introduce another assumption which on WP7 is likely be true (probably would be elsewhere). During display of the view the contents of the collections are not modified nor are the names of the items. Hence the Observable nature of these objects (whilst possibly being useful elsewhere) are not necessary for the view to work.
If these assumptions are true then a value converter and an additional class can be used to present the items in more acceptable way:-
public class EntryHolder
{
public EntryItem Entry {get; set;}
public CategoryItem Category {get; set; }
}
public class CategoryToEntryItemExConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
UserDataViewModel model = value as UserDataViewModel;
if (model != null)
{
return model.Data.SelectMany(c => c.Entries
.Select(e => new EntryHolder() { Category = c, Entry = e})
);
}
else
{
return null;
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Now you would adjust your Xaml:-
<Grid x:Name="LayoutRoot">
<Grid.Resources>
<local:CategoryToEntryItemExConverter x:Key="ItemsConv" />
</Grid.Resources>
</Grid>
...
<ListBox x:Name="MyListBox"
Margin="0,0,-12,0"
ItemsSource="{Binding Converter={StaticResource ItemsConv}}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,17">
<!--This should display EntryItem.Name-->
<TextBlock Text="{Binding Entry.Name}"
TextWrapping="Wrap"
Margin="12,0,0,0"
Style="{StaticResource PhoneTextExtraLargeStyle}" />
<!--This should display CategoryItem.Name-->
<TextBlock Text="{Binding Category.Name}"
TextWrapping="Wrap"
Margin="12,-6,0,0"
Style="{StaticResource PhoneTextSubtleStyle}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Upvotes: 2
Reputation: 16319
It looks to me like your trying to display grouped data in a single control from a single (nested) data source, in which case you should consider using the LongListSelector control from the Silverlight Toolkit for WP7. WindowsPhoneGeek has a good blog post about how to use it in a similar situation to yours.
Alternatively, you would need to use nested items controls. If you don't need the concept of selection, then just set the item template for the ListBox to be an ItemsControl with ItemsSource="{Binding Entries}". For the ItemsControl, the DataContext will be an individual CategoryItem, so you can add a TextBlock header that binds to the Name property if necessary. This is basically what the LongListSelector is doing, but offers greater flexibility.
If you need the concept of selection for the entries, then I suspect you don't need it at the CategoryItem level, so make the root and ItemsControl and the ItemTemplate a ListBox. This way round you'll need to be careful with scrolling, which the ListBox provides for itself, so you may end up with a confusing user experience, hence my original suggestion of trying the LongListSelector.
Upvotes: 2
Reputation: 30840
You are trying to bind a ObservableCollection<T>
to a TextBox
. Think about it.
ObservableCollection<EntryItem>
does not have a property named Name
. EntryItem
class does.
I suggest you use an ItemsControl
instead or use a Converter
to convert EntryItem
names into a comma separated string.
After looking at your edit:
Try following code:
<ListBox x:Name="MyListBox"
Margin="0,0,-12,0"
ItemsSource="{Binding Data}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Name="RootGrid">
<ItemsControl ItemsSource="{Binding Entries}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,17">
<!--This should display EntryItem.Name-->
<TextBlock Text="{Binding Name}"
TextWrapping="Wrap"
Margin="12,0,0,0"
Style="{StaticResource PhoneTextExtraLargeStyle}" />
<!--This should display CategoryItem.Name-->
<TextBlock Text="{Binding ElementName=RootGrid, Path=DataContext.Name}"
TextWrapping="Wrap"
Margin="12,-6,0,0"
Style="{StaticResource PhoneTextSubtleStyle}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
--EDIT--
Looks like it's a known problem in Silverlight 3
.
http://forums.silverlight.net/forums/p/108804/280986.aspx
To workaround that, either put a reference of CategoryItem
in EntryItem
named Parent
or something similar to access it.
public class EntryItem : INotifyPropertyChanged
{
public CategoryItem Parent { get; set; }
....
}
Or as discussed in the link above, put your DataTemplate
into a UserControl
for it to work.
Upvotes: 3
Reputation: 3116
Your question doesn't really make sense - which item in the Entries list do you want the name of? The first item? There is no name of the entire collection, only on each element within that collection.
If you want the first item you could have your TextBox bind to Entries[0].Name - I think that works for Silverlight on Windows Phone (I can't remember if indexers are supported).
If indexers aren't supported, then you'd need to write an IValueConverter that can convert from ObservableCollection<EntityItem>
to string and use that as the Converter in the Binding.
Upvotes: 1