Here's the solution I eventually came up with. This link helped tremendously; I pirated the XAML directly:
<DataTemplate x:Key="DataTemplateLevel2">
<ContentPresenter Content="{Binding}"/>
</DataTemplate>
<DataTemplate x:Key="DataTemplateLevel1">
<ItemsControl ItemsSource="{Binding}" ItemTemplate="{DynamicResource DataTemplateLevel2}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:DisplayGridViewModel}">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Lists}" ItemTemplate="{DynamicResource DataTemplateLevel1}"/>
</ScrollViewer>
</DataTemplate>
Below is a stripped-down version of my viewmodel. The basic trick is that an Initialize method is called when the grid dimensions are known; this method populates the grid with the following:
- Cells: items of type ICoordViewModel
- Row headers: items of type RowHeaderViewModel
- Column headers: items of type ColumnHeaderViewModel
- Corner: item of type CornerHeaderViewModel.
Next, as new viewmodels arrive, the Add method is called to find the X/Y location where the viewmodel should be placed, and it is injected into the placeholder; and as this happens it is displayed in the grid in the appropriate location. (All viewmodels are template in XAML).
public class DisplayGridViewModel
{
// Fields and constructor.
public void Initialize()
{
var rows = GetRows().ToList();
var columns = GetColumns().ToList();
_lists.Clear();
var cornerHeader = new CornerHeaderDisplayViewModel(_displayEditor /* + other content */);
var columnHeaders = columns.Select(c => new ColumnHeaderViewModel(c, _displayEditor));
_lists.Add(new ReactiveList<object>(Enumerable.Repeat((object)cornerHeader, 1).Union(columnHeaders)));
var index = 1;
foreach (var row in rows)
{
_lists.Add(new ReactiveList<object>());
_lists[index].Add(new RowHeaderViewModel(row, _displayEditor /* + other content */));
foreach (var column in columns)
{
_lists[index].Add(new CellViewModel(_displayEditor /* + other content */));
}
index++;
}
}
private IEnumerable<object> GetRows()
{
// custom implementation
}
private IEnumerable<object> GetColumns()
{
// custom implementation
}
public void Add(ICoordChart coordChart)
{
var match = _lists.SelectMany(l => l).OfType<CoordViewModel>().Single(cc => IsMatch(cc, coordChart));
match.ViewModel = coordChart.ViewModel;
}
private static bool IsMatch(ICoordChart cc, ICoordChart chart)
{
// custom implementation
}
private readonly IReactiveList<IReactiveList<object>> _lists = new ReactiveList<IReactiveList<object>>();
public IReactiveList<IReactiveList<object>> Lists
{
get { return _lists; }
}
}
Finally there is a DisplayEditorViewModel which provides a central location for cell width and height. This allows the user to adjust the display dimensions elsewhere on the UI and the cells are automatically resized.
Apologies for anything that's unclear. Hopefully the code here will provide a decent balance between providing some high-level clues for others tackling this issue while avoiding too much unnecessary detail.