FoxPro and .NET Interop
wwDotNetBridge -- A .NET object I cannot manipulate...
Gravatar is a globally recognized avatar based on your email address. wwDotNetBridge -- A .NET object I cannot manipulate...
  Joe Kaufman
  All
  Nov 6, 2018 @ 11:25am

Hello,

I am trying to perform interops with a tool called DocuWare. I have gotten things to work from .NET, and also from Visual Foxpro utilizing the XMLHTTP Windows COM object.

After seeing what wwDotNetBridge can do, I tried connecting to the DocuWare server using the DocuWare .NET assemblies from their NuGet packages. I was able to connect and get a "ServiceConnection" object! So, time to implement a basic operation -- downloading a document from a file cabinet (DocuWare is a document management system).

I was able to successfully complete an asynchronous method call and get the *Document *object back by grabbing the "Result" property (the "shortcut" for doing async work I read about in another blog post). No errors, and I appear to get a *Document *object back:

loTask = _DWDLLBridgeService.InvokeStaticMethod(DW_DLLBRIDGE_NAMESPACE + ".SchemaExtensions", "GetFromDocumentForDocumentAsync", loDWServiceConnection, pnDocID, lcFileCabinetGUID)
loDocument = _DWDLLBridgeService.GetProperty(loTask, "Result")

The problem is, it isn't really a *Document *object like I would see in C#. For example, here is what happens if I try to get the document's "Id" property:

loDocument = _DWDLLBridgeService.GetProperty(loTask, "Id")

I get error: "OLE IDispatch exception code 0 from wwDotNetBridge: Index was outside the bounds of the array..."

If I try to invoke a method:

loFileDownload = _DWDLLBridgeService.CreateInstance(DW_DLLBRIDGE_NAMESPACE + ".FileDownload")
loTask = _DWDLLBridgeService.InvokeMethod(loDocument, "PostToFileDownloadRelationForStreamAsync", loFileDownload)

I get the error:

OLE IDispatch exception code 0 from wwDotNetBridge: Method 'DocuWare.Services.Http.DeserializedHttpResponse`1[[DocuWare.Platform.ServerClient.Document, DocuWare.Platform.ServerClient, Version=6.12.0.0, Culture=neutral, PublicKeyToken=879da714589ba9ea]].PostToFileDownloadRelationForStreamAsync' not found...

Ah, that tells me it maybe has something to do with generics? That the *Document *type is really some sort of wrapper around the HTTP response I got from the service connection?

One more thing: If I try to access a property of this object directly, as in:

lnDocId = loDocument.Id

Visual Foxpro crashes immediately, nothing to catch, no C0000005 error, nada. Just a pure, hard crash.

Additionally, these same sorts of errors occur if I use proper async code and grab the *Document *instance with a callback class in VFP. The type listed in the InvokeMethod() error is slightly different, but I get the same errors when using GetPropertyEx() or crashing out if I try to access the property directly.

I am fine if this just isn't going to work, as I am sort of re-inventing the wheel a third time. I just thought it was so cool to be summoning .NET code (I can call the global library I've already developed for things like viewing text and data!) in this way that it would be awesome if I could get this to work. Is it possible the deeper workings of DocuWare's assemblies simple go too far with generics, etc. such that wwDotNetBridge isn't going to work?

Thanks for the wonderful tool! Aloha!

Thanks, JoeK

Gravatar is a globally recognized avatar based on your email address. re: wwDotNetBridge -- A .NET object I cannot manipulate...
  Rick Strahl
  Joe Kaufman
  Nov 6, 2018 @ 03:34pm

You should check and see what you get back as as the Task.Result. Check the actual type:

loType = loBridge.GetType(loDocument)
? loBridge.GetProperty(loType,"Name")

Generics always require you use indirect methods (ie. InvokeMethod() or GetProperty()) because generic types cannot be translated into a COM interface. I'm not sure why Fox crashes rather than capturing the error, but it sure does if you touch any generic instance or interface. Short answer: Don't do that. Using regular COM interop (ie. without wwDotnetBridge) you wouldn't be able to access generic types at all - they just wouldn't be available at all.

You probably also should check for an error on the task call.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: wwDotNetBridge -- A .NET object I cannot manipulate...
  Joe Kaufman
  Rick Strahl
  Nov 6, 2018 @ 05:57pm

Rick,

The actual type is:

DeserializedHttpResponse`1

as the first part of the InvokeMethod() error indicates. I can't quite recall the correct terminology for that notation with the tick -- is that just a generic type? I feel like I am forgetting the name of that sort of "on the fly" instantiation...?

I have been using InvokeMethod() and GetPropertyEx() almost exclusively, as I can never tell when it is safe not to. I can't see any of this code, only headers, so I am at a disadvantage compared to most of your examples. I do think the downloading of a document is completely over-designed by DocuWare -- they're German. 😃 I know I can make their stuff work, as this afternoon I got uploading a file to work in about an hour. That's because it is all synchronous and non-generic (according to the header files). Still had to figure a few things out, but the code is cleaner than using XMLHTTP to roll my own calls. It is frustrating, because it would appear that Documents and ServiceConnections are just wrappers around HttpClient stuff.

I checked for errors on the bridge every step of the way -- all clean until I try to InvokeMethod() or GetProperty() or GetPropertyEx() on that dang object.

The only reason I am doing this is because using the DocuWare ServiceConnection instance is smarter at marshaling license usage by client machine. I burn through license more quickly from Foxpro if I try my own stuff (though I have it working currently). I can do all this in C# and then command-line stuff to call it, but that is obviously inelegant. Your bridge gave me the hope of re-adding some elegance by consuming a .NET assembly from VFP 9.0 (which still blows my mind). Ye standard C# libs are easier to call than some of this DocuWare stuff...

Hm, though, I guess if I took my wrappers around C# for all this and accessed THEM from the bridge, I could download files all day. Instead of waiting for file-system based files I could access a file stream from inside one of my own C# libs. So, one more question -- can LoadAssembly() load types from an EXE file as opposed to a DLL? They are essentially the some when it comes to disassembly, are they not? I already have Imaging.EXE that runs against DocuWare -- if I can expose that I can see a ton of functionality in my own custom namespace.

Sorry, one more question -- do other toolkits/languages have these sorts of bridges? Or do they just play nicer with interopping from the get-go? Python? PHP? Java(Script)?

As much as all this makes my head hurt, it must make your head meta-hurt. I really appreciate all you have done on this -- it's amazing, frankly.

Thanks, JoeK

Gravatar is a globally recognized avatar based on your email address. re: wwDotNetBridge -- A .NET object I cannot manipulate...
  Rick Strahl
  Joe Kaufman
  Nov 6, 2018 @ 09:08pm

Yes the backtick DeserializedHttpResponse\1` syntax is a generic type representation in .NET - the syntax is internal Reflection syntax which points at the first generic type.

It seems to me though that the response you get is not a document (ie. no Id property) but a de-serialized response. You probably have to drill into that object to get at the document.

Either way you should be able to access the type just fine using GetProperty() assuming you're referencing the right object and the propery exists!

Without looking closer at the actual type definitions (with Reflector or other .NET decompiler) it's hard to say what the issue might be.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: wwDotNetBridge -- A .NET object I cannot manipulate...
  Rick Strahl
  Joe Kaufman
  Nov 6, 2018 @ 09:13pm

Yes for complex APIs I generally would recommend that you write your code in C# and the create a wrapper function that you can call from FoxPro with parameters that are simple to pass and understandable in FoxPro.

Async code is usually better handled by passing in a FoxPro object and updating and notifying the Fox object when the data is changed. This usually requires you to create a wrapper, but that's exactly what wwDotnetBridge does for async calls which allows you to make any .NET method call async basically.

You have to be very careful with Task based APIs in .NET - if you call .Result it can very easy hang if multiple requests are going side by side due to deadlocks.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: wwDotNetBridge -- A .NET object I cannot manipulate...
  Joe Kaufman
  Rick Strahl
  Nov 7, 2018 @ 07:24am

Rick,

EDIT: I figured it out! I just needed to keep F12-ing into the *DeserializedHttpReponse *class, where it had a *Content *property. That represented the actual *Document *instance which I can access with straightforward property references. No errors, no crashing. (I am still curious to whether or not one can load a .NET EXE with LoadAssembly() (as opposed to just DLLs). Thank you so much for talking me through this!

EDIT 2: All is working now. Not sure if you are going to blog any more about various bridge "gotchas", but one thing I ran into when finally downloading the response stream as a file was that my file system file was zero bytes and locked. That's because in C# you would do something like:

using (FileStream file = File.Create(fileName))
{
    using (Stream stream = downloadResponse.Content)
    {
        stream.CopyTo(file);
    }
}

depending on the *using *blocks to properly dispose of the stream resources. wwDotNetBridge is amazing, but it can't implement .NET syntax structures, I don't think. So, I call Close() and Dispose() explicitly to make sure streams are properly de-utilized:

	* All is well, so we can stream the HTTP response content to a file.
	loDownloadStream = _DWDLLBridgeService.GetPropertyEx(loHttpResponse, "Content")
	loFileStream = _DWDLLBridgeService.InvokeStaticMethod("System.IO.File", "Create", pcExportTo)
	_DWDLLBridgeService.InvokeMethod(loDownloadStream, "CopyTo", loFileStream)
	* We must be sure to Close()/Dispose() the streams, otherwise they do not write and they keep the zero-byte export file locked.
	_DWDLLBridgeService.InvokeMethod(loDownloadStream, "Close")
	_DWDLLBridgeService.InvokeMethod(loDownloadStream, "Dispose")
	_DWDLLBridgeService.InvokeMethod(loFileStream, "Close")
	_DWDLLBridgeService.InvokeMethod(loFileStream, "Dispose")

Just something that might help someone else diving into this brave new world!

(original post follows...)

Thanks again for all this. I have switched to using a callback object, just a real poor man's blocking implementation where I set flags and wait for success or error, and then grab the result back so I can keep code inline from the calling program. The type of the result I get in the successful callback event is now:

System.Threading.Tasks.Task`1

and the error I get when I try to invoke a subsequent method is:

Method 'System.Threading.Tasks.Task1[[DocuWare.Services.Http.DeserializedHttpResponse1[[DocuWare.Platform.ServerClient.Document, DocuWare.Platform.ServerClient, Version=6.12.0.0, Culture=neutral, PublicKeyToken=879da714589ba9ea]], DocuWare.RestClient, Version=6.12.0.0, Culture=neutral, PublicKeyToken=879da714589ba9ea]].PostToFileDownloadRelationForStreamAsync' not found.

This all makes sense, since the header for the method I cam calling is:

public Task<DeserializedHttpResponse<Stream>> PostToFileDownloadRelationForStreamAsync(FileDownload dataToSend);

Zowie, my head hurts when there are types within types, and all async to boot. I think I am out of my league.

I don't really know how to drill down into the object I have back, though it does have an Id property now, since it is a Task. 😃 Is there any way to assess the properties and methods of the returned object from the Foxpro side, or is the only route to dig more into the DocuWare assembly via disassembly tools? I can see the headers via good ol' F12 in Visual Studio, but I am not getting much insight from that.

And can one use an EXE in LoadAssembly(), or does it only allow DLLs? I guess I can just try it...

Thanks for all your help -- if nothing else, I am seeing what all the bridge can do for other scenarios!

Thanks, JoeK

Gravatar is a globally recognized avatar based on your email address. re: wwDotNetBridge -- A .NET object I cannot manipulate...
  Rick Strahl
  Joe Kaufman
  Nov 7, 2018 @ 02:57pm

I am still curious to whether or not one can load a .NET EXE with LoadAssembly() (as opposed to just DLLs)

Yes you can. A .NET Exe is just a packaged DLL with a portable format header. Internally it just a DLL.

As to the complexity of nested types etc.: That's really a vendor specific issue - yes it's a little easier in .NET code natively because there's a lot of syntax that can help with this, but a badly implemented (ie. read deeply nested types) API is a bad API any way you look at it 😃

As I mentioned earlier - if you are writing a ton of wwDotnetBridge to access functionality in an API you should probably create a .NET wrapper and call that from FoxPro. It'll be easier to write that .NET code and get all the help of Intellisense, plus it's probably faster and gives you access to all those fancy language features that you can abstract away in the wrapper call. Really wwDotnetBridge is one of the easiest ways you can get your feet wet with writing .NET code because it's usually small blocks of code and very isolated to a single file etc.

FWIW I do this with most of my Fox applications - I have a .NET assembly I create where I have all my wrapper methods that I call from various places in the application. There's lots of use cases including popping up modal UI windows from FoxPro.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: wwDotNetBridge -- A .NET object I cannot manipulate...
  Joe Kaufman
  Rick Strahl
  Nov 7, 2018 @ 06:15pm

Rick,

I'll try EXEs at some point. The reason that is appealing is because I have all my desktop EXEs set up so that linked assemblies are built in (embedded) and they auto-resolve with some entry-point and project file trickery (none of it my own concoction, but better than ILMerge, mostly from this):

http://blogs.interknowlogy.com/2011/07/13/merging-a-wpf-application-into-a-single-exe/

Lots of other alternatives for that, but I love the idea of an EXE or DLL that has everything inside so that I don't have to worry about deploying more than one file.

I can definitely see how providing a simpler wrapper (hopefully one with just native data types and no generics -- maybe even no async) would be advantageous on the desktop. One thing I am trying to do with wwDotNetBridge is to simply create a List<T> , but I think I will post on a separate topic about that. This DocuWare interface keeps throwing up new challenges.

Thanks,

JoeK

Gravatar is a globally recognized avatar based on your email address. re: wwDotNetBridge -- A .NET object I cannot manipulate...
  Rick Strahl
  Joe Kaufman
  Nov 8, 2018 @ 01:44am

One thing I am trying to do with wwDotNetBridge is to simply create a List , but I think I will post on a separate topic about that. This DocuWare interface keeps throwing up new challenges.

That's not supported at the moment - you can't create a generic type from FoxPro. You can use and access them but you can't create them.

Update

Took a look at this and added that functionality to COM value. You can now do something like this using the new ComValue.SetValueFromCreateGenericInstance() method:

loBridge = GetwwDotnetBridge()

loValue = loBridge.CreateComValue()

*** Create an array of strings for generic type parameters
loGenericTypes = loBridge.CreateArray("System.String")
loGenericTypes.AddItem("System.String") && List<string>

*** Create a new array of List Items to pass into ctor
loList = loBridge.CreateArray("System.String")
loList.AddItem("New Item 1")
loList.AddItem("New Item 2")

*** Add the array as parameter to the constructor
loParms = loBridge.CreateArray("System.Object")
loParms.AddItem(loList)  && parameter to List<string>

*** Create List<string>(list)  on the ComValue structure
loValue.SetValueFromCreateGenericInstance("System.Collections.Generic.List",loGenericTypes,loParms)
? loValue.Value

? loBridge.GetProperty(loValue.Value,"Count")  && 2

*** Add a new item with Collection Method
? loBridge.InvokeMethod(loValue.Value,"Add","New Item 3 (code)")
? loBridge.GetProperty(loValue.Value,"Count")  && 3

*** Access with Indexers
loItem = loBridge.GetProperty(loValue,"Value[0]")
? loItem
loItem = loBridge.GetProperty(loValue,"Value[2]")
? loItem

Not released yet and will show up in Web Connection and Client Tools first. Web Connection first since that's coming out in the next few days.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: wwDotNetBridge -- A .NET object I cannot manipulate...
  Joe Kaufman
  Rick Strahl
  Nov 8, 2018 @ 06:27am

Rick,

Very cool!

So, this code at the top:

*** Create an array of strings for generic type parameters
loGenericTypes = loBridge.CreateArray("System.String")
loGenericTypes.AddItem("System.String") && List<string>

*** Create a new array of List Items to pass into ctor
loList = loBridge.CreateArray("System.String")
loList.AddItem("New Item 1")
loList.AddItem("New Item 2")

...is where you could do some user-defined type (as opposed to something like string)? What if I needed:

List<DocumentIndexField>

Regardless, thanks for the enhancement. I am liking the idea of writing more straightforward wrappers in .NET at this point, but this makes wwDotNetBridge that much tighter!

Thanks,

JoeK

Gravatar is a globally recognized avatar based on your email address. re: wwDotNetBridge -- A .NET object I cannot manipulate...
  Rick Strahl
  Joe Kaufman
  Nov 9, 2018 @ 12:25pm

...is where you could do some user-defined type (as opposed to something like string)? What if I needed:

Yes you can specify any type for the generic types.

I am liking the idea of writing more straightforward wrappers in .NET at this point

Yes that is the better approach for complex logic - it's just that much easier to write the code in .NET and then just call the wrapper from FoxPro.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: wwDotNetBridge -- A .NET object I cannot manipulate...
  Joe Kaufman
  Rick Strahl
  Nov 9, 2018 @ 01:03pm

Rick,

Indeed, I made my Foxpro code the most streamlined of any other method I use to upload and index a document yesterday, and it works with large files better than using straight HTTP requests (which sometimes chokes on too-large files).

Thanks again for all your help, and you can consider this topic closed!

Thanks, JoeK

© 1996-2024