Wednesday, November 28, 2012

MVC and SEO basics: inject title, keywords and description into views

Almost by accident, I came across Google's starter guide for SEO optimisation and decided that it is a good idea to make some improvements I've been neglecting. Here's what I did so far and how I applied it to the MVC framework.

1. Create unique, accurate page titles

One way to do it with the MVC framework is to create a placeholder on the master page and then override it on the view page.

Master:


    
    
        <%=this.Page.Title%>
    

View:


       Home Page

For now, I chose the easier approach to set the title in the _Layout.cshtml

@ViewBag.Title

And assign it in each view separately

@{
    ViewBag.Title = "Main Page - The Stepping Stone Markov Chain Algorithm - MVC Stepping Stone Example";
}

2. Make use of the "description" and "keywords" meta tags

This takes a little more work. Here's my chosen approach: First, make sure each controller inherits from the BaseController. Then create two new classes, MetaDescriptionAttribute and MetaKeywordsAttribute, and inherit them from System.Attribute

public class MetaDescriptionAttribute : Attribute
{
 private readonly string _parameter;

 public MetaDescriptionAttribute(string parameter)
 {
  _parameter = parameter;
 }

 public string Parameter { get { return _parameter; } }
}

public class MetaKeywordsAttribute : Attribute
{
 private readonly string _parameter;

 public MetaKeywordsAttribute(string parameter)
 {
  _parameter = parameter;
 }

 public string Parameter { get { return _parameter; } }
}

In BaseController, override OnActionExecuting

protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
 var keywords = filterContext.ActionDescriptor.GetCustomAttributes(typeof(MetaKeywordsAttribute), false);
 if (keywords.Length == 1)
  ViewData["MetaKeywords"] = ((MetaKeywordsAttribute)(keywords[0])).Parameter;

 var description = filterContext.ActionDescriptor.GetCustomAttributes(typeof(MetaDescriptionAttribute), false);
 if (description.Length == 1)
  ViewData["MetaDescription"] = ((MetaDescriptionAttribute)(description[0])).Parameter;

 base.OnActionExecuting(filterContext);
}

Decorate the appropriate controller method with newly created attributes

[MetaKeywords("C#, MVC, Markov Chain, Stepping Stone, Programming")]
[MetaDescription("Stepping Stone Markov Chain model is an example that has been used in the study of genetics. In this model we have an n-by-n array of squares, and each square is initially any one of k different colors. For each step, a square is chosen at random. This square then chooses one of its eight neighbors at random and assumes the color of that neighbor")]
public ActionResult Index()
{
 SteppingStoneHelpers.CreateNewTable();
 HtmlString table = new HtmlString(SteppingStoneHelpers.table.ToString());
 return View(table);
}

Finally, in the _Layout.cshtml, add the following


All done! There we go, the resulting html:




    
    
    Main Page - The Stepping Stone Markov Chain Algorithm - MVC Stepping Stone Example
    
    

References:

Google Starter Guide
ASP.NET MVC - View with master page, how to set title?
asp.net mvc - strategy for including SEO information such as meta keywords and descriptions
by . Also posted on my website

Monday, November 26, 2012

WebGrid: AJAX Updates, Server Sorting

This is a brief summary of the changes I did to implement the AJAX updates to the WebGrid and sorting behaviour. I plan to put more detailed notes and the source code on my website

To use AJAX and update grid content, firstly the grid needs to be placed in the div which has an id. The ajaxUpdateContainerId has to be specified in the WebGrid declaration. To put it simple, in my main view I have a div

<div id="wbgrid" style="float:left; min-width:500px;">
 @Html.Partial("_WebGrid", Model)
</div>

And in the partial view the div name wbgrid is specified as ajaxUpdateContainerId.

@{ var grid = new WebGrid<Recipes.Models.Yahoo.YahooData>(null, rowsPerPage: 5, defaultSort: "YahooSymbolName", ajaxUpdateContainerId: "wbgrid");
grid.Bind(Model.Datas, rowCount: Model.TotalRows, autoSortAndPage: false);
}

The link on the WebGrid column has the following format http://localhost/Yahoo/Index?sort=DateTime&sortdir=ASC

Therefore, the controller function can automatically receive those parameters with the following signature:

public ActionResult Index(int page = 1, string sort = "YahooSymbolName", string sortDir = "Ascending")

The parameters will be then passed over to the function that retrieves data

public List<YahooData> GetData(out int totalRecords, int pageSize, int pageIndex, string sort = "YahooSymbolName", SortDirection sortOrder = SortDirection.Ascending )
{
 IQueryable<YahooData> data = db.YahooData;
 totalRecords = data.Count();

 Func<IQueryable<YahooData>, bool, IOrderedQueryable<YahooData>> applyOrdering = _dataOrderings[sort];
 data = applyOrdering(data, sortOrder == SortDirection.Ascending);

 List<YahooData> result = data.ToList();
 if(pageSize > 0 && pageIndex >=0)
 {
  result = result.Skip(pageIndex*pageSize).Take(pageSize).ToList();
 }
 return result;
}

A couple of helper functions are utilized by GetData: CreateOrderingFunc and _dataOrderings

// helpers that take an IQueryable<Product> and a bool to indicate ascending/descending
// and apply that ordering to the IQueryable and return the result
private readonly IDictionary<string, Func<IQueryable<YahooData>, bool, IOrderedQueryable<YahooData>>>
 _dataOrderings = new Dictionary<string, Func<IQueryable<YahooData>, bool, IOrderedQueryable<YahooData>>>
       {
        {"YahooSymbolName", CreateOrderingFunc<YahooData, string>(p=>p.DataName)},
        {"Ask", CreateOrderingFunc<YahooData, decimal?>(p=>p.Ask)},
        {"Time", CreateOrderingFunc<YahooData, DateTime>(p=>p.DateTime)}
        // Add for more columns ...
       };

/// returns a Func that takes an IQueryable and a bool, and sorts the IQueryable (ascending or descending based on the bool).
/// The sort is performed on the property identified by the key selector.
private static Func<IQueryable<T>, bool, IOrderedQueryable<T>> CreateOrderingFunc<T, TKey>(Expression<Func<T, TKey>> keySelector)
{
 return
  (source, ascending) => ascending ? source.OrderBy(keySelector) : source.OrderByDescending(keySelector);
}

Finally, to complete the functional example, I added a jQuery dialog that displays the data that was retrieved from Yahoo. In the view, the RetrieveData function triggers the controller action AddDataToDB (which calls the Yahoo website and adds results to the database).

function RetrieveData() {
 $.post('@Url.Action("AddDataToDB","yahoo")',
 function (d) {
  ShowDialog(d.o);
 });
}

function ShowDialog(msg) {
 $('<div/>').dialog({ title: 'Retrieved the following data', width: 450, height: 250, close: function(){ location.reload(); }}).html(msg);
}

AddDataToDB returns a Json result, containing the html table.

public ActionResult AddDataToDB()
{
 List<YahooData> datas = GetSingleSet();
 datas.ForEach(d => db.YahooData.Add(d));
 db.SaveChanges();

 string s = "<table><thead><tr class=\"webgrid-header\"><th>Company</th><th>Time</th><th>LTP</th><th>Volume</th><th>Ask</th><th>Bid</th><th>High</th><th>Low</th></tr></thead><tbody>";

 foreach (var yahooData in datas)
 {
  s = s + "<tr class=\"webgrid-row-style\">" + 
   "<td class=\"company\">" + yahooData.DataName + "</td>" +
   "<td class=\"time\">" + yahooData.DateTime.ToString("dd/MM/yyyy hh:mm") + "</td>" +
   "<td class=\"ask\">" + yahooData.LTP + "</td>" +
   "<td class=\"volume\">" + yahooData.Volume + "</td>" +
   "<td class=\"ask\">" + yahooData.Ask + "</td>" +
   "<td class=\"ask\">" + yahooData.Bid + "</td>" +
   "<td class=\"ask\">" + yahooData.High + "</td>" +
   "<td class=\"ask\">" + yahooData.Low + "</td>" +
   "</tr>";
 }

 s = s + "</tbody></table>";

 return Json(new { o = s });
}

The result is then utilised by the ShowDialog function, that displays a jQuery dialog. When the user closes the dialog, the page is refreshed so that the WebGrid contents are updated with the latest data retrieved.

Complete example

References

Get the Most out of WebGrid in ASP.NET MVC
by . Also posted on my website

Sunday, November 25, 2012

WebGrid: Stronly Typed with Server Paging

Continuing with the WebGrid, I first made it strongly typed. To achieve that, I created a derived type WebGrid. The source came from the reference at the end of the post, and the code I used is displayed below too. While it does not seem to change much in terms of functionality, the main advantage is that the IntelliSense and compiler checking will work with the grid now.

Next I added server paging. Why wouldn't I use the built-in paging? Well, the database table behind WebGrid may have hundreds of records. I wouldn't want to pass it all to my ViewModel and then to the view just to display 5 or 10 of the records I actually need. It is handy that the WebGrid paging is in the form of http://website/Items/ShowAll?page=3. This way my controller knows which page is to be displayed and can preselect the data just for this page only.

To implement paging, I added the TotalRows to the model - that will specify the total number of records in the database table.

The controller method now looks as follows:

public ActionResult Index(int page=1)
{
 int totalRecords;
 List<YahooData> datas = GetData(out totalRecords, pageSize: 5, pageIndex: page - 1);
 List<YahooSymbol> symbols = db.YahooSymbols.ToList();
 YahooSymbol symbol = symbols.First();
 int id = symbol.YahooSymbolID;
 return View(new YahooViewModel(id, symbol, symbols, datas, totalRecords));
}

public List<YahooData> GetData(out int totalRecords, int pageSize, int pageIndex)
{
 List<YahooData> data = GetData();
 totalRecords = data.Count;
 if(pageSize > 0 && pageIndex >=0)
 {
  data = data.Skip(pageIndex*pageSize).Take(pageSize).ToList();
 }
 return data.ToList();
}

The concept is quite simple - get data from the database table, identify the records that will be displayed on the WebGrid page that is requested, and only pass those records to the view. The WebGrid part of the view now looks as follows

<div id="webgrid" style="float:left; min-width:500px;">
 @{ var grid = new WebGrid<ViewModels.YahooData>(null, rowsPerPage: 5, defaultSort: "YahooSymbolName");
    grid.Bind(Model.Datas, rowCount: Model.TotalRows, autoSortAndPage: false);
    @grid.GetHtml(columns: grid.Columns( 
    grid.Column("DataName", header:"Company", format:@<text>@Html.ActionLink((string)item.DataName, "Details", "Company", new {id=item.SymbolId}, null)</text>),
    grid.Column("DateTime", header:"Time", style:"time", format:@<text>@item.DateTime.ToString("dd/MM/yyyy hh:mm")</text>), 
    grid.Column("LTP"), grid.Column("Volume"), grid.Column("Ask"), grid.Column("Bid"), grid.Column("High"), grid.Column("Low")),
    tableStyle: "webGrid", headerStyle: "header", alternatingRowStyle: "alt");
  }
</div>

My plan from here is to implement AJAX updates to the WebGrid content.

The strongly typed WebGrid samples:

//Strongly Typed WebGrid
public class WebGrid<T> : WebGrid
{
 // Wrapper for System.Web.Helpers.WebGrid that preserves the item type from the data source
 public WebGrid(IEnumerable<T> source = null, IEnumerable<string> columnNames = null, string defaultSort = null, int rowsPerPage = 10, bool canPage = true, bool canSort = true, string ajaxUpdateContainerId = null, string ajaxUpdateCallback = null, string fieldNamePrefix = null, string pageFieldName = null, string selectionFieldName = null, string sortFieldName = null, string sortDirectionFieldName = null)
  : base(source.SafeCast<object>(), columnNames, defaultSort, rowsPerPage, canPage, canSort, ajaxUpdateContainerId, ajaxUpdateCallback, fieldNamePrefix, pageFieldName, selectionFieldName, sortFieldName, sortDirectionFieldName)
 {
 }
 public WebGridColumn Column(string columnName = null, string header = null, Func<T, object> format = null, string style = null, bool canSort = true)
 {
  Func<dynamic, object> wrappedFormat = null;
  if (format != null)
  {
   wrappedFormat = o => format((T)o.Value);
  }
  WebGridColumn column = base.Column(columnName, header, wrappedFormat, style, canSort);
  return column;
 }
 public WebGrid<T> Bind(IEnumerable<T> source, IEnumerable<string> columnNames = null, bool autoSortAndPage = true, int rowCount = -1)
 {
  base.Bind(source.SafeCast<object>(), columnNames, autoSortAndPage, rowCount);
  return this;
 }
}

public static class EnumerableExtensions
{
 public static IEnumerable<TTarget> SafeCast<TTarget>(this IEnumerable source)
 {
  return source == null ? null : source.Cast<TTarget>();
 }
}
//WebGrid extensions
public static class WebGridExtensions
{
 // Light-weight wrapper around the constructor for WebGrid so that we get take advantage of compiler type inference
 public static WebGrid<T> Grid<T>(this HtmlHelper htmlHelper, IEnumerable<T> source, IEnumerable<string> columnNames = null,
   string defaultSort = null, int rowsPerPage = 10, bool canPage = true, bool canSort = true,
   string ajaxUpdateContainerId = null, string ajaxUpdateCallback = null, string fieldNamePrefix = null,
   string pageFieldName = null, string selectionFieldName = null, string sortFieldName = null, string sortDirectionFieldName = null)
 {
  return new WebGrid<T>(source, columnNames, defaultSort, rowsPerPage,
    canPage, canSort, ajaxUpdateContainerId, ajaxUpdateCallback, fieldNamePrefix, 
    pageFieldName, selectionFieldName, sortFieldName, sortDirectionFieldName);
 }

 // Light-weight wrapper around the constructor for WebGrid so that we get take advantage of compiler type inference and to automatically call Bind to disable the automatic paging and sorting (use this for server-side paging)
 public static WebGrid<T> ServerPagedGrid<T>(this HtmlHelper htmlHelper, IEnumerable<T> source, int totalRows, IEnumerable<string> columnNames = null,
   string defaultSort = null, int rowsPerPage = 10, bool canPage = true, bool canSort = true, string ajaxUpdateContainerId = null, 
   string ajaxUpdateCallback = null, string fieldNamePrefix = null,
   string pageFieldName = null, string selectionFieldName = null, string sortFieldName = null, string sortDirectionFieldName = null)
 {
  var webGrid = new WebGrid<T>(null, columnNames, defaultSort, rowsPerPage, canPage,
    canSort, ajaxUpdateContainerId, ajaxUpdateCallback, fieldNamePrefix,
    pageFieldName, selectionFieldName, sortFieldName, sortDirectionFieldName);
  return webGrid.Bind(source, rowCount: totalRows, autoSortAndPage: false); ;
 }
}

References

Get the Most out of WebGrid in ASP.NET MVC
by . Also posted on my website

Tuesday, November 13, 2012

Starting with WebGrid

WebGrid is an HTML helper provided as part of the MVC framework to simplify rendering tabular data. It is actually very simple to start with WebGrid. The following is enough to create a complete working example:

@model YahooViewModel

...

@{ var grid = new WebGrid(Model.Datas);
   @grid.GetHtml();
 }

Here the "Datas" is my list of YahooData entities. This, however, looks a little ugly, so I'll spend a few minutes on styling straight away. The following is a basic style for a WebGrid

<style type="text/css">
    .webGrid {margin: 4px; border-collapse: collapse; width: 300px;}
    .header {background-color: #E8E8E8; font-weight: bold; color: #FFF;}
    .webGrid th, .webGrid td { border: 1px solid #C0C0C0; padding: 5px;}
    .alt {background-color: #E8E8E8; color: #000;}
</style>

The style is applied as follows

@{ var grid = new WebGrid(Model.Datas);
   @grid.GetHtml(tableStyle: "webGrid", headerStyle: "header", alternatingRowStyle: "alt");
 }

First WebGrid

I don't want to show each and every column to the user. I can rewrite the WebGrid specifying the actual columns to show. Only specified columns will be displayed. Also, now the order of the columns is the same as the order I define them.

@{ var grid = new WebGrid(Model.Datas, 
       columnNames: new[] {"DataName", "Date", "LTP", "Time", "Volume", "Ask", "Bid", "High", "Low"});
   @grid.GetHtml(tableStyle: "webGrid", headerStyle: "header", alternatingRowStyle: "alt");
 }

Specific columns

Another way to do it is to actually define columns explicitly. First advantage is that I can now specify a name for the header.

@{ var grid = new WebGrid(Model.Datas, columnNames: new[] {"DataName", "Date", "LTP", "Time", "Volume", "Ask", "Bid", "High", "Low"});
   @grid.GetHtml(columns: grid.Columns( grid.Column("DataName", header: "Company"), grid.Column("Date"), grid.Column("LTP"), grid.Column("Time"), grid.Column("Volume"), 
   grid.Column("Ask"), grid.Column("Bid"), grid.Column("High"), grid.Column("Low")),
   tableStyle: "webGrid", headerStyle: "header", alternatingRowStyle: "alt");
 }

Finally, let's assume I want to let the user click the Company name and navigate to the page that provides some more information about the company. I can use format parameter of the Column to display an ActionLink.

@{ var grid = new WebGrid(Model.Datas, columnNames: new[] {"DataName", "Date", "LTP", "Time", "Volume", "Ask", "Bid", "High", "Low"});
   @grid.GetHtml(columns: grid.Columns( 
   grid.Column("DataName", header:"Company", format:@<text>@Html.ActionLink((string)item.DataName, "Details", "Company", new {id=item.SymbolId}, null)</text>),
   grid.Column("Date"), grid.Column("LTP"), grid.Column("Time"), grid.Column("Volume"), 
   grid.Column("Ask"), grid.Column("Bid"), grid.Column("High"), grid.Column("Low")),
   tableStyle: "webGrid", headerStyle: "header", alternatingRowStyle: "alt");
 }

The ActionLink will be created in the following format: "http://localhost/Company/Details/1"

Finally (for today) I would like to combine Date and Time in a single column and format it. The last bit of code shows how to format the date in the column and how to apply the style to a specific column.

<style type="text/css">

...

    .time {width: 200px; font-weight:bold;}
</style>

@{ var grid = new WebGrid(Model.Datas, columnNames: new[] {"DataName", "Date", "LTP", "Time", "Volume", "Ask", "Bid", "High", "Low"});
   @grid.GetHtml(columns: grid.Columns( 
   grid.Column("DataName", header:"Company", format:@<text>@Html.ActionLink((string)item.DataName, "Details", "Company", new {id=item.SymbolId}, null)</text>),
   grid.Column("DateTime", header:"Time", style:"time", format:@<text>@item.DateTime.ToString("dd/MM/yyyy hh:mm")</text>), 
   grid.Column("LTP"), grid.Column("Volume"), grid.Column("Ask"), grid.Column("Bid"), grid.Column("High"), grid.Column("Low")),
   tableStyle: "webGrid", headerStyle: "header", alternatingRowStyle: "alt");
 }

Better formatting

The plan from here is to add server-side paging to reduce the stress on the view when the number of records is high.

References

Get the Most out of WebGrid in ASP.NET MVC
WebGrid WebHelper in ASP.NET MVC 3 RC
by . Also posted on my website

Wednesday, November 7, 2012

Yahoo Data Download

Stock data can be downloaded from http://finance.yahoo.com/d/quotes.csv?s=[stock symbol string]&f=[special tags]. Some tags are listed in the table at the end of the post, but that's not the point. I'll be using a static url for a code example, such as http://download.finance.yahoo.com/d/quotes.csv?s=GOOG+AAPL+MSFT+YHOO&f=snd1l1t1vb3b2hg which will return values for Symbol, Name, Last trade date, Last trade (price only), Last trade time, Volume, Bid (real-time), Ask (real-time), Day's High and Day's Low.

The plan is to have a list of symbols (configurable), to get the data from yahoo and dynamically load the data into the WebGrid control. Therefore, I started with the basic ViewModel that has two sets of entities - one for the symbols and one for the data itself. Eventually the list of symbols will be made configurable.

//ViewModel
public class YahooViewModel
{
 public List<YahooData> Datas { get; set; }
 public List<YahooSymbol> Symbols { get; set; }
 public YahooSymbol Symbol { get; set; }
 public int YahooSymbolID { get; set; }

 public YahooViewModel(int symbolid, YahooSymbol symbol, List<YahooSymbol> symbols, List<YahooData> datas)
 {
  Symbol = symbol;
  YahooSymbolID = symbolid;
  Symbols = symbols;
  Datas = datas;
 }
}

The controller requests and populates the data, and later the automatic authentication may be added as described in the previous post.

//Controller
public ActionResult Index()
{
 List<YahooData> datas = GetData();
 List<YahooSymbol> symbols = db.YahooSymbols.ToList();
 YahooSymbol symbol = symbols.First();
 int id = symbol.YahooSymbolID;
 return View(new YahooViewModel(id, symbol, symbols, datas));
}

public List<YahooData> GetData()
{
 List<YahooData> datas = new List<YahooData>();

 HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://download.finance.yahoo.com/d/quotes.csv?s=GOOG+AAPL+MSFT+YHOO&f=snd1l1t1vb3b2hg");
 HttpWebResponse resp = (HttpWebResponse)req.GetResponse();

 using (StreamReader streamReader = new StreamReader(resp.GetResponseStream()))
 {
  string t = streamReader.ReadToEnd();
  string[] strings = t.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
  datas = InsertData(strings);
 }
 return datas;
}

private List<YahooData> InsertData(string[] lines)
{
 List<YahooData> datas = new List<YahooData>();

 foreach (string line in lines)
 {
  if (!String.IsNullOrEmpty(line))
  {
   YahooData datum = GetDatum(line);
   datas.Add(datum);
  }
 }
 return datas;
}

private YahooData GetDatum(string line)
{
 var datum = new YahooData();
 string[] splitLine = line.Split(',');
 datum = new YahooData()
 {
  DataName = splitLine[1].Replace("\"", ""),
  Date = DateTime.ParseExact(splitLine[2].Replace("\"", ""), "MM/d/yyyy", CultureInfo.InvariantCulture),
  LTP = decimal.Parse(splitLine[3]),
  Time = DateTime.Parse(splitLine[4].Replace("\"", "")),
  Volume = decimal.Parse(splitLine[5]),
  Ask = decimal.Parse(splitLine[6]),
  Bid = decimal.Parse(splitLine[7]),
  High = decimal.Parse(splitLine[8]),
  Low = decimal.Parse(splitLine[9])
 };
 return datum;
}

The symbols are seeded initially, and may be later made configurable.

//Seeding database with initial values
public class SampleData : DropCreateDatabaseIfModelChanges<RecipesEntities>
{
 protected override void Seed(RecipesEntities context)
 {
  AddSymbols(context);
 }
}

public static void AddSymbols(RecipesEntities context)
{
 List<YahooSymbol> symbols = new List<YahooSymbol>
 {
  new YahooSymbol {YahooSymbolID = 1, YahooSymbolName = "GOOG"},
  new YahooSymbol {YahooSymbolID = 2, YahooSymbolName = "AAPL"},
  new YahooSymbol {YahooSymbolID = 3, YahooSymbolName = "MSFT"},
  new YahooSymbol {YahooSymbolID = 4, YahooSymbolName = "YHOO"}
 };

 symbols.ForEach(p => context.YahooSymbols.Add(p));
 context.SaveChanges();
}

Finally, the table of tags and their meanings - just for the interest.

a Ask a2 Average Daily Volume a5 Ask Size
b Bid b2 Ask (Real-time) b3 Bid (Real-time)
b4 Book Value b6 Bid Size c Change & Percent Change
c1 Change c3 Commission c6 Change (Real-time)
c8 After Hours Change (Real-time) d Dividend/Share d1 Last Trade Date
d2 Trade Date e Earnings/Share e1 Error Indication (returned for symbol changed / invalid)
e7 EPS Estimate Current Year e8 EPS Estimate Next Year e9 EPS Estimate Next Quarter
f6 Float Shares g Day's Low h Day's High
j 52-week Low k 52-week High g1 Holdings Gain Percent
g3 Annualized Gain g4 Holdings Gain g5 Holdings Gain Percent (Real-time)
g6 Holdings Gain (Real-time) i More Info i5 Order Book (Real-time)
j1 Market Capitalization j3 Market Cap (Real-time) j4 EBITDA
j5 Change From 52-week Low j6 Percent Change From 52-week Low k1 Last Trade (Real-time) With Time
k2 Change Percent (Real-time) k3 Last Trade Size k4 Change From 52-week High
k5 Percebt Change From 52-week High l Last Trade (With Time) l1 Last Trade (Price Only)
l2 High Limit l3 Low Limit m Day's Range
m2 Day's Range (Real-time) m3 50-day Moving Average m4 200-day Moving Average
m5 Change From 200-day Moving Average m6 Percent Change From 200-day Moving Average m7 Change From 50-day Moving Average
m8 Percent Change From 50-day Moving Average n Name n4 Notes
o Open p Previous Close p1 Price Paid
p2 Change in Percent p5 Price/Sales p6 Price/Book
q Ex-Dividend Date r P/E Ratio r1 Dividend Pay Date
r2 P/E Ratio (Real-time) r5 PEG Ratio r6 Price/EPS Estimate Current Year
r7 Price/EPS Estimate Next Year s Symbol s1 Shares Owned
s7 Short Ratio t1 Last Trade Time t6 Trade Links
t7 Ticker Trend t8 1 yr Target Price v Volume
v1 Holdings Value v7 Holdings Value (Real-time) w 52-week Range
w1 Day's Value Change w4 Day's Value Change (Real-time) x Stock Exchange
y Dividend Yield
by . Also posted on my website