Wednesday, November 25, 2009

From Image to Image: The best of both worlds

Ax offers the Image class for manipulation of bitmap images. This class has some nice features, like cropping, rotating, changing the image size, ...

On the other hand, we can use CLR Interop. That way, we can use the Windows builtin functions to manipulate images.

And maybe, you want a bit of both. In that case, you can start with the Image class in Ax, and pass the image along to your CLR object and modify it even further. You can do this by using following code:

static void Image2Image(Args _args)
{ Image AxImage;
System.Drawing.Image ClrImage;
System.IntPtr ImagePtr;
;
AxImage = new Image();
AxImage.loadImage(@'C:\MyBitmap.bmp');
ImagePtr= new System.IntPtr(AxImage.exportBitmap());
ClrImage = System.Drawing.Image::FromHbitmap(ImagePtr);
}

We use the ExportBitmap method on the Image class in Ax, to get a pointer to the Image object.
With this pointer, we instantiate the Image CLR object. No need to save the image into an intermediate file, or reload the data from file.

Enumerating with CLR Interop

Like seen in previous posts on this blog, with CLR Interop, a whole world of programming libraries are available to Ax.
But these come with little annoyances as well. Like when you try to use an enum in your programming library.
As an example, take a look at following code:

static void ClrEnum(Args _args)
{ System.Drawing.Image myImage;
System.Drawing.RotateFlipType myRotateFlipType;
;
myImage = System.Drawing.Image::FromFile(@'C:\MyBitmap.bmp');
myRotateFlipType=System.Drawing.RotateFlipType::Rotate90FlipNone();
myImage.RotateFlip(myRotateFlipType);
myImage.Save(@'C:\NewBitmap.bmp');
}

Everything looks OK. When typing in the code, IntelliSense even added the enumerations, but now if fails to compile. Saying

The class System.Drawing.RotateFlipType does not contain this function.

When working with CLR Interop, you might get this error from time to time. You check and doublecheck, there is no typing error.

So this wouldn't be much of a blog post if it didn't offer some workaround.

You can use the ClrInterop::parseClrEnum command.
With this command, you can convert a string value of an enumerated constant to an equivalent CLRObject instance.

Like this:

myRotateFlipType=ClrInterop::parseClrEnum('System.Drawing.RotateFlipType','Rotate90FlipNone');

Now that compiles! So now, enumerations in assemblies won't bother you again.

Monday, November 9, 2009

An alternative to the WinApi functions

The WinApi class holds some nice functions for file manipulation, like fe



WinApi::fileExists

WinApi::copyFile

WinApi::deleteFile

WinApi::moveFile



I think every programmer has used one of those functions in Ax at one time or another, as they can be really helpful.

They have one big drawback though: They need to be executed on the client tier. Trying to use these functions in batch processing (so on the AOS), will result in errors.

Now you might wanna have a look at the WinAPIServer class. This class holds similar functions as the WinApi class, but these methods run on the server. This is a good alternative when you need to do some file manipulation in batch mode, but overall functionality is a bit limited here. Of course, you are free to extend the WinAPIServer class with your own, desired methods.



Or you can take a shortcut and use the CLRInterop way. Use the System.IO.File and System.IO.Directory namespace.



Examples:



System.IO.File::Copy

System.IO.File::Delete

System.IO.File::Exists

System.IO.File::Move

System.IO.Directory::Exists

System.IO.Directory::CreateDirectory



Assigning your CLR Interop permissions, and you're good to go!

Error - Multiple calls of CodeAccessPermission.Assert

When dealing with asserting access permissions, a simple line of code may be sufficient. For example like this:

new FileIOPermission(myfilename, 'r').assert();

Above line may be used for assigning rights for reading a CSV file with Ax, with the ASCIIIO class.
But sometimes, this ain't enough and you need to assign more permissions. If you just combine statements like the one above, you may run into following error:

Multiple calls of CodeAccessPermission.Assert

Ax does not support multiple, successive calls to assert in the same calling code. You either have to use the revertAssert method (in between calls), or use assertMultiple.
An example of this last method:

Set permissionSet;
permissionSet = new Set(Types::Class);
permissionSet.add(new InteropPermission(InteropKind::ClrInterop));
permissionSet.add(new FileIOPermission(myfilename1, 'r'));
permissionSet.add(new FileIOPermission(myfilename2, 'rw'));
CodeAccessPermission::assertMultiple(permissionSet);

// do your thing

CodeAccessPermission::revertAssert();

Thursday, November 5, 2009

User 'UserName' is not authorized to update a record in table 'TableName'. Request denied.

You thought you were the administrator.
You thought it was your data.
You thought you could do with it what you want.

You thought wrong!

Even as an administrator, you may come across these kinds of error:

User 'UserName' is not authorized to insert a record in table 'TableName'. Request denied.
Cannot create a record in TableLabel (TableName). Access Denied: You do not have sufficient authorization to modify data in database.

Or, from real life:

User Admin is not authorized to update a record in table LedgerTrans. Request denied.
Cannot update a record in LedgerTrans. Access Denied: You do not have sufficient authorization to modify data in database.

The reason behind this, is a security measure. The AOS is guarding the access to the table data. This has been set up by using the AOSAuthorization property on the table.

And, is there a workaround available? Yes, there is.
But first of all: Beware! Changing or inserting data in tables that are protected like this should be done with extreme caution, as essential business logic is at risk.

The easiest thing to do is to change the AOSAuthorization property on the specified table. STOP! Don't do that, don't put your data at risk like that. Badly written code can easily mess up your data, and that's not something you want with a table like fe LedgerTrans. You only want to modify this data in a very controlled setup.

A better solution is to use the unchecked statement. With this statement, you can switch off any AOSAuthoratization setup on the table, only for the current code block.

Example:

LedgerTrans.skipTTSCheck(true);
select firstonly forupdate
LedgerTrans where LedgerTrans.RecId==123;

if(LedgerTrans.RecId)
{
LedgerTrans.Txt="Hello";
unchecked(Uncheck::TableSecurityPermission)
{
lLedgerTrans.doupdate();
}
}


All this should get you interested in Ax and it's security setup. A recommended reading for that can be found over on the Microsoft site.

Error - Client: Unexpected target (0)

When posting a packing slip in the Sales module, we got confronted with following error:

Client: Unexpected target (0)

The packing slip got posted however, only the printout failed.

For some users, everything went smooth and the packing slip got posted and printed. For others, it failed at printing. That's why we tought of a security issue involved, but testing with different security rights did not solve the problem.

When searching the web for a solution, we came across the blog of Palle Agermark, which featured a post about this particular error message. Unfortunately, no such luck for us, the problem persisted. (Excellent blog though, definitely recommended.)

Next focus for a solution: SysLastValue. Lots of time before when we ran into problems with reports, deleting the SysLastValue for the user solved the problem. Only in this case, we could not find an entry for the report SalesPackingSlip. Searching harder, we found an entry for SalesFormLetter_PackingSlip. And bingo: When we deleted the user data for SalesFormLetter_PackingSlip, our problem was solved.

Once again, deleting the appropriate values in SysLastValue (usage data) solved the problem.