Praetorian
Praetorian

Reputation: 109289

WP7 - ListBox Binding to nested ObservableCollection

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 TextBlocks. 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

Answers (4)

AnthonyWJones
AnthonyWJones

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

Derek Lakin
Derek Lakin

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

decyclone
decyclone

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

Austin Lamb
Austin Lamb

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

Related Questions