Introduction:
This article describes the construction of a custom
control used to display a three day weather forecast based upon a designated zip
code. The control is driven by a public, free web service that returns the seven
forecast for any area in the United States by zip code or location. This
demonstration only uses the first three days of the seven day forecast and it
implements only the zip code based request for forecast data.
In addition to returning the weather forecast, the web
service also returns the place name (e.g., City), State, and the
latitude/longitude pair for the zip code. It also returns a few other things
that may be of interest such the FIPS code for the location.
Another interesting feature of the web service is that
it also returns a path to an image that reflects the forecast (e.g., a picture
of it raining outside, or sunny, etc.). With forecast images involving
precipitation, the percentage of precipitation is also shown as an addition to
the forecast image. This path is used to dynamically load the image when the web
service's web method "GetWeatherByZipCode" is evoked.
In using the control, if you were to retain a user's
zip code or persist it on the user's machine by stashing it into a cookie, the
user see the forecast for their particular geographic location when returning to
the site. In the demonstration project, examples are provided of the control
initializing with preset zip code and changing the zip code on the fly are both
demonstrated.
The associated download includes both the source of the
control itself and for a demonstration web site. The public US Forecast web
service may be found at this address:
http://www.webservicex.net/WS/WSDetails.aspx?CATID=12&WSID=68
Figure 1: Weather Forecast Custom Control in Use
Getting Started:
The files included with this project include a web
control library project and a demonstration web site. In order to get started,
open the included zip file and install the two projects onto your file system.
Open IIS and create a virtual directory for the web application. Open the
solution into Visual 2005 and make any changes necessary to bring both projects
into the solution. Once properly configured, your solution explorer should show
these projects, references, and files:
Figure 2: Solution Explorer with Web App and Control
Library
In examining the solution, note that the
"WeatherReport" control library contains only a single control and that control
is called "Forecast". This project also includes a web reference that points to
the
http://www.webservicex.net
site; this public site supplies the web service used to capture the US Weather
Forecast information displayed by the control.
The web application contains only a single web page
(default.aspx) and includes a reference to the "WeatherReport" DLL.
The web application serves as a container used to test the custom control; the
default.aspx page contains a single Forecast control along with some controls
used to change the zip code applied to the forecast. The calendar displayed on
the web page is just there for eyewash and the hyperlink will open up a new
window displaying the US Postal Service's Zip Code finder page.
The Code: Forecast
The "Forecast" custom control is constructed to
retrieve the information from the web service upon initialization and to use
that information to display the first seven days of the weather forecast. In
this demo, I maintain the zip code in view state but I resupply the forecast
data each time the control initializes. It would be better to maintain all of
the forecast values in view state and only update them in response to a post
back event after the zip code has been updated. To keep the code short, I opted
not to do that in this demo.
The web service returns the requested data as a class
called WeatherForecasts, and the weather details for each day are each included
as a collection of subordinate classes called WeatherDetails. The
WeatherForecasts object contains the information about the place (city, state,
latitude, longitude, etc.) while WeatherDetails contains the date, minimum and
maximum temperatures (degrees F and degrees C), and the path to the appropriate
weather forecast image.
In examining the code, note that, only the default
imports are included in the project. The class itself inherits from the
WebControl class.
Imports
System
Imports
System.Collections.Generic
Imports
System.ComponentModel
Imports
System.Text
Imports
System.Web
Imports
System.Web.UI
Imports
System.Web.UI.WebControls
Imports
System.Xml
<ToolboxData("<{0}:Forecast
runat=server></{0}:Forecast>")> _
Public
Class Forecast
Inherits WebControl
Following the class declaration, a region entitled "Declarations" is created and
within that region are the declarations for the private member variables used
within the control.
#Region
"Declarations"
Private mForecast As
net.webservicex.www.WeatherForecast
Private WxDetails() As
net.webservicex.www.WeatherData
Private Wx As
net.webservicex.www.WeatherForecasts
#End
Region
After
the variable declarations, there is another region defined (Methods) and within
that region is the code used to capture the data from the web service and
populate the mForecast member variable. The initialization handler calls a
subroutine called "GetWeather" each time the control is initialized. GetWeather
accepts a single argument in the form of a string containing the five digit zip
code.
Inside
GetWeather, mForecast object is defined as new instance of the web services
weather forecast class. From this class, the weather report and weather details
are captured and assigned to the appropriate variables. These variables are used
directly during rendering to define the contents of the control.
The
code contained in the Methods region is as follows:
#Region
"Methods"
Private Sub Forecast_Init(ByVal
sender As Object,
ByVal e As
System.EventArgs)
Handles Me.Init
If Not
String.IsNullOrEmpty(ZipCode) Then
GetWeather(ZipCode)
Else
GetWeather("36201")
End If
End Sub
Public Sub GetWeather(ByVal
zip As String)
Try
mForecast =
New net.webservicex.www.WeatherForecast
Wx =
mForecast.GetWeatherByZipCode(zip)
WxDetails = Wx.Details
Catch
Exit Sub
End Try
End Sub
#End
Region
The next region defined in the code is called
"Properties"; this section contains the properties used by the control. In this
case, aside from what was passed down through the inheritance of the WebControl
class, the only property to define is a string value used to contain the zip
code and this value is stashed in view state.
To make this a little more efficient, it would be
better to stash the weather forecast and weather details into view state or
control state as well.
The properties region and its single property are
defined as follows:
#Region
"Properties"
<Category("Weather"),
Description("Set Forecast Zip Code"),
Browsable(True)> _
Property ZipCode() As
String
Get
Dim s As
String = CStr(ViewState("ZipCode"))
If s Is
Nothing Then
Return String.Empty
Else
Return s
End If
End Get
Set(ByVal Value
As String)
ViewState("ZipCode")
= Value
End Set
End Property
#End
Region
The attributes of category, browsable, and description
are used to provide design time support for the custom control. The category and
description text will be displayed in the IDE's property editor whenever this
control is selected by the developer using the control.
Having captured the values necessary for the control
through the web service, the only thing left to do is to actually render the
control on the page.
The code used to render the control is pretty simple;
the HtmlTextWriter is used to define a table and set up its characteristics
(cell padding in this example), each row of the table contains one cell, within
the cells, text is written out to label the value, and the value itself is
added. Once all of the data has been written into the table, the ending tag is
rendered and the control is complete.
Each section of the table definition is annotated and
empty lines break the table rendering code up into specific sections. If you
follow the annotation and the breaks, you should see how the control is rendered
easily enough. The process following is basically to define a row, add a cell,
add contents to the cell, close the cell, close the row, and move onto the next
row.
Naturally, you can change the configuration of the
table or remove some of the data returned from the web service by making changes
in the definition of the HTML as defined through the HtmlTextWriter. The
RenderContents subroutine is overridden and the HTML is formatted within this
subroutine through the use of the HtmlTextWriter.
If you wanted to make the control more useful, it might
be interesting to build it with a vertical and a horizontal layout option and
use the select case statement in the renderer to lay the table out all on one
row or as I did it in the following (all in one column). It might also be nice
to allow the developer to specify the numbers of days (1 to 7) and use that
value to determine how many days to show in the weather report.
#Region
"Rendering"
Protected Overrides
Sub RenderContents(ByVal
output As HtmlTextWriter)
' the web
service actually returns seven days, I am just using the
' first three
days to make a 3 day forecast but the additional days
' could be
added in a similar manner
Try
' set padding
and start the table
output.AddStyleAttribute(HtmlTextWriterStyle.Padding,
"3")
output.RenderBeginTag(HtmlTextWriterTag.Table)
' display
location information based on zip code
' in first
row of the table
output.RenderBeginTag(HtmlTextWriterTag.Tr)
output.RenderBeginTag(HtmlTextWriterTag.Td)
output.Write("<b>Location:
</b>" & Wx.PlaceName.ToString() & ", "
& _
Wx.StateCode.ToString() &
"<br/>")
output.Write("<b>Zip
Code: </b>" & ZipCode & "<br/>")
output.Write("<b>Lat/Long:
</b>" & Wx.Latitude.ToString() & _
"/" & Wx.Longitude.ToString() &
"<br/>")
output.RenderEndTag()
output.RenderEndTag()
' display
highs and lows for day 1
output.RenderBeginTag(HtmlTextWriterTag.Tr)
output.RenderBeginTag(HtmlTextWriterTag.Td)
output.Write("<hr/>")
output.Write("<b>
Day: </b>" & WxDetails(0).Day.ToString() &
"<br/>")
output.Write("<b>
High/Low: </b>" &
WxDetails(0).MaxTemperatureF.ToString() & _
"/" &
WxDetails(0).MinTemperatureF.ToString() &
"<br/><br/>")
output.RenderEndTag()
output.RenderEndTag()
' get weather
service image and add it to control
output.AddAttribute(HtmlTextWriterAttribute.Align,
"center")
output.RenderBeginTag(HtmlTextWriterTag.Tr)
output.RenderBeginTag(HtmlTextWriterTag.Td)
Dim img As
New Image()
img.ImageUrl =
WxDetails(0).WeatherImage.ToString()
img.BorderStyle =
WebControls.BorderStyle.Inset
img.BorderWidth = 2
img.RenderControl(output)
output.RenderEndTag()
output.RenderEndTag()
' display
highs and lows for day 2
output.RenderBeginTag(HtmlTextWriterTag.Tr)
output.RenderBeginTag(HtmlTextWriterTag.Td)
output.Write("<hr/>")
output.Write("<b>Day:
</b>" & WxDetails(1).Day.ToString() &
"<br/>")
output.Write("<b>High/Low:
</b>" &
WxDetails(1).MaxTemperatureF.ToString() & _
"/" &
WxDetails(1).MinTemperatureF.ToString() &
"<br/><br/>")
output.RenderEndTag()
output.RenderEndTag()
' get weather
service image and add it to control
output.AddAttribute(HtmlTextWriterAttribute.Align,
"center")
output.RenderBeginTag(HtmlTextWriterTag.Tr)
output.RenderBeginTag(HtmlTextWriterTag.Td)
Dim img2 As
New Image()
img2.ImageUrl =
WxDetails(1).WeatherImage.ToString()
img2.BorderStyle =
WebControls.BorderStyle.Inset
img2.BorderWidth = 2
img2.RenderControl(output)
output.RenderEndTag()
output.RenderEndTag()
' display
highs and lows for day 3
output.RenderBeginTag(HtmlTextWriterTag.Tr)
output.RenderBeginTag(HtmlTextWriterTag.Td)
output.Write("<hr/>")
output.Write("<b>Day:
</b>" & WxDetails(2).Day.ToString() &
"<br/>")
output.Write("<b>High/Low:
</b>" &
WxDetails(2).MaxTemperatureF.ToString()
& _
"/" & WxDetails(2).MinTemperatureF.ToString()
& "<br/><br/>")
output.RenderEndTag()
output.RenderEndTag()
' get weather
service image and add it to control
output.AddAttribute(HtmlTextWriterAttribute.Align,
"center")
output.RenderBeginTag(HtmlTextWriterTag.Tr)
output.RenderBeginTag(HtmlTextWriterTag.Td)
Dim img3 As
New Image()
img3.ImageUrl =
WxDetails(2).WeatherImage.ToString()
img3.BorderStyle =
WebControls.BorderStyle.Inset
img3.BorderWidth = 2
img3.RenderControl(output)
output.Write("<br/><br/>")
output.RenderEndTag()
output.RenderEndTag()
' close the
table
output.RenderEndTag()
Catch
' the control
will not render without contacting the web service
' so just
display text if the data is unavailable or the web
' service web
method has not be evoked
output.Write("Weather
Report Control")
End Try
End Sub
#End
Region
The Code: The Demo Site's Default Page
The default.aspx page contained within the demo site
serves only a test container for the control. The page contains a table that is
laid out such that three rows exist in the left hand column while the right hand
column contains one row (three merged cells). In the right hand column, a label
was added and set to display "Your 3-day Forecast". A single copy of the custom
forecast control was dropped beneath the label. The control's zip code property
was set to "36201" which is a valid zip code in the State of Alabama.
On the left hand side of the table, the first cell
contains a textbox and a button used to update the zip code applied to the
custom control. The cell also contains a hyperlink used to open up the US Postal
Service's Zip Code Finder web site. I dropped a calendar control into the middle
cell but it does not serve any useful purpose other than to display the date.
The bottom cell in the left hand column is empty.
Figure 3: Setting the Forecast Control Properties at
Design Time
There is not a lot of code to speak of in the
default.aspx page; the button click event handler used to update the zip code is
the only of interest. In this code, the textbox is checked for content and for
the presence of letters, if the checks are passed, the custom control's zip code
property is updated and the control's public "GetWeather" subroutine is evoked.
Once the zip code property has been changed, the GetWeather subroutine will
force an update of the custom control's weather information and display the new
data in the control.
The click event handler's code is as follows:
Protected
Sub Button1_Click(ByVal
sender As Object,
ByVal e As
System.EventArgs) Handles Button1.Click
If Not
String.IsNullOrEmpty(txtZipCode.Text.ToString())
Then
Try
Dim chr() As
Char = txtZipCode.Text.ToCharArray()
Dim iLoop As
Integer
For iLoop = 0 To chr.Length - 1
If Char.IsLetter(chr(iLoop))
Then
txtZipCode.Text =
"INVALID"
Exit Sub
End If
Next
Forecast1.ZipCode =
txtZipCode.Text
Forecast1.GetWeather(txtZipCode.Text)
Catch ex As Exception
txtZipCode.Text =
"ERROR"
End Try
End If
End
Sub
Summary
This project was intended to describe a useful, easy to
build custom control. While this demonstration was limited to describing the
Forecast custom control, the same approach applied herein would work with a
variety of other custom controls.