Showing posts with label form data source. Show all posts
Showing posts with label form data source. Show all posts

Monday, April 4, 2022

How to resolve reference with a form control value

If you need not just to maintain lookup for a refence group but also to validate a manually input value, you need to implement resolveReference method for the field of the form data source. Say, we want to validate a custom financial dimension value.





You can check resolveReference* methods in EcoResCategory table as a good example.


The most interesting detail for me is the way how the related form control value is found inside of the given reference group.

            /// <summary>
            /// Resolve reference
            /// </summary>
            /// <param name = "_formReferenceControl"></param>
            /// <returns></returns>
            public Common resolveReference(FormReferenceControl _formReferenceControl)
            {
                Common ret;
            
                ret = myAssignedBankAccountDimension.dimensionResolveReference(_formReferenceControl);
            
                return ret;
            }

    public Common dimensionResolveReference(FormReferenceControl _formReferenceControl)
    {
        DimensionAttribute                          dimensionAttribute;
        DimensionAttributeDirCategory               dimAttributeDirCategory;
        DimensionFinancialTag                       dimensionFinancialTag;
        myFinancialDimensionValueFinancialTagView  view;
        myAssignedBankAccountDimension             myAssignedBankAccountDimension;
        DimensionDisplayValue                       dimensionDisplayValue;

        if (!_formReferenceControl || _formReferenceControl.handle() != classNum(FormReferenceGroupControl) )
        {
            throw(error(strFmt("@SYS137393", Error::wrongUseOfFunction(funcName())) ));
        }

        dimensionDisplayValue = _formReferenceControl.filterValue(AbsoluteFieldBinding::construct(fieldStr(DimensionFinancialTag, Value), tableStr(DimensionFinancialTag))).value();
        dimensionDisplayValue = strLRTrim(dimensionDisplayValue);
        
        <..  implement your logic with the display value ...>

Thursday, March 17, 2022

Lookup, JumpRef, Modified for a form data source field through CoC

 As  explained in his old article https://community.dynamics.com/365/financeandoperations/b/ievgensaxblog/posts/d365foe-how-to-override-form-data-source-field-lookup-method, it is much better to override methods directly on a data source field than on its linked form controls.

I just want to re-iterate it and place here code snippets.


  • User can add new control using form personalization and this control won’t support overridden logic. It could be critical if you are restricting field lookup values or adding validations.
  • One form could have several controls that refers to one data source field so you have to duplicate your code.
  • Number of delegates are limited as well.
So, say we need to implement Lookup, JumpRef, and Modified methods for a custom field on CustInvoiceTable data source of CustFreeInvoice form. Implement these three aforementioned methods, e.g., directly in an extension to the form class.

 
[ExtensionOf(formStr(CustFreeInvoice))]
final class myCustFreeInvoice_Form_Extension
{
    public void myAssignedBankAccountIdModified(FormDataObject _targetField)
    {
       <logic>
    }
    public void myAssignedBankAccountIdJumpRef(FormDataObject _targetField)
    {
       <logic>
}
    // Different parameter here!
    public void myAssignedBankAccountIdLookup(FormStringControl _callingControl)
    {
       <logic>
    }

Now simply override them for the field when the data source is initialized.
 
public class myCustFreeInvoice_Form_EventHandler
{
    [FormDataSourceEventHandler(formDataSourceStr(CustFreeInvoice, CustInvoiceTable), FormDataSourceEventType::Initialized)]
    public static void myCustInvoiceTable_OnInitialized(FormDataSource _sender, FormDataSourceEventArgs _e)
    {
        FormRun         eogFormRun                  = _sender.formRun();
        FormDataObject  eogCustomField              = _sender.object(fieldNum(CustInvoiceTable, eogCustomField));
fdoEOGAssignedBankAccountId.registerOverrideMethod(methodStr(FormDataObject, jumpRef), formMethodStr(CustFreeInvoice, myAssignedBankAccountIdJumpRef), myFormRun); fdoEOGAssignedBankAccountId.registerOverrideMethod(methodStr(FormDataObject, lookup), formMethodStr(CustFreeInvoice, myAssignedBankAccountIdLookup), myFormRun); fdoEOGAssignedBankAccountId.registerOverrideMethod(methodStr(FormDataObject, modified), formMethodStr(CustFreeInvoice, myAssignedBankAccountIdModified), myFormRun); }

Wednesday, July 8, 2020

Event handler: Get access from FormDataSource argument to other data sources and form controls

Kind a code template to accelerate our job:


    [FormDataSourceEventHandler(formDataSourceStr(<FormName>, <FormDataSourceName>), FormDataSourceEventType::Activated)]
    public static void FormDataSourceName_OnActivated(FormDataSource _sender, FormDataSourceEventArgs _e)
    {
        <FormDataSourceTable>       formDataSourceTable                 = _sender.cursor();
        FormRun                     formRun                             = _sender.formRun();
        FormDataSource              anyFormDataSource_ds                = formRun.dataSource(formDataSourceStr(<FormName>, <AnyFormDataSourceName>)) as FormDataSource;
        <AnyFormDataSourceTable>    anyFormDataSourceTable              = anyFormDataSource_ds.cursor();
        FormControl                 anyFormControl                      = formRun.design(0).controlName('AnyFormControlName');
        
        // your logic goes here, for example
        if(formDataSourceTable.enabled())
        {
            anyFormControl.visible(false);
            anyFormControl.enabled(!anyFormDataSourceTable.RecId);
        }
    }

Tuesday, March 10, 2020

Data validation by ValidateField and ValidateWrite (old but good)

The most time data are meant to be changed by a user via different kinds of forms or even without it, and sometimes we need to implement additional business logic there.

Say, we want that a discount never be more than 10%, while a user creates lines in a sales order. For such a scenario the best way is to implement ValidateField and check the value for the field of Discount. After that the form does not allow the user to leave the field of discount until its value is less or equal to 10%.


public boolean validateField(FieldId _fieldIdToCheck)
{
    boolean ret;

    ret = super(_fieldIdToCheck);
    switch(_fieldIdToCheck)
    {
        case fieldNum(MySalesLine, Discount):
            if(this.Discount <= 10)
            {
               ret = true;
            }
            else
            {
                ret = checkFailed("Discount cannot be more than 10%");
            }
            break;
    }

    return ret;


But we should remember that ValidateField method is called automatically for a form data source field only.



In other words, if the table record is inserted or updated by code, this logic will be NOT triggered.



[Form]
[DataSource]
     [DataField]
            public boolean validate()
    [Table]
     public boolean validateField(FieldId _fieldIdToCheck)


So, if we need to implement validation logic for the whole record, which is supposed to be triggered every time Insert or Update is called (not for form data sources only), we go with ValidateWrite.






Thanks Mohamed for this brilliant article Microsoft dynamics ax2012 : forms and tables methods call sequences, How To? Watch this presentation! It contains much more very useful information.



Friday, June 14, 2019

How to enable a few fields on a form data source

/// <summary>
/// Helper for form data source functionality
/// </summary>
class FormDataSourceHelper
{
    /// <summary>
    /// Allow edit for given fields only; the rest is non-editable
    /// </summary>
    /// <param name = "_fds">Caller form data source</param>
    /// <param name = "_fields">Container with fields numbers for allowing</param>
    public static client void allowEditFields(FormDataSource _fds, container _fields)
    {
        DictTable                           dictTable;
        int                                 fieldCnt, fieldNumber;
        Set                                 fieldsSet;
        
        if(!_fds)
        {
            throw Error(Error::wrongUseOfFunction(funcName()));
        }

        dictTable = new DictTable(_fds.table());
        
        if(!dictTable)
        {
            throw Error(Error::wrongUseOfFunction(funcName()));
        }
        // everything is fine if we are here already

        // first convert a given container of fields numbers to a set of unique values
        fieldsSet = new Set(Types::Integer);
        for(fieldCnt = 1 ; fieldCnt <= conLen(_fields); fieldCnt++)
        {
            fieldsSet.add(conPeek(_fields, fieldCnt));
        }
        // disbale a field if it is not included in the set
        for(fieldCnt = 1 ; fieldCnt <= dictTable.fieldCnt(); fieldCnt++)
        {
            fieldNumber = dictTable.fieldCnt2Id(fieldCnt);
            if(_fds.object(fieldNumber) && !fieldsSet.in(fieldNumber))
            {
                _fds.object(fieldNumber).allowEdit(false);
            }
        }
    }

}

Tuesday, March 26, 2019

How to get number of rows loaded in a grid

In fact we can use numberOfRowsLoaded on a linked form data source; however, it shows the number of those cached only.

So, if you are editing row number 28, for example, and then call ExecuteQuery() on the data source, you will probably get a lower number of rows loaded in the grid yet, say, 19.

In order to get the right number, you should use the aforementioned method together with allRowsLoaded() one.

public int myNumberOfRowsLoaded()
{
    int myNumOfRec;
    int myCounter;

    myNumOfRec = PSAActivityEstimates_DS.numberOfRowsLoaded();
    // nothing to load yet
    if(!myNumOfRec)
    {
        return myNumOfRec;
    }
    // so far we numbered those lines cached only
    while(!PSAActivityEstimates_DS.allRowsLoaded())
    {
        // so let's move it on until the end of loading
        PSAActivityEstimates_DS.setPosition(myNumOfRec);
        PSAActivityEstimates_DS.getNext();
        // just to avoid never ending adventure
        if(myCounter>1000)
        {
            break;
        }
        myCounter++;
        // get the number for the next iteration
        myNumOfRec = PSAActivityEstimates_DS.numberOfRowsLoaded();
    }
    // now all numberOfRowsLoaded() calls will return the correct number of rows in grids
    myNumOfRec = PSAActivityEstimates_DS.numberOfRowsLoaded();

    return myNumOfRec;
}

Monday, February 25, 2019

TempDB table populated in CIL

We have multiple options to provide additional information to records in grids. Some of them, like display methods, are easy to implement. Some of them, like a temporary table, are not.

Let's consider a scenario when our lovely user wants to see reservation info directly in the transaction grid as filterable columns.






Evidently, we cannot use display methods because they cannot be filterable. Given the way this info calculated behind the curtain, there is no way to create a view to join InventTrans on InventTransItem form. So, the last feasible option is to create a temporary table and join it to the main form data source.

In order to populate this TempDB table (it is not enough to create an InMemory one) as much fast as possible, we have to run the logic in CIL.

The following shows how to work with such tables by passing their physical names.

First, create a TempDB table.





Then add it to the form data source so that it would be outer joined to InventTrans table





and place its Init() method, which links the local buffer with a populated TempDB on the SQL server side.

We also need to init its populating engine and calculate its data at least once when the form opens and maybe later via Refresh button for all or one particular item.

final class FormRun extends ObjectRun
{
    myInventSumFormEngine              myInventSumFormEngine;
}

void init()
{
    super();
    element.myInitParameters();
}

private void myInitParameters()
{
    myInventSumFormEngine               = new  myInventSumFormEngine(element);
}

//myTmpInventSum_DS.Init
public void init()
{
    myTmpInventSum                  myTmpInventSumLoc;
    myTmpInventSumLoc.doInsert();
    delete_from myTmpInventSumLoc;
    super();
    myTmpInventSum.linkPhysicalTableInstance(myTmpInventSumLoc);
}

public void run()
{
    super();
    element.myRecalcData();
}

public void myRecalcData(boolean _forCurrentItemOnly = false)
{
    myInvenTransByItemDimView  myInvenTransByItemDimView;

    if(_forCurrentItemOnly)
    {
        myInventSumFormEngine.parmItemIdCalcFor(InventTrans.ItemId);
        myInventSumFormEngine.run();
    }
    else
    {
        myInventSumFormEngine.parmItemIdCalcFor('');
        // todo: uncomment if needed to measure the execution time
        //myInventSumFormEngine.parmShowCalcInfo(true);
        startLengthyOperation();
        myInventSumFormEngine.run();
        endLengthyOperation();
    }
}

Now let's take a look at the populating class. It has parameters for showing elapsed time and run for a particular item id only.


/// <summary>
/// Populating TempDB tables in CIL
/// </summary>
/// <remarks>
/// Passing physical table names between CIL and form
/// </remarks>

public class myInventSumFormEngine
{
 // add more data sources to support multiple TempDB tables populating
 FormDataSource                      fdsTmpInventSum;
 myTmpInventSum                     myTmpInventSum;
 ItemId                              itemIdCalcFor;
 boolean                             showCalcInfo;
}

/// <summary>
/// Gets TempDB tables physical names
/// </summary>
/// <returns>
/// Container with names
/// </returns>
/// <remarks>
/// Creates buffers if not exist yet
/// </remarks>
private container getTempTablesNames()
{
 if(!myTmpInventSum.getPhysicalTableName())
 {
  select firstOnly myTmpInventSum;
 }
 return [myTmpInventSum.getPhysicalTableName()];
}

/// <summary>
/// Creates new populating engine for a given form
/// </summary>
/// <param name="_formRun">
/// Caller form
/// </param>
/// <remarks>
/// Looks for a particular data sources to link TempDB tables
/// </remarks>
public void new(FormRun _formRun = null)
{
 Object formObject = _formRun;

 if (_formRun)
 {
  fdsTmpInventSum      = _formRun.dataSource(tableId2Name(tableNum(myTmpInventSum)));
 }
}

/// <summary>
/// To calculate TempDB table for a particular item only
/// </summary>
/// <param name="_parm">
/// Item id to calculate data
/// </param>
/// <returns>
/// Item id
/// </returns>
/// <remarks>
/// If empty, then calculate for all items
/// </remarks>
public ItemId parmItemIdCalcFor(ItemId _parm = itemIdCalcFor)
{
 itemIdCalcFor = _parm;
 return itemIdCalcFor;
}

/// <summary>
/// Show consumed time to populate TempDB tables
/// </summary>
/// <param name="_parm">
/// Shows if True
/// </param>
public boolean parmShowCalcInfo(boolean _parm = showCalcInfo)
{
 showCalcInfo = _parm;
 return showCalcInfo;
}

public void run()
{
 str                     myTmpInventSumTempDBTableName;
 // add here more tempDB tables if needed
 [myTmpInventSumTempDBTableName] = this.getTempTablesNames();
 [myTmpInventSumTempDBTableName] = myInventSumFormEngine::calc([itemIdCalcFor, myTmpInventSumTempDBTableName, showCalcInfo]);
 // push calculated data back to the form data source
 if(fdsTmpInventSum)
 {
  // this assignment works only on the client tier outside of the populating method
  fdsTmpInventSum.cursor().linkPhysicalTableInstance(myTmpInventSum);
  fdsTmpInventSum.research();
 }
}

/// <summary>
/// Launcher for CIL
/// </summary>
/// <param name="_cont">
/// Container with parameters and all TempDB tables physical names to populate
/// </param>
/// <returns>
/// Container with all populated TempDB tables physical names
/// </returns>
private static server container calc(container _cont)
{
 container               cont;
 XppILExecutePermission  xppILExecutePermission;
 FromTime                startTime               = timeNow();
 ItemId                  itemIdCalcFor           = conPeek(_cont, 1);
 boolean                 showCalcInfo            = conPeek(_cont, 3);

 xppILExecutePermission = new XppILExecutePermission();
 xppILExecutePermission.assert();

 cont = runClassMethodIL(
         classStr(myInventSumFormEngine),
         staticMethodStr(myInventSumFormEngine, calcIL),
         _cont
        );
 CodeAccessPermission::revertAssert();

 if(showCalcInfo)
 {
  info(strFmt("Refreshed for %1 in %2",  itemIdCalcFor ? itemIdCalcFor : "all items", timeConsumed(startTime, timeNow())));
 }

 return cont;
}

/// <summary>
/// Implements the calculating logic and populating TempDB tables
/// </summary>
/// <param name="_con">
/// Container with parameters and all TempDB tables physical names to populate
/// </param>
/// <returns>
/// Container with all populated TempDB tables physical names
private static server container calcIL(container _con)
{
 WHSInventReserve                    whsInventReserve;
 myTmpInventSum                      myTmpInventSum;
 ItemId                              itemIdCalcFor                   = conPeek(_con, 1);
 str                                 myTmpInventSumTempDBTableName  = conPeek(_con, 2);
 myInvenTransByItemDimView           myInvenTransByItemDimView;
 // Link to an exiting table
 myTmpInventSum.useExistingTempDBTable(myTmpInventSumTempDBTableName);
 // empty existing data
 myInventSumFormEngine::cleanTmpInventSum(myTmpInventSum, itemIdCalcFor);
 /***** Implements the calculating logic here *****/
 while select whsInventReserve
  join RecId from myInvenTransByItemDimView
  where
   (!itemIdCalcFor || whsInventReserve.ItemId == itemIdCalcFor) &&
   myInvenTransByItemDimView.ItemId == whsInventReserve.ItemId &&
   myInvenTransByItemDimView.InventDimId == whsInventReserve.InventDimId
 {
  myTmpInventSum.clear();
  myTmpInventSum.Itemid                  = whsInventReserve.itemId;
  myTmpInventSum.InventDimId             = whsInventReserve.InventDimId;
  myTmpInventSum.AvailOrdered            = whsInventReserve.AvailOrdered;
  myTmpInventSum.AvailPhysical           = min(whsInventReserve.displayPhysAvailUpHierarchy(), whsInventReserve.AvailPhysical);
  // here you can add any other method calls to get more info about reservation
  myTmpInventSum.AvailReservDelta        = myTmpInventSum.AvailPhysical - myTmpInventSum.AvailOrdered;
  myTmpInventSum.insert();
 }
 // send tables physical names back to the form
 return [myTmpInventSum.getPhysicalTableName()];
}

/// <summary>
/// Empties the whole table or for given item id only
/// </summary>
/// <param name="_myTmpInventSum">
/// TempDB table buffer to empty
/// </param>
/// <param name="_itemIdCalcFor">
/// Item id
/// </param>
static private void cleanTmpInventSum(myTmpInventSum _myTmpInventSum, ItemId _itemIdCalcFor = '')
{
 delete_from _myTmpInventSum
 where
  (!_itemIdCalcFor || _myTmpInventSum.ItemId == _itemIdCalcFor);
}

As you can see the tricky point is to provide tables physical names to CIL and back in calc() and calcIL() methods.

Now you get it.




Do not forget to build CIL and check it in your Development user options.