Recently in our WPF project, we wanted to use ItemsControl to list out different types of objects. I couldn’t use the general List, ObservableCollection, or any other type of collection, for that matter, to set ItemSource of ItemsControl since they restrict only one type of object. Thus, I searched on the internet and found out about CompositeCollection. In this article, I am going to explain how to use CompositeCollection to list out different types of objects in WPF.
Requirements
To understand how to use CompositeCollection to list out a variety of objects in a single list, we need to go through what we are trying to achieve here.
The above WPF application is a conversation view for a test chat application. In this conversation view, we have ItemsControl to list out the messages. The only tricky part here is that we have different types of objects, i.e., InboundMessage and OutboundMessage. InboundMessage objects will appear on the left side and the OutboundMessage objects will appear on the right-hand side. Thus, we will use CompositeCollection along with DataTemplate of ItemsControls to achieve these results.
I am going to explain steps in terms of modules in this project.
Model
We are going to create two objects - InboundMessage and OutboundMessage. Each of these will have corresponding properties which we’ll bind to the main UI and in addition to that, it will have one static method that returns ObservableCollection of corresponding objects.
- public class InboundMessage
- {
- public int MessageId { set; get; }
- public string TextMessage { set; get; }
- public DateTime ReceivedTime { set; get; }
-
- public static ObservableCollection<InboundMessage> GetInboundMessages()
- {
-
- }
- }
Similarly, we will have OutboundMessage object.
Controls
We will create two UserControls to represent these model objects - InboundMessage and OutboundMessage -- and we'll bind the properties of these objects to corresponding controls.
- <Grid.RowDefinitions>
- <RowDefinition Height="3*"/>
- <RowDefinition Height="1*"/>
- </Grid.RowDefinitions>
- <Border Grid.Row="0" Background="White" CornerRadius="10" Padding="10" Margin="5,5,5,0" HorizontalAlignment="Left">
- <Grid>
- <Grid.RowDefinitions>
- <RowDefinition Height="3*"/>
- <RowDefinition Height="1*"/>
- </Grid.RowDefinitions>
-
- <TextBlock Name="tbTextMessage"
- TextWrapping="Wrap"
- FontSize="{Binding InboundMessageBubbleFontSize, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
- Text="{Binding TextMessage, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"/>
- <TextBlock Name="lblTimeStamp"
- Grid.Row="2"
- Padding="0"
- HorizontalAlignment="Right"
- Text="{Binding TimeStamp, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}">
- </TextBlock>
- </Grid>
- </Border>
- <Path Grid.Row="1" Stroke="Black" Fill="White" StrokeThickness="0" Data="M 12,0 L 18,10 L 25,0"/>
Now, for the main control which is going to be a list to display these objects, even though there are many list controls in WPF, we are going with simple ItemsControl because we don’t need selection events for our view. We will also use scroll viewer to provide scrolling functionality.
We’ll use two DataTemplates to represent Inbound and Outbound message controls. Now the problem is we can only set one data template in datatemplate property of ItemsControl. To overcome this, we will create multiple DataTemplate in the resource property of ItemsControl.
Since our collection is going to have multiple objects and ItemsControl is going to have multiple DataTemplates we need selector which will apply data template to object. For this, we set DataType property of DataTemplate, which sets that template to that object only.
E.g.
<DataTemplate DataType="{x:Type localmodel:InboundMessage}">
The above DataTemplate will be applied to only InboundMessages.
- <ScrollViewer VerticalScrollBarVisibility="Auto" Background="CadetBlue">
- <ItemsControl Name ="conversationList">
- <ItemsControl.Resources>
- <DataTemplate DataType="{x:Type localmodel:InboundMessage}">
- <localcontrols:InboundMessageBubble
- Margin="0,0,100,0"
- HorizontalAlignment="Left"
- InboundMessageBubbleFontSize="24"
- TextMessage="{Binding TextMessage}"
- TimeStamp="{Binding ReceivedTime, Converter={StaticResource objectToString}}"/>
- </DataTemplate>
- <DataTemplate DataType="{x:Type localmodel:OutboundMessage}">
- <localcontrols:OutboundMessageBubble
- Margin="100,0,0,0"
- HorizontalAlignment="Right"
- OutboundMessageBubbleFontSize="24"
- TextMessage="{Binding TextMessage}"
- TimeStamp="{Binding SentTime, Converter={StaticResource objectToString}}"/>
- </DataTemplate>
- </ItemsControl.Resources>
- </ItemsControl>
- </ScrollViewer>
The above code will apply different DataTemplates to Inbound and Outbound objects.
Helper
We can convert most of the objects to a string using ToString() method in code behind. But to do the same thing in XAML, we will have Helper class in our project which is a Converter class in WPF that converts any Object to String.
- public class StringFormatConverter : BaseConverter, IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
- {
- string format = parameter as string;
- if (!string.IsNullOrEmpty(format))
- {
- return string.Format(culture, format, value);
- }
- else
- {
- return value.ToString();
- }
- }
-
- public object ConvertBack(object value, Type targetType, object parameter,System.Globalization.CultureInfo culture)
- {
- return null;
- }
- }
Final Step
The final step is to take both objects using their corresponding static methods.
- ObservableCollection<InboundMessage> inboundMessages = new ObservableCollection<InboundMessage>();
- ObservableCollection<OutboundMessage> outboundMessages = new ObservableCollection<OutboundMessage>();
- inboundMessages = InboundMessage.GetInboundMessages();
- outboundMessages = OutboundMessage.GetOutboundMessages();
Assign these ObservableCollections to Collection property of CollectionContainer objects. Then, add these Collection Containers to CompositeCollection using its Add method as follows.
- compositeCollection.Add(new CollectionContainer() { Collection = inboundMessages });
- compositeCollection.Add(new CollectionContainer() { Collection = outboundMessages });
Lastly, set ItemSource property of ItemsControl with this composite collection.
- conversationList.ItemsSource = compositeCollection;