Demystifying Unified Selection and Selection Scopes

By Grigas P.

iTwin.js
iTwin.js
7 min readApr 21, 2020

--

The term unified selection has been used in different Bentley platforms (MicroStation CONNECT, Graphite, DgnDb, iModel.js) for a while now, but it seems many people still find it as some kind of magic that just works (or doesn’t, in some cases). In addition, not so long ago a new concept called selection scopes was added into the game, further complicating things. In this blog post I’ll try to explain what these concepts are, how they work and how they are related.

In short unified selection could be explained as a single global storage of what’s currently selected in the application.

What makes this complicated is that different components represent selection differently and sometimes it’s hard to follow why selecting something in one component doesn’t select it in another. But before we can jump to how each of the 4 primary unified selection -enabled components interact with it, we need to understand the concept of selection levels.

Selection Levels

By default, whenever a component changes unified selection, that happens on 0 (top) selection level. And similarly, whenever a component requests current selection from the storage, by default the top selection level is used. However, there are cases when we want to have multiple levels of selection. Here’s the situation:

  1. Component A puts some elements into selection at level 0.
  2. Component B displays those elements as a list. It also allows selecting each list item individually so Component C can show individual element properties.

At this point try to imagine what happened if Component B changed the selection at the 0 selection level — it would basically change the source of the data it’s displaying. So as soon as a row is selected, the Component A would update to highlight selected row element and Component B would completely reload and show just one row!

That’s definitely not what our intent was — we want Component A to stay intact, Component B to still show rows for initially selected elements and Component C to show properties of the individual element.

To achieve this, we need to decide how each component reacts to different selection levels. To make the decision we need to understand a few facts:

  1. Higher level selection has lower index. So top level selection is 0, lower level is 1, and so on.
  2. Changing higher level selection clears all lower level selections.
  3. Lower level selection doesn’t have to be a sub-set of higher level selection.

With that in mind, the above components A, B and C could be configured as follows:

  • Component A only cares about top level selection. Whenever something is selected in the component, unified selection is updated at the top level. Similarly, whenever unified selection changes, the component only reacts if that happened at the top level.
  • Component B reloads its content if the selection changes at the top level. Row selection is handled using lower level, so selecting a row doesn’t affect Component A’s selection or Component B’s content.
  • Component C reloads its content no matter the selection level.

Hopefully that explains selection levels and now we can move on to how each unified selection -enabled component handles it.

Selection Handling in Components

Tree

Tree components show a hierarchy of nodes. In case of unified selection -enabled tree, the nodes are expected to represent some kind of ECInstance (a model, element or basically anything from the EC world).

The rules for interacting with unified selection are very simple in this case:

  • when unified selection changes, we mark nodes as selected if ECInstances they represent are in the unified selection storage
  • when a node is selected, we add ECInstance represented by the node to unified selection storage

In short, this is similar to how Component A works in the selection levels example.

Table

Table is a component that displays data in a table layout. In the context of EC it’s used to display ECInstance properties — one column per property, one row per ECInstance.

The rules for interacting with unified selection are:

  • when unified selection changes at the 0 level, we load properties for selected ECInstances.
  • when unified selection changes at the 1 level, we highlight rows that represent selected ECInstances.
  • when a row is selected, we add the ECInstance it represents to unified selection at the 1 level.

In short, this is similar to how Component B works in the selection levels example.

Property Grid

Property Grid is a component that can show multiple categorized property label — value pairs. In the context of EC, it shows properties of one ECInstance. It can also show properties of multiple ECInstances by merging them into one before displaying.

The property grid has no way to change the selection and reacts to unified selection changes by simply displaying properties of ECInstances that got selected during the last selection change (no matter the selection level).

In short, this is similar to how Component C works in the selection levels example.

Viewport

The Viewport component is used to display graphical BisCore.Element ECInstances simply called Elements. The component handles a container called the highlight (or often just hilite) set to represent selected elements.

The rules for interacting with unified selection are:

  • when unified selection changes at the 0 level, we create a hilite set for the current selection and ask the viewport to hilite it.
  • when an element is selected in the viewport, we compute the selection based on selection scope and add that to our unified selection storage at the top level.

The two key concepts — hilite set and selection scope are explained next.

Hilite Set

This is a set of IDs that we want hilited for a given selection. The IDs are separated by type (model, sub-category and element) which is determined based on the types of ECInstances in selection and presentation rules to create the hilite set.

The rules are as follows:

  • for BisCore.Subject return IDs of all models that are recursively under that Subject.
  • for BisCore.Model just return its ID.
  • for BisCore.PhysicalPartition just return ID of a model that models it.
  • for BisCore.Category return IDs of all its SubCategories.
  • for BisCore.SubCategory just return its ID.
  • for BisCore.GeometricElement return ID of its own and all its child elements recursively.

So for example when unified selection contains a subject, the hilite set for it will contain all models under that subject, it’s child subjects, their child subjects, etc. Given such hilite set, the viewport component will hilite all elements in those models.

Selection Scopes

Before we had selection scopes, whenever a user picked an element in the viewport, its ID would go straight into unified selection storage. With selection scopes we can modify that and add something different. So basically the input to selection scopes’ processor is element IDs and scope to apply, and the output is element keys (class name + ID). We get the input when user picks some elements in the viewport. We put the output into unified selection storage.

Here are the scopes we have:

  • element — return key of selected element
  • assembly — return key of selected element’s parent element (or just the element if it has no parent)
  • top-assembly — return key of selected element’s topmost parent element (or just the element if it has no parents)
  • category — return key of element’s category
  • model — return key of element’s model

Key APIs

Presentation packages provide a number of APIs used to work with unified selection. Below are the key ones.

React

In most trivial cases it’s just enough to use provided React hook or HOC to get the component working with unified selection. Hooks and HOCs are in @bentley/presentation-components package.

We are moving away from using HOCs towards using hooks in all our new components. As of this moment, only the new ControlledTree component has hooks.

Hooks

HOCs

Accessing Unified Selection

In more advanced cases there may be a need to access unified selection manually. That can be done through API’s in @bentley/presentation-frontend package:

ClassAccessUseSelectionManagerPresentation.selectionEntry point to all unified selection -related functionsSelectionScopesManagerPresentation.selection.scopesEntry point to selection scopes -related functionality

Examples

Hooking a React Tree into unified selection (with hooks):

Hooking a React Table into unified selection (with a HOC):

Get hilite set for current selection:

Modify element IDs based on some selection scope:

Add something to unified selection:

Gotchas

There’s really only one that I can think of, but it’s something really confusing so definitely worth mentioning. There are two selection-related APIs named very similarly: SelectionSet (accessed through IModelConnection.selectionSet) and SelectionManager (accessed through Presentation.selection). Not only they're named similarly, but also work very similarly as well. And to make matters worse, they're somewhat synchronized... Let me try to explain.

The SelectionManager, as explained in the beginning of this post, is a single global storage of what's currently selected in the application. Additionally, I should note that it allows selecting any ECInstance (model, category, graphical element or even an ECClass!) and can be used without a viewport.

The SelectionSet, on the other hand, is what the tools (the ones you use in the viewport) think is selected. It's like a viewport-specific selection which doesn't necessarily have to match the global selection, similar how the tree component maintains it's list of selected nodes. It only maintains graphical elements and only makes sense in a context of a viewport (someone correct me if I'm wrong).

[Edit: Nov 11, 2019] Paul C. also points out that SelectionSets are shared across all viewports associated with the same IModelConnection.

When you enable unified selection on a viewport component, we start synchronizing the two sets so picking an element in the viewport puts it into global selection (after going through all the selection scopes machinery) and putting something into unified selection gets selected in the SelectionSet so it can be used by tools in the viewport.

Generally, if your application uses unified selection (and by now I hope it’s clear), you should be interacting with SelectionManager API. I've seen all kinds of issues related to using SelectionSet instead of the former. Like:

  • why does selection stop working if we don’t create a viewport?
  • why adding a (non-graphical) element to selection doesn’t select it in other components?
  • etc..

Wrap-up

The post already got way longer than I anticipated and I don’t expect you’ve read everything, so just a quick summary here:

  • Unified Selection is a single global storage of what’s currently selected in the application.
  • Selection Levels are used to create sub-selections.
  • Selection scopes are used to modify selection after it’s made in a viewport before it’s added to unified selection storage.
  • Hilite set contains IDs of stuff that needs to be highlighted in a viewport. Usually it’s input is the contents of unified selection storage.
  • Use SelectionManager API to modify / listen to selection if you’re using unified selection.

--

--