I was building a new server control for my employer when I stumbled across this unexpected error:

"The control collection cannot be modified during DataBind, Init, Load, PreRender or Unload phases."

This proved to be a misleading error. The problem occurred because I tried to modify the Control collection outside of the new server control's own collection. This could only be done when the Parent property had been set, which is never until the Init cycle. Effectively, modifying a parent's control collection is disallowed.

The solution to my problem was ensure that all dynamically added controls were added to the custom server control's collection only.

Happy Coding!


 
Categories: ASP.NET

Download the source code for this demo here.

Introduction

The demand for high quality web interfaces is continually increasing. The movement towards Web 2.0 places an increasing emphasis in making a web application look and feel like it were a home desktop application, with all the bells and whistles attached. Users expect each page they visit to interact with them in a smooth and natural way.

Asynchronous JavaScript and XML, AJAX, is one tool that helps to create this user experience. In a typical scenario, JavaScript on the page sends and retrieves data from the web server asynchronously, updating the web page without flicker and with minimal delay. Google Inc.'s GMail is an example. Each GMail page retrieves content from GMail only a portion at a time, "on demand". The entire experience is seamless.

Microsoft recently released a framework for AJAX, and a corresponding collection of control extenders called the AJAX Control Toolkit. The toolkit contains several control extenders. An AJAX control extender adds AJAX functionality to a non-ajax web control. The extender approach can be particularly useful when there is a requirement to add similar AJAX behavior to more than one control type, such as adding behaviors to buttons, images, and panels.

This tutorial introduces the basics of creating a custom AJAX control extender. We discuss embedded resources, property decorations (attributes), and client-side Web service calls.

The Scenario

Consider a form built for a web application. The form contains several places for input. From time to time, users flood you with questions about the meaning of each element. You want to add some context sensitive help to the page to spare yourself all the most frequently asked questions, saving both time and money.

We'll create a very simple AJAX control extender that wraps a server control, such as a Button or a Label, and adds context sensitive help at the click of a mouse. Moreover, the extender will populate the help with the results of a web service call to separate the content from the functionality.

Getting Started

Below is an example of the body of a very simple page. There are three main elements. On each, we want to add context sensitive help. The fourth element, the Panel at the bottom, is a placeholder for our help content.

  <form id="form1" runat="server">
    <asp:Button ID="Button1" runat="server" Text="Looking for help..."/>
    <br />
    <br />
    <asp:Image ID="Image1" runat="server" ImageUrl="~/flower.jpg" />
    <br />
    <br />
    <asp:Panel ID="Panel1" runat="server">
        This is the content of the panel.<br />
        If you would like to find help, kindly click among this text.<br />
        Good luck!
    </asp:Panel>
    <br />
    <br />
    <asp:Panel ID="HelpPanel" runat="server" />
    <br />
    <br />
  </form>

To make our control extender consumable from any website, we'll compile the extender into a separate assembly. To do this, first make sure that the extender templates are installed. Download the AJAX control toolkit from http://ajax.asp.net, extract the files into the directory of choice, and execute the .vsi installers located in the archive at AjaxControlExtender\AjaxControlExtender.vsi. Once this is installed, add the new extender project to your Visual Studio solution by right clicking on the solution and selecting Add -> New Project, or selecting File -> Add -> New Project from the menu.

Select ASP.NET AJAX Control Project from the templates.

Delete the default .cs and .js files, and create a directory, Help to hold the new control. Right click on the folder, select Add -> New Item, and select ASP.NET AJAX Extender Control from the templates. Name the control "Help", since the convention of "HelpExtender" is added by default.

We now have the basic file structure for the control.

Configuring the Namespaces

It is important to give some thought to the namespace conventions for your control. There are three namespaces of immediate concern:

  1. The namespace of your server code
  2. The namespace of your client JavaScript
  3. The default namespace of your assembly

Adjusting the namespace of server code is plain and direct. I chose KbrProductions.Web.Extensions, after the pattern set by Microsoft's System.Web.Extensions. The client namespace is adjusted in the .js file. For this project the client namespace is KbrProductions.Ajax. A simple search and replace can fix the handful of references. Finally, the default namespace of the assembly can be configured by right clicking on the project file, selecting Properties, and editing the namespace in the Application tab. This namespace is important because it influences embedded resources.

For the JavaScript file to be consumable by the website, we must include it as a resource to the project assembly. Do this by selecting the file in Solution Explorer, viewing its properties, and setting the Build Action attribute to Embedded Resource.

Now visit the HelpExtender.cs file. Edit the assembly attribute to read:

[assembly: System.Web.UI.WebResource(
    "KbrProductions.Web.Extensions.Help.HelpBehavior.js",
    "text/javascript")]

The first parameter is a resource path for the javascript file. The naming convention for the path is: [Assembly Namespace][.directory][.FileName]. Remember, the assembly namespace is the default namespace set in the project properties. For the directory, use dots (.) to separate folders instead of backslashes. Misconfiguring this line is the common cause of the following web page error:

Assembly 'Extensions, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' contains a Web resource with name 'Extensions.Help.HelpBehavior.js', but does not contain an embedded resource with name 'Extensions.Help.HelpBehavior.js'.

When in doubt, fire up ILDASM and view the contents of the control assembly. The manifest contains the proper path for the JavaScript file on a .mresource line.

The extender class includes the JavaScript file by means of a ClientScriptResource decoration. Modify the decoration as shown below, where the first string indicates the client class name, and the second string specifies the resource exactly as specified in the assembly attribute earlier in the page.

[ClientScriptResource(
    "KbrProductions.Ajax.HelpBehavior",
    "KbrProductions.Web.Extensions.Help.HelpBehavior.js")]

At this point, the namespaces of the control assembly should be configured.

Server-Side Extender Properties

Next, we'll specify the properties that will be available as attributes to our extender markup. Because we inherit from ExtenderControlBase, we already have a TargetControlID property, which we can use to specify the control we want to extend.

We also want to specify the control that will contain the contents of our help. We decorate the property with the IDReferenceProperty attribute to enable the extender to automatically resolve the specified ID into a ClientID.

/// <summary>
/// ID of the control that will contain the help content.
/// </summary>
[ExtenderControlProperty]
[DefaultValue("")]
[IDReferenceProperty(typeof(WebControl))]
[ClientPropertyName("helpPanelID")]
public string HelpPanelID
{
    get { return GetPropertyValue("HelpPanelID", ""); }
    set { SetPropertyValue("HelpPanelID", value); }
}

Next, we'll build properties to define the location of a Web service that provides the text for the context-sensitive help. We decorate this property as a UrlProperty, which provides automatic resolving of server-relative URLs, that is, any URL starting with "~/". We also decorate it with a TypeConverter to handle some Web service specific url handling.

/// <summary>
/// Path to the webservice that the extender will pull the images from.
/// </summary>
[UrlProperty()]
[ExtenderControlProperty()]
[TypeConverter(typeof(ServicePathConverter))]
[ClientPropertyName("servicePath")]
public string ServicePath
{
    get { return GetPropertyValue("ServicePath", ""); }
    set { SetPropertyValue("ServicePath", value); }
}

Next we add a required property to define which method should be called.

/// <summary>
/// The webservice method that will be called to supply help.
/// </summary>
[ExtenderControlProperty]
[RequiredProperty]
[DefaultValue("")]
[ClientPropertyName("serviceMethod")]
public string ServiceMethod
{
    get { return GetPropertyValue("ServiceMethod", ""); }
    set { SetPropertyValue("ServiceMethod", value); }
}

And, finally, we add a property to define a parameter for the Web service call. We could treat this parameter as a key to determine which piece of help to return from the service.

/// <summary>
/// Context key to pass to the service method to get help text.
/// </summary>
[ExtenderControlProperty]
[DefaultValue("")]
[ClientPropertyName("helpContextKey")]
public string HelpContextKey
{
    get { return GetPropertyValue("HelpContextKey", ""); }
    set { SetPropertyValue("HelpContextKey", value); }
}

The next step is to provide a client-side equivalent interface to these properties.

Client-Side Extender Properties

Basic handling of each property from JavaScript is made extremely simple due to the AJAX Control Toolkit. All that is required is to add variable declarations to the constructor function, and add get/set functions to the object prototype. It is important to note that the property names for the get and set functions are case sensitive and should reflect the ClientPropertyName configured in the server-side extender code. Additional utility variables can be added to the constructor function as well.

KbrProductions.Ajax.HelpBehavior = function(element) {

    KbrProductions.Ajax.HelpBehavior.initializeBase(this, [element]);

    this._helpContextKey = null; // HelpContextKey property
    this._serviceMethod = null;  // ServiceMethod property
    this._servicePath = null;    // ServicePath property
    this._helpPanelID = null;    // HelpPanelID property

    this._helpPanel = null;      // HelpPanel element
    this._clickHandler = null;   // Click Event handler for the extended control
    this._targetElement = null;  // Element of the extended control
}

KbrProductions.Ajax.HelpBehavior.prototype = {
    // ...

    // extended properties
    get_serviceMethod : function() { return this._serviceMethod; },
    set_serviceMethod : function(value) { this._serviceMethod = value; },

    get_servicePath : function() { return this._servicePath; },
    set_servicePath : function(value) { this._servicePath = value; },

    get_helpContextKey : function() { return this._helpContextKey; },
    set_helpContextKey : function(value) { this._helpContextKey = value; },

    get_helpPanelID : function() { return this._helpPanelID; },
    set_helpPanelID : function(value) { this._helpPanelID = value; }
}

Fleshing out the Client Behavior

All that remains to complete our extender is to implement the client behavior. To provide feedback to the user that a control contains help, change the mouse cursor of the extended element. The object prototype's initialize method is a good place to do this.

initialize : function() {
    KbrProductions.Ajax.HelpBehavior.callBaseMethod(this, 'initialize');

    // Apply the CSS cursor style, "help"
    this._targetElement = this.get_element();
    this._targetElement.style.cursor = "help";

    // ...

Next, wire up an event handler in the initialize function to handle the click event of the target element. Use the Function.createDelegate method to create the handler. This ensures that multiple handlers can potentially be attached to the element without conflicting.

    // Attach an event handler to the click event of the element
    if(this._targetElement) {
        this._clickHandler = Function.createDelegate(this, this._onClick);
        $addHandler(this._targetElement, 'click', this._clickHandler);
    }

Add functionality to the dispose method to clean up the event handler. This is good practice to keep our memory clean.

dispose : function() {

    // Remove the event handler from the element if attached.
    if(this._clickHandler) {
        $removeHandler(this._targetElement, 'click', this._clickHandler);
        this._clickHandler = null;
    }

    KbrProductions.Ajax.HelpBehavior.callBaseMethod(this, 'dispose');
},

We now create the this._onClick function to handle the mouse click. We first implement a few lines to prevent any full-page postback that might occur. Parameters to the service method should be prepared as a dictionary key-value pair. We then can invoke the method using the ASP.NET AJAX Sys.Net.WebServiceProxy.invoke method. The method has seven parameters:

  1. The path to the Web service.
  2. The method to invoke.
  3. A parameter to indicate whether the call should use HTTP POST. For security reasons, this defaults to false.
  4. The parameters for the method.
  5. An event handler to be called when the service call returns successfully.
  6. An event handler to be called if the service call fails.
  7. Any JavaScript structure to pass to the event handler. This is a place holder if you want to send more than the defaults.

// click event for the extended element
_onClick : function(e) {

    // prevent post back.
    e.preventDefault();
    e.stopPropagation();

    // prepare parameters, if necessary
    var params = null;
    if (this._helpContextKey) {
        params = { 'helpContextKey' : this._helpContextKey };
    }

    // Invoke the web service
    Sys.Net.WebServiceProxy.invoke(
        this._servicePath,      // Service path
        this._serviceMethod,    // Service method
        false,                  // use POST (default false)
        params,                 // params
        Function.createDelegate(this, this._onServiceReply),  // on success
        null,                   // on failure
        null);                  // additional context
},

The last addition to the script is the this._onServiceReplay handler. The toolkit uses sender and eventArgs as parameters to the method. I think they're misnamed, because the object coming in through the sender parameter is the server reply; a string. At any rate, we fill our help panel with the contents of the reply.

// reply event for the service call
_onServiceReply : function(sender, eventArgs) {
    document.getElementById(this._helpPanelID).innerHTML = sender;
}

At this point, we completed all the code for the extender. All that remains is to reference this assembly by the website and add our extender to the page.

Using the Help Extender

Building the extender took a fair handful of code lines, but it all pays off when we start using the extender in the Web site. Add a reference to the extender project, and add the folling line to the <system.web><pages><controls> element of the Web.config file:

<add tagPrefix="kbr"
     namespace="KbrProductions.Web.Extensions"
     assembly="Extensions"/>

Note that the application will need references to the ASP.NET AJAX assembly and the AJAX Control Toolkit as well. Add a ScriptManager to the page, and extenders for the Button, Image and Panel.

<kbr:HelpExtender ID="help1" runat="server"
    TargetControlID="Button1"
    HelpContextKey="1"
    HelpPanelID="HelpPanel"
    ServiceMethod="GetHelp" />
<kbr:HelpExtender ID="help2" runat="server"
    TargetControlID="Image1"
    HelpContextKey="2"
    HelpPanelID="HelpPanel"
    ServiceMethod="GetHelp" />
<kbr:HelpExtender ID="help3" runat="server"
    TargetControlID="Panel1"
    HelpContextKey="3"
    HelpPanelID="HelpPanel"
    ServiceMethod="GetHelp" />

All that remains is to define the Web service that provides our help. We'll inject an AJAX-friendly service method right into this page. In a more practical application, the if/else if structure could be replaced with database access, file IO, or any other data retrieval. All that is required is that the method is static, takes the appropriate parameters, and returns the expected string result.

<script runat="Server" type="text/C#">
[System.Web.Services.WebMethod]
[System.Web.Script.Services.ScriptMethod]
public static string GetHelp(string helpContextKey)
{
    if (helpContextKey == "1")
        return "There is help awaiting you.";

    else if (helpContextKey == "2")
        return "Excellent clicking, friend!";

    else
        return "There is no help found for the likes of you.";
}
</script>

Running the page now shows that each control has a help cursor. When clicked, JavaScript issues an asynchronous Web service call which populates a portion of the page with context-sensitive help. The control extender is finished.

Conclusion

ASP.NET AJAX is an excellent tool to provide a richer experience to the user by taking advantage of the control extender model and asynchronous callbacks. With a little footwork, it is possible to create extenders that add complex behaviors to a variety of server controls, potentially saving considerable effort down the road. The AJAX Control Toolkit provides classes that simplify the process of creating your own extenders. Also, because the AJAX Control Toolkit is open source, you can download the existing control extenders and add behaviors to them, as well.

Happy Coding!


 
Categories: ASP.NET | AJAX

June 13, 2007
@ 09:12 PM

A few months ago I contributed to an open-source project for the first time. This was an exciting moment for me. Until recently, I had always borrowed knowledge from blogging gurus such as Scott Guthrie and Scott Hanselman, and open source repositories such as Code Project and SourceForge. This world of programming was, and still is, a great well of programming tips and tricks that help me out quite regularly.

As I developed the framework for Notes on Code, I started using the CSS Friendly Control Adapters, an open source library available on CodePlex.com. These control adapters improve the quality of ASP.NET page markup by reducing the frequency of <table> objects on the page when rendering complex controls such as the TreeView.

On my pages, I kept a small link to W3C to verify that my XHTML was correct. What to my surprise when I found that it was not. The culprit turned out to be the control adapters, which injected a <link> tag in the <body> section. After searching Google for some time, it became clear to me that this was an unidentified bug.

It was a simple fix, really. It only took a moment to find the offending lines in the source code and, due to the quality design of the original product, find a suitable alternative.

I was very impressed with the management of the project by bdemarzo. By the evening of the same day, the source code for the project was updated to include the changes.

I'll say that it felt like a rite of passage for me. I felt like I had something to contribute to the community, however small that contribution might be. If you haven't tried it yet, go for it. Go anywhere there is code and try it out.

Happy Coding!


 
Categories: Misc

Have you ever started out writing code one way and later discover that another way is much, much better? It happens to me daily. I face a difficulty, sometimes, when I want to refactor my code into a more concise, elegant form, but some of my teammates already use the existing methods.

One neat feature of C# is the ability to decorate methods and other structures in code. A useful decoration for this situation is the Obsolete decoration. Here are several examplse:

[Obsolete]
void ObsoleteMethod1() { }

[Obsolete("This is why we don't use this...")]
void ObsoleteMethod2() { }

[Obsolete("If you use this, there will be compile errors", true)]
void ObsoleteMethod3() { }

The description comes in handy because it will show up in the tooltip when typing in the method in Visual Studio. This ensures that it is not necessary for developers to have source code to detect whether a method should be discontinued.

Descriptions specified in the Obsolete decoration appear in the tooltip

You can also forcefully terminate the use of the method without blindly removing it. The third sample demonstrates this. Using the ObsoleteMethod3 method would cause a compile-time error. This provides a good opportunity to discontinue a method and provide an explanation as to why it is no longer used.

Happy coding!


 
Categories: C#

Copyright ©2006 By Keith Rimington

Screenshot Image - LineCounter counts the lines in files and places the results for each file in a DataGridView

Introduction

If you have ever wanted to know how quickly you write code, you have probably counted lines. As programs get more involved and work is shared among a team, it is difficult to know how many lines are "owned" by you.

Lines of code may not be the best way to measure performance (I am imagining a classroom full of monkeys striking Enter, Enter, Enter... ). But sometimes is satisfying to say, "Look what was done!"

This articles explores basic file IO, how to manually store results in a DataGridView, and basic event handling.

Wiring up the GUI

The GUI has just a few components. A TextBox provides a place where we can enter the file extensions we are interested in counting. A DataGridView provides a place to show results. A CheckBox provides us some options and a StatusStrip helps us summarize results to the user.

Use the Anchor property to adjust how each Control "Clings" to the walls of the Form. This allows us to resize the window to get a better view if we have long filenames or a large number of results. Anchoring is one of the convenient layout tools builtin to the environment.

New controls in the form editor make changing Anchoring a breeze.

Now we will wire up the columns in the DataGridView control. The form editor makes this a breeze. Highlight the DataGridView and select from the properties Columns.

The properties editor makes creating data columns easy.

We now have a dialog that can be used to generate our columns. Clicking on the Add button opens an additional dialog that can be used to create columns. Choose a name and header text, and the column type of DataGridViewTextBoxColumn. Other interesting column properties include FillWeight, which affects how much priority the column receives during layout.

Individual column properties can be adjusted in these dialogs.

The next step to giving the GUI functionality is to add event handlers. For example, when we click on the Go button, we want the application to find files that match our search criteria, open them, and count lines.

We can use the form editor to add this handler easily. Just double-click on the Go button, and the event handler is added for us, like so:

  private void goButton_Click(object sender, EventArgs e)
  {
    // TODO: Count files here...
  }

Also, the function is registered with the event delegate in the designer.cs file. This is what tells the runtime that we want to run the goButton_Click method when the goButton.Click event is fired.

  this.goButton.Click += new System.EventHandler(this.goButton_Click);

Because there may be files found by our program that we want to ignore, we will wire up the DataGridView to gather information about selected files only. We can then pick and choose which results we want to tally up for our final line count.

Using the form editor, we set the MultiSelect property to True and the SelectionMode property to FullRowSelect. All that's left is adding an event handler to respond to changes in the selection. Find the SelectionChanged property in the form editor and double-click to generate the following method:

  private void dgvResult_SelectionChanged(object sender, EventArgs e)
  {
    // TODO: Count the selection here.
  }

Which is wired up in the designer.cs file like so:

  this.dgvResult.SelectionChanged +=
      new System.EventHandler(this.dgvResult_SelectionChanged);

A lot of changes can happen by playing with the properties in the form editor, but lets move on to the meat of the project, cranking out our own lines of code.

Using a StreamReader to count lines

Reading a file from the system is very easy in C#. The key objects involved are File and StreamReader. The static method File.OpenText opens a file specified by a local or fully-qualified path name, and generates a StreamReader object to digest the file contents. StreamReader is built with the ReadLine method; perfect for our task.

  private int CountLinesInFile(string filename)
  {
    StreamReader sr = File.OpenText(filename);
    int count = 0;
    while (sr.ReadLine() != null)
      count++;
    sr.Close();
    return count;
  }

Finding Files in a Directory

Our next task is to determine which files we should open. We will make use of the System.Collections.Generic.List<> class to contain our filenames. The following code snippet demonstrates how we collect all the filenames that match the requested extensions.

  private void CountFiles()
  {
    // Repository for discovered filenames
    List<string> filenames = new List<string>();

    // Running linecount total
    int total = 0;

    // The Split method is a quick and dirty way to parse a string.
    string[] extensions = txtExts.Text.Split("" "".ToCharArray());

    foreach (string ext in extensions)
    {
      // The third parameter of this overload allows us to
      // automatically inspect subdirectories.  Default is
      // SearchOption.TopDirectoryOnly, and the argument
      // could have been omitted, but is included here for
      // information's sake.
      filenames.AddRange(
        chkSubDir.Checked ?
        Directory.GetFiles(
            Directory.GetCurrentDirectory(),
            ""*."" + ext,
            SearchOption.AllDirectories) :
        Directory.GetFiles(
            Directory.GetCurrentDirectory(),
            ""*."" + ext,
            SearchOption.TopDirectoryOnly));
    }

    // ...

Our next task is to open each file, count the lines, and add the results to the DataGridView. We will use the DataGridView.Rows.Add(object[] params) method to add each new row. With this method, each object should correspond, in order, to the columns of the DataGridView.

    // ...

    // Wipe out any previous results
    dgvResult.Rows.Clear();

    // No that we have all the filenames,
    // open the files to count their lines.
    foreach (string filename in filenames)
    {
      int count = CountLinesInFile(filename);
      total += count;

      // This line adds a new row to the DataGridView.
      // We choose the string.Substring method to display
      // only the relative path of the file
      dgvResult.Rows.Add(
        filename.Substring(Directory.GetCurrentDirectory().Length+1),
        count.ToString());
    }

    // Add the total to the bottom
    dgvResult.Rows.Add(
       new object[] { ""Total:"", total.ToString() });
  }

Call this method from the goButton_Clicked event handler and we have a line counter!

Using MultiSelect with the DataGridView

To wrap things up, we will add functionality to ignore some of the results, if we wish, in our total. The following code snippet counts the results in the selected rows, and writes the results to the StatusStrip below.

  private void CountSelection()
  {
    int selected = 0;
    int total = 0;
    foreach (DataGridViewRow row in dgvResult.Rows)
    {
      try
      {
        // Ignore rows that are either not selected
        // or are the total row.  Because some ""filler""
        // rows exist, use the catch block to effectively
        // ignore the invalid data in those rows.
        if (row.Selected &&
            row.Cells[0].Value.ToString() != ""Total:"")
        {
          selected++;
          total += int.Parse(row.Cells[1].Value.ToString());
        }
      }
      catch { }
    }

    // Update the status label with the results
    lblFileCt.Text = selected.ToString() + "" files selected"";
    lblLineCt.Text = total == 0 ?
        ""No lines"" :
        total.ToString() + "" lines"";
  }

Conclusion

This article just scratches the surface of file IO and data controls like the DataGridView. I hope you found this article useful. This article was published on CodeProject


 
Categories: C#