Introduction
As tablets and SmartPhones become more popular, developers face increased demand to write applications that can run on those devices as well as on
desktops. HTML5 and JavaScript can help developers achieve this goal with a single code base.
Until recently, writing applications using JavaScript and HTML was quite difficult, because developers had to create all the business logic, the user
interface, and then connect all these elements using JavaScript code and the HTML document object model. By comparison, writing web applications in
Silverlight was much easier, because the logic and the user interface could be implemented largely independently, and later connected using declarative bindings.
This situation started to change with the introduction of JavaScript libraries
such as jQuery, jQuery UI, and KnockoutJS. These libraries introduce standard
ways to manipulate the HTML document (jQuery), to add user interface widgets
to HTML documents (jQueryUI), and to bind the business logic to the UI
declaratively (KnockoutJS).
In parallel with the introduction of these popular JavaScript libraries, most
popular browsers are gradually adding support for HTML5 features such as local
storage, geo-location, rich media, and others. These features also contribute
to make the HTML/JavaScript combination a great platform for web applications.
This article describes the implementation of Invexplorer, a stock
portfolio application similar to Google finance. The application was written
in HTML5 and JavaScript, using the libraries mentioned above.
To run the application in your browser, click
here.
Users can add stocks to the portfolio using a smart auto-complete textbox. If
they type "ford mot" for example, the textbox will show a list with two symbols
that correspond to the "Ford Motor Company". They can then select one of the
symbols and click the "Add to Portfolio" button. Once items have been added to
the portfolio, users can chart their price history by clicking a checkbox, and
can edit the trading information by typing directly into the grid.
Portfolios are
automatically persisted to local storage. When users open the application,
their portfolio is loaded automatically.
The application follows the MVVM pattern. The view model classes were
implemented using the KnockoutJS library. They can be tested independently
of the view, and could be re-used in case we wanted to create new views for
tablets or phones. Properties are implemented as KnockoutJS "observable" and
"observableArray
" objects, which allows them to be used as sources and targets
in KnockoutJS bindings.
The view was implemented using HTML5 and two custom controls: an auto-search
box from the jQueryUI library and a chart control from the Wijmo library.
Both libraries have extensions that support KnockoutJS and both can be used
simply by including the appropriate links in your HTML pages (no
download/install is required).
Shortly before this article was finished, CodeProject published an excellent
article by Colin Eberhardt entitled "KnockoutJS vs. Silverlight". Colin
focuses on KnockoutJS, and uses a simple sample that requires no custom
controls. The article is excellent - definitely recommended reading. You
can find it
here.
If you would like to see a comparison between Silverlight and KnockoutJS
implementations of the Invexplorer application, along with a quick introduction
to JavaScript and MVVM, you can find that
here.
The latest revision of this article added a TypeScript version of the Invexplorer application. The TypeScript version is discussed in the last section of the article.
Important disclaimer:
Invexplorer is a sample application designed to show how to use KnockoutJS and custom controls. It uses financial data from Yahoo Finance,
which is not a free service. If you would like to use the code provided as a basis for actual applications, you must contact Yahoo or some other financial data provider to obtain the
licenses required.
Very Brief Introduction to MVVM
The MVVM pattern (Model/View/ViewModel) was introduced by Microsoft as a
variation of the more traditional MVC pattern (Model/View/Controller).
MVVM encapsulates the application logic in a set of ViewModel classes that
expose an object model that is View-friendly. Views typically focus on the
user interface, and rely on bindings to connect UI elements to properties
and methods in the ViewModel. This separation between logic and markup
brings the following important benefits:
- Testability: The ViewModel does not contain any user interface elements,
and is therefore easy to test using unit tests.
- Separation of Concerns: Business logic is written using programming
languages such as C# or JavaScript. User interfaces are written using markup
languages such as XAML or HTML. The skills required and tools used for each
type of development are fundamentally different. Separating these elements
makes team development simpler and more efficient.
- Multi-Target Applications: Encapsulating an application's business
logic into ViewModel classes makes it easier to develop several versions of
an application, targeting multiple devices. For example, one could develop
a single ViewModel and different views for desktop, tablet, and phone
devices.
The diagram below illustrates the relationship between elements in MVVM
applications (the diagram was taken from a good MVVM introduction that
can be found
here.):
Very Brief Introduction to KnockoutJS
KnockoutJS enables MVVM development by providing two main elements:
- JavaScript classes such as observable and observableArray, which are used to
implement ViewModel variables that issue notifications when their value changes
(similar to
INotifyPropertyChanged
in .NET development). - HTML markup extensions that refer to these observables and automatically
update the page when their values change. The markup extensions are very rich.
In addition to showing values such as numbers and strings, they can be used
to customize styles, to enable or disable UI elements, and to elements that
represent collections such as lists, grids, or charts. The markup extensions are
similar to Binding objects in XAML development.
To develop applications using KnockoutJS, you start by creating ViewModel
classes that contain the application logic and expose an object model
built of observable objects. These classes can be tested before moving
on to the next step, which is creating the View.
The View is created using HTML and css. The only difference is the addition
of markup extensions such as "data-bind: text" (to show static content) or
"data-bind: value" (to create two-way bindings in elements such as text
boxes).
Finally, the ViewModel and the View are connected with a single call to the
KnockoutJS applyBindings
method, which takes the object model as a parameter
and actually builds the bindings.
This is KnockoutJS in a nutshell. In my opinion, this conceptual simplicity
is one of the library's main strengths. But there's a lot more to it. The
official KnockoutJS site has many samples, tutorials, and great documentation.
You can get all the details at knockoutjs.com.
The Invexplorer Sample Application
The Invexplorer application was loosely based on the Google Finance site.
It allows users to select stocks and add them to portfolios. Once a stock is
added to the portfolio, the application retrieves historical price data and
shows the evolution of the prices on a chart. The user may select which stocks
should be charted and over what period. The user may also enter the prices he
paid for the stocks and the amount purchased. If this information is provided,
the application calculates the return on each item.
The portfolio information is persisted so when users quit the application so
their edits are not lost and can be re-loaded automatically when the
application runs again.
The original version of the Invexplorer application was written in
Silverlight, using the MVVM pattern (you can see the Silverlight version
here).
When KnockoutJS was introduced, we thought it would be an ideal candidate
for a port to HTML5/JavaScript.
Porting the application took only a couple of days. Most of the work went
into re-writing the view model classes in JavaScript, using the KnockoutJS
library to implement properties as observable objects. Writing the view
was very easy, because the controls we needed were easy to find: the
auto-search box used to select new stocks is part of the jQueryUI
library, and the chart we used is part of the Wijmo library. Both control
libraries support KnockoutJS, and both are stable, powerful, and
easy-to-use.
View Model Implementation (JavaScript)
The class diagram below shows the classes that implement the view model:
The main properties in each class are described below:
ViewModel
class main properties:
-
companies
: An array of Company
objects that contains the complete list of ticker symbols,
company names, and historical price data. The list is populated with symbols and names
when the model is created, and the price data is retrieved on demand. -
portfolio
: A Portfolio object that contains a list of portfolio items. -
chartSeries
, chartStyles
: Two observableArray
objects that contain data prepared for
display on a line chart control. The chartSeries
array contains price variations rather
than absolute values, so series for different companies can be compared easily. The
chartStyles
array contains CSS style definitions used to specify the color to be used
for each data series. -
minDate
: An observable date object that defines the starting date for the chart. This
allows users to compare different stocks over specific periods such as this year, last
12 months, etc. -
updating
, chartVisible
: Two observable objects that expose the state of the view model
and enable the view to provide user feedback while data is being loaded and to hide
chart-related elements when the chart is empty.
Company
class main properties:
-
symbol
: The trading symbol for this company. -
name
: The full company name. -
prices
: An observable containing an array of date/price objects. Note that this is
not an observableArray
object; rather it is a regular observable object whose value
is a regular array. This array is filled on demand, one time per company. -
chartSeries
: An observable containing a custom object used to define a single series
for the chart control. This custom object specifies the series x and y values as well
as the series label, markers, and whether it should be displayed on the chart legend.
This array is created and refreshed when the prices array is filled and also when the
minDate
property of the parent ViewModel changes value.
Portfolio
class main properties:
-
items
: An observableArray
containing PortfolioItem
objects. Each portfolio item refers
to a company, and may include transaction data such as the number of shares purchased
and the purchase price. -
newSymbol
: An observable containing a stock symbol to be added to the portfolio. This
value is bound to an auto-search textbox in the View which allows users to select stocks
they want to add to the portfolio. -
canAddNewSymbol
: An observable containing a Boolean value that determines whether the
newSymbol
property currently contains a valid symbol that is not already included in
the current portfolio. This variable is used to enable or disable the UI element used
to add new items to the items collection.
PortfolioItem
class main properties:
-
symbol
: Trading symbol of the company that this item represents. This value is used as
a key into the ViewModel’s companies array. -
company
: The Company object that contains the company information including name and
price history. -
lastPrice
, change
: These are observable objects that are initially set to null, and
are updated when the price history for the company becomes available. -
shares
, unitCost
: These are observable objects that can be edited by the user. These
values represent the user transactions, and are used to calculate the cost of each
portfolio item. These values belong to the portfolio item itself. -
chart
: An observable Boolean variable that determines whether this item should be
included in the chart. -
value
, cost
, gain
, etc: Several computed objects that provide values that are
calculated based on other properties. For example, value = shares * lastPrice.
The constructor of the ViewModel
class is implemented as follows:
function ViewModel() {
var self = this;
this.companies = [];
this.updating = ko.observable(0);
this.minDate = ko.observable(null);
this.chartSeries = ko.observable([]);
this.chartStyles = ko.observable([]);
this.chartHoverStyles = ko.observable([]);
this.chartVisible = ko.observable(false);
this.setMinDate(6);
this.minDate.subscribe(function () { self.updateChartData() });
this.portfolio = new Portfolio(this);
this.palette = ["#FFBE00", "#C8C800", …];
The first part of the constructor declares the properties that were described above.
Notice that the minDate
property is an observable and the ViewModel
class
subscribes to it. When the value of the minDate
property changes, the
ViewModel
calls updateChartData
so the chart is re-generated to reflect the date range requested
by the user.
The palette property contains an array with colors used to create the chart series for each
item. The colors are also displayed in the grid, which acts as a legend for the chart.
Adding this type of purely UI-related elements to ViewModel classes is fairly common.
After all, ViewModels exist to drive views (that is why they called "ViewModels", and
not just "Models").
Once the properties have been declared, the constructor populates the
companies
array as
follows:
$.get("StockInfo.ashx", function (result) {
var lines = result.split("\r");
for (var i = 0; i < lines.length; i++) {
var items = lines[i].split("\t");
if (items.length == 2) {
var c = new Company(self, $.trim(items[0]), $.trim(items[1]));
self.companies.push(c);
}
}
self.portfolio.loadItems();
});
The code uses the jQuery get
method to invoke a service called "StockInfo.ashx", which
is part of the Invexplorer application. The service executes asynchronously and returns
list of company symbols and names which is parsed and added to the companies
array.
.NET Developers: Notice the use of the "self" variable to access the ViewModel
class from within local functions. In this scope, the variable this refers to the inner
function itself, not to the ViewModel. This is a common JavaScript technique.
After the company data has been loaded, the constructor calls the portfolio’s
loadItems
method to load the last saved portfolio from local storage. In order for this to work,
the portfolio must be saved when the user closes the application. This is done in the
last block of the constructor:
$(window).unload(function () {
self.portfolio.saveItems();
});
}
This code uses jQuery to connect to the window’s unload
event, which is called when the
user closes the application. At this point, the portfolio’s saveItems
method is called
and the current portfolio is saved to local storage.
The saveItems
and loadItems
methods are part of the
Portfolio
class. Before we show their
implementation, here is the Portfolio
constructor:
function Portfolio(viewModel) {
var self = this;
this.viewModel = viewModel;
this.items = ko.observableArray([]);
this.newSymbol = ko.observable("");
this.newSymbol.subscribe(function () { self.newSymbolChanged() });
this.canAddSymbol = ko.observable(false);
}
The constructor keeps a reference to the parent ViewModel, creates the items
observable array that will contain the portfolio items, and declares a newSymbol
observable. The newSymbol
property contains the symbol of a company to be added
to the portfolio.
The constructor
subscribes to changes in the newSymbol
property so whenever its value
changes, the newSymbolChanged
method is called. The newSymbolChanged
method in turn
sets the value of the canAddSymbol
property to true if the new symbol is valid and
does not correspond to any of the items already in the portfolio. The newSymbol
and
canAddSymbol
properties are used by the view to allow users to add items to the portfolio.
And here are the methods that save and load portfolio items from local storage:
Portfolio.prototype.saveItems = function () {
if (localStorage != null) {
var items = [];
for (var i = 0; i < this.items().length; i++) {
var item = this.items()[i];
var newItem = {
symbol: item.symbol,
chart: item.chart(),
shares: item.shares(),
unitCost: item.unitCost()
};
items.push(newItem);
}
localStorage["items"] = JSON.stringify(items);
}
}
.NET Developers: Notice the use of the Portfolio.prototype.saveItems
syntax
used to define the method. The "prototype
" keyword attaches the method to every instance
of the Portfolio
class. This a common way to implement objects in JavaScript.
The method starts by checking that the localStorage
object is defined. This is an HTML5
feature available in all modern browsers. Then it builds an array containing the
information that should be persisted for each portfolio item (symbol, chart, shares, and
unitCost). Finally, the array is converted into a string using the JSON.stringify
method
and saved to storage under the key "items".
The loadItems
method reads this information back from local storage:
Portfolio.prototype.loadItems = function () {
var items = localStorage != null ? localStorage["items"] : null;
if (items != null) {
try {
items = JSON.parse(items);
for (var i = 0; i < items.length; i++) {
var item = items[i];
this.addItem(item.symbol, item.chart, item.shares, item.unitCost);
}
}
catch (err) {
}
}
if (this.items().length == 0) {
this.addItem("AMZN", false, 100, 200);
this.addItem("YHOO", false, 100, 15);
}
}
The method starts by retrieving the "items" string from localStorage
. It uses the
JSON.parse
method to convert the string into a JavaScript array object and then loops
through the items in the array calling the addItem
method on each one.
.NET Developers: The JSON.stringify
and JSON.parse
methods are the
JavaScript analogous of .NET serializers. The provide a simple way to convert objects
into and from strings.
The addItem
method is implemented as follows:
Portfolio.prototype.addItem = function (symbol, chart, shares, unitCost) {
var item = new PortfolioItem(this, symbol, chart, shares, unitCost);
this.items.push(item);
}
The PortfolioItem
class represents items in the portfolio. Here is the constructor:
function PortfolioItem(portfolio, symbol, chart, shares, unitCost) {
var self = this;
this.portfolio = portfolio;
this.symbol = symbol;
this.lastPrice = ko.observable(null);
this.change = ko.observable(null);
this.shares = ko.observable(shares == null ? 0 : shares);
this.unitCost = ko.observable(unitCost == null ? 0 : unitCost);
this.chart = ko.observable(chart == null ? false : chart);
this.shares.subscribe(function () { self.parametersChanged() });
this.unitCost.subscribe(function () { self.parametersChanged() });
this.chart.subscribe(function () { self.updateChartData() });
this.company = portfolio.viewModel.findCompany(symbol);
if (this.company != null) {
this.company.prices.subscribe(function () { self.pricesChanged() });
this.pricesChanged();
this.company.updatePrices();
}
this.name = ko.computed(function() {…}, this);
this.value = ko.computed(function () {…}, this);
this.cost = ko.computed(function () {…}, this);
this.gain = ko.computed(function () {…}, this);
this.color = ko.computed(this.getColor, this);
this.updateChartData();
this.parametersChanged();
}
The constructor starts by storing a reference to the parent portfolio and to the company
symbol. Then it declares the lastPrice
and change
properties, two observables which will
be obtained later from the web service.
Next, the constructor declares the properties that can be changed by the user:
chart
,
unitCost
, and shares
. Notice how the constructor also subscribed to these observables
so when the user edits any of these values, the portfolio item invokes the methods needed
to update the item parameters or the chart data.
The pricesChanged
method, invoked when the price history for the item’s company becomes
available, is implemented as follows:
PortfolioItem.prototype.pricesChanged = function () {
var prices = this.company.prices();
if (prices.length > 1) {
this.lastPrice(prices[0].price);
this.change(prices[0].price - prices[1].price);
if (this.chart()) {
this.updateChartData();
}
}
}
The method retrieves the price history from the company.prices
property. If the price
history is already available, then the code updates the value of the lastPrice
and
change
properties. If the item is currently configured to appear on the chart, the method calls
updateChartData
, which calls the updateChartData
method on the
ViewModel
class:
PortfolioItem.prototype.updateChartData = function () {
var vm = this.portfolio.viewModel;
vm.updateChartData();
}
Here is the implementation of the updateChartData
method:
ViewModel.prototype.updateChartData = function () {
var seriesList = [], stylesList = [], hoverStylesList = [];
var items = this.portfolio.items();
for (var i = 0; i < items.length; i++) {
var item = items[i];
if (item.chart()) {
var series = item.company.updateChartData();
seriesList.push(series);
var style = { stroke: item.getColor(), 'stroke-width': 2 };
stylesList.push(style);
var hoverStyle = { stroke: item.getColor(), 'stroke-width': 4 };
hoverStylesList.push(hoverStyle);
}
}
this.chartVisible(seriesList.length > 0);
this.chartStyles(stylesList);
this.chartHoverStyles(hoverStylesList);
this.chartSeries(seriesList);
}
The updateChartData
data method loops through the portfolio items. For each item with
the chart property set to true, the code creates a new chart series, a new chart style,
and a new chart 'hover style'. All these objects are added to arrays.
When the loop is complete, the arrays are used to update the chartStyles
,
chartHoverStyles
, and chartSeries
properties. These are all observable
properties bound to the chart control in the view.
The series
variable contains the actual data to be plotted on the chart. It is
calculated by the updateChartData
method of the Company
class:
Company.prototype.updateChartData = function () {
var xData = [], yData = [];
var prices = this.prices();
for (var i = 0; i < prices.length; i++) {
if (prices[i].day < this.viewModel.minDate()) {
break;
}
xData.push(prices[i].day);
yData.push(prices[i].price);
}
var baseValue = yData[yData.length - 1];
for (var i = 0; i < yData.length; i++) {
yData[i] = yData[i] / baseValue - 1;
}
var series = {
data: { x: xData, y: yData },
label: this.symbol,
legendEntry: false,
markers: { visible: false }
};
return series;
}
The code converts the historical prices into percentage changes, making it easier to
compare the performance of multiple companies shown on the same chart. The values
calculated are wrapped into a "series" object that contains properties expected by the
Wijmo chart control that will be used to show the data.
The last interesting method in our ViewModel
class is the one that loads the historical
price data. This method is given below:
Company.prototype.updatePrices = function () {
var self = this;
if (self.prices().length == 0) {
var vm = self.viewModel;
vm.updating(vm.updating() + 1);
$.get("StockInfo.ashx?symbol=" + self.symbol, function (result) {
vm.updating(vm.updating() - 1);
var newPrices = [];
var lines = result.split("\r");
for (var i = 0; i < lines.length; i++) {
var items = lines[i].split("\t");
if (items.length == 2) {
var day = new Date($.trim(items[0]));
var price = $.trim(items[1]) * 1;
var item = { day: day, price: price };
newPrices.push(item);
}
}
self.prices(newPrices);
self.updateChartData();
});
} else {
self.updateChartData();
}
}
The method starts by checking whether the prices have already been loaded for this company.
If they haven’t, the method increments the updating
property (so the UI can that there is
download activity going on).
The method then calls the "StockInfo.ashx" service again, this time passing the stock symbol
as a parameter. When the service returns, the updating
property is decremented and the value
returned is parsed into a newPrices
array. Finally, the method updates the value of the prices
property and updates the chart data.
View Implementation (HTML/CSS)
The View is implemented in the default.html file. The file starts with a block of include
statements which load the libraries used by the application. This is similar to adding
references in .NET projects. The Invexplorer application uses the following libraries:
- KnockoutJS: data binding for JavaScript applications.
- jQuery: utilities for manipulating the DOM and calling web services.
- jQueryUI: UI controls including the auto-search box.
- Wijmo: UI controls including the line chart control.
- Knockout-jQuery and Knockout-Wijmo: libraries that add KnockoutJS support to the jQuery
and Wijmo control libraries.
- ViewModel: the Invexplorer view model classes described above.
The Knockout-jQuery library used in this project was written by Mike Edmunds and
is available from
github.
The Knockout-Wijmo library is included with Wijmo and can be included directly from the
Wijmo CDN.
In addition to these includes, the view includes a small script block that does two things:
- Instantiates the
ViewModel
and applies the bindings (using the KnockoutJS
applyBindings
method) - Configures the jQueryUI
autoComplete
control to show HTML in the dropdown list instead of
plain text. This is an optional step, not related to the Invexplorer application. It
allows us to highlight partial matches in the auto-complete list.
<script type="text/javascript">
$(function () {
var vm = new ViewModel();
ko.applyBindings(vm);
$("#autoComplete").autocomplete().data("autocomplete")._renderItem =
function (ul, item) {
return $("<li></li>")
.data("item.autocomplete", item)
.append("<a>" + item.label + "</a>")
.appendTo(ul);
};
});
</script>
The body of the default.html page starts with a header that displays a title and some information about the application. Below the header comes the portfolio table, which
is a standard HTML table element defined as follows:
<table>
<thead>
<tr>
<th class="left">Name</th>
<th class="left">Symbol</th>
<th class="left">Chart</th>
<th class="right">Last Price</th>
<th class="right">Change</th>
<th class="right">Change %</th>
<th class="right">Shares</th>
<th class="right">Unit Cost</th>
<th class="right">Value</th>
<th class="right">Gain</th>
<th class="right">Gain %</th>
<th class="center">Delete</th>
</tr>
</thead>
The table header simply specifies the header for each column on the table.
The interesting part is the table body, which uses KnockoutJS bindings as follows:
<tbody data-bind="foreach: portfolio.items">
<tr>
<td>
<span data-bind="style: { backgroundColor: color }">
</span>
<span data-bind="text: name"></span></td>
<td data-bind="text: symbol"></td>
<td class="center">
<input data-bind="checked: chart" type="checkbox" /></td>
<td class="right" data-bind="text: Globalize.format(lastPrice(), 'n2')"></td>
<td class="right" data-bind="text: Globalize.format(change(), 'n2'),
style: { color: $root.getAmountColor(change()) }"></td>
<td class="right" data-bind="text: Globalize.format(changePercent(), 'p2'),
style: { color: $root.getAmountColor(changePercent()) }"></td>
<td><input class="numeric" data-bind="value: shares" /></td>
<td><input class="numeric" data-bind="value: unitCost" /></td>
<td class="right" data-bind="text: Globalize.format(value(), 'n2')"></td>
<td class="right" data-bind="text: Globalize.format(gain(), 'n2'),
style: { color: $root.getAmountColor(gain()) }"></td>
<td class="right" data-bind="text: Globalize.format(gainPercent(), 'p2'),
style: { color: $root.getAmountColor(gainPercent()) }"></td>
<td class="center">
<a class="hlink" data-bind="click: $root.portfolio.removeItem">x</a></td>
</tr>
</tbody>
</table>
The tbody
element specifies the data source for the table. In this case, the source
is the portfolio items property. Below the tbody
element, we specify a single row (tr
element) with several cells (td
elements). Each cell contains a
data-bind
attribute that
specifies the data it will display and in some cases the formatting and the color to be
used for showing the cells.
The most common binding is "data-bind: text", which specifies the content of the cell.
The content can be any JavaScript expression, including calls to the Globalize library.
Similarly, the "data-bind: value
" binding is used to display editable values in text boxes.
Another common binding is "data-bind: style", which allows you to specify
CSS elements
to be used when rendering the cell. The table above uses style bindings to show positive
amounts in green and negative amounts in red. This is done with a call to the
getAmountColor
method, which plays the role of a binding converter in XAML.
Finally, the "data-bind: click" is used to create a column with buttons that can be used
to remove items from the portfolio. The click event is bound to the portfolio.removeItem
method, which is invoked and automatically receives a parameter that specifies the item
that was clicked.
Building HTML tables with KnockoutJS is very similar to building data grids in XAML.
Below the portfolio table comes the section that allows users to add items to the
portfolio. This is implemented using a jQueryUI auto-complete control and a regular HTML
button:
<div class="addSymbol">
Add Symbol:
<input id="autoComplete" type="text" data-bind="
value: portfolio.newSymbol,
jqueryui: {
widget: 'autocomplete',
options: {
/* require two characters to start matching */
minLength: 2,
/* use ViewModel's getSymbolMatches to populate drop-down */
source: function(request, response) {
response(getSymbolMatches(request))
},
/* update current portfolio's newSymbol property when drop-down closes */
close: function() {
portfolio.newSymbol(this.value)
}
}
}" />
<button data-bind="
click: function() { portfolio.addNewSymbol()},
enable: portfolio.canAddNewSymbol">
Add to Portfolio
</button>
<span class="floatRight" data-bind="visible: updating">
<i> getting data...</i>
</span>
</div>
The input element gets the auto-complete behavior from the jQueryUI library. We use
data binding to specify the list of valid choices and the action to be taken when the
user makes a selection.
The source
option specifies that the list of valid choices will be provided by the
getSymbolMatches
method of the ViewModel
class. This method takes the input provided
by the user (for example "gen mot") and returns a list of companies that have those
terms in their name or symbol (in this case, a match would be "General Motors"). The
values returned are HTML, so matches are highlighted in the auto-complete drop-down.
The close
option specifies a method that is invoked when the user picks an item from the
list. In this case, the method sets the value of the portfolio’s newSymbol
property.
Recall that setting this value will automatically update the value of the
canAddNewSymbol
property, which is used in the next binding.
Binding controls involves setting options and property values in HTML. This is similar to
setting property values in XAML.
Next to the input element there is a button with two bindings: the click binding invokes
the addNewSymbol
method in the portfolio class; the enable binding ensures the button is
enabled only when the canAddNewSymbol
property is set to true (which happens when a symbol
is selected and if this symbol is not yet included in the portfolio). These bindings play
the role of the ICommand
interface in XAML.
The last element in this section is a "getting data" message with a visible binding that
ensures the message is visible only while the ViewModel
is downloading some data.
The next section contains commands used to select the time span shown on the chart:
<div data-bind="visible: chartVisible">
<a class="hlink" data-bind="click: function() { setMinDate(6) }">6m</a>
<a class="hlink" data-bind="click: function() { setMinDate(0) }">YTD</a>
<a class="hlink" data-bind="click: function() { setMinDate(12) }">1y</a>
<a class="hlink" data-bind="click: function() { setMinDate(24) }">2y</a>
<a class="hlink" data-bind="click: function() { setMinDate(36) }">3y</a>
<a class="hlink" data-bind="click: function() { setMinDate(1000) }">All</a>
</div>
The whole section has a visible binding that ensures it is displayed only if the
chart is currently visible. Within the section, there are links with click bindings
that invoke the setMinDate
method in the ViewModel
and pass the desired time span as
a parameter.
The final part of the view is the chart control, implemented as follows:
<div id="chart" data-bind="
wijlinechart: {
/* bind series, styles */
seriesList: chartSeries,
seriesStyles: chartStyles,
seriesHoverStyles: chartHoverStyles,
/* axis label formats */
axis: {
y: { annoFormatString : 'p0' },
x: { annoFormatString : 'dd-MMM-yy' }
},
/* series tooltip */
hint: {
content: function() {
return this.label + ' on ' +
Globalize.format(this.x, 'dd-MMM-yy') + ':\n' +
Globalize.format(this.y, 'p2');
}
},
/* other properties */
animation: { enabled: false },
seriesTransition: { enabled : false },
showChartLabels: false,
width: 800, height: 250,
}">
The data-bind attribute is used to specify the chart properties we need. Recall that
the seriesList
, seriesStyles
, and seriesHoverStyles
are properties
implemented by the ViewModel and tracked by KnockoutJS. Whenever any of these properties
change, the chart is refreshed automatically.
The data-bind attribute also initializes chart properties that are not bound, the same
way you would set control properties in XAML. In this case, the code sets the format for
the axis annotations, adds a tooltip that shows the current symbol, date, and value as
the user moves the mouse over the chart, disables animations, and so on.
WebService Implementation (C#)
Recall that our view model classes use a "StockInfo.ashx" service to retrieve company
names and historical price data. This service is part of the Invexplorer application.
It is a "Generic Handler" implemented as follows:
public class StockInfo : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
string symbol = context.Request["symbol"];
string content = string.IsNullOrEmpty(symbol)
? GetSymbols()
: GetPrices(symbol);
context.Response.ContentType = "text/plain";
context.Response.Write(content);
}
The service checks to see if the request has a "symbol" parameter. If it does, the
content is obtained by calling the GetPrices
method. Otherwise, the content is obtained
by calling GetSymbols
. Both methods return strings containing the information requested,
with line breaks between items and tabs between values.
The GetSymbols
method simply reads a resource file that contains the company names and
symbols. The GetPrices
method calls Yahoo finance services as the Silverlight application
did. We will not show the implementation of those methods here, but they are included in
the source code in case you want to check them out. Remember that the Yahoo financial
data is not free; if you want to use it in commercial application, you will need to
contact Yahoo to obtain a license.
The services return tab-separated items instead of JSON to reduce the size of the download.
Each of these calls returns thousands of items, and parsing them in this case is very easy.
Conclusion
HTML5 and JavaScript have come a long way over the last couple of years. First, jQuery
brought browser-independence and easy DOM manipulation. At the same time, new browsers
started to support HTML5 features such as geo-location, isolated storage, and the flexible
canvas element. Then KnockoutJS and other similar libraries made it easy to separate the
HTML (View) from the JavaScript (ViewModel). This separation makes creating and maintaining
JavaScript applications much easier.
Finally, some popular control libraries have added support for KnockoutJS, making
development in HTML5 and JavaScript about as easy as it is in Silverlight.
In my opinion, the main pieces still missing on the HTML5/JavaScript stack are:
- A business-ready data layer. Silverlight has had this for a long time in the form of
RIA services. JavaScript still does not have it, but hopefully that will change in the
near future.
- Better support for extending and creating re-usable custom controls (including layout
elements such as the XAML Grid element).
- More advanced development tools, with built-in error-checking, refactoring support,
and IntelliSense.
What does all this mean? Is the HTML/JavaScript platform ready to replace Silverlight?
In my opinion, the answer is it depends on the complexity of the application and on
whether the application must be able to run on tablets and phones.
The Invexplorer application is relatively simple. It does not require database updates,
validation, or complex data types. The page layout is also simple. This makes it an ideal
candidate for an HTML5/JavaScript implementation.
TypeScript Version
The second revision of the Invexlorer application incorporates TypeScript (http://www.typescriptlang.org/).
TypeScript is an open source project headed by Anders Hejlsberg. TypeScript adds optional types, classes, and modules to JavaScript. It compiles to readable, standards-based JavaScript. In addition to these great extensions, the TypeScript compiler integrates with Visual Studio and provides automatic compilation, static error-checking, and IntelliSense. It goes a long way towards fixing the limitations listed in item 3 in the previous section.
TypeScript is still quite new, but it is already popular. In fact, there are already at least two CodeProject articles that describe it very nicely: Getting Started With TypeScript and An Introduction to TypeScript.
You can download and install TypeScript from CodePlex at http://typescript.codeplex.com/.
Note that after installing it, you may have to run the vsix file manually in order to complete the installation (I spent a few hours figuring this out). The vsix can usually be found here:
c:\Program Files (x86)\Microsoft SDKs\TypeScript\0.8.0.0\TypeScriptLanguageService.vsix
Once you've installed it, you can create new TypeScript projects in Visual Studio using the File | New Project menu, then selecting the Visual C# node and picking the "HTML Application with TypeScript" option (not very intuitive).
Converting the original ViewModel from plain JavaScript to TypeScript was very easy. The process consisted of the following steps:
1) Breaking up the original "js" file into several "ts" files (one per class)
2) Adding external variable declarations to the top of each "ts" file. These declarations instruct the compiler to ignore names defined in external files. The Invexplorer project requires these declarations:
declare var $;
declare var ko;
declare var Globalize;
3) Adding "reference path" statements that allow the TypeScript compiler to find objects defined in other files within the same project. For example, our ViewModel class references the Portfolio and Company classes, so it needs these references:
4) Adding class, member, constructor, and method declarations to the classes and their elements. For example (just to give you a flavor of the syntax):
class PortfolioItem {
portfolio: Portfolio;
symbol: string;
company: Company;
constructor(portfolio: Portfolio, symbol: string, chart = false, shares = 0, unitCost = 0)
{
this.portfolio = portfolio;
this.symbol = symbol;
5) Adding type information to members and method signatures.
This may seem like a lot of work, but it is actually very easy. And once you are done, you will get static error checking and IntelliSense right in your TypeScript files. In fact, it is likely that the compiler will find a few bugs in your project as soon as you finish the conversion. As a C# developer, I really missed this support when I wrote JavaScript code.
The images below show how the TypeScript compiler integrates with Visual Studio to provide static error-checking and IntelliSense.
Static error-checking provided by TypeScript
IntelliSense provided by TypeScript
Running the project causes the TypeScript compiler to generate the "js" files which contain plain JavaScript. The final project will contain no traces of TypeScript, it's just good old HTML and JavaScript as before.
I am a fan of TypeScript already, and plan to use it in my future HTML/JS projects. If you develop in JavaScript and haven't tried TypeScript yet, you are in for a treat.
References and Resources
- http://demo.componentone.com/wijmo/InvExplorer/: Live version of the Invexplorer application described in this article.
- http://www.codeproject.com/Articles/365120/KnockoutJS-vs-Silverlight:
CodeProject article by Colin Eberhart, compares MVVM development using KnckoutJS and
Silverlight. Excellent resource for Silverlight developers getting started with HTML5
and JavaScript.
- http://publicfiles.componentone.com/Bernardo/MVVM in Silverlight and in HTML5 IX.pdf:
Article that compares Silverlight and JavaScript implementations of the Invexplorer application.
- http://knockoutjs.com/: KnockoutJS home page. This is the ultimate resource on KnockoutJS. It contains conceptual
information, documentation, samples, and tutorials.
- http://jqueryui.com/: jQueryUI home page. The official resource for jQueryUI, including documentation and samples
for the controls (widgets), effects, and utilities included with jQueryUI.
- http://wijmo.com/: Wijmo home page. The official resource for Wijmo, a control library that includes grid
and chart controls that support KnockoutJS.
- http://typescript.codeplex.com/: TypeScript home page. The TypeScript compiler adds type information, static error-checking, and IntelliSense to JavaScript.