SingingEels : Development Community & Resource

Login

Articles

  • ADO.NET (2)
  • ASP.NET (36)
  • LINQ (5)
  • Security (2)
  • Silverlight (3)
  • SQL (7)
  • Standards (5)
  • WCF (2)

Syndication

  • Articles RSS
  • Blogs RSS

Contribute

  • Our Authors List
  • Member Sign-Up
  • Suggestions Box

Creating Your Own ListView Control

(Sep 08 2007 - 08:15:49 PM by Timothy Khouri) - [print article]

The Microsoft .NET Framework 3.5 is another huge improvement for web developers. The language enhancements, introduction of LINQ and new web controls all make it a long anticipated upgrade. But have you ever wondered how these new controls are made? Even if you've already read the articles, "The ListView Dominates The Repeater" and "Custom Controls And Control Builders", you'll learn even more about custom control development, and even how to make your own "ListView" control in ASP.NET 2.0.

The tool that I'm going to use to peer into the code of the .NET 3.5 ListView control is Lutz Roeder's Reflector. It's an amazing tool that has helped me to retrieve some of my lost code, as well as to understand how things are done in the .Net framework. However, because the ListView control is a part of the .NET framework 3.5, there is a lot of functionality that we are going to skip over due to the fact that we'd have to pretty much re-write everything.

Basically, we are going to create a control that, like the ListView, allows users to create an "ItemTemplate", and a "LayoutTemplate". They will be able to databind just like a ListView (or a Repeater) and have each item created inside of the LayoutTemplate. We'll also take on the DataKeyNames functionality (common to users of the GridView) that was added in the ListView.

Creating ITemplate Properties - TemplateContainer

The Repeater, ListView and several other controls allow developers to define their own "templates" of how data should be presented. This is a really awesome ability that was brought to us with the introduction of ASP.NET. If you've never made your own ITemplate property then the implementation might be a little illusive, but It's easy to do and it makes sense once you understand it.

First you'll have to create a control that will "hold" you're custom ITemplate instance (which in our case we'll call the "ItemTemplate" and the "LayoutTemplate"). The only rule here is that template container control MUST implement the INamingContainer interface. So if I were to house the LayoutTemplate in it's own control, all I'd have to do is this:

using System;
using System.Web.UI;

public class MyLayoutTemplateContainer : Control, INamingContainer
{
   // That's my entire class... quite sad.

}

Like I said before, the only rule is that your control has to implement the INamingContainer interface. So, this control above, although completely simple, is a valid ITemplate container control. Because there is no benefit to wrapping the LayoutTemplate in yet another control, I'm just going to let the "MyListView" be its template container. Again, this will mean that I have to implement the INamingContainer interface (just like the ListView does).

Creating ITemplate Properties - PersistenceMode.InnerProperty

Now I'll show you how we add our "LayoutTemplate" property on our ListView 'clone' control. Just like any other property that you want people to have the ability to edit in the source, you'll have to make it public, and you'll have to allow people to be able to "set" the value. You can also allow them to "get" the value, but that's not required here. So, here is the code for our LayoutTemplate property:

private ITemplate _layoutTemplate;

[PersistenceMode(PersistenceMode.InnerProperty), TemplateContainer(typeof(MyListView))]
public ITemplate LayoutTemplate
{
   get { return this._layoutTemplate; }

   set { this._layoutTemplate = value; }
}

Again, this is not very hard. As you can see, we are specifying the "MyListView" control (which is what I call the ListView clone) to be the 'template container' for our ITemplate property. The only other noteworthy thing above is that we specified the "PersistenceMode" to be set to "InnerProperty". This tells ASP.NET that we don't want people to set the value of this property through an HTML attribute, but rather, through a child HTML node. Example:

<!-- If we don't set PersistenceMode to InnerProperty -->
<my:Control SomeField="Hello!" />

<!-- If we do set PersistenceMode to InnerProperty -->
<my:Control>
   <SomeField>
       Hello
   </SomeField>
</my:Control>

Reacting To a Databind

Assuming that we also created a template property for the ItemTemplate, we will now look into what we have to do when someone databinds our control. I should also note at this point that instead of inheriting from the basic "Control" or "WebControl" classes, I've chosen to inherit from "DataBoundControl" which gives me "DataSource" properties. Also, I've added a property called "Items" (which I made up) that will reflect all the items that are databound and I've also created my own event called "ItemCreated". This gives people the ability to tap into some code when each item is created in case they want to do some additional functionality.

Let's examine the "DataBound" method step by step to see what we did, and more importantly, why we did it. So as not to break up the code, I'll explain things through C# comments in the method below:

public override void DataBind()
{
   // Starting off small, we want to run whatever code the .NET team felt

   // was important to run when data-binding.

   base.DataBind();

   // To ensure that we are starting off fresh, we'll remove all child

   // controls that might have been added by a previous call to DataBind.

   this.Controls.Clear();

   // Now we'll add the LayoutTemplate to our "Controls" collection.

   this._layoutTemplate.InstantiateIn(this);

   // Next, we need to identify the server side control that is going to

   // hold all of the child ItemTemplate instances.

   Control container = this.FindControl(this._itemContainerID);

   // Now I'll need to create all of the "MyListViewItem" instances that

   // we'll later add to our 'container' control.

   List<MyListViewItem> items = new List<MyListViewItem>();

   foreach (object item in (IEnumerable)this.DataSource)
   {
       MyListViewItem currentItem = new MyListViewItem(item, items.Count);

       items.Add(currentItem);
   }

   // Also, I like to allow future developers to tap into the created

   // items, so I'll assign it to a public property called "Items".

   this._items = items.ToArray();

   // Now we'll actually make each item and add it to our container.

   foreach (MyListViewItem currentItem in this._items)
   {
       if (this._itemTemplate != null)
       {
           this._itemTemplate.InstantiateIn(currentItem);
       }

       // I've also created this custom event in case someone needs

       // to run their own custom code for every item.

       if (this.ItemCreated != null)
       {
           this.ItemCreated(this, new MyListViewItemCreatedEventArgs(currentItem));
       }

       container.Controls.Add(currentItem);

       // If you forget to call "DataBind" on each item, then they

       // will not parse <%# Eval %> statements correctly and other

       // data-binding related issues will come up.

       currentItem.DataBind();
   }
}

Making Your Own DataKeyNames Property

Now that we've finished building all the functionality for the layout and item templates, we'll implement our own "DataKeyNames" (and "DataKeys") properties. This will allow our control to "remember" certain data from the datasource even after posting back. Basically, the way it works is it serializes the data (all of the columns in your DataKeyNames property) into the ViewState.

We'll have to add our properties (DataKeyNames and DataKeys) as well as a little code in the DataBind method to get the data that needs to be serialized. We'll also have to retrieve the values from the viewstate on the event of a post back. You can see the entire source code at the end of the article, but for now I'll skip the properties and go right into the changes we'll need to make in the DataBind method as well as some other methods we'll have to implement.

Lets see how to implement the "DataKeys" items in the "ControlState" (which ultimately goes into the ViewState of the page) so that the data persists across post backs, giving the user the ability to get the original (strongly typed) values that came from the datasource when the control was databound.

// This is the first "foreach" loop that was in our databind method above. We'll
// make a few changes so that we can set the values in our DataKeys property.

foreach (object item in (IEnumerable)this.DataSource)
{
   MyListViewItem currentItem = new MyListViewItem(item, items.Count);

   items.Add(currentItem);

   // Here is where we are adding new code to the method above. We'll check

   // to see if the devevloper has assigned some "DataKeys" that we should

   // remember. If he has, we'll store for later retrieval.

   if (this.DataKeyNames.Length > 0)
   {
       // We'll create a container that is as big as our our DataKeyNames array.

       OrderedDictionary keyTable = new OrderedDictionary(this.DataKeyNames.Length);

       // Now we'll store the values from the current data item.

       foreach (string dataKeyName in this.DataKeyNames)
       {
           object propertyValue = DataBinder.GetPropertyValue(item, dataKeyName);

           keyTable.Add(dataKeyName, propertyValue);
       }

       // Lastly, we'll add this table to an ArrayList so that we can store

       // them in the ViewState later (in our SaveControlState method).

       this.DataKeysArrayList.Add(new DataKey(keyTable, this.DataKeyNames));
   }
}

Something to keep in mind when you are making your own custom controls is that ASP.NET will not call "LoadControlState" or "SaveControlState" by default. Instead, you have to tell the parent "Page" object that this control requires those methods to be called. It's as simple as this:

protected override void OnInit(EventArgs e)
{
   base.OnInit(e);

   // You can call this method almost anywhere, but I choose the OnInit

   // method because it makes sense to me :)

   this.Page.RegisterRequiresControlState(this);
}

The saving and loading of the DataKeyNames in the ViewState is pretty easy to do as well. When saving, we'll have to see if we have values in our ArrayList, and if so, we'll call "SaveViewState" on them and store those items in the ControlState. When loading, we'll basically just perform this functionality in reverse :)

protected override object SaveControlState()
{
   if (this._dataKeysArrayList != null && this._dataKeysArrayList.Count > 0)
   {
       object[] serializedDataKeys = new object[this._dataKeysArrayList.Count];

       for (int i = 0; i < this._dataKeysArrayList.Count; i++)
       {
           serializedDataKeys[i] = ((IStateManager)this.DataKeysArrayList[i]).SaveViewState();
       }

       return new object[]{
                  base.SaveControlState(),
                  this._dataKeyNames,
                  serializedDataKeys
              };
   }
   else
   {
       return base.SaveControlState();
   }
}

protected override void LoadControlState(object savedState)
{
   if (savedState is object[])
   {
       object[] savedStateItems = (object[])savedState;

       base.LoadControlState(savedStateItems[0]);

       this._dataKeyNames = (string[])savedStateItems[1];

       object[] serializedDataKeys = (object[])savedStateItems[2];

       for (int i = 0; i < serializedDataKeys.Length; i++)
       {
           this.DataKeysArrayList.Add(new DataKey(new OrderedDictionary(this.DataKeyNames.Length), this.DataKeyNames));

           ((IStateManager)this.DataKeysArrayList[i]).LoadViewState(serializedDataKeys[i]);
       }
   }
   else
   {
       base.LoadControlState(savedState);
   }
}

Remembering with the ViewState

The last "issue" that needs to be implemented is that of the ViewState. Now, I'm not talking about the work we just did with the DataKeyNames and the ControlState, but rather just the ViewState. The functionality that we want to copy is this: When you databind a ListView control (or Repeater for that matter), and then post back to the server (due to a button click or something)... even if you don't assign a datasource and re-databind, the ListView will remember how it was rendered last time, and it will do it again!

Do not confuse this with the DataKeyNames functionality because the values from the datasource are not stored, nor are they retrievable to the developer. But instead, all that is stored is the "string" representations of those values so that the ListView can re-render itself correctly. The .NET framework is going to handle almost all of the work for you. All that is required is that you "remember" how many items need to be rebuilt.

I'm going to store the "datasource count" inside of the ViewState using under the name "_!ItemCount". You can name it whatever you want, but that's what the ListView and Repeater classes use, so we'll just call it the same thing. Something else you'll need to remember is that if we are rebuilding from the ViewState, we DO NOT want to call ".DataBind" on each of the items that are re-created. This will cause them to forget everything.

Because I have most of the functionality that I want right there in the DataBind method, I'll just add a little bit of code in there (again) that will tell me to re-build the controls, but NOT to call databind if I am rebuilding from the ViewState. Also, if I'll have to make sure not to mess with the DataKeys if this is a simple "re-build" as apposed to a real databind.

Here are the changes I've made:

// I changed this:
if (this.DataKeyNames.Length > 0)

// Into this:

if (isReal && this.DataKeyNames.Length > 0)

And further down...

// I changed this:
currentItem.DataBind();

// Into this:

if (isReal)
{
   currentItem.DataBind();
}

Functionality That Was Ignored

Please note that I have barely scratched the surface of functionality that is found in the ListView. For instance, I've skipped the AlternateItemTemplate, EditItemTemplate, EmptyDataItemTemplate and more. I've also skipped the ability to do two-way binding with a datasource, and I'm not even going to get into pagination right now.

The point of this article is to show how you can begin to create your own 'ListView' like controls, but I'll always recommend that you use the real thing (in scenarios that apply) as apposed to making your own. I suggest to everyone that when the 3.5 framework is fully released (which it will be soon) that you begin using it right away!

Here is a website project made in Visual Studio 2005 that has the full code for the "MyListView" class as well as an example page that has the control and a repeater control on it to show the differences between the two: Singing Eels - ListView Clone.

You must be logged in to add comments. If you have not already done so, you can create an account here. If you already are a member, you first need to login before you can comment.

Developer / Architect / Author

People to Follow

Experts in the categories related to this article.

  • Jonathan Carter

Related Blogs

These are the most recent blog posts related to this article.

  • Follow up to Self Sorting GridView
  • A Change to the MVC "ActionSelectionAttribute"?
  • How to Handle "Side Content" in ASP.NET MVC
  • LINQ to SQL - Am I Hitting The Database?
  • ASP.NET MVC - Issue with CSS Class Name on ActionLinks

Related Ads

SingingEels.com as of Mar 20 2010 - 11:50:29 AM - (0.3593911)