This article is compatible with the latest version of Silverlight for Windows Phone 7.
Watch video accompanying this article!
This article is Part 3 of the series WP7 Stock Quote Demo.
Part 2 showed how we built our UX for our Stock quoting application now we will add the data access that I want to hide behind an interface so that I can change providers easily. This must be accomplished without using MEF as sadly MEF is not available for the phone as Reflection.Emit was omitted from the CLR for the Phone. Another design goal is have this data access occur on a background thread as we must issue WebClient calls to obtain the real data for the Quote objects. The Background thread will use MVVMLight’s Messaging to communicate between the threads, I could have just as easily used native events . We add an Anonymous delegate to our ViewModel that will accept QuoteDTOs as they are received from the Background thread
Overall the process looks like:
The first step is to add the delegate to the ViewModel to receive the DTO’s so we change the constructor for the ViewModel to look like:
public MainViewModel()
{
quotesList = new ObservableCollection<Quote>();
if (DesignerProperties.IsInDesignTool)
return;
// start the retrieval engine
quoteSuppier = new StreamStockInfo();
//using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetUserStoreForApplication())
//{
// isoStore.DeleteFile("myfile.xml");
//}
quotesList = IsolatedStorageCacheManager<ObservableCollection<Quote>>.Retrieve("myfile.xml");
//IsolatedStorageCacheManager will return null if there is no file containing the serialized quotes
if (quotesList == null)
{
quotesList = new ObservableCollection<Quote>();
// quotesList.Add(new Quote() { Symbol = "^dji" });
// IsolatedStorageCacheManager<ObservableCollection<Quote>>.Store("myfile.xml", quotesList);
}
if (quotesList.Count > 0)
quoteSuppier.AddSymbol(quotesList);
//this delegate will be called by the dataaccess layer when it has data to display
Messenger.Default.Register<List<QuoteDTO>>(this, true,
q =>
{
q.ForEach(dto =>
{
quotesList.ToList().ForEach(model =>
{
if (dto.Symbol.Contains(model.Symbol.ToUpper()))
{
model.Time = dto.Time;
model.Trade = dto.Trade;
model.Volume = dto.Volume;
model.Ask = dto.Ask;
model.Bid = dto.Bid;
model.DayHi = dto.DayHi;
model.DayLow = dto.DayLow;
model.YrHi = dto.YrHi;
model.YrLow = dto.YrLow;
}
});
});
}
);
}
As the DTOs’s are received from the background thread we hydrate the equivalent Quote models thus calling the property setters which results in the databinding to the listview we created in part 2. Before we setup the background thread lets add a new project that adds a new Class Library called DataAccess to our solution.
To this project I add a new class called getStockInfo that initially only contains the definition of the interface that I will use to hide the data access called IStockInfo:
public interface IStockInfo
{
List<QuoteDTO> GetQuote(string ListOfSymbols);
void SendConfigInfo(string QuoteInfo);
}
Now the implementation of GetQuote will return a list of quotes using the input list of symbols To stage our solution I will add a Mock provider so we do not go live immediately by adding the MockQuotes class to getStockInfo:
public class MockQuotes:IStockInfo
{
double i = 0;
#region IStockInfo Members
public List<QuoteDTO> GetQuote(string ListOfSymbols)
{
Random ran = new Random();
return new List<QuoteDTO>()
{
new QuoteDTO(){ Symbol="msft", Time = DateTime.Now, Trade=25.0 + ran.Next(1,2)},
new QuoteDTO(){ Symbol="aapl", Time = DateTime.Now,Trade=250.0},
new QuoteDTO(){ Symbol="c", Time = DateTime.Now,Trade=5.0},
new QuoteDTO(){ Symbol="bac", Time = DateTime.Now, Trade=25.0},
new QuoteDTO(){ Symbol="ibm", Time = DateTime.Now,Trade=250.0},
new QuoteDTO(){ Symbol="csco", Time = DateTime.Now,Trade=30.0}
};
}
so we are just returning random values for the Trade amount to simulate the up (and mostly down) stock values. These are returned in a QuoteDTO object that the Quote model but does not need properties that implement INotifyPropertyChanged. It looks like:
public class QuoteDTO
{
public string Symbol { get; set; }
public DateTime Time { get; set; }
public Double Trade { get; set; }
public Double Volume { get; set; }
public Double Bid { get; set; }
public Double Ask { get; set; }
public Double DayHi { get; set; }
public Double DayLow { get; set; }
public Double YrHi { get; set; }
public Double YrLow { get; set; }
This mock provider will get us started and now we look at supplying the background thread that will use this interface and provider.
Add the StreamStockInfo class that will create the background thread that will actually get the quotes from the provider. The constructor starts a Timer delegate (GetData) that will pop every 3 seconds and retrieve the quote information from the current provider. Now the timer will run in a background thread allocated from the CLR threadpool thus we do not tie up the UI thread while retrieving the data.
Timer t = null;
public StreamStockInfo()
{
t = new Timer(new TimerCallback(GetData), App.Current.RootVisual.Dispatcher, 0, 3000);
}
We now add the Timer method that will operate on the background thread.
object Mylock = new object();
string ListOfSymbols = string.Empty;
Dispatcher mainThread;
public IStockInfo TheModel { get; set; }
//The method does not execute on the thread that created the timer; it executes on a ThreadPool thread supplied by the system.
void GetData(object state)
{
//this line of code will establish the provider of the quote info
TheModel = new MockQuotes();
mainThread = state as Dispatcher;
Monitor.Enter(Mylock);
if (ListOfSymbols != string.Empty)
{
List<QuoteDTO> quotes = TheModel.GetQuote(ListOfSymbols);
if (quotes != null && quotes.Count > 0)
mainThread.BeginInvoke(() => Messenger.Default.Send<List<QuoteDTO>>(quotes));
}
Monitor.Exit(Mylock);
Debug.WriteLine("Timer exit");
}
Note that it takes as a parameter a reference to the Dispatcher object so we can communicate back to the UI thread. As mentioned above the actual retrieval of the stock info is abstracted behind an interface and we instantiated the provider in this line.
TheModel = new MockQuotes();
This is one way of getting by the lack of MEF or even Unity for Dependency Injection. I only have to change this line of code to switch providers. It is not perfect but it works and is simple.I could have added a Config file setting to make this solution complete.
Finally we add the code to do the actual retrieval of the quote information:
List<QuoteDTO> quotes = TheModel.GetQuote(ListOfSymbols);
When the results are returned, the results are sent to the UI thread using Messaging.
This is where we use the Dispatcher that we passed in as input so the messaging call executes on the UI thread:
mainThread.BeginInvoke(() => Messenger.Default.Send<List<QuoteDTO>>(quotes));
We also need a way to get the symbol that we entered into the above ListofSymbols so we add a new method to StreamStockInfo called AddSymbol (Note the use of a Monitor to protect the shared list)
public void AddSymbol(ObservableCollection<Quote> quotes)
{
Monitor.Enter(Mylock);
string temp = string.Empty;
quotes.ToList().ForEach(q => temp += q.Symbol + "+");
ListOfSymbols = temp.TrimEnd('+');
Monitor.Exit(Mylock);
}
We then add 2 lines of code to the ICommand handler, GetSymbol in the ViewModel:
IsolatedStorageCacheManager<ObservableCollection<Quote>>.Store("myfile.xml", quotesList);
quoteSuppier.AddSymbol(quotesList);
Note that the quotesList is persisted to the IsolatedStorage using a class that I borrowed from Syed Mehroz Alam at http://smehrozalam.wordpress.com/2009/02/10/saving-data-objects-to-isolated-storage-in-silverlight/ . So now we can enter one of the symbols in our Mock list such as MSFT and see the value of the stock bounce between 24 and 27.
Now to show that my provider model works I switch to getting real quotes from Yahoo by adding the following class to GetStockInfo.cs
public class YahooQuotes : IStockInfo
{
#region IStockInfo Members
private AutoResetEvent m_autoResetEvent = new AutoResetEvent(false);
List<QuoteDTO> quotes = null;
public List<QuoteDTO> GetQuote(string ListOfSymbols)
{
WebClient yahoo = new WebClient();
string url = "http://quote.yahoo.com/d/quotes.csv?s=" + ListOfSymbols + "&d=t&f=sl1d1t1abghjkv";
yahoo.DownloadStringAsync(new Uri(url));
yahoo.DownloadStringCompleted += new DownloadStringCompletedEventHandler(yahoo_DownloadStringCompleted);
Debug.WriteLine("GetQuote ");
m_autoResetEvent.WaitOne();
return quotes;
}
void yahoo_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
if (e.Error == null)
quotes = ParseCSV((string)e.Result);
m_autoResetEvent.Set();
}
The real implementation is YahooQuotes. Here I wanted the GetQuote methodto be synchronous which is a little difficult given the asynchronous nature of Silverlight I/O. However I came up with solution based on an article by Benjamin Day that relies on the fact that we are running on a background thread and there is no danger in making this thread wait for the I/O as we are not tying up the UI thread. It makes use of AutoResetEvent CLR object to have the GetQuote thread wait for the asynchronous thread to complete.
The code makes a web request to Yahoo, which will return quote info for the symbols in the ListOfSymbols string . This data is returned as a CSV file. The details of the parameters in the request can be found at http://cliffngan.net/a/13 .
The “f” parameter controls what quote information is returned I added the ParseCSV method to hydrate the QuoteDTO objects from the csv file that looks like:
List<QuoteDTO> ParseCSV(string csvFile)
{
List<QuoteDTO> quotesinCSV = null;
quotesinCSV = (from c in csvFile
let quoteRecord = c.Split(',')
select new QuoteDTO()
{
Symbol = quoteRecord[0],
Trade = ConvertDouble(quoteRecord[1]),
Ask = ConvertDouble(quoteRecord[5]),
Bid = ConvertDouble(quoteRecord[4]),
Time = ConvertTime(quoteRecord[3]),
DayLow = ConvertDouble(quoteRecord[6]),
DayHi = ConvertDouble(quoteRecord[7]),
YrLow = ConvertDouble(quoteRecord[8]),
YrHi = ConvertDouble(quoteRecord[9]),
Volume = ConvertDouble(quoteRecord[10])
}).ToList();
return quotesinCSV;
}
The key line in the Linq is the Let that will parse the input string into an array of strings that we then assign to properties of the QuoteDTO object.
This example is little more complex then most UX need to be for a phone application as we would never release an app that would eat up our battery like this one would. However it demonstrates that the WP7 phone architecture contains a wonderful set of the CLR that combined with Silverlight gives you an incredible platform to deliver apps with.
The next series I will look at the developing a Push service where we move the Yahoo code to a WCF service.