The NOAA WebService Weather Forecast ServerControl
by Peter A. Bromberg, Ph.D.

Peter Bromberg

"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

 



Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.
Article Discussion: