Recently I had the need to implements some remoting solutions and I ran into a problem. After googling around a bit, I confirmed that the .NET Framework does not provide you with a remoting-compatible collection (if someone knows of one, please let me know). So I went about writing one. I wanted a strongly typed collection which I used to achieve by inheriting from CollectionBase and adding the strongly typed implementation. In 2.0, I started inheriting from the List<T> generic collection which was even better. The problem is that neither of these two objects inherit from MarshalByRefObject, which is required for remoting solutions.
My solution was to create a new class (BusinessCollectionBase), accept a generic for the kind of object it will contain (TBusinessObject), and add a protected property as type List<TBusinessObject>. This class inherits from MarshalByRefObject which makes it usable in a remoting situation. Now, the trick was to expose everything I had to and map it to the internal list. The trick to doing this is to implement all the correct interfaces and perform the mapping there. This way, any collection object you create that inherits from this class will have all the necessary enumeration and binding compatibility of any standard collection (this is the luxury we got inheriting from CollectionBase or DictionaryBase in the past, or one of the generic collections now).
The result is listed below. Notice the implementation of both the generic- nterfaces and the non-generic interfaces. This is very important for the collection to be both strongly typed and bindable to visual controls. Also notice that the implementations of the non-generic interfaces have been changed to Protected, as opossed to the default Public. They are only needed for the data binding compatibility where the control doing the binding is casting to the appropriate interface anyway. The generic-compatible implementations are the only thing I want to expose because they provide the strong typing.
You can of course alter this to accomodate any type of collection you want. In this case I used a List<T>, but you can contain anything that suits your needs.
Also, notice that I added a couple of methods for the class to support serialization. Though usually tagging it “Serializable“ is all that is necessary, with these methods, you can actually return a string representation of your collection so you can put it into a cookie, if you're using it on the web.
If anyone further modifies or enhances this, I'd love to hear about it.
Imports System
Imports System.Collections
Imports System.Collections.Generic
Imports System.Xml.Serialization
Namespace InfoTek.Business
Public MustInherit Class BusinessCollectionBase(Of TBusinessObject As New)
Inherits MarshalByRefObject
Implements IList(Of TBusinessObject)
Implements IList
Implements ICollection(Of TBusinessObject)
Implements ICollection
Implements IEnumerable(Of TBusinessObject)
Implements IEnumerable
Protected innerList As List(Of TBusinessObject) = New List(Of TBusinessObject)
Public Function Add() As TBusinessObject
Dim o_Item As TBusinessObject = New TBusinessObject()
innerList.Add(o_Item)
Return o_Item
End Function
Public Function SerializeObject() As String
Dim o_MemStream As MemoryStream = New MemoryStream
Dim o_Serializer As XmlSerializer = New XmlSerializer(Me.GetType())
o_Serializer.Serialize(o_MemStream, Me)
Return ASCII.GetString(o_MemStream.ToArray())
End Function
Public Function DeserializeObject(ByVal xmlString As String) As Object
Dim o_Serializer As XmlSerializer = New XmlSerializer(Me.GetType())
Dim o_MemStream As MemoryStream = New MemoryStream(ASCII.GetBytes(xmlString))
Return o_Serializer.Deserialize(o_MemStream)
End Function
#Region "ICollection(Of TBusinessObject) implementation"
Public Sub Add(ByVal item As TBusinessObject) Implements System.Collections.Generic.ICollection(Of TBusinessObject).Add
CType(innerList, ICollection(Of TBusinessObject)).Add(item)
End Sub
Public Sub Clear() Implements System.Collections.Generic.ICollection(Of TBusinessObject).Clear
CType(innerList, ICollection(Of TBusinessObject)).Clear()
End Sub
Public Function Contains(ByVal item As TBusinessObject) As Boolean Implements System.Collections.Generic.ICollection(Of TBusinessObject).Contains
Return CType(innerList, ICollection(Of TBusinessObject)).Contains(item)
End Function
Public Sub CopyTo(ByVal array() As TBusinessObject, ByVal arrayIndex As Integer) Implements System.Collections.Generic.ICollection(Of TBusinessObject).CopyTo
CType(innerList, ICollection(Of TBusinessObject)).CopyTo(array, arrayIndex)
End Sub
Public ReadOnly Property Count() As Integer Implements System.Collections.Generic.ICollection(Of TBusinessObject).Count
Get
Return CType(innerList, ICollection(Of TBusinessObject)).Count
End Get
End Property
Public ReadOnly Property IsReadOnly() As Boolean Implements System.Collections.Generic.ICollection(Of TBusinessObject).IsReadOnly
Get
Return CType(innerList, ICollection(Of TBusinessObject)).IsReadOnly
End Get
End Property
Public Function Remove(ByVal item As TBusinessObject) As Boolean Implements System.Collections.Generic.ICollection(Of TBusinessObject).Remove
Return CType(innerList, ICollection(Of TBusinessObject)).Remove(item)
End Function
#End Region
#Region "IEnumerable(Of TBusinessObject) implementation"
Public Function GetEnumerator() As System.Collections.Generic.IEnumerator(Of TBusinessObject) Implements System.Collections.Generic.IEnumerable(Of TBusinessObject).GetEnumerator
Return CType(innerList, IEnumerable(Of TBusinessObject)).GetEnumerator()
End Function
#End Region
#Region "IList(Of TBusinessObject) implementation"
Public Function IndexOf(ByVal item As TBusinessObject) As Integer Implements System.Collections.Generic.IList(Of TBusinessObject).IndexOf
Return CType(innerList, IList(Of TBusinessObject)).IndexOf(item)
End Function
Public Sub Insert(ByVal index As Integer, ByVal item As TBusinessObject) Implements System.Collections.Generic.IList(Of TBusinessObject).Insert
CType(innerList, IList(Of TBusinessObject)).Insert(index, item)
End Sub
Default Public Property Item(ByVal index As Integer) As TBusinessObject Implements System.Collections.Generic.IList(Of TBusinessObject).Item
Get
Return CType(innerList, IList(Of TBusinessObject)).Item(index)
End Get
Set(ByVal value As TBusinessObject)
CType(innerList, IList(Of TBusinessObject)).Item(index) = value
End Set
End Property
Public Sub RemoveAt(ByVal index As Integer) Implements System.Collections.Generic.IList(Of TBusinessObject).RemoveAt
CType(innerList, IList(Of TBusinessObject)).RemoveAt(index)
End Sub
#End Region
#Region "IEnumerable implementation"
Protected Function GetEnumerator1() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
Return CType(innerList, IEnumerable).GetEnumerator()
End Function
#End Region
#Region "ICollection implementation"
Protected Sub CopyTo1(ByVal array As System.Array, ByVal index As Integer) Implements System.Collections.ICollection.CopyTo
CType(innerList, ICollection).CopyTo(array, index)
End Sub
Protected ReadOnly Property Count1() As Integer Implements System.Collections.ICollection.Count
Get
Return CType(innerList, ICollection).Count
End Get
End Property
Protected ReadOnly Property IsSynchronized() As Boolean Implements System.Collections.ICollection.IsSynchronized
Get
Return CType(innerList, ICollection).IsSynchronized
End Get
End Property
Protected ReadOnly Property SyncRoot() As Object Implements System.Collections.ICollection.SyncRoot
Get
Return CType(innerList, ICollection).SyncRoot
End Get
End Property
#End Region
#Region "IList implementation"
Protected Function Add1(ByVal value As Object) As Integer Implements System.Collections.IList.Add
Return CType(innerList, IList).Add(value)
End Function
Protected Sub Clear1() Implements System.Collections.IList.Clear
CType(innerList, IList).Clear()
End Sub
Protected Function Contains1(ByVal value As Object) As Boolean Implements System.Collections.IList.Contains
Return CType(innerList, IList).Contains(value)
End Function
Protected Function IndexOf1(ByVal value As Object) As Integer Implements System.Collections.IList.IndexOf
Return CType(innerList, IList).IndexOf(value)
End Function
Protected Sub Insert1(ByVal index As Integer, ByVal value As Object) Implements System.Collections.IList.Insert
CType(innerList, IList).Insert(index, value)
End Sub
Protected ReadOnly Property IsFixedSize() As Boolean Implements System.Collections.IList.IsFixedSize
Get
Return CType(innerList, IList).IsFixedSize
End Get
End Property
Protected ReadOnly Property IsReadOnly1() As Boolean Implements System.Collections.IList.IsReadOnly
Get
Return CType(innerList, IList).IsReadOnly
End Get
End Property
Protected Property Item1(ByVal index As Integer) As Object Implements System.Collections.IList.Item
Get
Return CType(innerList, IList).Item(index)
End Get
Set(ByVal value As Object)
CType(innerList, IList).Item(index) = value
End Set
End Property
Protected Sub Remove1(ByVal value As Object) Implements System.Collections.IList.Remove
CType(innerList, IList).Remove(value)
End Sub
Protected Sub RemoveAt1(ByVal index As Integer) Implements System.Collections.IList.RemoveAt
CType(innerList, IList).RemoveAt(index)
End Sub
#End Region
End Class
End Namespace
Check out www.dnrtv.com - yours truly has episodes 1 and 2 on Developing Custom Webcontrols (of course)
This is a hybrid between a webcast and a standard .NET Rocks interview, but much more technical.
It's a flash presentations where you get to witness code being developed, and at the same time there is a lot of interaction between Carl and I. He can see my screen from his monitor so he interviews me and asks me questions specifically about what I am doing at the time. I think the results made for a very informative format.
Hope to do another one soon.
And unfortunately, it's a detail I always seem to forget whenever I teach Webcontrol development.
Anyone who has ever taken my webcontrol class or has heard my user group presentations about them, knows that I start by telling you the simplest thing: webcontrols render HTML to the browser. Now, while this has become pretty much common knowledge in the ASP.NET community, here's one detail that I haven't even seen the books tell. A standard composite control inherits from the System.Web.UI.WebControl. A very common practice is to build an HTML table inside your composite control to surround any child controls you may have. The problem is that the WebControl class by default, renders the control container as a Span tag in HTML. In fact if you create an empty WebControl with no children and run the page, you'll just see an empty Span tag. The Span tag is an inline element, while the Table tag is a block element. The HTML specification states that a block element cannot be contained inside an inline element - only the other way around. For those that don't understand the difference: the main characteristic of these two types of HTML elements is that inline elements can go next to each other, while block elements cannot. Ever try putting two Tables next to each other? Doesn't work. You have to put them in side-by-side cells of another table. So if you develop a composite control and build a table surrounding its child controls, you end up with illegal HTML. The problem is that IE is so forgiving and lets you get away with it without screaming at you. Well, the fix is easy.
If you inherit from the WebControl class, you need to override the default constructor of your control and call its base constructor with a certain argument:
Public Sub New()
MyBase.New(HtmlTextWriterTag.Div)
End Sub
or
public WebControl1() : base(HtmlTextWriterTag.Div) { }
In the C# example, I am assuming the webcontrol class is called WebControl1.
Notice the argument I am sending into the base constructor. It's an enum that Microsoft gives us for almost every HTML tag value. In this case, we are changing the default tag that is rendered as the control container to Div. Div is a block element and allows the legal containment of other block elements (or inline elements) such as the Table tag.
In ASP.NET 2.0, we have a new class from which to inherit composite controls; it's called CompositeControl. This class does a few things automatically for you, such as calling EnsureChildControls before rendering and implementing INamingContainer. But it does not correct the default container tag. I say “the correct” one because my opinion is that the default tag for composite controls should be Div, not Span. Overriding the default constructor on a webcontrol that inherits from the CompositeControl class does not work because it is not able to acces the overload with the argument we need. In this case, we have a property we can override, called TagKey:
Protected Overrides ReadOnly Property TagKey() As System.Web.UI.HtmlTextWriterTag
Get
Return HtmlTextWriterTag.Div
End Get
End Property
or
protected override System.Web.UI.HtmlTextWriterTag TagKey
{
get { return HtmlTextWriterTag.Div; }
}
In this case, we're achieving the same thing just in a different manner.
So the important thing to remember is to do this if you are going to start your Controls collection with a block element like Table.
Don't forget this !!!