The Dynamic Grid Dashboard Component
Are you frustrated by volume limitations and the inability to edit data in grid view? Do you find the SQL Table Editor restrictive because it only supports a single table? Are your items taking too long to load in these grids? Discover a smarter way to load data on dashboards and grids: the Dynamic Grid dashboard component. Traditionally, OneStream grids have been limited in customizations, flexibility, data retrieval and data manipulation. The Dynamic Grid changes that traditional pattern. It allows each of those items in a more robust and enhanced way. Also, the Dynamic Grid allows for multiple data sources from a single component (however out of scope for this article). The Dynamic Grid loads only the data you need—nothing more. Dynamic Grid delivers unmatched flexibility: you can customize your grid to fit your company’s exact requirements. Enjoy traditional column formatting from the SQL Table Editor, now enhanced with powerful row and specific cell formatting options. I could go on about how powerful this new dashboard component is—but you are here to learn how to set it up. Let us dive into an example of creating a Dynamic Grid. The Dynamic Grid relies on two out‑of‑the‑box functions: GetDynamicGridData and SaveDynamicGridData. GetDynamicGridData retrieves the data for display in the grid. SaveDynamicGridData persists changes made in the grid—whether you are adding, updating, or deleting rows. Together, these functions power efficient data retrieval and safe, transactional saves for the Dynamic Grid. GetDynamicGridData Example Code: The first line— If args.Component.Name.XFEqualsIgnoreCase("Grid7") Then —acts as a conditional check to segment multiple Dynamic Grids within the same assembly. In other words, it ensures that the logic applies only to the grid named Grid7. This name must exactly match the Dynamic Grid component name defined in your dashboard configuration. Immediately after the opening If condition, you will notice two key arguments: startRowIndex and pageSize. These variables define where the table should begin rendering and how many rows appear per page. Without them, the grid would attempt to render the entire data, which would create serious performance issues. Here is what you need to know about pageSize: pageSize is driven by the Rows Per Page property on the grid. By default, Rows Per Page is set to -1, which means the grid uses your security settings to determine row limits. If you specify any value other than -1, that value overrides the security setting. The maximum allowed page size is 3,000 rows per page. Setting these arguments correctly is critical for performance and user experience. The next few lines use standard OneStream and .NET objects to open a database connection and create a command—nothing unusual there. The real magic happens in the SQL query, which leverages the lesser known OFFSET and FETCH clauses for efficient paging. These functions allow you to return only the rows you need, rather than loading the entire dataset. Alternatively, you can implement paging by generating a RowNumber column and filtering with a WHERE clause, such as: “WHERE ((RowNumber >= {StartRowNumber}) AND (RowNumber <= {EndRowNumber}))” Both approaches achieve the same goal: controlled data retrieval for better performance. Note: When building a Dynamic Grid with SQL, always follow best practices for writing SQL queries. This ensures your solution is secure, efficient, and maintainable. Notice the StartRowNumber and EndRowNumber variables integers. These are driving the SQL parameters inside of the OffSet and Fetch section of the SQL query. Using parameters is essential for pagination because it ensures your query retrieves only the requested rows while preventing SQL injection. The next section of code handles data retrieval and paging metadata: First, a new DataTable is created and populated using the SQL query for the current page of records. Then, “Dim SQL As String” defines a COUNT query to determine the total number of rows in the underlying table. A second DataTable is used to execute this count query and retrieve the result. Finally, the line: “result.TotalNumRowsInOriginalDataTable = $"{dt(0)(0)}" assigns the total row count to the XFDataTable result object. This property is critical for paging—without it, the grid cannot calculate how many pages to display. In short: no total count, no paging. Once all the arguments are in place, the final step is to create the XFDynamicGridGetDataResult object. This constructor requires three key inputs: XFDataTable – The data table you populated earlier (e.g., the result variable). Column definitions – We will cover these in detail later in the article. DataAccessLevel – Determines how users interact with the grid data. You can choose from: .AllAccess – Full read/write access (most common for editable grids). .ReadOnly – Users can view but not modify data. .NoAccess – No data interaction allowed. For most scenarios involving data manipulation, .AllAccess is the recommended setting. Note: As of now, it’s on the roadmap for paging and the ability to ascend and descend columns. This functionality is not currently available as of the writing of this article. SaveDynamicGridData Example Code: In the previous section, we focused on rendering data. Now, let us shift to saving modified data, whether inserting, updating, or deleting rows. With the SQL Table Editor, save functionality is built in. However, the Dynamic Grid offers far greater flexibility, which means you will need to implement custom-saving logic. This approach gives you full control over: How data changes are processed Custom validation rules User messaging and error handling Custom saving functionality ensures your grid behaves exactly as your business requires. Let us break down the code for the SaveDynamicGridData Function step by step. In the save routine, you will see two argument objects sourced from args: Dim getDataArgs As DashboardDynamicGridGetDataArgs = args.GetDataArgs Retrieves context from the earlier GetDynamicGridData call (e.g., paging, filters, sort)—useful if your save logic needs to reference the current view. Dim saveDataArgs As DashboardDynamicGridGetDataArgs = args.SaveDataArgs Initializes the save payload: inserted, updated, and deleted rows, plus column metadata. Next, the code gathers what is editable: Dim editedDataRows As List(Of XFDataRow) = saveDataArgs.EditedDataRows Gets the list of rows that have been modified, inserted, or deleted. This is often the entire row set, but you can scope it to specific rows when only part of the data should be editable. Dim columns As List(Of XFDataColumn) = saveDataArgs.Columns Provides the columns list (names, types, formats) for the target table—critical for validation and parameter binding. You will also see a simple table name string (e.g., Dim tableName As String = "dbo.sample_data") to identify where the changes are saved, and a Boolean flag used later to control behavior (such as enabling/disabling validation or transactional commits). Finally, the code re-opens a database connection to perform the actual writes (insert, update, delete). This is done in the save path to keep read and write operations logically separated and to ensure that any transactional logic is scoped to the save operation. Now we are moving into the fun section of code. This opens with a Loop (For Each) of all our possible edited data. Then you see the Case Statement “editedDataRow.InsertUpdateOrDelete” This lets the application know that we want to apply the insert, update or delete functions to our available edited data rows variable. There is no easy part to the modified sections. Each of them takes their own separate lines of code that are functionally SQL Statements. As you can see from my previous screenshot, I have created three separate functions to dynamically run these queries. I will post these code snippets as an appendix to this blog. For the function of InsertDataRow, this is using the standard SL Insert statement. Now we get to the save loop. The routine starts with a For Each loop over the collection of edited rows. Inside the loop, a Select Case editedDataRow.InsertUpdateOrDelete directs the logic for each row, applying the appropriate insert, update, or delete operation. There is no shortcut here, each operation requires its own parameterized SQL command and validation. In the screenshot, you will see I’ve split the logic into three dedicated functions to keep the code clean and testable: InsertDataRow(...) UpdateDataRow(...) DeleteDataRow(...) I will include these helpers in the appendix for reference. For InsertDataRow, we use a standard SQL INSERT statement with parameters (not string concatenation), ensuring safety and better performance. INSERT INTO <tableName> (Column1, Column2, Column3) VALUES (Value1, Value2, Value3) The UpdateDataRow function uses a standard SQL UPDATE statement to modify existing records. Here’s the basic pattern: UPDATE <tableName> SET column1 = value1, column2 = value2 WHERE <condition> The DeleteDataRow function uses a standard SQL DELETE statement to remove records. Here’s the basic pattern: DELETE FROM <tableName> WHERE <condition> Now that we have done all this coding magic, what’s next? First, create an empty XFDynamicGridSaveDataResult: “Dim result as New XFDynamicGridSaveDataResult()” This step is non‑negotiable: you must send data back to the gird after the save. Without it, the grid will lose context and paging will break. That’s why this line is essential: “result.DataTable = GetDynamicGridData(si, brGlobals, workspace, args)?.DataTable” . By calling GetDynamicGridData again, you ensure the save result reflects the latest filters, sort order, and paging logic—keeping the user experience consistent after modifications. With the empty XFDynamicGridSaveDataResult created and the original data table rehydrated, the next step is to restore paging, so the user returns to the correct page after a save. Compute the current page index using VB.NET integer division: “result.PageIndex = (getDataArgs.StartRowIndex \ getDataArgs.PageSize)”. Finally, before returning the result, there’s one last piece of logic, a small but important snippet that ties everything together. This step ensures the save result object is fully populated with the data, paging info, and any additional metadata the grid needs to render correctly after modifications. result.SaveDataTaskResult = New XFDynamicGridSaveDataTaskResult() With { .IsOK = True, .ShowMessageBox = True, .Message = "Save Finished" } This allows us to assign the result to the XFDynamicGridSaveDataTaskResult. However, you will notice the “With” statement. This is a cool vb.net trick that allows you to assign a series of properties or arguments inside an enclosed block of code. Simply put, you wouldn’t have to keep assigning, result.isOk = True and so forth. This is a quick way to assign several properties that are related. This is where we assign values to the XFDynamicGridSaveDataTaskResult. You’ll notice a With block—an idiomatic VB.NET construct that lets you set multiple properties on the same object without repeating the variable name. In other words, instead of writing result.IsOk = True, result.PageIndex = …, and so forth line by line, you can group them neatly inside one block. Now that we have all this code in place, you can finally return the result. We’ve covered how to render data and how to save modified rows—but the Dynamic Grid’s most used reporting feature is its ability to apply conditional column formatting. This functionality mirrors the familiar options found in the SQL Table Editor, giving you full control over how columns look and behave based on dynamic conditions. Below is a screenshot of the SQL Table Editor column formatting properties. All these formatting properties are available in the Dynamic Grid—but only through code. You can apply them directly within the GetDynamicGridData function or encapsulate them in a dedicated formatting function for better organization and reuse. In my implementation, I chose the latter approach, creating a separate function to handle conditional column formatting. Inside the GetDynamicGridData function (see screenshot above), I added the following line: “Dim columnDefinitions As List(Of XFDynamicGridColumnDefinition) = Me.GetColumnDefinitions()”. This creates a list of XFDynamicGridColumnDefinition objects, which define the properties and formatting rules for each column in the grid. The call to Me.GetColumnDefinitions() retrieves these definitions from a dedicated function, keeping the logic clean and reusable. As you can see, this entire line of code is commented out. I wanted to highlight all the possible properties. If you look at the available properties, they match one-for-one with the properties that exist inside the SQL Table Editor component. You must initialize the variable named columnDefinition6, and as we previously discussed, the With statement is used here. Each of these properties should function exactly as they do within the SQL Table Editor. Once you have set the properties, you need to add them to the collection of column definitions. This is what you see with “columnDefinition.Add(columnDefinition#)”, which adds the specific column definition to the list of all column definitions. Finally, as with every function, the return clause simply returns the final output. In conclusion, we recognize that new code‑based components can feel intimidating at first. But with the insights from this blog, we hope we’ve lowered the barrier to entry and given you the confidence to start taking full advantage of this powerful new grid. InsertDataRow Sub Code Sample: UpdateDataRow Sub Code Sample: DeleteDataRow Sub Code Sample:30Views0likes1CommentMenu Component in Practice
If you create the new Menu Component in your Maintenance Unit, you will quickly notice that it has very few properties. That's because its configuration will actually come from an attached Data Adapter, which must produce a set of tables containing all menu items and their configuration. The format of such tables has to be somewhat precise, matching what the component expects. For this reason, the best way to produce them (at least while you familiarize yourself with this mechanism) is to create a Dashboard DataSet using a couple of utility classes built for this specific task. The first thing we will do, in our rule, is to create an XFMenuItemCollection object. This represents our menu, which we will populate with items and eventually return (as a DataSet) at the end of the function. Public Function Main(ByVal si As SessionInfo, ByVal globals As BRGlobals, ByVal api As Object, ByVal args As DashboardDataSetArgs) As Object Try Select Case args.FunctionType Case Is = DashboardDataSetFunctionType.GetDataSet If args.DataSetName.XFEqualsIgnoreCase("MainMenu") Then ' create the menu object Dim menu As New XFMenuItemCollection() Menu items will be created by instantiating objects of type XFMenuItem. These objects will hold all the configuration properties for the item, including the font and colors that it will use. There are a few different constructors you can use, to specify all sorts of properties; the one used here is the shortest one! ' create top-level item ' XFMenuItem(string uniqueName, string headerText, string foreground, string background, bool isBold, bool isItalic, bool isEnabled, bool isSeparator, string parameterValue) Dim parentMenuItemOne As New XFMenuItem("1", "Parent", _ "White", "SlateGray", False, False, True, False, Nothing) ' create items for the second level Dim childMenuItemOne As New XFMenuItem("1.1", "Child 1", _ "Black", "White", True, True, True, False, Nothing) Dim childMenuItemTwo As New XFMenuItem("1.2", "Child 2", _ "Black", "White", True, True, True, False, Nothing) ' create item for the third level Dim grandChildMenuItemOne As New XFMenuItem("1.1.1", "Grandchild 1", _ "White", "SlateGray", True, True, True, False, Nothing) Most of the properties are self-explanatory, but you might wonder what "parameterValue" is. That's the value that will be pushed into the Bound Parameter specified on the actual component later on, so that Actions or other components can pick it up. We will come back to this later. Now we need to define the relationships between items. We do that by manipulating the .Children property of each parent item, which must be a list containing the children (duh!) items. ' create the hierarchy by adding children to direct parents as a List ' attach 1.1.1 as a child of 1.1 childMenuItemOne.Children = New List(Of XFMenuItem) From {grandChildMenuItemOne} ' attach 1.1 as a child of 1 parentMenuItemOne.Children = New List(Of XFMenuItem) From {childMenuItemOne} ' you can also manipulate the list once created. ' attach 1.2 as a child of 1 parentMenuItemOne.Children.Add(childMenuItemTwo) In a similar way, top-level items are added to the list contained in the .MenuItems property of our XFMenuItemCollection instance. ' add item 1 as a top-level members of the menu menu.MenuItems.Add(parentMenuItemOne) Before we return the resulting menu, if you are dealing with dynamically-generated structures with a lot of members, you might want to perform a safety check and purge extra members: While menu.IsAboveMaxMenuItemLimit(si) menu.MenuItems.RemoveAt(menu.MenuItems.Count - 1) End While Last, we use the .CreateDataSet method of our menu object to return the DataSet. ' generate the dataset and return it Return menu.CreateDataSet(si) Now that we have the rule, we can create a DataAdapter to execute it. Notice how, when testing it, it produces two tables: one with item properties, and one with their relationships. Now that we have an Adapter, we can create the Menu Component and attach the Adapter to it. You can then assign it to a Dashboard and preview it, after you save it. This is fun but a bit pointless! We want menus so that the user will actually choose something and we'll get the result of that choice. In order to do that, we need to specify the Bound Parameter. Whenever the user selects an item, the "parameterValue" associated with that item will be pushed into the specified Parameter; we can then reference that Parameter in an Action or extender rule, to trigger something like navigating to a website. Note that the Parameter doesn't need to exist! OneStream will just create one for you in the background. Then we place an Action on our Component, referring to the Parameter. The last step is to go back to our rule and specify a different parameterValue for "leaf" items, so that the Parameter will contain something. Dim childMenuItemTwo As New XFMenuItem("1.2", "Main Page", _ "Black", "White", True, True, True, False, "https://www.onestream.com") ' create item for the third level Dim grandChildMenuItemOne As New XFMenuItem("1.1.1", "OneCommunity", _ "White", "SlateGray", True, True, True, False, "https://community.onestreamsoftware.com") Et voilà! You can now execute the Dashboard and verify that it works!514Views7likes4CommentsFloor's Lava—Time to Branch Out: Building Your Tree
Welcome to the blog! In today's post, we're diving deep into the world of hierarchical data visualization—Mission Tree-Ting style. If you’ve ever needed to see your organization’s structure at a glance or wanted a smart, efficient way to visualize complex relationships like employee-manager hierarchies, you're in the right place. Objective: We’ll show you how to build a robust tree structure using VB.NET that transforms raw data into an intuitive, expandable TreeView. By leveraging a custom DataAdapter and binding it to a dashboard component, you'll learn to effortlessly bring your data to life—no messy code left behind! Use Case: Imagine managing an organizational chart where each node represents an employee and their reporting relationships. Whether you're in HR, IT, or any department that thrives on clear, visual insights, this solution provides a reusable method to: Parse and filter your data via dynamic SQL queries. Construct a hierarchical tree with unique nodes and child collections. Seamlessly embed this TreeView into your OneStream dashboard, allowing for interactive exploration of your organizational structure. So, grab your coding toolkit and get ready to transform your data into a dynamic tree that not only meets everyday business needs but also adds a touch of Mission Impossible flair to your projects. Let's get started on turning the impossible into possible—tree style! Although this example demonstrates a use case from People Planning, the same approach can be applied to any dataset with a Parent/Child hierarchy — such as product hierarchies, organizational structures, Capital Planning or cost centers. Note: The method described in this blog is one of many possible ways to visualize and manage hierarchical data in OneStream. Depending on your business requirements, alternate implementations may better suit your needs. Please refer below Blogs related to the Tree View Introduction To Tree Views | OneStream Community How to build a Multi-Select TreeView | OneStream Community Overview What is Tree View? The TreeView is primarily used for intuitive navigation and visualization of hierarchical relationships. It helps users quickly identify structural paths and validate lineage, making it useful for both data review and administrative control. Generating Tree View Our function, GetTreeViewEmpMgr, performs the following steps: Constructs an SQL Query: The query selects employee data along with manager names. A NULL value for the manager indicates that the employee is at the top level (a root node). Executes the Query and Retrieves a DataTable: Using a database connection, the function executes the SQL query and stores the result in a DataTable named "Employee". Builds the Tree Structure: The function uses custom classes—XFTreeItem and XFTreeItemCollection—along with a dictionary called nodesByName to construct the tree: XFTreeItem represents each node in the tree. It holds properties such as the display text (HeaderText), state information (bold, selected, expanded), visual properties (colors and images), and a list of child nodes. XFTreeItemCollection is a container for the tree’s nodes (typically the root nodes) and provides methods for creating a DataSet from the entire tree. The nodesByName dictionary is used to quickly look up and manage nodes by their unique identifier (in this case, by name), ensuring that each node is only created once and can easily have children added later. Returns the Tree as a DataSet: Finally, the tree is converted into a DataSet using treeItems.CreateDataSet(si) and returned for UI rendering. Constructs an SQL Query The data is retrieved from the XFW_PLP_Register (People Planning Register) based on the current Workflow Profile, Time, and Scenario — all passed as parameters to ensure contextual relevance. Select CASE WHEN M.RegisterID = E.RegisterID THEN NULL Else Concat(M.FirstName,' ',M.LastName) End as 'Parent' , Concat(E.FirstName,' ',E.LastName) as 'Child' FROM XFW_PLP_Register E LEFT JOIN XFW_PLP_Register M ON E.Code11 = M.RegisterID Where E.Status <> 'NewHire' And E.WFScenarioName = '" & WFScenarioName & "' And E.WFTimeName = '" & WFTimeName & "' And E.WFProfileName = '" & WFProfileName & "' And (M.WFScenarioName = '" & WFScenarioName & "' And M.WFTimeName = '" & WFTimeName & "' And M.WFProfileName = '" & WFProfileName & "' OR M.RegisterID IS NULL) For each employee, we extract the First Name and Last Name, along with their manager’s corresponding First Name and Last Name. In this configuration, the manager's Employee ID is stored in the Code11 field. By performing a self-join on the XFW_PLP_Register table using this reference, we can resolve and display the appropriate manager name for every employee. Emphasis on Key Components XFTreeItem The XFTreeItem class is the building block of our tree structure. Each XFTreeItem represents a node that can have: HeaderText: The display text for the node. Visual Properties: Such as text color, images (using properties like imageSource and imageName), and formatting (bold, enabled, selected, expanded). Children Collection: A list that holds any child nodes, making the node hierarchical. Additional Data: You can attach other metadata as needed, making XFTreeItem flexible for various applications. ' Common visual properties. Dim textColour As String = XFColors.Black.Name Dim imageSource As String = XFImageFileSourceType.ClientImage Dim imageName As String = XFClientImageTypes.StatusGrayBall.Name Dim isBold As Boolean = False Dim isEnabled As Boolean = True Dim isSelected As Boolean = False Dim isExpanded As Boolean = False In our code, we create new XFTreeItem objects for every child and parent as needed, ensuring that each node correctly reflects its employee or manager data. Initializing the node with default properties ' Create a node for the child. Dim childNode As New XFTreeItem(childName, childName, textColour, isBold, isEnabled, isSelected, isExpanded, imageSource, imageName, childName, Nothing) updating the parent node properties ' Create a new parent node if it doesn't exist. parentNode = New XFTreeItem(parentName, parentName, textColour, isBold, isEnabled, isSelected, True, imageSource, imageName, parentName, Nothing) 'Updating the properties of parent node If parentNode.Children Is Nothing Then parentNode.IsBold = True parentNode.IsExpanded =True parentNode.Children = New List(Of XFTreeItem) End If parentNode.HeaderText = parentName+ " ("+(parentNode.Children.Count+1).ToString+")" 'Adding the child node to the parent's children collection parentNode.Children.Add(childNode) XFTreeItemCollection The XFTreeItemCollection class serves as the container for the entire tree. It typically holds the collection of root nodes and offers helper functions to manipulate or output the tree. In our function, after building the tree, we call: Return treeItems.CreateDataSet(si) This method converts the tree structure into a DataSet, making it easier to bind to UI controls like a TreeView. By using a dedicated collection class, we encapsulate all tree-related functionality, keeping the code modular and maintainable. Item Table holds the detailed data for each node. Relationship Table captures how these nodes relate to each other (i.e., which node is the parent of which). Note: These are random names not any Org data nodesByName Dictionary The nodesByName dictionary is a crucial element in building the tree. Its main purposes are: Ensuring Uniqueness: It prevents duplicate creation of nodes by keeping track of every node by its unique name (or identifier). Facilitating Lookup: When processing a new row, the dictionary is quickly checked to see if a node already exists for the given parent or child. This way, if a parent node is referenced later, it is easily found and updated. Simplifying Tree Building: By centralizing node management, it simplifies the process of attaching children to the correct parent nodes. ' If there is a parent, check if the parent node already exists. Dim parentNode As XFTreeItem = Nothing If nodesByName.ContainsKey(parentName) Then parentNode = nodesByName(parentName) BRApi.ErrorLog.LogMessage(si,"Parent is there: "+parentNode.HeaderText) Else ' Parent does not exist yet; create it as a root node. parentNode = New XFTreeItem(parentName, parentName, textColour, isBold, isEnabled, isSelected, True, imageSource, imageName, parentName,Nothing) treeItems.TreeItems.Add(parentNode) nodesByName(parentName) = parentNode End If This approach avoids the overhead of scanning a list for a matching node each time and guarantees that each node appears only once in the tree. The Complete Code Below is the complete VB.NET code for the GetTreeViewEmpMgr function with detailed comments explaining the use of XFTreeItem, XFTreeItemCollection, and nodesByName. Public Function GetTreeViewEmpMgr(ByVal si As SessionInfo, ByVal args As DashboardDataSetArgs) As DataSet Try Dim sql As New Text.StringBuilder() Dim WFProfileName As String = args.NameValuePairs.XFGetValue("WFProfileName") Dim WFScenarioName As String = args.NameValuePairs.XFGetValue("WFScenarioName") Dim WFTimeName As String = args.NameValuePairs.XFGetValue("WFTimeName") sql.append("Select") sql.Append(" CASE WHEN M.RegisterID = E.RegisterID THEN NULL ") sql.append(" Else Concat(M.FirstName,' ',M.LastName) End as 'Parent'") sql.append(", Concat(E.FirstName,' ',E.LastName) as 'Child'") sql.append(" FROM XFW_PLP_Register E ") sql.append("LEFT JOIN XFW_PLP_Register M ON E.Code11 = M.RegisterID ") sql.append("Where ") sql.Append("E.Status <> 'NewHire' And") sql.append(" E.WFScenarioName = '" & WFScenarioName & "' And E.WFTimeName = '" & WFTimeName & "' And E.WFProfileName = '" & WFProfileName & "' And ") sql.append(" (M.WFScenarioName = '" & WFScenarioName & "' And M.WFTimeName = '" & WFTimeName & "' And M.WFProfileName = '" & WFProfileName & "' OR M.RegisterID IS NULL)") BRApi.ErrorLog.LogMessage(si,sql.ToString) Dim dt As DataTable 'Execute the query Using dbConnApp As DbConnInfo = BRApi.Database.CreateApplicationDbConnInfo(si) dt = BRApi.Database.ExecuteSql(dbConnApp, sql.ToString(), False) dt.TableName = "Employee" End Using ' Create the main tree collection. Dim treeItems As New XFTreeItemCollection ' Dictionary to keep track of nodes by name. Dim nodesByName As New Dictionary(Of String, XFTreeItem)() ' Common visual properties. Dim textColour As String = XFColors.Black.Name Dim imageSource As String = XFImageFileSourceType.ClientImage Dim imageName As String = XFClientImageTypes.StatusGrayBall.Name Dim isBold As Boolean = False Dim isEnabled As Boolean = True Dim isSelected As Boolean = False Dim isExpanded As Boolean = False #Region "Buid Tree" ' Process each row from the DataTable. Dim i As Integer = 0 For Each row As DataRow In dt.Rows Dim parentName As String = "" If Not IsDBNull(row("Parent")) Then parentName = row("Parent").ToString().Trim() End If Dim childName As String = row("Child").ToString().Trim() ' Create a node for the child. Dim childNode As New XFTreeItem(childName, childName, textColour, isBold, isEnabled, isSelected, isExpanded, imageSource, imageName, childName, Nothing) BRApi.ErrorLog.LogMessage(si,"Adding to Child Node: "+childNode.HeaderText) If String.IsNullOrEmpty(parentName) Then ' If there is no parent, then this is a root node. treeItems.TreeItems.Add(childNode) nodesByName(childName) = childNode Else ' If there is a parent, check if the parent node already exists. Dim parentNode As XFTreeItem = Nothing If nodesByName.ContainsKey(parentName) Then parentNode = nodesByName(parentName) Else ' Parent does not exist yet; create it as a root node. parentNode = New XFTreeItem(parentName, parentName, textColour, isBold, isEnabled, isSelected, True, imageSource, imageName, parentName,Nothing) treeItems.TreeItems.Add(parentNode) nodesByName(parentName) = parentNode End If ' Add the child node to the parent's children collection. If parentNode.Children Is Nothing Then parentNode.IsBold = True parentNode.IsExpanded =True parentNode.Children = New List(Of XFTreeItem) End If parentNode.HeaderText = parentName+ " ("+(parentNode.Children.Count+1).ToString+")" parentNode.Children.Add(childNode) ' Also add the child node to the dictionary. If Not nodesByName.ContainsKey(childName) Then nodesByName.Add(childName, childNode) End If End If Next #End Region Return treeItems.CreateDataSet(si) Catch ex As Exception Throw ErrorHandler.LogWrite(si, New XFException(si, ex)) End Try End Function How to Call the Employee-Manager Tree Code via DataAdapter The code in the GetTreeViewEmpMgr function is designed to be invoked from the DataAdapter layer using a parameterized string. This allows you to easily pass in filtering values and trigger the building of the hierarchical tree structure based on the specified employee-manager relationships. {TreeView}{EmpMgr}{WFProfileName= [PLP_US.PeoplePlanning],WFScenarioName=|WFScenario|,WFTimeName=|WFTime|} Attach the DataAdapter to the TreeView and Embed it into the Dashboard: Attach and Bind: Create a TreeView component in your application and bind the DataSet produced by the GetTreeViewEmpMgr function directly to this TreeView. This binding ensures that the hierarchical data is rendered correctly in the TreeView control Embed into the Dashboard: Once the TreeView is populated, embed it into your dashboard. This integration allows end-users to interact with and explore the employee-manager hierarchy in a visually intuitive manner. Together, these steps transform raw data into a dynamic, interactive component that sits proudly on your dashboard—just like a successful mission from Mission Impossible. Your organizational structure is now displayed in a neat, expandable tree format, and your mission is accomplished! For Complete Code and Detailed Setup Instructions, Please Refer to the GitHub Repository: https://github.com/Sudha8990AI/TreeView275Views1like0CommentsMap Component Tutorial
So you’ve bought the OneStream Advanced Reporting and Dashboards book - congratulations on being on your way to mastering these tools! The book is chocked full of examples and guidance on how to tailor your user experience, but here’s something extra – a tutorial on how to use Map Components in your application. We’ll walk through setting up a Dashboard with an interactive map to provide users with a visual display of locations, from collecting coordinates to displaying locations that are click-enabled to display relevant data.3.1KViews9likes3CommentsEnriching Embedded Dashboards with Custom Controls
This is an introduction to Custom Controls in OneStream Workspaces. In this blog we will walk you through a simple (almost) No-Code example, creating a Custom Control dashboard with an embedded Button component. Custom Control is a new Dashboard Type that enriches what you can do with an Embedded Dashboard in terms of OneStream Dashboard wizardry. It was released in conjunction with Dynamic Dashboards in v8+ (see my previous blog on this concept here) but uses a different approach to solve for a different type of use case. Custom Controls are much more about taking an existing dashboard template (our Custom Control Dashboard) that contains embedded elements and then repurposing it to behave differently by embedding the embedded version of this Custom Control Dashboard into a new Dashboard and using something called Event Listeners to control that component / or components differently, when each independent instance of that embedded Custom Control Dashboard is invoked inside a new Dashboard. This means that Dashboard Templates can be repurposed in different ways, without always recreating the same Dashboards and components for each slightly different use case. Think of this a bit like the old trick to create a custom Embedded Dashboard and pass a parameter value in to it, to render different Dashboard content based on User Selection e.g. But with a much more sophisticated layer of control over the actions performed by your Dashboard Components that reside in your Custom Control Dashboard template. Now let's look at the setup. Setup: Dashboards This example has been created in its own Workspace called WsCustomControl. Let's start by looking at the Custom Control Dashboard. Here we have set the Dashboard Type as Custom Control and selected Layout Type Vertical Stack Panel. Inside this Custom Control Dashboard we have our Button component. When we review the Button itself, we can see that we have to set the Navigation options as below e.g. Selection Changed Navigation Action is Forward Custom Control To Parent Event Listener and the Selection Changed Navigation Arguments must include the EventListenerName Key in the KeyValuePair passed in to the arguments. Hint: If you hover over the Selection Changed Navigation Arguments. You receive some guidance on this (see below). The ButtonEventLister value (it could be called anything) we pass in to EventListenerName KeyValuePair will be explained in the next steps. Let's run the Custom Control Dashboard to see what it looks like, before we dive into the setup. So a simple button, that performs no actions. Since we haven't configured the Button component to perform an action, only to forward a Custom Control Event. Dashboard Button Action 1: Open Dialog Setup So the first example of using an Embedded Custom Control Dashboard, we want our button to perform the action of opening another Dashboard as a Dialog. So let's review that Dialog first before we review the Button itself (in the Custom Control Dashboard) in more detail. In this Dashboard is a Label and a Supplied Parameter (to handle the parameter handshake from Button to Label via Dialog). Let's look at the Label next. Here you can see that we pass in a parameter called |!prm_Label!| Now, let's review the button again. This time observe the Bound Parameter called prm_Button and the Parameter Value for Button Click. Also, please observe that here is where we pass in our Key Value Pair value ButtonEventListener that we saw earlier to the EventListenerName key e.g. EventListenerName=ButtonEventListener. So when do we get to open the Dialog? I hear you ask.... Well we needed some context before we step into the Embedded Dashboard setup. We start with the Dashboard we are launching using our Embedded Custom Control Dashboard template. Here we have added the Embedded version of the Dashboard that we have created. So far, pretty standard right? A Uniform dashboard with an Embedded Dashboard; albeit we don't normally add the Embedded version of a Dashboard to itself... But, let's now have a look at the Embedded Dashboard itself. Dashboard Button Action 1: Open Dialog Logic There are a couple of things to observe here before we get to the Collection. The Embedded Dashboard property is set to pick up our Custom Control Dashboard e.g. CustomControl_Button. Each fresh instance of the Custom Control Dashboard template must have a unique instance name e.g. CustomControl_Button1, CustomControl_Button2, etc. Now let's look at the Event Listeners Collection for this embedded Dashboard. Here we can see this is where we name our Event Listener e.g. ButtonEventListener. We also pass the value from our Button parameter (prm_Button) to our Label parameter (prm_Label) in the Map Bound Parameters property e.g. prm_Label=prm_Button. Remember these from earlier? Finally we set the Button component action to (a) Open Dialog and (b) to open the Dialog we had created earlier with our Label embedded in it e.g. dlg_CustomControl. This means that we are invoking the Button action in the Embedded Dashboard itself by using the Embedded Custom Control Dashboard and changing the action for this instance of the template. This means that when we open our new Dashboard (that uses this Embedded Dashboard), it creates a fresh instance of our Embedded Custom Control Dashboard, where the Button component now opens a Dialog based on our Event Listener logic e.g. Observe, The Label in the Dialog is now displaying the Parameter Value For Button Click value from the Button itself. Next we are going to look at another example of making this Custom Control Dashboard perform a different action. This time it will return a Message to the Dashboard via a Dashboard Extender Business Rule. Dashboard Button Action 2: Return Message to Dashboard Setup So to execute a Dashboard Extender (or Component Service), we first need an Assembly (mine is called Code) to be created in our Workspace (or we can use traditional Business Rules) and then some logic. You'll see how simple this example is below. All we are doing is executing a Dashboard Extender Business Rule to return a Message Box with a custom Message using the XFSelectionChangedTaskResult object e.g. "Event Listener Message Example" Dashboard Button Action 2: Return Message to Dashboard Logic Now we understand how to setup the Embedded Custom Control Dashboard, let's review our second example using the same Custom Control Dashboard template as before. Here we have a new Dashboard, again that is embedded to itself (the same setup as before), but this time we create a different instance of the Embedded Custom Control Dashboard. So let's look at the Embedded Dashboard, again we have set the Embedded Dashboard to look at our Custom Control Dashboard e.g. CustomControl_Button. But we have also given the Embedded Custom Control Dashboard a unique Instance Name e.g. CustomControl_Button2. Now, if we review the Event Listeners Collection in this example, we can see that we are using the same EventListenerName e.g. ButtonEventListener, but this time we have updated the Selection Changed Server Task property to Execute a Dashboard Extender Business Rule and then set the Selection Changed Server Task Arguments to execute our Dashboard Extender Business Rule and Function, which will return a Message Box Dialog to the Dashboard. So let's have a look at this in action. This time when we click the Button from our fresh instance of the same Custom Control Dashboard, it performs the action to return a Message Box that contains the Message from our Dashboard Extender Business Rule instead. So we can make a Component perform a completely different action for each instance by using the same Custom Control Dashboard as a template. Conclusion Now that you have a better understanding of some of these concepts, we can see a lot of new possibilities for reusing Dashboards and Components based on a templated approach and for varying use cases. Once you understand how to harness Custom Controls and combine these techniques with Dynamic Dashboards or even traditional Dashboard techniques, you will have the capability to make more strategic design decisions in your Workspace Development, that simply were not possible before Workspaces and hopefully more creativity and fun on your Dashboard development journey.888Views3likes3Comments