2

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 Buttons 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.

Informagic
  • 1,132
  • 1
  • 11
  • 23
  • Another thought: there’s a 1-to-many relationship between `Entry` and `Coordinate`. The `Coordinate` determines both the layer and the grid. Would it be more feasible to have the `ItemsControl` as the outer tag and the `TabControl` as the inner one? But how would one then render the `TabItem`s so that they contain their corresponding `Grid`s? – Informagic Jan 04 '18 at 10:16

1 Answers1

0

After some very valuable offline advice from a peer expert, I decided against the customer suggested data model and provide them with an alternative solution. The approach is now to fill the View in a top-down fashion.

In essence, this means that what was previously my Z coordinate is now the index of a dedicated EntryLayer object defining the TabItems. Each EntryLayer has an ObservableCollection<Entry> that refers to those Entrys that a parts of that EntryLayer.

This contrasts to the initial model in the sense that now it’s not possible anymore that a Entry can have multiple coordinates; instead, the Entry itself might exist multiple times with different coordinates.

How did I sell it to the customer? I told them that now an Entry could have customization options that may differ between its representations on the same or other EntryLayers, e.g., by using user-specified colors and fonts, while still pointing back to the same base information. It gives me some more work to do implementation-wise, but it elegantly solves the deadlock by putting it in the shape of a new feature.

Informagic
  • 1,132
  • 1
  • 11
  • 23