A Crash Course on Custom ASP.NET Data-bound Controls

ata-bound controls make ASP.NET programming much easier. They expose a bunch of properties and methods to link their properties to a valid data source and they know how to build their user interface to reflect the contents of the data source. Data-bound controls work by repeating a template for each data row, and they try to optimize their use of system resources such as ViewState. This article guides you to building a real-world, data-bound control that displays a pure HTML bar chart.

Several years ago when the word “online” made most people think only of a bright and kind voice over the phone, I asked a friend about his company’s Web site. At the time, my friend was employed by the IT department of a very popular museum engaged in a cutting edge project?publishing digital pictures of their artwork on the Web.

When the framework calls the DataBind method, data-bound controls get fresh data and update their user interface. When this happens, it’s a good safety habit to clean up child controls and memory.

The very first version of that site?I really doubt it ever went online?was written using pure HTML. Yes, you got it right-no server-side technologies, no active pages, no data-binding. The site was simply a collection of HTML pages each padded with an tag and a few heading blocks.

We were sipping our espresso coffee at a bar in Rome when he asked the key question. “Do you know of any technologies to help us generalize the structure of the site?” he tentatively asked. As you imagine, they were looking for a tool to read data out of a database and populate some predefined tags in the pages. Moreover, they were really dreaming of the capability of using just one parametric page to show all of the pictures.

No more sardonic grins, please-that was really a cutting edge project for the time. Did I say that there was no version of Active Server Pages (ASP) around yet?

Now, a decade later, we’re so accustomed to the idea of data binding and server programming that the idea of building distinct pages with a hard-coded tag for publishing distinct pictures seems absurd.

Data binding has evolved a lot in the past ten years. In this article, I’ll explore what it means today to ASP.NET developers and provide a crash course on how to build rich, powerful, and effective custom data-bound controls.

Basics of Data Binding
Data-binding is a technology designed to establish a connection between a data source and a server component. In ASP.NET 1.x, data-binding is a way to link enumerable objects to control properties. By the way, this definition will be enhanced and refined in ASP.NET 2.0 to support more than just enumerable objects as the data source.

In the context of data-binding, data-bound controls are an essential building block. Data-bound controls expose a bunch of properties and methods to link to a valid data source. In addition, a data-bound control is designed to incorporate, in its own user interface, any data it gets from the source.

ASP.NET comes with a number of pre-defined data-bound controls. Examples include the DataGrid, Repeater, DropDownList, and ListBox controls. More importantly, ASP.NET allows you to build your own, custom data-bound controls.

When it comes to building new ASP.NET controls, you have two basic options: inherit from an existing control that already provides most of the features you need, or build your new control from scratch. In most real-world cases, the second option is the only one worth considering. Unfortunately, in real ASP.NET applications, you must either live with the pre-defined data-bound controls or commit to building complex and feature-rich new ones.

The ASP.NET 1.x framework doesn’t provide an effective diagram of classes for data-bound controls. For one thing, there’s no common base class for all data-bound controls. Some controls, though, are grouped logically on a per-function basis and inherit from intermediate, abstract classes such as ListControl and BaseDataList. The overall design of the diagram is not consistent, however, and doesn’t encourage developers to take advantage of it. Add some less-than-perfect documentation and you get a clear picture.

Thankfully, things are going to change with ASP.NET 2.0 as this article from the ASP.NET site reports.

In summary, a custom data-bound control is often a composite control that pumps data out of a source. This control builds its own user interface by composing together various existing controls in a table or flow layout. The control’s interface may contain links and buttons and generate events and postbacks. As a control developer, you should use the ViewState but avoid storing too much data there. Finally, the control must be able to redesign itself without requiring fresh data on each postback. In other words, any postback generated outside the control should not empty the data source of the control. This point may sound like an unnecessary remark; it is, instead, just one of the most common pitfalls for control developers.

What We’re Going to Do
In the rest of the article, I’ll develop an HTML charting control that reads data from an enumerable data source and builds a bar chart. No images or graphics will be used?the resulting bar chart will be nothing more than a colorful HTML table.

In the sample BarChart control, the Items collection is populated during the data binding step and consumed at rendering time.

Table 1 lists all the key members that a data-bound control should have. Filling the table, I considered properties and methods that are commonly used. You might not agree with my choices for some of the properties, or you might need to add a few more or different members. Just take the content of Table 1 for what it really is?an indicative table of members for realistic data-bound controls.

Table 1: Typical members of a data-bound control.

Member

Description

DataSource

Property of type object gets or sets the data source that the control is displaying data for.

DataTextField

String property gets or sets the data source field to use to label the data-bound items of the control. The exact role of this property depends on the user interface of the control.

DataValueField

String property gets or sets the data source field to use to determine the value of data-bound items of the control. The exact role of this property depends on the user interface of the control.

Items

Collection property exposes the list of items that form the user interface of the control. The exact role of this property depends on the user interface of the control. A variation of this property (that is, an additional similar property or a different implementation of this one), can also be used to extend the data source of the control.

DataBind

Built-in, method you can override to fire the data binding process

CreateChildControls

Protected method you can override to build the user interface of the control. For complex controls, using this method is preferable to overriding the Render method.

CreateControlHierarchy

Internal method that most built-in controls use to factor out the process of creating the user interface of the control.

Style properties

Set of Style-derived objects used to give a graphical skin to parts of the control (e.g., header, items, footer).

In addition, a data-bound control usually fires a few events to notify callers of some key operations it is performing. Good examples of events to fire are the creation of any constituent items and the binding of these items to data elements. To be really effective and powerful, each data-bound control should fire a pair of events like the ItemCreated and ItemDataBound events in the DataGrid control. The output of a data-bound control must always be customizable enough through styles and properties. However, item-related events add more power as they enable developers to apply context-sensitive customization based on runtime conditions.

?
Figure 1: The BarChart Control: The figure shows the BarChart sample control in action.

Let’s go on and build an HTML bar charting control which, in the end, will look a lot like the control in Figure 1.

What are the building blocks of the control in the figure? To start off, the control has a twofold header?title and subtitle, each with its own style. Next, it features a handful of data items and a footer that is not shown in the figure. The whole control renders out like an HTML table where each data item is a table row with two cells?label and value. The value cell is made of a couple of Label controls.

By the way, I originally wrote the BarChart control as one of the examples for my book Programming ASP.NET, from Microsoft Press. Portions of that control also found their way to an MSDN Magazine article (June 2003). The BarChart control described here is based on the same idea, but it is powered by substantially different code. The source has been cleaned up, refactored, made simpler?in short, rewritten from scratch.

It’s my pleasure to give credit to Steve Orr for his great idea of using a Label control with the Width property set to a percentage. Steve’s article is online at http://steveorr.net/articles/BarGraphs.aspx.

The DataSource Property
In ASP.NET 1.x, a data-bound control is centered around the DataSource property. The property points to the object that feeds the control with data. The final user interface of the control reflects the contents of the data source object. So when you get to write a new custom data-bound control, adding the DataSource property is the first step. The following code shows the typical implementation, borrowed from one of the many ASP.NET native data-bound controls.

   private object _dataSource;   public object DataSource      {   get {return _dataSource;}   set          {   if (value == null ||    value is IListSource ||    value is IEnumerable)               _dataSource = value;   else   throw new ArgumentException();         }      }

As the preceding code demonstrates, the data source is not persisted anywhere, let alone in the page’s ViewState. Furthermore, the set accessor of the property performs a type checking to ensure that invalid objects are not bound. Null is usually considered a valid value for the DataSource property, and so it is for any object that implements either the IEnumerable or the IListSource interface. It goes without saying that the code snippet above is only a sample. You can modify the implementation at will to make the set accessor throw a different exception, accept a particular type, or refuse null values.

IEnumerable is the base interface for .NET data binding. It exposes an enumerator object to support a simple iteration over a collection. Any managed object that implements, directly or indirectly, IEnumerable can be associated to a data-bound control.

You typically use IListSource to return a list that can be used as a data source from an object that does not implement IEnumerable itself. The interface is supported by most built-in data-bound controls and implemented by a couple of key data objects?DataSet and DataTable. In other words, if you can bind a DataTable to a DataGrid or a DropDownList, it’s because of the IListSource interface.

   private class DataTable : MarshalByValueComponent,      IListSource, ISupportInitialize, ISerializable

If you ever wrote a custom ASP.NET control, you know that most of the public properties are persisted in the ViewState. As mentioned earlier, this doesn’t happen to be the case with DataSource. The reason is that the data source is potentially too large of an object to be effectively stored in the ViewState. Storing the data source in the ViewState is not an option?doing that would have serious repercussions on the performance of the page.

So what’s up with non-persistent properties when the page that hosts the control posts back? Those properties are blank upon postback and must be reinitialized to work properly across two successive requests.

Other Data-binding Properties
The DataSource property references the global data source object bound to the control. In general, a few more properties are required to select particular columns of data out of the source. How many, and which, columns depends on the characteristics of the control. For example, the built-in DropDownList control features two additional properties: DataTextField and DataValueField. The former indicates the data source column to be used as the display text of each listed item. The latter indicates the data source column that provides the underlying values for the items. For example, take a look at the HTML snippet below that renders a drop-down list.

   

The body of each tag contains the display text; the value attribute of the tag contains a likely unique value to identify the element. The ASP.NET DropDownList control just uses DataTextField and DataValueField properties to bind distinct columns of the data source to the text and value of each tag being generated.

You can apply a similar pattern to virtually any data-bound control. For example, hyperlink columns of a DataGrid feature a DataNavigateUrlField property to let you bind a data source property to the URL of the hyperlink.

The BarChart control here needs a couple of properties to data-bind the label and value of each rendered bar. You can choose any name for these properties; I stuck to classic DataTextField for the bar label and DataValueField for the bar value. Here’s the implementation of DataTextField.

   private void Form1_Load(object sender, _      System.EventArgs e)   {         public string DataTextField      {         get          {            object o = ViewState["DataTextField"];            if (o == null)               return "";            return (string) o;         }         set {ViewState["DataTextField"] = value;}      }   }

The DataValueField is nearly identical?just change the property name and you have it. The content of the property is saved to and read from the page’s ViewState. Note the convention used to assign a default value to a property in the get accessor.

The value of the property is first read out of the ViewState and assigned to an object. If the reference is null, you return what’s expected to be the default value of the property. If the reference is not null, you cast it to the right type and return. Properties of built-in ASP.NET controls are coded in this way. There’s no need to initialize properties in the class constructor.

To make the control more complete, I’ve also added a couple of extra string properties to add formatting to both the label text and value. The properties are named DataTextFormatString and DataValueFormatString. Their implementation closely reflects the code snippet above.

The Items Property
In a data-bound control, the Items property can have a double goal. In simple list controls such as DropDownList and ListBox, the Items property returns a reference to the list of items that are currently stored in the control. With this reference, you can add items, remove items, and obtain a count of the items in the collection. Put another way, the Items collection is an alternative data source that can be used along with the contents of the DataSource property. You can populate a ListBox with the following code, for instance.

   for (int i = 1; i <= 50; i++)   {      list.Items.Add("Item " + i.ToString());   }

More complex data-bound controls, such as the DataList, assign a different role to the Items property. The property gets a collection of objects representing the individual items within the control. The exact configuration of this object depends on the control. For the DataList, an item is an instance of the HTML template used to render a data-bound item?mostly a table row. For the BarChart control, the Items property returns a custom collection, as shown below.

   [Browsable(false)]   public virtual BarChartItemCollection Items   {      get       {         if (_items == null)            _items = new BarChartItemCollection();         return _items;      }   }

The source code of the BarChartItemCollection class is presented in Listing 1.

If you look back at Figure 1, you should grasp what items are here pretty easily. An item is a row in the table output by the BarChart control. The row contains a couple of cells?label and graphical representation of the bar. Just because an item here is a table row, the BarChartItem class (Listing 1) inherits from TableRow.

A BarChartItem object is simply an enhanced version of a table row, extended to contain item type and data item properties. ItemType indicates the type of the item in the context of the BarChart control?be it the title row, the subtitle row, a data row, or perhaps the footer. DataItem references the n'th data object in the enumerable collection bound to the control. If you're only a little bit familiar with DataList or DataGrid internals, this approach should be nothing new. Classes like DataGridItem and DataListItem are implemented in a similar manner.

All data-bound controls of some complexity expose the internal collection of items as a way to ease developers with any required customization. Internally, the collection of constituent items helps immensely with the rendering of the control, as you'll see in a moment.

An important thing to keep in mind about the Items property is that, as implemented here, it doesn't represent an extension to the data source. You can't add data to the control through this property. The Items property is read-only in the sense that page developers can't assign or replace it with another collection. However, you can add or remove items to the collection as long as the underlying collection class permits that. Finally, the name of the property is totally arbitrary. Likewise, your control can have as many similar collections as needed.

In the BarChart control, the Items collection is populated during the data binding step and consumed at rendering time.

As the code snippet above shows, the Items collection is not saved to the ViewState. I'll return to this later, as it is a critical point for the behavior of complex and composite data-bound controls.

The DataBind Method
Listing 2 shows the typical implementation of the DataBind method in a data-bound control. To start off, the method invokes itself on the base class. Next, it clears the collection of child controls and their ViewState. When the DataBind method is called, data-bound controls get fresh data and update their user interface. When this happens, it's a good and safe habit to clean up child controls and memory.

The control's ViewState is reset and an internal engine that tracks changes to the control's ViewState is turned on. (This is what TrackViewState really does.) Finally, the control builds the user interface.

The user interface of a typical composite control consists of a hierarchy of child controls. The method CreateControlHierarchy creates this hierarchy for the BarChart control. In ASP.NET 1.x, CreateControlHierarchy is not a framework method to override; however, most data-bound controls define it internally as a protected and virtual member. In this way, inherited controls can easily override it thus modifying the rendering of the control. Listing 3 shows the source code of the method.

By design, when CreateControlHierarchy returns all child controls have been created. This situation is notified to base classes through a writeable Boolean property?ChildControlsCreated. Defined on the Control class, the property just indicates whether the server control's child controls have been created.

Building the Control's User Interface
As Listing 3 shows, the CreateControlHierarchy method accepts a Boolean argument that the DataBind method sets to true. What's the role of this argument? Some hints come through the formal name of the parameter?useDataSource.

CreateControlHierarchy gets an enumerable collection of data and then starts building the BarChart's hierarchy of child controls. The BarChart control consists of an outermost table with a bunch of rows. The first row contains the title of the chart; the second row shows the subtitle. These items are not data-bound meaning that the rendering of these components is not influenced by the data source contents. The following code snippet demonstrates the creation of the title row. The row contains a single cell that spans over the number of columns. The BarChart control contains two columns?label and value.

   private void CreateTitle(Table t)   {      // Create the table row      BarChartItem item = new BarChartItem(          BarChartItemType.Title);      t.Rows.Add(item);      TableCell cell = new TableCell();      cell.ColumnSpan = BarChart.ColumnsCount;      cell.Text = Title;      //      // Fire HERE the ItemCreated event      item.Cells.Add(cell);   }

Next, the CreateControlHierarchy method creates all data-bound items and completes the structure with the footer row. Listing 4 details the creation of all data-bound items.

The Boolean argument originally passed to CreateControlHierarchy is forwarded to a couple of internal methods?GetDisplayData first and CreateBarChartItem next. The Boolean argument indicates how to build the control's infrastructure?from the data source or the ViewState. In other words, if the parameter useDataSource is true, the DataSource property is set to a non-null value; otherwise, the data source is empty and the BarChart control must rebuild itself from the ViewState.

Before going any further with more details, I need to bear down on the reasons that determine the value of the useDataSource parameter. When a data-bound control is first populated?that is, a call to DataBind is made?the parameter is set to true and a valid data source object is available.

When the host page posts back, the control needs to redraw itself but the data source may not be there. If the sender of the postback is the control itself, you handle one of its events and rebind fresh data. But what if the sender of the postback is another control in the page?

To minimize the workload, the control should not be re-bound to data if the data to display hasn't changed. So how can the control refresh?

The page ViewState doesn't contain the whole data source as a separate object, but each and every constituent control saved its visual properties to the ViewState. To make the BarChart data-bound control survive a postback, you must write its rendering code so that the phase of creation of constituent controls is separated from the data binding phase. In this way, the structure is correctly recreated after each postback and the binding takes place only if necessary. In addition, each constituent control?table cells, labels, textboxes?will automatically restore themselves from the ViewState thus recreating the overall interface. Let me illustrate this with an example.

You know from Figure 1 that the BarChart control is made of a collection of rows. Each row has two cells, the second of which is generated by the following code.

   Label lblGraph = new Label();   Label lblText = new Label();   cell.Controls.Add(lblGraph);   cell.Controls.Add(new LiteralControl("
")); cell.Controls.Add(lblText);

The horizontal bar is rendered through a Label control whose Width property is set to a percentage that represents the associated value. The Width property is assigned at binding time:

   Label lblGraph = (Label) cell.Controls[0];   object o = DataBinder.Eval(dataItem,       DataValueField);   float val = Convert.ToSingle(o);   float perc = 100*val/Maximum;   lblGraph.Width =Unit.Percentage(perc);

When the page with the BarChart control posts back, what really matters is that the Label control be recreated in the same place. The Label control will then restore itself from the ViewState and maintains the assigned width.

The number of bars depends on the bound data source. When there's no data source?that is, after a postback?how can you determine the correct number of bars that need to be displayed? By design, this information is known only at binding time and to access it later you must save it somewhere. Where? In the ViewState.

   if (useDataSource)      ViewState[ViewStateItemCount] = itemCount;

This trick is widely used in most ASP.NET built-in controls (e.g., DataGrid) and sets a clear dependency between these controls and the ViewState. For the trick to work, though, it is essential that you add each newly created control to its parent in the hierarchy as soon as possible?ideally, right after instantiation.

Getting Data Source Elements
The CreateControlHierarchy method is invoked during the binding phase to retrieve an enumerable object to use as the data source. The same method is also used to build the control's user interface when the page posts back. In this case, no DataBind method is ever invoked. So who's the caller?

Under certain conditions, CreateControlHierarchy is invoked from within the CreateChildControls method?a protected overridable method defined on the root Control class.

   protected override void CreateChildControls()   {      // Clears all child controls      Controls.Clear();      // If the control is being redrawn because       // of a postback, restore from the ViewState      if (ViewState[ViewStateItemCount] != null)         CreateControlHierarchy(false);   }

If there's no valid information at the known location in the ViewState, the method exits; otherwise, it requires the creation of the control's hierarchy from the ViewState.

As in Listing 3, CreateControlHierarchy invokes a method named GetDisplayData. This method retrieves the enumerable collection of data for building the control's user interface. The full source code of the GetDisplayData method is shown in Listing 5. The method always returns a non-empty collection with as many elements as there are (or there should be) items in the final control. If called from within a binding operation, GetDisplayData just returns the contents of the DataSource property. If called from within a ViewState load operation, the method returns a properly sized, but empty array. The size of the array is read from the ViewState and reflects the number of bars to display.

Note that most ASP.NET controls persist just one value in the ViewState?typically, the number of rows to rebuild. This behavior is arbitrary and merely suggested by the control's architecture. There's no limitation or problem in persisting more values.

A data-bound control written in accordance with the rules and guidelines hitherto detailed offers two key benefits. First, it works as expected and survives postbacks in the most efficient way. Second, it has a neat and modular internal architecture that is fairly easy to understand, modify, and extend.

There's no need for you to add a Render method unless you want to add styling capabilities to the control.

Styling the Control
The richer a control is, the more it requires visual attributes. A realistic implementation of the BarChart control, for example, would require border, font, and colors for title, subtitle, footer, bar, and labels. Each property is fairly easy to code, but the overall number of properties would soon make it unmanageable. So what's the way out? Style properties.

A style property groups in a single object a variety of visual and graphical settings. In addition, it leaves to the control infrastructure the burden of applying style attributes to individual elements. Finally, Visual Studio .NET 2003 provides great support for styles through the expandable editor in the Properties window of the solution. Overall, reasoning in terms of styles rather than properties makes the whole approach more neat and elegant.

Here's what a style is in this context.

   private TableItemStyle _titleStyle;   public virtual TableItemStyle TitleStyle   {      get      {         if (_titleStyle == null)         {            _titleStyle = new TableItemStyle();            // Can initialize the header HERE          }         if (IsTrackingViewState)         {            IStateManager ism;            ism = (IStateManager) _titleStyle;            ism.TrackViewState();            return _titleStyle;         }      }   }

The TitleStyle property is of type TableItemStyle and represents the visual settings for the title row. A similar property is defined for the subtitle, footer, label, and bar. For example, you can visually choose the border of the bars, the header's font, the width of the labels, and whatever else you like.

If the TableItemStyle lacks some important properties you absolutely need, you can define your own style class.

A common mistake that developers sometimes make is styling a child control at the wrong time?for example, immediately after creation. I recommend that you use a sort of delayed styling when it comes to rendering constituent controls. When is the latest time to style a child element? When the parent control gets to render.

Here's why you now need to override the Render method.

   protected override void Render(      HtmlTextWriter output)   {      // Style controls before rendering      PrepareControlHierarchyForRendering();      // Generate markup      RenderContents(output);   }

The implementation of the Render method is straightforward except for the call to an internal method that prepares the control for rendering. The code snippet below is excerpted from the method PrepareControlHierarchyForRendering. It shows how to apply global settings to the control as a whole?border, font, colors?and how to style a particular element?the title.

   // Style the control's body    Table t = (Table) Controls[0];   t.CopyBaseAttributes(this);   if (ControlStyleCreated)      t.ApplyStyle(ControlStyle);      // Style the title's cell    t.Rows[0].Cells[0].ApplyStyle(TitleStyle);

The CopyBaseAttributes method imports all visual settings defined through the property grid of Visual Studio .NET.

?
Figure 2: Editing the TitleStyle property.

When you implement style properties, it is essential that you add the following attributes to the property's declaration.

   [PersistenceMode(PersistenceMode.InnerProperty)]   [DesignerSerializationVisibility(       DesignerSerializationVisibility.Content)]   [NotifyParentProperty(true)]

In particular, the PersistenceMode attribute indicates that the style will be serialized in the ASPX page markup as an inner tag named after the property. This is exactly what happens with the style properties of built-in DataGrid and DataList controls. Figure 2 shows the property grid of the BarChart control when the user is editing the TitleStyle property.

At this point, the BarChart control is all set and you can focus on the tricks needed to make it effectively work in a host page.

Using the BarChart Control
The first step to import a custom control in an ASP.NET page is add a @Register directive to make the control visible to the ASP.NET runtime.

   <%@ Register TagPrefix="expo"       Namespace="Expoware.Controls"       Assembly="BarChart" %>

The TagPrefix attribute can take an arbitrary string. The same is not true for the other two attributes which will be set to the control's namespace and assembly name respectively. Here's a styled BarChart control that looks like Figure 1.

                                         

To bind the control to its data, you use the following code:

   DataTable data = ExecuteQuery(1997);   BarChart1.Maximum = 150000;   BarChart1.Title = "Northwind Sales";   BarChart1.SubTitle = "(Year 1997)";   BarChart1.DataSource = data.DefaultView;   BarChart1.DataTextField = "Employee";   BarChart1.DataValueField = "Sales";   BarChart1.DataBind();

The sample page runs a query against the Northwind database and obtains a result set with at least two columns?Employee and Sales. The former is bound as the label; the latter end up feeding the bar. The Maximum property defines the scale for the graph.

What's Next?
Although it is fairly complex already, the BarChart control only scratches the surface of a real-world data-bound control. It lacks at least three key features: events, templates, and designers. Let me briefly review the benefits of each, and related development issues.

The control should fire a couple of events at the very minimum?ItemCreated and ItemDataBound. The former is meant to indicate when an item of a given type (title, subtitle, item) is created; the latter informs when data has been bound to the bars. By wiring some custom code up to these events, you can apply context-sensitive changes and, for example, change the color of bars if the represented value exceeds a given threshold. The source code of the BarChart control clearly indicates where events should be fired.

Templates are an interesting add-on feature that you can implement to let developers customize at least header and footer. Required changes don't prefigure a significant effort. For each template, you add a property and a standard if block in the routines that create the item. The if block looks at the template property and decides whether the template should be instantiated in lieu of the default layout.

Finally, I want to mention design-time features. As is, the control doesn't provide a WYSIWYG experience. To make up for this, you need to create a custom designer?a class that inherits from ControlDesigner and overrides quite a few methods. The overridden methods will basically return the HTML to display in the Visual Studio .NET site.

Real-world data-bound controls are not for the faint-hearted. They require deep knowledge of the ASP.NET internals and familiarity with attributes and base classes. In this article, I built a non-toy control from scratch and added a good deal of features. More importantly, along the way I tried to emphasize pitfalls and caveats that could compromise your efforts, which happened to me several times.

All in all, the good news is that ASP.NET 2.0 is just around the corner and it will bring a full bag of goodies for ASP.NET control developers. Sure, you'll find new and improved controls but also new and improved base classes to build custom data-bound controls more quickly, effectively, and with a dramatically reduced surface for bugs and programming errors.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: