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?):
... and we get this result:
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...
Blogs from Experts on OneStream