Tuesday, December 11, 2012

When good habits matter

Ever wondered why

if (x == 1)
{
    y = 2;
}
 

is better than

if (x == 1)
    y = 2;

?

Find a bug in Dynamics AX 2012 RTM then:

Friday, November 30, 2012

Value cannot be null. Parameter name: x

Don't get confused, when trying to find where this error message was thrown: "Value cannot be null. Parameter name: x". Especially, when this is something thrown during AIF processing.

You might suggest that this is a label and you do find the label @SYS91439, but still you have no idea what is going on, e.g. no breakpoints with this label are hit.

So, it may well be something thrown by an arithmetic operation, which was passed a null value.


Another good reason to give meaningful names to variables and check for null values.

Sunday, November 4, 2012

Computed columns in Views by example

There is a bug in the standard AX, in the query named AxdPurchaseRequisition. The problem is here:


PurchLineAllVersions datasource is based on a View object, so PurchLineAllVersions.TableId contains table ID of the view itself, and not that of the underlying tables. As a result, no DocuRefTrans records will be found with this quiry.

The issue may be fixed by using computed columns in views - the new feature in AX 2012.

As you may already know, the PurchLineAllVersions view is based on the quiry PurchLineAllVersions, which, in turn, is a union of 2 other views: PurchLineArchivedVersions and PurchLineNotArchivedVersions.

First, add a method to the 2 views.

PurchLineNotArchivedVersions:

public static server str purchLineTableId()
{
    return SysComputedColumn::returnLiteral(tableNum(PurchLine));
} 

PurchLineArchivedVersions:

public static server str purchLineTableId()
{
    return SysComputedColumn::returnLiteral(tableNum(PurchLineHistory));
}

Add a column to the 2 views: right-click on the Fields, then "New -> Int Computed Column". Set the new fields' properties like that:


Then, add new PurchLineTableId fields to the PurchLineAllVersions query datasources and the PurchLineAllVersions view.

Finally, change the faulty link in the AxdPurchaseRequisition quiry, so that it will connect PurchLineAllVersions.PurchLineTableId and DocuRefTrans.RefTableId.

By the way, there is another, better example of how computed columns may be used: take a look at the InventValue* views, which are used by the Inventory Value report.

Thursday, November 1, 2012

xRecord.setTmp() and inactive configuration keys

Be carefull when using setTmp method on table buffers. If your table is assigned a configuration key, turning it off will make this line of code throw an error at runtime: "Table '<table name>' of type TempDB cannot be changed to table of type InMemory".

For example, \Classes\SysRecordTemplateEdit\setFormDataSourceRecord has this problem. It loops through form datasources and tries to set them all to InMemory withouth checking first if they are TempDB or not.

In order to avoid this error, you can use the following construction:

    if (!myTable.isTempDb())
    {
        myTable.setTmp();
    }

More about TempDB tables on MSDN.

Exporting to Excel with Microsoft.Dynamics.AX.Fim.Spreadsheets.* classes

While looking for a way to export to Excel in batch, I investigated what they do in financial statements (LedgerBalanceSheetDimPrintExcelEngine class).

Pros:
  • It is possible to use these .NET components in batch on the server side
  • Execution is way faster than that of the SysExcel* classes

Cons:
  • It looks like it is not possible to create more columns than there are letters in the English alphabet
  • Less flexible comparing to standard SysExcel classes, so manual formatting will likely be needed in the end. 
static void AnotherWayToExportToExcel(Args _args)
{
    #define.ReadWritePermission('RW')
    #define.FileName('c:\myFile.xlsx')
    #define.ExcelColumnWidth(15)
    #define.ExcelCellFontSize("Microsoft.Dynamics.AX.Fim.Spreadsheets.CellFontSize")
    #define.Size9("Size9")    
 
    CustTable custTable;
 
    Microsoft.Dynamics.AX.Fim.Spreadsheets.Spreadsheet spreadsheet;
    Microsoft.Dynamics.AX.Fim.Spreadsheets.CellProperties cellProperties;    
    Microsoft.Dynamics.AX.Fim.Spreadsheets.ColumnProperties columnProperties;    
 
    void addColumn(str _name)
    {
        columnProperties = new Microsoft.Dynamics.AX.Fim.Spreadsheets.ColumnProperties();
        columnProperties.set_Width(#ExcelColumnWidth);
        spreadSheet.InstantiateColumn(columnProperties);
 
        cellProperties = new Microsoft.Dynamics.AX.Fim.Spreadsheets.CellProperties();
        cellProperties.set_FontSize(CLRInterop::parseClrEnum(#ExcelCellFontSize, #Size9));
        cellProperties.set_Bold(true);
 
        spreadSheet.AddStringCellToWorkbook(_name, cellProperties);
    }    
 
    new FileIOPermission(#FileName, #ReadWritePermission).assert();
 
    spreadSheet = new Microsoft.Dynamics.AX.Fim.Spreadsheets.Spreadsheet();
 
    if (!spreadSheet.CreateSpreadsheet(#FileName))
    {
        throw error(strFmt("@SYS72245", #FileName));
    }
 
    addColumn("Customer name");
    addColumn("Balance");    
 
    while select custTable
    {
        spreadSheet.MoveToNextRowInWorkbook();        
 
        cellProperties = new Microsoft.Dynamics.AX.Fim.Spreadsheets.CellProperties();
        cellProperties.set_FontSize(CLRInterop::parseClrEnum(#ExcelCellFontSize, #Size9));        
 
        spreadSheet.AddStringCellToWorkbook(custTable.name(), cellProperties);            
        spreadSheet.AddNumberCellToWorkbook(real2double(custTable.openBalanceCur()), cellProperties);                    
    }
 
    spreadSheet.WriteFile();
    spreadSheet.Dispose();
 
    CodeAccessPermission::revertAssert();    
}

Output:

Wednesday, October 17, 2012

Manupulating forms from code - continued...

After publishing a post about manipulating the Marking form from code, I got some feedback (special thanks to Maxim Gorbunov).

I would like my readers to remember, that forms should never be used like that in business logic executed on a daily basis. If this is something that should be run more often than once, this logic should preferably be run on the server, and in the case with the Marking form, it should rather be TmpInventTransMark::updateTmpMark() method called directly. You will need to prepare parameters for it, though, so you will first need to figure out how the form does that.

In other words, please use out-of-the-box solutions you find on the Internet with care.

Thursday, October 11, 2012

Manipulating the Marking form from code

Businesses buy AX, because they would like to automate their processes. Sometimes they would like to automate more, than standard AX allows. And if business logic is built into forms, it is sort of hard to automate.

One of our clients had to migrate open sales and purchase orders to AX, and mark their lines against each other afterwards. Apparently, calling InventTransOrigin::updateMarking method was not enough, as sales and purchase lines were still not pointing to each other in the reference fields. So, I tried to use the standard Marking form for that ad-hoc task.

Below is the code sample that takes a purchase line lot ID, opens the Marking form, selects the target sales order line and marks it (or unmarks, if it was marked before). Just like you do it by hand.

static void main(Args _args)
{
    #define.TmpInventTransMarkDsNo(2)
    #define.InventTransOriginDsNo(4)
    #define.InventTransOriginMarkDsNo(5)
    #define.MarkNowControlName('markNow')
 
    #define.PurchLineLotId("GSC-000983")
    #define.SalesLineLotId("GSC-000982")
 
    FormRun formRun;
    Args args;
    TmpInventTransMark tmpInventTransMark;
    SalesLine salesLine;
    InventTransOrigin salesLineOrigin;
    PurchLine purchLine;
    InventTransOrigin inventTransOriginMark;
    FormCheckBoxControl markNowCheckBox;
 
    purchLine = PurchLine::findInventTransId(#PurchLineLotId);
 
    args = new Args();
    args.name(formStr(InventMarking));
    args.record(purchLine);
 
    // Work-around: first if-statement in
    // Classes\ReqCalc\argsItemId method expects a caller
    args.caller(new InventMarkingEmulator());
 
    formRun = classfactory.formRunClass(args);
    formRun.init();
    formRun.run();
 
    // The temp table is filled in when this active() is called
    formRun.dataSource(#InventTransOriginDsNo).active();
 
    tmpInventTransMark = 
        formRun.dataSource(#TmpInventTransMarkDsNo).cursor();
    inventTransOriginMark = 
        formRun.dataSource(#InventTransOriginMarkDsNo).cursor();
 
    ttsbegin;
 
    // Find the required reference and mark it    
    formRun.dataSource(#TmpInventTransMarkDsNo).first();
    Debug::assert(tmpInventTransMark.RecId != 0);
    do
    {
        if (inventTransOriginMark.InventTransId == #SalesLineLotId)
        {
            markNowCheckBox = 
                formRun.design().controlName(#MarkNowControlName);
            markNowCheckBox.value(1);
            markNowCheckBox.clicked();
 
            formRun.closeOk();
            break;
        }
    } while (formRun.dataSource(#TmpInventTransMarkDsNo).next());
 
    ttscommit;
}

Monday, September 10, 2012

Figuring out where some table field is modified

Sometimes I need to find out where a particular table field is modified in the X++ code. Normally, I call "Used by" form,  filter records out by Reference = Write, put manually breakpoints in the corresponding X++ lines and then run the scenario to see which breakpoint is eventually hit.

However, if there are too many cross-references, I don't bother adding breakpoints manually. Instead, I add a button to xRefReferencesUsedByTypedTree form, set its Label property to "Add breakpoint" and MultiSelect to "Yes", and then override its clicked method like this:

void clicked()
{
    container breakpoints;
    boolean enable = true;
 
    xRefReferences xRefReferencesLocal;
 
    breakpoints = infolog.breakpoint();
 
    for (xRefReferencesLocal = XRefReferences_ds.getFirst(true) ?
        XRefReferences_ds.getFirst(true) : XRefReferences_ds.cursor();
        xRefReferencesLocal;
        xRefReferencesLocal = XRefReferences_ds.getNext())
    {
        if (xRefReferencesLocal.line > 0)
        {
            breakpoints += [xRefReferencesLocal.path()];
            breakpoints += [xRefReferencesLocal.line];
            breakpoints += [enable];
        }
    }
 
    infolog.breakpoint(breakpoints);
}

 Now, after opening "Used by" form, I simply click "Ctrl+A" and then the new "Add breakpoint" button, so all required breakpoints are in place.


Friday, September 7, 2012

How one new table relation may break your code somewhere

Yesterday I spent some time figuring out why intercompany functionality on my box stopped working after upgrade to CU3. No errors were thrown - it just didn't create IC chains, although the setup was correct. There were no recent changes in the TFS history that would help understand why that happened.

Whyle debugging, I found that the quiry used to fetch sales lines was broken in some method. In the standard, it linked SalesLine and InventTable by using QueryBuildDataSource.relations() method, passing true as a parameter. In the customized version of SalesLine, another relation to InventTable was added, linking a new SalesLine field with InventTable.ItemId field. So, the relations method desided that this is the new relation that should be used when building a query for IC chain creation.

It was not an issue before the upgrade, as the old relation was still selected properly. But somehow the upgrade updated IDs or whatever that determined in which priority the relations should be used by relations method.

So, beware of that effect next time you add a table relation.

Wednesday, September 5, 2012

ID change in Dynamics AX data dictionary

The other day we upgraded to Cumulative Update 3 for Dynamics AX 2012. After that we got some problems in the SqlDictionary table - several table and field IDs did not much those in the AOT anymore.

One of our developers found this post, which contained a job fixing such issues. We had to correct the job a bit, otherwise it failed when trying to process Views or update field IDs that had been "swapped" during upgrade (e.g. before: fieldId1 = 6001, fieldId2 = 6002; after installing CU3: fieldId1 = 6002, fieldId2 = 6001).

This is the final version of the job. I know the change violates DRY principle, but for an ad-hoc job it is probably OK :)

static void fixTableAndFieldIds(Args _args)
{
    Dictionary dictionary = new Dictionary();
    SysDictTable dictTable;
    DictField dictField;
    TableId tableId;
    FieldId fieldId;
    SqlDictionary sqlDictionaryTable;
    SqlDictionary sqlDictionaryField;
 
    setPrefix("Update of data dictionary IDs");
    tableId = dictionary.tableNext(0);
    ttsbegin;
 
    while (tableId)
    {
        dictTable = new SysDictTable(tableId);
 
        setPrefix(dictTable.name());
 
        if (!dictTable.isSystemTable() && !dictTable.isView())
        {
            //Finds table in SqlDictionary by name in AOT, if ID was changed.
            //Empty field ID represents a table.
            select sqlDictionaryTable
                where sqlDictionaryTable.name == dictTable.name()
                && sqlDictionaryTable.fieldId == 0
                && sqlDictionaryTable.tabId != dictTable.id();
 
            if (sqlDictionaryTable)
            {
                info(dictTable.name());
                //Updates table ID in SqlDictionary
                if (ReleaseUpdateDB::changeTableId(
                    sqlDictionaryTable.tabId,
                    dictTable.id(),
                    dictTable.name()))
                {
                    info(strFmt("Table ID changed (%1 -> %2)", sqlDictionaryTable.tabId, dictTable.id()));
                }
            }
 
            fieldId = dictTable.fieldNext(0);
 
            //For all fields in table
            while (fieldId)
            {
                dictField = dictTable.fieldObject(fieldId);
 
                if (!dictField.isSystem())
                {
                    //Finds fields in SqlDictionary by name and compares IDs
                    select sqlDictionaryField
                        where sqlDictionaryField.tabId == dictTable.id()
                        && sqlDictionaryField.name == dictField.name()
                        && sqlDictionaryField.fieldId != 0
                        && sqlDictionaryField.fieldId != dictField.id();
 
                    if (sqlDictionaryField)
                    {
                        //Updates field ID in SqlDictionary
                        if (ReleaseUpdateDB::changeFieldId(
                            dictTable.id(),
                            sqlDictionaryField.fieldId,
                            -dictField.id(),
                            dictTable.name(),
                            dictField.name()))
                        {
                            info(strFmt("Pre-update: Field %1 - ID changed (%2 -> %3)",
                                dictField.name(),
                                sqlDictionaryField.fieldId,
                                -dictField.id()));
                        }
                    }
                }
                fieldId = dictTable.fieldNext(fieldId);
            }
 
            fieldId = dictTable.fieldNext(0);
 
            //For all fields in table
            while (fieldId)
            {
                dictField = dictTable.fieldObject(fieldId);
 
                if (!dictField.isSystem())
                {
                    select sqlDictionaryField
                        where sqlDictionaryField.tabId == dictTable.id()
                        && sqlDictionaryField.name == dictField.name()
                        && sqlDictionaryField.fieldId < 0;
 
                    if (sqlDictionaryField)
                    {
                        //Updates field ID in SqlDictionary
                        if (ReleaseUpdateDB::changeFieldId(
                            dictTable.id(),
                            sqlDictionaryField.fieldId,
                            -sqlDictionaryField.fieldId,
                            dictTable.name(),
                            dictField.name()))
                        {
                            info(strFmt("Final update: Field %1 - ID changed (%2 -> %3)",
                                dictField.name(),
                                sqlDictionaryField.fieldId,
                                -sqlDictionaryField.fieldId));
                        }
                    }
                }
                fieldId = dictTable.fieldNext(fieldId);
            }
        }
        tableId = dictionary.tableNext(tableId);
    }
    ttscommit;
}

Friday, August 10, 2012

Nice X++ syntax highlighter

Thanks to Lemming from AxForum.info for the link:

http://highlight.hohli.com/

The validateWrite method sample in my previous post was formatted via this service.

Thursday, August 9, 2012

ok = ok ? ok : !ok

While working in CUS layer, I keep finding validate* methods that basically look like this:

public boolean validateWrite()
{
    boolean ok = true;
 
    ok = this.validateSomething() && ok;
    ok = this.validateSomethingElse() && ok;
    ...
    // johndoe 11.11.11 ->
    ok = this.validateWrittenByJohnDoe();
    // johndoe 11.11.11 <- 
 
    return ok;
}


As you can see, John forgot something: should validateWrittenByJohnDoe return true, the whole validateWrite would succeed, even if both validateSomething and validateSomethingElse failed. That may result in corrupted data.

There is only one case when skipping "&& ok" in the chain of validations is valid - when you explicitly set boolean variable to false:

    ok = checkFailed("@CUS1111");

Otherwise, either place an "ok" in front of your check, so that it would be skipped, if the result were false anyway:

    ok = ok && this.validateWrittenByJohnDoe();

or place it in the end, so all possible errors will be found and displayed in the infolog:

    ok = this.validateWrittenByJohnDoe() && ok;

Keeping all this in mind is especially important in AX 2012, where eventing is possible, as faulty event handlers on validate-method may lead to quite sneaky bugs.

Thursday, July 26, 2012

Parm-methods generator script

It may be boring to manualy create parm-methods for each an every class member variable, although there is an editor script for that. By the way, there is a bug-o-feature in AX 2012 editor, which does not allow you to select anything but an EDT for the variable type.

This is a script that auto-generates parm-methods for a class, based on its class declaration:
static void createParmMethod(Args _args)
{
    #AOT
   
    ClassName className = classStr(MyClass);  // <---------------- Write your class name here
   
    TreeNode classDeclarationTreeNode;
    TreeNode classTreeNode;
    TreeNode parmMethodNode;
   
    Source classDeclaration;
   
    System.Text.RegularExpressions.MatchCollection mcVariables;
    System.Text.RegularExpressions.Match mVariable;
    int matchCount;
    int matchIdx;
   
    System.Text.RegularExpressions.GroupCollection gcVariableDeclaration;
    System.Text.RegularExpressions.Group gVariableDeclarationPart;
   
    str variableType;
    str variableName;
   
    str pattern = ' (?<VarType>[a-zA-Z0-9_]+)[ ]+(?<VarName>[a-zA-Z0-9_]+);';   
   
    Source parmMethodBody;
   
    classTreeNode = TreeNode::findNode(strFmt(@"%1\%2", #ClassesPath, className));
   
    classDeclarationTreeNode = TreeNode::findNode(
        strFmt(@"%1\%2\ClassDeclaration",
        #ClassesPath,
        className));
   
    classDeclaration = classDeclarationTreeNode.AOTgetSource();
   
    mcVariables = System.Text.RegularExpressions.Regex::Matches(
        classDeclaration,
        pattern,
        System.Text.RegularExpressions.RegexOptions::Singleline);

    matchCount = CLRInterop::getAnyTypeForObject(mcVariables.get_Count());   
   
    for (matchIdx = 0; matchIdx < matchCount; matchIdx++)
    {
        mVariable = mcVariables.get_Item(matchIdx);
        gcVariableDeclaration = mVariable.get_Groups();
       
        gVariableDeclarationPart = gcVariableDeclaration.get_Item('VarType');
        variableType = gVariableDeclarationPart.get_Value();
       
        gVariableDeclarationPart = gcVariableDeclaration.get_Item('VarName');
        variableName = gVariableDeclarationPart.get_Value();
       
        parmMethodBody = new xppSource().parmMethod(variableType, variableName);
       
        parmMethodNode = classTreeNode.AOTadd('method1');
        parmMethodNode.AOTsetSource(parmMethodBody);
        classTreeNode.AOTsave();
    }
   
    classTreeNode.AOTcompile();
}

Tuesday, July 24, 2012

Regex looking for return calls within ttsbegin/ttscommit

Return calls should never appear within a ttsbegin/ttscommit pair. If they do, the application may eventually complain that


I have recently had to look for such an issue in a third-party code, and this is the job I used to looks for suspecious "returns", which did find one. Please note, that with the current regex pattern, there may be false positives, but in my case it would take more time to write a perfect pattern, than to manually look through those false positives:

 static void checkSourceForReturnsInTTS(Args _args)
{
    TreeNode treeNode;
    TreeNode sourceTreeNode;   
    TreeNodeIterator it;
    ProjectNode projectNode;
    TreeNodeTraverserSource traverser;
    Source source;
   
    System.Text.RegularExpressions.MatchCollection mcReturnsInTTS;  
    int matchCount;
    int matchIdx;   
   
    str pattern = 'ttsbegin.*[^a-z0-9_]return[^a-z0-9_].*ttscommit';
    str matchString;
   
    projectNode = SysTreeNode::getPrivateProject().AOTfindChild("MyProject");
    treeNode = projectNode.loadForInspection();
  
    traverser = new TreeNodeTraverserSource(treeNode);
    while (traverser.next())
    {
        sourceTreeNode = traverser.currentNode();
       
        source = sourceTreeNode.AOTgetSource();
        source = System.Text.RegularExpressions.Regex::Replace(
            source,
            '[/][*].*[*][/]',
            '',
            System.Text.RegularExpressions.RegexOptions::Singleline);
        source = System.Text.RegularExpressions.Regex::Replace(
            source,
            '[/]{2,}.*\n',
            '');       
       
        mcReturnsInTTS = System.Text.RegularExpressions.Regex::Matches(
            strLwr(source),
            pattern,
            System.Text.RegularExpressions.RegexOptions::Singleline);
       
        matchCount = CLRInterop::getAnyTypeForObject(mcReturnsInTTS.get_Count());
        if (matchCount > 0)
        {
            info(sourceTreeNode.treeNodePath());
        }
    }   
}

Wednesday, July 18, 2012

The X++ debugger does not open...

... although you turned off "Execute business operations in CIL" in the "Tools > Options".

Then there is probably a runAs(...) method somewhere down the call stack.

You can find such a runAs-call by setting a breakpoint in Visual Studio, attaching to the process and running your logic again. After the runAs-call is found, you can simply go replace it with a direct static method call.

For example:


After this change, the X++ debugger will stop where needed.

Remember - this change is only for debugging purpose. You should not do that in the production code.

Monday, July 16, 2012

If you forgot to fix DataSource property on a grid,...

... although its child controls are set up properly, then your form may behave in a weird way.

For example, we have a form with 2 linked datasources (SalesTable and SalesLine) and 2 grids accordingly:


The second grid, SalesLineGrid, has DataSource property set to SalesTable. That value was set by default at the moment the grid control was created. You can also see 3 controls in the SalesLineGrid, but their DataSource properties are set to SalesLine, so they are fine.

Now, if we open the form and switch between sales orders back and forth, the bottom grid with sales order lines will not work properly:


As you can see, the cursor position in the second grid changes in sync with that of the first grid, there are a lot of "empty" sales order lines, and, finally, the rendered sales order lines are actually wrong.

Going back to the form in the AOT and setting the SaleLineGrid datasource property to SalesLine will fix the issue:

Wednesday, July 4, 2012

AIF: Mind auto-generated Axd<DocumentName>.findCorrespondingAxBC method

Problem

You have just created a document service with the wizard. You need the document to contain an unbound value. So, you create a display method on one of the tables, and add a corresponding parm-method to Ax<TableName> class. Then you refresh the services, but the new field is not presented in the schema.

Solution

Check the new Axd<DocumentName> class. By default, it may have findCorrespondingAxBC method, overriding the base class method and always returning classnum(AxCommon). This is why your new parm-method in the Ax<TableName> class is ignored.

P. S. There are actually TODOs added to the auto-generated methods. It may be a good idea to clean up the TODOs first, and then continue with the service development.

Wednesday, June 27, 2012

What would happen to my event handlers, if the sys-layer method disappeared?

The other day I explained to my colleagues, what the benefits of events and models, introduced in AX 2012, were.

That time, I was asked a question "What would happen to my event handlers, if the method they are attached to in the AOT was removed in the next release?" I could not give a precise answer, but I was sure that nothing terrible would happen in that case.

Let's check what would actually happen. First, let's create 2 models, Bands and Fans:


Now, let's add class TheBeatles to the Bands model, and class Fans to the Fans model. On top of that, we will create two event handers in the Fans model, so Fans would shout whenever TheBeatles gave a concert, and buy albums whenever TheBeatles recorded one:



More details:





If you run TheBeatles::main method now, the infolog will look like this:


Let's export the Fans model to save the original version of the Fans class:


OK. Let's assume now that TheBeatles stopped giving concerts (hell, no...). We have to delete the giveConcert method from TheBeatles class:


As we can see, the fansShoutEventHandler is gone, too. More than that, it does not exist in the model either:


But, we have our exported Fans model. Let's import it back and see what will happen:


The import failed, as the model file referenced a non-existing element, giveConcert method. Let's run the same import command, but with "/createparents" parameter, as it is proposed in the error message:


As we can see, a virtual model has been created to help the Fans model get imported properly. In the AOT, TheBeatles class looks like this now:


giveConcert method is there again, now in the "Fans (Virtual 1)" model.

In the end, we can decide what to do with the fansShoutEventHandler - remove it at all, move it somewhere else or maybe refactor.

For more information on how the events and models can save you time, I would recommend this blog.

Monday, April 30, 2012

Welcome!

Hi all,

I am about to leave Microsoft, and my old MSDN blog will be frozen.

Whenever I find something interesting on my way through the Dynamics AX land, I will let you know.

Keep in touch,

Sasha (Oleksandr) Nazarov