Building the Web Layer Using ASP.NET AJAX

This chapter is excerpted from Building a Web 2.0 Portal with ASP.NET 3.5: Learn How to Build a State-of-the-Art Ajax Start Page Using ASP.NET, .NET 3.5, LINQ, Windows WF, and More by Omar AL Zabir, published by O'Reilly Media

Building a Web 2.0 Portal with ASP.NET 3.5

Logo

Buy Now

The biggest challenge you'll face developing an Ajax web portal is providing almost all of your web application's functionality on a single page. A web portal is all about aggregating many services in one place. It's a never-ending challenge to have as many features on one page as possible yet keep them small, simple, and fast. So, the Default.aspx is the most complicated of all the pages in your application because it does everything from loading Ajax frameworks to serving the widgets on the page. Sometimes it is the only page users will see during their entire stay on the web site.

In this chapter, you will learn to code the Start page Default.aspx, the widget container, and the IWidget and IWidgetHost interfaces. Then you will put it all together and build a Flickr photo widget and a RSS widget. Finally, you will use ASP.NET 2.0 Membership and Profile providers to implement authentication, authorization, and profiles.

Implementing the Start Page of a Web Portal

The Default.aspx page in this web project is the Start page of the Ajax web portal. (see Figure 3.1, "The Default.aspx page contains almost everything in the Start page").

It contains the following:

  • The header which shows a search box

  • The Add Stuff area where users can add more widgets

  • The tab bar

  • The columns

  • The footer

The Default.aspx page starts with regular ASP.NET page syntax (see Example 3-1, "Default.aspx, part 1: Declarations of external components").

Figure 3.1. The Default.aspx page contains almost everything in the Start page

The Default.aspx page contains almost everything in the Start page

Example 3-1. Default.aspx, part 1: Declarations of external components

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_
Default" Theme="Default" EnableSessionState="False" %>
<%@ OutputCache Location="None" NoStore="true" %>
<%@ Register Src="Header.ascx" TagName="Header" TagPrefix="uc1" %>
<%@ Register Src="Footer.ascx" TagName="Footer" TagPrefix="uc2" %>
<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit"
TagPrefix="ajaxToolkit" %>
<%@ Register Assembly="CustomDragDrop" Namespace="CustomDragDrop" TagPrefix="cdd" %>
<%@ Register Src="WidgetContainer.ascx" TagName="WidgetContainer" TagPrefix="widget" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/
xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title>Ajax Web Portal</title>

The code block in Example 3-1, "Default.aspx, part 1: Declarations of external components" does the following:

  • Registers the WidgetContainer control, which is the web control for the widget container.

  • Adds a reference to the CustomDragDrop assembly, which contains the CustomDragDropExtender and the CustomFloatingBehavior.

  • Turns off all caching because the page must be downloaded from the server every time a user visits. If the page is cached, then users will not see the latest content in widgets after making changes on the page.

  • Registers the header and footer web controls.

Real-Life Example: When Caching Works Against You

Problem: Caching the Default.aspx page on the user's browser made site fail to work.

Solution: Turn off caching from Default.aspx and serve it directly from the server.

At Pageflakes, we used to cache the Default.aspx page on the user's browser. But we began receiving complaints that after the page loaded from the browser's cache, user actions-like clicking a button on a widget or adding a new widget-always failed. We tried our best to locate the problem but could never produce it.

Sometime after this, I was in the U.S. to attend Microsoft's MIX06. While I was at the hotel and using the hotel's Internet connection, I encountered the problem myself. Pageflakes was my browser homepage, and it loaded as soon as I started the browser for the first time. But when I tried to use the site, all XML HTTP calls failed. If I did a hard refresh, everything worked fine. After using the Fiddler Tool for a bit, which shows all HTTP requests and responses, I found that hotel had an intermediate Welcome page that loaded when you accessed the Internet for the first time to make sure the user is legitimate. As the Default.aspx page was coming from the cache, there was no request sent to the server and thus the hotel Internet provider could not validate who was using the Internet. So, all XML HTTP calls trying to reach the server were redirected to the Welcome page and failed. The same problem happened when I tried to access Pageflakes from Starbucks or from airport Wi-Fi zones. So, we turned off caching from Default.aspx and instead made sure that it was always served from our server and the problem disappeared. We stopped receiving complaints too.

The Header Area

The header area displays the logo, the search bars for Live.com and Google, and the Login link. After the header area, there's a script blockthat contains some client-side scripts that I will explain later on. These scripts are used to provide some client-side behaviors like calling a web service when dragging and dropping, showing/hiding elements, and so on.

Example 3-2, "Default.aspx, part 2: HTML snippet for the header part" shows the start of the <body> tag in Default.aspx.

Example 3-2. Default.aspx, part 2: HTML snippet for the header part

<body> 
    <form id="form1" runat="server">

        <!-- Render header first so that user can start typing search criteria while the
        huge runtime and other scripts download -->
        <uc1:Header ID="Header1" runat="server" />
        <div id="body">

        <asp:ScriptManager ID="ScriptManager1" runat="server"
        EnablePartialRendering="true" LoadScriptsBeforeUI="false" ScriptMode="Release">
            <Services> 
          <asp:ServiceReferenceInlineScript="true" Path="WidgetService.asmx"/>
            </Services>
        </asp:ScriptManager>

Tip

Browsers render HTML top to bottom. Put HTML elements before script blocks so that browser can render the HTML block before it starts downloading scripts.

The header control is put before the ScriptManager block. This makes the header emit its HTML right after the body tag. Because browsers render top to bottom, putting the HTML blockfor the search bar at the top of the page will help the browser render the search bar as soon as the user visits the site. Users typically launch their browsers many times a day for the sole purpose of web searching, so they need to be able to get to the search bar as soon as possible. Otherwise, they will by-pass your page. The HTML block in Example 3-2, "Default.aspx, part 2: HTML snippet for the header part" gives us the Google and Live.com search bars (see Figure 3.2, "The header needs to be rendered as soon as possible because it contains the search bar, which is usually one of the first tools used").

Figure 3.2. The header needs to be rendered as soon as possible because it contains the search bar, which is usually one of the first tools used

The header needs to be rendered as soon as possible because it contains the search bar, which is usually one of the first tools used

The Live Search box is pretty cool. It shows the search result in a pop-up right on the page as shown in Figure 3.3, "Live.com search shows the results on a popup DIV right on the page, which is very handy for web portals because users can stay on the page and see search results".

The HTML snippet shown in Example 3-3, "HTML snippet for adding Live.com search bar on any page" downloads the necessary components for providing the Live.com search functionality right on the page. Just put the snippet anywhere on the page and you have search functionality in that exact spot. Best of all, you don't need to register on the Live.com web site to get code snippets nor do you need to make web service calls to get search result. Both the UI and the functionality is provided inside the snippet.

Figure 3.3. Live.com search shows the results on a popup DIV right on the page, which is very handy for web portals because users can stay on the page and see search results

Live.com search shows the results on a popup DIV right on the page, which is very handy for web portals because users can stay on the page and see search results

Example 3-3. HTML snippet for adding Live.com search bar on any page

<div id="WLSearchBoxDiv" style="width:325px;">
  <table cellpadding="0px" cellspacing="0px">
     <tr id="WLSearchBoxPlaceholder">
         <td style="white-space:nowrap; color: White; padding-right: 5px; font-size
         12pt">Live Search</td>
         <td style="border-style:solid none solid solid;border-color:#4B7B9F;border
         width:2px;">
         <input id="WLSearchBoxInput" type="text" value="loading..." disabled="disabled"
         style="background-image:url(http://search.live.com/s/siteowner/searchbox_
         background.png);background-position-x:right;background-position-y:50;background-r
        epeat:no-repeat;height:16px;width:293px;border:none 0px #FFFFFF;"/>
        </td>
         <td style="border-style:solid;border-color:#4B7B9F;border-width:2px;">
               <input id="WLSearchBoxButton" type="image" src="http://search.live.com/s/
               siteowner/searchbutton_normal.PNG" align="absBottom" style="border-style:
              none"/>
          </td>
    </tr>
</table>

<script type="text/javascript" charset="utf-8">
  var WLSearchBoxConfiguration= 
  {
    "global":{
      "serverDNS":"search.live.com"
    },
    "appearance":{
      "autoHideTopControl":false,
      "width":600,
      "height":400
    },
    "scopes":[
      {
        "type":"web",
        "caption":"Web",
        "searchParam":""
      }
    ]
  }
</script>
<script language="javascript" defer="defer" src="http://search.live.com/bootstrap.
js?ServId=SearchBox&ServId=SearchBoxWeb&Callback=WLSearchBoxScriptReady"></script>
</div>

This pop-up area appears when the user clicks on the Add Stuff link. This widget gallery showcases all the available widgets. Users can choose which widget to add to the page from this area. It renders a five-column view of widgets as shown in Figure 3.4, "Add Stuff area shows where all the widgets in the database are available".

Figure 3.4. Add Stuff area shows where all the widgets in the database are available

Add Stuff area shows where all the widgets in the database are available

The Add Stuff area is inside a single UpdatePanel control that contains buttons for adding and hiding widgets, plus the collection (list) of available widgets. The UpdatePanel control makes sure that user actions, such as adding a widget, happen asynchronously, without any postbacks (see Example 3-4, "Default.aspx, part 3(a): HTML snippet for the add stuff area (partial)").

Example 3-4. Default.aspx, part 3(a): HTML snippet for the add stuff area (partial)

<asp:UpdatePanel
        ID="AddContentUpdatePanel"
        runat="server"
        UpdateMode="conditional">

    <ContentTemplate>
        <asp:LinkButton
            ID="ShowAddContentPanel"
            runat="server"
            Text="Add stuff &#187;"
            CssClass="add_stuff_toggle"
            OnClick="ShowAddContentPanel_Click"/>

    <asp:LinkButton
        ID="HideAddContentPanel"
        runat="server"
        Text="Hide Stuff"
        CssClass="add_stuff_toggle"
        Visible="false"
        OnClick="HideAddContentPanel_Click" />

The update panel contains the Add Stuff and Hide Stuff linkbuttons. They toggle on the user's click. When the user clicks the Add Stuff link, the widget collection is loaded and displayed inside a panel named AddContentPanel. The HTML markup of AddContentPanel is shown in Example 3-5, "Default.aspx, part 3(b): AddContentPanel HTML snippet".

Example 3-5. Default.aspx, part 3(b): AddContentPanel HTML snippet

<asp:Panel ID="AddContentPanel" runat="Server"
     Visible="false"
     CssClass="widget_showcase" >
          <div style="float:left">
                <b>Click on any of the item to add it to your page.</b>
            </div>
          <div style="float:right">
                 <asp:LinkButton ID="WidgetListPreviousLinkButton" runat="server"
                      Visible="false"
                      Text="< Previous"
                      OnClick="WidgetListPreviousLinkButton_Click" />
                 |
                 <asp:LinkButton ID="WidgetListNextButton" runat="server"
                      Visible="false"
                      Text="Next >"
                      OnClick="WidgetListNextButton_Click" />
          </div>
        <br /><br />
          <asp:DataList ID="WidgetDataList" runat="server"
               RepeatDirection="Vertical"
               RepeatColumns="5"
               RepeatLayout="Table"

               CellPadding="3"
               CellSpacing="3"
               EnableViewState="False"
               ShowFooter="False"
               ShowHeader="False">
                    <ItemTemplate>
                         <asp:Image ID="Icon" runat="server"
                              ImageUrl='<%# Eval("Icon") %>'
                              ImageAlign="AbsMiddle" />&nbsp;
                         <asp:LinkButton ID="AddWidget" runat="server"
                              CommandArgument='<%# Eval("ID") %>'
                              CommandName="AddWidget" >
                              <%# Eval("Name") %>
                         </asp:LinkButton>
                    </ItemTemplate>
          </asp:DataList>
</asp:Panel>

The AddContentPanel appears when the user clicks the Add Stuff link. Inside, the DataList named WidgetDataList is bound to the widget collection in the database at runtime from the code. The Add Stuff area fades in when a user clicks the Add Stuff link. An AnimationExtender runs the fade-in and fade-out effect (see Example 3-6, "Default.aspx, part 3(c): The AnimationExtender fades in and out in the Add Stuff area").

Example 3-6. Default.aspx, part 3(c): The AnimationExtender fades in and out in the Add Stuff area

          <ajaxToolkit:AnimationExtender ID="AddContentPanelAnimation" runat="server"
               TargetControlID="AddContentPanel">
               <Animations>
                    <OnLoad>
                         <FadeIn minimumOpacity=".2" />
                    </OnLoad>
               </Animations>
          </ajaxToolkit:AnimationExtender>
     </ContentTemplate>
</asp:UpdatePanel>

The widget list is loaded by the LoadAddStuff function in Default.aspx.cs. The function just binds a list of widgets to the DataList control, and the rendering is done via simple data binding (see Example 3-7, "Default.aspx.cs: LoadAddStuff function").

Example 3-7. Default.aspx.cs: LoadAddStuff function

private void LoadAddStuff()
    {
        this.WidgetDataList.ItemCommand += new DataListCommandEventHandler(WidgetDataList_
        ItemCommand);

        var itemsToShow = WidgetList.Skip(AddStuffPageIndex*30).Take(30);
        this.WidgetDataList.DataSource = itemsToShow;
        this.WidgetDataList.DataBind();

        // Enable/Disable paging buttons based on paging index

        WidgetListPreviousLinkButton.Visible = AddStuffPageIndex > 0;
        WidgetListNextButton.Visible = AddStuffPageIndex*30+30 < WidgetList.Count;
    }

Inside the LoadAddStuff function, you will load the widget list from the database.List<Widget>WidgetList is a private variable that loads all the widgets from the data-base only once and then stores in the cache. Example 3-8, "Loading WidgetList once and cache it for the lifetime of the application" uses the DashboardFacade to load the widget list.

Example 3-8. Loading WidgetList once and cache it for the lifetime of the application

private List<Widget> WidgetList
{
     get
    {
          List<Widget> widgets = Cache["Widgets"] as List<Widget>;
          if( null == widgets )
          {
               widgets = new DashboardFacade(Profile.UserName).GetWidgetList( );
               Cache["Widgets"] = widgets;
          }
          return widgets;
    }
}

WidgetList returns all the widgets defined in the widget table. You can't show all the widgets at once on the data list, so you need to do paging on the WidgetList collection. Paging can be done by using two new function extensions-Skip and Take- that were introduced in LINQ. LINQ adds these two functions on all lists so you can easily do paging on any List<> instance. In the LoadAddStuff function, Skip skips 30 widgets per page and Take takes 30 widgets from the current page. The result after Skip and Take is another generic List<T> of widgets of type List<Widget>, which always contains 30 or fewer items.

When a user clicks on any of the widget links on the widget list, a new instance of the widget is added on the first column of the page. This is handled in the ItemCommand event of the WidgetDataList as shown in Example 3-9, "Creating new widget when user clicks on a widget from the widget list".

Example 3-9. Creating new widget when user clicks on a widget from the widget list

void WidgetDataList_ItemCommand(object source, DataListCommandEventArgs e)
{
    int widgetId = int.Parse( e.CommandArgument.ToString( ) );

    DashboardFacade facade = new DashboardFacade(Profile.UserName);
    WidgetInstance newWidget = facade.AddWidget( widgetId );

    ...
    ...
}

The Tab Bar

The tab area shown in Figure 3.5, "Each tab represents a virtual page where the user can put more widgets" is inside an UpdatePanel because you want the user to add, edit, and delete tabs without refreshing the page. Actions like adding a new tab or editing a tab title do not require the widget area to be refreshed. However, when the user switches tabs by clicking on another tab, both the tab bar and the widget area refresh to show the new widgets on the new tab.

Figure 3.5. Each tab represents a virtual page where the user can put more widgets

Each tab represents a virtual page where the user can put more widgets

The HTML snippet for the tab bar is shown in Example 3-10, "Default.aspx, part 4: HTML snippet for the tab bar".

Example 3-10. Default.aspx, part 4: HTML snippet for the tab bar

<asp:UpdatePanel ID="TabUpdatePanel" runat="server"
     UpdateMode="conditional">
    <ContentTemplate>
          <div id="tabs">
               <ul class="tabs" runat="server" id="tabList">
                    <li class="tab inactivetab">
                         <asp:LinkButton id="Page1Tab" runat="server"
                              Text="Page 1">
                         </asp:LinkButton>
                    </li>
                    <li class="tab activetab">
                         <asp:LinkButton id="Page2Tab" runat="server"
                              Text="the section called "Defining a Web Portal"">
                         </asp:LinkButton>
                    </li>
               </ul>
          </div>
     </ContentTemplate>
</asp:UpdatePanel>

The tabs are generated at runtime inside the <UL> named tabs. Each tab is represented by one <LI> tag containing one LinkButton for the Tab title. Example 3-11, "Creating tabs dynamically" shows how the tab bar is generated from the user's Page collection.

Example 3-11. Creating tabs dynamically

 private void SetupTabs( )
    {
        tabList.Controls.Clear( );

        var setup = _Setup;
        var currentPage = setup.CurrentPage;

    foreach( Page page in setup.Pages )
    {
        var li = new HtmlGenericControl("li");
        li.ID = "Tab" + page.ID.ToString( );
        li.Attributes["class"] = "tab " + (page.ID == currentPage.ID ? "activetab" :
        "inactivetab");

        var linkButton = new LinkButton( );
        linkButton.ID = page.ID.ToString( );
        linkButton.Text = page.Title;
        linkButton.CommandName = "ChangePage";
        linkButton.CommandArgument = page.ID.ToString( );

        if( page.ID == currentPage.ID )
            linkButton.Click += new EventHandler(PageTitleEditMode_Click);
        else
            linkButton.Click += new EventHandler(PageLinkButton_Click);

        li.Controls.Add(linkButton);
        tabList.Controls.Add(li);
    }

You create one <LI> inside the <UL> for each page. You will notice that I have marked the <UL> tag as runat="server", which means the <UL> tag is now a server control and is available from behind the code. There are two types of <LI> created, one for the active tab (current page) and one for inactive tabs (other pages). Each <LI> contains one LinkButton that acts as a clickable tab title. When you click the title of the active tab, it allows you to change the tab title. But when you clickon an inactive tab, it just switches to that tab and loads the widgets on that page.

At the end of the tab bar, there's a "new tab" linkthat adds one new Page object to the user's Page collection. The new page is represented as a new tab on the tab bar. The "new tab" link is a dynamically created LinkButton, as shown in Example 3-12, "Creating a Add New Tab button, which adds a new tab on the tab bar and creates a brand new page for user".

Example 3-12. Creating a Add New Tab button, which adds a new tab on the tab bar and creates a brand new page for user

    var addNewTabLinkButton = new LinkButton( );
    addNewTabLinkButton.ID = "AddNewPage";
    addNewTabLinkButton.Text = "new tab";
    addNewTabLinkButton.Click += new EventHandler(addNewTabLinkButton_Click);
    var li2 = new HtmlGenericControl("li");
    li2.Attributes["class"] = "newtab";
    li2.Controls.Add(addNewTabLinkButton);
    1tabList.Controls.Add(li2);

When the user clicks on the Add New Tab link, he creates a new Page object with the default setting, makes the new page a current page, reloads the tab bar, and refreshes columns to show the widgets on the new page. The clickhandler is defined in Example 3-13, "Handling click on "Add New Tab" link and creating new page for user".

    void addNewTabLinkButton_Click(object sender, EventArgs e)
    {
        new DashboardFacade(Profile.UserName).AddNewPage( );

        this.ReloadPage(wi => true);

        this.RefreshAllColumns( );
    }

The ReloadPage function loads the widgets on the current page. A lambda expression-wi => true-is used here. The details of the ReloadPage function are explained later in this chapter. But basically, it informs the function that all the widgets on this page will have the FirstVisit flag set to true. This means the widgets will not see that this is a postback, but instead see it as a first visit. Remember, widgets use IWidgetHost.IsPostback to determine whether it's a postbackor a first visit. Because tab switching is like a first visit for the widgets on the new tab, you need to tell all widgets that it's a first visit, not a postback. This makes IWidgetHost.IsPostback return false. Details about how tab switching works and how widgets know whether they should assume that the visit is postbackor nonpostback are explained in the upcoming section "Page Switching: Simulating a Nonpostback Experience."

When a user clicks on an inactive tab, it changes to active. The widgets on that page appear on the columns. The new page becomes the user's current page. So, when the user comes backto the site, he is taken to the last active tab, not the first tab. The user's last active tab is stored in a UserSetting table. The clickhandler for inactive tabs is defined in Example 3-14, "The click handler on the inactive tab makes the tab active when clicked". All inactive tabs call the same event handler.

Example 3-14. The click handler on the inactive tab makes the tab active when clicked

    void PageLinkButton_Click(object sender, EventArgs e)
    {
        var linkButton = sender as LinkButton;

        // Get the page ID from the title link button ID

        var pageId = int.Parse(linkButton.ID);
        if( _Setup.UserSetting.CurrentPageId != pageId )
        {
            DatabaseHelper.Update<UserSetting>( _Setup.UserSetting, delegate( UserSetting
            u )
            {
                u.CurrentPageId = pageId;
            });
...

        }
    }

The user can edit a tab title by clicking on the active tab title. When a user clicks on the title LinkButton, it replaces itself with a text box and a "save" button as shown in Figure 3.6, "When a user clicks on the My First Page tab, it switches to Edit Mode and allows the user to change the tab title".

Figure 3.6. When a user clicks on the My First Page tab, it switches to Edit Mode and allows the user to change the tab title

When a user clicks on the My First Page tab, it switches to Edit Mode and allows the user to change the tab title

The clickon the tab is handled by PageTitleEditMode_Click, the server-side event handler, which replaces the title LinkButton with one TextBox and a Button. The event handler is defined in Example 3-15, "The event handler when the user switchs to edit mode on a tab".

Example 3-15. The event handler when the user switchs to edit mode on a tab

    void PageTitleEditMode_Click(object sender, EventArgs e)
    {
        var linkButton = sender as LinkButton;

        var editTextBox = new TextBox();
        editTextBox.ID = "PageNameEditTextBox";
        editTextBox.Text = linkButton.Text;

        var saveButton = new Button();
        saveButton.Text = "Save";

        linkButton.Parent.Controls.Add(editTextBox);
        linkButton.Parent.Controls.Add(saveButton);
        linkButton.Parent.Controls.Remove(linkButton);
    }

Because these two controls are created dynamically, they must have a fixed ID. Otherwise they will fail to postback.

Notice that there's no clickhandler for the saveButton in Example 3-15, "The event handler when the user switchs to edit mode on a tab". So, how does the changed page title get saved? If you use the standard clickhandler and server-side event approach of ASP.NET, you have to do the following:

  • Remember which page is being edited in ViewState

  • Render the tabs in SetupTabs function

    - Check to see if the tab is being edited before creating a tab
    - Render the LinkButton for the title, instead of rendering a text box and a save button
    - Hook the click handler on the button

Why a Fixed ID for Dynamically Created Controls Is Needed

When ASP.NET creates a dynamic control, it assigns a sequential ID to it, e.g., ctrl0. The ID is assigned according to the order it is added in the parent control's Controls collection. This means that if a button is dynamically created on the first visit, it can get an ID like ctrl0. But on a postback, if some other UI elements are created in the same Controls collection before you create the button again, it will get a different ID- say, ctrl1. As a result, when the button tries to restore its state from ViewState, it will lookfor entries against ID ctrl1, not ctrl0. But on the first visit, the button persisted its state against ctrl0. So, it will fail to restore its state properly. To avoid this, assign a unique ID to each dynamically created control.

This was too much effort. So, let's try a quick hack. Whenever there's a postback, you can access the posted controls' values directly from Request object. So, you can easily get the value of the text box when it is posted back (see Example 3-16, "Code called during the Page_Load event").

Example 3-16. Code called during the Page_Load event

    if( ScriptManager1.IsInAsyncPostBack )
    {
           ...
           ...
             string pageName = Request["PageNameEditTextBox"];
             if( !string.IsNullOrEmpty(pageName) )
             {
                 new DashboardFacade(Profile.UserName).ChangePageName(pageName);
                 _Setup.CurrentPage.Title = pageName;
                 this.SetupTabs( );
             }
            ...
    }

In Example 3-16, "Code called during the Page_Load event", the value of the TextBox is read directly from the Request object and the page title in the database is changed accordingly. There's no need to recreate TextBox and Button just to read the page name. Another advantage of this approach is that these functions are never recreated during the postback, so you don't have to remove them and switch backto view mode after the edit. It's a quickway to implement in-place edit modules that pop up and go away. At the clickof a button, you can create the controls that will serve the in-place edit area and then read the value of the controls directly from the Request object in the Page_Load handler.

However, if you want server-side validation, then you have to go backto complicated approach of always recreating the controls on asynchronous postbackbecause you need to show validation errors, and the controls need to be there to show their invalid content error message. For simplicity, I skipped validation here.

The Widget Area: The Three-Column Widget View

There are the three columns below the tab bar reserved for widgets, which we will refer to as the widget area. The widget area is a simple three-column HTML table. Each column contains one UpdatePanel and one DropCue, as shown in Example 3-17, "Default.aspx, part 7: Defining the three-column view of the widget area".

Example 3-17. Default.aspx, part 7: Defining the three-column view of the widget area

<table width="98%" cellspacing="10" align="center" class="table_fixed"
height="100%">
<tbody>
     <tr>
         <td class="column">
              <asp:UpdatePanel ID="LeftUpdatePanel" runat="server"
                   UpdateMode="Conditional" >
                   <ContentTemplate>
                        <asp:Panel ID="LeftPanel" runat="server"
                             class="widget_holder"
                             columnNo="0">
                             <div id="DropCue1" class="widget_dropcue">
                             </div>
                        </asp:Panel>

                        <cdd:CustomDragDropExtender ID="CustomDragDropExtender1"
                       runat="server"
                             TargetControlID="LeftPanel"
                             DragItemClass="widget"
                             DragItemHandleClass="widget_header"
                             DropCueID="DropCue1"
                             OnClientDrop="onDrop" />

                   </ContentTemplate>
              </asp:UpdatePanel>
        </td>

Widgets are loaded inside Panels named LeftPanel, MiddlePanel, and RightPanel. Each widget is dynamically loaded and added inside these panels. If a widget is on the first column, it is added inside LeftPanel. The CustomDragDropExtender attached to these Panels provides the drag-and-drop support for widgets, as shown in Figure 3.7, "With the three-column view of widgets, each column contains one UpdatePanel where the widgets are loaded".

Figure 3.7. With the three-column view of widgets, each column contains one UpdatePanel where the widgets are loaded

With the three-column view of widgets, each column contains one UpdatePanel where the widgets are loaded

Loading the Start Page

Unlike regular ASP.NET pages, there is no code in the Page_Load event; instead, it is in the CreateChildControls function. ASP.NET calls this function when it needs to create server-side controls on the page. During postback processing on a page, unless all the dynamically created controls are already in place by the time the page loads ViewState, the events on those dynamic controls will not get fired properly. The Page_Load event happens too late to do this. In Default.aspx, the entire page is constructed dynamically, including the page bar. So, the controls must be created before they load their ViewState and process postback information, as shown in Example 3-18, "Dynamically create all controls and widgets on the page". In the ASP.NET page life cycle, CreateChildControls is the best place to create all dynamic controls that need to load states from ViewState and process postback information.

Example 3-18. Dynamically create all controls and widgets on the page

protected override void CreateChildControls( )
    {
        base.CreateChildControls( );

        this.LoadUserPageSetup(false);
        this.SetupTabs( );
        this.LoadAddStuff( );

        if( ScriptManager1.IsInAsyncPostBack )
        {
...
...
        }
        else
        {
            // First visit, non postback
           this.SetupWidgets( wi => true );
           this.SetupTabs( );
        }
    }

There are three steps involved in loading the full page:

  1. Load the user page setup and user setting (the false passed to the method tells the method not to look for cached information)

  2. Render the tabs that shows user's pages

  3. Load the widgets on the current page

The first step is to load the user's page setup, which includes the following:

  • User's setting, e.g., current page

  • User's page collection

  • Widgets only on the current page

The LoadUserPageSetup method checks whether this is a first-time user visiting the site. On the very first visit, the user's page setup is created. On subsequent visits, the user's existing page setup is loaded, as shown in Example 3-19, "For a new user, create the page setup; for an existing user, load the existing page setup".

Example 3-19. For a new user, create the page setup; for an existing user, load the existing page setup

private void LoadUserPageSetup(bool noCache)
    {
        if( Profile.IsAnonymous )
        {
            if( Profile.IsFirstVisit )
            {
                // First visit
                Profile.IsFirstVisit = false;
                Profile.Save( );

                _Setup = new DashboardFacade(Profile.UserName).NewUserVisit( );
            }
            else
            {
                _Setup = Cache[Profile.UserName] as UserPageSetup;
                if( noCache || null == _Setup )
                    _Setup = new DashboardFacade(Profile.UserName).LoadUserSetup( );
            }
        }
        else
        {
            _Setup = Cache[Profile.UserName] as UserPageSetup;
            if( noCache || null == _Setup )
                _Setup = new DashboardFacade(Profile.UserName).LoadUserSetup( );
        }
        // Cache the user setup in order to avoid repeated loading during postback
        Cache[Profile.UserName] = _Setup;
    }

In Example 3-19, "For a new user, create the page setup; for an existing user, load the existing page setup", the user's page setup is stored in an ASP.NET cache so that the whole setup is not repeatedly loaded from the database during asynchronous post-backs from widgets. Because there's no chance of a user's page setup being changed unless the user adds, edits, or deletes widgets or pages, you can safely cache the whole setup. However, the cache works for single-server and single-process hosting. If you have multiple servers in a web farm or if you have just one server, but Application Pool is configured to use multiple processes in web garden mode, then this cache approach will not work. For example, say two servers in your web farm have cached a user's page setup. The user now deletes the current page. Say Server A received the postback. The page is deleted from database and the latest page setup is now cached in Server A. But Server B still has the old page setup cached where the deleted page still exists. If the user adds a new widget and the request goes to Server B, it will try to add the widget on the nonexistent page and fail.

In web garden mode, multiple processes serve the application pool for the web site and suffer from a similar problem as requests from the same user go to different processes. The solution is to use commercial distributed cache solutions, but they are quite expensive. Such solutions give you a cache that is synchronized between all servers. If Server A updates an entry in the cache, it will be synchronized with Servers B, C, and D. When you have a large amount of traffic on the site, you won't be able to repeatedly load the whole page setup because it will put a lot of stress on the database server. In that case, you will have to go for caching the whole setup for as long as possible and use a distributed cache solution.

The next step is to dynamically create the widgets on the three columns. The function SetupWidgets shown in Example 3-20, "Dynamically create widget controls inside column panels" does this difficult job.

Example 3-20. Dynamically create widget controls inside column panels

    private void SetupWidgets(Func<WidgetInstance, bool> isWidgetFirstLoad)
    {
        var setup = Context.Items[typeof(UserPageSetup)] as UserPageSetup;

        var columnPanels = new Panel[] {
            this.FindControl("LeftPanel") as Panel,
            this.FindControl("MiddlePanel") as Panel,
            this.FindControl("RightPanel") as Panel };

        // Clear existing widgets if any
        foreach( Panel panel in columnPanels )
        {
            List<WidgetContainer> widgets = panel.Controls.OfType<WidgetContainer>( ).
           ToList( );
            foreach( var widget in widgets ) panel.Controls.Remove( widget );
        }

        foreach( WidgetInstance instance in setup.WidgetInstances )
        {
            var panel = columnPanels[instance.ColumnNo];

            var widget = LoadControl(WIDGET_CONTAINER) as WidgetContainer;
            widget.ID = "WidgetContainer" + instance.Id.ToString( );
            widget.IsFirstLoad = isWidgetFirstLoad(instance);
            widget.WidgetInstance = instance;

            widget.Deleted += new Action<WidgetInstance>(widget_Deleted);

            panel.Controls.Add(widget);
        }
    }

The reference to Func<> in the method parameters pertains to new functionality in C#.

This function first clears all the widgets from the three Panels. During asynchronous postback, SetupWidgets is called once to recreate the controls and give ASP.NET the exact same control hierarchy so that it can restore ViewState and fire events properly. After the events are processed, if there's any change on the widget area, such as a widget deleted or added, then SetupWidgets is called again to render the latest widgets. It's like calling a Bind method on a DataGrid or DataList again when something changes on the data source.

When SetupWidgets is called for a second time within the same request, the Panels already have the widgets. Thus, some widgets are created twice unless the Panels are cleared. Example 3-21, "Clearing all WidgetContainers from a panel using the OfType<> extension method in C# 3.0" uses some C# 3.0 language extensions. We use the OfType<> function to get only the widgets inside the Panels, not any other control. The purpose is to delete only the widgets and preserve everything else, like the extenders and the drop cue.

Example 3-21. Clearing all WidgetContainers from a panel using the OfType<> extension method in C# 3.0

     List<WidgetContainer> widgets = panel.Controls.OfType<WidgetContainer>().
    ToList( );
     foreach( var widget in widgets ) panel.Controls.Remove( widget );

After clearing the panels, one widget container is created for each widget instance. Now comes the Func<> part, which is another very useful extension in C# 3.0. It is called predicate. You can create lambda expressions and pass them as predicate during a function call. Lambda expressions are shorthand for delegates. Instead of creating a delegate function for simple expressions, you can pass them in a lambda expression form. For example, when a page is loaded for the first time, SetupWidgets is called with a lambda expression, which always returns true for all widget instances.

  SetupWidgets( wi => true );

This means, the following statement is always true for all widget instances:

   widget.IsFirstLoad = isWidgetFirstLoad(instance);

But you can also specify expressions that return true or false based on a specific condition:

 SetupWidgets( wi => wi.ColumnNo == 1 );

This will evaluate as true only for those widget instances that are on the middle column (column 1). By doing this, we are telling the function to load middle column widgets as if they are being loaded for the first time. An equivalent C# 2.0 implementation would be as follows:

    delegate bool SetupWidgetsDelegate( WidgetInstance wi);

    SetupWidgets( new SetupWidgetsDelegate(delegate(WidgetInstance wi)
    {
        return wi.ColumnNo == 1;
    }));

The reason why a lambda expression is used here instead of a simple Boolean true/ false is explained in the section "Page Switching: Simulating a Nonpostback Experience" later in this chapter. The idea is to set widget.IsFirstLoad = true for some widget instances and set widget.IsFirstLoad = false for some other widget instances. This decision depends on who is calling the SetupWidgets function. The expression that can take this decision is only known to the function that calls SetupWidgets. Thus, by using predicates, the caller can easily pass the expression to SetupWidgets and simplify the logic inside SetupWidgets.

Building a Custom Drag-and-Drop Extender for a Multicolumn Drop Zone

I first considered a plain vanilla JavaScript-based solution for my drag-and-drop functionality. It required less code, less architectural complexity, and was faster. Another reason was the high learning curve for properly making extenders in ASP.NET AJAX, given that there's hardly any documentation available on the Web (or at least that was the case when I was writing this book). However, writing a proper extender that pushes ASP.NET AJAX to the limit is a very good way to learn the ASP.NET AJAX framework's under-the-hood secrets. So, the two extenders introduced here will tell you almost everything you need to know about ASP.NET AJAX extenders.

Before I wrote my own implementation of drag and drop, I carefully looked at existing solutions. The Ajax Control Toolkit comes with a DragPanel extender that could be used to provide drag-and-drop support to panels. It also has a ReorderList control, which could reorder the items into a single list. Widgets are basically panels that flow vertically in each column. So, it could be possible to create a reorder list in each column and use the DragPanel to drag the widgets. But ReorderList couldn't be used because:

  • It strictly uses the HTML table to render its items in a column. But I have no table inside the columns, only one UpdatePanel per column.

  • It takes a drag handle template and creates a drag handle for each item at runtime. But there already is a drag handle created inside a widget, which is the widget header, so ReorderList can't create another drag handle.

  • It must have client-side callbackto JavaScript functions during drag and drop to make Ajax calls and persist the widget positions. The callback must provide the Panel where the widget is dropped, depending on which widget is dropped, and at what position.

The next challenge is with the DragPanel extender. The default implementation of drag and drop in the Ajax Control Toolkit doesn't work for these reasons:

  • When you start dragging, the item becomes absolutely positioned, but when you drop it, it does not become statically positioned. A small hackis needed for restoring the original positioning to static.

  • It does not bring the dragging item on top of all the items. As a result, when you start dragging, you see the item being dragged below other items, which makes the drag get stuck, especially when there's an IFrame.

For all these reasons, I made CustomDragDropExtender and CustomFloatingExtender. CustomDragDropExtender is for the column containers where widgets are placed. It provides the reordering support. You can attach this extender to any Panel control.

Example 3-22, "How to attach CustomDragDropExtender to a Panel" shows how you can attach this extender to any Panel and make that Panel support dragging and dropping widgets.

Example 3-22. How to attach CustomDragDropExtender to a Panel

<asp:Panel ID="LeftPanel" runat="server" class="widget_holder" columnNo="0">
        <div id="DropCue1" class="widget_dropcue">
        </div>
</asp:Panel>

<cdd:CustomDragDropExtender ID="CustomDragDropExtender1"
     runat="server"
     TargetControlID="LeftPanel"
    DragItemClass="widget"
     DragItemHandleClass="widget_header"
     DropCueID="DropCue1"
    OnClientDrop="onDrop" />
<cdd:CustomDragDropExtender> offers the following properties:
  • TargetControlID
    ID of the Panel that becomes the drop zone

  • DragItemClass
    All child elements inside the Panel having this class will become draggable, e.g., the widget DIV has this class so that it can become draggable.

  • DragItemHandleClass
    Any child element having this class inside the draggable elements will become the drag handle for the draggable element, e.g., the widget header area has this class, so it acts as the drag handle for the widget.

  • DropCueID ID
    ID of an element inside the Panel, which acts as DropCue.

  • OnClientDrop
    Name of a JavaScript function that is called when the widget is dropped on the Panel.

LeftPanel becomes a widget container that allows widgets to be dropped on it and reordered. The DragItemClass attribute on the extender defines the items that can be ordered. This prevents nonwidget HTML DIVs from getting ordered. Only the DIVs of the class "widget" are ordered. Say there are five DIVs with the class named widget. It will allow reordering of only the five DIVs, not any other element (see Example 3-23, "CustomDragDropExtender allows only drag-and-drop support for elements with a specific class").

Example 3-23. CustomDragDropExtender allows only drag-and-drop support for elements with a specific class

<div id="LeftPanel" class="widget_holder" >
        <div class="widget"> ... </div>
        <div class="widget"> ... </div>

        <div class="widget"> ... </div>
        <div class="widget"> ... </div>
        <div class="widget"> ... </div>

        <div>This DIV will not move</div>
        <div id="DropCue1" class="widget_dropcue"></div>
</div>

When a widget is dropped on the panel, the extender fires the function specified in OnClientDrop. It offers standard Ajax events. But unlike basic Ajax events where you have to programmatically bind to events, you can bind the event handler declaratively. So, instead of doing this:

  function pageLoad( sender, e ) {

      var extender1 = $get('CustomDragDropExtender1');
      extender1.add_onDrop( onDrop );

    }

you can do this:

    <cdd:CustomDragDropExtender ID="CustomDragDropExtender1"
         runat="server"
         OnClientDrop="onDrop" />

When the event is raised, the function named onDrop gets called. This is done with the help of some library functions available in ACT project. When the event is fired, it passes the container, the widget, and the position of where the widget is dropped as an event argument, as specified by the code in Example 3-24, "Client-side JavaScript event handler for receiving drag-and-drop notification".

Example 3-24. Client-side JavaScript event handler for receiving drag-and-drop notification

   function onDrop( sender, e )
    {
        var container = e.get_container( );
        var item = e.get_droppedItem( );
        var position = e.get_position( );

        var instanceId = parseInt(item.getAttribute("InstanceId"));
        var columnNo = parseInt(container.getAttribute("columnNo"));
        var row = position;

        WidgetService.MoveWidgetInstance( instanceId, columnNo, row );
    }

The widget location is updated on the server by calling the WidgetService.MoveWidgetInstance.

CustomDragDropExtender has three files:

  • CustomDragDropExtender.cs
    The server side extender implementation

  • CustomDragDropDesigner.cs
    Designer class for the extender

  • CustomDragDropExtender.js
    Client-side script for the extender

The code for the server-side class CustomDragDropExtender.cs is shown in Example 3-25, "Code for CustomDragDropExtender.cs".

Example 3-25. Code for CustomDragDropExtender.cs

[assembly: System.Web.UI.WebResource("CustomDragDrop.CustomDragDropBehavior.js", "text/
    javascript")]

namespace CustomDragDrop
{
    [Designer(typeof(CustomDragDropDesigner))]
    [ClientScriptResource("CustomDragDrop.CustomDragDropBehavior",
    "CustomDragDrop.CustomDragDropBehavior.js")]
    [TargetControlType(typeof(WebControl))]
    [RequiredScript(typeof(CustomFloatingBehaviorScript))]
    [RequiredScript(typeof(DragDropScripts))]
    public class CustomDragDropExtender : ExtenderControlBase
    {
        [ExtenderControlProperty]
        public string DragItemClass
        {
            get
            {
                return GetPropertyValue<String>("DragItemClass", string.Empty);
            }
            set
            {
                SetPropertyValue<String>("DragItemClass", value);
            }
        }

        [ExtenderControlProperty]
        public string DragItemHandleClass
        {
            get
            {
                return GetPropertyValue<String>("DragItemHandleClass", string.Empty);
            }
            set
            {
                SetPropertyValue<String>("DragItemHandleClass", value);
            }
        }

        [ExtenderControlProperty]
        [IDReferenceProperty(typeof(WebControl))]
        public string DropCueID
        {
            get
            {
                return GetPropertyValue<String>("DropCueID", string.Empty);
            }
            set
            {
                SetPropertyValue<String>("DropCueID", value);
            }
        }

        [ExtenderControlProperty( )]
        [DefaultValue("")]
        [ClientPropertyName("onDrop")]
        public string OnClientDrop
        {
            get
            {
                return GetPropertyValue<String>("OnClientDrop", string.Empty);
            }
            set
            {
                SetPropertyValue<String>("OnClientDrop", value);
            }
        }
    }
}

Most of the code in the extender defines the properties. The important part is the declaration of the class shown in Example 3-26, "Declaration of the class CustomDragDropExtender defines the required scripts for the extender and the type of control it can attach to".

Example 3-26. Declaration of the class CustomDragDropExtender defines the required scripts for the extender and the type of control it can attach to

[assembly: System.Web.UI.WebResource("CustomDragDrop.CustomDragDropBehavior.js", "text/
     javascript")]

namespace CustomDragDrop
{
    [Designer(typeof(CustomDragDropDesigner))]
    [ClientScriptResource("CustomDragDrop.CustomDragDropBehavior", "CustomDragDrop.
          CustomDragDropBehavior.js")]
    [TargetControlType(typeof(WebControl))]
    [RequiredScript(typeof(CustomFloatingBehaviorScript))]
    [RequiredScript(typeof(DragDropScripts))]
    public class CustomDragDropExtender : ExtenderControlBase
    {

The extender class inherits from ExtenderControlBase as defined in the Ajax Control Toolkit (ACT) project. This base class has additional features beyond those found with the extender base class that ships with ASP.NET AJAX. The ACT extender allows you to use the RequiredScript attribute, which makes sure all the required scripts are downloaded before the extender script is downloaded and initialized. The CustomDragDrop extender has a dependency on another extender named CustomFloatingBehavior. It also depends on ACT's DragDropManager. So, the RequiredScript attribute makes sure required scripts are downloaded before the extender script downloads. The ExtenderControlBase is a pretty big class and does a lot of workfor us. It contains default implementations for discovering all the script files for the extender and then renders them in proper order so that the browser downloads the scripts in correct order.

The [assembly:System.Web.UI.WebResource] attribute defines the script file containing the script for the extender. The script file is an embedded resource file.

The [ClientScriptResource] attribute defines the scripts required for the extender. This class is also defined in ACT. ExtenderControlBase uses this attribute to find out which JavaScript files are working for the extender and renders them properly.

The challenge is in writing the client side JavaScript for the extender. On the CustomDragDrop.js file, there's a JavaScript class that is the extender implementation, as shown in Example 3-27, "The JavaScript implementation of the extender class's constructor".

Example 3-27. The JavaScript implementation of the extender class's constructor

Type.registerNamespace('CustomDragDrop');

CustomDragDrop.CustomDragDropBehavior = function(element) {

    CustomDragDrop.CustomDragDropBehavior.initializeBase(this, [element]);

    this._DragItemClassValue = null;
    this._DragItemHandleClassValue = null;
    this._DropCueIDValue = null;
    this._dropCue = null;
    this._floatingBehaviors = [];
}

During initialization, this extender hooks on the Panel and the DropCue while the widget is being dragged and dropped over the Panel. See Example 3-28, "Initialize the CustomDragDrop extender and hook on the items".

Example 3-28. Initialize the CustomDragDrop extender and hook on the items

CustomDragDrop.CustomDragDropBehavior.prototype = {

    initialize : function( ) {
    // Register ourselves as a drop target.
    AjaxControlToolkit.DragDropManager.registerDropTarget(this);

    // Initialize drag behavior after a while
    window.setTimeout( Function.createDelegate( this, this._initializeDraggableItems
    ), 3000 );

    this._dropCue = $get(this.get_DropCueID( ));
    },

After initializing the DragDropManager and marking the Panel as a drop target, a timer is started to discover the draggable items inside the Panel and apply FloatingBehavior to them. FloatingBehavior makes a DIV draggable.

Tip

FloatingBehavior makes one DIV freely draggable on the page. But it does not offer drop functionality. DragDropBehavior offers the drop functionality that allows a freely moving DIV to rest on a fixed position.

Discovering and initializing FloatingBehavior for the draggable items is challenging work, as you see in Example 3-29, "Discovering draggable items and creating FloatingBehavior for each of item".

Example 3-29. Discovering draggable items and creating FloatingBehavior for each of item

    // Find all items with the drag item class and make each item
    // draggable
    _initializeDraggableItems : function( )
    {
        this._clearFloatingBehaviors( );

        var el = this.get_element( );

        var child = el.firstChild;
        while( child != null )
        {
            if( child.className == this._DragItemClassValue && child != this._dropCue)
            {
                var handle = this._findChildByClass(child, this._
               DragItemHandleClassValue);
                if( handle )
                {
                    var handleId = handle.id;
                    var behaviorId = child.id + "_WidgetFloatingBehavior";

                // make the item draggable by adding floating behaviour to it
                     var floatingBehavior = $create(CustomDragDrop.CustomFloatingBehavior,
                             {"DragHandleID":handleId, "id":behaviorId, "name":
                            behaviorId}, {}, {}, child);

                     Array.add( this._floatingBehaviors, floatingBehavior );
                }
            }
            child = child.nextSibling;
        }
    },

Here's the algorithm:

  • Run through all immediate child elements of the control to which the extender is attached.

  • If the child item has the class for draggable item, then:

    - Find any element under the child item that has the class for a drag handle; if such an item is found, then attach a CustomFloatingBehavior with the child item.

The _findChildByClass function recursively iterates through all the child elements and looks for an element that has the defined class. The code is shown in Example 3-30, "Handy function to find HTML elements by class name". It's an expensive process. So, it is important that the drag handle is very close to the draggable element. Ideally, the drag handle should be the first child of the draggable element, so the search for a widget header doesn't have to iterate through too many elements.

Example 3-30. Handy function to find HTML elements by class name

    _findChildByClass : function(item, className)
    {
        // First check all immediate child items
        var child = item.firstChild;
        while( child != null )
        {
            if( child.className == className ) return child;
            child = child.nextSibling;
        }

        // Not found, recursively check all child items
        child = item.firstChild;
        while( child != null )
        {
            var found = this._findChildByClass( child, className );
            if( found != null ) return found;
            child = child.nextSibling;
        }
    },

When a user drags an item over the Panel to which the extender is attached, DragDropManager fires the events shown in Example 3-31, "Events raised by DragDropManager".

Example 3-31. Events raised by DragDropManager

    onDragEnterTarget : function(dragMode, type, data) {
        this._showDropCue(data);
    },

    onDragLeaveTarget : function(dragMode, type, data) {
        this._hideDropCue(data);
    },

    onDragInTarget : function(dragMode, type, data) {
        this._repositionDropCue(data);
    },

While drag and drop is going on, you need to deal with the drop cue. The challenge is to find out the right position for the drop cue (see Figure 3.8, "When you drag a widget, a drop cue shows you where the widget will be dropped when mouse is released").

We need to find out where we should show the drop cue based on where the user wants to put the item. The idea is to find the widget that is immediately underneath the dragged item. The item underneath is pushed down by one position and the drop cue takes its place. While dragging, the position of the drag item can be found easily. Based on that, you can locate the widget below the drag item with the _findItemAt function shown in Example 3-32, "Find the widget at the x, y coordinate of the mouse".

Figure 3.8. When you drag a widget, a drop cue shows you where the widget will be dropped when mouse is released

When you drag a widget, a drop cue shows you where the widget will be dropped when mouse is released

Example 3-32. Find the widget at the x, y coordinate of the mouse

_findItemAt : function(x, y, item)
    {
        var el = this.get_element( );

        var child = el.firstChild;
        while( child != null )
        {
            if( child.className == this._DragItemClassValue && child != this._dropCue &&
            child != item )
            {
                var pos = Sys.UI.DomElement.getLocation(child);

                if( y <= pos.y )
                {
                    return child;
                }
           }
           child = child.nextSibling;
        }
        return null;
    },

The _findItemAt function returns the widget that is immediately underneath the dragged item. Now you can add the drop cue immediately above the widget to show the user where the widget being dragged can be dropped. The _repositionDropCue function, whose code is shown in Example 3-33, "Move the drop cue to the place where a widget can be dropped", relocates the drop cue to the position where a widget can be dropped.

Example 3-33. Move the drop cue to the place where a widget can be dropped

_repositionDropCue : function(data)
    {
        var location = Sys.UI.DomElement.getLocation(data.item);
        var nearestChild = this._findItemAt(location.x, location.y, data.item);

        var el = this.get_element( );

        if( null == nearestChild )
        {
            if( el.lastChild != this._dropCue )
            {
                el.removeChild(this._dropCue);
                el.appendChild(this._dropCue);
            }
        }
        else
        {
            if( nearestChild.previousSibling != this._dropCue )
            {
                el.removeChild(this._dropCue);
                el.insertBefore(this._dropCue, nearestChild);
            }
        }
    },

One exception to consider here is that there may be no widget immediately below the dragged item. This happens when the user is trying to drop the widget at the bottom of a column. In that case, the drop cue is shown at the bottom of the column.

When the user releases the widget, it drops right on top of the drop cue, and the drop cue disappears. After the drop, the onDrop event is raised to notify where the widget is dropped, as shown in Example 3-34, "Place the dropped widget on the right place and raise the onDrop event".

Example 3-34. Place the dropped widget on the right place and raise the onDrop event

_placeItem : function(data)
    {
        var el = this.get_element( );

        data.item.parentNode.removeChild( data.item );
        el.insertBefore( data.item, this._dropCue );

        // Find the position of the dropped item
        var position = 0;
        var item = el.firstChild;
        while( item != data.item )
        {
            if( item.className == this._DragItemClassValue ) position++;
            item = item.nextSibling;
        }
        this._raiseDropEvent( /*Container*/ el, /*dropped item*/ data.item, /*position*/
        position );
    }

Generally, you can define events in extenders by adding two functions in the extender as shown in Example 3-35, "Provide event subscription support in ASP.NET Ajax Extenders".

Example 3-35. Provide event subscription support in ASP.NET Ajax Extenders

    add_onDrop : function(handler) {
        this.get_events( ).addHandler("onDrop", handler);
    },

    remove_onDrop : function(handler) {
        this.get_events( ).removeHandler("onDrop", handler);
    },

But this does not give you the support for defining the event listener name in the ASP.NET declaration:

 <cdd:CustomDragDropExtender ID="CustomDragDropExtender1"
        runat="server"
        TargetControlID="LeftPanel"
        DragItemClass="widget" 
        DragItemHandleClass="widget_header"
        DropCueID="DropCue1"
        OnClientDrop="onDrop" />

Such declarative approaches allows only properties of a control. To support such a declarative assignment of events, you need to first introduce a property named OnClientDrop in the extender. Then, during assignment of the property, you need to find the specified function there and attach an event notification to that function. The discovery of the function from its name is done by CommonToolkitScripts.resolveFunction, which is available in the ACT project and used in Example 3-36, "Allow the event name to be specified as a property on the extender".

Example 3-36. Allow the event name to be specified as a property on the extender

// onDrop property maps to onDrop event
    get_onDrop : function( ) {
        return this.get_events( ).getHandler("onDrop");
    },

    set_onDrop : function(value) {
        if (value && (0 < value.length)) {
            var func = CommonToolkitScripts.resolveFunction(value);
            if (func) {
                this.add_onDrop(func);
            } else {
                throw Error.argumentType('value', typeof(value), 'Function', 'resize
                handler not a function, function name, or function text.');
            }
        }
    },

Raising the event is the same as basic Ajax events:

    _raiseEvent : function( eventName, eventArgs ) {
            var handler = this.get_events().getHandler(eventName);
        if( handler ) {
            if( !eventArgs ) eventArgs = Sys.EventArgs.Empty;
            handler(this, eventArgs);
        }
    },

The next challenge is to make CustomFloatingBehavior. The server-side class CustomFloatingBehavior.cs is declared, as shown in Example 3-37, "CustomFloatingBehavior.cs content".

Example 3-37. CustomFloatingBehavior.cs content

[assembly: System.Web.UI.WebResource("CustomDragDrop.CustomFloatingBehavior.js", "text/
javascript")]

namespace CustomDragDrop
{
    [Designer(typeof(CustomFloatingBehaviorDesigner))]
    [ClientScriptResource("CustomDragDrop.CustomFloatingBehavior", "CustomDragDrop.
    CustomFloatingBehavior.js")]
    [TargetControlType(typeof(WebControl))]
    [RequiredScript(typeof(DragDropScripts))]
    public class CustomFloatingBehaviorExtender : ExtenderControlBase
    {
        [ExtenderControlProperty]
        [IDReferenceProperty(typeof(WebControl))]
        public string DragHandleID
        {
            get
            {
                return GetPropertyValue<String>("DragHandleID", string.Empty);
            }
            set
            {
                SetPropertyValue<String>("DragHandleID", value);
            }
        }
    }
}

There's only one property-DragHandleID, in which the widget's header works as the drag handle. So, the header ID is specified here. This extender has dependency on DragDropManager, which requires the [RequiredScript(typeof(DragDropScripts))] attribute.

Besides the designer class, there's one more class that CustomDragDropExtender needs to specify its dependency over this FloatingBehavior:

    [ClientScriptResource(null, "CustomDragDrop.CustomFloatingBehavior.js")]
    public static class CustomFloatingBehaviorScript
    {
    }

This class can be used inside the RequiredScript attribute. It defines only which script file contains the client-side code for the extender.

The client-side JavaScript is same as FloatingBehavior, which comes with ACT. The only difference is a hackwhen the drag starts. DragDropManager does not return the item being dragged to the static position once it makes it absolute. It also does not increase the zIndex of the item. If the drag item does not become the top-most item on the page, then it goes below other elements on the page during drag. So, I have made some changes in the mouseDownHandler attribute of the behavior to add these features, shown in Example 3-38, "Revised mouseDownhandler in CustomFloatingBehavior.js".

Example 3-38. Revised mouseDownhandler in CustomFloatingBehavior.js

 function mouseDownHandler(ev) {
        window._event = ev;
        var el = this.get_element( );

        if (!this.checkCanDrag(ev.target)) return;

        // Get the location before making the element absolute
        _location = Sys.UI.DomElement.getLocation(el);

        // Make the element absolute
        el.style.width = el.offsetWidth + "px";
        el.style.height = el.offsetHeight + "px";
        Sys.UI.DomElement.setLocation(el, _location.x, _location.y);

        _dragStartLocation = Sys.UI.DomElement.getLocation(el);

        ev.preventDefault( );

        this.startDragDrop(el);

        // Hack for restoring position to static
        el.originalPosition = "static";
        el.originalZIndex = el.style.zIndex;
        el.style.zIndex = "60000";
    }

Setting el.originalPosition ="static" fixes the bug in DragDropManager. It incorrectly stores absolute as the originalPosition when startDragDrop is called. So, after calling this function, reset to the correct originalPosition, which is "static."

When drag starts, zIndex is set to a very high value so that the dragged item remains on top of everything on the page. When drag completes, the original zIndex is restored and the left, top, width, and height attributes are cleared. DragDropManager makes the item position static, but it does not clear the left, top, width, and height attributes. This moves the element away from the place where it is dropped. This bug is fixed in the onDragEnd event, as coded in Example 3-39, "onDragEnd event fixes the zIndex related problem".

  this.onDragEnd = function(canceled) {
        if (!canceled) {
            var handler = this.get_events().getHandler('move');
            if(handler) {
                var cancelArgs = new Sys.CancelEventArgs();
                handler(this, cancelArgs);
                canceled = cancelArgs.get_cancel();
            }
        }

        var el = this.get_element();
        el.style.width = el.style.height = el.style.left = el.style.top = "";
        el.style.zIndex = el.originalZIndex;
    }

That's all folks! Now you have two handy extenders that you can attach to HTML elements and provide complete drag-and-drop support.

Implementing WidgetContainer

WidgetContainer dynamically creates a widget inside its body area. The container consists only of header and body areas. The rest is provided by the actual widget loaded dynamically inside the container's body area. The settings area that you see when you click"edit" on the header also comes from the actual widget. WidgetContainer informs the widget only when to show it. The "Building Widgets" section later in this chapter shows how the widget handles this notification. WidgetContainer acts as a bridge between widgets and the core. The core communicates to the widgets via the container, and the widgets use the core's features via the container.

WidgetContainer's header contains the title text, the expand and collapse button, the "edit" link, and the close button within an UpdatePanel, as shown in Example 3-40, "WidgetContainer's header panel".

Example 3-40. WidgetContainer's header panel

<asp:Panel ID="Widget" CssClass="widget" runat="server">
    <asp:Panel id="WidgetHeader" CssClass="widget_header" runat="server">
        <asp:UpdatePanel ID="WidgetHeaderUpdatePanel" runat="server"
        UpdateMode="Conditional">
        <ContentTemplate>
          ...
          ...
          ...
        </ContentTemplate>
        </asp:UpdatePanel>
    </asp:Panel>

By doing this, we are preventing the body area from refreshing when something changes in the header. If the body area refreshes, the widget hosted inside it will unnecessarily refresh. To avoid downloading and refreshing a large amount of data in the whole widget, the header and body contain separate UpdatePanel controls.

There is an UpdateProgress extender attached to the header UpdatePanel, which shows a "Working…" indicator when the header is going through asynchronous postback. This happens when a user clicks on the title to change it or clicks some button on the header area.

 <asp:UpdateProgress ID="UpdateProgress2" runat="server"
            DisplayAfter="10"
            AssociatedUpdatePanelID="WidgetHeaderUpdatePanel" >
                 <ProgressTemplate>
                         <center>Working...</center>
                 </ProgressTemplate>
    </asp:UpdateProgress>

After this comes the UpdatePanel body, where the actual widget is loaded.

 <asp:UpdatePanel ID="WidgetBodyUpdatePanel" runat="server"
     UpdateMode="Conditional" >
         <ContentTemplate>
                 <asp:Panel ID="WidgetBodyPanel" runat="Server"></asp:Panel>
         </ContentTemplate>
    </asp:UpdatePanel>

The widget is not directly added inside the UpdatePanel. Instead it is added inside another regular Panel named WidgetBodyPanel (see Figure 3.9, "WidgetContainer layout showing distribution of UpdatePanels").

Figure 3.9. WidgetContainer layout showing distribution of UpdatePanels

WidgetContainer layout showing distribution of UpdatePanels

When a widget is collapsed, the body area goes away. This is done by setting Visible=False to the UpdatePanel. But that does not hide the DIV, which is generated for the UpdatePanel, nor does it clear the content of that DIV. But if you put a Panel inside the UpdatePanel and make it invisible, the UpdatePanel will become blank because there's nothing to show anymore. This is exactly what needs to happen when a widget is collapsed. By using an HTTP tracer tool like Fiddler or Charles, you can observe that the following data is transferred by the asynchronous postback when the widget is collapsed and the WidgetBodyPanel is hidden:

    933|updatePanel|WidgetContainer89605_WidgetHeaderUpdatePanel|
                <table class="widget_header_table" cellspacing="0" cellpadding="0">
                <tbody>
                <tr>
                <td class="widget_title"><a id="WidgetContainer89605_WidgetTitle" ...
                 <td class="widget_edit"><a id="WidgetContainer89605_EditWidget" ...
                 <td class="widget_button"><a id="WidgetContainer89605_ExpandWidget" ...
                 <td class="widget_button"><a id="WidgetContainer89605_CloseWidget" ...
                </tr>
                </tbody>
                </table>

            |20|updatePanel|WidgetContainer89605_WidgetBodyUpdatePanel|
            |0|hiddenField|_ _EVENTTARGET||0|hiddenField|_ _EVENTARGUMENT||...

The response contains the header UpdatePanel's ID (WidgetContainer89605_WidgetHeaderUpdatePanel) that was updated, followed by the new HTML snippet that needs to be placed inside the header update panel. The new HTML contains the expand button, and the collapse button is no longer present. Once the HTML is set to the header area, the collapse button disappears and the expand button appears.

After the header's UpdatePanel HTML, the body's UpdatePanel (WidgetContainer89605_WidgetBodyUpdatePanel) HTML is sent. Because there's no visible control inside the body's UpdatePanel, there's no HTML sent to the UpdatePanel. As a result, the representative DIV's innerHTML is set to blank. This clears the WidgetContainer's body area and the real widget disappears.

After the body area's UpdatePanel, there's only one UpdateProgress extender attached, which shows a "Working…" message when the body area is going through any asynchronous postback due to some activity on the widget itself.

   <asp:UpdateProgress ID="UpdateProgress1" runat="server" DisplayAfter="10"
    AssociatedUpdatePanelID="WidgetBodyUpdatePanel" >
        <ProgressTemplate><center>Working...</center></ProgressTemplate>
    </asp:UpdateProgress>

That's all inside the WidgetContainer.ascx. The code behind the file is, however, quite challenging.

WidgetContainer.cs

The WidgetContainer class implements the IWidgetHost interface because containers host widgets.

 public partial class WidgetContainer : System.Web.UI.UserControl, IWidgetHost
    {

The container maintains a reference to the hosted widget via the IWidget interface. It also stores a reference to the WidgetInstance object, which represents the instance of the widget it contains.

    private WidgetInstance _WidgetInstance;

    public WidgetInstance WidgetInstance
    {
        get { return _WidgetInstance; }
        set { _WidgetInstance = value; }
    }

    private IWidget _WidgetRef;

The Default.aspx notifies WidgetContainer about a first-or second-time load via a public property called IsFirstLoad.

    private bool _IsFirstLoad;

    public bool IsFirstLoad
    {
        get { return _IsFirstLoad; }
        set { _IsFirstLoad = value; }
    }

WidgetContainer then passes this property's value to the widget via the IWidgetHost.IsFirstLoad property.

During the OnInit event of WidgetContainer, it loads the widget using LoadControl and hosts it inside its body area:

   protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);
        var widget = LoadControl(this.WidgetInstance.Widget.Url);

        widget.ID = "Widget" + this.WidgetInstance.Id.ToString( );

        WidgetBodyPanel.Controls.Add(widget);
        this._WidgetRef = widget as IWidget;
        this._WidgetRef.Init(this);
    }

If you do not set the widget.ID to a specific ID that is always the same for the same widget instance, asynchronous postbacks will fail. You will get a message box saying an invalid asynchronous postbackhas been performed. When the asynchronous postback happens, ASP.NET needs to know which control has produced the post-back. To do this, it needs to load the page with all the controls exactly in the same state as before the postback occurred. Otherwise, it won't be able to find the control that produced the postback by the ID it received from the Ajax framework.

Also, if the WidgetContainer's ID isn't set to a specific value, ASP.NET will assign ctrl0 or ctrl1, whatever it finds free. This will vary the ID, and postbacks will map to nonexistent controls and fail. Setting WidgetContainer's ID to the widget instance ID ensures the container will always have the same ID for a particular widget instance.

When the WidgetContainer is expanded or collapsed, the following events get fired:

    protected void CollapseWidget_Click(object sender, EventArgs e)
    {
        (this as IWidgetHost).Minimize( );
    }

    protected void ExpandWidget_Click(object sender, EventArgs e)
    {
        (this as IWidgetHost).Maximize( );
    }

Here you call the interface implementations where the actual work is done:

  void IWidgetHost.Maximize( )
    {
        DatabaseHelper.Update<WidgetInstance>(this.WidgetInstance,
        delegate(WidgetInstance i)
        {
            i.Expanded = true;
        });

    this.SetExpandCollapseButtons( );
    this._WidgetRef.Maximized( );

    WidgetBodyUpdatePanel.Update( );
    WidgetHeaderUpdatePanel.Update( );
    }

    void IWidgetHost.Minimize( )
    {
        DatabaseHelper.Update<WidgetInstance>(this.WidgetInstance,
        delegate(WidgetInstance i)
        {
            i.Expanded = false;
        });

        this.SetExpandCollapseButtons( );
        this._WidgetRef.Minimized( );

        WidgetBodyUpdatePanel.Update( );
        WidgetHeaderUpdatePanel.Update( );
    }

Updating

We now need to update the Expanded property of the WidgetInstance object, as well as the database row to persist the widget's visibility. The details of how the DatabaseHelper works are discussed in Chapter 4, Building the Data and Business Layers Using .NET 3.5. For the time being, let's assume the following code updates one row of WidgetInstance:

    DatabaseHelper.Update<WidgetInstance>(this.WidgetInstance,
    delegate(WidgetInstance i)
    {
        i.Expanded = false;
    });

After updating the database row, update the header area by switching between the expand and collapse buttons, inform the widget about the Minimize or Maximize events, and update both the header and body UpdatePanel. The header update panel is updated to reflect the change in the expand/collapse button. The body update panel is updated to toggle the widget's visibility inside the body area.

Saving and editing

When a user clicks on the title of the WidgetContainer header, it switches to a text box and a save button. Users can enter a new title and save to set the new title for the widget instance. This is done by switching between LinkButton, TextBox, and Button. When the title LinkButton is clicked, the following event is fired:

    protected void WidgetTitle_Click(object sender, EventArgs e)
    {
        WidgetTitleTextBox.Text = this.WidgetInstance.Title;
        WidgetTitleTextBox.Visible = true;
        SaveWidgetTitle.Visible = true;
        WidgetTitle.Visible = false;
    }

When the user clicks save, it switches back to the LinkButton showing the new title and hides the text box and save button. After the UI changes, the WidgetInstance object is updated in database with the new title.

  protected void SaveWidgetTitle_Click(object sender, EventArgs e)
    {
        WidgetTitleTextBox.Visible = SaveWidgetTitle.Visible = false;
        WidgetTitle.Visible = true;
        WidgetTitle.Text = WidgetTitleTextBox.Text;

        DatabaseHelper.Update<WidgetInstance>(this.WidgetInstance,
        delegate(WidgetInstance wi)
        {
            wi.Title = WidgetTitleTextBox.Text;
        });
    }

When a user clicks the edit button, WidgetContainer informs the widget to show its settings area. The edit button then switches itself with "cancel edit" button. Both the edit and cancel edit buttons, when clicked, fire the same event:

  protected void EditWidget_Click(object sender, EventArgs e)
        {
            if( this.SettingsOpen )
            {
                this.SettingsOpen = false;
                this._WidgetRef.HideSettings( );
                EditWidget.Visible = true;
                CancelEditWidget.Visible = false;
            }
            else
            {
                this.SettingsOpen = true;
                this._WidgetRef.ShowSettings( );
                (this as IWidgetHost).Maximize( );
                EditWidget.Visible = false;
                CancelEditWidget.Visible = true;
            }
            WidgetBodyUpdatePanel.Update( );
        }

The SettingsOpen property is stored in ViewState to remember whether the settings area is already open or not.

 public bool SettingsOpen
        {
            get
            {
                object val = ViewState[this.ClientID + "_SettingsOpen"] ?? false;
                return (bool)val;
            }
            set { ViewState[this.ClientID + "_SettingsOpen"] = value; }
        }

Remember, ViewState will always return null until the user clicks the edit button and a true value is set to it. You should always check ViewState entries for null before casting them to a data type. Checkwhether it's null, and if so, then the default value is false.

Adding InstanceID

During the rendering of WidgetContainer, one more attribute is added to the DIV: InstanceID. The onDrop event on the client side needs to know the WidgetInstance ID of the widget that is moved so it can call the web service and notify the server about which WidgetInstance was moved. The additional attribute is rendered by overriding the RenderControl function:

   public override void RenderControl(HtmlTextWriter writer)
    {
        writer.AddAttribute("InstanceId", this.WidgetInstance.Id.ToString( ));
        base.RenderControl(writer);
    }

This results in the following HTML on the client:

               <div InstanceId="151" id="WidgetContainer151_Widget" class="widget">

The onDrop function on the client side reads this InstanceID and calls the web service:

  function onDrop( sender, e )
    {
        var container = e.get_container( );
        var item = e.get_droppedItem( );
        var position = e.get_position( );

        var instanceId = parseInt(item.getAttribute("InstanceId"));
        var columnNo = parseInt(container.getAttribute("columnNo"));
        var row = position;

        WidgetService.MoveWidgetInstance( instanceId, columnNo, row );
    }

Closing the widget

Closing is more complicated than other behavior because WidgetContainer cannot close itself-it needs to be closed from outside and removed from the page's controls collection. The container just raises the Deleted event when it is closed, and the Default.aspx handles the UI update and database changes. The following is the code when the close button is clicked:

  protected void CloseWidget_Click(object sender, EventArgs e)
    {
        this._WidgetRef.Closed( );
        (this as IWidgetHost).Close( );
    }

First, the contained widget gets notification and it can then perform cleanup operations, like removing widget-specific information from the database. Then the Deleted event is raised:

   void IWidgetHost.Close( )
    {
        Deleted(this.WidgetInstance);
    }

The Default.aspx removes the widget from the database and refreshes the column:

    void widget_Deleted(WidgetInstance obj)
    {
        new DashboardFacade(Profile.UserName).DeleteWidgetInstance(obj);

        this.ReloadPage(wi => false);

        this.RefreshColumn(obj.ColumnNo);
    }

Building Widgets

Now that you've seen how to implement WidgetContainer, let's lookat how you build the widgets it hosts. First, we'll create a simple widget to display Flickr photos, followed by another widget to display RSS and Atom feeds.

Building a Flickr Photo Widget

Let's look first at a simple Flickr photo widget that downloads photos from Flickr and displays them in a 3×3 grid, as shown in Figure 3.10, "The Flickr widget downloads the Flickr photo stream as XML and parses using LINQ to XML and the photo grid is dynamically rendered".

Figure 3.10. The Flickr widget downloads the Flickr photo stream as XML and parses using LINQ to XML and the photo grid is dynamically rendered

The Flickr widget downloads the Flickr photo stream as XML and parses using LINQ to XML and the photo grid is dynamically rendered

The widget downloads Flickr photos as an XML feed from the Flickr web site and then renders a 3×3 grid with the pictures. The Flickr photo stream is available as an XML feed (not a RSS feed) at http://www.flickr.com/services/rest/?method=flickr.photos.getRecent\&api\_key.

You need to first obtain an application key from the Flickr developer zone and pass it at the end of the URL. The key I have embedded inside the project may not work if the request quota has already been exceeded.

The URL returns recent Flickr photos uploaded by the user as XML:

   <?xml version="1.0" encoding="utf-8" ?>
    <rsp stat="ok">
    <photos page="1" pages="10" perpage="100" total="1000">
      <photo id="431247461" owner="25524911@N00" secret="cb9370fd16" server="162"
      farm="1" title="P1020899" ispublic="1" isfriend="0" isfamily="0" />
      <photo id="431247462" owner="46871506@N00" secret="036edda0e9" server="188"
      farm="1" title="black" ispublic="1" isfriend="0" isfamily="0" />
      <photo id="431247458" owner="91583992@N00" secret="6cd9a27d6d" server="153"
      farm="1" title="DSC00647" ispublic="1" isfriend="0" isfamily="0" />

However, the XML does not contain the URL of the photo file. It needs to be built dynamically.

The first step is to download and parse the XML using LINQ to XML, which is avail-able in .NET 3.5. Here's an easy way to prepare a XElement from an URL:

    var xroot = XElement.Load(url);

Next we convert each photo node inside the XML to an object of the PhotoInfo class for convenient processing:

 var photos = (from photo in xroot.Element("photos").Elements("photo")
    select new PhotoInfo
    {
             Id = (string)photo.Attribute("id"),
             Owner = (string)photo.Attribute("owner"),
             Title = (string)photo.Attribute("title"),
             Secret = (string)photo.Attribute("secret"),
             Server = (string)photo.Attribute("server"),
             Farm = (string)photo.Attribute("Farm")
    })

This will produce one PhotoInfo object for each <photo> node in the XML. Paging support has been added to it to select nine photos at a time by using the Skip and Take functions:

 var photos = (from photo in xroot.Element("photos").Elements("photo")
    select new PhotoInfo
    {
    ...
    }).Skip(pageIndex*Columns*Rows).Take(Columns*Rows);

This takes only nine photos from the current pageIndex. Page index is changed when the user clicks the next or previous links. The Skip method skips the number of items in the XML, and the Take method takes only the specified number of nodes from XML.

A 3×3 HTML table renders the photos from the collection of PhotoInfo objects:

  foreach( var photo in photos )
    {
            if( col == 0 )
                    table.Rows.Add( new HtmlTableRow( ) );

            var cell = new HtmlTableCell( );

            var img = new HtmlImage( );
            img.Src = photo.PhotoUrl(true);
            img.Width = img.Height = 75;
            img.Border = 0;

            var link = new HtmlGenericControl("a");
            link.Attributes["href"] = photo.PhotoPageUrl;
            link.Attributes["Target"] = "_blank";
            link.Attributes["Title"] = photo.Title;
            link.Controls.Add(img);

            cell.Controls.Add(link);
            table.Rows[row].Cells.Add(cell);
            col ++;
            if( col == Columns )
            {
                    col = 0; row ++;
            }

            count ++;
    }

The reasoning behind using HtmlGenericControl instead of HtmlLink is that HtmlLink does not allow you to add controls inside its Controls collection. This is a limitation of the HtmlLink class in ASP.NET 2.0.

The PhotoPageUrl property of PhotoInfo class gives the URL of the photo. There's no special logic inside the PhotoInfo class besides the public properties:

    public class PhotoInfo
    {
        private const string FLICKR_SERVER_URL="http://static.flickr.com/";
        private const string FLICKR_PHOTO_URL="http://www.flickr.com/photos/";

        public string Id;
        public string Owner;
        public string Title;
        public string Secret;
        public string Server;
        public string Farm;
        public bool IsPublic;
        public bool IsFriend;
        public bool IsFamily;
        public string PhotoUrl(bool small)
        {
            return FLICKR_SERVER_URL + this.Server + '/' + this.Id + '_' + this.Secret +
        (small ? "_s.jpg" : "_m.jpg");
        }
        public string PhotoPageUrl
        {
        get { return FLICKR_PHOTO_URL + this.Owner + '/' + this.Id; }
        }
    }

When the widget loads, it checks whether it's a first-time load or a postback. If it's a first-time load, then it fetches the XML photo feed from Flickr and stores it in the cache. When it's a postback, it renders the photos from the cached XML.

    protected void Page_Load(object sender, EventArgs e)
        {
            if( this._Host.IsFirstLoad )
            {
                this.LoadState( );
                this.LoadPictures( );
                this.PageIndex = 0;
                this.ShowPictures(0);
            }
            else
            {
                this.ShowPictures(this.PageIndex);
            }
        }

First the widget loads its UI state, downloads the Flickr photo XML, and shows the pictures in the grid. But when the page is having a postback, the widget just shows the last pictures. The reason you need to render the pictures again on postbackis that the pictures are shown dynamically and are not part of the HTML markups that ASP.NET creates. If they are declaratively specified in the .ascx file, then ASP.NET would have created that itself.

The content of the FlickrWidget.ascx page is the following:

 <%@ Control Language="C#" AutoEventWireup="true" CodeFile="FlickrWidget.ascx.cs"
    Inherits="FlickrWidget" %>
    <asp:Panel ID="settingsPanel" runat="server" Visible="False">
        <asp:RadioButton ID="mostInterestingRadioButton" runat="server"
        AutoPostBack="True"
            Checked="True" GroupName="FlickrPhoto" OnCheckedChanged="photoTypeRadio_
            CheckedChanged"
            Text="Most Interesting" />
            <br />
        <asp:RadioButton ID="mostRecentRadioButton" runat="server" AutoPostBack="True"
        GroupName="FlickrPhoto"
            OnCheckedChanged="photoTypeRadio_CheckedChanged" Text="Most Recent" />
            <br />
        <asp:RadioButton ID="customTagRadioButton" runat="server" AutoPostBack="True"
        GroupName="FlickrPhoto"
            OnCheckedChanged="photoTypeRadio_CheckedChanged" Text="Tag: " />
            <asp:TextBox ID="CustomTagTextBox" runat="server" Text="Pretty" />
            <hr />
    </asp:Panel>
    <asp:Panel ID="photoPanel" runat="server">

    </asp:Panel>
    <center>
        <asp:LinkButton ID="ShowPrevious" runat="server" OnClick="ShowPrevious_Click">
        <Prev</asp:LinkButton>
        <asp:LinkButton ID="ShowNext" runat="server" OnClick="ShowNext_Click">Next >
        </asp:LinkButton></center>

In the Visual Studio designer, the Flickr widget web control looks pretty simple, as shown in Figure 3.11, "At design view, the widget is nothing but a regular web control; use Visual Studio's Visual Designer to design the UI".

There's a blank panel in the middle of the control that shows the pictures. The Prev and Next LinkButtons do the pagination. The radio buttons are part of the settings area that you see only when you click on the edit link on the widget header area, as shown in Figure 3.12, "Settings contain options to customize a widget, such as different types of photo streams from Flickr".

Figure 3.11. At design view, the widget is nothing but a regular web control; use Visual Studio's Visual Designer to design the UI

At design view, the widget is nothing but a regular web control; use Visual Studio's Visual Designer to design the UI

Figure 3.12. Settings contain options to customize a widget, such as different types of photo streams from Flickr

Settings contain options to customize a widget, such as different types of photo streams from Flickr

Flickr widget UI controls

Let's explain what the controls do on the Flickr widget UI. The simplest part is the pagination where the click handlers of the Previous and Next LinkButtons do the paging:

    protected void ShowPrevious_Click(object sender, EventArgs e)
        {
            this.PageIndex --;
            this.ShowPictures(this.PageIndex);
        }
        protected void ShowNext_Click(object sender, EventArgs e)
        {
            this.PageIndex ++;
            this.ShowPictures(this.PageIndex);
        }

When a user changes the type of photo to show, this information is stored in the widget's state. The widget's state is simple XML in this format:

 <state>
        <type>MostRecent</type>
        <tag>Pretty</tag>
    </state>

The state is loaded using LINQ to XML's XElement class:

 private XElement _State;
    private XElement State
    {
         get
         {
              if( _State == null )
              {
                  string stateXml = this._Host.GetState( );
                  if (string.IsNullOrEmpty(stateXml))
                  {
                       _State = new XElement("state",
                            new XElement("type", "MostPopular"),
                            new XElement("tag", ""));
                  }
                  else
                  {
                       _State = XElement.Parse(stateXml);
                  }
              }
              return _State;
         }
    }

This is a read-only property that loads the widget instance's state XML and returns an XElement reference to it. The host that is the widget container for this widget returns the state content. If no state was stored before, it creates a default state XML with default selections.

Reading and writing to individual properties in the state is very easy when we have a XElement object to work with:

  public PhotoTypeEnum TypeOfPhoto
    {
        get { return (PhotoTypeEnum)Enum.Parse( typeof( PhotoTypeEnum ), State.
            Element("type").Value ); }
        set { State.Element("type").Value = value.ToString( ); }
    }
    public string PhotoTag
    {
        get { return State.Element("tag").Value; }
        set { State.Element("tag").Value = value; }
    }

After changing the state, it is saved permanently in the database by calling the IWidgetHost.SaveState function. Here's the code that collects current settings from the UI and stores in widget instance's state:

    private void SaveState( )
    {
        if( mostRecentRadioButton.Checked )
            this.TypeOfPhoto = PhotoTypeEnum.MostRecent;
        else if( mostInterestingRadioButton.Checked )
            this.TypeOfPhoto = PhotoTypeEnum.MostPopular;
        else if( customTagRadioButton.Checked )
        {
            this.TypeOfPhoto = PhotoTypeEnum.Tag;
            this.PhotoTag = this.CustomTagTextBox.Text;
        }
        this._Host.SaveState(this.State.Xml);
    }

Whatever is stored here is stored permanently in the database. If the user closes the browser and visits again, these states will be retrieved from the database. However, temporary state variables, like PageIndex, are not stored as state. They are stored in the ViewState:

   private int PageIndex
        {
            get
            {
                return (int)(ViewState[this.ClientID + "_PageIndex"] ?? 0);
            }
            set { ViewState[this.ClientID + "_PageIndex"] = value; }
        }

So far we made a regular ASP.NET web control. This control becomes a widget when the IWidget interface is implemented on it. The implementation is straightforward:

    void IWidget.Init(IWidgetHost host)
    {
        this._Host = host;
    }
    void IWidget.ShowSettings( )
    {
        settingsPanel.Visible = true;
    }
    void IWidget.HideSettings( )
    {
        settingsPanel.Visible = false;
    }
    void IWidget.Minimized( )
    {
    }
    void IWidget.Maximized( )
    {
    }
    void IWidget.Closed( )
    {
    }

Most of implementations are blank because there's nothing special to do here other than show and hide the settings area.

The Flickr photo widget is now ready. Put it in the widgets folder, create a row in widget table in the database, and it's good to go. The user will see the widget listed in the Add Stuff area, as shown in Figure 3.13, "Widget table entry for the Flickr photo widget", and can add it to the page.

Figure 3.13. Widget table entry for the Flickr photo widget

Widget table entry for the Flickr photo widget

The widget is marked as IsDefault=True, so it is added automatically to the first page for first-time visitors.

Building an Atom or RSS Widget

In this section, you will see how to build a simple RSS widget, like the one shown in Figure 3.14, "The RSS widget shows feed items as links".

Figure 3.14. The RSS widget shows feed items as links

The RSS widget shows feed items as links

RSS is arguably the most popular widget for a start page because it serves the core functionality of web portals, to aggregate content from different sources. Users use RSS to read news and blogs, and to subscribe to groups, bulletins, notifications, and so on. Nowadays RSS is an almost universal format for content syndication. It is nothing but a fixed and formatted XML feed that is universally agreed upon. Atom, which is another format, is also quite popular. So, the widget you will make will be able to parse both RSS and Atom format feeds.

The RSS widget stores the URL and the number of articles to show on the widget inside its State. Just like a Flickr photo widget, you can customize the number of items you want to see from the settings area. You can also change the feed URL and read a different feed. State handling is handled the same way as a Flickr photo widget, by storing the count and feed URL inside State.

The UI contains the settings panel and a DataList that is bound to a collection of feed items at runtime, as shown in Example 3-41, "RSS Widget .ascx content".

Example 3-41. RSS Widget .ascx content

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="RSSWidget.ascx.cs"
Inherits="Widgets_RSSWidget" EnableViewState="false" %>
<asp:Panel ID="SettingsPanel" runat="Server" Visible="False" >
URL: <asp:TextBox ID="FeedUrl" Text="" runat="server" MaxLength="2000" Columns="40" /><br
/>
Show
<asp:DropDownList ID="FeedCountDropDownList" runat="Server">
<asp:ListItem>1</asp:ListItem>
<asp:ListItem>2</asp:ListItem>
<asp:ListItem>3</asp:ListItem>
<asp:ListItem>4</asp:ListItem>
<asp:ListItem>5</asp:ListItem>
<asp:ListItem>6</asp:ListItem>
<asp:ListItem>7</asp:ListItem>
<asp:ListItem>8</asp:ListItem>
<asp:ListItem>9</asp:ListItem>
</asp:DropDownList>
items
<asp:Button ID="SaveSettings" runat="Server" OnClick="SaveSettings_Click" Text="Save" />
</asp:Panel>

<asp:DataList ID="FeedList" runat="Server" EnableViewState="False">
<ItemTemplate>
<asp:HyperLink ID="FeedLink" runat="server" Target="_blank" CssClass="feed_item_link"
NavigateUrl='<%# Eval("link") %>' ToolTip='<%# Eval("description") %>'>
<%# Eval("title") %>
</asp:HyperLink>
</ItemTemplate>
</asp:DataList>

The DataList shows a list of hyperlinks where each hyperlink is bound to a title, description, and a linkproperty at runtime. From the code, a projection with properties named title, description, and linkis bound to the DataList. The function ShowFeeds loads the feeds using LINQ to XML and then converts the feed XML to a projection using a LINQ expression, as shown in Example 3-42, "Loading feeds and converting to a projection".

Example 3-42. Loading feeds and converting to a projection

private void ShowFeeds( )
{
     string url = State.Element("url").Value;
     int count = State.Element("count") == null ? 3 : int.Parse( State.Element("count").
     Value );

     var feed = Cache[url] as XElement;
     if( feed == null )
     {
          if( Cache[url] == string.Empty ) return;
          try
          {
            HttpWebRequest request =WebRequest.Create(url) as HttpWebRequest;
               request.Timeout = 15000;
               using( WebResponse response = request.GetResponse( ) )
               {
                    XmlTextReader reader = new XmlTextReader(
                   response.GetResponseStream( ) );

                    feed = XElement.Load(reader);

                    if( feed == null ) return;

                    Cache.Insert(url, feed, null, DateTime.MaxValue, TimeSpan.
                   FromMinutes(15));
               }
          }
          catch
          {
               Cache[url] = string.Empty;
               return;
          }
     }

     XNamespace ns = "http://www.w3.org/2005/Atom";

     // RSS Format
     if( feed.Element("channel" ) != null )
          FeedList.DataSource = (from item in feed.Element("channel").Elements("item")
                                select new
                                {
                                     title = item.Element("title").Value,
                                     link = item.Element("link").Value,
                                     description = item.Element("description").Value
                                }).Take(this.Count);
     // Atom format
     else if( feed.Element(ns + "entry") != null )
         FeedList.DataSource = (from item in feed.Elements(ns + "entry")
                               select new
                               {
                                    title = item.Element(ns + "title").Value,
                                    link = item.Element(ns + "link").Attribute("href").
                                   Value,
                                    description = item.Element(ns + "content").Value
                                    }).Take(this.Count);


     FeedList.DataBind( );
}

The ShowFeeds function first loads the XML feed from the feed URL using HttpWebRequest. Then it passes the response stream to an XmlReader, which, in turn, gets passed into a XElement.Load function. XElement.Load loads the XML, parses it, and builds an XElement object model of nodes and attributes. Based on the XML feed format (RSS or Atom), different type of nodes and attributes appear in XElement. So, a LINQ expression converts both types of object models to a projection with title, description, and link properties. Once the projection is prepared, it is bound to the FeedList DataList control. The DataList binds one hyperlinkfor each item in the project and thus shows the feed items.

Now that you have learned how to build widgets, you will learn some framework-level challenges that need to be solved to make widgets work properly on the Start page. Some of these challenges are handling postbackin different scenarios, implementing security, and handling Profile objects from web services.

Page Switching: Simulating a Nonpostback Experience

Widgets load on page in three ways:

  • The very first time they are created; they have no state at this stage

  • When a user revisits the page; they load in nonpostbackmode and restore their state from persisted state data

  • During asynchronous postback; they load in postback mode and restore their state from both ViewState and persisted states

Normally, on a regular visit to the page (i.e., nonpostback, second scenario), widgets load their settings from their persisted state and render the UI for the first time. Upon postback, widgets don't always restore settings from persisted state and instead update the state or reflect small changes on the UI. For example, when you click the Next button on the Flickr photo widget, it's a postback experience for the widget. It does not go and fetch Flickr photos again, it just updates the current page index in its ViewState. So, it's important for widgets to know when they are being rendered for the first time or when it is a postback.

The definition of nonpostbackand postbackis different when you have multiple tabs on one page. When you click on another tab, it's a regular asynchronous postback for ASP.NET because a LinkButton gets clicked inside an UpdatePanel. This makes the tab's UpdatePanel postback asynchronously, and on the server side you can see which tab is clicked. You can then load the widgets on the newly selected tab. But when widgets load, they call Page.IsPostBack, and they get true. So, widgets assume they are already on the screen and try to do a partial rendering or access their own ViewState. But this is not the case because they are not rendered yet and there's no ViewState. As a result, the widgets behave abnormally and any ViewState access fails.

So, we need to make sure that during the tab switch, even though it's a regular ASP. NET postback, the widgets don't see it as postback. The idea is to inform widgets whether it is a regular visit or a postback via the IWidgetHost interface.

On Default.aspx, the SetupWidgets function creates the WidgetContainer and loads the widgets. Here's how it works:

 private void SetupWidgets(Func<WidgetInstance, bool> isWidgetFirstLoad)
    {
    ...
    foreach( WidgetInstance instance in setup.WidgetInstances )
    {
    var panel = columnPanels[instance.ColumnNo];

    var widget = LoadControl(WIDGET_CONTAINER) as WidgetContainer;
    widget.ID = "WidgetContainer" + instance.Id.ToString( );
    widget.IsFirstLoad = isWidgetFirstLoad(instance);
    widget.WidgetInstance = instance;

    widget.Deleted += new Action<WidgetInstance>(widget_Deleted);

    panel.Controls.Add(widget);
    }

The public property IsFirstLoad is determined by what calls the SetupWidget and when. SetupWidget's job is to render the widgets on the page. So, it gets called during the first visit and subsequent postbacks. The caller knows whether it's postback or not and can pass a predicate, which decides the value of the IsFirstLoad property. WidgetContainer just passes the value of the property to its contained widget via the IWidgetHost interface.

So, why not just send true when it's postbackand false when it's not and declare the function as SetupWidgets(bool)?

When a new widget is added on the page, it is a first-time loading experience for the newly added widget, but it's a regular postbackfor existing widgets already on the page. If true or false is passed for all widgets, then the newly added widget will see it as a postback just like all other existing widgets on the page and thus fail to load properly. To make sure it's a nonpostback experience for only the newly added widget, and a postback experience for existing widgets already on the page, use this predicate feature:

 DashboardFacade facade = new DashboardFacade(Profile.UserName);
    WidgetInstance newWidget = facade.AddWidget( widgetId );

    this.ReloadPage(wi => wi.Id == newWidget.Id);

Here the predicate will return true for the newly added widget, but false for any other widget. As a result, the newly added widget get IsFirstLoad =true, where existing widgets get IsFirstLoad = false.

Using the Profile Object Inside a Web Service

The web project uses a Profile object like IsFirstVisit to store a user's state. Profile object is also used to get the currently visiting user's name from the Profile.UserName property. The Default.aspx is already full of Profile.Something, and so are the widget container and the widgets. The next step is to add a new web service and access the Profile object, but when you type context.Profile from the web service code, IntelliSense doesn't show any of your custom properties.

At runtime, ASP.NET generates a class looking at the specification provided in web. config, which becomes the Profile object in .aspx pages and .ascx controls. But this object is not available in the web service (.asmx.cs) and you cannot see the custom properties you have added in the Profile object. Although HttpContext.Current.Profile will reference the Profile object, its type is ProfileBase, which does not show your custom properties because the class is generated at runtime.

To overcome this problem, you have to handcode the profile class in your App_Code folder and then configure web.config so it doesn't autogenerate a class but instead uses yours. Here's what you do in web.config:

   <profile enabled="true" inherits="UserProfile">

Now go to App_Code and make a UserProfile class like this:

 public class UserProfile : System.Web.Profile.ProfileBase
    {
    [SettingsAllowAnonymousAttribute(true)]
    public virtual int Timezone
    {
    get
    {
    return ((int)(this.GetPropertyValue("Timezone")));
    }
    set
    {
    this.SetPropertyValue("Timezone", value);
    }
    }

This is an example property implementation. Don't forget to add the [SettingsAllowAnonymousAttribute(true)] to the properties that you want to make available to anonymous users. At the end of the class, add this method:

  public virtual ProfileCommon GetProfile(string username)
    {
    return ((ProfileCommon)(ProfileBase.Create(username)));
    }

Here's an easy way to avoid handcoding this class and generate it automatically instead. Before you make the changes in web.config and create the UserProfile class, run your web project as it was before. But before running it, turn off SQL Server. This will make the ASP.NET execution break on the first call to a Profile object's property. For example, if you have defined a custom property TimeZone in the Profile object in web.config, execution will break on this line:

    public virtual int Timezone
    {
    get
    {
    return ((int)(this.GetPropertyValue("Timezone")));

It will fail to load the Profile object values from the database because the database is down. If you scroll up, you will see that this is the class that ASP.NET generates at runtime. All the properties are already declared on this class. So, you can just copy and paste it in your own class easily! However, after copying, you will realize there's no [SettingsAllowAnonymousAttribute(true)] attribute in the generated class. So, you will have to add it manually. Once you have made the class, you will have to remove all the custom properties declared inside <properties> node in the web.config.

Now that you have your own Profile class, you can cast (HttpContext.Current.Profile as UserProfile) and use all the custom properties inside the web service.

If you don't want to have strict coding on the web service, then you can use the old way of accessing the Profile properties via Profile.GetPropertyValue("TimeZone"). This will workfor both web pages and web services. You don't need to handcode a Profile class if you go for this approach, then again you don't get the strong typing and the IntelliSense feature.

Implementing Authentication and Authorization

The ASP.NET membership provider and profile provider take care of the authentication and authorization for the application. In addition to this, an anonymous identification provider is used, which is not common among web applications because, unlike web portals, other web apps don't require anonymous user creation on the first visit. The anonymous identification provider creates anonymous users whenever there is a cookieless visit, and takes care of creating entries in the aspnet_users table for the new anonymous user.

The web.config defines the providers in Example 3-43, "Defining the providers with web.config".

Example 3-43. Defining the providers with web.config

<authentication mode="Forms">
      <forms
          name=".DBAUTH" loginUrl="Login.aspx"
          protection="All" timeout="20160"
          path="/" requireSSL="false"
          slidingExpiration="true" defaultUrl="Default.aspx"
          cookieless="UseDeviceProfile" enableCrossAppRedirects="false"/>
    </authentication>
    <membership defaultProvider="DashboardMembershipSqlProvider"
    userIsOnlineTimeWindow="15">
      <providers>
        <add name="DashboardMembershipSqlProvider"
               type="System.Web.Security.SqlMembershipProvider,
                    System.Web, Version=2.0.0.0,
                    Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
               connectionStringName="DashboardConnectionString"
               enablePasswordRetrieval="true"
               enablePasswordReset="true"
               requiresQuestionAndAnswer="false"
               applicationName="Dashboard"
               requiresUniqueEmail="false"
               passwordFormat="Clear"
               minRequiredPasswordLength="1"
               passwordStrengthRegularExpression=""
               minRequiredNonalphanumericCharacters="0"/>
      </providers>
    </membership>
    <roleManager enabled="true" cacheRolesInCookie="true"
          defaultProvider="DashboardRoleManagerSqlProvider"
          cookieName=".ASPXROLES" cookiePath="/"
          cookieTimeout="30" cookieRequireSSL="false"
          cookieSlidingExpiration="true" createPersistentCookie="false"
          cookieProtection="All">
    <providers>
      <add name="DashboardRoleManagerSqlProvider"
             type="System.Web.Security.SqlRoleProvider, System.Web,
                  Version=2.0.0.0, Culture=neutral,
                  PublicKeyToken=b03f5f7f11d50a3a"
             connectionStringName="DashboardConnectionString"
             applicationName="Dashboard"/>
    </providers>
  </roleManager>
  <profile enabled="true" automaticSaveEnabled="false"
        defaultProvider="DashboardProfileSqlProvider"
        inherits="UserProfile">
      <providers>
        <clear/>
        <add name="DashboardProfileSqlProvider"
               type="System.Web.Profile.SqlProfileProvider"
               connectionStringName="DashboardConnectionString"
               applicationName="Dashboard"
               description="SqlProfileProvider for Dashboard"/>
      </providers>
    </profile>

However, there are several performance issues involved with anonymous identification providers, which are addressed later.

The anonymous identification provider is defined as:

 <anonymousIdentification
         enabled="true"
         cookieName=".DBANON"
         cookieTimeout="43200"
         cookiePath="/"
         cookieRequireSSL="false"
         cookieSlidingExpiration="true"
         cookieProtection="All"
         cookieless="UseCookies"/>

An anonymous identification provider generates a cookie in the browser that identifies the anonymous user for 43,200 minutes (a little over 29 days) after the last hit. (The timeout is on a sliding scale, so each visit restarts the 29-day clock.) This means if a user closes the browser and comes backwithin a month, the user will be identified and will get the same page setup as it before. As a result, users can keep using the web portal without actually ever signing up. But if the browser cookie is cleared, the page setup will be lost forever. The only solution is to register using a login name and password, so even if the cookie is lost, the user can log in to see his pages.

Web services, which let you modify data, are vulnerable to malicious attacks. For example, there is a web service method that moves a widget from one position to another. Imagine someone trying to call this web service with an arbitrary widget instance ID. The attacker will be able to mess up page setups by trying instance IDs from 1 to 1,000. To prevent such attempts, each web service operation needs to ensure operations are performed only on the objects that the caller owns. Remember from the previous example that you cannot the move position of a widget unless you are the owner. Such security checks are implemented in the business layer because if they were implemented in web layer, the logic for checking ownership, which is a business rule, would get into the web layer. Some might argue that such checks can easily be put on the web layer to kick out malicious calls before they reach the business layer. But this pollutes the web layer with business rules. Besides maintaining such architectural purity, business layer methods are called from many sources, such as a Windows service or a different web frontend. So, it becomes a maintenance issue to preserve conformance to such validations in all places. This topic is covered in more detail in Chapter 7, Creating Asynchronous, Transactional, Cache-Friendly Web Services.

Implementing Logout

Wait, wait! Don't skip this section. A simple logout can be very cool.

First question: why do people implement a logout page as an .aspx file when it just calls FormsAuthentication.Signout and redirects to a homepage? You really don't need to make ASP.NET load an .aspx page, produce HTML, and process through the page life cycle only to do a cookie cleanup and redirect. A simple HTTP 302 can tell the browser to go backto the homepage. So, the logout page is a great candidate for HTTP handlers without any UI.

Example 3-44, "Implementing a logout handler in the web application" shows how to implement a logout handler inside a file named Logout.ashx:

Example 3-44. Implementing a logout handler in the web application

<%@ WebHandler Language="C#" Class="Logout" %>

using System;
using System.Web;
using System.Web.Security;
using System.Collections.Generic;

public class Logout : IHttpHandler {

    public void ProcessRequest (HttpContext context) {

        /// Expire all the cookies so browser visits us as a brand new user
        List<string> cookiesToClear = new List<string>( );
        foreach (string cookieName in context.Request.Cookies)
        {
            HttpCookie cookie = context.Request.Cookies[cookieName];
            cookiesToClear.Add(cookie.Name);
        }

        foreach (string name in cookiesToClear)
        {
            HttpCookie cookie = new HttpCookie(name, string.Empty);
            cookie.Expires = DateTime.Today.AddYears(-1);

            context.Response.Cookies.Set(cookie);
        }
        context.Response.Redirect("~/Default.aspx");
    }
    public bool IsReusable {
        get {
            return true;
        }
    }
}

Handlers

Handlers are a lot lighter than the .aspx page because they have a very simple life cycle, are instance reusable, and generate a small amount of code when compiled at runtime.

The idea here is to remove all cookies related to the site instead of just removing the forms authentication cookie. When you use an anonymous identification provider, you will find two cookies: .DBAUTH and .DBANON. The form's authentication provider generates the first one and the other one is from the anonymous identification provider. These cookies are because an anonymous user is different than the user that is logged in. If you call FormAuthentication.Signout(), it will just clear the .DBAUTH cookie, but the other one will remain as is. So, after logout, instead of getting a brand new setup, you will get the old setup that you saw when you were an anonymous user during your first visit. The anonymous user is converted to a registered user by directly modifying the aspnet_users table. So, the anonymous user no longer exists in the database. This means the cookie for the anonymous user points to something that no longer exists. So, when the ASP.NET membership provider tries to find the user from the anonymous cookie, it fails.

In a web portal, we want the user to start over with a fresh setup. So, we need to clear both cookies and any other cookie that the widget scripts have used for storing temporary states. You never know what widgets will do with the cookie. Some widgets can secretly keep track of your logged-in session by storing info in a different cookie. When you log out and become an anonymous user, the widget can still access that secret cookie and find out about you. For example, it can easily store your email address when it is loaded in a logged-in session, and after you log out, it can still read that email address from the cookie. It's a security risk to have any cookie left from your logged-in session after logging out.

Summary

The web layer is the most difficult part of a web portal application because it is so UI intensive. Although ASP.NET AJAX offers a lot of off-the-shelf features, there are some tweaks that need to be made when using it in a real application. The business and the data access layers that I explain in next chapter are quite simple compared to the web layer. However, they use two hot technologies-Workflow Foundation and LINQ to SQL. Brace yourself for some really cool implementation of these cuttingedge technologies.

Additional Resources