I’ve got an ObservableCollection
of objects that are classified with a name and a set of 3D coordinates. I’ve also got an ObservableCollection
of layers, each supposed to hold a 2D grid.
Problem Setting
The objective is to use an ItemsControl
, probably nested, to display those objects in the following fashion: if the Z coordinate determines the layer, and the X and Y coordinates specify each object’s position on the grid, then the object should be displayed in a TabControl
, where the TabItem
corresponds to the Z coordinate and hosts a Grid
where the Grid.Row
and Grid.Column
attributes determine where the object’s name is written on the TabItem
.
Important: while each 3D coordinate is only used once, one “Entry” object may have multiple 3D coordinates, and as such may appear various times on a grid and/or different TabItems
.
Note: the data model is not carved in stone; if the problem can be solved with another data model, I’m open to change it. The display, however, is a customer requirement—it might be modified, but I’d need extremely good arguments for that.
Background Info
The objects look like this (BindableBase
is from the Prism Library):
public class Entry : BindableBase {
public string Name { set; get; }
private EntryCoordinates coordinates;
public EntryCoordinates Coordinates {
set { SetProperty(ref coordinates, value); }
get { return coordinates; }
}
}
public class EntryCoordinates : BindableBase {
private int x;
public int X {
set { SetProperty(ref x, value); }
get { return x; }
}
private int y;
public int Y {
set { SetProperty(ref y, value); }
get { return y; }
}
private int z;
public int Z {
set { SetProperty(ref z, value); }
get { return z; }
}
}
The Entry
objects are hosted in “entry layers”:
public class EntryLayer : ObservableCollection<Entry> {
}
Eventually, I want to be able to modify the Entry
objects (which are more complex in reality) via the UI, so two-way data binding is an absolute necessity.
Effort so far
Using @Rachel’s excellent WPF Grid Extension, I implemented an ItemsControl
that populates a Grid
as desired:
<ItemsControl ItemsSource="{Binding EntryLayers, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid GridExtension.RowCount="{Binding RowCount}"
GridExtension.ColumnCount="{Binding ColumnCount}"
GridExtension.StarRows="{Binding StarRows}"
GridExtension.StarColumns="{Binding StarColumns}"
IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Grid.Row" Value="{Binding Coordinates.X}"/>
<Setter Property="Grid.Column" Value="{Binding Coordinates.Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<GridCellThumb XCoordinate="{Binding Coordinates.X}"
YCoordinate="{Binding Coordinates.Y}">
<Thumb.Template>
<ControlTemplate>
<TileControl Entry="{Binding}"/>
</ControlTemplate>
</Thumb.Template>
</GridCellThumb>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
The GridCellThumb
is a custom control that allows for drag and drop (omitted here for clarity’s sake) and exposes coordinate dependency properties:
public class GridCellThumb : Thumb {
public static readonly DependencyProperty XCoordinateProperty = DependencyProperty.Register("XCoordinate", typeof(int), typeof(GridCellThumb));
public int XCoordinate {
get { return (int)GetValue(XCoordinateProperty); }
set { SetValue(XCoordinateProperty, value); }
}
public static readonly DependencyProperty YCoordinateProperty = DependencyProperty.Register("YCoordinate", typeof(int), typeof(GridCellThumb));
public int YCoordinate {
get { return (int)GetValue(YCoordinateProperty); }
set { SetValue(YCoordinateProperty, value); }
}
}
The TileControl
is a user control that displays an Entry
’s name:
<StackPanel Orientation="Vertical">
<Label Content="{Binding Path=Name}" />
</StackPanel>
I’m stuck in finding out how to wrap the original ItemsControl
in a TabControl
template so that the objective of displaying an entry, possible various times, correctly. For instance, binding to the Coordinates.Z
path works, but creates as many TabItems
as there are entries:
<TabControl ItemsSource="{Binding Entries}">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header"
Value="{Binding Coordinates.Z}" />
<Setter Property="TabIndex"
Value="{Binding Coordinates.Z}" />
</Style>
</TabControl.ItemContainerStyle>
<!-- the ItemsControl goes here -->
</TabControl>
I’ve tried the solutions proposed by @greg40 (nested ItemsControl
), @d.moncada (another nested ItemsControl
), and @Sheridan (going up the visual tree), but I always gloriously fail when relating an Entry
to a given EntryLayer
.
Does anyone have further ideas to explore? As I said, I’m also open to re-structuring my data model, if that leads to an easier solution.
Update 2018-01-08
I’ve explored the path of using Button
s instead of a TabControl
in the sense of data-binding their clicked state to displaying varying information on the grid. This, however, only shifted the problem and created a new one: the data isn’t pre-loaded anymore, which is crucial to the customer’s requirements.
I’m now strongly considering to suggest a change of requirements to the customer and devise a different data model altogether. In order to avoid taking a wrong route again, I’d very much appreciate if there was someone in the community with a strong opinion about this problem that they’d be willing to share with me.