The goal of this final chapter is to present an entire ASP.NET 3.5 application that takes advantage of many of the new features of the .NET 3.5 Framework. In this chapter, we build a code sample website that also includes a simple blog.
There are two motivations behind this chapter. First, most of the code samples in this book are very brief. The code samples are intended to illustrate a particular point in the fewest lines of code possible. There are challenges, however, that you do not encounter until you build a full-blown web application. My hope is that you can apply the lessons I learned while building this application to an application you are building (in other words, learn from my pain).
The second motivation for writing this chapter is to tie together many of the technologies discussed in this book. This is a long book. Not many people read it from end to end (I thank the crazy few readers who manage to do it). My hope is that you’ll study the application presented in this chapter and become more interested in new .NET 3.5 technologies such as LINQ to SQL and the ASP.NET AJAX Extensions. (You might even use the VirtualPathProvider
class someday.)
In the first part of this chapter, I provide you with a walkthrough of the pages contained in the sample application. Next, we examine more closely how different features of the website are implemented. In particular, we examine how data access and form validation are implemented in the sample website. We also discuss the Ajax features of the sample site. Finally, we discuss how the website enables “live” code samples.
The primary purpose of the website is to act as a code sample website for .NET code. Anyone who visits the website can post a new code sample entry. A code sample entry can consist of one or more code sample files. The author of the code sample entry can provide a description of each of the files. Furthermore, the author can add one or more tags to the code samples to categorize and describe their purpose.
Visitors to the website can browse the existing code sample entries. They can rate code samples when they view them. Furthermore, they can copy the code samples so they can use the samples in their own applications.
The website also includes a simple blog. The administrator of the website can post blog entries about the code samples or about any other topic. Visitors to the website can add comments to a blog entry.
The website exposes both an ATOM and RSS feed. If someone wants to subscribe to the blog, that person can subscribe to either the ATOM or RSS feed.
The home page of the website displays a code cloud and a list of recent blog entries (see Figure 34.1). The code cloud consists of a distinct list of all the code entry tags. The more code entries that share a tag, the larger the tag appears in the code cloud. In the figure, you’ll notice that a lot of code samples are related to validation. If you click a tag in the code cloud, you are transferred to a page that contains a list of code samples associated with the tag.
The home page also contains a list of three recent blog entries. A summary of each blog entry appears in the home page. You can click a blog entry to view the full entry.
In this section, you are provided with a walkthrough (with lots of screen shots) of the process of adding both blog entries and code entries.
Only a member with the Administrators role can post new blog entries. If you want to add a new blog entry, you must log in by clicking the Login link that appears at the top of every page.
I’m set up as the administrator for the website by default. If you log in using the username Stephen and password secret, you will be logged in as an administrator.
You can create a new administrator account (and delete the Stephen account) by using the Website Administration Tool. After opening the sample site in Visual Web Developer, select the menu option Website, ASP.NET Configuration. When the Website Administration Tool opens, select the Security tab to manage users and roles.
After you log in, you can post a new blog entry by clicking the {Add Blog Entry} link that appears under the current list of blog entries on the home page. Clicking this link transfers you to the page displayed in Figure 34.2.
When you create a new blog post, you complete the following fields:
Title—. The title of the blog post.
Introduction Text—. The introduction text appears on the home page of the website as a summary of a blog entry. The introduction text is also used by the ATOM and RSS feeds.
Post—. The full blog entry post.
Is Pinned—. When this box is checked, the blog entry appears before all other blog entries on the home page.
Notice that the input field for entering a blog post uses a rich text editor. The sample website uses the open source FCKeditor. You can download the FCKeditor from www.fckeditor.net. The FCKeditor displays a rich editor in the case of Internet Explorer and Firefox. A plain text editor is displayed when the page is requested using the Opera browser.
If you attempt to submit the form without completing a required field, validation errors are displayed using callouts (see Figure 34.3). Validation errors are not displayed until you actually click the Next button.
After you successfully complete the form for adding a new blog entry and click the Next button, you are redirected to a page that you can use to tag the new blog entry (see Figure 34.4). You can add as many tags to a blog entry as you desire. The tags enable users to cross-navigate among related blog entries. Blog entries that share the same tags are linked.
The TextBox for adding a new tag uses the AJAX Control Toolkit AutoComplete
Extender control. As you type, matching tags are retrieved from the database. Using the AutoComplete
extender makes it more likely that you’ll use the same tags for multiple blog posts.
When you are done adding all your tags, you can click the Finish button to finish the process of adding a new blog entry. You are redirected to the finished blog entry that the world sees.
After you create a blog entry, you always have the option of editing or deleting the entry. To edit or delete a blog entry, log in to the website using an Administrator account, navigate to the blog entry page, and click either the {Edit} or {Delete} link.
The main purpose of the code sample website is to enable people to post new code samples, browse existing code samples, and rate code samples. Any registered user can post a new code sample entry at the website.
Before you can post a new code sample, you must navigate to the main code sample page. Click the Code Samples link that appears at the top of any page. You’ll see the page in Figure 34.5.
The main code sample page displays a list of the top ten highest rated code samples, the top ten most viewed code samples, and the top ten most recent code samples. At the bottom of the page, you can click the Add New Code Sample link to add a new code sample.
After you click the Add New Code Sample link, you see the form in Figure 34.6. A code sample entry can contain one more code samples. Typically, a code sample consists of multiple files. The form in Figure 34.6 enables you to provide a description of the entire code sample entry.
After you provide a description for the code sample entry and click Next, you are redirected to a page that you can use to associate one or more code samples with the code sample entry (see Figure 34.7). For example, you might want to create both a VB.NET and C# version of a code sample.
If you click the Add Code Sample link, you can add a new code sample. The form for adding a new code sample contains the following fields:
File Name—. The name of the code sample file (for example, SamplePage.aspx).
File Language—. The programming language used for the code sample.
Description—. The description of the code sample.
Code—. The actual source code of the code sample.
Enable Try It—. When this box is checked, the code sample can be executed “live” from the website. This field appears only for users in the Administrators role.
Try It Code—. The code executed when a code sample is executed “live” from the website. This field appears only for users in the Administrators role.
The last two fields require some explanation. If you are a member of the Administrators role, you can enable users to execute a code sample. When you check the Enable Try It check box, a Try It link appears with the code sample that a user can click to run the code sample (see Figure 34.8).
Because, most likely, you’ll want to use a different database connection with the code that gets executed when a user clicks Try It, there is a separate text field that you can use to enter the Try It code. The source code entered into the Try It Code text field is never displayed to the public.
After you submit a code sample, you can click Next to move to a page that enables you to tag a code sample entry. You can associate a maximum of three tags with any code sample entry. The tags appear in the code cloud. They also appear at the bottom of each code entry to provide visitors to the website with a way to navigate between related code samples.
In this section, you learn how data access and form validation are implemented for the sample website. Data access and form validation are implemented by taking advantage of new features of the .NET 3.5 Framework.
All data access performed by the sample website is performed using LINQ to SQL. No explicit SQL code was written. Taking advantage of LINQ to SQL enabled me to dramatically reduce the amount of time and code required to build the website.
LINQ to SQL is discussed in detail in Chapter 18, “Data Access with LINQ to SQL.”
Normally, when performing data access from a web application, you need to write an entire data access layer to bridge the divide between your application and your database. By taking advantage of LINQ to SQL, I could avoid writing a data access layer. Instead, I could concentrate on the real task that I needed to accomplish: writing the data access queries.
I used the Object Relational Designer to create my LINQ entities (see Figure 34.9). I ran into one issue when using the Designer. Several of the website database tables include a DateCreated column. This column has a default value generated by the SQL GetDate()
function. However, the Object Relational Designer did not pick up on this fact and I received errors when performing inserts and updates. To work around this problem, I had to manually update the DateCreated
property for each entity in the Object Relational Designer. Within the Object Relational Designer, you can select a property of an entity and modify that property in the Properties window. In the case of the DateCreated
property, I had to assign the value True to the Auto Generated Value property (see Figure 34.10).
I created a separate partial class for each entity. I added the LINQ to SQL queries that I needed to the partial class. For example, Listing 34.1 contains some of the code for the partial CodeSample
entity.
Example 34.1. CodeSample.cs (Partial)
public partial class CodeSample : EntityBase<CodeSample> { public IEnumerable<CodeSample> SelectByEntryId(int entryId) { return Table.Where( s => s.EntryId == entryId ); } partial void OnCreated() { this.LanguageId = -1; } }
Notice that the class is declared as a partial class. The other half of the partial class is generated by the Object Relational Designer. You can find the Designer-generated half of the CodeSample
partial class in the Superexpert.Designer.cs file in the App_Code folder.
The preceding partial class contains a method named SelectByEntryId
. This method executes a LINQ to SQL query that returns all code samples associated with a certain entryId
. The Table
property used within the method is a property exposed by the base class (we discuss this base class in the next section).
Notice that the class also includes an OnCreated()
method. Unfortunately, you can’t add a constructor to a LINQ to SQL partial class because the Object Relational Designer already creates a constructor. However, you can create the equivalent of a class constructor by handling the OnCreated()
event. In the case of the CodeSample
class, the OnCreated()
event is used to provide a default value for the CodeSample.LanguageId
property.
When building the LINQ to SQL queries for the sample application, I noticed that I ended up writing almost the exact same queries for each entity. For example, I needed to write Select, Insert, Update, and Delete queries for both the Blog
and the CodeSample
entities. Whenever you find yourself writing duplicate code, you should stop yourself and determine whether there is a way to make the code more generic. In this case, I took advantage of a custom entity base class.
All the entity partial classes derive from the base EntityBase
class. This class contains generic Get
, Select
, Update
, Delete
, and Insert
methods. It also contains generic methods for sorting and paging database data.
For example, the Blog
partial class is declared like this:
public partial class Blog : EntityBase<Blog>
Because the Blog
class derives from the EntityBase
class, it includes methods such as Get
, Select
, Insert
, Update
, and Delete
for free. It inherits all the properties and methods of the EntityBase
class (the EntityBase
class is located in the App_CodeEntityBaseClasses folder).
We discuss the EntityBase
class in more detail in the last part of Chapter 18.
The sample application includes forms for inserting and updating blog entries, inserting and updating code sample entries, and inserting and updating individual code samples. All the forms in the sample application follow the same pattern. A FormView that contains a single EditItemTemplate is used for displaying the form for both inserting and updating the form data.
For example, the page used for inserting and updating a blog entry is contained in Listing 34.2.
Example 34.2. Edit.aspx
<%@ Page Language="C#" MasterPageFile="~/Design/MasterPage.master" Title="Blog Post" %> <script runat="server"> /// <summary> /// Add default values for new blog entry /// </summary> protected void srcBlog_Updating ( object sender, ObjectDataSourceMethodEventArgs e ) { // If new blog entry, add user name Blog newBlog = (Blog)e.InputParameters[1]; if (newBlog.Id == 0) { newBlog.AuthorUserName = User.Identity.Name; } } /// <summary> /// If no problems, then redirect to blog tags page /// </summary> protected void srcBlog_Updated(object sender, ObjectDataSourceStatusEventArgs e) { if (e.Exception == null) { Blog newBlog = (Blog)e.ReturnValue; Response.Redirect("~/Admin/BlogTags/Edit.aspx?blogId=" + newBlog.Id); } } /// <summary> /// If there was a problem, keep the form in edit mode /// and show validation errors /// </summary> protected void frmBlog_ItemUpdated(object sender, FormViewUpdatedEventArgs e) { if (e.Exception != null) { e.KeepInEditMode = true; e.ExceptionHandled = true; ValidationUtility.ShowValidationErrors(this, e.Exception); } } </script> <asp:Content ID="Content1" ContentPlaceHolderID="cphMain" Runat="Server"> <asp:UpdatePanel ID="up1" runat="server"> <ContentTemplate> <asp:FormView id="frmBlog" DataSourceID="srcBlog" DataKeyNames="Id,Version" DefaultMode="Edit" OnItemUpdated="frmBlog_ItemUpdated" Width="100%" Runat="server"> <EditItemTemplate> <div class="field"> <div class="fieldLabel"> <asp:Label id="lblTitle" Text="Title:" AssociatedControlID="txtTitle" Runat="server" /> </div> <div class="fieldValue"> <asp:TextBox id="txtTitle" Text='<%# Bind("Title") %>' Columns="60" Runat="server" /> </div> <div class="fieldValue"> <super:EntityCallOutValidator id="valTitle" PropertyName="Title" Runat="server" /> </div> </div> <div class="field"> <div class="fieldLabel"> <asp:Label id="lblIntroductionText" Text="Introduction Text:" AssociatedControlID="txtIntroductionText" Runat="server" /> </div> <div class="fieldValue"> <asp:TextBox id="txtIntroductionText" Text='<%# Bind("IntroductionText") %>' TextMode="MultiLine" Columns="60" Rows="4" Runat="server" /> </div> <div class="fieldValue"> <super:EntityCallOutValidator id="valIntroductionText" PropertyName="IntroductionText" Runat="server" /> </div> </div> <div class="field"> <div class="fieldLabel"> <asp:Label id="lblPost" Text="Post:" AssociatedControlID="txtPost" Runat="server" /> </div> <div class="fieldValue"> <fck:FCKeditor id="txtPost" BasePath="~/FCKEditor/" ToolbarSet="Superexpert" Value='<%# Bind("Post") %>' Width="600px" Height="600px" runat="server" /> </div> <div class="fieldValue"> <super:EntityCallOutValidator id="EntityCallOutValidator1" PropertyName="Post" Runat="server" /> </div> </div> <div class="field"> <div class="fieldLabel"> </div> <div> <asp:CheckBox id="chkIsPinned" Text="Is Pinned" Checked='<%# Bind("IsPinned") %>' Runat="server" /> </div> </div> <div class="field"> <div class="fieldLabel"> </div> <div> <asp:Button id="btnNext" CommandName="Update" Text="Next" Runat="server" /> </div> </div> </EditItemTemplate> </asp:FormView> </ContentTemplate> </asp:UpdatePanel> <super:EntityDataSource id="srcBlog" TypeName="Blog" SelectMethod="Get" UpdateMethod="Save" OnUpdating="srcBlog_Updating" OnUpdated="srcBlog_Updated" Runat="Server"> <SelectParameters> <asp:QueryStringParameter Name="id" QueryStringField="blogId" /> </SelectParameters> </super:EntityDataSource> </asp:Content>
The page in Listing 34.2 contains a FormView
control bound to a DataSource
control. The FormView
control contains a single EditItemTemplate template. Notice that it does not include an InsertItemTemplate, even though the FormView
is used both for inserting new blog entries and editing existing blog entries. By using a single template, you reduce the amount of code you must write and maintain by half.
Most of the pages in the sample application use a DataSource
control named the EntityDataSource
control. We discussed the EntityDataSource
control in Chapter 18. This control is derived from the ObjectDataSource
control and provides default values for some of the ObjectDataSource
control’s properties. The EntityDataSource
control is contained in the App_CodeEntityBaseClasses folder on the CD.
The DataSource
control includes a select QueryStringParameter that grabs an id
value from the query string. The DataSource
control calls the Get()
method to grab an instance of the Blog
entity when the page is first requested.
The Get()
method is a method of the EntityBase
class. It looks like this:
public static T Get(int? id) { if (id == null) return new T(); return Table.Single(GetDynamicGet(id.Value)); }
When a null ID is passed to the Get()
method, it simply returns a new instance of the entity (in this case, the Blog
entity). If the id
parameter does have a value, on the other hand, the entity with a matching ID is retrieved from the database with the help of the GetDynamicGet()
method.
Therefore, if you request the page in Listing 34.2 without passing an id
parameter in the query string, a form that represents a new Blog
entry is displayed. Otherwise, if you do pass an id
parameter, a form for editing the existing Blog
entity is displayed.
The sample application discussed in this chapter does not use any of the standard ASP.NET validation controls. Validation is handled at the entity level. In other words, validation is performed in the business logic layer, where validation should be performed, instead of the user interface layer.
The EntityBase
class includes an abstract (MustInherit
) method named Validate()
. Each of the entities implements this abstract method. All the validation logic is contained in the entity’s Validation()
method.
For example, Listing 34.3 contains the Validation()
method used by the Blog
entity.
Example 34.3. Blog.cs (Partial)
public partial class Blog : EntityBase<Blog> { /// <summary> /// Where all validation happens /// </summary> protected override void Validate() { // Required fields if (!ValidationUtility.SatisfiesRequired(Title)) ValidationErrors.Add("Title", "Required"); if (!ValidationUtility.SatisfiesRequired(IntroductionText)) ValidationErrors.Add("IntroductionText", "Required"); if (!ValidationUtility.SatisfiesRequired(Post)) ValidationErrors.Add("Post", "Required"); } }
The Validate()
method takes advantage of the ValidationUtility to check for several required fields. If any of the validation checks fails, an error message is added to the entity’s ValidationErrors
collection.
We discuss the ValidationUtility in Chapter 18. The ValidationUtility contains additional methods for validating property values against regular expressions stored in the web configuration file.
If you look closely at the EditItemTemplate
contained in the FormView in Listing 34.2, you’ll notice that EntityCallOutValidator
controls are associated with each TextBox. For example, the following EntityCallOutValidator
control is associated with the txtTitle
TextBox:
<super:EntityCallOutValidator id="valTitle" PropertyName="Title" Runat="server" />
When the ValidationUtility.ShowValidationErrors()
method is called in the frmBlog_ItemUpdated()
event handler, any validation error that matches the value of an EntityCallOutValidator
control’s PropertyName
property is displayed.
The advantage of placing your validation logic in your business logic layer is that your validation logic is applied automatically wherever you use the entity. For example, if the Blog
entity is used in multiple pages (or even multiple applications), you don’t need to rewrite the very same validation logic.
The traditional advantage of placing your validation logic in the user interface layer is responsiveness. You don’t need to perform a postback to view validation error messages. However, Ajax is blurring this traditional divide between server and client. All the forms used in the sample application use the UpdatePanel
control in order to make the forms more responsive.
The sample application discussed in this chapter takes advantage of the Microsoft server-side AJAX controls. The UpdatePanel
control is used with almost all the forms for inserting and editing data. The sample application also takes advantage of the ASP.NET AJAX Control Toolkit. Two controls from the Toolkit, the AutoCompleteExtender
and the Rating
control, are used to create a more interactive experience.
Almost all the FormView
controls used in the sample application are wrapped in an UpdatePanel
control. When you submit a form, a disruptive postback is not performed. Instead, a sneaky postback is performed in the background by the UpdatePanel
control.
The overall user experience is improved by the UpdatePanel
control. For example, the UpdatePanel
control creates the illusion that the validation error messages are being generated on the client when, in fact, the validation error messages are being generated by the server.
We discuss the UpdatePanel
control in detail in Chapter 31, “Using Server-Side ASP.NET AJAX.”
However, using the UpdatePanel
control made debugging the sample application more difficult. The UpdatePanel
control prevents normal error messages from being displayed in the browser. Furthermore, because an UpdatePanel
control times out, stepping through code with the Visual Web Developer debugger in a page that contains an UpdatePanel
is difficult.
My recommendation is that you don’t add UpdatePanel
controls to a web application until after it is fully debugged.
The sample application uses two controls from the ASP.NET AJAX Control Toolkit: the AutoCompleteExtender
control and the Rating
control.
The ASP.NET AJAX Control Toolkit is discussed in Chapter 32, “Using the ASP.NET AJAX Control Toolkit.”
The AutoCompleteExtender
control can be used to extend a TextBox control so that suggestions appear while you type (like in Google Suggest). The suggestions are retrieved from a web method. The web method can be defined in the page that contains the AutoCompleteExtender
control, or the web method can be defined in a separate web service.
The AutoCompleteExtender
control is used in multiple pages within the sample application. For example, it is used both in the page for editing blog tags (see Figure 34.11) and in the page for editing code sample tags. Listing 34.4 is extracted from the page for editing blog tags.
Example 34.4. AdminBlogTagsEdit.aspx (Partial)
<asp:TextBox id="txtTag" AutoComplete="Off" Text='<%# Bind("Name") %>' Runat="server" /> <ajaxToolkit:AutoCompleteExtender ID="AutoCompleteExtender1" ServiceMethod="GetSuggestions" TargetControlID="txtTag" MinimumPrefixLength="1" Runat="server" />
Notice that the TextBox control in Listing 34.4 includes an AutoComplete="Off"
attribute. This attribute disables the built-in browser auto-complete (for Internet Explorer and Firefox) so that it does not interfere with the Ajax auto-complete.
In Listing 34.4, the AutoCompleteExtender
is associated with the TextBox control through its TargetControlID
property. The MinimumPrefixLength
property configures the control to start displaying suggestions as soon as you type at least one character into the TextBox control. Finally, the AutoCompleteExtender
control is set up to retrieve its suggestions from a web method named GetSuggestions()
. This method is declared in the same page as the AutoCompleteExtender
control. The code for the GetSuggestions()
method is contained in Listing 34.5.
Example 34.5. AdminBlogTagsEdit.aspx (Partial)
[System.Web.Services.WebMethod] public static string[] GetSuggestions(string prefixText, int count) { return BlogTag.GetSuggestions(prefixText, count); }
When a web method is declared in a page, it must be declared as a static method. Furthermore, it must be decorated with the WebMethod
attribute.
The GetSuggestions()
method in Listing 34.5 calls the GetSuggestions()
method of the BlogTag
entity to get existing blog tags that match the prefix from the database. The BlogTag.GetSuggestions()
method is contained in Listing 34.6.
Example 34.6. BlogTag.cs (Partial)
public partial class BlogTag : EntityBase<BlogTag> { public static string[] GetSuggestions(string prefixText, int count) { return Table.Where( t => t.Name.StartsWith(prefixText) ) .Select(t => t.Name).Distinct().Take(count).ToArray(); } }
The other control from the ASP.NET AJAX Control Toolkit used in the sample application is the Rating
control. This control is used to enable users to rate the quality of a code sample (see Figure 34.12).
The Rating
control is declared with the following attributes in the CodeSamplesEntry.aspx page:
<ajaxToolkit:Rating ID="Rating1" BehaviorID="RatingBehavior1" CurrentRating="2" MaxRating="5" StarCssClass="ratingStar" WaitingStarCssClass="savedRatingStar" FilledStarCssClass="filledRatingStar" EmptyStarCssClass="emptyRatingStar" runat="server" style="float:left" Tag='<%# Eval("Id") %>' OnChanged="Rating1_Changed" />
When a user clicks the Rating
control and selects a rating, the Rating
control raises its Changed
event. The Changed
event is handled by the following event handler:
protected void Rating1_Changed(object sender, RatingEventArgs e) { EntryRating.Insert( new EntryRating(){EntryId=Int32.Parse(e.Tag), Rating = Int32.Parse(e.Value)} ); }
This event handler inserts a new EntryRating
entity into the database that represents the user rating. The second parameter passed to this method is an instance of the RatingEventArgs
class. Two properties of this class are used when inserting the new record: Value
and Tag
. The Value
property represents the rating the user selected. The Tag
property can represent any information you want to associate with the rating. When the Rating
control is declared, the code sample Entry.Id
is assigned to the tag.
The VirtualPathProvider
class was introduced into the ASP.NET 2.0 Framework. Not many people know about this class. The VirtualPathProvider
class can be used to abstract the location of an ASP.NET file away from the file system.
I want visitors to the code sample website to be able to try the code samples “live.” Someone browsing the code samples should be able to click a Try It link and execute a code sample (see Figure 34.13).
In the sample application, code samples are stored in a database table. In order to get the Try It functionality to work, the application must be able to execute code samples directly from the database. This is exactly what the VirtualPathProvider
class enables you to do. The code sample application takes advantage of the VirtualPathProvider
class to execute ASP.NET pages directly from a database table.
The VirtualPathProvider
class hijacks a particular file system path. Any path that starts with a directory named /virtual is passed to VirtualPathProvider
. For example, when you click the Try It link next to a code sample, a request for a page at the following location is sent to the server:
/virtual/codesample/166/ShowAjaxValidator.aspx
Because this path starts with a directory named /virtual, the request gets routed to the VirtualPathProvider
class, which grabs the file from the TryItCode column in the CodeSample database table. The file gets dynamically compiled by the ASP.NET Framework and served as a normal ASP.NET page.
The VirtualPathProvider
class is discussed in detail in Chapter 21, “Advanced Navigation.”
The goal of this chapter was to demonstrate how you can take advantage of several of the new features of the ASP.NET 3.5 Framework when building a web application. In this chapter, you were presented with an overview of a complex application written with ASP.NET 3.5. You learned about the code sample application.
In the first part of this chapter, you learned how data access and form validation are implemented in the sample application. My hope is that studying this application will convince you to stop writing SQL queries and start writing LINQ queries.
In the second part of this chapter, you learned how the sample application takes advantage of the new server-side Ajax features introduced into the ASP.NET 3.5 Framework. You learned how the sample application uses the UpdatePanel
control to create more responsive forms. We also discussed two controls from the ASP.NET AJAX Control Toolkit used in the sample application: the AutoCompleteExtender
and Rating
controls.
Finally, we discussed one of the more obscure and overlooked classes contained in the ASP.NET Framework: the VirtualPathProvider
class. You learned how the sample application uses the VirtualPathProvider
class to execute ASP.NET pages directly from a database table.