← Back to Blog

Let's Code: Learning New Assembly Services (Dynamic Grid)

Eric Padua·
I recently decided to really learn the Dynamic Grid component inside and out. Since it's fresh in my mind, I thought it would be a good opportunity to give some insight on how I learn new things in OneStream, especially in the absence of really good detailed documentation or boilerplate code.
The Dynamic Grid isn't something that I really dove into very deeply before, as I didn't really need to use it much other than knowing its capabilities in case of needing to implement it for something. Also, this isn't really a holistic view of the component or anything; it's just my take on it. So, if I miss anything, feel free to comment somewhere (whether here or on LinkedIn).

The Setup

Really, at the end of the day, all I want to do is use this component as the input/output interface, similar to the Calculation Register setup, but instead of a table view, I'll be using this dynamic grid.
The Dynamic Grid is basically just a SQL Table Editor, but it seems like it's far more parameter-friendly if you need dropdowns for value validation (at least from what I can tell from perusing the relevant objects). What this means is the actual user experience (and look and feel) is essentially that of a SQL Table Editor.
I really wish we could style it a bit more other than just color highlighting for the cells. But I guess to that end, all I would really want is maybe font sizing at the bare minimum—at least then we could fit more columns into the screen without any scrolling.
I slightly digress though, so let's get on with it.

The Approach

(The following can be found in the OneStream Design and Reference Guide)
Create a dynamic grid component:
Create dynamic grid component
Attach it to its own dashboard within its own maintenance unit:
Attach to dashboard
Dashboard setup
Give it its own assembly:
Assembly setup
Assembly configuration
Create its own service factory as well as a dynamic grid service file:
Service factory
Dynamic grid service file
And finally, add the service factory kick-off to the maintenance unit itself (will show this later).
Oddly enough, it seems like you can only have one dynamic grid per maintenance unit. At least that's what you can intuit from the directions in the Design and Reference guide. Feel free to put me in my place on this one.
Update: It seems you can only have one service per maintenance unit, but you can create multiple dynamic grid components and handle content through the args.Component.Name string.

Setting Up The Assembly

For now, we'll focus on the dynamic grid service file before we set up the service factory. Upon creation of a Dynamic Grid service file, we're greeted with this blank canvas:
C#Custom Dynamic Grid
1
2using System;
3using System.Collections.Generic;
4using System.Data;
5using System.Data.Common;
6using System.Globalization;
7using System.IO;
8using System.Linq;
9using Microsoft.CSharp;
10using OneStream.Finance.Database;
11using OneStream.Finance.Engine;
12using OneStream.Shared.Common;
13using OneStream.Shared.Database;
14using OneStream.Shared.Engine;
15using OneStream.Shared.Wcf;
16using OneStream.Stage.Database;
17using OneStream.Stage.Engine;
18using OneStreamWorkspacesApi;
19using OneStreamWorkspacesApi.V800;
20
21namespace Workspace.__WsNamespacePrefix.__WsAssemblyName
22{
23public class CustomDynamicGrid : IWsasDynamicGridV800
24{
25      public XFDynamicGridGetDataResult GetDynamicGridData(SessionInfo si, BRGlobals brGlobals, DashboardWorkspace workspace, DashboardDynamicGridArgs args)
26      {
27          try
28          {
29              if ((brGlobals != null) && (workspace != null) && (args != null))
30              {
31                  // Insert logic here to retrieve data table.
32              }
33
34              return null;
35          }
36          catch (Exception ex)
37          {
38              throw new XFException(si, ex);
39          }
40      }
41
42      public XFDynamicGridSaveDataResult SaveDynamicGridData(SessionInfo si, BRGlobals brGlobals, DashboardWorkspace workspace, DashboardDynamicGridArgs args)
43      {
44          try
45          {
46              if ((brGlobals != null) && (workspace != null) && (args != null))
47              {
48                  // Insert logic here to save data table.
49              }
50
51              return null;
52          }
53          catch (Exception ex)
54          {
55              throw new XFException(si, ex);
56          }
57      }
58}
59}
60
...and we're presented with some comments inside methods: "Insert logic here to retrieve/save data"— intriguing, I wonder what they meant by this, and further, how do we start?
It even returns nulls. Neat.
Sarcasm aside — in C# each of these methods returns a specific XF object.
I know exactly what to do here. So intuitive.
So it seems we need to return these separate objects for either section (instead of the null).

Getting Data into The Dynamic Grid

For the GetDynamicGridData method that OneStream has provided us with, we're going to need to figure out how to create this XFDynamicGridGetDataResult object and configure it so that it returns data to your Dynamic Grid component.
Let's see what's inside:
Blank canvas
Looks like there are these properties within XFDynamicGridGetDataResult that we need to pay attention to:
  • AccessLevel
  • ColumnDefinitions
  • DataTable
If we highlight over each one, you can see the type of object that needs to be set for this property, so we'll explore these new things as soon as we create a new XFDynamicGridGetDataResult object.
Now let's set the properties one-by-one, simultaneously trying to understand what they are.
For AccessLevel:
A DataAccessLevel needs to be set, but what the heck is that? Looks like it's some kind of pre-determined list of integers that have names attached for what kind of rights are available for the table:
XFDynamicGridGetDataResult properties
Let's just set it to all access and leave it alone. Note you can just put the raw integer here too (though at the risk of me looking at you weird, use those pre-determined lists whenever you can for readability).
DataAccessLevel options
For ColumnDefinitions:
Looks like it takes a list of XFDynamicGridColumnDefinition objects. Let's see what those are:
Set access level
Seems like it's just a bunch of settings for each column to customize the behavior of each one (i.e., you can allow updates on a column if you want to allow the user to write to it).
I bet we can just leave this blank/null/empty and it'll have some defaults that will get applied, so let's plan to just pass a null in here and see what happens.
Actually, let's ask cursor's agent instead just to have another layer of confidence:
Column definitions
Looks like it's fine. Let's just hope that's true and figure it out later if it doesn't work.

Creation of XFDataTable

For DataTable:
This one takes an XFDataTable. Let's see what it looks like inside. On the surface, it seems to be a custom OneStream data-table-like object. I wonder if you can create one of these things from a regular data table.
Cursor response about column definitions
Heck yeah, you can. Let's see what other things it needs for this one specific constructor. We will need:
  • A SessionInfo
  • A Dictionary<string, object> of columns to skip
  • And an Integer of how many rows we can have in the result
The first one is pretty self-explanatory (the SessionInfo).
The second is a bit weird; why can't we just pass a list of strings? Let's ask cursor:
XFDataTable constructor
Seems like you can just pass a null here too if you don't want to skip any columns. But if you did, you would just need the keys as the column names you need to skip. Odd that you would need a dictionary for this though, there might be some performance reason that I'm missing.
The third one looks like it's just a hardcoded integer of how many row results can be shown for the XFDataTable. Let's ask cursor if I can just pass a -1 for unlimited rows.
Cursor response about dictionary
Yessir, but let's put 100. (Spoilers: I'm going to just return the Member database table). We'll assume cursor is telling the truth on this one too then just come back to it if it's a problem.

Setting up the Service Factory

Pretty easy to do, just return the class that we created for the Dynamic Grid Service:
Cursor response about row limit
You'll also have to set the service factory on the maintenance unit itself (this is how we connect your Dynamic Grid component to our Dynamic Grid service file and also what I was referring to about having only one Dynamic Grid per maintenance unit):
Service factory setup

Creating the Data Table And Putting It All Together

I'm just going to grab the Member database table through a regular SQL pull.
C# has this cool syntax where you don't have to wrap everything in a using statement; you can just declare it once, and it assumes it'll be used for the rest of the method until it's done doing its thing.
Now that we have everything we need, let's create the object and run the component.
Maintenance unit setup
Final code
Super secret content, sorry.
Looks like the cursor responses were accurate, at least on the surface. But at least we have data in the table now.
Fun, right?

Saving Data

So, for that other XFDynamicGridSaveDataResult object, I'm going to skip the exercise of exploring the entire object.
I found out we can just use BRApi.Database.SaveDataTableRows to handle all inserts, updates, and deletes and then return an empty XFDynamicGridSaveDataResult object to handle the saving of any columns that we set to AllowUpdates.
Final result
We didn't create a list of XFDynamicGridColumnDefinition objects to feed into the construction of the XFDynamicGridGetDataResult, but I'll leave that as an exercise for y'all to do (if anyone is actually seriously following along).

Conclusion

But there you have it, a working Dynamic Grid (complete with saving data as a hand-wavy exercise). I still need to go through some understanding of how pages work, it doesn't seem like (at least for the setup in this blog) the data paginates properly by just feeding one large SQL statement into the component. It seems like you might have to do the offset and fetch manually. I may leave that for another blog.
In any case, many don't really have a mentor to walk you through this process, so I thought I'd give a look into the inner-workings of how I do things (this service basically comes free when you do work with me wink wink).
Anyway, happy coding, once again, until next time.
C#Reference Code - Custom Dynamic Grid
1
2using System;
3using System.Collections.Generic;
4using System.Data;
5using System.Data.Common;
6using System.Globalization;
7using System.IO;
8using System.Linq;
9using System.Xml;
10using Microsoft.CSharp;
11using OneStream.Finance.Database;
12using OneStream.Finance.Engine;
13using OneStream.Shared.Common;
14using OneStream.Shared.Database;
15using OneStream.Shared.Engine;
16using OneStream.Shared.Wcf;
17using OneStream.Stage.Database;
18using OneStream.Stage.Engine;
19using OneStreamWorkspacesApi;
20using OneStreamWorkspacesApi.V800;
21
22namespace Workspace.__WsNamespacePrefix.__WsAssemblyName
23{
24  public class CustomDynamicGrid : IWsasDynamicGridV800
25  {
26      public XFDynamicGridGetDataResult GetDynamicGridData(SessionInfo si, BRGlobals brGlobals, DashboardWorkspace workspace, DashboardDynamicGridArgs args)
27      {
28          try
29          {
30              if ((brGlobals != null) && (workspace != null) && (args != null))
31              {
32
33                  using DbConnInfo dbConnApp = BRApi.Database.CreateApplicationDbConnInfo(si);
34
35                  string sql = @"
36                      SELECT TOP 100 * FROM Member
37                  ";
38
39                  DataTable dt = BRApi.Database.ExecuteSql(dbConnApp, sql, true);
40
41                  XFDynamicGridGetDataResult result = new XFDynamicGridGetDataResult()
42                  {
43                      DataTable = new XFDataTable(si, dt, null, 100),
44                      ColumnDefinitions = new List<XFDynamicGridColumnDefinition>(), // Empty list for now
45                      AccessLevel = DataAccessLevel.AllAccess
46                  };
47
48                  return result;
49              }
50
51              return null;
52          }
53          catch (Exception ex)
54          {
55              throw new XFException(si, ex);
56          }
57      }
58
59      public XFDynamicGridSaveDataResult SaveDynamicGridData(SessionInfo si, BRGlobals brGlobals, DashboardWorkspace workspace, DashboardDynamicGridArgs args)
60      {
61          try
62          {
63              if ((brGlobals != null) && (workspace != null) && (args != null))
64              {
65                  using DbConnInfo dbConnApp = BRApi.Database.CreateApplicationDbConnInfo(si);
66                  BRApi.Database.SaveDataTableRows(
67                      dbConnApp, "XFC_TestTable",
68                      args.SaveDataArgs.Columns,
69                      args.SaveDataArgs.HasPrimaryKeyColumns,
70                      args.SaveDataArgs.EditedDataRows,
71                      throwIfRowForUpdatesDoesntExist: true,
72                      throwIfRowForDeleteDoesntExist: true,
73                      logErrors: true
74                  );
75
76                  return new XFDynamicGridSaveDataResult();
77              }
78
79              return null;
80          }
81          catch (Exception ex)
82          {
83              throw new XFException(si, ex);
84          }
85      }
86  }
87}
88
C#Reference Code = Dynamic Grid Service Factory
1
2using System;
3using System.Collections.Generic;
4using System.Data;
5using System.Data.Common;
6using System.Globalization;
7using System.IO;
8using System.Linq;
9using Microsoft.CSharp;
10using OneStream.Finance.Database;
11using OneStream.Finance.Engine;
12using OneStream.Shared.Common;
13using OneStream.Shared.Database;
14using OneStream.Shared.Engine;
15using OneStream.Shared.Wcf;
16using OneStream.Stage.Database;
17using OneStream.Stage.Engine;
18using OneStreamWorkspacesApi;
19using OneStreamWorkspacesApi.V800;
20
21namespace Workspace.__WsNamespacePrefix.__WsAssemblyName
22{
23  public class DGServiceFactory : IWsAssemblyServiceFactory
24  {
25      public IWsAssemblyServiceBase CreateWsAssemblyServiceInstance(SessionInfo si, BRGlobals brGlobals,
26          DashboardWorkspace workspace, WsAssemblyServiceType wsAssemblyServiceType, string itemName)
27      {
28          try
29          {
30              switch (wsAssemblyServiceType)
31              {
32                  case WsAssemblyServiceType.DynamicGrid:
33                      return new CustomDynamicGrid();
34
35                  default:
36                      return null;
37              }
38          }
39          catch (Exception ex)
40          {
41              throw new XFException(si, ex);
42          }
43      }
44  }
45}
46