New to Kinetic: Best Way to Add Custom Column to Panel Card Grid?

I’m trying to add the ExpirationDate column from the PartLot table to the Panel Card Grid entitled “On Hand - Bins” of the Part Maintenance Activity screen (see attached screenshot).

I’m still fairly new to the Epicor Kinetic UX and Application Studio, so I’m not sure whether I should modify the existing panel card or take a different approach.

Any guidance on how to accomplish this would be much appreciated!

I’ve done this many different ways, but I find this one the easiest

Thank you Michael! I will have a look at this.

I’m using the Edge debugger (see screenshot) and identified that the service being called is Erp.BO.PartBinSearch. Based on that, I’ve started creating a post-method directive for Erp.BO.PartBinSearch.GetFullBinSearch.

I’m still new to working with C# in directives, and after looking through the example code provided (thanks for that!), I realized I’m out of my depth. I’ve got basic C# knowledge, but haven’t had much hands-on experience with it in Epicor’s customization layer.

I may need to hit pause on this piece until I’m more confident, but if anyone can help clarify what this kind of directive typically looks like or offer pointers for how to approach modifying GetFullBinSearch, I’d really appreciate it.

Technical Breakdown / What I’m Trying to Do:

  • Goal: Add logic after GetFullBinSearch runs (e.g., modify results, filter output). Specifically, adding {PartLot.ExpirationDate} to the results.
  • Approach: Use a post-method directive in Application Studio.
  • Challenge:
    • Not sure how to structure the directive logic in the custom C# code widget.
    • Unsure whether GetFullBinSearch is the best method to intercept for this change.

If anyone has sample code snippets, conceptual guidance, or links to helpful documentation or examples, I’d love to learn more. Thanks in advance!

You are on the right track. A post processing directive on GetFullBinSearch will work.

Your code will look something like this:

foreach (var row in result.PartBinSearch)
{
    row["ExpirationDate"] = DateTime.Now; // Replace with Query to get actual expiration date
}

Once the BPM is enabled, you should see the field be returned in the dataset

Then you just bind the field up in app studio

I’ve been able to use this method to add columns when there is a Erp.BO… service to put the Method Directive on, such as in QuoteDtl and PartQty grids, but I’m having trouble with a few dashboards that don’t have Erp.BO services.

For example, Sales Order Backlog Status dashboard.
When I look in Chrome’s developer tools, the service that is being called is Ice.BO.KineticERP.ExecuteBAQ.

When I add a Method Directive to that service, I can put in a condition to trigger on queryID = “zSalesOrderBacklog”, and that successfully lets me pop up a ‘Yay, it worked!’ message when the dashboard loads. But - when I put in the custom code block to populate the new columns, I get stuck at:

foreach (var row in result.zSalesOrderBacklog)

I’ve tried, result.queryID, result.zSalesOrderBacklogSearch, result.zSalesOrderBacklogList, result.ExecuteBaq, result.ExecuteBaqSearch, etc., etc. and get the same error every time:

‘List’ does not contain a definition for ‘zSalesOrderBackLog’ and no accessible extension method ‘zSalesOrderBackLog’ accepting a first argument of type ‘List’ could be found (are you missing a using directive or an assembly reference?)

I’m no programmer, so please be kind - is what I’m trying to do possible in Epicor? Where am I going wrong?

Here’s the full custom code block as it is right now:

foreach (var row in result.zSalesOrderBackLog)
{
  var OrderData = Db.OrderDtl.Where( x=> x.Company==row.Company && x.OrderNum==row.OrderNum && x.OrderLine==row.OrderLine).FirstOrDefault();
  if (OrderData != null)
  {
    row["ProdGrp"]=OrderData.ProdCode;
  }   
}

and the error:

With a grid based on a BAQ you can just add the column to the BAQ and grid. The BPM workaround is for ootb landing pages that are not based on an editable query and the workaround has caveats that a BAQ edit doesn’t such as filtering and sorting on added columns isn’t available.

So add the column to zSalesOrderBackLog in BAQ designer is all you need to do.

My problem is that zSalesOrderBackLog is a “System” BAQ, so I can’t edit it. Is there any way to work around that?

You can copy it to a new BAQ then change BAQs on your dashboard layer

I guess that’s where I’m stuck then. I have no problem changing which BAQ to reference in a custom dashboard, but I tried doing that in this system dashboard and couldn’t get everything to match up (the filters and connected tables).
I think the Events in AppStudio are hidden from me because of that, so I might have to completely rebuild the dashboard as custom just to add 2 columns to the grid :frowning:

The dashboard is part of the CRM module: Erp.UI.SalesOrderBackLogSts

You can change the BAQ in the DataView like so:

Then add the column to the gridModel.columns

:sweat_smile: Thank you for pointing that out. Here I’ve been adding a new data view and trying to remap everything :face_with_hand_over_mouth:

Just a note that your methodology here to add rows in a post method directive on Ice.BO.KineticErp/ExecuteBaq would work, i’ve done it myself in some cases because its simpler than copying the baq and making a custom layer etc.

You need to use Late Binding vs. Early Binding - the format you showed is Early Binding, when the compiler is aware of the object class at compile time. Late binding is used when the object is not aware of the class at compile time. (ExecuteBaq works with a dynamic dataset - columns and datatypes differ for each run so no way for it to know ahead of time)

Late Binding format for a DataSet.DataTable is DataSet.Tables[“DataTable”]

foreach (var row in result.Tables["zSalesOrderBacklog"]) {
  row["bob"] = "is your uncle";
}

if it’s a calculated field based on other columns in the same row:

foreach (var row in result.Tables["zSalesOrderBacklog"]) {
  row["bob"] = row["othercolumn"] * row["onemorecolumn"];
}

Finally, the above will make an un-typed column, and Kinetic behaves a little better if you type the column first (if its not a string, which is default) - below strongly-types the column.

result.Tables["zSalesOrderBacklog"].Columns.Add(Ice.IceColumn.Create("AnInteger", typeof(Int)));
foreach (var row in result.Tables["zSalesOrderBacklog"]) {
  row["AnInteger"] = row["othercolumn"] * row["onemorecolumn"];
}

Wow, if this way could work I would much prefer it over changing the app layer!

When I try to use result.Tables[“zSalesOrderBacklog”] in my foreach, I get this error:

‘List’ does not contain a definition for ‘Tables’ and no accessible extension method ‘Tables’ accepting a first argument of type

Oh, the result object in that BO must be a “TableList”
try result[“zSalesOrderBacklog”]

I give up :white_flag: :sweat_smile:, I couldn’t get the code to work so app layer it is.

I booked time through EpicCare with one of Epicor’s principal technical consultants and just had a discovery meeting. I asked if there was a way to add columns using a method directive or function instead of replacing the grid, and they said no, no way :sob:.
Has anyone successfully added columns using a method directive on Ice.BO.KineticERP.ExecuteBAQ?

Yes, I can get the data in the call, but getting that column to show up in the grid seems impossible.

Well, only easily possible if the columns object is undefined in the layout.jsonc.

The below works 100%, needs a Pre/Post on ExecuteBAQ, and a Post on GetApp.

@Carol_Di this is all your fault, I hate it when Epicor says things can’t be done, so I had to do this one out of spite. Let the Prinicpal Consultant know that they can consult with me if they like :wink: Had I read your error more closely earlier, I would have seen Epicor told you it was a <List>DataSet, so that informs us how to read/write it.

image
image

ExecuteBaq Pre BPM:

if (queryID == "UBC_BAQ") {
    QueryExecutionTableset qets = executionParams[0];
    var r = qets.ExecutionSetting.FirstOrDefault(x => x.Name == "Select");
    if (r != null) {
        var cols = r.Value;
        var kept = cols
            .Split(',')
            .Select(s => s.Trim())
            .Where(s => !s.Equals("[bob]", StringComparison.OrdinalIgnoreCase))
            .ToArray();
        r.Value = string.Join(", ", kept);
    }
}

ExecuteBaq Post BPM:

if (queryID == "UBC_BAQ") {
    DataSet ds = result.FirstOrDefault();
    if (ds != null) {
        var t = ds.Tables["Results"];
        if (t != null) {
            t.Columns.Add(new DataColumn("bob", typeof(string)));
            foreach (DataRow r in t.Rows) {
                r["bob"] = "test";
            }
        }
    }
}

Tricky Tricky Tricky…
This Post BPM on Ice.Lib.MetaFX.GetApp will properly dynamically add the column to the gridModel.columns array on the first component (which is the first panel)

That works, but now since the BAQ is set up in providermodel, Epicor sends a call to that BAQ with SELECT … [bob], and it fails because bob doesn’t exist in the BAQ’s columns collection.

So I think the methodology gets even more dicey in that we need to inject a property-set on Form_OnLoad, which will dynamically add the column after the gridmodel is painted.

Edit: Nope, the Pre BPM to remove the column before hitting ExecuteBaq was easier.

Ice.MetaFX.GetApp Post BPM:

//reference Ice.Lib.Contracts.MetaFX
//reference Newtonsoft.Json
//using Epicor.MetaFX.Core.Models;
//using Newtonsoft.Json;
//using Newtonsoft.Json.Linq;
EpMetaFxRequest mfxreq = request;
ViewMetadataResponse mfxresp = (ViewMetadataResponse)result;

if (mfxreq.id == "Erp.UI.UBC_ChecksInfo") {
    JObject layout = JObject.Parse(mfxresp.Layout.ToString());
    JArray columns = (JArray)layout["components"]?[0]?["model"]?["gridModel"]?["columns"];
    
    if (columns != null) {
        var copycol = (JObject)columns.Last;
        var newcol = (JObject)copycol.DeepClone();
        newcol["field"] = "bob";
        newcol["title"] = "bob";
        newcol["hidden"] = false;
        
        columns.Add(newcol);
    }
    
    mfxresp.Layout = JObject.Parse(layout.ToString());
}

Result, added columns to Grid bound to ExecuteBaq on base layer, no layer customization needed, only BPM:


Base Layer:

I did the exact same pretty much, but the gris would never show data in my column.