Build a C# Stock Quote WebService
and WinForms Client (Reloaded)
By Peter A. Bromberg, Ph.D.

Peter Bromberg

A few days ago, XML MVP and guru extraordinaire Daniel Cazzulino sent me a friendly email about one of my WebService articles where I had basically been concatenating and sending back a string (of Xml) from the webservice. Daniel's point is that this is bad form. A lot of developers do it; but that doesn't make it good programming practice! What Daniel didn't know (and which I pointed out, of course) is that the article was written back in 2001 and I've learned an awful lot about .NET since then. Unfortunately, there are only 24 hours in a day, and although I'd love to go back and refactor all the bad code I've ever written, it's not likely I ever will. However, in the case of this particular webservice, I knew that I could revisit it rather quickly and re-do the code to my (current) level of coding satisfaction. This isn't CRAPWHAT (Constant Refactoring After Programming With Hardly Any Testing), its the satisfaction that you know how to do something right, and taking a few minutes out of your busy day to go ahead and do it.



The issue here is that we now have XmlTextWriter and HtmlTextWriter classes to create well-formed XML and HTML respectively, and developers should take the time to learn to use them. They aren't there to make your life more complicated, they are there to help you write better and more performant code. If you want to persist in your "old ways", I guess I can't help you!

The XmlTextWriter class, which derives from XmlWriter, writes XML to a file, console, stream, and other output types. When writing XML, the methods do extra work for you to produce well-formed XML. Its contructor takes a filename, stream, or TextWriter and has overloads to take an additional parameter that defines the encoding type. The Close method checks to see if the XML is an invalid XML document, and if so, an InvalidOperationException is thrown.

Additionally, XmlTextWriter ensures that the XML elements are written out in the correct order. For example, it won't let you write an attribute outside of an element, write a CDATA block inside an attribute, or write multiple root elements.

Moreover, it ensures that the <?xml declaration comes first and that the <!DOCTYPE node comes before the root element. It ensures that value and format of the xml:space attribute is correct and makes sure that its value is acceptable according to the Extensible Markup Language (XML) 1.0 (Second Edition) recommendation. Finally, XmlTextWriter checks when a string is used as a parameter, for example Null==String.Empty and String.Empty and whether it follows the W3C rules.

Methods in the XmlWriter also come in pairs; the WriteStartDocument and WriteEndDocument, WriteStartElement and WriteEndElement, and the WriteStartAttribute and WriteEndAttribute method pairs. Using these methods you can create nested elements or attributes. It is with these method pairs that an XML document is built and it allows the creation of complex elements or attributes without making errors.

So, true to form and with gratitude to Daniel, from whom I have learned a lot, here is the code for a rewritten Yahoo Stock Quote webservice, using the XmlTextWriter class, and which does another neat trick that I like, returns a DataSet directly from the webservice, making things like consumption in a Windows Forms application about as easy as it can possibly get. First, the code for the Service:

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Web;
using System.Web.Services;
using System.Net;
using System.IO;
using System.Text;
using System.Xml;
namespace YahooMultiQuote
{
 [WebService(Namespace="http://localhost/YahooMultiQuote/")]
 public class Service1 : System.Web.Services.WebService
 {
  public Service1()
  {
   InitializeComponent();    
  }
  #region Component Designer generated code  
  private IContainer components = null;  
  private void InitializeComponent()
  {
  }  
  protected override void Dispose( bool disposing )
  {
   if(disposing && components != null)
   {
    components.Dispose();
   }
   base.Dispose(disposing);  
  }  
  #endregion
  
[WebMethod (Description="Enter Stock Symbol(s) separated by spaces.") ]
public DataSet GetQuotes(string QuoteList)
{
string serverURL=  @"http://finance.yahoo.com/q?s=" + QuoteList+"&d=e";
HttpWebRequest webreq = (HttpWebRequest)WebRequest.Create(serverURL);
webreq.MaximumAutomaticRedirections =60;
webreq.UserAgent="Mozilla/4.0 (compatible; MSIE 6.0;Windows NT 5.0)";
HttpWebResponse webresp = (HttpWebResponse)webreq.GetResponse();     
StreamReader strm = new StreamReader(webresp.GetResponseStream(), Encoding.ASCII);
string res = strm.ReadToEnd();
strm.Close();
int startpos = res.IndexOf("yfnc_tabledata1");
string tmpQuote="";
string[] arSymbols=QuoteList.Split(Convert.ToChar(" "));
MemoryStream ms = new MemoryStream();
XmlTextWriter writer = new XmlTextWriter(ms,Encoding.ASCII); 
writer.WriteStartDocument(true);
writer.WriteStartElement("Quotes");   
 for(int i = 0;i<arSymbols.Length;i++)
 {
  res=res.Substring(startpos);
  startpos=res.IndexOf("<a href=\"/q?");
  res=res.Substring(startpos);
  startpos=res.IndexOf(">"+arSymbols[i].ToUpper()+"</a>");
  res=res.Substring(startpos);
  startpos = res.IndexOf("<b>" );
  res=res.Substring(startpos);
  int endpos = res.IndexOf("</b>");
  tmpQuote=res.Substring(3,endpos-3);
  writer.WriteStartElement("quote");
  writer.WriteAttributeString("Symbol",arSymbols[i]);
  writer.WriteAttributeString("price",tmpQuote);    
  writer.WriteEndElement();        
  startpos=endpos+3;
 }
writer.WriteEndElement();// Quotes
writer.WriteEndDocument(); 
writer.Close(); 
DataSet ds = new DataSet();
ms.Seek(0,0); // rewind it, just in case...
 try
 {
  ds.ReadXml(ms,XmlReadMode.InferSchema);  
 }
 catch(Exception ex)
 {
  System.Diagnostics.Debug.WriteLine(ex.Message+ex.StackTrace);
 }
 return ds;
  } // end GetQuotes
 } // end class
} // end namespace

There! Now that wasn't so bad! Note that I make a WebRequest to http://finance.yahoo.com/q?s=" + QuoteList+"&d=e -- which is one of several ways to get realtime quotes from Yahoo finance, and read out the string of Yahoo's web page from the stream.

Then I just use old fashioned substring searching and positioning to scrape out the prices returned for each of my symbols. (Yes, I've used fancy Regex matching too - probably a little more robust, but when Yahoo changes their HTML I bet you that either method can break).

Then we fire up an XmlTextWriter and iterate over the list, creating an XmlDocument as we go. Note that my writer wraps a MemoryStream, which will make it much easier to read the Xml into the DataSet when I'm done. The rest of the code should be self-explanatory and we send our SOAP-ified DataSet back to the caller, on its merry way, thanks to the gurus at Microsoft.

Now, on the Winforms Client, all I need to do is set a WebReference to my service and use its proxied GetQuotes method. Use ASCII Encoding for stuff like this so you don't get strange UTF-8 byte order mark characters in your stream:

private void button1_Click(object sender, System.EventArgs e)
  {
   YahooMultiQuote.Service1 s= new YahooMultiQuote.Service1();
   s.RequestEncoding = new System.Text.ASCIIEncoding();
   DataSet ds = new DataSet();
      ds=s.GetQuotes(this.textBox1.Text);
   this.dataGrid1.DataSource=ds.Tables[0];
  }

The result on the client, when all this works properly, would look like so:

Does it get any easier than this? Now I feel better! The only thing I have left to do is study Daniel's Mvp.Xml project from sourceforge, which does some really cool things - a lot more sophisticated than the above. You will see there that other luminaries such as Aaron Skonnard, Dimitre Novatchev, and Don_Xml round out the developer list. Thanks, Daniel.

Download the 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: