Export (0) Print
Expand All
7 out of 8 rated this helpful - Rate this topic

Northwind Pocket Inventory: Logistics for Windows Mobile-based Pocket PCs

 

Christian Forsberg
Business anyplace

Contribution from: Odyssey Software

April 2005

Applies to:
   Windows Mobile–based Pocket PCs
   Visual Studio .NET 2003
   .NET Compact Framework version 1.0

Summary: Learn about mobile logistics and how to design and develop solutions for Windows Mobile–based Pocket PCs by using Visual Studio .NET and the .NET Compact Framework. The source code in this article implements server components, a database, and a Pocket PC client. (47 printed pages)


Download Northwind Pocket Inventory - Logistics for Windows Mobile-based Pocket PC.msi from the Microsoft Download Center.

Odyssey Software CFCOM enables transparent access to controls such as the Windows Media Player, objects such as the Pocket Outlook Object Model and ADOCE, and virtually any third-party COM or ActiveX component. CFCOM can be licensed from Odyssey Software, although the code sample in this article will work with the license key found in the source code.

Download cfcom.exe from the Microsoft Download Center.

Contents

Introduction
Northwind Traders's Inventory Business Process
XML Web Service Integration
Northwind Pocket Inventory Application Client Walkthrough
Code Walkthrough
Conclusion

Introduction

This article builds on the topics presented in:

The first article describes key elements in how to develop mobile field service applications, and the second focuses more on developing these applications for Smartphone. The third and fourth articles focus on developing mobile field sales applications.

This article is about designing and developing a mobile inventory application based on the .NET Framework, the Windows Mobile platform for Pocket PCs, and the .NET Compact Framework. This article provides a sample solution that addresses the needs of the fictitious company, Northwind Traders, from a logistic process and in-house inventory point of view. From a technology point of view, this article demonstrates how to connect directly to a server database (SQL Server 2000), how to use XML Web services for integration, how to implement simple bar-code scanner support, how to use the Pocket PC Help system from a .NET Compact Framework application, and more.

Northwind Traders's Inventory Business Process

As described in previous articles, the customers of Northwind Traders have vending machines that sell the various products from Northwind Traders. In a business like this, keeping a healthy warehouse is critical, because missing stock items will probably affect the sales instantly. The logistic process not only includes keeping track of items in stock (inventory) but also ensures that new products arrive in time to avoid running out of products in stock (purchase). These two aspects are normally handled in two different business processes. Normally, a warehouse employee performs the inventory of products in stock, and then another employee purchases new products. However efficient this procedure might be, the two employees are usually not in direct contact, and information exchange about what products should be purchased, and from what supplier, might be lost.

Information technology in general, and a mobile solution specifically, can bring control and decisions closer to the origin of the information. The employees working with inventory in the warehouse could be connected to the back-office system and get feedback about changes in the warehouse. Also, those employees could make instant decisions about how many products should be purchased. This way, the two processes can be brought together. To take this concept even further, the warehouse employees could also compare offerings from competing suppliers, and decide what products to buy, right on the warehouse floor. The steps in such a combined process would include:

  1. Find number of items in stock.
  2. Determine need for purchase.
  3. View standard supplier offering.
  4. Evaluate optional suppliers.
  5. Make selection and purchase.

Figure 1 illustrates the steps in the combined process.

Figure 1: Combined inventory and purchase process

Documenting the process in a format like the one shown in Figure 1 helps ensure that all the people involved in implementing the new process have the same view of what they should achieve. To ensure maximum creative input, process charts like this are best created in a meeting session that includes various roles: managers and workers from both business and technology areas.

After the new process is defined, the next step is to design the solution.

Application Design

The article Northwind Pocket Service: Field Service for Windows Mobile-based Pocket PCs provided an introduction to the architectural work in a mobile solution. Because article readers requested more information about activities involved in designing the application, some of the artifacts in the design of the application are presented in this article. Much of this work builds on the well-known process called Unified Process (for example, XUP, which combines the best attributes of Unified Process in the different methodologies). In addition, many of the illustrations in this article use the even more well-known notation called Unified Modeling Language. There are many tools available for creating these diagrams, but a general tool like Microsoft PowerPoint can produce excellent results. The downloadable sample that accompanies this article contains all the following figures as a PowerPoint presentation that can be reused for the creation of new diagrams.

The real design of the application starts with the creation of the use cases. The business process shown earlier in Figure 1 renders three use cases, as shown in Figure 2.

Figure 2: Use case model

The following list is the normal flow of the example use case Make Inventory:

  1. The user (in this case, a warehouse employee) taps Inventory on the main menu.
  2. The system displays the product list dialog.
  3. The user enters a product number or other search criteria, and then taps Find.
  4. The system presents a list of search results.
  5. The user selects a product in the list, and then taps View.
  6. The system displays the product detail dialog.
  7. The user enters the number of products in stock, and then taps Update.
  8. The system displays a confirmation message.

An alternative flow occurs if the user chooses to scan a bar code instead of entering a product number in step 3. The normal flow then continues with step 6.

Flows are typically described in the preceding text form, but they can also be described as an activity diagram, as shown in Figure 3.

Figure 3: Activity diagram

The activities shown in Figure 3 are the actions that the user takes. Note how the alternative flow is included as an alternate path.

The use case documentation also includes the preconditions (for example, that the user is logged on), postconditions, and nonfunctional requirements (for example, response time). It is also a good idea to include some sample dialogs.

Later in the application design, the use case is implemented (turned into a use case realization), which means that it is updated into a form that is more useful for a developer. Then, a scenario diagram can be used to show the interactions between the objects (class instances) of the system. Figure 4 is a very high-level sequence diagram for the three use cases shown earlier in Figure 2.

Figure 4: Sequence diagram

In Figure 4, the arrows that initiate from the leftmost dotted line indicate actions that the user performs. These actions include entering the number of items, which causes the mobile application to call the back-office system to get the number of items.

In a real-world scenario, the sequence included in the use case realization includes the various objects that are involved in implementing the use case. Each call then corresponds to an actual method call.

Figure 5 combines class, collaboration, and entity-relationship diagrams into a single design model. Although the design model is nonstandard, it can be helpful in creating the use case realization.

Figure 5: Design model

The design model covers the dialogs (forms), the components (classes), and the database (tables). Because it includes the navigation, call, queries, and database relations, it provides a good overview of all the parts involved in implementing the use case.

A critical artifact to develop as soon as possible in the design process is the dialog model and sample dialogs. Figure 6 shows a sample dialog model.

Figure 6: Dialog model

The main purpose of the dialog model is to show the navigation between the dialogs (forms) of the application. Note that the product list (and product) form is used in both the Inventory and Products options on the main menu. It is implemented as a single form, but the functionality changes somewhat depending on what role the form has.

Figure 7 shows some sample dialogs.

Click here for larger image

Figure 7: Sample dialogs. Click the thumbnail for a larger image.

Someone knowledgeable in user interface design can draw sample dialogs in a general drawing tool (in the samples shown in Figure 7, PowerPoint). Samples give potential users an opportunity to understand what the application will look like and how it will work. Changes are easy to make at this stage. These drawings are suitable for inclusion in the use case documentation.

XML Web Service Integration

The fourth business process step in the solution is to evaluate options. Here, the user (warehouse employee) can compare products from alternative suppliers. Even though the user interface needs to be consistent with the rest of the application, the idea is to provide the user with the most up-to-date information directly from the source. Therefore, an XML Web service is set up in the back office at Northwind Traders to provide information to the mobile client application about products at the suppliers. Figure 8 shows an overview of the architecture.

Figure 8: Integrating through XML Web services

The form (SupplierProductList) in the Pocket PC application calls the inventory XML Web service in the back office to get information about the supplier's products. That XML Web service in turn requests the information from the supplier. If the supplier provides an XML Web service interface for querying about products, a call is made to get the product information. But if the supplier provides only a standard Web page with the required information, the inventory Web service parses that page. However, before the information is returned to the client, it is normalized so that the user can use it to compare product information from different suppliers. Figure 9 shows an example supplier Web page.

Click here for larger image

Figure 9: Example supplier Web page. Click the thumbnail for a larger image.

The sample supplier Web page shown in Figure 9 is from the online samples created for the Northwind demonstrations included in Access 95 back in 1996. It reflects the popular style of Web page design at the time. This sample is actually still online.

The inventory Web service uses the supplier page to retrieve information about the products. The Web service parses a standard Web page — a process also known as screen scraping — by looking for placeholders in the page source (the HTML) and separating the data from the layout. The data is then transformed into something more structured, like XML or a DataSet, and returned to the client.

Though efficient for structuring unstructured content, this technique should be used with care because the code will break if the structure of the Web page changes. A way to avoid this problem is to make an agreement with the supplier to provide the desired Web page in a specified format, even if changes are made to the supplier's Web site. Even if a clearly defined XML Web service interface is preferred, this is a way to start integrating with partners immediately.

The introduction of a single point of integration makes the clients independent of the status of the integration with the suppliers. If a supplier starts providing an XML Web service, only the inventory Web service will need to be modified (by "scrapping the scraping"). Replacing the traditional many-to-many integration with a many-to-one-to-many model will help minimize maintenance costs.

The enterprise solution is to use an integration engine like Microsoft BizTalk, which offers many possibilities to set up integration between internal and external systems. BizTalk can replace the inventory Web service previously shown in Figure 8, and it offers many standard adapters for connecting to the suppliers and their business systems.

Northwind Pocket Inventory Application Client Walkthrough

The example client scenario is a Pocket PC application that was written with Microsoft Visual Studio .NET 2003 in C# and that targets the Microsoft .NET Compact Framework.

The application shows how to support a combined inventory and purchase business process by using a mobile handheld device. The design choices in the application align to the process as much as possible and also maximize efficiency for the warehouse worker. This walkthrough describes the design elements of the user interface and explores parts of the code.

Login

When the application starts, the first screen is the login screen, as shown in Figure 10.

Figure 10. Login screen

The login screen is branded with an application symbol. It can also be used to show notification messages about news, new features, important changes, and so on. In addition, this screen is a good place to inform the user about copyrights and license conditions.

When you enter the user name and password, and then tap OK, the application confirms that the credentials are valid. If the login is successful, the application displays the main menu, as shown later in Figure 11. If the login is not successful, an error message appears, and then the application displays the Options screen, as shown in later in Figure 29. If you leave the password blank before tapping OK, the application closes.

Main Menu

The functionality of the application is aligned with the business process of the warehouse worker. The main steps in this process are to do the inventory, determine need for addition products, evaluate the default supplier in addition to alternative suppliers, and create a purchase order. The main menu, shown in Figure 11, indicates the other screens that the application contains. A ListView control has been used to present the main menu options as icons that you can tap.

Figure 11. Main menu

You can also access all menu options by tapping Menu in the lower-left corner of the screen. Using a ListView control like this enables the switching of modes for the menu. With very small modifications, the menu (now with large icons) can be shown as small icons, a list with one column, or even a multiple-column list with details.

Inventory

The first step in the inventory process is to do the actual inventory. From the Main Menu screen, tap Inventory.

You can use the Inventory screen, shown in Figure 12, to search for products that you are inventorying. You can enter search criteria like product number, product name, product category, and supplier of the product. When you tap the Find button, the matching products appear in the list.

Figure 12. Inventory product search

To inventory a product, tap and hold it in the list until a pop-up menu appears with a View command, as shown in Figure 13.

Figure 13. Inventory product selection

When you tap the View pop-up menu command, detailed product information appears on the General tab, as shown in Figure 14. The product information includes the product number, unit price, quantity per unit, reorder level, unit currently on order, product category, supplier of the product, and most importantly, current number of units in stock.

Figure 14. Inventory product details

You can also get directly to this screen if you enter a product number on the Inventory screen, and then tap ENTER on the on-screen keyboard. This is how you can emulate a bar-code scanner. If you have a bar-code scanner, you can probably configure it to scan directly into the Number box and then send an additional ENTER keystroke.

The second tab on the product information screen is Image. As shown in Figure 15, it contains the Get image button.

Figure 15. Get image button

When you tap the Get image button, the application connects to a Web server to get the product image, and then displays the image. Figure 16 shows a displayed product image.

Click here for larger image

Figure 16. Product image. Click the thumbnail for a larger image.

Back on the first (General) tab, shown earlier in Figure 14, you can view the details about the supplier of the product by tapping the supplier name. Figure 17 shows supplier details.

Figure 17. Supplier details

Also on the General tab, you can choose to update the number of units in stock. The default value in the text box is always the same as the currently recorded number of units in stock. When you enter a new value and tap the Update button, the application updates the server database instantly. When the update is completed, a confirmation message appears, as shown in Figure 18.

Figure 18. Update of units in stock

When the inventory is finished, the newly entered information (that is, units in stock) may trigger you (as a warehouse worker) to identify a need for purchasing new items of this product. This signal could come from the server (back-office system) as an addition to the confirmation message shown in Figure 18, or it could be a decision that you make when evaluating the information on this screen.

But before ordering new items of this product, you might want to consider alternative products to order. You can investigate that option by tapping Find alternative products at the bottom of the product information screen. This option takes you to the screen that contains information about products available from different suppliers. Figure 19 shows this screen. Here, you can enter search criteria like product name, product category, and supplier of the product.

Figure 19. Products from alternative suppliers

As shown in Figure 19, there are currently only two suppliers that you can query online for product information. When you tap Find, the application makes an online request to an XML Web service, which queries the suppliers' product information in real time. The application then presents the matching products in the list.

In a real-world scenario, this way of integrating your partners' solutions into your own business processes opens endless possibilities. Not only will the warehouse staff get information more quickly, but they will also get more accurate information because it is retrieved directly from the source — the suppliers' information systems.

Information about the found products includes the product name, unit price, and number of units in stock at the supplier. For more details about a product, tap and hold an item in the list, and then tap the View pop-up menu command, as shown in Figure 20.

Figure 20. Supplier product selection

After you tap the View pop-up menu option, detailed supplier product information appears, as shown in Figure 21. The supplier product information includes the unit price, number of units in stock, quantity per unit, product category, and supplier.

Figure 21. Supplier product details

When you tap the supplier name, the application displays details about the supplier of the product, as shown in Figure 22.

Figure 22. Supplier details

If the supplier has a Web site, you can view that site directly by tapping the link beside Web site. This action opens Internet Explorer on the Pocket PC, as shown in Figure 23.

Figure 23. Supplier Web site

Go back to the supplier product information screen, shown previously in Figure 21 by minimizing (tap the minimize [X]) button) the browser. If this is an interesting alternative product to buy, you can enter a number of items required in the Quantity box. Then, when you tap the Purchase button, the application generates a XML Web service request to place the actual purchase order. When the purchase request is completed, a confirmation message appears, as shown in Figure 24.

Figure 24. Purchase order created

In a real-world scenario, the purchase order request is probably sent to a back-office business system (preferably by using XML Web services similar to what is described in the article Using XML Web Services to Communicate with Microsoft Axapta. With such a system correctly implemented, the process of making the actual purchase at the supplier can be completely automated. In other words, purchase orders made from the warehouse floor can be sent more or less directly to the supplier. Such efficiency improvements can save time and money for any business.

This concludes the mobile application's support for the combined inventory and purchase business process. The following sections continue by describing some of the application's support functions.

Products

When you tap Products on the main menu, the Products screen appears. You can then search for products in stock by number, name, category, and supplier. When you tap Find, the list of products is filled. Each product in the list displays a name, number of products in stock, and number of products on order. You can view details about a product by tapping and holding a product in the list, and then selecting the View pop-up menu command, as shown in Figure 25.

Figure 25. Product list

Figure 26 shows the product details.

Figure 26. Product details

The screen shown in Figure 26 is actually the same as that shown earlier in Figure 14 (although the example products are different), and the functionality is identical. However, the option to update the number of items in stock, as shown in Figure 14 and Figure 18, is not available here.

Suppliers

You can view suppliers by tapping Suppliers on the main menu. On the Suppliers screen that appears, you can search for suppliers by company name and contact name. When you tap Find, the list is updated with the matching suppliers. You can view details about a supplier by tapping and holding a name in the list, and then selecting the View pop-up menu command, as shown in Figure 27.

Figure 27. Supplier list

Figure 28 shows the supplier details.

Figure 28. Supplier details

The screen shown in Figure 28 is actually the same as that shown earlier in Figure 17 and Figure 22 (although the supplier names are different), and the functionality is identical.

Options

When you select Options on the main menu, the Options screen appears, as shown in Figure 29.

Figure 29. Options

The application uses the Database Connection options to connect to the server database. Server is the name (or IP address) of the server, and Database is the name of the SQL Server database. Web Service (URL) is the URL for the XML Web service that the application uses to handle both the searching of supplier products and the placing of purchase orders. Finally, Product Images (URL) is the URL for the virtual directory on the Web server where the application can retrieve product images.

About

When you tap About on the main menu, the About screen appears and displays the application's name, version, copyright information, and other legal information. Figure 30 shows the About screen.

Figure 30. About the application

This screen can also include a link to a Web page that contains product and/or support information.

World Ready

When you change the regional settings (tap Start, tap Settings, and then tap Regional Settings) to Portuguese (Brazil) on your Pocket PC, and then restart the application, the complete application is translated. The following figures are the Brazilian Portuguese versions of the screens shown earlier in Figure 11, Figure 12, Figure 14, and Figure 24.

Figure 31 shows the main menu with all user interface text — including the title bar, the screen name, the icon names, and the Menu menu — translated.

Figure 31. Translated main menu

Figure 32 shows the translated inventory product selection screen. Just as before, the form title and form controls (labels and buttons) are translated. Further, the column titles in the list are translated, in addition to the Edit menu.

Figure 32. Translated inventory product selection screen

Figure 33 shows the translated product details screen.

Figure 33. Translated product details

Finally, Figure 34 shows that even messages, like the purchase order confirmation, are translated.

Figure 34. Translated supplier product details (confirmation)

Code Walkthrough

The previous sections provided an example client scenario for the Pocket Inventory application. The next part of the article provides the source code of that example. The general parts of the code are covered in the article Northwind Pocket Service: Field Service for Windows Mobile-based Pocket PCs, so the focus in this article will be some of the unique aspects of the code for this example.

Connection to SQL Server 2000

The sample application connects to the server database directly. The namespace for the SQL Server managed provider used to connect to the database is System.Data.SqlClient. If you have been using the managed provider for SQL Server 2000 for Windows CE (SQL Server CE), in the namespace System.Data.SqlServerCe, you will recognize that many of the constructs are the same. If you come from desktop computer development with the SQL Server managed provider, the constructs will look even more familiar.

The following code in the main form (MainForm) opens the database connection.

Common.Values.DatabaseConnection = new
    SqlConnection(Common.Values.ConnectionString);
Common.Values.DatabaseConnection.Open();

The singleton property (Common.Values.DatabaseConnection) is declared as follows.

private SqlConnection databaseConnection;
public SqlConnection DatabaseConnection
{
    get { return databaseConnection; }
    set { databaseConnection = value; }
}

The code for the connection string property (Common.Values.ConnectionString) looks like the following (somewhat simplified).

public string ConnectionString
{
    get
    {
        return "Data Source=" + server +
        ";Initial Catalog=" + database +
        ";User Id=" + userName +
        ";Password=" + password + ";";
    }
}

The variable specifying which server the SQL Server 2000 database resides on (server) and the variable holding the database name in that SQL Server instance (database) are both saved in the device registry between sessions and modified in the options form (OptionsForm). The user name (userName) is also saved in the registry and shown in the login form (LoginForm), where it is also saved. Both the user name and the password are captured by the login form, and they are stored in the singleton with the following code.

Common.Values.UserName = userNameTextBox.Text;
Common.Values.Save();
Common.Values.Password = passwordTextBox.Text;

After the connection is established, it is closed immediately. The first opening of the connection simply validates the credentials in the database. Each time a business entity handler class is created, the connection reopens. The beginning of the product handler class is as follows.

public class ProductHandler : IDisposable
{
    private string defaultSQL = "SELECT * FROM Products";
    private SqlConnection cn;
    private SqlDataAdapter da;
    
    public ProductHandler()
    {
        cn = Common.Values.DatabaseConnection;
        cn.Open();
        da = new SqlDataAdapter(defaultSQL, cn);
        SqlCommandBuilder cb = new SqlCommandBuilder(da);
        da.InsertCommand = cb.GetInsertCommand();
        da.UpdateCommand = cb.GetUpdateCommand();
        da.DeleteCommand = cb.GetDeleteCommand();
    }

In the constructor, the connection is opened and a data adapter (da) is created with the insert, update, and delete commands initialized through a command builder (cb). If you are accustomed to using the SQL Server CE provider, you simply have to drop the Ce part of each class name (for example, SqlCeConnection becomes SqlConnection).

With this initialization finished, the code to retrieve the product information looks like the following.

public DataSet GetForID(string productID)
{
    DataSet ds = new DataSet();
    da.SelectCommand.CommandText = defaultSQL +
        " WHERE ProductID='" + productID + "'";
    da.Fill(ds, "Product");
    da.SelectCommand.CommandText = defaultSQL;
    return ds;
}

Through a command object, a single value can be retrieved as follows.

public string GetIDForNo(int productNo)
{
    SqlCommand cmd = cn.CreateCommand();
    cmd.CommandText = "SELECT ProductID FROM Products" +
        " WHERE ProductNo=" + productNo.ToString();
    return cmd.ExecuteScalar().ToString();
}

When the database processing is finished (and the handler class is disposed), the data adapter is disposed and the connection is closed as follows.

da.Dispose();
cn.Close();

Opening the connection as late as possible and closing it as soon as possible saves system resources and minimizes the vulnerability to network interruptions. An application such as this is probably used on the wireless local area network (WLAN, or WiFi network) in the warehouse, and if the device leaves the area of coverage, the network connection will end. Therefore, keeping the connections open for the shortest time possible increases the probability of successful database access. However, if a network interruption occurs, the worst thing that can happen is that the database transaction is rolled back. For example, an update was not performed and needs to be initiated again. The applications should include sound exception handling so such a situation can be detected and properly handled.

XML Web Services Integration

Similar to almost all the places in the source code when searches are finished, the code for the Find button shown earlier in Figure 19 looks like the following.

private void findButton_Click(object sender, System.EventArgs e)
{
    Cursor.Current = Cursors.WaitCursor;

    string category = string.Empty;
    if(categoryComboBox.SelectedIndex > -1)
        category = categoryComboBox.SelectedValue.ToString();
    string supplier = string.Empty;
    if(supplierComboBox.SelectedIndex > -1)
        supplier = supplierComboBox.SelectedValue.ToString();

    try
    {
        // Get product data
        DataSet ds;
        using (SupplierProductHandler supplierProductHandler =
                        new SupplierProductHandler())
            ds = supplierProductHandler.GetList(nameTextBox.Text,
                category, supplier);

        // Fill ListView from DataSet
        ListViewItem lvi;
        itemsListView.BeginUpdate();
        itemsListView.Items.Clear();
        foreach (DataRow dr in ds.Tables[0].Rows)
        {
            lvi = new ListViewItem(dr["ProductName"].ToString());
            lvi.SubItems.Add(string.Format("{0:##0.00}",
                Convert.ToDouble(dr["UnitPrice"])));
            lvi.SubItems.Add(dr["UnitsInStock"].ToString());
            lvi.SubItems.Add(dr["QuantityPerUnit"].ToString());
            lvi.SubItems.Add(dr["CategoryName"].ToString());
            lvi.SubItems.Add(dr["SupplierName"].ToString());
            lvi.SubItems.Add(dr["SupplierID"].ToString());
            itemsListView.Items.Add(lvi);
        }
        itemsListView.EndUpdate();
    }
    catch (Exception)
    {
        MessageBox.Show(GlobalHandler.Translate.Text(
            "MsgCantFindProducts", "Could not find products!"),
            this.Text);
    }
    Cursor.Current = Cursors.Default;
}

The parameters are gathered from the controls, the call is made to perform the search, and the ListView is filled with the returned data. Note that all the data is inserted into the ListView to avoid another call when the detail form appears. The code in the SupplierProductHandler class is as follows.

public DataSet GetList(string name, string categoryID,
                       string supplierID)
{
    WebServices.Inventory ws = new WebServices.Inventory();
    return ws.GetSupplierProducts(name, categoryID, supplierID);
}

The generated XML Web service proxy (when adding the Web Reference) is instantiated and called with the same search parameters.

The beginning of the GetSupplierProducts Web method looks like the following.

[WebMethod]
public DataSet GetSupplierProducts(string productName, string categoryID, string supplierID)
{
    HttpWebRequest req;
    HttpWebResponse resp;
    StreamReader sr;
    DataRow dr;
    string supID;
    string supplierName;
    string catID;
    string categoryName;
    string s;
    string t;
    int i;
    int j;

    SqlConnection cn = new SqlConnection(connectionString);
    cn.Open();
    SqlCommand cmd = cn.CreateCommand();

After the variable declarations, the database connection is created and opened by means of a connection string, like the following, that is pulled from the configuration file (Web.config).

private string connectionString = 
    ConfigurationSettings.AppSettings["ConnectionString"];

The section read in the configuration file looks like the following.

<appSettings>
    <add key="ConnectionString" value="data source=(local);initial
                    catalog=NorthwindX;integrated security=SSPI;"/>
</appSettings>

In continuing the code for the GetSupplierProducts method, the next step is to set up the DataSet to hold the search results. That step begins with creating a table (dt) with the following required columns.

DataTable dt = new DataTable();
dt.Columns.Add("ProductName", Type.GetType("System.String"));
dt.Columns.Add("SupplierID", Type.GetType("System.String"));
dt.Columns.Add("SupplierName", Type.GetType("System.String"));
dt.Columns.Add("CategoryID", Type.GetType("System.String"));
dt.Columns.Add("CategoryName", Type.GetType("System.String"));
dt.Columns.Add("UnitPrice", Type.GetType("System.Double"));
dt.Columns.Add("UnitsInStock", Type.GetType("System.Int32"));
dt.Columns.Add("QuantityPerUnit", Type.GetType("System.String"));

The next step is to get the information for each supplier. The following code retrieves a supplier's identity.

supplierName = "G'day, Mate";
cmd.CommandText = "SELECT SupplierID FROM Suppliers WHERE" +
    " CompanyName='" + supplierName.Replace("'", "''") + "'";
supID = cmd.ExecuteScalar().ToString();

The identity is retrieved with a query that returns only one row and one column. Note that any citation mark (') in the company name needs to be handled.

The next step is to get the information from the supplier Web page — that is, to do the actual "screen scraping." The code to set up the HTTP request (and response) looks like the following.

req = (HttpWebRequest) WebRequest.Create(supplierWebUrl + "gdayprod.htm");
resp = (HttpWebResponse) req.GetResponse();
sr = new StreamReader(resp.GetResponseStream());
s = sr.ReadToEnd();

The supplier Web URL prefix (supplierWebUrl) is retrieved from the configuration file (just like the connection string mentioned earlier), and the actual product page (gdayprod.htm) is added to make up the complete URL. A stream reader captures the response and in turn reads the stream into a string. This process is possible for smaller Web pages, but for larger pages, a more serialized approach (for example, reading a row at a time from the stream) might be necessary.

The next step is to parse the page (string with HTML) to get to the product information. The HTML behind the page shown earlier in Figure 9 is as follows.

<HTML><HEAD><TITLE>G'Day Mate Products</TITLE></HEAD>
<BODY vLink=#800080 link=#0000ff background=gdaybck.gif>
<P align=right>
<TABLE cellSpacing=0 cellPadding=7 width=667 border=0>
    <TBODY>
    <TR>
        <TD vAlign=top width="18%">
            <P align=center><A name=Herbs><IMG height=76 
            src="gday1.gif" width=73><FONT 
            face=Verdana><BR></FONT><B><FONT face=Verdana
            color=#0000ff size=2>Herbs, 
            Nuts,<BR>&amp; Oils</B></FONT></A></P></TD>
        <TD vAlign=top width="29%"><FONT face=Verdana size=2>
            <BR>Gumleaf Oil<BR>Lemon Myrtle Leaf (Dried)<BR>Lemon
            Myrtle Oil<BR>Macadamia Nut Pieces<BR>Mountain Pepper
            (Dried)</FONT></TD>
        <TD vAlign=top width="27%"><FONT face=Verdana size=2>
            <BR>6 - 25 ml bottles<BR>12 - 50 gm packages<BR>6 – 25
            ml bottles<BR>6 - 250 gm packages<BR>12 - 50 gm
            packages</FONT></TD>
        <TD vAlign=top width="13%" align=right><FONT face=Verdana
        size=2>
            <BR>A$30.00<BR>A$96.00<BR>A$180.00<BR>
            A$49.50<BR>A$120.00</FONT></TD>
        <TD vAlign=top width="13%" align=right><FONT face=Verdana         size=2>
            <BR>6<BR>12<BR>24<BR>32<BR>71</FONT></TD>
        <TD vAlign=top width="13%">
            <P>&nbsp;</P></TD></TR>

If you analyze the HTML, you see that each of the interesting pieces of information is prefixed with the string "size=2>" and suffixed by either "</B>" or "</FONT>" (all in bold in the preceding code example). Inside these strings are a number of line breaks (and in the category, also a "<BR>" string) that need to be removed. Also, all extra spaces need to be removed.

The code that parses the preceding HTML looks like the following.

while(s.IndexOf("size=2>") > -1)
{
    // Get category
    i = s.IndexOf("size=2>");
    j = s.IndexOf("</B>", i);
    if(j < 0)
        break;
    t = s.Substring(i + 7, j - (i + 7));
    t = t.Replace("<BR>", "");
    t = t.Replace("\r\n", "");
    categoryName = string.Empty;
    if(t.IndexOf("Herb") > -1)
        categoryName = "Grains/Cereals";
    if(t.IndexOf("Jam") > -1)
        categoryName = "Condiments";
    if(t.IndexOf("Pasta") > -1)
        categoryName = "Grains/Cereals";
    if(t.IndexOf("Fruit") > -1)
        categoryName = "Confections";
    if(t.IndexOf("Sauce") > -1)
        categoryName = "Condiments";
    cmd.CommandText = "SELECT CategoryID FROM Categories" +
        " WHERE CategoryName='" + categoryName + "'";
    catID = cmd.ExecuteScalar().ToString();
    s = s.Substring(j);

    // Get product names
    i = s.IndexOf("size=2>");
    j = s.IndexOf("</FONT>", i);
    t = s.Substring(i + 7, j - (i + 7));
    t = t.Replace("<BR>", "¤");
    t = t.Replace("\r\n", "");
    while(t.IndexOf("  ") > -1)
        t = t.Replace("  ", " ");
    string[] productNames = t.Split(new char[] { '¤' });
    s = s.Substring(j);

    // Get quantities per unit
    i = s.IndexOf("size=2>");
    j = s.IndexOf("</FONT>", i);
    t = s.Substring(i + 7, j - (i + 7));
    t = t.Replace("<BR>", "¤");
    t = t.Replace("\r\n", "");
    while(t.IndexOf("  ") > -1)
        t = t.Replace("  ", " ");
    string[] quantityPerUnits = t.Split(new char[] { '¤' });
    s = s.Substring(j);

    // Get unit prices
    i = s.IndexOf("size=2>");
    j = s.IndexOf("</FONT>", i);
    t = s.Substring(i + 7, j - (i + 7));
    t = t.Replace("<BR>", "¤");
    t = t.Replace("\r\n", "");
    t = t.Replace("A$", "");
    while(t.IndexOf("  ") > -1)
        t = t.Replace("  ", " ");
    string[] unitPrices = t.Split(new char[] { '¤' });
    s = s.Substring(j);

    // Get units in stock
    i = s.IndexOf("size=2>");
    j = s.IndexOf("</FONT>", i);
    t = s.Substring(i + 7, j - (i + 7));
    t = t.Replace("<BR>", "¤");
    t = t.Replace("\r\n", "");
    while(t.IndexOf("  ") > -1)
        t = t.Replace("  ", " ");
    string[] unitsInStock = t.Split(new char[] { '¤' });
    s = s.Substring(j);

    // Create data rows
    for(int k = 1; k < productNames.Length; k++)
    {
        dr = dt.NewRow();
        dr["ProductName"] = productNames[k];
        dr["SupplierID"] = supID;
        dr["SupplierName"] = supplierName;
        dr["CategoryID"] = catID;
        dr["CategoryName"] = categoryName;
        dr["UnitPrice"] = unitPrices[k];
        dr["UnitsInStock"] = unitsInStock[k];
        dr["QuantityPerUnit"] = quantityPerUnits[k];
        dt.Rows.Add(dr);
    }
}
dt.AcceptChanges();

The category is extracted and replaced with a valid category name, which is then searched to retrieve a category identity. Then, the product information (name, quantity per unit, unit price, and unit in stock) is extracted and saved in arrays for each information type. Finally, a new row (dr) is created with the parsed information and added to the table (dt).

A similar procedure occurs for all suppliers. Then, the method (GetSupplierProducts) selects from the previously created table (dt) based on the search criteria provided as parameters to the method.

    string where = "0=0";
    if(!productName.Equals(string.Empty))
        where += " AND ProductName LIKE '%" + productName + "%'";
    if(!supplierID.Equals(string.Empty))
        where += " AND SupplierID='" + supplierID + "'";
    if(!categoryID.Equals(string.Empty))
        where += " AND CategoryID='" + categoryID + "'";
    DataRow[] drs = dt.Select(where, "ProductName");
    DataTable dtx = dt.Clone();
    foreach(DataRow drx in drs)
        dtx.Rows.Add(drx.ItemArray);
    dtx.AcceptChanges();
    DataSet ds = new DataSet();
    ds.Tables.Add(dtx);

    return ds;
}

A selection (variable where) is made from the table (dt) into an array of rows (drs). Then, the table structure is copied (cloned) into a new table (dtx), and the selected array of rows is inserted into this new table. Finally, the new table is added to a DataSet (ds) that is returned to the client.

As stated before, more suppliers can be added in the same manner. If a supplier supplies its own XML Web service, the code to include that information is even simpler.

Basic Bar-Code Scanner Support

As mentioned earlier in the procedure about inventorying products, you can select a product and view the product details screen shown in Figure 14. The code for the KeyUp event of the product number text box (numberTextBox) looks like the following.

private void numberTextBox_KeyUp(object sender,
    System.Windows.Forms.KeyEventArgs e)
{
    if(e.KeyCode == Keys.Return)
    {
        int productNo = 0;
        try { productNo = Convert.ToInt32(numberTextBox.Text); } 
        catch {}
        if(productNo > 0)
        {
            string productID;
            using(ProductHandler productHandler = new ProductHandler())
                productID = productHandler.GetIDForNo(productNo);
            parentForm = true;
            ProductForm productForm = (ProductForm)
                FormCache.Instance.Load(typeof(ProductForm));
            productForm.FormMode = formMode;
            productForm.ProductID = productID;
            FormCache.Instance.Push(typeof(ProductForm));
            e.Handled = true;
        }
    }
}

If you enter a product number on the Inventory screen, and then press ENTER on the on-screen keyboard, and if the input field holds an integer value (a product number), the product identity is retrieved, and the product detail form opens.

Correctly configured, most bar-code scanners have the ability to enter a product number on the Inventory screen and then send an ENTER keystroke. However, most bar-code scanners have even more sophisticated abilities (even with support for .NET Compact Framework) like custom controls that are barcode scanner aware. Such controls increase the control of the scans (such as giving detailed exceptions when something goes wrong) but make support for multiple bar-code scanners much more complicated.

Main Menu ListView

Using a ListView as a menu, as in the main form (MainForm), can be somewhat tricky because it must be set up correctly to work correctly. First, the ListView activation should be set up as follows.

menuListView.Activation = ItemActivation.OneClick;

The activation option (OneClick) indicates that an item in the ListView is activated (the ItemActivate event is fired) by a single tap. Then, in the Activate event of the ListView, the following code handles the menu selections.

private void menuListView_ItemActivate(object sender,
    System.EventArgs e)
{
    if (menuListView.SelectedIndices.Count > 0)
    {
        int selectedItemNo = menuListView.SelectedIndices[0];
        switch (selectedItemNo)
        {
            case 0: // Inventory
                inventory();
                break;
            case 1: // Products
                products();
                break;
            case 2: // Suppliers
                suppliers();
                break;
            case 3: // Options
                options();
                break;
            case 4: // About
                about();
                break;
        }
        menuListView.Items[selectedItemNo].Selected = false;
    }
}

Note that the last line of code is necessary to allow selection of the same menu command more than one time — because the Activate event will not be fired when an already selected item is tapped.

Reuse of Similar Forms

As mentioned in the Application Design section toward the beginning of the article, the product list and product detail forms are used for both inventory and general product information. Either of these forms is implemented through a form mode parameter described in the following code. This technique is often used in Web applications to make a specific Web page support multiple functions, but it can also be used with similar success in a Windows-based application.

Both forms use the following enumeration (declared in the Common class) to indicate their current modes.

public enum ProductFormMode : int
{
    Products,
    Inventory
}

When you tap the Inventory icon on the main form (MainForm), the application uses the following code to create the inventory form.

ProductListForm productListForm = (ProductListForm)
    FormCache.Instance.Load(typeof(ProductListForm));
productListForm.FormMode = ProductFormMode.Inventory;
FormCache.Instance.Push(typeof(ProductListForm));

When you tap the Products icon, the application uses the following code.

ProductListForm productListForm = (ProductListForm)
    FormCache.Instance.Load(typeof(ProductListForm));
productListForm.FormMode = ProductFormMode.Products;
FormCache.Instance.Push(typeof(ProductListForm));

In both of the preceding code examples, note how the form mode is set before the form is shown (pushed). For more information about the form cache (FormCache), see the section Form Cache and Stack.

The property in the product list form (ProductListForm) looks like the following.

public ProductFormMode FormMode
{
    set { this.formMode = value; }
}

The application then uses the form mode to set the heading of the form as follows.

headingLabel.Text = Common.ProductFormModeText[(int)formMode];

The application retrieves the text by using the following array (declared in the Common class).

public static readonly string[] ProductFormModeText = new string[]
{
    GlobalHandler.Translate.Text("ProductFormModeProducts",     "Products"),
    GlobalHandler.Translate.Text("ProductFormModeInventory",     "Inventory")
};

Note that the texts in the array are translated (localized) if required. (For more information, see the later "GlobalHandler Class" section.)

When the products detail form is opened from the product list form, the form mode is passed on in the same manner as for the product list form. In the product details form (ProductForm), the form mode is used to modify the user interface as follows.

if(formMode != ProductFormMode.Inventory)
{
    actualNumericUpDown.Visible = false;
    updateButton.Visible = false;
    findMenuItem.Enabled = false;
}

If the form is not in inventory mode (from the main menu), the ability to update the current number of items in stock is removed.

Though efficient for similar forms, this technique should be used with care because too many differences between the forms will generate complicated code that will be expensive to maintain. An alternative approach is to use inheritance with the common parts of the forms in a base class, although this approach requires efficient form design. Note also that inheritance prevents the use of the forms designer in Visual Studio .NET 2003.

Implementation of Help

All applications should include Help support. If you are on the Today screen and tap Start and Help, you will open the Help system on your Pocket PC. The Help system looks in the \Windows\Help folder that contains shortcuts and dynamically creates the menu that appears on your screen. The list links to Help files installed on your system. The linked shortcuts point at Help files in the \Windows folder.

To add your own Help file:

  1. Create the help file and place it in the \Windows folder.
  2. Make a shortcut to that file and place the shortcut in the \Windows\Help folder.

To create a Pocket PC Help file, which is really a plain HTML file, you can either write the HTML directly in any text editor or use a standard WYSIWYG HTML editor. In either case, you should write content that is compliant with most of HTML 3.2. You will not use scripting, Dynamic HTML (DHTML), and XML, but you will use special HTML comments that are important for the Help system.

The beginning of the sample Help file is as follows.

<HTML>
<HEAD>
<META HTTP-EQUIV="Htm-Help"
    Content="Inventory.htm#Main_Contents">
<TITLE>Pocket Inventory Help</TITLE>
</HEAD>
<BODY BGCOLOR=#ffffff TEXT=#000000>
<!-- PegHelp -->

<A NAME="Main_Contents"></A><B>Pocket Inventory
    Help</B><BR>
<BR>
<B>Concepts</B><BR>
<A HREF="Inventory.htm#About">About Pocket
    Inventory</A><BR>
<BR>
<B>How To</B><BR>
<A HREF="Inventory.htm#DoProductInventory">Do
    Product Inventory</A><br>
<BR>
<B>Dialogs</B><BR>
<A HREF="Inventory.htm#LoginForm">Login Form</A><BR>
<A HREF="Inventory.htm#MainForm">Main Form</A><BR>
<A HREF="Inventory.htm#InventoryForm">Inventory
    Form</A><BR>
<A HREF="Inventory.htm#InventoryDetailForm">Inventory
    Detail Form</A><BR>
<A HREF="Inventory.htm#ProductListForm">Product
    List Form</A><BR>
<A HREF="Inventory.htm#ProductForm">Product Form</A><BR>
<A HREF="Inventory.htm#OptionsForm">Options Form</A><BR>
<A HREF="Inventory.htm#SupplierListForm">Supplier
    List Form</A><BR>
<A HREF="Inventory.htm#SupplierForm">Supplier
    Form</A><BR>
<A HREF="Inventory.htm#SupplierProductListForm">Supplier
    Product List Form</A><BR>
<A HREF="Inventory.htm#SupplierProductForm">Supplier
    Product Form</A><BR>
<BR>
<FONT SIZE=2>Copyright &copy;2005 Microsoft
    Corporation. All rights reserved.</FONT>

<BR CLEAR=all>
<!-- PegHelp --><HR>

<!-- *********************** Topic Break ************************ -->

<P><A NAME="About"></A><B>About Pocket Inventory</B></P>
<!-- CS topic for About dialog -->
<P>
<IMG SRC="..\Program Files\Inventory\about.bmp">
</P>
<P>
Pocket Inventory is a sample application accompanying the
article "Northwind Pocket Inventory: Logistics for Windows
Mobile-based Pocket PCs." Its purpose is simply to
show various functionality available in Microsoft .NET
Compact Framework.
</P>

<BR CLEAR=all>
<!-- PegHelp --><HR>

<!-- *********************** Topic Break ************************ -->

First, you need to include the <META> tag that points to the main topic in the Help file. That topic must also have exactly the name Main_Contents. The Help system will jump to a tag with this name when the user taps the Help file shortcut.

Another important construct is the special HTML comment <!-- PegHelp --> that separates topics for the Help system. You should place it directly after the <BODY> tag and then after each topic in the file. As shown in the preceding code, there is a common practice to place <HR> just after each topic separator (<!-- PegHelp -->) because it helps when you view the file in a normal HTML browser (or the HTML creation tool of your choice). When the Help system uses the file on the device, horizontal rules will not be shown.

Another common practice is to add a <!-- **** Topic Break **** --> comment after each topic separator. The reason is the same as with <HR>, but in this case, the addition is for editing the file in HTML (like in a text editor). Yet another common practice is to include a comment for each of the topics that are used as context-sensitive Help in your application. You can see an example of this practice in the <!-- CS topic for About Dialog --> comment in the preceding code.

The "About" topic also includes an image: the application logo. The logotype image is in bitmap (.bmp) format because this and the older bitmap (.2bp) format are the only ones that the Help system supports. The default location for images is the \Windows folder. There is a relative path to the image to prevent the \Windows folder from being clogged with application-specific image files.

If you use the same topic names, you can translate the Help files into localized versions because it does not require you to change the code of the application.

Figure 35 shows what the first screen of the Help system looks like.

Figure 35. Help file sample

As always with Pocket PCs, you should minimize the space that the application uses. When you create a Help file, you should:

  • Write efficiently to convey as much information in as few words as possible. For example, use "File > Open" in favor of "On the File menu, tap Open."
  • Try to use as few images as possible. Consider making any necessary images black and white.
  • Use as few HTML tags as possible because you really want the text to come through, not the formatting.
  • Include the Help file name in each of the links because the Help system requires it.

Although you should minimize space, remember that none of your users will carry instruction manuals with them. The Help file has to provide enough text to help users solve the problems they encounter when they use the application. Try to include a "How To" section in your Help file that guides your users through the most common tasks that they will perform with your application.

For more information about creating Help files, see the article Developing Microsoft Windows CE Help for the Pocket PC and Handheld PC.

Now, let's look at how this Help file is used from the sample application.

The Help command on the Start menu uses a native Windows message (WM_HELP) to notify an application that it has been selected. A .NET Compact Framework application cannot normally intercept such messages, but a replacement for the System.Windows.Forms.Application class called ApplicationEx is included in the Smart Device Framework from OpenNETCF. When the correct reference (OpenNETCF.Windows.Forms) and equivalent namespace have been added, the Main method of the application is modified to look like the following.

public static void Main()
{
    ApplicationEx.Run(new MainForm());
}

After the Main method of the application is modified, a filter class that captures Windows messages can be added to the application. The following code from the main form constructor adds the message filter class.

ApplicationEx.AddMessageFilter(new MessageFilter());

This message filter class (MessageFilter) looks like the following.

public class MessageFilter : IMessageFilter 
{
    private const int WM_HELP = 0x53;

    public bool PreFilterMessage(ref Message m)
    {
          if (m.Msg == WM_HELP)
        {
            string s = Common.Values.HelpTopic;
            if(!s.Equals(string.Empty))
                s = "#" + s;
            Process.Start("peghelp.exe", @"Inventory.htm" + s);
            return true;
        } 
        return false;
    }
}

The message filter class defines the Help message constant (WM_HELP). When such a message is received, a new process starts loading the Pocket PC Help system (peghelp.exe) with the Help file and the current Help topic (Common.Values.HelpTopic) as a parameter. If the Help topic is empty, no subtopic is supplied, and the main topic (Main_Contents) is loaded.

The current Help topic is declared in the singleton class (Common) as follows.

private string helpTopic = string.Empty;
public string HelpTopic
{
    get { return helpTopic; }
    set { helpTopic = value; }
}

This property is then set by means of the singleton (only) instance (Common.Values) in each form activation (Activated) event. In the main form (MainForm), the property is simply set as follows.

Common.Values.HelpTopic = string.Empty;

The property is set in the activation event of the supplier form (SupplierForm) as follows.

Common.Values.HelpTopic = "SupplierForm";

In the activation event of the inventory form and the product list form (ProductListForm), the code looks like the following.

Common.Values.HelpTopic = "InventoryForm";
if(formMode != ProductFormMode.Inventory)
    Common.Values.HelpTopic = "ProductListForm";

The Help topic is set depending on the mode (formMode) in which the form was opened.

Web Services for Application Integration

The best way to standardize application integration is to use XML Web services, as shown by the product information and purchase order functionality of the sample application. However, less sophisticated methods can sometimes be used to communicate with a server. For example, the application offers a feature to show an image of a product, as shown earlier in Figure 15 and Figure 16. The application uses the HTTP protocol to retrieve the image that is directly streamed into a PictureBox control. The following code enables this functionality.

HttpWebRequest req = (HttpWebRequest)WebRequest.Create(
    Common.Values.ProductImageURL + productNoLabel.Text + ".jpg");
HttpWebResponse resp = (HttpWebResponse) req.GetResponse();
productPictureBox.Image = new
    System.Drawing.Bitmap(resp.GetResponseStream());

The product number (taken from the productNoLabel label) is used to form the complete URL for the image. The request (HttpWebRequest) and response (HttpWebResponse) objects are used to get the image as a stream. Because the Bitmap constructor can create a new bitmap by using a stream, the final code is straightforward. And because the image is never stored on the Pocket PC, this approach is memory efficient. However, note that if a network connection is not available, the image cannot be shown.

Similarly, anything that resides on a Web server can be retrieved and used directly and does not have to be stored locally. Note also that the image is retrieved only when the Get image button (shown earlier in Figure 15) is tapped. This way of making the images available on demand is efficient with regard to memory usage and bandwidth usage.

GlobalHandler Class

Any enterprise Pocket PC solution that is intended to be used in more than one language needs to be easily translated. The GlobalHandler class can be used to translate complete forms, including all controls and menus. It requires only the following single line of code in each form.

GlobalHandler.Translate.Form(this);

Even simple texts, such as error messages, can be translated by means of the following code.

MessageBox.Show(GlobalHandler.Translate.Text("MsgCantOpenWebPage",
    "Could not open web page!"), this.Text);

For more details about the management and code related to globalization, please see the article Northwind Pocket Sales: Field Sales for Windows Mobile-based Pocket PCs.

Form Cache and Stack

Each enterprise application that contains a large amount of forms requires the forms, and the memory they consume, to be managed in an efficient way. The FormCache class supports both the caching and the stacking of forms. Briefly, the loading of a new form, or actually pushing a new form on the form stack, requires the following code.

FormCache.Instance.Push(typeof(SalesForm));

The push implicitly loads the form if it is not already loaded. And if any parameters need to be passed to the new form, the code looks like the following.

OptionsForm optionsForm = (OptionsForm)
        FormCache.Instance.Load(typeof(OptionsForm));
optionsForm.DatabaseExist = databaseExist;
FormCache.Instance.Push(typeof(OptionsForm));

For more details about the management and code related to caching and stacking forms, please see the article Northwind Pocket Sales: Field Sales for Windows Mobile-based Smartphones.

Conclusion

By taking advantage of mobile technology, any business can improve its business processes and the information technology solutions that support those processes. Connecting the business and its systems to partners in an efficient way can create new and distinct benefits that can be measured in time and money. The technological tools are in place today to make such a solution possible. By providing a complete sample, this article can help you start immediately.

Did you find this helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft. All rights reserved.