JackLacava
Community Manager
Community Manager

The CPM world has traditionally allowed users to define custom lists of members in code, which is very useful when built-in expansions are not enough for their needs. This is a fairly rare occurrence in OneStream, because of the superior power of its filters (e.g. .Where, .Remove, .List, etc etc - check out the Samples tab in your Member Filter Builder if you've not done it already!); still, there are situations where even those can't get you all the way to the best-looking solution. In those cases, when no-one else can help, and if you can write them, maybe you can hire the A-Team use Member Lists.

"But those are flat!", I hear you shout. "We want nice trees, like we'd get with .TreeDescendants! You can't do that with Member Lists!"... and that is just not true. Let's get down to it.

To build Member Lists, we have to work with two sections of a Finance Business Rule:

 

Select Case api.FunctionType
	Case Is = FinanceFunctionType.MemberListHeaders
		' ...
	Case Is = FinanceFunctionType.MemberList
		'...

 

Strictly speaking, only the second one is really required; but it's best-practice to satisfy the first too. That's because the first block is where we declare, to the system, which lists this rule can provide. There are corners of the application that might try to introspect your rule for this info; and it's nice to effectively auto-document, at the top of the file, which lists you built, so that the next person reading your code (who might well be a serial killer that knows where you live) won't have to delve through hundreds of lines just to figure it out. It's super-simple anyway, just wrap your names in MemberListHeader objects:

 

Case Is = FinanceFunctionType.MemberListHeaders
	Dim listHeaders As New List(Of MemberListHeader)
	listHeaders.Add(New MemberListHeader("MyPineTree"))
	' ... if you have more lists, just add other objects:
	' listHeaders.Add(New MemberListHeader("MyCherryTree"))
	Return listHeaders

 

Now for the real work...

The first thing we have to do, when asked to produce a custom list, is to determine which list was requested. That's similar to how we do things in a number of other rules (Dashboard DataSet, Custom Calculations, etc etc).

 

Case Is = FinanceFunctionType.MemberList
	If args.MemberListArgs.MemberListName.XFEqualsIgnoreCase("MyPineTree") Then
		' ... do some work and return your list

	' ... if you have more lists, just check again
	' Else If args.MemberListArgs.MemberListName.XFEqualsIgnoreCase("MyCherryTree") Then
		' ... do some other work and return the other list

	End If

 

The endgame is to build a MemberList object and return it. In order to do that, we need two things: a MemberListHeader object (which we've already seen how to build) and a List of Member or MemberInfo objects. Choosing one type of objects over the other is typically considered a convenience thing: some calls return MemberInfos (like api.Members.GetMembersUsingFilter), and others return Members (like api.Members.GetBaseMembers), so it makes sense to let us pick what is handier.

However, there is an important distinction: MemberInfo objects have a property .IndentLevel, which specifies their position in a tree. And it's read-write, so you can manipulate it! And the resulting MemberList object will actually pass that info to Cube Views! OMG GUYS!

Let's look at this in practice. The following snippet gets a list of base entities, arbitrarily indents the second member, and then returns the memberlist:

 

Case Is = FinanceFunctionType.MemberList
	If args.MemberListArgs.MemberListName.XFEqualsIgnoreCase("MyPineTree") Then
		' build the header
		Dim listHeader As New MemberListHeader("MyPineTree")
		' build the list
		Dim listOfMemberInfos As List(Of MemberInfo) = api.Members.GetMembersUsingFilter( _
			args.MemberListArgs.DimPk, _
			"E#Houston.Base", _
			Nothing)
		' indent the second item, because we like it like that
		listOfMemberInfos(1).IndentLevel += 1
		' build and return the member list
		Return New MemberList(listHeader, listOfMemberInfos)
	End If

 

We drop it in our CubeView (with the condensed syntax, because why type more than you have to?):

JackLacava_1-1689936242897.png

... and we get this result:

JackLacava_0-1689936192715.png

Nice!

If you stick to using GetMembersUsingFilter with an indent-producing filter (e.g. TreeDescendants), you don't even need to do anything - all MemberInfo objects returned will already have IndentLevel set. But what if you have Member objects instead?Just wrap them in MemberInfo instances:

 

' oh no, a list of Member objects!
Dim listOfMembers As List(Of Member) = api.Members.GetAllMembers(args.MemberListArgs.DimPk)
' let's turn it into a list of MemberInfo
Dim listOfMemberInfos As New List(Of MemberInfo)
For Each myMember As Member In listOfMembers
	listOfMemberInfos.Add(New MemberInfo(myMember))
Next

 

(Note: I know there are cooler ways to do this conversion - I'm keeping it simple here...)

Now you can manipulate .IndentLevel on the objects as you see fit, and build lots of weird and wonderful trees. Just don't try to water them through your laptop...

1 Comment
JussiPukki
New Contributor III

Thanks for this useful post!

Just wanted to comment here that it seems to me like this doesn't actually create a tree but rather an indented list. The difference here is:

  1. If I put this memberlist on a cube view, it is not expandable; all the rows of the list are visible all the time
  2. If I use this as the member filter in a member selector button, it shows a flat list even though the indent values are there in the MemberInfos

Looks like getting an expandable tree that also works in a button member filter requires using the .Tree option. Apparently the .Tree member filter only returns the MemberInfo for the top member of the hierarchy with the following properties:

  1. DisplayUsingExpandableTree = True
  2. SupportsChildren = True

Since it creates the tree only using this one MemberInfo, there doesn't seem to be any way to e.g. filter the descendants in a member rule. This is probably also why things like E#Parent.Tree.Where(...) don't seem to work.

Let me know if I'm mistaken. It would be very useful to be able to filter actual trees that could be used in e.g. member selector buttons.