Friday, September 21, 2012
Windows Client of a Web App

A web app created with Code On Time may function as an application server for a client on any platform.

Let’s create a native Windows Forms application that will allow browsing and searching products available at http://demo.codeontime.com/Northwind/Pages/Products.aspx.

This is how a Quick Find search is performed by the web app. The native Windows client will provide a similar presentation and search facility.

The 'Quick Find' feature in action in a web app created with Code On Time

Click on the following link and you will see a response in XML format from the REST service of the web application:
http://demo.codeontime.com/northwind/appservices/Products?_q=40%20biscuit

The response will look similar to this sample.

<?xml version="1.0" encoding="utf-8"?>
<Products totalRowCount="2" pageSize="100" pageIndex="0" rowCount="2">
  <items>
    <item ProductName="Sir Rodney's Marmalade" SupplierID="8" CategoryID="3" 
          QuantityPerUnit="30 gift boxes" UnitPrice="$81.00" UnitsInStock="40" 
          UnitsOnOrder="0" ReorderLevel="0" Discontinued="False" ProductID="20" 
          SupplierCompanyName="Specialty Biscuits, Ltd." 
          CategoryCategoryName="Confections" />
    <item ProductName="Sir Rodney's Scones" SupplierID="8" CategoryID="3" 
          QuantityPerUnit="24 pkgs. x 4 pieces" UnitPrice="$10.00" UnitsInStock="3" 
          UnitsOnOrder="40" ReorderLevel="5" Discontinued="False" ProductID="21" 
          SupplierCompanyName="Specialty Biscuits, Ltd." 
          CategoryCategoryName="Confections" />
  </items>
</Products>

The “_q” parameter represents the Quick Find argument of the web resource. If the parameter is not specified, then the first 100 items are returned. Other parameters for sorting, paging, and filtering are also supported.

The native Windows client of the Northwind application server will construct a similar URL to retrieve data for presentation.

Start Visual Studio and select File | New Project option. Choose Windows Forms Application under Visual C# or Visual Basic template group, enter “ProductBrowser” in the name, and click OK to create project files.

The empty 'Northwind Product Browser' project contains a single Windows Form

Drop TextBox, Button, and ListView components from toolbox on the design surface of Form1 and have them arranged as shown next.

Form 'Form1' enhanced with a TextBox, Button, and ListView components

Use Properties Window to modify the Columns collection of listView1. Create five columns Product Name, Supplier Company Name, Category Name, Quantity per Unit, and Unit Price to capture a few properties of products.

Five columns in the list view 'listView1' represent properties of products stored in the Northwind database

Also set the following properties of listView1 component:

Property Value
FullRowSelect True
GridLines True
View Details
VirtualMode True

The list view is configured to behave as a virtual view.

If the property VirtualListSize is set to a value other than zero then the virtual list view will request items from the application as needed.

The list view will continuously request items representing a visible subset of data rows as a user scrolls the list.

Right-click Form1 node in the Solution Explorer and select View Code option in the context menu.

Add the highlighted namespaces to the top of code file:

C#:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.Xml.XPath;
using System.IO;

namespace ProductBrowser
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
    }
}

Visual Basic:

Imports System.Net
Imports System.Xml.XPath
Imports System.IO

Public Class Form1

End Class

The namespaces will link required .NET library components used by Windows native client to communicate with the web app.

Add fields _dataPage, _query, _cache, and method GetDataPage to the definition of class Form1.

C#:

protected XPathNavigator _dataPage;
protected string _query;

private SortedDictionary<int, XPathNavigator> _cache = new SortedDictionary<int, XPathNavigator>();

protected XPathNavigator GetDataPage(int pageIndex)
{
    XPathNavigator result = null;
    if (!_cache.TryGetValue(pageIndex, out result))
    {
        Cursor = Cursors.WaitCursor;
        try
        {
            WebRequest request = WebRequest.Create(String.Format(
                "http://demo.codeontime.com/northwind" +
                "/appservices/Products?_pageIndex={0}&_pageSize=10&_q={1}",
                pageIndex, _query));
            request.Method = "GET";
            using (WebResponse response = request.GetResponse())
            {
                using (Stream stream = response.GetResponseStream())
                    result = new XPathDocument(stream)
                        .CreateNavigator().SelectSingleNode("/Products");
            }
            _cache.Add(pageIndex, result);
        }
        finally
        {
            Cursor = Cursors.Default;
        }
    }
    return result;
}

Visual Basic:

Protected _dataPage As XPathNavigator
Protected _query As String

Private _cache As SortedDictionary(Of Integer, XPathNavigator) =
    New SortedDictionary(Of Integer, XPathNavigator)

Protected Function GetDataPage(pageIndex As Integer) As XPathNavigator
    Dim result As XPathNavigator = Nothing
    If Not (_cache.TryGetValue(pageIndex, result)) Then
        Cursor = Cursors.WaitCursor
        Try
            Dim request As WebRequest = WebRequest.Create(String.Format(
                "http://demo.codeontime.com/northwind" +
                "/appservices/Products?_pageIndex={0}&_pageSize=10&_q={1}",
                pageIndex, _query))
            request.Method = "GET"
            Using response As WebResponse = request.GetResponse()
                Using stream As Stream = response.GetResponseStream()
                    result = New XPathDocument(stream).
                        CreateNavigator().SelectSingleNode("/Products")
                    _cache.Add(pageIndex, result)
                End Using
            End Using
        Finally
            Cursor = Cursors.Default
        End Try
    End If
    Return result
End Function

The implementation of method GetDataPage creates a web request and retrieves the page of data from the web app if the page has not been found in the dictionary of cached data pages. The method parses the XML response received from the web app and stores an instance of XPathNavigator class initialized with a page data in the _cache field. The web request URI incorporates the requested page index and search criteria in _pageIndex and _q parameters accordingly.

Select listView1 in form designer and activate Properties Window, choose Events button, and double click RetrieveVirtualItem event.

Creating 'RetrieveVirtualItem' event handler for listView1 list view

Enter the following definition of the method listView1_RetrieveVirtualItem as shown next.

C#:

private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
    // analyze the current page item range
int pageIndex = Convert.ToInt32(_dataPage.GetAttribute("pageIndex", String.Empty)); int pageSize = Convert.ToInt32(_dataPage.GetAttribute("pageSize", String.Empty)); int rowCount = Convert.ToInt32(_dataPage.GetAttribute("rowCount", String.Empty)); int firstAvailableRowIndex = pageIndex * pageSize; int lastAvailableRowIndex = firstAvailableRowIndex + rowCount - 1; // if the current page does not contain the item then retrieve the page from the app if (!(firstAvailableRowIndex <= e.ItemIndex && e.ItemIndex <= lastAvailableRowIndex)) { pageIndex = e.ItemIndex / pageSize; firstAvailableRowIndex = pageIndex * pageSize; _dataPage = GetDataPage(pageIndex); } // create a virtual list item XPathNavigator dataRow = _dataPage.SelectSingleNode( String.Format("items/item[{0}]", e.ItemIndex - firstAvailableRowIndex + 1)); e.Item = new ListViewItem(dataRow.GetAttribute("ProductName", String.Empty)); e.Item.SubItems.Add(dataRow.GetAttribute("SupplierCompanyName", String.Empty)); e.Item.SubItems.Add(dataRow.GetAttribute("CategoryCategoryName", String.Empty)); e.Item.SubItems.Add(dataRow.GetAttribute("QuantityPerUnit", String.Empty)); e.Item.SubItems.Add(dataRow.GetAttribute("UnitPrice", String.Empty)); }

Visual Basic:

Private Sub listView1_RetrieveVirtualItem(sender As System.Object,
        e As System.Windows.Forms.RetrieveVirtualItemEventArgs) Handles listView1.RetrieveVirtualItem
    ' analyze the current page item range
    Dim pageIndex As Integer = Convert.ToInt32(_dataPage.GetAttribute("pageIndex", String.Empty))
    Dim pageSize As Integer = Convert.ToInt32(_dataPage.GetAttribute("pageSize", String.Empty))
    Dim rowCount As Integer = Convert.ToInt32(_dataPage.GetAttribute("rowCount", String.Empty))
    Dim firstAvailableRowIndex As Integer = pageIndex * pageSize
    Dim lastAvailableRowIndex As Integer = firstAvailableRowIndex + rowCount - 1
    ' if the current page does not contain the item then retrieve the page from the app
    If Not (firstAvailableRowIndex <= e.ItemIndex And e.ItemIndex <= lastAvailableRowIndex) Then
        pageIndex = e.ItemIndex \ pageSize
        firstAvailableRowIndex = pageIndex * pageSize
       _dataPage = GetDataPage(pageIndex)
    End If
    ' create a virtual list item
    Dim dataRow As XPathNavigator = _dataPage.SelectSingleNode(
        String.Format("items/item[{0}]", e.ItemIndex - firstAvailableRowIndex + 1))
    e.Item = New ListViewItem(dataRow.GetAttribute("ProductName", String.Empty))
    e.Item.SubItems.Add(dataRow.GetAttribute("SupplierCompanyName", String.Empty))
    e.Item.SubItems.Add(dataRow.GetAttribute("CategoryCategoryName", String.Empty))
    e.Item.SubItems.Add(dataRow.GetAttribute("QuantityPerUnit", String.Empty))
    e.Item.SubItems.Add(dataRow.GetAttribute("UnitPrice", String.Empty))
End Sub

Consult the sample of XML response at the top of article to understand the method implementation.

The implementation makes an assumption that a page of XML data is stored in the _dataPage instance of XPathNavigator. The property e.ItemIndex specifies the index of the item that the virtual list view needs. The code tries to determine if the current data page contains the item. If the item is not in current page _dataPage, then the application will retrieve a page of data from the web app by invoking GetDataPage method. The method will either find the page in the cache or reach out to the web app with a web request for data.

Next, the event handler will create an instance of a ListViewItem. Its sub-items represent the values displayed in the columns of the list view. The instance of the ListViewItem is assigned to e.Item property.

At this point the entire mechanism of populating the list view with data has been implemented.

The only remaining step is to collect user input for the search criteria and to assign a value to the VirtualListSize property of listView1.

Activate the design surface of Form1 and double-click the Find button. This will create an event handler that shall be defines as explained below.

C#:

private void button1_Click(object sender, EventArgs e)
{
    // assign the search criteria
    _query = textBox1.Text;
    // remove any previously cached data pages
    _cache.Clear();
    // get the first page of data with index "zero"
    _dataPage = GetDataPage(0);
    // figure the total number of rows in the virtual listview
    int totalRowCount = Convert.ToInt32(_dataPage.GetAttribute("totalRowCount", String.Empty));
    listView1.VirtualListSize = totalRowCount;
}

Visual Basic:

Private Sub button1_Click(sender As System.Object, e As System.EventArgs) Handles button1.Click
    ' assign the search criteria
    _query = textBox1.Text
    ' remove any previously cached data pages
    _cache.Clear()
    ' get the first page of data with index "zero"
    _dataPage = GetDataPage(0)
    ' figure the total number of rows in the virtual listview
    Dim totalRowCount As Integer = Convert.ToInt32(_dataPage.GetAttribute("totalRowCount", String.Empty))
    listView1.VirtualListSize = totalRowCount
End Sub

The implementation of button1_Click method copies the search criteria from the text box in the field _query.  Then it clears the previously cached data pages and obtains the first page of data. Last, the method will figure the size of the virtual list.

In less than a hundred lines of code a remarkable functionality has been brought to life.

Press F5 to start the Northwind Product Browser for Windows. Click the Find button.

Product Browser retreives pages of data from the web app as user scrolls the list view

Try scrolling the list of products. The Windows client will make at the most 8 requests to retrieve the 77 products from the Northwind database sample. The page size in this implementation is deliberately set to 10 items to ensure that a client-server interaction does occur upon scrolling.  If the page size is not specified then the default page size of 100 will be used.

Enter “40 biscuit” in the text box textBox1 and hit the button one more time. The two products matching the search criteria will be retrieved. Compare the result to the web app screenshot at the top.

Product Browser perform 'Quick Find' function with the help of the application server emdeded in the web app created with Code On Time

The same technique of data paging can be used in any application that supports XML or JSON data formats. That includes Java applications and mobile native Android and iPhone/iPad apps.