Practical .NET

Understanding .NET Using Read-Only Collections

Even if you're not working in the Microsoft .NET Framework 4.5, .NET provides you with a way to create a read-only collection or to convert an existing List into a read-only collection.

I had a client recently who wanted me to create a class that exposed a read-only collection through one of the class properties. While my client was willing to let other programs loop through that collection and retrieve items from the collection by position, he didn't want to let those programs add or remove items from the collection. In the Microsoft .NET Framework 4.5 you can use the read-only collections that Eric Vogel covered in an earlier C# Corner column, "The New Read-Only Collections in .NET 4.5," to define that property. My client, however, was working with the .NET Framework 4 and didn't intend to upgrade.

Fortunately, in earlier versions of .NET you can also create a read-only class from an existing List just by calling your List's AsReadOnly method. However, if you need something special (a List that users can add items to but not remove items from), inheriting from the ReadOnlyCollectionBase class makes that very easy to do. So easy, in fact, that I don't need a full column to do it (and if that's all you want from this column, you can skip to the end where I discuss both the AsReadOnly method and using the ReadOnlyCollectionBase class).

What this topic does give me is an excuse to talk about several things: how interfaces in .NET and the compiler work together; how creating generic classes gives you flexibility; how you can exploit one of the built-in .NET interfaces; and (finally) how to simplify ugly syntax by building your own extension methods. I also think that you don't really understand anything unless you can see a way to write it yourself, and this topic lets me do that, too, by showing how you can create the .NET ReadOnlyCollectionBase class. So this column is as much about providing insight into how .NET works as it is about doing something practical.

Foundation Knowledge
First, some definitions: A read-only collection is like a List in that it supports iterating through the items with a For…Each loop or retrieving an item in the collection by position. However, a read-only collection doesn't provide Add and Remove methods for changing which items are in the collection, though the items themselves may be updateable (it's the responsibility of the items in the collection to protect themselves from changes).

When you write a For…Each loop to use with a collection, the compiler checks the collection class to see if it implements the IEnumerable interface (or the generic version of that interface, IEnumerable<T>). If the class does implement that interface, then the compiler calls the single method that the interface forces the class to have: the GetEnumerator method. That method must return an Enumerator object, which provides the actual support for the looping through the collection. Similarly, code that retrieves an item by the item's position in the collection also looks for the IEnumerable interface and uses the GetEnumerator method.

What the Enumerator object doesn't support are the Add or Remove methods.

Ignoring the ReadOnlyCollectionBase
I'm going to start by being perverse and pretending that the ReadOnlyCollectionBase class doesn't exist. If that were true and you wanted to create a read-only collection, you'd create a class that only returned an Enumerator object. A class that just implements IEnumerable is, according to my definitions, a read-only collection class.

Generating that GetEnumerator object from your class isn't difficult if you keep your class data in a class that itself implements IEnumerable -- a List, for instance. If you do keep your data in a List, then you can use the List GetEnumerator method to retrieve an Enumerator object, which you can then return to the program using your class.

Listing 1 has the basics of this class (I've called it PhvReadOnlyList). For maximum flexibility, my class doesn't specify the data type of the List in which it keeps its actual data. Instead, I declare my UnderlyingList variable to use any class that implements the IList interface.

I've also made this a generic class: Rather than specify what kind of List I'm working with, I've forced my class to be instantiated with a data type that the code refers to as T. Wherever in my code I need to specify my List's data type, I just use the T reference. Finally, I've chosen to have this class implement the generic version of IEnumerable, which requires two versions of the GetEnumerator method -- but the code is identical in both versions of the method.

Listing 1. The basics of the PhvReadOnlyList class.

Public Class PHVReadOnlyList(Of T)
  Implements IEnumerable(Of T)

  Private UnderlyingList As IList(Of T)

  Public Function GetEnumerator() As IEnumerator(Of T) _
    Implements IEnumerable(Of T).GetEnumerator
    Return UnderlyingList.GetEnumerator()
  End Function

  Public Function GetEnumerator1() As IEnumerator _
    Implements IEnumerable.GetEnumerator
    Return UnderlyingList.GetEnumerator()
  End Function
End Class

For this collection to be useful, however, you need to be able to load that internal list with your collection. The easiest way to do that is to add a constructor to the class that accepts the List to be put in the class' UnderlyingList collection (again, I'm accepting anything that implements the IList interface with the data type specified by T):

Public Sub New(InitialList As IList(Of T))
  UnderlyingList = InitialList
End Sub

Using the Read-Only List
In the class that exposes a read-only property based on PhvReadOnlyList, you still need an ordinary List, with its Add and Remove methods, so that your class can put items in the List both before and after creating a PhvReadOnlyList from it. Putting that all together, a class that exposes a read-only collection needs three things:

  1. A property of type PhvReadOnlyList
  2. A backing field for that property of type PhvReadOnlyList
  3. An ordinary List to hold the original data, used to create the PhvReadOnlyList

Listing 2 shows a sample class (called PhvClass) that implements that design. In the constructor for this class, I create a List of Strings (called ClassList), add a few items to the List and then pass the List to a PhvReadOnlyList object. That PhvReadOnlyList is used as the backing field for a property called, cleverly, AReadOnlyList.

Listing 2. Sample class PhvClass.

Public Class PHVClass

  Private ClassList As List(Of String)
  Private rol As PhvReadOnlyList(Of String)

  Public Sub New()
    ClassList = New List(Of String)
    ClassList.Add("Peter")
    ClassList.Add("Jan")
    rol = New PhvReadOnlyList(of String)(ClassList)
  End Sub

  Public ReadOnly Property AReadOnlyList _
    As PhvReadOnlyList(Of String)
    Get
      Return rol
    End Get
  End Property

End Class

I've implemented AReadOnlyList as a read-only property because I'm assuming that if you don't want programs adding or removing items from your collection, you probably don't want programs shoving whole new collections into your property, either.

Simplifying Code with Extension Methods
There's nothing wrong with the line of code that creates my PhvReadOnlyList:

rol = New PhvReadOnlyList(of String)(ClassList)

However, the syntax is awkward. Adding an extension method can simplify that syntax tremendously.

Extension methods have at least three convenient features: The first is that they attach themselves to whatever classes you specify, automatically appearing in the IntelliSense dropdown lists for those objects; second, they're automatically passed whatever object they're called from as their first parameter; third, when an extension method is used with a generic class (for instance, List<Customer>), the method automatically figures out the data type of the generic parameter. The result is that using an extension method can simplify some otherwise-awkward syntax.

My read-only class works with any class that implements the IList interface, so -- taking a leaf from the .NET Framework 4.5 book -- it would be more convenient to have an extension method that would attach itself to anything that implements the IList interface. That extension method would then create my read-only class and return it. For instance, creating an extension method called AsPhvReadOnly would let me replace that last line of code with this much simpler code:

rol = ClassList.AsPhvReadOnly()

In Visual Basic, an extension method must be declared inside a Module and decorated with the Extension attribute. Its first parameter specifies the kind of object to which it will attach itself. I want my extension method to appear in the IntelliSense dropdown lists for any class that implements the IList interface, so I'll declare the method's first (and only) parameter as IList(of T) -- the compiler will figure out the type that T represents based on the List passed to my method. I also want my method to return a PhvReadOnlyList of the same data type as List passed in -- whatever the compiler deduces that T is. Putting all that together, my extension method's declaration looks like this:

Public Module PHVExtensionMethods
  <Extension>
  Public Function AsPHVReadOnly(Of T)(AList As IList(Of T)) _
    As PhvReadOnlyList(Of T)

  End Function
End Module

Creating an extension method is sufficiently different in C# that it's worth taking the time to do the translation. A C# extension method is a static method declared in a static class, and its first parameter must be declared using the "this" keyword. So the C# version of this method looks like this:

public static class PhvExtensionMethods
{
  public static PhvReadOnlyList<T> AsPhvReadOnly<T>(this IList<T> AList)
  {

  }
}

The single line of code required by my extension method just instantiates my PhvReadOnly class (using the datatype that the extension method has deduced), passing whatever IList object the extension method is being used on. That line of code also returns the resulting read-only List to the calling program:

Return New PhvReadOnlyList(Of T)(AList)

The ReadOnlyBaseCollection Class
You've probably identified what pattern the PhvReadOnlyList implements: it's a wrapper. In the .NET Framework 4.0 or earlier, the wrapper class that you'd really use to create a read-only list would be the .NET ReadOnlyCollectionBase class. As with my example, you use the ReadOnlyCollectionBase class to create your own class with whatever functionality you need -- but you don't need to write the GetEnumerator method or create your own UnderlyingList because the ReadOnlyCollectionBase handles both.

To create your own version of a read-only class, you start with a class that inherits from ReadOnlyCollectionBase class, like this:

Public Class PhvReadOnlyList(Of T)
  Inherits ReadOnlyCollectionBase

You still need a constructor that accepts an existing List. However, in that constructor you just add your List to the base class' InnerList property using its AddRange method (the InnerList is the ReadOnlyCollectionBase class equivalent of my UnderlyingList):

Public Sub New(InitialList As IList(Of T))
  Me.InnerList = InitialList
End Sub 

And that's all you need to write. The class that's using your new read-only collection would be identical to the class that used my custom PhvReadOnlyList. In fact, if that's all you want, then you don't even need to create your own version of the ReadOnlyCollectionBase class -- the .NET Framework AsReadOnly extension method will do that for you. The AsReadOnly method returns a ReadOnlyCollection object (still just a wrapper around your original List). Rewriting my class to use the AsReadOnly method would give the result shown in Listing 3.

Listing 3. A class rewritten to use the AsReadOnly method.

Public Class PHVClass

  Private ClassList As List(Of String)
  Private rol As ReadOnlyCollection(Of String)

  Public Sub New()
    ClassList = New List(Of String)
    ClassList.Add("Peter")
    ClassList.Add("Jan")
    rol = ClassList.AsReadOnly()
  End Sub

  Public ReadOnly Property AReadOnlyList _
    As ReadOnlyCollection(Of String)
    Get
      Return rol
    End Get
  End Property

End Class

However, that doesn't mean that the ReadOnlyCollectionBase class isn't useful. For instance, by leveraging the InnerList property, you can create a collection that supports whatever custom functionality you need. Perhaps you want to create a collection for audit log entries where users can add but not remove items. To make that happen you just need to support adding new items in your collection class. This code, added to the PhvReadOnlyList based on the ReadOnlyCollectionBase, does just that:

Public Sub Add(Item As T)
  Me.InnerList.Add(Item)
End Sub

Used from code, the Add method would work just the way you'd expect it to:

Dim phvc As New PhvClass()
phvc.AReadOnlyList.Add("Tracey")

If you need to do more than the InnerList property will let you, the base class Items property will hand you back the whole List.

As I said, creating a read-only collection class in .NET is easy to do -- just use AsReadOnly or create a class based on ReadOnlyCollectionBase. But, as you've seen, talking about this object -- and writing your own version of it -- can help you understand some of the fundamentals of .NET.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

  • TypeScript Tops New JetBrains 'Language Promise Index'

    In its latest annual developer ecosystem report, JetBrains introduced a new "Language Promise Index" topped by Microsoft's TypeScript programming language.

Subscribe on YouTube