This article is the third of a four part series:

Check out the AdventureWorks ASP.NET Sample ASP.NET app online.

Now that we designed the blueprints and built an object model, its time to start to develop a website. I am going to create an ASP.NET 3.5 website project. I want to take advantage of the model I built with the Entity Framework and use LINQ to easily query the model. These two technologies will help me rapidly develop against this otherwise daunting database.

To build a website I usually start with the most critical piece first and work down the chain from there. So in my opinion the most important part of a retail company's website is the product catalog. Some people might argue that it is the shopping cart, but the way I see it, no one is going to buy until they know what the product is. So we will assume there is a direct sales team to take orders until our shopping cart is launched.

General practices

Before we dig into code, here are a few of general practices I use when doing ASP.NET web development.

Follow MVC Patterns (even in WebForms)

I have been following this pattern subconsciously for years of WebForms development. It just makes sense. So how do I follow the pattern in WebForms? In a nutshell: loosely. I like to seperate all of my UI "chunks" or "modules" into UserControls and create Classes for them to inherit. I usually make the Classes more general like "ProductModule" and many UserControls inherit from it to take advantage of common data models and params. I then organize my UserControls into folders that group them logically. For this project I create "Products" and "Order" to section the modules into product catalog UI and shopping cart UI. In the root UserControls folder I add common UI elements like Header and Footer.

Take Advantage of ASP.NET UI Features

I like to utilize App_Themes, Skins and MasterPages to make UI development faster and more efficient. App_Themes are a nice way to stay organized and adding Skins to them helps to apply theme and behavior settings across the entire app. MasterPages are probably my favorite feature added in ASP.NET 2.0 and have saved me countless hours of work. I am also a fan of using nested MasterPages to really build rich UI templates across a large web app, but we will just use on in this project.

Import a CSS Layout Framework

Once of the biggest frustrations in web design is layout. I consider myself a CSS guru and still get stumped on some complex layout issues. If you want to spend your time actually styling things with CSS and not trying to hack your layout to work in all browsers then this practice is for you. In this app (and all my work) I chose to use YUI stylesheets including reset, base and grids. Reset will basically reset style on every html element to create a clean pallete across all browsers. Base (which requires reset) will give consistent styling to all html elements. Grids (which requires reset and base) creates a layout framework for you to build pretty much any type of column layout you need. It gets a little crazy when writing the markup, so they also provide a grid builder to make things easier. I can't recommend this practice enough. It has saved me so much time and prevented so many headaches.

Use 3rd Party Controls

As much as I would love to think I can build every aspect of my projects alone, it just isn't reality. I know it sounds like hot air coming from me, but using 3rd party controls is a great way to shift focus from the tedious code to the fun stuff. I have already wasted too many precious keystrokes writing sort algorithms and browser hacks. I had to draw the line and let the pro's do it for me. One of the most productive things you can do as a developer is know when to delegate work. For me, that delegation includes using custom controls that usually have more functionality than I would ever care to write for myself. Seriously, who really wants to sit down and write a PDF writer? No offense if that kind of programming is your cup of tea, but it sure isn't mine. I want to build the app, get it out the door and watch people use it. Then I can spend my time refining the UX and not trying to fix bugs in the chart control I was to stubborn to purchase. That's right, you need to think of how much effort you are going to spend supporting the custom controls you build on your own, not just how long it would take to develop. The next time you go to develop some controls I would ponder how valuable your time is and whether it's worth your keystrokes. I'm sure you get the picture, so I will stop with the C1 fanboy stuff.

Let's Dev!

Build the Navigation

The AdventureWorks database products are organized by Category and SubCategory. So the first thing to do is make a few methods to build these lists. Let's start with one we need to make the main menu. For the menu we will need a list of the the base categories in the database. So here is what that method would look like using LINQ against my Entity Model.

public static List GetMainCategories()  
{  
   var cats = from cat in Common.DataEntities.ProductCategory  
               select cat;  
   return cats.ToList();  
}

I want a menu that looks similar to Digg's so I am going to use C1TabStrip and used nested Tabs to pull it off. Here is the simple markup of the TabStrip.

<cc1:C1TabStrip ID="MnuMain" runat="server" CssClass="menu" VisualStyle="" UseEmbeddedVisualStyles="False">  
    <ScrollSettings ScrollMode="Buttons" ScrollButtonAlign="TwoSides" />  
</cc1:C1TabStrip>

Now for the simple for loop to build the Tabs and child Tabs for our navigation control.


        List mainCatetories = CategoryManager.GetMainCategories();  
        foreach (ProductCategory c in mainCatetories)  
        {  
            var itm = new C1.Web.UI.Controls.C1TabStrip.C1Tab(c.Name, c.Name,Page.ResolveUrl("~/Products.aspx?Category="   c.Name), "");  
            MnuMain.Tabs.Add(itm);  
            if (c.Name == CurrentProductCategoryName)  
            {  
                itm.Selected = true;  
                itm.CssClass = "current";  

                // load sub categories  
                c.ProductSubcategory.Load();  
                foreach (ProductSubcategory productSubcategory in c.ProductSubcategory)  
                {  
                    var subitm = new C1.Web.UI.Controls.C1TabStrip.C1Tab(productSubcategory.Name, productSubcategory.Name, Page.ResolveUrl("~/Products.aspx?Category="   c.Name   "&SubCategory="   productSubcategory.Name), "");  
                    subitm.CssClass  = productSubcategory.Name.Replace(" ", "");  
                    itm.Tabs.Add(subitm);  
                    if (CurrentProductSubcategory!=null && subitm.Text == CurrentProductSubcategory.Name)  
                    {  
                        subitm.CssClass = "current";  
                        subitm.Selected = true;  
                    }  
                    subitm.ScrollSettings.ScrollMode = C1.Web.UI.ScrollMode.Buttons;  
                    subitm.ScrollSettings.ScrollButtonAlign = C1.Web.UI.ScrollButtonAlign.TwoSides;  
                }  
            }  
        }

There are a few things to note in the above code. I am checking to see if the item I am adding matches the current category on this particular page to mark it as the current item in the navigation. This will help people see where they are in the website. Also, I am making sure to load in the SubCategory reference from the main ProductCategory entity. This is called differed or lazy loading that is part of the Entity Framework. It helps keep requests to a minimum and forces you to explicitly request mapped objects. Lastly, I am setting the ScrollMode on the tabs to that we do not cutoff the navigation when there are too many items to display horizontally across the page. And voila, we have navigation!
TheWeb-Menu

List the Categories

OK, now we have a menu, I guess it would be a good time to make the pages we just linked to in it. I am going to add Products.aspx as a controller that simply loads one of three different UserControls (ListCategories.ascx, ListProducts.ascx and ViewProduct.ascx) based on URL parameters. He is the code that does the loading just so you understand what we are getting at.

if (CurrentProduct != null)  
{  
    PnlLoader.Controls.Add(LoadControl("~/UserControls/Products/ViewProduct.ascx"));  
    return;  
}  
if (CurrentProductSubcategory != null)  
{  
    PnlLoader.Controls.Add(LoadControl("~/UserControls/Products/ListProducts.ascx"));  
    return;  
}  
if (CurrentProductCategory != null)  
{  
    PnlLoader.Controls.Add(LoadControl("~/UserControls/Products/ListCategories.ascx"));  
    return;  
}

Basically we are checking the most explicit request first which is for viewing a single product only. Then if it is not specified we check for a specified SubCategory to show a list of products that match it. Lastly, we check to see if a main ProductCategory is specified and show a list of SubCategories within it. This reflects a very rudimentary MVC pattern of keeping my different UI views separate and having a controller that loads them based on the request.

Let's take a look at those in reverse order since its how most people will navigate: list of Categories, list of Products in Category and then details of Product. Which leads me to developing my favorite page in the whole site! The category list module.

TheWeb-ListCategories

The goal for this page is to link to the SubCategories within a main ProductCategory, provide quick links to Products within each SubCategory and display a graph showing the overall properties of the SubCategory. So how do we build this cool page? Well, first I used a very simple control to get a list of SubCategories, a Repeater. The Repeater simply repeats a template for each DataItem in its DataSource. Here is what the markup looks like for the Repeater.

<asp:Repeater ID="LstCategories" runat="server" OnItemDataBound="LstCategories_ItemDataBound">  
    <ItemTemplate>  
        <div class="SubCategory">  
            <div runat="server" id="chartContainer" class="yui-u C1ChartContainer">  
                <uc1:ChartCategory ID="ChartCategory1" runat="server" />  
            </div>  
            <div class="yui-gc">  
                <h3 class="SubCategoryName">  
                    <a id="LnkItem" runat="server">  
                        </a>  
                </h3>  
                <div class="yui-u first">  
                    <cc1:C1MultiPage ID="c1MultiPage" runat="server" ShowToolBar="true" Animation="Auto"  
                        Easing="EaseOutExpo" VisualStyle="" Width="100%">  
                    </cc1:C1MultiPage>  

                </div>  
            </div>  
        </div>  
    </ItemTemplate>  
</asp:Repeater>

Notice that I have added the LstCategories_ItemDataBound event handler so I can add Products into the C1MultiPage control. I am using a MultiPage so the users can page through 7 bikes at a time. I also have a UserControl setup for the Chart. In this handler I simply load in the Products that match the SubCategory in the DataItem.

Now you are probably wondering how we go the cool Chart and ToolTip to integrate with this list. First, I made a UserControl called ChartCategory.ascx. Here is what the markup looks like.

<div class="">  
        <div class="quart">  
            <cc1:C1ProgressBar ID="PrgSpeed" runat="server" AnimationDelay="1000"   
                AnimationDuration="700" Easing="EaseOutQuad" FillDirection="FromRightOrBottom"   
                Height="60px" LabelAlign="RightOrBottom" Orientation="Vertical" Value="30"   
                Width="30px" />  
        </div>  
        <div class="quart">  
            <cc1:C1ProgressBar ID="PrgRugged" runat="server" AnimationDelay="1500"   
                AnimationDuration="700" Easing="EaseOutQuad" FillDirection="FromRightOrBottom"   
                Height="60px" LabelAlign="RightOrBottom" Orientation="Vertical" Value="35"   
                Width="30px" />  
        </div>  
        <div class="quart">  
            <cc1:C1ProgressBar ID="PrgSmooth" runat="server" AnimationDelay="2000"   
                AnimationDuration="700" Easing="EaseOutQuad" FillDirection="FromRightOrBottom"   
                Height="60px" LabelAlign="RightOrBottom" Orientation="Vertical" Value="60"   
                Width="30px" />  
        </div>  
        <div class="quart">  
            <cc1:C1ProgressBar ID="PrgShocks" runat="server" AnimationDelay="2500"   
                AnimationDuration="700" FillDirection="FromRightOrBottom" Height="60px"   
                LabelAlign="RightOrBottom" Orientation="Vertical" Value="80" Width="30px" />  
        </div>  
</div>

That's right, I am using the C1ProgressBar control instead of an actual Chart! I wanted to add animation and decided the ProgressBar is perfect for what I need. It displays a value in percentage, is has an option for vertical orientation and it supports really nice animation with easing. Hint: Always use easing when adding animation to elements in a UI. It really makes a huge difference in how natural the behavior is. I must admit, there is not actual data for them, I just made up the numbers for each Bike Category from what I thought made sense. I then set the value of each ProgressBar based on category like so.

if (Category == "Mountain Bikes")  
{  
    PrgRugged.Value = 100;  
    PrgShocks.Value = 90;  
    PrgSmooth.Value = 20;  
    PrgSpeed.Value = 40;  
}  
else if (Category == "Road Bikes")  
{  
    PrgRugged.Value = 20;  
    PrgShocks.Value = 30;  
    PrgSmooth.Value = 80;  
    PrgSpeed.Value = 60;  
}  
else if (Category == "Touring Bikes")  
{  
    PrgRugged.Value = 10;  
    PrgShocks.Value = 10;  
    PrgSmooth.Value = 70;  
    PrgSpeed.Value = 100;  
}  
else  
{  
    this.Visible = false;  
}

For the Netflix-style ToolTip I am using C1ToolTip and only need to add one of them to the page. I will then populate its content on demand using the OnAjaxUpdate handler. Here is the markup for adding the ToolTip to the UserControl.

<cc2:C1ToolTip ShowDelay="200" CssClass="CategoryToolTip" ID="c1Tooltip" Width="300px"  
    Height="130px" runat="server" VisualStyle="Vista" OnAjaxUpdate="OnAjaxUpdate"  
    ShowAnimation="FadeIn" ShowDuration="200" HideAnimation="FadeOut" HideDuration="200"  
    Position="TopCenter">  
</cc2:C1ToolTip>

And here is the code for handling the OnAjaxUpdate event. I simply lookup information based on the ProductID and build some HTML to be used as the ToolTip's content. In a nutshell this code is just creating a simple Unordered List html element and filling in data.

int pid = 0;  
int.TryParse(prodId, out pid);  
Product prod = ProductManager.GetProductByProductId(pid);  

if (prod != null)  
{  
    StringBuilder sb = new StringBuilder();  

    sb.Append(""

);
sb.AppendFormat("{0}", prod.Name);
sb.AppendFormat("{0}", prod.ListPrice.ToString("C"));
sb.Append("");
sb.Append("Specs");
sb.Append("Color:");
sb.AppendFormat("{0}", prod.Color);
sb.Append("Size:");
sb.AppendFormat("{0}", prod.Size);
sb.Append("Weight:");
sb.AppendFormat("{0}Kg", prod.Weight);
sb.Append("");
sb.Append("");
PlaceHolder ph = new PlaceHolder();
ph.Controls.Add(new LiteralControl(sb.ToString()));
panel.ContentTemplateContainer.Controls.Add(ph);
sb.Remove(0, sb.Length);
}

List the Products

Next, we will take a look at building ListProducts.ascx which is what the end user sees when clicking a SubCategory link from the menu. We will be loading Products based on SubCategoryId and building some filter controls to help users find a bike more easily.

TheWeb-ListProducts

To pull this page off we are going to use almost the same approach as the previous module. We will use a repeater with a C1MultiPage to pull off a clean list of Products with paging. Since the pages are so similar I am just going to focus on building the filter pieces.

To build the filters I simply add two controls: C1ComboBox for the color selection and C1Slider for the Size selection.

<div class="Filter">  
    <h3>  
        <asp:Label class="C1ColorComboLabel" ID="LblColor" runat="server" AssociatedControlID="CboColorPick"  
            Text="Color"></asp:Label></h3>  
    <div id="ItmColor" runat="server" class="C1ColorComboContainer">  
        <c1:C1ComboBox CssClass="C1ColorCombo" ID="CboColorPick" runat="server" DropDownResizable="true" AutoPostBack="True"  
            Width="150px">  
        </c1:C1ComboBox>  
    </div>  
    <h3>  
        <asp:Label class="C1SizeSliderLabel" ID="LblSize" runat="server" AssociatedControlID="SldSize"  
            Text="Size"></asp:Label></h3>  
    <div id="ItmSize" runat="server" class="C1SizeSliderContainer">  
        <c1:C1Slider CssClass="C1SizeSlider" ID="SldSize" runat="server" AutoPostBack="true">  
        </c1:C1Slider>  
    </div>  
</div>  

The most important part of developing this filter UI is to only display options that will result in products. My design is actually limited since a combination of filters could result in no products being found, but that could be solved in a similar fashion.

int currentCategoryId = CurrentProductSubcategory.ProductSubcategoryID;  
List<string> colors = ProductManager.GetProductColor(currentCategoryId);  

if (colors.Count() > 0)  
{  
    CboColorPick.Items.Clear();  
    CboColorPick.AppendDataBoundItems = true;  
    CboColorPick.Items.Add(new C1ComboBoxItem("All", "All"));  
    CboColorPick.DataSource = colors;  
    CboColorPick.DataBind();  

}  
else  
{  
    ItmColor.Visible = false;  
}  

List<string> sizes = ProductManager.GetProductSize(currentCategoryId);  
int sizeCnt = sizes.Count();  
if (sizeCnt > 0)  
{  
    SldSize.MinimumValue = 0;  
    SldSize.MaximumValue = sizeCnt;  
    List<string> sizeList = new List<string>();  
    sizeList.Add("All");  
    foreach (string size in sizes)  
    {  
        sizeList.Add(size);  
    }  
    ViewState["SizeList"] = sizeList;  
}  
else  
{  
    ItmSize.Visible = false;  
}

And here are the two get methods referenced from above. The key aspect of this code is the call .Distict() LINQ query. It returns only unique or distinct values.

public static List<string> GetProductColor(int categoryId)  
{  
    List<string> colorList = null;  
    colorList = (from p in Common.DataEntities.Product  
                 where p.Color != null && p.ProductSubcategory.ProductSubcategoryID == categoryId  
                 orderby p.Color  
                 select p.Color).Distinct().ToList();  
    return colorList;  
}  
public static List<string> GetProductSize(int categoryId)  
{  
    List<string> sizeList = null;  
    sizeList = (from p in Common.DataEntities.Product  
                where p.Size != null && p.ProductSubcategory.ProductSubcategoryID == categoryId  
                orderby p.Size  
                select p.Size).Distinct().ToList();  
    return sizeList;  
}

Sell It!

Add the Shopping Cart

OK, by this point in the project I'm sure a manager would be asking when the Shopping Cart will be online. So let's keep this project moving and put together a shopping cart to start bringing in the money.

TheWeb-ShoppingCart

Since there is a lot of code in the shopping cart, I am just going to highlight a few things in it. To start, let's look at the cart itself. No shopping cart would be complete without a grid. I am going to use C1GridView for this purpose. Here is the markup I am using. Notice the DataFormatString property to help my numeric data look format as currency. I also added some CssClasses to make the currency columns align right. There is also a TemplateField with a C1NumericInput in it for the end user to change the LineItem's Quantity.

<c1:C1GridView ID="gvShopCart" CallbackOptions="None" runat="server" DataKeyNames="ShoppingCartItemID" AutoGenerateColumns="false" Width="950px" OnRowDeleting="gvShopCart_RowDeleting" OnRowCommand="gvShopCart_RowCommand" VisualStyle="Windows7">  
    <Columns>  
        <c1:C1BoundField DataField="Name" HeaderText="Item">  
        </c1:C1BoundField>  
        <c1:C1BoundField DataField="ListPrice" HeaderText="Price" DataFormatString="{0:C}">  
            <ItemStyle CssClass="number" />  
            <HeaderStyle CssClass="number" />  
        </c1:C1BoundField>  
        <c1:C1TemplateField HeaderText="Quantity">  
            <ItemStyle CssClass="number" />  
            <HeaderStyle CssClass="number" />  
            <ItemTemplate>  
                <asp:HiddenField ID="HFProductId" runat="server" Value='' />  
                <c1:C1NumericInput ID="NIQuantity" Width="40px" runat="server" DecimalPlaces="0" Value='' />  
            </ItemTemplate>  
        </c1:C1TemplateField>  
        <c1:C1BoundField DataField="Cost" HeaderText="Cost" DataFormatString="{0:C}">  
            <ItemStyle CssClass="number" />  
            <HeaderStyle CssClass="number" />  
        </c1:C1BoundField>  
        <c1:C1TemplateField HeaderText="Update">  
            <ItemStyle CssClass="action" />  
            <ItemTemplate>  
                <asp:Button ID="BtnUpd" runat="server" Text="Update" CommandName="Update" CommandArgument="" />  
            </ItemTemplate>  
        </c1:C1TemplateField>  
        <c1:C1CommandField ShowEditButton="false" ShowDeleteButton="true" ButtonType="Button" HeaderText="Remove" DeleteText="Remove">  
            <ItemStyle CssClass="action" />  
        </c1:C1CommandField>  
    </Columns>  
</c1:C1GridView>

Now that we have the markup, we need the data for binding. I just used a simple LINQ expression in this case and added a Cost field calculated by multiplying the LineItem's ListPrice by the LineItem's Quantity.

private void LoadData()  
{  
    List ls = RequestContext.Current.UserShoppingCart.ShoppingCarItems;  
    var binddata = from i in ls  
                   select new {i.ShoppingCartItemID, i.Product.ProductID, i.Product.Name, i.Product.ListPrice, i.Quantity, Cost = i.Product.ListPrice * i.Quantity };  
    gvShopCart.DataSource = binddata;  
    gvShopCart.DataBind();  
}

Notice I added two event handlers in the markup: OnRowCommand and OnRowDeleting. The OnRowCommand handles updating the LineItem's Quantity when the Update button is clicked in the GridView's row. The OnRowDeleting simply fires to delete a LineItem when the Remove button is clicked. Here are the actual handlers.

protected void gvShopCart_RowCommand(object sender, C1GridViewCommandEventArgs e)
{
if (e.CommandName.Equals("Update"))
{
int itemId = Convert.ToInt32(gvShopCart.DataKeys[Convert.ToInt32(e.CommandArgument)].Value);
int quantity = (int)(e.Row.FindControl("NIQuantity") as C1NumericInput).Value;
ShoppingCartManager.UpdateShoppingCartItem(itemId, quantity);
LoadData();
BindTotalData();
}
}

protected void gvShopCart_RowDeleting(object sender, C1GridViewDeleteEventArgs e)  
{  
    string itemId = gvShopCart.DataKeys[e.RowIndex].Value.ToString();  
    ShoppingCartManager.DeleteShoppingCartItem(int.Parse(itemId));  
    LoadData();  
    BindTotalData();  
}

Collect Billing Information

Next we need to add a screen to submit billing info for the order.

TheWeb-ShoppingCart2

This page uses some cool little controls to make it looks a behave much better than a normal form. I am using C1ComboBox instead of DropDownList, C1DateInput for the Expiration date field, C1MaskedInput for the Phone number field and C1NumericInput for the Code field. I also added C1FormDecorator to make the Credit Card type RadioButtons look better. The Form decorator is one of my all time favorite controls for improving WebForms apps. It styles everything from TextBoxes to CheckBoxes and everything in between to make every form element look way better than the native controls. It's a great way to sexy up any page in seconds. Here is the markup for the Billing Form.

<fieldset>  
    <legend>Info</legend>  
    <ul>  
        <li>Email</li>  
        <li>  
            <cc1:C1MaskedInput Width="190px" ID="EmailCon" runat="server" Mask="CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"  
                HidePromptOnLeave="True" InvalidInputColor="Red"></cc1:C1MaskedInput>  
        </li>  
        <li>First Name</li>  
        <li>  
            <asp:TextBox runat="server" ID="FirstNameCon"></asp:TextBox>  
        </li>  
        <li>Last Name </li>  
        <li>  
            <asp:TextBox runat="server" ID="LastNameCon"></asp:TextBox>  
        </li>  
        <li>Address </li>  
        <li>  
            <asp:TextBox Width="190px" runat="server" ID="AddressLine1Con" Text="201 S Highland Ave"></asp:TextBox>  
            <asp:TextBox Width="190px" runat="server" ID="AddressLine2Con" Text="3rd Floor"></asp:TextBox>  
        </li>  
        <li>City </li>  
        <li>  
            <asp:TextBox runat="server" ID="CityCon" Text="Pittsburgh"></asp:TextBox>  
        </li>  
        <li>State </li>  
        <li>  
            <cc2:C1ComboBox runat="server" ID="StateCon" Width="180px" DropDownHeight="250">  
                <Items>  
                    <cc2:C1ComboBoxItem Value="AL" Text="Alabama">  
                    </cc2:C1ComboBoxItem>  

                    <cc2:C1ComboBoxItem Value="WY" Text="Wyoming">  
                    </cc2:C1ComboBoxItem>  
                </Items>  
            </cc2:C1ComboBox>  
        </li>  
        <li>Zip </li>  
        <li>  
            <asp:TextBox runat="server" ID="ZipCon" Width="60px" Text="15206"></asp:TextBox>  
        </li>  
        <li>Phone </li>  
        <li>  
            <cc1:C1MaskedInput ID="PhoneCon" runat="server" Mask="(999)000-0000" HidePromptOnLeave="True"  
                InvalidInputColor="Red" Text="(412)681-4343"></cc1:C1MaskedInput>  
        </li>  
    </ul>  
</fieldset>
<fieldset class="Payment">  
    <legend class="PaymentTypeTitle">Payment Type</legend>  
    <ul class="PaymentType">  
        <li class="PaymentCard">  
            <asp:RadioButtonList runat="server" ID="PaymentType" RepeatLayout="Flow" Repealiirection="Horizontal">  
                <asp:ListItem Text="Visa" Value="Visa" Selected="True"></asp:ListItem>  
                <asp:ListItem Text="MasterCard" Value="MasterCard"></asp:ListItem>  
                <asp:ListItem Text="American Express" Value="AmericanExpress"></asp:ListItem>  
            </asp:RadioButtonList>  
        </li>  
        <li class="PaymentCardNumber">Card Number</li>  
        <li class="PaymentCardNumberValue">  
            <asp:TextBox runat="server" ID="CardNumber" Text="1234123412341234" />  
            <asp:RequiredFieldValidator runat="server" ID="cardnumberVal" ControlToValidate="CardNumber" ErrorMessage="Card Number can't be empty" Display="Dynamic"></asp:RequiredFieldValidator>  
            <asp:RangeValidator ID="cardRange" runat="server" ControlToValidate="CardNumber" MinimumValue="1" ErrorMessage="Invalid Card Number" Display="Dynamic" MaximumValue="9999999999999999" Type="Double"></asp:RangeValidator>  
        </li>  
        <li class="CardExpirationDate">Expiration</li>  
        <li class="CardExpirationDateValue">  
            <cc2:C1DateInput runat="server" ID="Expiration" DateFormat="MM/yy" />  
        </li>  
        <li class="CardCode">Code</li>  
        <li class="CardCodeValue">  
            <cc2:C1NumericInput runat="server" ID="Code" DecimalPlaces="0" Value="123" />  
        </li>  
    </ul>  
</fieldset>

    <c1:C1FormDecorator runat="server" ID="C1FormDecorator1" DropDownHideAnimation="FadeOut" DropDownShowAnimation="FadeIn" VisualStyle="" />

And Shipping Info (if different than billing)

The Shipping Information form should be much easier, since I am going to reuse the Billing Information UserControl.

TheWeb-ShoppingCart3

On the left I have added a C1Calendar that allows the end user to select a shipping date anytime from the date the purchase is made through 2 weeks later. I know this isn't normal, but I liked the idea and figured it would show off our calendar nicely. Who knows, this concept might re-invent the entire shipping industry as we know it. So this is a really basic form in which I add the calendar and a checkbox before the BillInfo.ascx UserControl. If the end user unchecks the "Same address as Billing" CheckBox the Info UserControl will become editable. This is really simple code, but makes re-using the same UI possible. Anytime you can use a template or reuse the same markup you are adding consistency and familiarity to your app.

Confirm Order

TheWeb-ShoppingCart4
This page is pretty similar to the initial shopping cart screen. It is a GridView using BoundFields and DataFormatString for currency. The difference is that this grid's quantity field is read-only. Below the GridView there is a nice summary of billing and shipping information. We can build this without any fancy controls, just a couple of labels. I used a fieldset and and unordered list to lay out the information.

<fieldset>  
    <legend>Bill To</legend>  
    <ul>  
        <li class="BillName">  
            <asp:Label runat="server" ID="BillFirstName" CssClass="BillFirstName">  
            </asp:Label> <asp:Label runat="server"  
                ID="BillLastName" CssClass="BillLastName"></asp:Label>  
        </li>  
        <li class="CreditCardType">  
            <asp:Label runat="server" ID="BillCreditCardType"></asp:Label>  
        </li>  
        <li class="CreditCardNumber">  
            <asp:Label runat="server" ID="BillCreditCardNumber"></asp:Label>  
        </li>  
        <li class="CreditCardExpire">  
            <asp:Label runat="server" ID="BillCreditCardExpire"></asp:Label>  
        </li>  
        <li class="BillAddress">  
            <asp:Label runat="server" ID="BillAddressLine1"></asp:Label>  
        </li>  
        <li>  
            <asp:Label runat="server" ID="BillAddressLine2"></asp:Label>  
        </li>  
        <li>  
            <asp:Label runat="server" ID="BillAddress"></asp:Label>  
        </li>  
    </ul>  
</fieldset>

Would you like a receipt?

TheWeb-ShoppingCart5

If we are building an online store, we better give people receipts that are easy to print. Since the customer will want to be able to print or save the receipt, using the C1ReportViewer was a perfect choice. It has built in print and export features. Just by using it to display the receipt, the customers will be able to print a high quality receipt or save it as PDF, Excel, Word, RTF or XML. OK, so XML is probably a little ridiculous for this scenario, but it shows off the feature so I couldn't resist. Who knows, someone might want to integrate with our online store and import large orders into their system, right? Well, anyways here is the markup I wrote for the ReportViewer. I set Zoom="Fit Page" so the entire report is visible when the page loads.

<cc1:C1ReportViewer ID="C1ReportViewer1" runat="server"    
    FileName="~/adventureworks.xml"  ReportName="ReceiptReport" Width="950px" Height="600px" Zoom="Fit Page" />

Now, I need to DataBind the the report during the Page_Load event. I am also going to set the Cache so that the report is always generated as new. For large shared reports you would want to actually utilize the cache so that when large queries are run they are shared between users/views. Obviously, receipts can't be shared so we will turn off sharing between sessions.

protected void Page_Load(object sender, EventArgs e)  
{  
    Response.CacheControl = "private";  
    Response.Expires = 0;  
    Response.AddHeader("pragma", "no-cache");  
    this.C1ReportViewer1.Cache.ShareBetweenSessions = false;  
    if (this.C1ReportViewer1.HasCachedDocument)  
    {  
        this.C1ReportViewer1.Document = null;  
    }  
    this.C1ReportViewer1.Document = GetReport();  
    C1ReportViewer1.ToolsPaneVisible = false;  
}

Notice how I am setting the Document property to the returned result of another function. The GetReport function loads the reports definition (adventureworks.xml) and then sets the DataSource for the report and any sub-report it contains.

private object GetReport()  
{  
    Contact contact = RequestContext.Current.Contact;  
    Customer customer = CustomerManager.GetCustomerByContactID(contact.ContactID);  
    SalesOrderHeader salesOrderHeader = SalesOrderManager.GetLatestSalesOrderHeaderByCustomerID(customer.CustomerID);  

    C1.C1Report.C1Report rep = new C1.C1Report.C1Report();  
    rep.UseGdiPlusTextRendering = true;  
    rep.EmfType = System.Drawing.Imaging.EmfType.EmfPlusOnly;  
    // load report into control  
    string reportFile = MapPath("~/adventureworks.xml");  
    rep.Load(reportFile, "ReceiptReport");  
    DataView dv = GetReportRecordSet(salesOrderHeader);  
    rep.DataSource.Recordset = dv;  

    foreach (C1.C1Report.Field f in rep.Fields)  
    {  
        C1.C1Report.C1Report sr = f.Subreport;  
        if (sr == null) continue;  
        sr.DataSource.Recordset = GetReportGridRecordSet(salesOrderHeader);  

    }  

    return rep;  
}

Lastly, the GetReport function actually calls two functions that return DataViews to be bound to the report. This report has two custom DataViews. The first is for general order information and the second is for the the list of products purchased. Here are the two functions that return the DataViews for this report. Notice how the columns in the DataViews match the field names in the report.

private DataView GetReportRecordSet(SalesOrderHeader salesOrderHeader)  
{  
    salesOrderHeader.BillAddressReference.Load();  

    DataTable dataTable = new DataTable();  
    dataTable.Columns.Add("BillingAddress", typeof(string));  
    dataTable.Columns.Add("BillingAddress2", typeof(string));  
    dataTable.Columns.Add("BillingCity", typeof(string));  
    dataTable.Columns.Add("BillingPostalCode", typeof(string));  
    dataTable.Columns.Add("BillingStateProvinceCode", typeof(string));  

    dataTable.Columns.Add("ShippingAddress", typeof(string));  
    dataTable.Columns.Add("ShippingAddress2", typeof(string));  
    dataTable.Columns.Add("ShippingCity", typeof(string));  
    dataTable.Columns.Add("ShippingPostalCode", typeof(string));  
    dataTable.Columns.Add("ShippingStateProvinceCode", typeof(string));  
    dataTable.Columns.Add("SubTotal", typeof(decimal));  

    dataTable.Columns.Add("FullName", typeof(string));  
    dataTable.Columns.Add("EmailAddress", typeof(string));  
    dataTable.Columns.Add("Phone", typeof(string));  
    dataTable.Columns.Add("CardType", typeof(string));  
    dataTable.Columns.Add("CardNumber", typeof(string));  


    DataRow dr = dataTable.NewRow();  
    dr["BillingAddress"] = salesOrderHeader.BillAddress.AddressLine1;  
    dr["BillingAddress2"] = salesOrderHeader.BillAddress.AddressLine2;  
    dr["BillingCity"] = salesOrderHeader.BillAddress.City;  
    dr["BillingPostalCode"] = salesOrderHeader.BillAddress.PostalCode;  
    salesOrderHeader.BillAddress.StateProvinceReference.Load();  
    dr["BillingStateProvinceCode"] = salesOrderHeader.BillAddress.StateProvince.StateProvinceCode;  

    dr["ShippingAddress"] = salesOrderHeader.ShipAddress.AddressLine1;  
    dr["ShippingAddress2"] = salesOrderHeader.ShipAddress.AddressLine2;  
    dr["ShippingCity"] = salesOrderHeader.ShipAddress.City;  
    dr["ShippingPostalCode"] = salesOrderHeader.ShipAddress.PostalCode;  
    salesOrderHeader.ShipAddress.StateProvinceReference.Load();  
    dr["ShippingStateProvinceCode"] = salesOrderHeader.ShipAddress.StateProvince.StateProvinceCode;  
    dr["SubTotal"] = salesOrderHeader.SubTotal;  
    salesOrderHeader.ContactReference.Load();  
    dr["FullName"] = salesOrderHeader.Contact.FirstName   " "   salesOrderHeader.Contact.LastName;  
    dr["EmailAddress"] = salesOrderHeader.Contact.EmailAddress;  
    dr["Phone"] = salesOrderHeader.Contact.Phone;  
    salesOrderHeader.CreditCardReference.Load();  
    dr["CardType"] = salesOrderHeader.CreditCard.CardType;  
    dr["CardNumber"] = salesOrderHeader.CreditCard.CardNumber;  
    dataTable.Rows.Add(dr);  
    return dataTable.DefaultView;  
}  

private DataView GetReportGridRecordSet(SalesOrderHeader salesOrderHeader)  
{  

    DataTable dataTable = new DataTable();  
    dataTable.Columns.Add("SalesOrderID", typeof(int));  
    dataTable.Columns.Add("Name", typeof(string));  
    dataTable.Columns.Add("UnitPrice", typeof(decimal));  
    dataTable.Columns.Add("OrderQty", typeof(short));  
    dataTable.Columns.Add("LineTotal", typeof(decimal));  

    salesOrderHeader.SalesOrderDetail.Load();  
    IEnumerator dt =  salesOrderHeader.SalesOrderDetail.GetEnumerator();  
    while (dt.MoveNext())  
    {  
        DataRow dr1 = dataTable.NewRow();  
        dr1["SalesOrderID"] = dt.Current.SalesOrderID;  
        dt.Current.SpecialOfferProductReference.Load();  
        dt.Current.SpecialOfferProduct.ProductReference.Load();  
        dr1["Name"] = dt.Current.SpecialOfferProduct.Product.Name;  
        dr1["UnitPrice"] = dt.Current.UnitPrice;  
        dr1["OrderQty"] = dt.Current.OrderQty;  
        dr1["LineTotal"] = dt.Current.LineTotal;  

        dataTable.Rows.Add(dr1);  
    }  

    return dataTable.DefaultView;  
}

Phew, glad we got through that one. So that is (in a nutshell) how we built (more or less) the AdventureWorks web app. I know there are plenty of things we did not cover completely, but I think we hit on most of the important pieces of the project. The one thing I did not dive into was Membership. The reason I left that out is that we used the ASP.NET out-of-the-box Membership Provider. This topic is well documented and if you are interested in it, please feel free to check out these resources: Introduction to Membership & How To: Use Membership in ASP.NET 2.0. So, checkout the code and try the app for yourself. There is plenty of code in there that you can use in any project, especially all the cool interactive stuff.

If you have any questions or feedback on the project, please don't hesitate to post a comment!

So, now you must be asking: How did we make this app look so cool? That my friends will have to wait until the next article featuring a guest appearance by Jebro our CSS Ninja!