RubberDuck
RubberDuck

Reputation: 12789

How to create a read-only property exposed to COM?

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

Answers (2)

RubberDuck
RubberDuck

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.

vb error dialog

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.

compile error: can't assign to read only property

Upvotes: 8

Rufus L
Rufus L

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

Related Questions