Carl Franklin

.NET Wonk

A shopping cart web control with styles

So, I did this talk on building Composite Web Controls at Thom Robbins' awesome Microsoft CodeCamp II in Waltham, MA last weekend. And, I've been really digging into building these suckers lately. I got stuck on persisting control styles with ViewState. The documentation is a bit scatterbrained, but I did manage to find an ok example in the docs. 

So, what follows is a Shopping Cart Item web control that shows you a picture, has a product name, description, price, a button, and a textbox for entering the quantity. The quantity logic needs work, I know, but the ViewState stuff is killer. It raises a button click event from inside the control to the consumer of that control. That little piece of code was a doozey. Apparently if you don't set the ID property of a control which you create on the fly.... all sorts of problems.

So here it is, the fruits of my most recent labor. I hope it inspires you. And hey, if you actually come up with something even more cool, send it to me and I'll give you a shout on the show... unless it sucks, of course. :-)

Imports System.ComponentModel
Imports System.Web.UI
Imports System.Web.UI.WebControls

<ToolboxData("<{0}:ShoppingCartControl runat=server></{0}:ShoppingCartControl>")> _
Public Class ShoppingCartControl
    Inherits WebControl
    Implements INamingContainer

#Region " Privates "

    Private lblDescription As Label
    Private linkImage As HyperLink
    Private lblProductName As Label
    Private lblPrice As Label
    Private txtQuantity As TextBox
    Private tblMain As Table
    Private row1, row2 As TableRow
    Private cellTopRight, cellTopLeft, cellBottomRight, cellBottomLeft As TableCell

#End Region

#Region " Button, Button Event, and Handler "

    Private WithEvents btnMain As Button

    Public Event ButtonClick As EventHandler

    '-- When the user clicks the button, this event happens internally.
    '   We simply raise the event to the consumer of the control.
    Private Sub btnMain_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnMain.Click
        If IsNumeric(txtQuantity.Text) Then
            Quantity += CInt(txtQuantity.Text)
        End If
        RaiseEvent ButtonClick(sender, e)
    End Sub
#End Region

#Region " Properties "

    Private _price As Decimal
    <Bindable(True), Category("Content")> _
    Public Property Price() As Decimal
        Get
            Return _price
        End Get
        Set(ByVal Value As Decimal)
            _price = Value
        End Set
    End Property

    Private _productName As String
    <Bindable(True), Category("Content")> _
    Public Property ProductName() As String
        Get
            Return _productName
        End Get
        Set(ByVal Value As String)
            _productName = Value
        End Set
    End Property

    Private _imageURL As String
    <Bindable(True), Category("Content")> _
    Public Property ImageURL() As String
        Get
            Return _imageURL
        End Get
        Set(ByVal Value As String)
            _imageURL = Value
        End Set
    End Property

    Private _navigateURL As String
    <Bindable(True), Category("Content")> _
    Public Property NavigateURL() As String
        Get
            Return _navigateURL
        End Get
        Set(ByVal Value As String)
            _navigateURL = Value
        End Set
    End Property

    Private _description As String
    <Bindable(True), Category("Content")> _
    Public Property Description() As String
        Get
            Return _description
        End Get
        Set(ByVal Value As String)
            _description = Value
        End Set
    End Property

    <Bindable(True), Category("Content")> _
    Public Property Quantity() As Integer
        Get
            If ViewState("Quantity") Is Nothing Then
                ViewState("Quantity") = CInt(0)
            End If
            Return CInt(ViewState("Quantity"))
        End Get
        Set(ByVal Value As Integer)
            ViewState("Quantity") = Value
        End Set
    End Property

#End Region

#Region " Style Properties "

    Private _descriptionStyle As TableItemStyle
    <Bindable(True), Category("Style"), _
    DesignerSerializationVisibility(DesignerSerializationVisibility.Content), _
    NotifyParentProperty(True), _
    PersistenceMode(PersistenceMode.InnerProperty)> _
    Public Property DescriptionStyle() As TableItemStyle
        Get
            If _descriptionStyle Is Nothing Then
                _descriptionStyle = New TableItemStyle
                '-- If our control is tracking viewstate, turn viewstate
                '   tracking on for this style control.
                If IsTrackingViewState Then
                    CType(_descriptionStyle, IStateManager).TrackViewState()
                End If
            End If
            Return _descriptionStyle
        End Get
        Set(ByVal Value As TableItemStyle)
            _descriptionStyle = Value
        End Set
    End Property

    Private _priceStyle As TableItemStyle
    <Bindable(True), Category("Style"), _
    DesignerSerializationVisibility(DesignerSerializationVisibility.Content), _
    NotifyParentProperty(True), _
    PersistenceMode(PersistenceMode.InnerProperty)> _
    Public Property PriceStyle() As TableItemStyle
        Get
            If _priceStyle Is Nothing Then
                _priceStyle = New TableItemStyle
                '-- If our control is tracking viewstate, turn viewstate
                '   tracking on for this style control.
                If IsTrackingViewState Then
                    CType(_priceStyle, IStateManager).TrackViewState()
                End If
            End If
            Return _priceStyle
        End Get
        Set(ByVal Value As TableItemStyle)
            _priceStyle = Value
        End Set
    End Property

    Private _buttonStyle As TableItemStyle
    <Bindable(True), Category("Style"), _
    DesignerSerializationVisibility(DesignerSerializationVisibility.Content), _
    NotifyParentProperty(True), _
    PersistenceMode(PersistenceMode.InnerProperty)> _
    Public Property ButtonStyle() As TableItemStyle
        Get
            If _buttonStyle Is Nothing Then
                _buttonStyle = New TableItemStyle
                '-- If our control is tracking viewstate, turn viewstate
                '   tracking on for this style control.
                If IsTrackingViewState Then
                    CType(_buttonStyle, IStateManager).TrackViewState()
                End If
            End If
            Return _buttonStyle
        End Get
        Set(ByVal Value As TableItemStyle)
            _buttonStyle = Value
        End Set
    End Property

    Private _productNameStyle As TableItemStyle
    <Bindable(True), Category("Style"), _
    DesignerSerializationVisibility(DesignerSerializationVisibility.Content), _
    NotifyParentProperty(True), _
    PersistenceMode(PersistenceMode.InnerProperty)> _
    Public ReadOnly Property ProductNameStyle() As TableItemStyle
        Get
            If (_productNameStyle Is Nothing) Then
                '-- If our control is tracking viewstate, turn viewstate
                '   tracking on for this style control.
                _productNameStyle = New TableItemStyle
                If IsTrackingViewState Then
                    CType(_productNameStyle, IStateManager).TrackViewState()
                End If
            End If
            Return _productNameStyle
        End Get
    End Property

#End Region

#Region " Render and CreateChildControls "

    Protected Overrides Sub Render(ByVal output As System.Web.UI.HtmlTextWriter)
        '-- Make sure the child controls exist
        EnsureChildControls()
        '-- Apply styles and set property values
        With lblDescription
            .Text = Description
            .ApplyStyle(DescriptionStyle)
        End With
        With lblPrice
            .Text = Format(Price, "C")
            .ApplyStyle(PriceStyle)
        End With
        With lblProductName
            .Text = ProductName
            .ApplyStyle(ProductNameStyle)
        End With
        With btnMain
            .Text = "Add to Cart"
            .ApplyStyle(ButtonStyle)
        End With
        txtQuantity.Text = Quantity.ToString
        With linkImage
            .ImageUrl = ImageURL
            .NavigateUrl = NavigateURL
        End With
        tblMain.CellSpacing = 5
        '-- Render
        MyBase.Render(output)
    End Sub

    Protected Overrides Sub CreateChildControls()
        '-- This gets called when controls need to be created
        '   Don't set property values in here that might change
        lblDescription = New Label
        '-- Without an ID tag, controls sometimes won't render in other browsers
        lblDescription.ID = "lblDescription"

        btnMain = New Button
        btnMain.ID = "btnMain"
        lblPrice = New Label
        lblPrice.ID = "lblPrice"
        linkImage = New HyperLink
        linkImage.ID = "linkImage"
        lblProductName = New Label
        lblProductName.ID = "lblProductName"
        txtQuantity = New TextBox
        txtQuantity.ID = "txtQuantity"

        tblMain = New Table
        row1 = New TableRow
        row2 = New TableRow
        cellTopLeft = New TableCell
        cellTopRight = New TableCell
        cellBottomLeft = New TableCell
        cellBottomRight = New TableCell

        With cellTopLeft
            .Controls.Add(linkImage)
            .VerticalAlign = VerticalAlign.Top
            .HorizontalAlign = HorizontalAlign.Right
        End With

        With cellTopRight
            .Controls.Add(lblProductName)
            .Controls.Add(New LiteralControl("<br>"))
            .Controls.Add(lblDescription)
            .Controls.Add(New LiteralControl("<br>"))
            .Controls.Add(lblPrice)
            .HorizontalAlign = HorizontalAlign.Left
            .VerticalAlign = VerticalAlign.Top
        End With

        With cellBottomLeft
            .Controls.Add(btnMain)
            .HorizontalAlign = HorizontalAlign.Right
            .VerticalAlign = VerticalAlign.Top
        End With

        With cellBottomRight
            .Controls.Add(txtQuantity)
            .HorizontalAlign = HorizontalAlign.Left
            .VerticalAlign = VerticalAlign.Top
        End With

        row1.Cells.Add(cellTopLeft)
        row1.Cells.Add(cellTopRight)

        row2.Cells.Add(cellBottomLeft)
        row2.Cells.Add(cellBottomRight)

        tblMain.Rows.Add(row1)
        tblMain.Rows.Add(row2)

        Controls.Add(tblMain)

    End Sub

#End Region

#Region " ViewState Management "

    Protected Overrides Function CreateControlStyle() As System.Web.UI.WebControls.Style
        '-- When the control style is created, make sure it's persisting
        '   in the ViewState
        Dim Style As New TableStyle(ViewState)
        Style.CellPadding = 0
        Return Style
    End Function

    Protected Overrides Function SaveViewState() As Object
        '-- Customized state management to handle saving
        '   state of contained objects such as styles.
        Dim baseState As Object = MyBase.SaveViewState()
        Dim productNameStyleState As Object
        Dim descriptionStyleState As Object
        Dim priceStyleState As Object
        Dim buttonStyleState As Object

        '-- Get the view state of each style
        If Not (_productNameStyle Is Nothing) Then
            productNameStyleState = CType(_productNameStyle, IStateManager).SaveViewState()
        Else
            productNameStyleState = Nothing
        End If
        If Not (_descriptionStyle Is Nothing) Then
            descriptionStyleState = CType(_descriptionStyle, IStateManager).SaveViewState()
        Else
            descriptionStyleState = Nothing
        End If
        If Not (_priceStyle Is Nothing) Then
            priceStyleState = CType(_priceStyle, IStateManager).SaveViewState()
        Else
            priceStyleState = Nothing
        End If
        If Not (_buttonStyle Is Nothing) Then
            buttonStyleState = CType(_buttonStyle, IStateManager).SaveViewState()
        Else
            buttonStyleState = Nothing
        End If

        '-- Return an array that contains the base view state plus the
        '   sub-control style viewstates
        Dim myState(4) As Object
        myState(0) = baseState
        myState(1) = productNameStyleState
        myState(2) = descriptionStyleState
        myState(3) = priceStyleState
        myState(4) = buttonStyleState

        Return myState
    End Function

    Protected Overrides Sub LoadViewState(ByVal savedState As Object)
        '-- Customized state management to handle saving
        '   state of contained objects.
        If Not (savedState Is Nothing) Then
            '-- The object is actually an array of objects. Cast it so
            Dim myState As Object() = CType(savedState, Object())
            If Not (myState(0) Is Nothing) Then
                MyBase.LoadViewState(myState(0))
            End If
            If Not (myState(1) Is Nothing) Then
                CType(ProductNameStyle, IStateManager).LoadViewState(myState(1))
            End If
            If Not (myState(2) Is Nothing) Then
                CType(ProductNameStyle, IStateManager).LoadViewState(myState(2))
            End If
            If Not (myState(3) Is Nothing) Then
                CType(ProductNameStyle, IStateManager).LoadViewState(myState(3))
            End If
            If Not (myState(4) Is Nothing) Then
                CType(ProductNameStyle, IStateManager).LoadViewState(myState(4))
            End If
        End If
    End Sub

    Protected Overrides Sub TrackViewState()
        '-- Customized state management to handle saving
        '   state of contained objects such as styles.

        MyBase.TrackViewState()

        '-- Call TrackViewState on each sub-control
        If Not (_productNameStyle Is Nothing) Then
            CType(_productNameStyle, IStateManager).TrackViewState()
        End If
        If Not (_descriptionStyle Is Nothing) Then
            CType(_descriptionStyle, IStateManager).TrackViewState()
        End If
        If Not (_priceStyle Is Nothing) Then
            CType(_priceStyle, IStateManager).TrackViewState()
        End If
        If Not (_buttonStyle Is Nothing) Then
            CType(_buttonStyle, IStateManager).TrackViewState()
        End If
    End Sub

#End Region

End Class

Comments

M Kenyon said:

Love it when an example drops right into VS with no code errors... so many are sloppy posting their examples. Thanks.

I'll try this out.

So, you can keep the Button and it's Click event handler private as long as you raise the public event? I thought you needed to keep all three public. I wonder why I thought that...

I also like how you declared your properties and the private variable they return. Keeps it all together...

Wow... what a great example, I was just working on doing a control. I understand you need to place info into the view state so the control persists correctly. Any good tutorials on Why/How/When to work with the viewstate? I've dinked around with getting html objects to manually fire events and pull the info out of viewstate, but I was just following example. I didn't really understand what/why I was doing it. (Well, I know what I wanted to accomplish, I just didn't know why this was it.) So... viewstate, teach me yoda.
# October 21, 2004 9:03 AM

Mike said:

Well, it's a start. It would be more cool if it was an actual shopping cart control that could hold multiple shopping cart items, which you could databind to. Maybe somebody will extend the idea. It works nice as an example of a webcontrol though.
# October 21, 2004 9:43 AM

Brian Swanson said:

I'm laughing right now...Watching Carl do a webcast for Microsoft on Server Side State Management, and he's working in VS.NET and I see a reference in his "find" toolbar to this shopping cart control.
# October 21, 2004 11:18 AM

Carl Franklin said:

Mike,

You can bind to it. That's the idea. Use it in a Repeater or DataList, and bind the Price, Description, ImageURL, ProductName, and NavigateURL properties to a data source.
# October 21, 2004 1:40 PM
Leave a Comment

(required) 

(required) 

(optional)

(required)