I'm trying to create a COM class with one method that will cast an object to a specific interface on behalf of VBScript.
This is the method signature I'm using:
public object GetInterface(object unknown, string iid)
I thought this would be possible because if the method explicitly declares the return type as :
public IRequestedInterface GetInterface(object unknown, string iid)
Then VBScript gets the reference to the desired interface.
So I tried just casting to the interface
return (IRequestedInterface)unknown;
Unfortunately, VBScript gets a reference to the default interface instead of the requested interface.
I have tried getting round this by creating a custom marshaller using ICustomMarshaler.
I thought this would work because the method MarshalManagedToNative returns a IntPtr.
Because of this I thought that if i just returned the IntPtr to the interface
return Marshal.GetComInterfaceForObject(unknown, typeof(IRequestedInterface));
it would work. But, obviously, it didn't have the desired effect :(
So does anybody know if it is posible and how you would do it?
EDIT:
I thought it would be helpful to add a concrete example (although it is contrived) to explain why I haven't accepted that VBScript will always get the default interface. I'm still clinging to my hope.
Below you will find the contents of 3 files, 'TestLib.cs', 'Build.cmd' and 'Test.vbs'. These hopefully demonstrate why I still think it 'should' be possible.
Note: I have tested this on Windows XP SP3 (x86).
TestLib.cs
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Forms;
[assembly: ComVisible(false)]
[assembly: Guid("64e20009-c664-4883-a6e5-1e36a31a0fd8")]
[assembly: AssemblyVersion("2012.06.*")]
[ComVisible(true)]
[Guid("EB77C7B1-D1B9-4BB3-9D63-FBFBD56C9ABA")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IPerformQi
{
[DispId(1000)]
object GetInterface(object unknown, string iid);
[DispId(2000)]
IRequested GetIRequested(object unknown);
}
[ComVisible(true)]
[Guid("7742BC0A-8719-483E-B1DF-AE9CD9A958DC")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IDefault
{
[DispId(1000)]
void SayHello(string name);
}
[ComVisible(true)]
[Guid("FFF34296-2A06-47D4-B09C-B93B63D5CC53")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IRequested
{
[DispId(1000)]
void SayGoodbye(string name);
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IPerformQi))]
[Guid("222BB88D-B9FA-4F23-8DB3-BA998F4E668B")]
[ProgId("TestLib.PerformQi")]
public class PerformQi : IPerformQi
{
object IPerformQi.GetInterface(object unknown, string iid)
{
if(iid == "FFF34296-2A06-47D4-B09C-B93B63D5CC53")
return (IRequested)unknown;
throw new Exception("Unable to find inteface");
}
IRequested IPerformQi.GetIRequested(object unknown)
{
return (IRequested)unknown;
}
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IDefault))]
[Guid("174ABED6-3325-4878-89E3-BF8BD1107488")]
[ProgId("TestLib.Test")]
public class Test : IDefault, IRequested
{
void IDefault.SayHello(string name)
{
MessageBox.Show(string.Format("Hello '{0}'", name));
}
void IRequested.SayGoodbye(string name)
{
MessageBox.Show(string.Format("Goodbye '{0}'", name));
}
}
Build.cmd
"%windir%\Microsoft.Net\Framework\v4.0.30319\csc.exe" /out:TestLib.dll /target:library /r:System.Windows.Forms.dll TestLib.cs
"%windir%\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe" TestLib.dll /codebase /tlb:TestLib.tlb
PAUSE
Test.vbs
Dim oPerformQi 'As TestLib.PerformQi
Dim oTest 'As TestLib.Test
Dim oTest2 'As IRequested
Dim oTest3 'As IRequested
Set oPerformQi = CreateObject("TestLib.PerformQi")
Set oTest = CreateObject("TestLib.Test")
Call oTest.SayHello("Robert")
Set oTest2 = oPerformQi.GetIRequested(oTest)
'Note: This works
Call oTest2.SayGoodbye("Robert")
Set oTest3 = oPerformQi.GetInterface(oTest, "FFF34296-2A06-47D4-B09C-B93B63D5CC53")
'Note: This does not work
Call oTest3.SayGoodbye("Robert")
Using the call oPerformQi.GetIRequested(oTest) makes the call to oTest3.SayGoodbye("Robert") work. This makes me think you are not limited to just the default interface in VBS.
Perhaps .Net is not capable of returning the specified interface because of an implicit cast on the return value? Ideally I would use generics for this, but as we all know COM does not support genrics.
Under this restriction is there any other way that you can think of to achieve this?
EDIT 2:
I have found that I can achieve this using VB6, below is the code for the class.
Option Explicit
Public Function GetInterface(ByVal oUnknown As Object, ByVal IID As String) As Variant
Dim oIRequested As IRequested
If IID = "FFF34296-2A06-47D4-B09C-B93B63D5CC53" Then
Set oIRequested = oUnknown
Set GetInterface = oIRequested
Else
Err.Raise 1, , "Unable to find inteface"
End If
End Function
I would still like to find a C# version if anybody can shed some light on the subject i would appreciate it.
In order to have multiple IDispatch-derived interfaces implemented on a single object, to be accessible from scripting environment you should rather implement IDispatchEx and have its methods called once a call from script is taking place.
The problem you are facing is caused by the fact that script queries for your IDispatch first, and both your IDispatch-derived interfaces return the same "main" IDispatch leaving no chance for methods of other interfaces to be accessible.
When VBS host is about to call a method on your object, it first queries IDispatchEx. If found, the calls are delivered via IDispatchEx::InvokeEx and your COM Class can internally route the call to the proper IDispatch implementation, both private or forward to external/inner object.
In case IDispatchEx is not found, it looks for IDispatch and there you are in trouble because it sees only your "main" interface. That is, the workaround for you is to implement IDispatchEx. You can do it either way: implement right on your COM class, or instead create a proxy class to accept scripting calls via IDispatchEx::InvokeEx and forward to correct IDispatch in your code.
Example: Both A and B classes implement IX and IY interfaces, B additionally implements IDispatchEx. Interface methods are IX::X1, IY::Y1.
On Error Resume Next
Set A = CreateObject("Test.A")
WScript.Echo A.X1 ' Success, via IX::Invoke
WScript.Echo A.Y1 ' Failure, A's IDispatch is IX's parent and does not have Y1 method
Set B = CreateObject("Test.B")
WScript.Echo B.X1 ' Success, via IDispatchEx::InvokeEx
WScript.Echo B.Y1 ' Success, via IDispatchEx::InvokeEx
Related
I have inherited a VB6 app that launches Excel, opens a workbook, and runs a macro on an interval. This macro returns values though its parameters. In my attempts to convert this to C# using interop, I can successfully run the macro, but these parameter values do not get returned.
Is there something missing/incorrect in the code below, or is this simply not supported?
VBA macro:
Sub Foo(bar As Long)
bar = 5
End Sub
C# code:
void CallFoo()
{
// Declared as an object to avoid losing the value in auto-boxing
// The result is the same if declared as int
Object bar = 0;
m_application.Run(m_fooCommand, a);
Console.WriteLine(a); // a is always 0
}
This (roughly) equivalent VB6 code gets the return value just fine.
Dim bar as Long
bar = 0
xlApp.Run "Test.xlsm!Foo", bar
MsgBox bar // prints 5
So as best as I can tell, no, you cannot return values through parameters from VBA to C# in this way. The next best thing is to simply create a type in .NET, make it COM visible, regasm it and then reference that in the VBA script.
So, for completeness...
A return type:
[ComVisible(true)]
[Guid("097B5B52-C73B-4BD0-A540-802D0BC7C49F")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IFooResult
{
int Value { get; set; }
}
[ComVisible(true)]
[Guid("76B6BCBD-6F4D-4457-8A85-CDC48F4A7613")]
[ClassInterface(ClassInterfaceType.None)]
public class FooResult : IFooResult
{
[DispId(1)]
public byte[] Buffer { get; set; }
}
Generate strong name (required by regasm)
sn -k FooLib.snk
Register the assembly
regasm FooLib.dll /tlb:FooLib.tlb /codebase
Then simply reference the library in the VBA project. It can now be passed as an argument and populated in the macro or created in and returned from the macro (I believe this would require cleanup on the .NET side, Marshal.ReleaseComObject()).
I have simple IScript interface. And I want enforce that all scripts implement it.
public interface IScript<T>
{
T Execute(object[] args);
}
I want to use Roslyn scripting API to achive this. Something like this is possible with CSScript (see Interface Alignment).
var code = #"
using System;
using My.Namespace.With.IScript;
public class Script : IScript<string>
{
public string Execute()
{
return ""Hello from script!"";
}
}
";
var script = CSharpScript.Create(code, ScriptOptions.Default); // + Load all assemblies and references
script.WithInterface(typeof(IScript<string>)); // I need something like this, to enforce interface
script.Compile();
string result = script.Execute(); // and then execute script
Console.WriteLine(result); // print "Hello from script!"
Type safety is a static thing enforced a compile time (of your application). Creating and running a CSharpScript is done at runtime. So you cannot enforce type safety at runtime.
Maybe CSharpScript is not the right way to go. By using this SO answer,
You can compile a piece of C# code into memory and generate assembly bytes with Roslyn.
You would then change the line
object obj = Activator.CreateInstance(type);
to
IScript<string> obj = Activator.CreateInstance(type) as IScript<string>;
if (obj != null) {
obj.Execute(args);
}
I have a working CLI interface between C++ and C# code. The code has a C++ abstract interface like:
-------------C++ Interface---------------
namespace cppns
{
class cppInterface
{
public:
virtual bool Start(const char *pcDir) = 0;
};
}
------Implementation of abstract C++ interface in same dll---------
namespace cppns
{
class cppimp : public cppInterface
private:
gcroot<MyInternalCSharpClass^> mInternalClassAccess;
public:
cppimp::cppimp()
{
mInternalClassAccess = gcnew MyInternalCSharpClass();
}
virtual bool cppimp::Start(const char *pcDir)
{
System::AppDomain ^appDom = AppDomain::CurrentDomain::get();
System::String ^strDomainName = appDom->FriendlyName;
mInternalClassAccess->Initalize(pcDir);
}
}
---------Method to create an instance of the class in a factory--------------
cppns::cppInterface *GetImplObject()
{
return new cppns::cppimp();
}
----------Factory class .h to allow C++ to get an instance of the cppimp class------
------The C++ code knows about the abstract interface by including the header file--
------FactoryExport is __declspec(dllexport) when compiled in dll and---------------
----- __declspec(dllimport) when used as a header file in exe that uses header------
class FactoryExport ClassFactory
{
public:
static cppns::cppInterface *CreateImpl();
};
----------Factory class .cpp to allow C++ to get an instance of the cppimp class------
cppns::cppInterface *ClassFactory::CreateImpl()
{
return GetImplObject();
}
This code correctly allows me to call CreateImpl to get an implementation of the interface that contains the Start method. My issue is that I'm trying to force the whole CLR/.NET loading and executing into an AppDomain that is not the default AppDomain. I can create a secondary AppDomain using the following code:
CComPtr<ICorRuntimeHost> pRuntimeHost;
//Retrieve a pointer to the ICorRuntimeHost interface
HRESULT hr = CorBindToRuntimeEx(
L"v2.0.50727", //Retrieve last version before 4.0.
// NULL, //Retrieve latest version by default
L"wks",
STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN | STARTUP_CONCURRENT_GC,
CLSID_CorRuntimeHost,
IID_ICorRuntimeHost,
(void**)&pRuntimeHost.p
);
hr = pRuntimeHost->Start();
DWORD dwAppDomainId = 22;
WCHAR domainName[80 + 1];
swprintf(domainName, 80, L"%s-%ld",L"NoDefaultDomain", dwAppDomainId);
CComPtr<IUnknown> pUnknownAppDomain;
hr = pRuntimeHost->CreateDomainEx(domainName, NULL, NULL, &pUnknownAppDomain);
CComPtr<_AppDomain> pAppDomain;
hr = pUnknownAppDomain->QueryInterface(__uuidof(_AppDomain), (VOID**)&pAppDomain.p);
BSTR bstrFriendlyName;
hr = pAppDomain->get_FriendlyName(&bstrFriendlyName);
if (SUCCEEDED(hr))
{
_bstr_t bstrFriendlyNameWrap(bstrFriendlyName, false);
}
_bstr_t bstrAssemblyName("InteropCode");
CComPtr<_Assembly> pAssembly;
hr = pAppDomain->Load_2(bstrAssemblyName, &pAssembly);
BSTR bstrFullName;
hr = pAssembly->get_FullName(&bstrFullName);
if (SUCCEEDED(hr))
{
_bstr_t bstrFullNameWrap(bstrFullName, false);
std::cout << "Assembly name is: " << bstrFullNameWrap << "\n";
}
Every attempt of getting the factory to return to me an interface to cppns::cppInterface within this secondary application domain has failed. I have even attempted to create a secondary factory that is a C# class that returns the pointer to the implemented interface so that an Invoke call on the Assembly would hopefully cause the rest of the code to execute in the AppDomain that I loaded the Assembly into but the Invoke returns an IDispatch pointer that I can't seem to map back into any type of C++ pointer on my interface.
namespace cppns
{
public ref class NetFactory
{
public:
NetFactory()
{
}
cppInterface *CreateInterop()
{
return GetImplObject();;
}
};
}
Is there another way to get everything to run in a secondary AppDomain or is the IDispatch pointer usable in calling the Start method?
I have managed to get most of the .NET stuff running in another domain. It seems like there is no way to get the CLI layer to run in anything other than the default AppDomain.
To make this work I needed to make the class that sits within both appdomains derive from MarshalByRefObject. In my example above that meant I had to change MyInternalCSharpClass so that it derived from MarshalByRefObject. It was also nessary to made the objects sent and returned from MyInternalCSharpClass also derive from MarshalByRefObject. Finally I these same objects that were passed and returned had to have the [Serializable] property and to also mark all their private variables public. Note if the classes being transferred though the AppDomains are already using the Serializable attribute you can use [XmlIgnore] on each formally private variable to avoid changing the serialization that is being done.
Now that everything can be moved between the AppDomains I created a second AppDomain by doing the following:
bool CreateInstanceInAppDomain(const char *pcAppDomainName)
{
bool bRtn = false;
gcroot<String^> csStrAppDomainName (gcnew String(pcAppDomainName));
mAppDomain = AppDomain::CreateDomain(csStrAppDomainName);
delete csStrAppDomainName;
Object^ MyInternalObject = mAppDomain->CreateInstanceAndUnwrap("AssemblyName", "ClassNameSpace.MyInternalCSharpClass");
mInternalClassAccess = dynamic_cast<MyInternalCSharpClass^>(MyInternalObject);
if (mInternalClassAccess)
{
bRtn = true;
}
return bRtn;
}
Edit: Looks like Jon Skeet had some similar questions: How does the C# compiler detect COM types?
How can I get the CLSID for a given interface within a Primary Interop Assembly? Here's what I'm talking about:
// The c# compiler does some interesting magic.
// The following code ...
var app = new Microsoft.Office.Interop.Outlook.Application();
// ... is compiled like so (disassembled with Reflector):
var app =((Microsoft.Office.Interop.Outlook.Application)
Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid("0006F03A-0000-0000-C000-000000000046"))));
Microsoft.Office.Interop.Outlook.Application is an interface, and therefore it cannot be instantiated directly. What's interesting here is that c# lets you treat these COM interfaces as if they were classes that you can instantiate with the new keyword.
What I want to know is, given the System.Type for a given interface, how can I get the CLSID?
Note: I ultimately want to be able to create an instance given the interface's System.Type - I don't really care how. I'm assuming here that the easiest way to do this would be to get CLSID given the Type, just as the c# compiler does.
Here's my current solution:
// Get the PIA assemby name, using the GUID of the typlib
string asmName;
string asmCodeBase;
var conv = new System.Runtime.InteropServices.TypeLibConverter();
conv.GetPrimaryInteropAssembly(new Guid("00062FFF-0000-0000-C000-000000000046"), 9, 3, 0, out asmName, out asmCodeBase);
// Load the PIA, and get the interface type
var assembly = System.Reflection.Assembly.Load(asmName);
var type = assembly.GetType("Microsoft.Office.Interop.Outlook.Application");
// Get the coclass
var coClassAttr = (System.Runtime.InteropServices.CoClassAttribute)
type.GetCustomAttributes(typeof(System.Runtime.InteropServices.CoClassAttribute), false)[0];
var coClass = coClassAttr.CoClass;
// Instantiate the coclass
var app = Activator.CreateInstance(coClassAttr.CoClass);
// If needed, the CLSID is also available:
var clsid = coClass.GUID;
I figured this out by disassembling the GAC'd PIA. I noticed that Outlook.Application was decorated with a CoClassAttribute, like so:
[ComImport, Guid("00063001-0000-0000-C000-000000000046"), CoClass(typeof(ApplicationClass))]
public interface Application : _Application, ApplicationEvents_11_Event
{
}
ApplicationClass looks something like:
[ComImport, ClassInterface((short) 0), ComSourceInterfaces("Microsoft.Office.Interop.Outlook.ApplicationEvents_11\0Microsoft.Office.Interop.Outlook.ApplicationEvents\0Microsoft.Office.Interop.Outlook.ApplicationEvents_10\0"), Guid("0006F03A-0000-0000-C000-000000000046"), TypeLibType((short) 11)]
public class ApplicationClass : _Application, Application, ApplicationEvents_11_Event, ApplicationEvents_Event, ApplicationEvents_10_Event
{
//...
}
Let me know what you all think, so I can decide if I should mark this as the chosen answer.
Try the GUID property.
I have written an Extension Method off of DataGridView called HideColumns.
public static class Extensions
{
public static void HideColumns(this DataGridView dataGridView, params string[] columnNames)
{
foreach (string str in columnNames)
{
if (dataGridView.Columns[str] != null)
{
dataGridView.Columns[str].Visible = false;
}
}
}
}
I pass my grid into an IronRuby script as a variable called main_grid
When my script calls
main_grid.HideColumns("FirstName","LastName")
the script blows up with Error in Script
undefined method 'HideColumns' for System.Windows.Forms.DataGridView:System::Windows::Forms::DataGridView
The extension methods seem to work okay from C#. What gives?
FWIW, IronRuby 1.1 (needs .net 4) provides the using_clr_extensions method -- as noted in the release notes this activates all extension methods defined on classes defined in a given namespace, regardless of the assembly they are defined in; assemblies loaded in the future that define extension methods in the activated namespace will automatically appear on the correct types, like this:
load_assembly "System.Core"
using_clr_extensions System::Linq
# ...
products.
where(lambda { |p| p.units_in_stock == 0 }).
each { |x| puts x.product_name }
The release notes also point at a whole set of examples at http://github.com/ironruby/ironruby/blob/master/Languages/Ruby/Samples/Linq/101samples.rb
The extension method is just syntatic sugar, you will need to call it as:
Extensions.HideColumns(main_grid, "FirstName", "LastName")
alternatively create a new class in C# which derives from DataGridView and add the method:
public class DataGridViewExt : DataGridView
{
public void HideColumns(params string[] columnNames)
{
foreach (string str in columnNames)
{
if (this.Columns[str] != null)
{
this.Columns[str].Visible = false;
}
}
}
}
and use this class rather than the System.Windows.Forms class on your form.
Since you mentioned it in the comments to JDunkeryly's answer, here's how you'd extend the grid from the ruby side. Just open the class and add a method (only works from the ruby side).
class System::Windows::Forms::DataGridView
def hide_columns(*columnNames)
column_names.each do |cn|
self.columns[cn].visible = false
end
end
end
As far as the suggestion to use the extension method directly, the params keyword is painful to IronRuby. You need to build a typed array with your arguments and pass it. You can't just wrap your ruby strings in a ruby array. I've pulled this off earlier today in a blog post. But if you have a smoother way to handle that, please let me know.