Okay, I finally accomplished what I wanted to see. First let's look into XAML. Here is only the part with the ListView and only the single column that contains the segments. Of course you need a window around this and can add more columns.
<ListView Name="KnownImages" ItemsSource="{Binding FileViewModels}">
<ListView.View>
<GridView>
<GridViewColumn Header="Segments" Width="100">
<GridViewColumn.CellTemplate>
<DataTemplate DataType="local:FileViewModel">
<ItemsControl Width="auto" Height="auto" ItemsSource="{Binding Segments}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="local:SegmentViewModel">
<Line ToolTip="{Binding ToolTip}" X1="1" X2="1" Y1="{Binding YTop}" Y2="15" StrokeThickness="3" Stroke="{Binding Stroke}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
The main window has a view model that contains
public ObservableCollection<FileViewModel> FileViewModels { get; set; }
The ListView
has a binding to that field.
The DataTemplate
has a view model FileViewModel
that contains stuff for a single line in the list view that visualises one file. It mostly contains
public ObservableCollection<SegmentViewModel> Segments { get; set; }
So we can bind the ItemsControl
to that field Segments
. The WrapPanel
inside the ItemsPanelTemplate
is now populated with a DataTemplate
that knows its data type to be SegmentViewModel
:
/// <summary>
/// View model of a single file segment, which is represented with
/// a vertical line. The changeable parts of the line are stored here
/// and are directly derived from the state of the segment in the
/// constructor (ToolTip, Color and the upper Y coordinate of the line)
/// </summary>
public class SegmentViewModel
{
private const int YTopEof=0;
private const int YTopIntermediate=3;
public string ToolTip { get; set; }
public Brush Stroke { get; set; }
public int YTop { get; set; }
/// <summary>
/// Derive appearance of the line from the business data received from queue
/// if we already received data for the segment.
/// </summary>
/// <param name="fromQ">our knowledge about the segment state</param>
public SegmentViewModel(SegmentState fromQ)
{
var sb = new StringBuilder();
sb.Append("Seg ");
sb.Append(fromQ.Number);
if (fromQ.IsInMemory)
sb.Append(", in memory");
if (fromQ.IsOnDisk)
sb.Append(", on disk");
if (!fromQ.IsComplete)
sb.Append(", INCOMPLETE!!!");
ToolTip = sb.ToString();
Color col;
if (!fromQ.IsComplete)
col = Colors.Magenta;
else if (fromQ.IsInMemory)
col = fromQ.IsOnDisk ? Colors.Black : Colors.Green;
else
col = fromQ.IsOnDisk ? Colors.RoyalBlue : Colors.Brown;
Stroke = new SolidColorBrush(col);
YTop = fromQ.EndOfFile ? YTopEof : YTopIntermediate;
}
/// <summary>
/// This constructor is for segments that we did not receive so far.
/// </summary>
/// <param name="segmentNumber">We only know that we expect data from this index.</param>
public SegmentViewModel(int segmentNumber)
{
ToolTip = $"Seg {segmentNumber} not yet seen";
YTop = YTopIntermediate;
Stroke = new SolidColorBrush(Colors.LightGray);
}
For each instance of SegmentViewModel
in the Segments
array we get a new instance of Line
as visualization. The variable visual properties are mapped with bindings (tooltip, color, line length). The non variable visual properties are constants in XAML. Note: the coordinates are not pixels but something smaller - at least on my screen.