Observable Collections
I didn't think it was possible, but .NET surprised me yet again with a cool feature I never knew existed: The ObservableCollection. This became available in .NET 3.0.
In essence, an ObservableCollection is a collection with an event you can connect to. The event fires when the collection changes. As usual, working with the .NET classes is so ridiculously easy, it feels like cheating.
However, using the ObservableCollection, or any other object with an event handler, can be tricky in Asp.Net. Asp.Net member RichardD pointed out some errors in my original post and I would like to thank him for his comments. He also showed me a better way to use HttpContext.Current.Handler which simplified the code. However, since his comments were no longer relevant after the rewrite I deleted them…sorry buddy.
The following is small test program to illustrate how the ObservableCollection works. To start, create an ObservableCollection and then store it in the Session object so it will persist between page post backs. I also added the code to pull it out of Session state when there is a page post back:
public partial class _Default : System.Web.UI.Page
{
public ObservableCollection<int> MyInts;
// ---- Page_Load ------------------------------
protected void Page_Load(object sender, EventArgs e)
{
if (IsPostBack == false)
{
MyInts = new ObservableCollection<int>();
MyInts.CollectionChanged += CollectionChangedHandler;
Session["MyInts"] = MyInts; // store for use between postbacks
}
else
{
MyInts = Session["MyInts"] as ObservableCollection<int>;
}
}
Here's the event handler I hooked up to the ObservableCollection, it writes status strings to a ListBox. Note: the event handler is a static method.
// ---- CollectionChangedHandler -----------------------------------
//
// Something changed in the Observable collection
// Member is static, otherwise it is a reference to the page
// object and the object will not be Garbage Collected.
static public void CollectionChangedHandler(object sender, NotifyCollectionChangedEventArgs e)
{
_Default CurrentPage = System.Web.HttpContext.Current.Handler as _Default;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
CurrentPage.ListBoxHistory.Items.Add("Add: " + e.NewItems[0]);
break;
case NotifyCollectionChangedAction.Remove:
CurrentPage.ListBoxHistory.Items.Add("Remove: " + e.OldItems[0]);
break;
case NotifyCollectionChangedAction.Reset:
CurrentPage.ListBoxHistory.Items.Add("Reset: ");
break;
default:
CurrentPage.ListBoxHistory.Items.Add(e.Action.ToString());
break;
}
}
In the original post, I had the event handler as a normal member function of the page. This caused two problems:
- Since the ObservableCollection was put in Session state, and it had a reference to the page object, the initial page object was NOT released for garbage collection. I verified this by putting trace statements in the page’s constructor and destructor and verified the initial page object persisted. On subsequent post backs the page objects were created, released and discarded correctly. The initial page object remained in memory...not too big a deal but it just ain't right.
- A member function of a page should have access to the page’s controls. However, since the function was in the original page object that was no longer active, it did not have access to any controls in the current page object that were to be rendered and sent to the browser.
Next, add some buttons and code to exercise the ObservableCollection:
<button id="ButtonAdd" önclick="ButtonAdd_Click" text="Add" runat="server" />
<button id="ButtonRemove" önclick="ButtonRemove_Click" text="Remove" runat="server" />
<button id="ButtonReset" önclick="ButtonReset_Click" text="Reset" runat="server" />
<button id="ButtonList" önclick="ButtonList_Click" text="List" runat="server" />
<br />
<textbox id="TextBoxInt" runat="server" width="51px" />
<br />
<listbox id="ListBoxHistory" runat="server" width="195px" height="255px">
// ---- Add Button --------------------------------------
protected void ButtonAdd_Click(object sender, EventArgs e)
{
int Temp;
if (int.TryParse(TextBoxInt.Text, out Temp) == true)
MyInts.Add(Temp);
}
// ---- Remove Button --------------------------------------
protected void ButtonRemove_Click(object sender, EventArgs e)
{
int Temp;
if (int.TryParse(TextBoxInt.Text, out Temp) == true)
MyInts.Remove(Temp);
}
// ---- Button Reset -----------------------------------
protected void ButtonReset_Click(object sender, EventArgs e)
{
MyInts.Clear();
}
// ---- Button List --------------------------------------
protected void ButtonList_Click(object sender, EventArgs e)
{
ListBoxHistory.Items.Add("MyInts:");
foreach (int i in MyInts)
{
// a bit of tweaking to get the text to be indented
ListItem LI = new ListItem(" " + i.ToString());
LI.Text = Server.HtmlDecode(LI.Text);
ListBoxHistory.Items.Add(LI);
}
}
Here's what it looks like after entering some numbers and clicking some buttons:
An interesting note: From the online help:
Any public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe
What does that mean to Asp.Net developers?
If you are going to share an ObservableCollection among different sessions, i.e. different threads, you’d better make it a static object and not use the Application state collection….right? WRONG!
The documentation is easily misread (there are a LOT of posts on this subject on a lot of web sites). It means static MEMBERS of the ObservableCollection class are thread-safe, not a static instance of the class itself. If the collection is to be shared among threads, you have to synchronize the access. Once again, I’d like to thank RichardD for his constant nagging and badgering me to get it right. Thanks pal!
I hope someone finds this useful.
Steve Wellens