AspAlliance.com LogoASPAlliance: Articles, reviews, and samples for .NET Developers
URL:
http://aspalliance.com/articleViewer.aspx?aId=1299&pId=-1
Working with Custom Provider Controls
page
by Brian Mains
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 35345/ 62

Introduction

The ASP.NET framework is made of up custom HTML and Server controls that are the building blocks for developing ASP.NET web applications.  Other technologies are highly utilized in the web development process, such as HTML, CSS, and JavaScript; however, for data-driven or server-based applications, the ASP.NET controls are the primary means to develop ASP.NET web sites.  These controls essentially render HTML, CSS, and JavaScript when represented on the client browser and the developer doesn't need to worry about how the rendering works.  His primary focus is using the server controls to their fullest.

You may have seen my other article on creating a custom provider.  This article will focus on creating user interface controls for that provider.  When developing a custom provider framework, it is wise to create a wide array of server controls to go along with them.  These server controls make use of the ASP.NET framework and server control architecture, but they also integrate with the custom provider and create almost maintenance-free server controls that work seamlessly with the provider.

Custom Server Controls Overview

Within the ASP.NET architecture, there are a variety of kinds of server controls.  There are controls that don't render any type of user interface but that have an important function such as data source controls, AJAX control extenders, etc.  There are web or HTML controls that map to existing HTML controls like the TextBox, CheckBox, and others.

There are more advanced controls that have additional capabilities, such as template controls, or controls that their interface is created from the template that is provided.  There are data-bound controls that render an interface based on the underlying data source, or validation controls to validate user input.

These are the various types of controls that can be created.  The base class for all controls is the System.Web.UI.Control class, which is often used for controls that don't render a user interface.  For more specific functionality, server controls in ASP.NET inherit from the System.Web.UI.WebControls.WebControl class, which provides added capabilities to server-control rendering.

For controls that consist of one or more child controls, the CompositeControl class can be used.  All of the child controls that make up the interface are added to the Controls collection, which this control can directly expose properties/methods of the children, grouping the entire multiple server control logic into one logical unit.  For data-bound controls, the DataBoundControl and CompositeDataBoundControl classes define methods for receiving a data source, performing the binding and rendering the final user interface based on the data provided.

This article assumes you have an understanding of these controls, and a basic understand of custom control development.  However, if you need more information, the following resources are helpful.

·         Developing ASP.NET Server Controls Overview on MSDN

·         A Crash Course on Creating Controls

Throughout the article, you will notice in the code segments statements like the following: "this.DesignMode". Because some of the code (like the CreateChildControls) method runs in the designer, there may be some problems that come up in the designer.  Wrapping certain code in an "if" statement, with this as its conditional, would prevent those errors from occurring.  Plus, using this statement within code means that I don't have to provide a custom designer, which saves some coding effort for the time being.

Custom controls also have to check the EnableViewState property within it, because the control has to run with or without ViewState enabled, and will perform certain actions more when it is disabled, such as data binding to a data source control.  There is another option of using ControlState instead, since it guarantees state storage for control values, but you should only use this for critical data.  You will also see the use of the ViewState collection in many of the property definitions.  Before returning an object, the control must ensure the value exists within ViewState; returning a value and converting it to the proper type without checking it first will raise an exception.

Rendering occurs through the HtmlTextWriter class; not all controls need to customize the rendering process, but when custom rendering is needed, this class does the job.  It has a Write() method to write whatever text it needs to, but it also has a better process for writing HTML tags; RenderBeginTag() and RenderEndTag() methods that allow you to dynamically create the tag by passing in the tag name only.  Any attributes that you want to add to an element being rendered with RenderBeginTag() need to be added before you call this method.  That means all of the AddAttribute() and AddStyleAttribute() definitions occur before RenderBeginTag() is called.

Controls also make use of attributes, but this will be discussed later.

Custom Provider Controls

Custom provider controls and the custom provider are integrated, coupled together so that the provider controls know how to access the provider.  This requires that the provider be configured and working correctly, because any error in the provider affects the control ultimately.

We're going to look at a few different types of controls.

Newsletter Signup Control

The newsletter signup control is the means to add subscribers to a newsletter item in the database.  Below is the class definition for the control.

Listing 1

[
ValidationProperty("EmailAddress"),
DefaultProperty("EmailAddress"),
DefaultEvent("SigningUp")
]
public class NewsletterSignup : WebControl, IPostBackDataHandler, IPostBackEventHandler
{
}

This control inherits from WebControl and also makes use of capturing post back information (through IPostBackDataHandler), as well as raising events from within the control (IPostBackEventHandler), as we will soon see.  Notice the attributes used; EmailAddress is the property that contains the email address used to sign up, which is the default.  It's also the property that is validated by any validation controls, as designated by the ValidationProperty attribute.  Lastly, DefaultEvent is the default event for the control, so when double-clicking the control in the designer, SigningUp is the event handler created.  Below are some of the properties defined:

Listing 2

[DefaultValue("Signup"), Localizable(true)]
public string ButtonText
{
  get
  {
    object o = ViewState["ButtonText"];
    return (o == null) ? "Signup" : o.ToString();
  }
  set
  {
    ViewState["ButtonText"= value;
  }
}
 
[Localizable(true)]
public string Description
{
  get
  {
    object o = ViewState["Description"];
    return (o == null) ? null : o.ToString();
  }
  set
  {
    ViewState["Description"= value;
  }
}
 
public string EmailAddress
{
  get
  {
    object o = ViewState["EmailAddress"];
    return (o == null) ? null : o.ToString();
  }
  set
  {
    ViewState["EmailAddress"= value;
  }
}
 
public string NewsletterName
{
  get
  {
    object o = ViewState["NewsletterName"];
    return (o == null) ? null : o.ToString();
  }
  set
  {
    ViewState["NewsletterName"= value;
  }
}

NewsletterName is the property that links the signup control to a specific newsletter.  At the current moment, because the values are stored in an underlying data source, the value has to be a manual string that must match up to an existing newsletter defined using the provider.  Designer support has not yet been added, which it may be possible through a custom designer.

The properties are stored in ViewState as the storage mechanism.  The attribute I will mention is the Localizable attribute.  By passing the true value, Localizable allows the value to be localized.  When creating a page resource file with all of the localization keys, these properties can be included in the resource file, and the final value provided at runtime.

This control has events that the page can subscribe to.  Two of the events are SigningUp and SignedUp, which the equivalent On<Event> methods are shown below:

Listing 3

protected virtual void OnSignedUp(EventArgs e)
{
  if (SignedUp != null)
    SignedUp(this, e);
 
  //Clear the email address after signing up
  this.EmailAddress = null;
}
 
protected virtual void OnSigningUp(CancelEventArgs e)
{
  if (SigningUp != null)
    SigningUp(this, e);
}

The data posted back is collected via the LoadPostData method handler, a part of the IPostBackDataHandler interface.  This interface marks a control for PostBack data handling and through this the email address property is returned.  Note that the unique ID is used; you will see later that the control given for the Email Address control is assigned the UniqueID property.

Listing 4

public bool LoadPostData(string postDataKey,
 System.Collections.Specialized.NameValueCollection postCollection)
{
  this.EmailAddress = postCollection[this.UniqueID];
  return false;
}

The IPostBackEventHandler handles events that are raised within the button controls.  That will come next; however, let's look at what happens when the event is raised:

Listing 5

public void RaisePostBackEvent(string eventArgument)
{
  if (SigningUp == null && string.IsNullOrEmpty(this.NewsletterName))
    throw new Exception("The SigningUp event must be handled.");
 
  CancelEventArgs args = new CancelEventArgs(false);
  this.OnSigningUp(args);
 
  if (!args.Cancel)
  {
    if (string.IsNullOrEmpty(this.NewsletterName))
      throw new NullReferenceException("The newsletter is null");
    if (!Newsletter.NewsletterExists(this.NewsletterName))
      throw new Exception("The newsletter provided does not exist");
 
    Newsletter.AddSubscription(this.EmailAddress, this.NewsletterName);
    this.OnSignedUp(EventArgs.Empty);
  }
}

Normally, multiple events can be handled within this event handler, noted by the event argument value.  However, only one event is raised and that is all that needs to be accounted for.  When the event is raised, the newsletter name must be provided through the NewsletterName property, or if the property isn't assigned the SigningUp event must be handled.  For this to work, during the SigningUp event, the NewsletterName must be provided.  If not provided by the end, then an error is raised because the control doesn't know for which newsletter to add the subscriber.  The subscription is added and the SignedUp event is raised.  Below is the rendering process of the control:

Listing 6

protected override void Render(HtmlTextWriter writer)
{
  base.AddAttributesToRender(writer);
  writer.RenderBeginTag(HtmlTextWriterTag.Span);
  writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);
  writer.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);
  writer.AddAttribute(HtmlTextWriterAttribute.Type, "text");
  writer.AddAttribute(HtmlTextWriterAttribute.Value, this.EmailAddress);
  writer.RenderBeginTag(HtmlTextWriterTag.Input);
  writer.RenderEndTag(); //input
 
  writer.Write("&nbsp;&nbsp;");
 
  writer.AddAttribute(HtmlTextWriterAttribute.Id, this.UniqueID + "signup");
  writer.AddAttribute(HtmlTextWriterAttribute.Href,
    Page.ClientScript.GetPostBackClientHyperlink(this"signup"true));
  writer.RenderBeginTag(HtmlTextWriterTag.A);
  writer.Write(this.ButtonText);
  writer.RenderEndTag(); //a
 
  //If the description exists, render it below the textbox.
  if (!string.IsNullOrEmpty(this.Description))
  {
    writer.Write("<br>");
    writer.Write(this.Description);
  }
 
  writer.RenderEndTag(); //span
}

I noted the process for using the HtmlTextWriter above; notice that in the Render method, the process for creating the interface is rendered.  This is one of the processes to create an interface for a custom control.  Notice that the UniqueID property, assigned to the name HTML attribute above, is used in the LoadPostData method.  Since the UniqueID property is assigned to the textbox, instead of for the link rendered below it, it can be used to retrieve the ViewState information.  The button doesn't matter as much, except for the value provided to the href attribute.  Page.ClientScript.GetPostBackClientHyperlink returns a script with the event argument "signup", which is passed the RaisePostBackEvent method.

Subscriber Administration

The next control is the SubscriberAdministration control.  This is a composite control, which combines a drop down of newsletter names, with a custom list of subscribers that belong to the list.  This control provides a simple way to lookup subscriber names that are registered in a list.  The two primary controls that are used within this composite are:

Listing 7

private SimpleDataList _dataList;
private DropDownList _dropDown;

SimpleDataList is another list that works with simple data, such as string arrays and such, and supports the {0} construct for the DataField property.  The setup of this control, called whenever the child control collection is created, is shown below:

Listing 8

protected override void CreateChildControls()
{
  this.Controls.Clear();
 
  this._dropDown = new DropDownList();
  this.Controls.Add(this._dropDown);
  this._dropDown.AutoPostBack = true;
  this._dropDown.Items.Add("Select One");
  this._dropDown.SelectedIndexChanged += new EventHandler
    (this._dropDown_SelectedIndexChanged);
 
  this._dataList = new SimpleDataList();
  this.Controls.Add(this._dataList);
  this._dataList.DataField = "{0}";
 
  if (!this.DesignMode && (!Page.IsPostBack || !this.EnableViewState))
  {
    string[]newsletters = Newsletter.GetAllNewsletters();
    foreach (string newsletter in newsletters)
      this._dropDown.Items.Add(newsletter);
  }
}

In the above code, a drop down list is created, given a default value, and sets up an event handler.  For the data list, since it supports the {0} notation, this is what is assigned the DataField property.  This control doesn't expose properties that affect binding or setup of these controls.  The reason is that the base data source for the drop down is the list of newsletters from the Newsletter provider.  These items are added to the drop down after the "Select One" default value.

There is one more thing to note; there isn't any need to render the controls; just by adding them to the collection, each child control is rendered automatically (because the render process calls each child control's render method).

Whenever the drop down selection changes, the event handler fires, and the subscribers for that newsletter are returned in a simple iterated list form:

Listing 9

private void _dropDown_SelectedIndexChanged(object sender, EventArgs e)
{
  DropDownList ddList = sender as DropDownList;
 
  if (ddList.SelectedItem.Text != "Select One")
  {
    string[]subscribers = Newsletter.GetSubscribers
      (this._dropDown.SelectedItem.Text);
    this._dataList.DataSource = subscribers;
    this._dataList.DataBind();
  }
}

If the selected item is not the default option, then the list of subscribers is passed to the simple data list and bound.  This control is primarily used to show the list of users that have subscribed, and to get a count of how many people did subscribe.  In the future, this control could be expanded to delete the subscribers from this list, as well as to send the users in that list an HTML-based email through an editor.  Paging and sorting would be a benefit to the list.

Newsletter Administration

The next control is something like the GridView control, where you can delete a newsletter from the list, as well as create new newsletters.  This control inherits from CompositeDataBoundControl, and supports the more advanced binding through a data source control or another enumerable data source.

Because the underlying data source comes from the provider, this control doesn't make use of the DataSource or DataSourceID properties at all.  Instead, it makes use of the provider internally, and so when being bound to, it uses the provider data source instead, as we shall soon see.

There is a scheme with these controls that you need to understand.  With these types of tabular data controls, there is a usually a header, a series of items, and the footer below it.  The header, in this case, is non-existent because it isn't needed.  The items will be the list of each of the newsletters, as well as a link to delete the newsletter.  In the footer, there are two textboxes for the newsletter name and description, and a link to add this newly found newsletter.

The CompositeDataBoundControl class uses a different CreateChildControls overloaded method for creating the inner data-bound controls.  The following is the definition for this overload in the newsletter administration control:

Listing 10

protected override int CreateChildControls(System.Collections.IEnumerable
  dataSource, bool dataBinding)
{
  int itemCount = 0;
 
  if (dataBinding)
    dataSource = Newsletter.GetAllNewsletters();
 
  Table table = new Table();
  this.Controls.Add(table);
  this.Items.Clear();
 
  foreach (object o in dataSource)
    this.Items.Add(this.CreateItem(table, o, itemCount++, dataBinding));
 
  this.CreateFooter(table);
  return itemCount;
}

This method takes an IEnumerable data source that is resolved from the manually bound data source (bound through the DataSource property) or a data source control bound through the DataSourceID property.  This isn't a concern for two reasons; the CompositeDataBoundControl class takes care of the differences through the backend architecture, and the second I will talk about in a moment.

For now, the dataBinding parameter is the key to determine whether we are in data binding mode, meaning that a data source is bound.  The data source is provided from the Newsletter.GetAllNewsletters() provider method, and assigned to the data.  The next time around, provided it isn't bound to, is the ViewState being bound, which means the data source is an empty object array.

When using the provider, the DataBinding property is the key because DataSource will be not be used no matter what.  Unfortunately, the DataSource and DataSourceID properties do not help in this situation, because the data source is provided within the control instead of on the outside.

Listing 11

new private object DataSource
{
  get
  {
    return null;
  }
  set
  {
    throw new NotImplementedException();
  }
}
 
new private string DataSourceID
{
  get
  {
    return string.Empty;
  }
  set
  {
    throw new NotImplementedException();
  }
}

The table is the structure that has the header, footer, and items, and the CreateChildControls method above regulates when that all happens.  The CreateItem method is responsible for the creation of the actual items.

Listing 12

protected virtual DataboundItem CreateItem(Table table, object dataItem, int
  index, bool dataBinding)
{
  DataboundItem item = new DataboundItem(dataItem, index);
  table.Rows.Add(item);
 
  TableCell newsletterCell = new TableCell();
  item.Cells.Add(newsletterCell);
  TableCell buttonCell = new TableCell();
  item.Cells.Add(buttonCell);
  this.OnItemCreated(new DataEventArgs < DataboundItem > (item));
 
  if (dataBinding)
  {
    newsletterCell.Text = (string)dataItem;
    this.OnItemDatabound(new DataEventArgs < DataboundItem > (item));
  }
 
  LinkButton button = new LinkButton();
  button.Text = this.DeleteButtonText;
  button.CommandName = "delete";
  button.CommandArgument = this.Items.Count.ToString();
  buttonCell.Controls.Add(button);
  return item;
}

Notice that there is added logic that runs when data binding.  When in data binding mode, the cell is assigned the text value of the newsletter passed from the string array of newsletters.  Any other fields that would be assigned something would also be assigned here, but since there is one field, only one field is needed.  Lastly, when binding, the ItemDataBound event fires for the row.

The control overrides the OnBubbleEvent to capture any events that were raised, and process them accordingly.  This method could be used to raise a RowCommand event, similar to the GridView's event that fires for any outstanding commands, if so desired.  We will get to the HandleEvent method in a moment.

Listing 13

protected override bool OnBubbleEvent(object source, EventArgs args)
{
  CommandEventArgs commandArgs = args as CommandEventArgs;
  if (commandArgs != null)
    return this.HandleEvent(commandArgs);
  return false;
}

This control implements IPostBackEventHandler; any button control that makes use of the GetPostBackClientHyperlink method raises the click event to this method.  It is here that the command name/argument are passed in as the event argument, parsed, and passed to the HandleEvent method.

Listing 14

public void RaisePostBackEvent(string eventArgument)
{
  if (eventArgument.Contains("$"))
  {
    string[]parts = eventArgument.Split('$');
    CommandEventArgs args = new CommandEventArgs(parts[0], parts[1]);
    this.HandleEvent(args);
  }
}

The HandleEvent method received the command argument raised.  If inserting, the name/description is retrieved and the newsletter is added through the provider.  If deleting, the index of the row is used to receive the name of the newsletter from the cell in which it is rendered.  So if row zero's button is clicked, row zero's newsletter name stored in the cell is retrieved.

Listing 15

private bool HandleEvent(CommandEventArgs args)
{
  if (args.CommandName == "insert")
  {
    Page.Validate("NewsletterAdministration_Add");
    if (Page.IsValid)
    {
      string name = _newsletterName.Text;
      string description = _newsletterDescription.Text;
 
      Newsletter.AddNewsletter(name, description);
    }
  }
  else if (args.CommandName == "delete")
    Newsletter.RemoveNewsletter(this.Items[int.Parse
      (args.CommandArgument.ToString())].Cells[0].Text);
  else
    return false;
 
  base.RequiresDataBinding = true;
  this.DataBind();
  return true;
}

After each is run, the control is rebound. RequiresDataBinding ensures that the data binding occurs.

The Future

There is plenty of more room for innovation, and for refactoring of these controls. There needs to be more support for adding/removing users, removing all subscribers when deleting a newsletter, providing a form for sending the newsletter that supports dynamic HTML support, and allowing users to manage the subscriptions they want.  All of these are the future ideas that the provider will implement, in the near future.

Downloads
Conclusion

This article shows how you can create provider controls that can consume a custom provider, making the use and reuse of a provider framework much easier to incorporate into an application. The original idea was taken from my Nucleo Framework.



©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-03-29 6:07:11 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search