Search code examples
c#observablecollectionobject-initializers

Is there a simpler way to have an ObservableCollection that can be modified only from within the same class?


Often I need a read-only ObservableCollection (so that it's reference or contents can be only modified from within this class). I need ObservableCollection because I will be binding to that property with WPF (not shown in the code below). This is a fully reproducible working example that shows my intent:

public class MyClass
{ 
    public ReadOnlyCollection<string> Values { get { return _valuesRO; } }

    private readonly ObservableCollection<string> _values;
    private readonly ReadOnlyObservableCollection<string> _valuesRO;

    public MyClass()
    {
        _values = new();
        _valuesRO = new(_values);
    }

    public void AddValue(string value)
    {
        _values.Add(value + ":");
    }
}

public static class MyApp
{
    public static void Main()
    {
        MyClass myClass = new();

        myClass.AddValue("192");        //-> should work
        Debug.Print(myClass.Values[0]); //-> should work
        myClass.Values = new();         //-> should NOT work
        myClass.Values.Add("192");      //-> should NOT work
    }
}

I got this pattern from here.

However, this requires quite a lot of code for each public collection - two additional fields and two more lines in the constructor. If I need many such collections in my class, it bloats up really fast, hurting code readability.

Seeing as that solution I linked to is 14 years old now, is there any way to achieve the same functionality with less code (just one statement for declaring/instantiating the collection), no additional fields and nothing in the constructor? Is it possible to define my own derived class that would abstract this behavior? Ideally declaration and usage would be something like this:

public class MyClass
{ 
    public MySpecialReadOnlyCollection<string> Values { get; init; } = new();

    public void AddValue(string value)
    {
        Values.Add(value + ":");
    }
}

public static class MyApp
{
    public static void Main()
    {
        MyClass myClass = new();

        myClass.AddValue("192");        //-> should work
        Debug.Print(myClass.Values[0]); //-> should work
        myClass.Values = new();         //-> should NOT work
        myClass.Values.Add("192");      //-> should NOT work
    }
}

If achieving this is not possible, what is the next best thing I can do here? The end goal is to implement this pattern without any backing fields for the collection and no constructor code, to make this as scalable as possible, similar to the idealized example above. Security is not much of an issue (modifying the underlying collection via Reflection via non-intended way), as the main goal here is for the code to show intent and avoid accidental modification of the collection.


Solution

  • TBH I do not have a lot of experience with corresponding collections and WPF but based on the provided usage you can try intorducing custom interface and class to implement it which will allow the collection to have an readonly view:

    public interface IMyNotifyCompletion<out T> : 
        INotifyCollectionChanged
        , INotifyPropertyChanged
        , IReadOnlyList<T>
        , IReadOnlyCollection<T>;
    
    internal class MyNotifyCompletion<T> : 
        ObservableCollection<T>
        , IMyReadonlyNotifyCompletion<T>;
    

    which will reduce the boilerplate to the following:

    public class MyClass
    {
        public IMyNotifyCompletion<string> Values => _values;
        private readonly MyNotifyCompletion<string> _values = new();
        
        public void AddValue(string value)
        {
            _values.Add(value + ":");
        }
    }
    

    There is at least one subtle difference - ReadOnlyObservableCollection has INotifyCollectionChanged and INotifyPropertyChanged interfaces explicitly implemented, which requires Values to be cast to them to use corresponding events. If you don't need those interfaces exposed then you can just use build-in readonly interfaces:

    public class MyClass
    {
        public IReadOnlyCollection<string> Values => _values;
        private readonly ObservableCollection<string> _values = new();
        // ...
    }
    

    As for the MySpecialReadOnlyCollection you desire - arguably it can't be done in general case by definition, the only option to achive something similar would be to move the MyClass and the collection into a separate assembly and "hide" the modification members as internal so other assemblies will not able to call them (though inside the defining assembly any member will be able to)