Note: This article is submitted by David Hyde for Silverlight: Write and Win contest. Thanks a lot, David! Hello All, Please drop a comment if you like it.
“It is time to vote now! Please, choose your favorite articles and enter your vote by going to contest page. Thank you!„
Introduction
As Silverlight approaches its official version 2 release, the hype for Microsoft’s powerful web development platform is certainly building. Through the many beta versions there have been countless changes, so some developers have shied from fully embracing Silverlight. Silverlight has many powers that are not often explored, and in this past week I’ve created this comprehensive tutorial to pull together a series of interesting findings on the subtleties of Silverlight 2. We will walk through creating a Stock Portfolio Manager – very much a real-world application – and in turn get up to speed on everything this amazing tool has to offer.
Download StockPortfolio.zip – This is the completed solution, and worked perfectly at the time I packaged it up. Let me know if you have any trouble with it. See also the live demo up and running here. Please note that you will need to manually instal Silverlight 2 RC0 - at least the runtime - to view the samples. You have to use the link in "Prerequisities" becasue the Silverlight download button that shows up does not work properly for this release :)
Prerequisites
For this article I’ll be using Silverlight 2 RC0 – the latest release at the moment. You can download all the runtimes, SDKs, and service packs you may need from here. And while you don’t need Expression Blend 2 to do Silverlight development, it sure helps me when designing!
Step #1 – Setting up the User Interface
To make sense of any modern application, we need a decent interface. Our Stock Portfolio Manager will not be too complicated, but instead we’ll try to make it look as readable and clean as possible. As we are not aiming to sell this product, we can sacrifice a shade of style for a lot more understandability on the developer’s end. There’s no special trick to setting up a Silverlight project in Visual Studio or designing the UI in raw XAML or in Blend, so I will go ahead and layout what the final code of our application’s main view – Page.xaml – will look like:
<UserControl x:Class="StockPortfolio.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="800" Height="600"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid x:Name="LayoutRoot" Background="White">
<TextBlock HorizontalAlignment="Left" Margin="8,8,0,0" VerticalAlignment="Top" TextWrapping="Wrap" Width="292.055">
<Run FontSize="24" Text="Stock Portfolio Manager" />
</TextBlock>
<Button Height="44" HorizontalAlignment="Left" Margin="8,48.8790016174316,0,0" x:Name="AddButton" VerticalAlignment="Top"
Width="178" Content="Add Current Stock" Click="AddButton_Click" />
<Button Height="44" HorizontalAlignment="Left" Margin="8,96.8789978027344,0,0" VerticalAlignment="Top" Width="178"
Content="Remove Selected Stock" x:Name="RemoveButton" Click="RemoveButton_Click" />
<ListBox HorizontalAlignment="Left" Margin="8,169,0,48" VerticalAlignment="Stretch" Width="60"
RenderTransformOrigin="0.5,0.5" x:Name="ListBox1">
<ListBox.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="1" />
<SkewTransform />
<RotateTransform />
<TranslateTransform />
</TransformGroup>
</ListBox.RenderTransform>
</ListBox>
<TextBlock Height="24.121" HorizontalAlignment="Left" Margin="8,144.878997802734,0,0" VerticalAlignment="Top" Width="131"
Text="Current Portfolio:" TextWrapping="Wrap" d:LayoutOverrides="Height" />
<TextBlock Height="25" HorizontalAlignment="Left" Margin="8,0,0,19" VerticalAlignment="Bottom" Width="277.371"
Text="Total Portfolio Value: " TextWrapping="Wrap" d:LayoutOverrides="Width" x:Name="PortfolioValueBlock" />
<Canvas HorizontalAlignment="Stretch" Margin="190,48.8790016174316,8,48" VerticalAlignment="Stretch">
<TextBox Height="35.121" Width="259" Canvas.Left="273" Canvas.Top="26.879" Text="" TextWrapping="Wrap"
d:LayoutOverrides="Height" x:Name="TickerBox" />
<TextBlock Height="34.758" Width="261" Canvas.Left="8" Canvas.Top="36"
Text="Lookup Stock Info By Ticker Symbol:" TextWrapping="Wrap" />
<Button Height="50" Width="48" Content="Go" Canvas.Top="20.758" Canvas.Left="536" x:Name="GoButton"
Click="GoButton_Click" />
<Button Height="30" Width="156" Content="Download This Portfolio" Canvas.Top="513" Canvas.Left="444"
RenderTransformOrigin="0.479999989271164,0.261000007390976" x:Name="SaveButton" Click="SaveButton_Click" />
<TextBlock Height="420.363" Width="419" Canvas.Left="165" Canvas.Top="74.758" Text="" TextWrapping="Wrap"
x:Name="QuoteBox" Foreground="#FFEA0A0A" />
</Canvas>
<ListBox HorizontalAlignment="Left" Margin="71,169,0,48" VerticalAlignment="Stretch" Width="60"
RenderTransformOrigin="0.5,0.5" x:Name="ListBox2">
<ListBox.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="1" />
<SkewTransform />
<RotateTransform />
<TranslateTransform />
</TransformGroup>
</ListBox.RenderTransform>
</ListBox>
<ListBox HorizontalAlignment="Left" Margin="135,169,0,48" VerticalAlignment="Stretch" Width="60"
RenderTransformOrigin="0.5,0.5" x:Name="ListBox3">
<ListBox.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="1" />
<SkewTransform />
<RotateTransform />
<TranslateTransform />
</TransformGroup>
</ListBox.RenderTransform>
</ListBox>
</Grid>
</UserControl>
and a picture of what this results in:
Basically all this XAML does is set up a header, and underneath it provide Add/Remove buttons for the “Current Portfolio” list boxes which list out the number of shares of a given company at a given price, respectively. In order to determine the company and price of the shares, we will be using some nifty web service techniques behind that TextBox and Go button you see. Finally, we will add the ability for the user of our manager to download the completed stock portfolio.
Step #2 - Show Me the Data!
While we could use a random number generator to come up with stock prices – and certainly freak out a few paranoid investors in the process – it might be somewhat more practical if we use real-world market info. Web services are definitely the way to go when it comes to accomplishing tasks like this. For our Stock Portfolio Manager, I’ve used the Stock Quote service by webservicex.net. If you follow the link and try out their raw demo, you’ll notice the nice XML document you can get filled with quite a bit of stock information. Here’s an example of what we’ll be dealing with:
<string>
<StockQuotes>
<Stock>
<Symbol>MSFT</Symbol>
<Last>27.40</Last>
<Date>9/26/2008</Date>
<Time>4:01pm</Time>
<Change>+0.79</Change>
<Open>26.23</Open>
<High>27.56</High>
<Low>26.14</Low>
<Volume>100744280</Volume>
<MktCap>250.2B</MktCap>
<PreviousClose>26.61</PreviousClose>
<PercentageChange>+2.97%</PercentageChange>
<AnnRange>23.50 - 37.50</AnnRange>
<Earns>1.867</Earns>
<P-E>14.25</P-E>
<Name>MICROSOFT CP</Name>
</Stock>
</StockQuotes>
</string>
We will harness this in Silverlight, but indeed will stray from the traditional process involving Service References, etc.. When possible to do so, I have found it simpler to use the WebClient class. You may already be familiar with this from other areas of .NET – it exists in every incarnation I can think of. If you don’t know about it, here’s the one sentence definition that we’ll be interested in: it downloads web pages. In the Click event handler for the Go button, we will insert the following:
Private Sub GoButton_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
AddHandler wc.DownloadStringCompleted, AddressOf DownloadCompleted
wc.DownloadStringAsync(New Uri("http://www.webservicex.net/stockquote.asmx/GetQuote?symbol=" & TickerBox.Text), QuoteXML)
End Sub
What this code accomplishes is the following. It adds a handler for the DownloadStringCompleted event of our instance variable wc (of WebClient). It then calls the asynchronous DownloadStringAsync method. This line is where we retrieve the stock data, so look closely! We simply use the GET access for the Stock Quote web service, passing as a parameter whatever ticker is in the TickerBox TextBox. When we call this method, it downloads the resulting XML as a string and assigns it to another instance variable QuoteXML. Look at that – with Silverlight, it takes two lines of code to check the stock market!
Step #3 – Much Ado about XML
We now have an XML document, which is a good start. But obviously we’re going to need to take our application at least a little beyond that level. In the last step you saw that we had an event handler that pointed to the DownloadCompleted method. It is there where we are going to process, format, and display the information. The most painless way to do this is by using the good old XmlReader class. We can use this class to iterate through all the XML elements and come up with some user-friendly text. Here is the completed method. It’s fairly well-commented in itself, so hopefully you can pick up on what’s going on:
Private Sub DownloadCompleted(ByVal sender As Object, ByVal e As System.Net.DownloadStringCompletedEventArgs)
'Make sure all the <'s, >'s, etc. are formatted properly
QuoteXML = System.Windows.Browser.HttpUtility.HtmlDecode(e.Result)
'Set up XmlReader
Dim reader As XmlReader = XmlReader.Create(New StringReader(QuoteXML))
'Make sure that QuoteBox is empty
QuoteBox.Text = ""
Dim LastName As String = ""
Do While (reader.Read())
Select Case reader.NodeType
Case XmlNodeType.Element 'The opening tag of an element
'Make sure we're not reading one of the opening "value-less" tags...
If Not reader.Name = "string" And Not reader.Name = "Stock"
And Not reader.Name = "StockQuotes" Then
'Add element name to QuoteBox
QuoteBox.Text += (reader.Name + ": ")
LastName = reader.Name
End If
Case XmlNodeType.Text 'The value of an element (in between opening/closing tags)
'Add the value of an element to the QuoteBox
QuoteBox.Text += (reader.Value + Environment.NewLine)
If LastName = "Symbol" Then
CurrentTicker = reader.Value
End If
If LastName = "Last" Then
CurrentPrice = reader.Value
End If
End Select
Loop
reader.Close()
End Sub
As you may have guessed from this sample, we take the element names and their values and stick them (formatted) into a TextBox called QuoteBox. Running this code, QuoteBox ends up containing something similar to the following:
You could add on to this by displaying it in some visually appealing way, but I think that is a bit beyond the scope of this article. For now we are just concerned about displaying the data, and we have done a dandy job of that! On to the next step…
Step #4 – Simple Modal Dialog – Really!
Sorry, Microsoft et. all – Silverlight isn’t absolutely perfect :) This is the first of two deficiencies we’re going to explore in this article. Imagine that in our Stock Portfolio Manager we wanted to create a modal dialog that prompts the user how many shares of a particular stock he wants to add/remove from his portfolio. Well, that’s exactly what I was attempting to stick into this example. But wouldn’t you know it – there was no simple ShowDialog method like in Windows Forms. I did a quick search and read up on this issue, and it seems like a common problem. So, I set to work and discovered a surprisingly simple solution that I don’t think anybody has realized yet! I first designed the prompt that I wanted in AddRemovePrompt.xaml. I’ll show you the code-behind as well, and then discuss what on Earth I’m messing with!
<UserControl x:Class="StockPortfolio.AddRemovePrompt"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid x:Name="LayoutRoot" Background="White">
<Rectangle Stroke="#FF000000">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FF000000" />
<GradientStop Color="#FF383E86" Offset="1" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Button HorizontalAlignment="Right" Margin="0,0,7.808,8" VerticalAlignment="Bottom" Width="82"
Content="Cancel" d:LayoutOverrides="Height" x:Name="CancelButton" Click="CancelButton_Click" />
<Button HorizontalAlignment="Right" Margin="0,0,94,8" VerticalAlignment="Bottom" Width="82"
Content="OK" x:Name="OKButton" Click="OKButton_Click" />
<TextBlock Height="34" Margin="59,98,71,0" VerticalAlignment="Top" Text="How many shares would you like to "
TextWrapping="Wrap" Foreground="#FFFFFFFF" x:Name="PromptLabel" />
<TextBox Margin="99,136,113,139" Text="" TextWrapping="Wrap" x:Name="SharesBox" />
</Grid>
</UserControl>
Partial Public Class AddRemovePrompt
Inherits UserControl
Private thePage As Page
Public Sub New(ByVal AddOrRemove As String, ByRef aPage As Page)
thePage = aPage
InitializeComponent()
If AddOrRemove = "add" Then
PromptLabel.Text += "add?"
Else
PromptLabel.Text += "remove?"
End If
End Sub
Private Sub CancelButton_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
Me.Visibility = Windows.Visibility.Collapsed
For Each uie As UIElement In thePage.LayoutRoot.Children
uie.IsHitTestVisible = True
Next
End Sub
Private Sub OKButton_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
If Not SharesBox.Text = Nothing Then
If PromptLabel.Text.Contains("add") Then
thePage.HandleAdd(CInt(SharesBox.Text))
Else
thePage.HandleRemove(CInt(SharesBox.Text))
End If
Me.Visibility = Windows.Visibility.Collapsed
For Each uie As UIElement In thePage.LayoutRoot.Children
uie.IsHitTestVisible = True
Next
End If
End Sub
End Class
So, here we go. For the record, this is why you comment code – so you don’t have to type out lengthy explanations later! The constructor of our dialog class takes two parameters: a string that’s either “add” or “remove”, and a reference (note the ByRef usage) to the parent page stored in the class variable thePage. We have a dynamic TextBlock which, depending on the value of the AddOrRemove parameter, can ask either type of question without having anything confusing like two TextBlocks. In the CancelButton_Click method we see the core trick that makes the dialog “modal.” Before we create the dialog and show it, we iterate through the controls in Page (via the core LayoutRoot control) and set the IsHitTestable to false. This is the closest equivalent in this instance to the Enabled property, as it prevents the user from doing anything else until the application re-enables hit testing. That, you can see, is done whenever the dialog is closed (by OK or Cancel). Thus we have achieved a simple alternative to a modal dialog box in Silverlight! We’ll now look at what HandleAdd and HandleRemove do in Page.xaml.vb, and show the final source code for that file.
Step #5 – Making A List, Checking it Twice…
And you thought that song was about Santa Claus! Anyway… as I mentioned earlier, the three ListBoxes in our Stock Portfolio Manager represent how many shares (ListBox1) of what stock (ListBox2) at what price (ListBox3). I thought it would be nice to have three separate ListBoxes for simplicity and clarity in this example, though it’s certainly possible to have just one ListBox display all the data. So, as promised, here are the HandleAdd and HandleRemove methods… gee, I wonder what they do?
Friend Sub HandleAdd(ByVal Shares As Integer)
For i As Integer = 0 To ListBox2.Items.Count - 1
If ListBox2.Items(i).Equals(CurrentTicker) Then
ListBox1.Items(i) += Shares
RecalculateWorth()
Return
End If
Next
ListBox1.Items.Add(Shares)
ListBox2.Items.Add(CurrentTicker)
ListBox3.Items.Add(CurrentPrice)
RecalculateWorth()
End Sub
Friend Sub HandleRemove(ByVal Shares As Integer)
Dim index As Integer
For i As Integer = 0 To ListBox2.Items.Count - 1
If ListBox2.Items(i).Equals(CurrentTicker) Then
index = i
End If
Next
If ListBox1.Items(index) > Shares Then
ListBox1.Items(index) -= Shares
'ListBox1.Items(index) = Shares
Else
ListBox1.Items.RemoveAt(index)
ListBox2.Items.RemoveAt(index)
ListBox3.Items.RemoveAt(index)
End If
RecalculateWorth()
End Sub
It’s really nice how simple the ListBox is to work with. Instead of having to say some expression like CInt(ListBox1.Items(i).Content), Silverlight just “knows” what type each ListBoxItem is and allows you to just use statements like ListBox1.Items(i) += Shares. Fun stuff! Now we have just about everything put together. Here is the final source code of Page.xaml.vb:
Imports System.IO
Partial Public Class Page
Inherits UserControl
Private QuoteXML, CurrentTicker, CurrentPrice As String
Private WithEvents wc As New WebClient()
Public Sub New()
InitializeComponent()
End Sub
Private Sub AddButton_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
For Each uie As UIElement In LayoutRoot.Children
uie.IsHitTestVisible = False
Next
Dim arp As New AddRemovePrompt("add", Me)
arp.Visibility = Windows.Visibility.Visible
arp.Width = 400
arp.Height = 300
LayoutRoot.Children.Add(arp)
End Sub
Private Sub DownloadCompleted(ByVal sender As Object, ByVal e As System.Net.DownloadStringCompletedEventArgs)
'Make sure all the <'s, >'s, etc. are formatted properly
QuoteXML = System.Windows.Browser.HttpUtility.HtmlDecode(e.Result)
'Set up XmlReader
Dim reader As XmlReader = XmlReader.Create(New StringReader(QuoteXML))
'Make sure that QuoteBox is empty
QuoteBox.Text = ""
Dim LastName As String = ""
Do While (reader.Read())
Select Case reader.NodeType
Case XmlNodeType.Element 'The opening tag of an element
'Make sure we're not reading one of the opening "value-less" tags...
If Not reader.Name = "string" And Not reader.Name = "Stock" And Not reader.Name = "StockQuotes" Then
'Add element name to QuoteBox
QuoteBox.Text += (reader.Name + ": ")
LastName = reader.Name
End If
Case XmlNodeType.Text 'The value of an element (in between opening/closing tags)
'Add the value of an element to the QuoteBox
QuoteBox.Text += (reader.Value + Environment.NewLine)
If LastName = "Symbol" Then
CurrentTicker = reader.Value
End If
If LastName = "Last" Then
CurrentPrice = reader.Value
End If
End Select
Loop
reader.Close()
End Sub
Private Sub RemoveButton_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
For Each uie As UIElement In LayoutRoot.Children
uie.IsHitTestVisible = False
Next
Dim arp As New AddRemovePrompt("remove", Me)
arp.Visibility = Windows.Visibility.Visible
arp.Width = 400
arp.Height = 300
LayoutRoot.Children.Add(arp)
End Sub
Friend Sub HandleAdd(ByVal Shares As Integer)
For i As Integer = 0 To ListBox2.Items.Count - 1
If ListBox2.Items(i).Equals(CurrentTicker) Then
ListBox1.Items(i) += Shares
RecalculateWorth()
Return
End If
Next
ListBox1.Items.Add(Shares)
ListBox2.Items.Add(CurrentTicker)
ListBox3.Items.Add(CurrentPrice)
RecalculateWorth()
End Sub
Friend Sub HandleRemove(ByVal Shares As Integer)
Dim index As Integer
For i As Integer = 0 To ListBox2.Items.Count - 1
If ListBox2.Items(i).Equals(CurrentTicker) Then
index = i
End If
Next
If ListBox1.Items(index) > Shares Then
ListBox1.Items(index) -= Shares
'ListBox1.Items(index) = Shares
Else
ListBox1.Items.RemoveAt(index)
ListBox2.Items.RemoveAt(index)
ListBox3.Items.RemoveAt(index)
End If
RecalculateWorth()
End Sub
Private Sub RecalculateWorth()
Dim Worth As Decimal
For i As Integer = 0 To ListBox1.Items.Count - 1
Dim shares As Decimal = ListBox1.Items(i)
Dim price As Decimal = ListBox3.Items(i)
Dim value As Decimal = shares * price
Worth += value
Next
PortfolioValueBlock.Text = "Total Portfolio Value: $" & Worth.ToString()
End Sub
Private Sub GoButton_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
AddHandler wc.DownloadStringCompleted, AddressOf DownloadCompleted
wc.DownloadStringAsync(New Uri("http://www.webservicex.net/stockquote.asmx/GetQuote?symbol="
& TickerBox.Text), QuoteXML)
End Sub
Private Sub SaveButton_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
Try
Dim data As String = ""
For i As Integer = 0 To ListBox1.Items.Count - 1
data += ListBox1.Items(i).ToString()
data += " "
data += ListBox2.Items(i).ToString()
data += " "
data += ListBox3.Items(i).ToString()
data += "^"
Next
Browser.HtmlPage.Window.Navigate(New Uri("DownloadPortfolio.ashx?data=" & Browser.HttpUtility.HtmlEncode(data)
& "&total=" & PortfolioValueBlock.Text, UriKind.Relative))
Catch ex As Exception
Browser.HtmlPage.Window.Alert(ex.Message)
End Try
End Sub
End Class
Step #6 – One More Thing…
Remember that pesky old “Download this Portfolio” Button we added? well, you can see the code of its Click event above. It loads all the information from the lists into a string, along with the portfolio's total worth. Then it uses that as a query string, passing it to an .ashx page. Here’s the code of that, which is the final part of our project.
<%@ WebHandler Language="VB" Class="DownloadPortfolio" %>
Imports System
Imports System.Web
Public Class DownloadPortfolio : Implements IHttpHandler
Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
Dim response As HttpResponse = context.Response
Dim request As HttpRequest = context.Request
Dim server As HttpServerUtility = context.Server
Dim data As String = HttpUtility.HtmlDecode(request.QueryString("data"))
response.Clear()
response.ContentType = "text/plain"
response.Write(data.Replace("^", Environment.NewLine))
Dim total As String = request.QueryString("total")
response.Write(Environment.NewLine & Environment.NewLine & total)
End Sub
Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
Get
Return False
End Get
End Property
End Class
All this does, as you can see, is properly format the query string that was passed to it, then output that as plain text to the user. Silverlight provides no built-in SaveFileDialog or anything – this is the other problem I noticed while building this project. Before you complain, as I was about to, people have already discussed this. Microsoft has withheld this feature due to “security risks.” Oh well. This is a good way to bypass that!
Summary
There you have it: a great little Stock Portfolio Manager. I always like to leave a little room for improvement. You could, for example, make sure the stock prices are updated every so often automatically. You could also provide a way so that txt files can be parsed and loaded as portfolios. Or, as mentioned, you could style up this poor app somewhat. Still, all the basics are there. This has been a really thorough investigation into Silverlight's inner workings, and I hope you’ve enjoyed the ride. Silverlight is such a great new tool, and there are a million different ways to use it! Most importantly, have fun in your development, and make some cool applications. Until next time, see you later!