Reputation: 12789
I have a C# class that I would like to expose to VB6 via COM. The problem is this requires having a default constructor for the class. So, I have to expose setters to the client so these properties can be set to begin with.
For example:
[Guid("B1E17DF6-9578-4D24-B578-9C70979E2F05")]
public interface _Class1
{
[DispId(2)]
string Message { get; set; }
[DispId(1)]
string TestingAMethod();
}
[Guid("197A7A57-E59F-41C9-82C8-A2F051ABA53C")]
[ProgId("Project.Class1")]
[ClassInterface(ClassInterfaceType.None)]
public class Class1 : _Class1
{
public string Message { get; set; }
public Class1() { } //default constructor for COM
public Class1(string message)
{
this.Message = message;
}
public string TestingAMethod()
{
return "Hello World";
}
}
Normally, I would declare the property as:
public string Message { get; private set; }
but this obviously won't work because COM can't use the constructor that takes in an argument.
So, the question is:
How can I ensure that the property gets set only once without using either private set
or readonly
?
Upvotes: 3
Views: 381
Reputation: 12789
According to this article on Code Project:
If the method call fails or business logic validation fails, the .NET component is expected to raise an exception. This exception usually has a failure HRESULT assigned to it and an Error description associated with it. The CCW gleans out details such as the Error Code, Error message etc. from the .NET Exception and provides these details in a form that can be consumed by the COM client.
Emphasis mine.
So, what we can check to see if the private field has been set. If it has been, throw an exception.
private string message;
public string Message {
get { return message; }
set
{
if (message != null)
{
throw new ReadOnlyPropertyException("Class1.Message can not be changed once it is set.");
}
message = value;
}
}
Consuming this from the following VB6/VBA code
Dim cls As New Rubberduck_SourceControl.Class1
cls.Message = "Hello"
Debug.Print cls.Message
cls.Message = "Goodbye" 'we expect a read only error here
Debug.Print cls.Message
Results in an error being raised.
Effectively making the property read-only once it has been set.
But that results in a the client experiencing runtime errors. The solution here is to create a class factory as @Hans Passant suggested.
Understanding the COM philosophy is important. COM provides a class factory to create objects but it doesn't support passing arbitrary arguments. Which is why you always need a default constructor. Well, no problem, just create your own factory. Hide Class1 and write a Class1Factory class. With a CreateClass1() method that returns _Class1. It can take any arguments you need.
So, I implemented the class the way I wanted to originally. (With no default constructor and a private set
property. It's important to note that the interface only has a get
for the property.
[Guid("B1E17DF6-9578-4D24-B578-9C70979E2F05")]
public interface _Class1
{
[DispId(2)]
string Message { get; }
[DispId(1)]
string TestingAMethod();
}
[Guid("197A7A57-E59F-41C9-82C8-A2F051ABA53C")]
[ProgId("Project.Class1")]
[ClassInterface(ClassInterfaceType.None)]
public class Class1 : _Class1
{
public string Message { get; private set; }
public Class1(string message)
{
this.Message = message;
}
public string TestingAMethod()
{
return "Hello World";
}
}
Then I created a class factory that does have a default constructor and takes in the same arguments as Class1
's constructor.
[Guid("98F2287A-1DA3-4CC2-B808-19C0BE976C08")]
public interface _ClassFactory
{
Class1 CreateClass1(string message);
}
[Guid("C7546E1F-E1DB-423B-894C-CB19607972F5")]
[ProgId("Project.ClassFactory")]
[ClassInterface(ClassInterfaceType.None)]
public class ClassFactory : _ClassFactory
{
public Class1 CreateClass1(string message)
{
return new Class1(message);
}
}
So, now if the client tries to set the Message
property at all, it gets a compile time error instead.
Upvotes: 8
Reputation: 37070
It's not pretty, and doesn't expose the fact that it's "read-only", but would something like a private tracking bool work?
[Guid("197A7A57-E59F-41C9-82C8-A2F051ABA53C")]
[ProgId("Project.Class1")]
[ClassInterface(ClassInterfaceType.None)]
public class Class1 : _Class1
{
private bool _isMessageSet = false;
private string _message;
public string Message
{
get { return _message; }
set
{
if (!_isMessageSet)
{
_message = value;
_isMessageSet = true;
}
}
}
public Class1() { } //default constructor for COM
public Class1(string message)
{
this.Message = message;
}
}
Upvotes: 1