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