Showing posts with label Extension. Show all posts
Showing posts with label Extension. Show all posts

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); }

Friday, February 11, 2022

Additional parameters in RunBaseBatch class extension

 Old good pack/unpack patterns with SysPackExtension call. Say, we need to augment RunBaseBatch based class <ClassName>.


/// <summary>
/// We are going to use a new additional parameter
/// </summary>
[ExtensionOf(classStr(<ClassName>))]
public final class My<ClassName>_Extension
{
    private boolean     myNewParm;   
    #define.CurrentVersion(1)
    #localmacro.CurrentList
        myNewParm
    #endmacro

    
    /// <summary>
    /// myNewParm access
    /// </summary>
    /// <param name = "_parm">boolean</param>
    /// <returns>boolean</returns>
    public boolean parmMyNewParm(boolean _parm = myNewParm)
{ myNewParm= _parm;
return myNewParm;
} /// <summary> /// Extends Pack /// </summary> /// <returns>container</returns> public container pack() { container packedClass = next pack(); return SysPackExtensions::appendExtension(packedClass, classStr(My<ClassName>_Extension), this.myPack());
} /// <summary> /// Extends Unpack /// </summary> /// <param name = "packedClass">container</param> /// <returns>boolean</returns> private boolean myUnpack(container packedClass) { Integer version = RunBase::getVersion(packedClass); switch (version) { case #CurrentVersion: [version, #currentList] = packedClass; break; default: return false; } return true; } /// <summary> /// Packs my locals /// </summary> /// <returns>container</returns> private container myPack() { return [#CurrentVersion, #CurrentList]; } /// <summary> /// Extends unpack /// </summary> /// <param name = "_packedClass">container</param> /// <returns>boolean</returns> public boolean unpack(container _packedClass) { boolean result = next unpack(_packedClass); if (result) { container myState = SysPackExtensions::findExtension(_packedClass, classStr(My<ClassName>_Extension));
//Also unpack the extension if (!this.myUnpack(myState)) { result = false; } } return result; } }

Thursday, February 10, 2022

Overload a method for a new button in Form extension

 This can be used as a copy-paste pattern, when you need to change a standard method for new form controls added as a form extensions.

At the form initialization step we can overload any form control methods with registerOverrideMethod.

[ExtensionOf(formStr(<FormName>))]
public final class my<FormName>_MyNewButton_Extension
{
   public void init()
    {
        next init();
		// MyNewButton button added in an extension to <FormName>
        FormButtonControl myButton = this.design().controlName(formControlStr(<FormName>, myNewButton));
		// Here we can overload its standard clicked() method in run-time		
        myButton.registerOverrideMethod(methodStr(FormButtonControl, clicked), formMethodStr(<FormName>, myNewButtonClicked), this);
    }

    public void myNewButtonClicked(FormButtonControl _sender)
    {
		<run some logic>
        _sender.clicked();
    }
}

Check this article for more complicated scenario https://alexvoy.blogspot.com/2018/09/lookup-and-modified-methods-for.html

Wednesday, April 22, 2020

New Image in FormGroupControl with BusinessCard Extended style

Developing and customizing forms in D365 are limited by predefined patterns and styles.

We can however overcome these limitations to some extent by placing new form controls and changing properties of existing ones and, of course, a bit of coding.

As an example, let's add a new Workflow image similar to Expense category to be shown in BusinessCard form group control, once an expense is assigned to the current user.



There are multiple ways to achieve the required change. To mention a few, playing with Style and ExtendedStyle properies in design, changing form controls placement with Top, Bottom, Left, Right properties, playing with DisplayOptions at run time, combining both images into one, replacing the standard images to customized ones: one for Assigned-to-me Category and standard Category, and so one.





Here we consider adding a new image of Workflow icon next to the standard Category one.We have a display method returning the required image. The key point here is to set its ExtendedStyle property to card_imageSquare, so that it would be shown properly.



Now it looks almost perfect, but the new form control pushed a bit the currency amount out of the card frame. 



Let's fix it by hiding the standard form control and placing its duplicate with ExtendedStyle = None.



The last thing is to make the text bold in order to emphasize it.


[ExtensionOf(tableStr(TrvExpTrans))]
final class TrvExpTrans_Extension
{
    boolean isCurrentUserWorkflow()
    {
        ...
    }

    display container currentUserWorkflowIndicator()
    {
        ImageReference imgRef;
        if (this.ApprovalStatus == TrvAppStatus::Pending && this.isCurrentUserWorkflow())
        {
            imgRef = ImageReference::constructForSymbol(ImageReferenceSymbol::Workflow);
            return imgRef.pack();
        }

        return conNull();
    }

    [FormDataSourceEventHandler(formDataSourceStr(TrvExpenses, TrvExpTrans), FormDataSourceEventType::DisplayOptionInitialize)]
    public static void ds_OnDisplayOptionInitialize(FormDataSource sender, FormDataSourceEventArgs e)
    {
        FormDataSourceDisplayOptionInitializeEventArgs  eventArgs   = e as FormDataSourceDisplayOptionInitializeEventArgs;

        FormDesign                                      fd          = sender.formRun().design(0);
        FormRowDisplayOption                            fo          = eventArgs.displayOption();
        FormControl                                     fc          = fd.controlName("newAmountCurrWithCurrencyCode");
        // if we can find our new form control for the expense amount
        if(fo && fc)
        {
            // let's make it bold to emphasize
            fo.affectedElementsByControl(fc.id());
            fo.fontBold(true);
        }
    }

}

The final view.


Wednesday, November 27, 2019

Enum extensions case in D365

Added two new values to an enum in the same model but in two different extensions.




The first one is looped perfectly (see code snippet below), and nothing but an index for the second. However, its value is present in a combobox.







[ExtensionOf(formStr(SysPolicyParameters))]
final public class mySysPolicyParametersForm_Extension
{
    public void populateTree()
    {
        DictEnum          policyRuleTypeEnum;
        int               i;
        policyRuleTypeEnum = new DictEnum(enumNum(SysPolicyRuleTypeEnum));


        for(i = 0; i < policyRuleTypeEnum.values(); i++)
        {
            str sym = policyRuleTypeEnum.value2Symbol(i);
            info(strFmt("%1 %2 %3", i, policyRuleTypeEnum.value2Name(i), sym));
        }


        next populateTree();
    }
} 
Already built and synchronized the whole world. What else can it be?

Take a look from the SQL side.




There are some old values that I created before but deleted later. All of them are still there.

I had to delete these non-synced values manually from SQL, then added needed values in AOT, and synched DB.

In fact, DB sync is triggered if you have some changes in tables/views only.
Now it is correctly recreated.






BTW there are two good articles about the subject

 1) Extensible enums: Breaking change for .NET libraries that you need to be aware of
 2) Development tutorial: Extensible base enumerations in Microsoft Dynamics AX 7

Wednesday, September 12, 2018

Lookup and Modified methods for FormReferenceGroup fields in D365

Let's say we need to keep in Purchase line a manufacturer code for a particular product. So each Product/Manufacturer code combination is unique.





Three tables are referenced via RecId fields.









So once Manufacturer code field is placed in the form, we end up with a FormReferenceGroup.





We can easily override its lookup method by subscribing to the relevant event on it.

 [FormControlEventHandler(formControlStr(PurchTable, myEcoResManufacturerProduct_myEcoResManufacturerProductRecId), FormControlEventType::Lookup)]
    public static void myEcoResManufacturerProduct_myEcoResManufacturerProductRecId_OnLookup(FormControl sender, FormControlEventArgs e)
    {
        PurchLine                           purchLine = sender.formRun().dataSource(formDataSourceStr(PurchTable, PurchLine)).cursor() as PurchLine;
        FormControlCancelableSuperEventArgs cancelableArgs = e as FormControlCancelableSuperEventArgs;
        
        myEcoResManufacturerProduct::lookupByItem(sender, purchLine.itemId);
        
        cancelableArgs.CancelSuperCall();
    }

public client static Common lookupByItem(FormReferenceControl _formReferenceControl, ItemId _itemId)
    {
        SysReferenceTableLookup sysReferenceTableLookup;
        Query                   query;
        QueryBuildDataSource    myEcoResMan;

        sysReferenceTableLookup = SysReferenceTableLookup::newParameters(tableNum(myEcoResManufacturerProduct), _formReferenceControl);
        sysReferenceTableLookup.addLookupfield(fieldNum(myEcoResManufacturerProduct, EcoResManufacturerRecId));
        sysReferenceTableLookup.addLookupfield(fieldNum(myEcoResManufacturerProduct, EcoResManufacturerPartNbr));

        query = new Query();
        myEcoResMan = query.addDataSource(tableNum(myEcoResManufacturerProduct));
        myEcoResMan.addRange(fieldNum(myEcoResManufacturerProduct, EcoResProductRecId)).value(SysQuery::value(InventTable::find(_itemId).Product));

        sysReferenceTableLookup.parmQuery(query);

        return sysReferenceTableLookup.performFormLookup() as myEcoResManufacturerProduct;
    }






But what if the user wants to create new values in appropriate tables if them do not exist yet?




We can catch the modified event in order to create new values before failing the validation.
However, given that its content may be changed, it is impossible to get access to its fields at design time.

We can do it during run-time by means of getting sought field form controls by their names and overloading then their Modified() methods. (see the similar trick for AX 2012 https://alexvoy.blogspot.com/2014/01/how-to-set-properties-for-reference.html)





The code you need to add.

[ExtensionOf(formStr(PurchTable))]
final class myPurchTableForm_PurchTableManuf_Extension
{
    private const  str          myFieldNameDisplayProductNumber    = 'EcoResManufacturerPartNbr';
    private const  str          myFieldNameEcoResManufacturerName  = 'EcoResManufacturerName';
    private FormStringControl   myFSCDisplayProductNumber;
    private FormStringControl   myFSCEcoResManufacturerName;
    
    // the only way to change the standard modified method for a control inside of a dynamically populated reference group
    // is to get it by its name looping all form controls of this group during run-time. then to overload it
    [FormEventHandler(formStr(PurchTable), FormEventType::Initialized)]
    public void PurchTable_OnInitialized(xFormRun sender, FormEventArgs e)
    {
        FormDesign                      formDesign = sender.design();
        FormReferenceGroupControl       formReferenceGroupControl;
        formReferenceGroupControl = formDesign.controlName(formControlStr(PurchTable, myEcoResManufacturerProduct_myEcoResManufacturerProductRecId)) as formReferenceGroupControl;
        this.registerManufacturerNameOverload(formReferenceGroupControl);
    }

    private void registerManufacturerNameOverload(FormReferenceGroupControl _formReferenceGroupControl )
    {
        int                                 i;
        Object                              childControl;
        FormStringControl                   formStringControl;

        for (i = 1; i <= _formReferenceGroupControl.controlCount(); i++) // FilterCategory is of FormReferenceGroupControl type
        {
            childControl = _formReferenceGroupControl.controlNum( i );
            formStringControl = childControl as formStringControl;
            if(formStringControl.DataFieldName() == myFieldNameEcoResManufacturerName)
            {
                myFSCEcoResManufacturerName = formStringControl;
                formStringControl.registerOverrideMethod(methodStr(formStringControl, modified), formMethodStr(PurchTable, myEcoResManufacturerName_modified_overload),
                    this);
            }
            if(formStringControl.DataFieldName() == myFieldNameDisplayProductNumber)
            {
                myFSCDisplayProductNumber = formStringControl;
                formStringControl.registerOverrideMethod(methodStr(formStringControl, modified), formMethodStr(PurchTable, myDisplayProductNumber_modified_overload),
                this);
            }
        }
    }

    /// <summary>
    /// We have to allow the user to insert any value, even though such a value is not found in the referenced table;
    /// then we will ask whether this new value must be created in the table and set this new value for the current record
    /// </summary>
    /// <param name = "_sender">EcoResManufacturerName</param>
    public void myEcoResManufacturerName_modified_overload(FormStringControl _sender)
    {
        myEcoResManufacturer myEcoResManufacturer = myEcoResManufacturer::findOrCreateByName(_sender.text());
        _sender.modified();
    }

    /// <summary>
    /// We have to allow the user to insert any value, even though such a value is not found in the referenced table;
    /// then we will ask whether this new value must be created in the table and set this new value for the current record
    /// </summary>
    /// <param name = "_sender">EcoResManufacturerName</param>
    public void myDisplayProductNumber_modified_overload(FormStringControl _sender)
    {
        Common          comm = _sender.dataSourceObject().cursor();
        PurchLine       purchLine   =  _sender.parentControl().dataSourceObject().cursor() as PurchLine;

        myEcoResManufacturerProduct myEcoResManufacturerProduct = myEcoResManufacturerProduct::findOrCreateEcoResManufacturerProduct(
                                                                                                        purchLine.itemId, 
                                                                                                        myFSCEcoResManufacturerName.text(),
                                                                                                        _sender.text());
        _sender.modified();
    }

}

Table methods

public static myEcoResManufacturer findOrCreateByName(myEcoResManufacturerName       _manufacturerName)
    {
        myEcoResManufacturer           myEcoResManufacturer = myEcoResManufacturer::findByName(_manufacturerName);
        if(!avrEcoResManufacturer)
        {
            if(Box::confirm("Do you want to creare new manufacturer?", strFmt(myEcoResManufacturer::txtNotExist(), _manufacturerName)))
            {
                try
                {
                    myEcoResManufacturer.EcoResManufacturerName = _manufacturerName;
                    if(myEcoResManufacturer.validateWrite())
                    {
                        myEcoResManufacturer.insert();
                    }
                }
                catch
                {
                    Error("Failed to create new manufacturer");
                }
            }
        }
        return myEcoResManufacturer;
    }


public static myEcoResManufacturerProduct findOrCreateEcoResManufacturerProduct(ItemId                         _itemId,
                                                                                    myEcoResManufacturerName       _manufacturerName,
                                                                                    myEcoResManufacturerPartNbr    _manufacturerPartNbr)
    {
        myEcoResManufacturerProduct    myEcoResManufacturerProduct;
        myEcoResManufacturer           myEcoResManufacturer;
        InventTable                     inventTable = InventTable::find(_itemId);
        // item and part number are given and exist
        if(inventTable.Product && _manufacturerPartNbr)
        {
            // such a manufacturer exists, so just try to find it for given combination
            myEcoResManufacturer           = myEcoResManufacturer::findOrCreateByName(_manufacturerName);
            myEcoResManufacturerProduct    = myEcoResManufacturerProduct::find(inventTable.Product, myEcoResManufacturer.RecId);
            if(!myEcoResManufacturerProduct)
            {
                if(Box::confirm("Do you want to create new part number", strFmt(myEcoResManufacturerProduct::txtNotExist(), _manufacturerPartNbr)))
                {
                    try
                    {
                        myEcoResManufacturerProduct.EcoResProductRecId         = inventTable.Product;
                        myEcoResManufacturerProduct.EcoResManufacturerRecId    = myEcoResManufacturer.RecId;
                        myEcoResManufacturerProduct.EcoResManufacturerPartNbr  = _manufacturerPartNbr;
                        myEcoResManufacturerProduct.EcoResManufacturerDefault  = NoYes::Yes;
                        if(myEcoResManufacturerProduct.validateWrite())
                        {
                            myEcoResManufacturerProduct.insert();
                        }
                    }
                    catch
                    {
                        Error("Failed to create new part number");
                    }
                }
            }
        }
        return myEcoResManufacturerProduct;
    }