Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Dino Esposito
Solid Quality Learning
April 2006
Applies to:
ASP.NET
Custom controls
Summary: This article explains how to add an internal editor to an ASP.NET custom control, and discusses ways to make changes persistent for each user. (12 printed pages)
Contents
Introduction
A Brief Recap of the RssReader Control
Incorporating a Property Editor
The Format of the Editor
Non-Postback Editors
Persisting Changes on a Per-User Basis
Conclusion
Click here to download the code sample for this article.
Introduction
In the previous article of this series, I covered the general AJAX programming model and the ASP.NET Script Callback implementation. I created a sample RSS reader control that includes a link to refresh the contents on-demand. Of course, when the user clicks on the link and gets up-to-date feeds, only the RSS reader control is refreshed; the rest of the page is unaffected, and the user doesn't experience page flickering and low page responsiveness. I finished the article by stating that, with ASP.NET Script Callbacks, Ajax.NET, and the forthcoming Atlas platform, the new era of the Web has just begun.
Great. There's more than just the AJAX lifestyle in the new generation of rich ASP.NET controls. For some controls, it might be useful (and handy for the end users) to incorporate pieces of a user interface, in order to allow dynamic changes to properties and members. For example, the RSS reader control discussed in the previous article features an RssFeed property for setting the URL to download information. You put the control in the page, and each user who navigates to the page sees the feed from the blog. This is what happens in most MSDN sites, where posts from technology influencers and architects are aggregated and displayed.
In this way, the page will use some RSS reader control to provide information to users, which is probably fine in many real situations. But what if, instead, you want to provide site visitors with just the tool to retrieve the information they need? In this case, the URL to the feed can't be hard-coded in the page; or, at least, users should be given a means to change it at-will in a persistent manner.
The goal of this article is to enhance the RssReader control so that it can display an internal editor and let users change the URL of the feed. Along the way, I'll also investigate ways to make this setting persistent on a per-user basis.
A Brief Recap of the RssReader Control
The RssReader control is a composite control that implements the ICallbackEventHandler interface in order to refresh the feed on-demand. It is rendered as a table, with a row for a title and subtitle, and a simple command bar. The command bar displays the time at which the feed was last updated, and it contains a link to refresh the feed. Finally, the control adds a cell with links to the posts. Styles have been defined so that each page author can further customize the title, subtitle, and feed.
Key to the building of the RssReader's user interface is the following code.
protected override void CreateChildControls() { base.CreateChildControls(); Controls.Clear(); CreateControlHierarchy(); ClearChildViewState(); PrepareControlForRendering(); }
The CreateControlHierarchy method is the nerve center of the rendering process. It creates the outermost table element, and takes care of filling it out with rows and cells as appropriate. The code of CreateControlHierarchy can factored out as follows.
protected virtual void CreateControlHierarchy() { // Build the outermost container table Table outer = new Table(); outer.Width = Width; outer.CellPadding = CellPadding; outer.CellSpacing = CellSpacing; // Render title/subtitle RenderControlHeader(outer); // Populate the table RenderControlUI(outer); // Add the table to the control tree Controls.Add(outer); }
The URL is expressed by the RssFeed string property. Data is downloaded when the DataBind method is called; when it is ready and fully parsed, data is injected into the table, and styled as needed.
Incorporating a Property Editor
An RssReader control placed in an ASP.NET page is bound to one URL in order to get its feed. There are a few scenarios in which changing the URL dynamically would be desirable. For example, this would be helpful in portal-like applications where users might want to customize the structure and contents of pages. If one of these pages aggregates feeds, changing the source is the most typical form of personalization.
To let users change control properties on-the-fly, you can either provide a page-wide form or, if the requirement must be fulfilled by a single control, implement an editor in the control itself.
How would you incorporate a property editor in a server control? Which properties should be defined, and which ones exposed, to page developers? Who would be in charge of defining the layout of the editor? This article attempts to answer these and other related questions.
Whether displayed or hidden, the property editor is definitely part of the control's user interface. In light of this, the property editor must be integrated with the control tree. Consequently, the code that creates the editor should depart from the nerve center of the control's rendering—the CreateControlHierarchy method.
Basically, the preceding code snippet for CreateControlHierarchy should be expanded to include a test, and an additional branch for when the editor is visible. The following code shows a refactored version of the method.
protected virtual void CreateControlHierarchy() { // Build the outermost container table Table outer = new Table(); outer.Width = Width; outer.CellPadding = CellPadding; outer.CellSpacing = CellSpacing; // Render the topmost part of the table RenderControlHeader(outer); // Populate the table if (ShowEditor) RenderControlEditor(outer); else RenderControlUI(outer); // Save the control tree Controls.Add(outer); }
As you can see, the method first creates the outermost container—a Table element—and the header, which includes a title and subtitle. The RssReader control can have two distinct types of user interface: the classic user interface that shows the feed, and the property editor. A new internal variable is defined in order to distinguish between these two working modes. ShowEditor is a Boolean property, defined as follows.
[Browsable(false)] protected bool ShowEditor { get { object o = ViewState["ShowEditor"]; if (o == null) return false; return (bool) o; } set { ViewState["ShowEditor"] = value; } }
Setting the Browsable attribute to false keeps the property off the Visual Studio 2005 property grid. Note that this is not a strict requirement; you can also make this property public, thus giving page authors a chance to display the control directly in edit mode. All things considered, though, this doesn't look like a very common need; therefore, I decided to keep the property off the design-time property grid.
The ShowEditor property is checked when the control's hierarchy is built; based on the value of the property, CreateControlHierarchy calls into a different code fragment to add a cell with the feed or the edit form.
if (ShowEditor) RenderControlEditor(outer); else RenderControlUI(outer);
In this way, the RssReader control comes with two distinct user interface blocks. By default, ShowEditor is set to false, meaning that the standard user interface is shown. The value of ShowEditor can't be changed programmatically from within the host page (unless you modify the visibility modifier of the property). The control, though, provides some interactive elements to enable and disable property editing. Event handlers bound to these elements set and reset the ShowEditor property.
When ShowEditor is true, the RenderControlEditor method runs and repopulates the control's tree.
protected virtual void RenderControlEditor(Table parent) { // Create the input form CreateInputForm(parent); // Create the button bar CreateButtonBar(parent); } protected virtual void CreateInputForm(Table parent) { _title.Text = "Change RSS Feed"; _subtitle.Text = "Enter new RSS Feed"; TableRow row = new TableRow(); parent.Rows.Add(row); TableCell cell = new TableCell(); row.Cells.Add(cell); _rssFeedTextBox = new TextBox(); cell.Controls.Add(_rssFeedTextBox); _rssFeedTextBox.Width = Unit.Percentage(100); _rssFeedTextBox.Text = RssFeed; Page.SetFocus(_rssFeedTextBox); }
RenderControlEditor receives a reference to the outermost table, which already contains up to two rows for the title and subtitle. It calls into the helper CreateInputForm method, which creates another table row that contains as many child controls as needed in order to edit the editable properties. In this case, the control supports editing of the sole RssFeed property, and it contains only one textbox.
Declared as a private member of the class, the text box control covers the whole width of the cell and defaults to the URL of the current RSS feed. As a favor to users, you might also want to automatically move the input focus to the text box control. The Page.SetFocus method does the trick.
Note that if you make ShowEditor a public property, page authors can set it from within Visual Studio 2005. If the property is set to true, the control should automatically switch the view and display the property editor. When this happens, the CreateInputForm code executes. The rub lies in the fact that the Page reference is null, and an exception is thrown as a result. To avoid that, you should filter out the code that sets the input focus if the control is being used at design-time.
if (!DesignMode) Page.SetFocus(_rssFeedTextBox);
Figure 1 shows the property editor embedded in the RssReader control.
Figure 1. The property editor of the RssReader control
As mentioned earlier, an additional piece of user interface is required to let end users drive the control in and out of the edit mode. The Update and Cancel link buttons in Figure 1 are created as part of the edit form in the CreateButtonBar helper method.
protected virtual void CreateButtonBar(Table parent) { // Create the button row TableRow row = new TableRow(); parent.Rows.Add(row); TableCell cell = new TableCell(); row.Cells.Add(cell); // Create the Update button LinkButton linkUpdate = new LinkButton(); cell.Controls.Add(linkUpdate); linkUpdate.Text = "Update"; linkUpdate.Click += new EventHandler(linkUpdate_Click); // Add some blanks cell.Controls.Add(new LiteralControl(" ")); // Create the Cancel button LinkButton linkCancel = new LinkButton(); cell.Controls.Add(linkCancel); linkCancel.Text = "Cancel"; linkCancel.Click += new EventHandler(linkCancel_Click); }
Both buttons are associated with handlers for the Click event. When the user clicks one of the two buttons, the page that contains the control posts back and executes the code in the event handler.
void linkCancel_Click(object sender, EventArgs e) { // Restore view mode ShowEditor = false; // Recreate the UI CreateChildControls(); DataBind(); } void linkUpdate_Click(object sender, EventArgs e) { // Modify the state of the control RssFeed = _rssFeedTextBox.Text; // Restore view mode ShowEditor = false; // Recreate the UI CreateChildControls(); DataBind(); }
The handler for the Cancel button simply hides the input form and restores the classic view of the control. It sets the ShowEditor property, recreates the control tree accordingly, and reads the feed again. The handler for the Update button updates the state of the control (for example, sets the RssFeed property), and then repeats the same actions. In this example, only one property can be dynamically modified through the embedded editor. This is arbitrary; you can enhance and extend the input form at-will, and make it include as many properties as needed.
How is the RssReader turned into edit mode? In general, you can do that in a couple of ways. You can use a public property (for example, a Boolean property such as ShowEditor) to set it programmatically, or you can opt for a user interface element, such as a made-to-measure button. Of course, these two options are not mutually exclusive, and they can be coded together. To implement the first option, all that you have to do is change the modifier of ShowEditor from protected to public. Let's tackle the second option, which entails adding a new user interface element.
The ideal place for such an interface element is the toolbar where, in the previous version of the control, I put the link button to refresh the contents. Figure 2 shows the new standard interface of the RssReader control.
Figure 2. The new look of the RssReader control
The Edit button is optionally added in the rendering process, by checking the value of a new Boolean property: AllowUsersToEdit.
if (AllowUsersToEdit) AddEditButton(toolbar);
The property returns false by default. The following code shows how the Edit button is created and appended to the RssReader control's tree.
protected virtual void AddEditButton(TableRow toolbar) { // Create the cell TableCell editCell = new TableCell(); editCell.HorizontalAlign = HorizontalAlign.Right; toolbar.Cells.Add(editCell); // Add and configure the link button _buttonEdit = new LinkButton(); editCell.Controls.Add(_buttonEdit); _buttonEdit.Attributes["hidefocus"] = "true"; _buttonEdit.ToolTip = "Click here to edit control properties"; _buttonEdit.Text = EditButtonText; // Add event handlers _buttonEdit.Click += new EventHandler(_buttonEdit_Click); }
The Click event handler sets the ShowEditor property and recreates the control tree.
protected void _buttonEdit_Click(object sender, EventArgs e) { ShowEditor = true; CreateChildControls(); }
At this point, the new version of the RssReader control is pretty much done. It incorporates a property editor, and supplies the ability to show and hide it, in order to refresh the values of some properties.
Two points remain open or, at a minimum, require further explanation. One regards the structure and layout of the property editor; the other regards values entered by different users, and touches on the key theme of persistence. Let's tackle layout first.
The Format of the Editor
In this example, the layout of the editor is hard-coded in the control. As a result, no aspects of it can be customized, except those aspects that you, as the control developer, decide to make customizable through ad hoc properties. Creating a form by using code adds flexibility to the resulting component, and doesn't make it dependent on other resources.
Are there other options?
The property editor is a collection of controls, and in ASP.NET, you can express a collection of child server controls through user controls or templates. In the former case, you define a string property to contain the URL of the user control (an .ascx resource), and load it when necessary. If you opt for templates, page authors using the RssReader control are required to specify the template according to any rules you set.
In the end, user controls and templates have common aspects. The key fact is that you can use a user control wherever a template is accepted. If you load a .ascx resource through the LoadTemplate method of the Page class, you obtain an object that implements the ITemplate interface—the same interface as any template declared within a server control.
Looking ahead, the best option seems to be a hard-coded format that is made customizable by using a template property. If the page author sets the template, the template overrides the hard-coded layout. To set the template, you can use a user control, or any custom object that implements the ITemplate interface.
You deliver a self-contained custom control that embeds its own property editor; your users are free to redefine and customize the editor at-will.
Non-Postback Editors
Whether you use a hard-coded editor or a user-defined template, the host page still needs to post back in order to display the editor and apply any changes. Is there any chance of displaying an editor without refreshing the whole page?
One option is to use popup windows. Basically, you bind the aforementioned Edit link button to a piece of script code that displays a popup window. The popup window needs a URL in order to display its contents. The URL can point to a companion HTTP handler that serves the HTML to set up the property editor.
Another option involves downloading hidden HTML elements for the editor the first time you download the host page. In other words, the RssReader control will download its own user interface markup, plus any markup necessary for the editor. The Link button points to another piece of script code that toggles the visibility flag between the standard user interface and the editor.
If you use a popup, a roundtrip is still necessary in order to display the editor; no roundtrips occur if you use hidden HTML elements. To save changes and refresh the contents of the control, a postback is required instead. If you don't want to refresh the whole page, you must resort to script callbacks or, in general, out-of-band, script-driven calls.
Persisting Changes on a Per-User Basis
Now that the RssReader is up and running, let's consider a realistic scenario. You create a page that contains the control, and the page goes live on a website. The first user visits the page and finds out that the page incorporates a given blog. The user dislikes that blog and wants to change it. Therefore, she clicks the Edit button and makes the reader point to another feed. At this point, the second user connects and receives the updated contents of the originally set blog. If the first user opens a new session later, any changes she made are lost.
What does this mean? Each user can modify what he or she sees about the RssReader, but changes are not persistent. Wouldn't it be nice if the control could remember each user's settings?
Saving data on a per-user basis is not necessarily an issue in ASP.NET 2.0, and for two good reasons. The first is the new user profile API; the second is the provider model.
The user profile API is designed for persistent storage of structured data, using a friendly and type-safe API. The application defines its own model of personalized data, and the ASP.NET runtime does the rest by parsing and compiling that model into a class. Each member of the personalized class data corresponds to a piece of information that is specific to the current user. Loading and saving personalized data is completely transparent to end users, and it doesn't even require the page author to know much about the internal plumbing.
When the application runs, and a page is displayed, ASP.NET dynamically creates a profile object that contains, properly typed, the properties you have defined in the data model. The object is then added to the current HttpContext object, and it is available to pages through the Profile property.
The data storage is hidden from the user and, to some extent, from the programmers. The user doesn't need to know how and where the data is stored; the programmer simply needs to indicate what type of profile provider he wants to use. The profile provider determines the database to use—typically, a Microsoft SQL Server database—but custom providers and custom data storage models can also be used.
Profile data is user-specific, but the data model is application-specific. What you need to do in this case is save user-specific data on a per-control basis. This means that the control itself should provide its own implementation of either a provider mechanism or profile data.
One possible approach is to have the RssReader control fire an event—for example, Updating—whenever the Update button is clicked. The host page can intercept the event, capture sensitive information (for example, the feed URL), and store it along with user information. The page has full access to the Profile object and knows about the user profile data model in use. Can the RssReader control do the same, and consume profile information directly? The answer is no, not if you want to keep the RssReader control loosely coupled with the host application. The profile data is specific to the application, and a server control should be usable in a variety of ASP.NET applications.
Can the control incorporate its own persistence layer? This is a different thing indeed. In the ASP.NET 2.0 framework, there's an example that demonstrates that server controls can maintain their own persistence: Web Parts.
Web Parts have editors for modifying property values on-the-fly. When you're done with your changes, the Web Parts framework—not the host application—takes care of saving data somewhere. When the page is refreshed, or when the same user reconnects to the modified page, the latest changes are retrieved and applied. Web Parts use the ASP.NET provider model to store control-specific information on a per-user basis. This is definitely the way to go if you want each user to be able to persistently change the RSS feed of an instance of the RssReader control.
You define a custom provider class that saves and retrieves the sensitive data for the control—in this case, the sole RSS feed string. Next, you modify the RssReader control to read and write the RssFeed property through this provider class. More exactly, the control should be modified to read/write personalizable properties through the currently selected provider. Registered providers should go to a custom section of the web.config file where the default provider is also indicated. The standard provider might save data to a SQL Server Express database or to an XML file—it's your choice. By writing custom providers, other developers can extend the behavior of the RssReader, without affecting the base code, and without requiring the full source code.
Conclusion
A rich control is primarily a control that counts a large number of properties. Most of these properties can be set at design-time and run-time. Most of these properties may be set dynamically, but not necessarily through programmatic code. Depending on the final goal of the control you write, there might be properties for which an embedded editor is just fine. As an example, think of Web Parts. You can visually edit the values of a number of Web Parts properties—all properties decorated with a given attribute. The property editor is embedded in the controls, and the new values are persisted automatically and on a per-user basis.
In this article, I demonstrated how to add an editor to a custom control and discussed ways to make changes persistent for each user.
About the author
Dino Esposito is a mentor at Solid Quality Learning, and the author of Programming Microsoft ASP.NET 2.0—Core Reference and Programming Microsoft ASP.NET 2.0 Applications—Advanced Topics, both from Microsoft Press. Late-breaking news is available at https://weblogs.asp.net/despos.