| "As
for now, I’m
in control here, in the White House..." -- Alexander Haig
Some time not too long ago, NOAA (the "National Weather Service",
among other neat things it does) published a WebService that allows anybody
to get weather forecasts if they care to parse the WSDL and include a
WebReference in their .NET project. There were a number of blog posts
about it and they all had one theme: "I can't get it to work", "It
times out", etc. That's because it's in RPC format (Yecch) which
pretty much reduces one to writing custom code to parse the returned
XML.
Then a couple days ago I happened across Mikhail Arkhipov's blog entry,
where he has done just that - written code to parse out the results.
He had created a nice ASCX User Control for it, and this made me think.
You can look
at Mikhail's blog here.
Not being a big fan of reinventing the wheel, and seeing that somebody
had been kind enough to do some of the heavy lifting, I turned back to
the Weather webservice. The only real remaining problem I saw was that
it requires the Latitude and Longitude as input parameters. Now I don't
know about you, but I know my Zip Code and my phone number, but for the
life of me I can't seem to recall the latitude and longitude of where
I live. Must be having a Senior Moment, I guess.
At any rate, I did think, and I remembered some of my early experiments
with SharpZipLib. One of the neat things I did with it was to export
a Zip Code Database I have as pipe-delimited text, the compress it with
SharpZipLib. I added the resultant file to a control as an embedded resource,
along with enough of the SharpZipLib classes to handle in-memory decompression.
Having extracted the Text file of results from the assembly at runtime,
it was a simple matter to split it into rows and the rows into arrays
and add all this to a DataTable - which now allows you to create a Primary
Key, perform Select queries and all that cool stuff. So I now had a Zipcode
database in an assembly, and I wrote a nice ZipCode lookup component
with the technique.
Remembering my ZipCode database, it also has almost all the Latitude
and Longitudinal coordinates that I've imported for every city in the
U.S. I originally used this for a method to get the distance between
two zip codes, but now I saw that it would be perfect for the Weather
Service! An example web page display follows:
Now here's the code:
using System.Diagnostics;
using ICSharpCode.SharpZipLib;
using System.Collections;
using System.Web.UI.WebControls;
using WeatherControl.gov.weather;
using System.ComponentModel;
namespace WeatherControl
{
[Designer(typeof(System.Web.UI.Design.ControlDesigner)),
ToolboxData("<{0}:WeatherLookup runat= \"server\"></{0}:WeatherLookup>")]
public class WeatherLookup : Control
{
bool IsDesignMode=false;
protected System.Web.UI.WebControls.Table WeatherTable;
private string zipCode;
[Bindable(true),
Description("ZipCode for Weather Lookup"),
Category("Misc")]
public string ZipCode
{
get
{
return zipCode;
}
set
{
zipCode=value;
}
}
#region control derived overrides
protected override void Render(HtmlTextWriter writer)
{
// have to do this, Thank you, MVP Rick Strahl!
if(IsDesignMode) return;
WeatherTable = new Table();
string cacheKey="weatherTable"+zipCode;
if(Context.Cache[ cacheKey]!=null)
{
WeatherTable=(Table)Context.Cache[cacheKey];
WeatherTable.RenderControl(writer);
return;
}
WeatherControl.gov.weather.ndfdXML weatherFetcher = new WeatherControl.gov.weather.ndfdXML();
WeatherControl.gov.weather.weatherParametersType weatherParams =
new WeatherControl.gov.weather.weatherParametersType();
string xmlWeather;
try
{
DayWeatherData[] arrDayWeather;
DateTime dtStart = DateTime.Now ;
if(zipCode==null ||zipCode=="") zipCode="32801";
FindLatitudeLongitudeByZipCode(zipCode);
xmlWeather = weatherFetcher.NDFDgen(latitude, longitude,
productType.glance, dtStart, dtStart.AddDays(7), weatherParams);
arrDayWeather = WeatherXMLParser.ParseWeatherXML(xmlWeather);
TableRow tr1 =new TableRow();
TableCell tc1= new TableCell();
tc1.ColumnSpan=7;
tc1.Text="Weather for " +foundCity +", " +foundState +" " +zipCode.ToString();
tr1.Cells.Add(tc1);
WeatherTable.Rows.Add(tr1);
if(arrDayWeather != null)
{
TableRow tr = new TableRow(); // titles
foreach (DayWeatherData dt in arrDayWeather)
{
TableCell tc = new TableCell();
tc.Wrap =true;
tc.Text ="<font size=2>"+ dt.DateTime.ToLongDateString().Replace(",","<br>") +"</font>";
tr.Cells.Add(tc);
}
tr.HorizontalAlign = HorizontalAlign.Center;
tr.VerticalAlign = VerticalAlign.Top;
WeatherTable.Rows.Add(tr);
tr = new TableRow(); // clouds
foreach(DayWeatherData dt in arrDayWeather)
{
TableCell tc = new TableCell();
tc.Text = "<img src='" + dt.CloudIconURL + "'/>";
tr.Cells.Add(tc);
}
tr.HorizontalAlign = HorizontalAlign.Center;
tr.VerticalAlign = VerticalAlign.Top;
WeatherTable.Rows.Add(tr);
tr = new TableRow(); // highs
foreach (DayWeatherData dt in arrDayWeather)
{
TableCell tc = new TableCell();
tc.Text = "<font size=1>Hi:" +dt.HighTempF +"</font>" ;
tc.Wrap = false;
tr.Cells.Add(tc);
}
tr.HorizontalAlign = HorizontalAlign.Center;
tr.VerticalAlign = VerticalAlign.Top;
WeatherTable.Rows.Add(tr);
tr = new TableRow(); // lows
foreach (DayWeatherData dt in arrDayWeather)
{
TableCell tc = new TableCell();
tc.Text = "<font size=1>Lo: "+dt.LowTempF +"</font>" ;
tc.Wrap = false;
tr.Cells.Add(tc);
}
tr.HorizontalAlign = HorizontalAlign.Center;
tr.VerticalAlign = VerticalAlign.Top;
WeatherTable.Rows.Add(tr);
Context.Cache.Insert(cacheKey,WeatherTable,null,DateTime.MaxValue,
TimeSpan.FromDays(1),System.Web.Caching.CacheItemPriority.Default,null);
Context.Cache[cacheKey]=WeatherTable;
WeatherTable.RenderControl(writer);
}
}
catch(Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message+ex.StackTrace);
}
}
#endregion
#region private fields
private string theZips=String.Empty;
public DataTable zipCodeTable = new DataTable();
private string foundCity;
private string foundState;
private string foundZip;
private decimal latitude;
private decimal longitude;
#endregion
# region Utility Methods
private void FindZipCodeByCityState (string city, string state)
{
string strExpr;
if(state!="")
{
strExpr = "city = '" +city +"' AND state LIKE '%" +state.Trim() + "%'";
}
else
{
strExpr = "city='" + city +"'";
}
// Use the Select method to find all rows matching the filter.
try
{
DataRow[] foundRows =
zipCodeTable.Select(strExpr);
foundZip=foundRows[0].ItemArray[2].ToString();
foundCity=foundRows[0].ItemArray[0].ToString();
foundState=foundRows[0].ItemArray[1].ToString();
}
catch(Exception ex)
{throw new Exception(ex.Message);
}
}
private void FindLatitudeLongitudeByCityState(string city, string state)
{
string strExpr;
if(state!="")
{
strExpr = "city = '" +city +"' AND state LIKE '%" +state.Trim() + "%'";
}
else
{
strExpr = "city='" + city +"'";
}
// Use the Select method to find all rows matching the filter.
try
{
DataRow[] foundRows =
zipCodeTable.Select(strExpr);
this.latitude=Convert.ToDecimal(foundRows[0].ItemArray[3]);
Decimal dec=Convert.ToDecimal(foundRows[0].ItemArray[4]);
dec=dec*-1;
this.longitude =dec;
foundZip=foundRows[0].ItemArray[2].ToString();
foundCity=foundRows[0].ItemArray[0].ToString();
foundState=foundRows[0].ItemArray[1].ToString();
}
catch(Exception ex)
{
throw new Exception(ex.Message);
}
}
private void FindLatitudeLongitudeByZipCode(string zip)
{
string strExpr;
strExpr = "zip='" +zip +"'";
// Use the Select method to find all rows matching the filter.
try
{
DataRow[] foundRows =
zipCodeTable.Select(strExpr);
this.latitude=Convert.ToDecimal(foundRows[0].ItemArray[3]);
Decimal dec=Convert.ToDecimal(foundRows[0].ItemArray[4]);
dec=dec*-1;
this.longitude =dec;
foundZip=foundRows[0].ItemArray[2].ToString();
foundCity=foundRows[0].ItemArray[0].ToString();
foundState=foundRows[0].ItemArray[1].ToString();
}
catch(Exception ex)
{
throw new Exception(ex.Message);
}
}
private void FindCityStateByZipCode (string zip)
{
string strExpr;
strExpr = "zip='" +zip +"'";
// Use the Select method to find all rows matching the filter.
try
{
DataRow[] foundRows =
zipCodeTable.Select(strExpr);
foundZip=foundRows[0].ItemArray[2].ToString();
foundCity=foundRows[0].ItemArray[0].ToString();
foundState=foundRows[0].ItemArray[1].ToString();
}
catch(Exception ex)
{ throw new Exception(ex.Message);
}
}
#endregion
#region ctor
public WeatherLookup()
{
this.IsDesignMode = (System.Web.HttpContext.Current == null);
if(IsDesignMode)return;
if(System.Web.HttpContext.Current.Cache["ZipCodeTable"]==null)
{
theZips = GetDecompressedResourceString("WeatherControl.Zipcodes.dat");
zipCodeTable.Columns.Add( "city", typeof(string) );
zipCodeTable.Columns.Add( "state", typeof(string) );
zipCodeTable.Columns.Add( "zip", typeof(string) );
zipCodeTable.Columns.Add( "Latitude", typeof(string) );
zipCodeTable.Columns.Add( "Longitude", typeof(string) );
// Set PrimaryKey
zipCodeTable.Columns[ "zip" ].Unique = true;
zipCodeTable.PrimaryKey =
new DataColumn[] { zipCodeTable.Columns["zip"] };
string[] zippies = theZips.Split(new Char[] {'\n'});
for (int i=0;i<zippies.Length;i++)
{
object[] theRow=zippies[i].Split(new Char[] {'|'});
zipCodeTable.Rows.Add( theRow);
}
zipCodeTable.AcceptChanges();
System.Web.HttpContext.Current.Cache["ZipCodeTable"]=zipCodeTable;
}
else
zipCodeTable =
(DataTable)System.Web.HttpContext.Current.Cache["ZipCodeTable"];
}
#endregion
#region Compression Methods
private string GetDecompressedResourceString(string resource)
{
try
{
Assembly asm = Assembly.GetExecutingAssembly();
Stream stm=asm.GetManifestResourceStream(resource);
BinaryReader br = new BinaryReader(stm);
long siz = stm.Length;
byte[] bytInput =null;
bytInput=br.ReadBytes((int)siz);
string theResource=Decompress(bytInput);
br.Close();
stm.Close();
return theResource;
}
catch(Exception ex)
{
throw new ApplicationException(ex.Message);
}
}
private string Decompress(byte[] bytInput)
{
string strResult="";
int totalLength = 0;
byte[] writeData = new byte[4096];
Stream s2 =
new ICSharpCode.SharpZipLib.Zip.Compression.Streams.InflaterInputStream(new MemoryStream(bytInput));
try
{
while (true)
{
int size = s2.Read(writeData, 0, writeData.Length);
if (size > 0)
{
totalLength += size;
strResult+=System.Text.Encoding.UTF8.GetString(writeData, 0,
size);
}
else
{
break;
}
}
s2.Close();
return strResult;
}
catch(Exception e)
{
throw new Exception(e.ToString());
}
}
#endregion
}
}
| You can see above that not only do
I cache the ZipCode table so there is only one "hit" when the control is first instantiated, I also cache the result HTML Output with a unique cache key that
includes the zipcode. The result is a control that renders pretty fast. The only holdup is the NOAA's weather webservice, which can occasionally be slow.
When you drag the built control from the toolbox to your webform designer, it has only one settable property - the zipcode. My code takes care of all
the rest!
One of the little "gotchas" I had to solve was getting
an object reference error when dragging one of these from the Toolbox onto
my WebForm. I spent some time checking over my code for other controls I've
written, and nothing seemed to be wrong. The only thing the MSDN documentation
said about this was something about malformed server tags, that was not my
issue. Finally I found the answer at fellow MVP Rick Strahl's blog. It seems
that if you have a server control that does anything at design time that it
would not be able to do without there being a current HttpContext, you will
get this error. You can see my comment in the source code above. Enjoy.
Download
the Visual Studio.NET Solution that accompanies this article
|