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-day 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 re-supply 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)
Try
output.AddStyleAttribute(HtmlTextWriterStyle.Padding, "3")
output.RenderBeginTag(HtmlTextWriterTag.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()
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()
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()
output.RenderBeginTag(HtmlTextWriterTag.Tr)
output.RenderBeginTag(HtmlTextWriterTag.Td)
output.Write("<hr/>")
output.Write("<b>Day: </b>" & _
WxDetails(1).Day.ToString() & _
"<br/>")
output.Write("&;lt;b>High/Low: </b>" & _
WxDetails(1).MaxTemperatureF.ToString() & _
"/" & WxDetails(1).MinTemperatureF.ToString() & _
"<br/><br/>")
output.RenderEndTag()
output.RenderEndTag()
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()
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()
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()
output.RenderEndTag()
Catch
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.