Search code examples
wpfdatatemplate

Displaying Items from Two Collections in One ListBox using WPF Datatemplates


I'm trying to create a Listbox that displays text and an image as one item. The listbox items have the text and the ID of the image. The image ID can be used in a LINQ expression to get the container of the image URI.

basically, I have two lists. Each item in the first list is a key to retrieve a specific item in the second list. How can I get both pieces of data to display in one listbox?

EDIT FOR CLARITY:

the first list is a collection of message objects - ObservableCollection:

class Message
{
    public String PosterID { get; set; }
    public String Text { get; set; } //this isn't important, it is the message body
}

The second list is a collection of user profiles - ObservableCollection:

class User
{
    public String UserID { get; set; }
    public String ProfilePictureURI { get; set; }
}

To get the profile data of the poster of a message, you must take 'Message.PosterID' and use that as a 'key' to match 'User.UserID'.

The listbox in question is currently databound to ViewModel.Messages. In the listbox's data template, I print out 'Message.Text' successfuly, BUT I still need to retrieve 'User.ProfilePictureURI.'

I am being recommended to use a ValueConverter to convert 'Message.PosterID' to 'User.UserID.' But to make this conversion, I need to pass ViewModel.Users into the ValueConverter. I currently don't know how to do this.


Solution

  • I think you have two ways:

    A. Use a binding converter to convert the image id by looking up your image id from the second list, then bind your ListBox to the list.

    B. Create a wrapper class that acts as a ViewModel for your data, and bind the ListBox to the ViewModel object.

    EDIT
    About using the value converter:
    If you could make the Users list static or use some Dependency Injection mechanism to obtain the Users list, then your value converter could easily do the conversion.
    The only other way is somehow pass the Users list from ViewModel (DataContext) to the binding converter.
    At first I thought that you can set the Users to a property of the converter, like this:

    <ListBox ItemsSource="{Binding Path=Messages}">
        <ListBox.Resources>
            <c:PosterIDConverter x:Key="pidConverter" Users="..."/>
        </ListBox.Resources>
    ...
    

    Or pass it as ConverterParameter to the binding:

    <TextBlock Text="{Binding Path=Text, Converter={StaticResource pidConverter,ResourceKey}, ConverterParameter=...}"/>
    

    But how should I get the Users property from the DataContext? In any of the two above options, you should be able to bind the property to Users, like this (incorrect):

    <c:PosterIDConverter x:Key="pidConverter" Users="{Binding Path=Users"/>
    

    But PosterIDConverter is not DependencyObject and does not support data binding.

    Working Solution:

    Here is what I finally came to.

    Your converter class:

    public class PosterIDConverter : IMultiValueConverter
    {
        public object Convert(object[] value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            string userId = (string)value[0];
            IEnumerable<User> users= (IEnumerable<User>)value[1];
    
            var user = users.FirstOrDefault(u => u.UserID == userId);
            if (user != null)
                return user.ProfilePictureURI;
            return null;
        }
    
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    

    Your xaml file (just a sample):

    <ListBox x:Name="lst" DataContext="{StaticResource vm}" ItemsSource="{Binding Path=Messages}">
        <ListBox.Resources>
            <c:PosterIDConverter x:Key="pidConverter"/>
        </ListBox.Resources>
        <ListBox.ItemTemplate>
            <DataTemplate DataType="{x:Type c:Message}">
                <Border BorderBrush="Blue" BorderThickness="1">
                    <StackPanel Orientation="Vertical">
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="Message: "/>
                            <TextBlock Text="{Binding Path=Text}"/>
                        </StackPanel>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="URI: "/>
                            <TextBlock>
                                <TextBlock.Text>
                                    <MultiBinding Converter="{StaticResource pidConverter}">
                                        <Binding Path="PosterID"/>
                                        <Binding ElementName="lst" Path="DataContext.Users"/>
                                    </MultiBinding>
                                </TextBlock.Text>
                            </TextBlock>
                        </StackPanel>
                    </StackPanel>
                </Border>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    

    In this sample I bound the Text property of a TextBlock to the PosterID and Users property at the same time and used the value converter (IMultiValueConverter) to convert them to image URI.