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", _
		' 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?):


... and we get this result:



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))


(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