Northwind Pocket Inventory: Logistics for Windows Mobile-based Smartphones

 

Christian Forsberg
Business Anyplace

August 2005

Applies to:
    Windows Mobile 2003 software for Smartphones
   Microsoft Visual Studio .NET 2003
   Microsoft .NET Compact Framework version 1.0

Summary: Learn how to develop mobile enterprise logistic applications for Smartphones that run Windows Mobile 2003 software by using Visual Studio .NET, the .NET Compact Framework, and the Windows Mobile 2003 SDK. The source code in this article implements server components, a database, and a Windows Mobile 2003–based Smartphone client. (47 printed pages)

Download Northwind Pocket Inventory - Logistics for Windows Mobile 2003 SP.msi from the Microsoft Download Center.

Contents

Introduction
Northwind Traders's Field Inventory Business Process
Application Design
Northwind Pocket Inventory Walkthrough
Code Walkthrough
Conclusion

Introduction

This article continues on the topics introduced in the following articles:

The first article describes key elements about how to develop mobile field service applications, and the second article focuses more on developing these applications for Smartphones. The third and fourth articles focus on developing mobile field sales applications. The fifth article introduces the use of mobile devices as logistic tools and provides an introduction to a merged inventory and purchase process. It also covers an in-house sample application for supporting that process.

This article is about designing and developing a mobile field inventory application based on the .NET Framework, the Windows Mobile platform for Smartphones, and the .NET Compact Framework. The sample solution described in this article addresses the needs of the fictitious company Northwind Traders from a logistic process and field inventory point of view. From a technology point of view, the sample demonstrates how to connect directly to a server database (Microsoft SQL Server 2000) over the Internet by using its XML support (Web Services Toolkit or SQL XML), how to use a handler for validating data entry by using regular expressions, how to implement simple bar-code scanner support, and more.

Northwind Traders's Field Inventory Business Process

The customers of Northwind Traders have vending machines that sell the various products from Northwind Traders. As described in the article Northwind Pocket Inventory: Logistics for Windows Mobile–based Pocket PCs, the control and decision in the logistic process can be brought closer to the origin of information. To take this one step further, the field workers (service technicians and salespersons) can be allowed not only to update the items in stock (in their vans), but also to make purchase orders. Purchase orders can be created not only from the standard supplier, but from other suppliers that provide similar products. Purchase orders require the most up-to-date information, and, therefore, the system connects directly to the suppliers to get product information before selecting a product to purchase. This merged field inventory and purchase process includes the following steps:

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

The process is shown in Figure 1.

Figure 1. Field inventory and purchase process

Assuming you have done a good job of defining the new process, the next step is to look at the design of the solution.

Application Design

The article Northwind Pocket Service: Field Service for Windows Mobile–based Pocket PCs provides a good introduction to the architectural work in a mobile solution. The article Northwind Pocket Inventory: Logistics for Windows Mobile–based Pocket PCs includes a description of the most important deliverables (artifacts) in the application design, and the design really begins with the definition of the use cases. Figure 2 shows the use cases for the sample application.

Figure 2. Use case model

Another very important artifact to create early in the design process is the Dialog Model and sample forms. Figure 3 illustrates the dialog model for the sample application.

Figure 3. Dialog model

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

Figure 4 shows some sample dialogs.

Click here for larger image

Figure 4. Sample dialogs. Click the thumbnail for a larger image.

The sample dialogs are drawn in a general drawing tool (in this case, Microsoft PowerPoint), and it is a clear recommendation that someone knowledgeable in user interface design should be involved in creating dialogs like these. The early visualization of the application dialogs gives users and other stakeholders an opportunity to understand how the application will look and work. Changes are also very easy to make at this stage.

In the download sample, you can find all of the preceding figures in a PowerPoint presentation that you can reuse when creating your own diagrams.

Validation Using Regular Expressions

Validation is about quality and security. Quality in input is critical in any enterprise application scenario. The golden rule of "trash in, trash out" will always apply, and validation is there to prevent the "trash in" part. Any experienced system owner knows that the better the quality of the information at the origin (the entry point), the better the whole system will perform. Also, data entry errors tend to escalate through any system.

As many have mentioned in the discussion on security, "all input is evil." The main message is that you just shouldn't trust any input until it has been validated. When you are dealing with any application that connects directly to a back-end database, validation instantly becomes even more important. Even text boxes — although they seem to allow anything to be entered — can definitely pose a risk. Things like SQL injection attacks can be prevented by either preventing the input of certain characters or by handling them (like doubling apostrophes).

Regular expressions can be used for searching strings or files. As you may have noticed, you can actually use regular expressions to search for text in the development environment (Visual Studio .NET). For example, you can search for the following.

Category(ID|Name)

This search will result in the match for both "CateogryID" and "CategoryName". A well-constructed search string based on regular expressions can save many normal search commands. When you are replacing items, it can be even more powerful.

More importantly for this discussion, regular expressions are a very useful construct in validating text boxes. With them, you can ensure that the entered data is in the expected format. Because the .NET Compact Framework has a solid support for regular expressions, that power should be used. With a regular expression, a string can be checked for validity against a pattern string. The regular expressions are the patterns for checking a string. If you want to check the validity of a password, the regular expression can look like the following.

^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,8}$

The password must be at least four characters and no more than eight characters, and it must include at least one uppercase letter, one lowercase letter, and one numeric digit.

Another example is to ensure the validity of a date as follows.

^(((0?[13578]|1[02])\/(0?[1-9]|[12]\d|3[01])\/
((1[6-9]|[2-9]\d)?\d{2}))|((0?[13456789]|1[012])\/
(0?[1-9]|[12]\d|30)\/((1[6-9]|[2-9]\d)?\d{2}))|(0?2\/
(0?[1-9]|1\d|2[0-8])\/((1[6-9]|[2-9]\d)?\d{2}))|
(0?2\/29\/((1[6-9]|[2-9]\d)?(0[48]|[2468][048]|
[13579][26])|((16|[2468][048]|[3579][26])00)|00)))$

This regular expression check dates in the U.S. format MM/dd/yyyy from 1600 through 9999, and it can even handle leap years. Note that there should be no line breaks inside the regular expression.

There is a lot that can be checked with regular expressions, and if you haven't looked into this powerful technology yet, now is a good time to start. In the walkthrough of the sample code (the next section of this article), you will see how regular expressions can be used to validate user input.

Although regular expressions will not solve all of your input validation, it can work as a first line of defense in making sure that the correct characters, in the correct format, are entered.

A good reading on the subject is the sample chapter (10) of the book Writing Secure Code, Second Edition by Michael Howard and David LeBlanc. Also, there's a good tutorial on regular expressions, in addition to some common examples, at Regular-Expressions.info.

Northwind Pocket Inventory Walkthrough

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

The application shows how to support a merged field inventory and purchase business process by using a mobile device. The design choices align to the process as much as possible and also maximize efficiency for the mobile worker. Note that this article explores parts of the code after it describes the application user interface design.

Login

When the application is started, the first screen is the Login screen, as shown in Figure 5.

Figure 5. Login screen

The Login screen is branded with an application symbol, but a login screen like this can be used for many things. It can be used to show notification messages about news, new features, important changes, and so on. This screen can also be a good place to inform about copyrights and license conditions.

The user enters the user name and password, and then presses Done. If the user name and password are valid, the main menu appears, as shown later in Figure 7. If the logon attempt is not successful, an error message appears, and then the application displays the Options screen, as shown later in Figure 32. The reason for showing the Options screen is that the user may want to change the setting (URL) for connecting to the server, and whether the setting was correct or not, the user will be able to make new login attempts by using the Login screen. If the user presses Cancel, the application closes.

Before the application makes a logon attempt, a validation control check is performed on the password provided. If the password is not valid, the screen in Figure 6 appears.

Figure 6. Password validation error

When the user presses OK, the Login screen appears again to allow another entry attempt.

The functionality of the application is aligned with the business process of the mobile field worker. The main steps in this process are to conduct the inventory, determine the need for additional products, evaluate the default supplier in addition to alternative suppliers, and finally create a purchase order. The user can access this functionality by selecting the first menu option (Inventory) on the main menu shown in Figure 7.

Figure 7. Main menu

The other options on the main menu include support functionality for looking up products (Products), looking up suppliers (Suppliers), entering application settings (Options), and getting information about the application (About).

Inventory

The first step in the inventory process is to conduct the actual inventory. Selecting Inventory on the main menu screen displays the Inventory screen shown in Figure 8.

Figure 8. Inventory product search criteria

This screen enables users to search for products for the inventory. It is implemented as a two-step wizard, and the first step enables the user to enter search criteria like product number, product name, product category, and product supplier. When the user presses Next, the next step in the wizard appears with the matching products presented in a list. To check the inventory for a product, the user selects a product in the list and then selects View on the menu, as shown in Figure 9.

Figure 9. Inventory product search result

Then, the detailed product information appears, as shown in Figures 10, 11, and 12. The product information includes the product number, unit price, quantity per unit, reorder level, unit currently on order, product category, product supplier, and most importantly, the current number of units in stock.

Figure 10. Inventory product details (scrolled to the top of the screen)

Figure 11. Inventory product details (scrolled to the middle of the screen)

Figure 12. Inventory product details (scrolled to the bottom of the screen)

The user can also go directly to this screen if he or she enters a product number (see Figure 8) and then presses the Action key. By using this method, you can emulate a bar-code scanner. If you have a bar-code scanner, it can probably be set up to scan directly into the product number text box, and then (if configured correctly) it will send a KeyUp event that simulates an additional press of the Action key.

On the Product screen shown in Figures 10 through 12, the user can view the details about the product supplier by selecting View Supplier on the menu. Figure 13 and Figure 14 show the supplier details.

Figure 13. Supplier details (scrolled to the top of the screen)

Figure 14. Supplier details (scrolled to the bottom of the screen)

Also, on the product information screen, the user can update the number of units in stock by selecting Update Stock on the menu. The read-only field (label) with the number of units in stock then becomes a text box, as shown in Figure 15.

Figure 15. Update 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 the user enters a new value, and then presses Done, an update is made instantly in the server database. When the update is completed, a confirmation message appears, as shown in Figure 16.

Figure 16. Units in stock updated

When the inventory is completed, the newly entered information (that is, units in stock) may signal the field worker about a need for purchasing new items of this product. This signal may come from the server (back-office system) as an addition to the confirmation message shown in Figure 16, or it may be a decision that the field worker makes when evaluating the product information.

But before ordering new items of this product, the user might want to consider alternative products to order. That option is available through the Find alt. products command at the bottom of the product information screen, shown earlier in Figure 11. Selecting this command takes the user to the screen that displays information about products available from different suppliers (shown in Figure 17). Here, the user can enter search criteria like product name, product category, and product supplier.

Figure 17. Supplier products search criteria

In Figure 17, only two suppliers can be queried online for product information. When the user presses Next, the application makes an online request to an XML Web service hosted on a central corporate server. That XML Web service works as a real-time gateway for supplier information that either makes a direct call to an XML Web service hosted by the supplier (if one is available) or queries the suppliers' product information on an extranet Web site hosted by the supplier. Then the application presents the matching products in a list, as shown in Figure 18.

Figure 18. Supplier products search results

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

For more details about a product, the user can select a product in the list and then select the View menu command. Figure 19 and Figure 20 show the supplier product details.

Figure 19. Supplier product details (scrolled to the top of the screen)

Figure 20. Supplier product details (scrolled to the bottom of the screen)

The application then shows the detailed supplier product information, which includes the unit price, number of units in stock, quantity per unit, product category, and product supplier.

On this screen, the user can also view the details about the product supplier by selecting the View Supplier menu command. Figure 21 and Figure 22 show the supplier details.

Figure 21. Supplier details (scrolled to the top of the screen)

Figure 22. Supplier details (scrolled to the bottom of the screen)

If the supplier has a Web site, the user can view that site directly by selecting View site. This action opens Internet Explorer on the Smartphone with the supplier's home page loaded, as shown in Figure 23.

Figure 23. Supplier Web site

Now, go back to the supplier product information screen, shown in Figures 19 and 20. If this is an interesting alternative product to buy, the user can initiate a purchase by first selecting the Purchase menu command shown in Figure 19. The application will then show a screen where the user can enter purchase quantity, as shown in Figure 24.

Figure 24. Enter purchase quantity

After the user enters a quantity, he or she can make a purchase by pressing Done, which generates an XML Web service request to place the actual purchase order. In a real-world scenario, this request would probably create the purchase order in the enterprise's back-office system that could in turn create a purchase order at the supplier in real time. When the purchase order request is completed, a confirmation message appears, as shown in Figure 25.

Figure 25. 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. Purchase orders made from the warehouse floor can be sent more or less directly to the supplier. Such efficiency improvements can really save time and money for any business.

This concludes the mobile application's support for the merged field inventory and purchase business process. This article will continue to describe some of the application's support functions.

Products

When the user selects Products on the main menu, he or she can search for products in stock by number, name, category, and supplier, as shown in Figure 26.

Figure 26. Product search criteria

When the user presses Next, the search results appear on the next screen, as shown in Figure 27.

Figure 27. Product search result

When the user selects a product in the list and then selects the View menu command, the product details appear, as shown in Figure 28.

Figure 28. Product details

The screen shown in Figure 28 is actually the same as the screen shown in Figure 10, and the functionality is identical. However, the options to update the number of items in stock and search for alternative products are not available here because this is general support functionality to look up product details — not part of the inventory process. The inventory process functionality is available in the Inventory main menu option.

Suppliers

Just as for products, there is an option to view the suppliers by selecting Suppliers on the main menu. Here, the user can search for suppliers by company name and contact name, as shown in Figure 29.

Figure 29. Supplier search criteria

When the user presses Next, the search results appear on the next screen, as shown in Figure 30.

Figure 30. Supplier search result

When the user selects a product in the list, and then selects the View menu command, the supplier details appear, as shown in Figure 31.

Figure 31. Supplier details

The screen shown in Figure 31 is actually the same as the screen shown in Figure 13 and Figure 21, and the functionality is identical.

Options

The Options screen, as shown in Figure 32, appears when the user selects Options on the main menu.

Figure 32. Options

The Connection (URL) options are used to connect to the server database. Template (v.name) is the virtual name (a term used with SQL XML; for more information, see the Code Walkthrough section later in this article) representing the path to the directory holding the XML schema used when the database is updated. Schema (filename) is the actual file name of the XML schema. Finally, Web Service (URL) is the URL for the XML Web service that is used to handle both the searching of supplier products and the placing of purchase orders.

About

All applications should include a screen with the product name, version, copyright, and other legal information. On the main menu, the About option displays this information, as shown in Figure 33.

Figure 33. About the application

This screen can also include a link to a Web page with product and support information.

This completes the walkthrough of the application, but because the application supports globalization (and localization), a few translated screens are also shown in the following section.

World Ready

When the user changes the regional settings (Start > Settings > More > Regional Settings) to Portuguese (Brazil) on the Smartphone and then restarts the application, the complete application is translated. The following figures are the Brazilian Portuguese versions of the screens shown in Figures 5 to 10.

Figure 34 shows the translated Login screen, and you can see that the form's title bar and the form's controls are translated.

Figure 34. Translated Login screen

If the user enters an invalid password, even the validation error message is translated, as shown in Figure 35.

Figure 35. Translated password validation error

Figure 36 shows the translated main menu screen, and you can see that the menu options are translated.

Figure 36. Translated main menu

Figure 37 shows the translated inventory product search criteria screen. Just as before, the form title and form controls (labels and soft keys) are translated.

Figure 37. Translated inventory product search criteria

On the inventory product search result screen, you can also see how menu options are translated, as shown in Figure 38.

Figure 38. Translated inventory product search result

In Figure 39, you can see the translated product details screen.

Figure 39. Translated inventory product details

This concludes the walkthrough of the client application.

Code Walkthrough

The previous section provided an example client scenario for the Pocket Inventory application, and now it's time to look at the source code of that sample. The article Northwind Pocket Service: Field Service for Windows Mobile–based Smartphones covers the general parts of the code, so the focus in this article will be some of the unique aspects of the sample.

Most of the code in the sample application is reusable, but the functionality related to XML access to SQL Server 2000 can be reused with great benefits, because this type of functionality enables access to the server database in a standard way (by using HTTP and XML).

Connecting to SQL Server 2000 by Using SQL XML

This code walkthrough starts by examining the way that the sample application connects directly to the server database by using XML. This approach is based on the SQL Server 2000 Web Services Toolkit, also known (and from now on referred to) as SQL XML, which you can download from the Microsoft Download Center.

SQL XML includes a number of interesting technologies that allow data to be retrieved and updated through XML. First, in Internet Information Services (IIS), you can set up a special type of virtual directory that works as a publication of a SQL Server database. You set up the virtual directory by using a tool (IIS Virtual Directory Management for SQL XML) that comes with SQL XML. Through that virtual directory, HTTP calls can be made to query or to update the database. The implementation allows HTTP GET and HTTP POST. If you set up a virtual root to the sample database, NorthwindX, with the same name, you can test the technology by using a simple HTTP GET in a Web browser as follows:

https://server/NorthwindX?sql=SELECT+*+FROM+Shippers+FOR+XML+AUTO,ELEMENTS&root=NewDataSet

The result is as follows.

<NewDataSet>
    <Shippers>
        <ShipperID>7E6479F0-6C7F-4FDF-8F1D-1E74DA0FA109</ShipperID> 
        <CompanyName>Federal Shipping</CompanyName> 
        <Phone>(503) 555-9931</Phone> 
    </Shippers>
    <Shippers>
        <ShipperID>51A02FC5-4AE5-4A09-B833-42D2A493CB4A</ShipperID> 
        <CompanyName>United Package</CompanyName> 
        <Phone>(503) 555-3199</Phone> 
    </Shippers>
    <Shippers>
        <ShipperID>8B821D70-62BD-471F-80E8-73E30F84B5C1</ShipperID> 
        <CompanyName>Speedy Express</CompanyName> 
        <Phone>(503) 555-9831</Phone> 
    </Shippers>
</NewDataSet>

A corresponding HTTP POST request looks like the following.

<NewDataSet xmlns:sql='urn:schemas-microsoft-com:xml-sql'>
    <sql:query>
        SELECT * FROM Shippers
        FOR XML AUTO,ELEMENTS
    </sql:query>
</NewDataSet>

The response to this POST looks exactly like the response from the HTTP GET request shown in the previous listing; the only difference is that the root tag (NewDataSet) includes the supplied namespace reference (xmlns:sql='urn:schemas-microsoft-com:xml-sql'). This HTTP interface allows any client to access the SQL Server 2000 database from anywhere on the Internet as long as the security is set up correctly.

Both the HTTP GET and POST can also be used to access stored procedures and templates. A template is a form of predefined requests that can be stored on the server and that are called from a client (optionally with parameters).

The toolkit also includes the ability to publish XML Web services directly from SQL Server 2000. In the setup of the virtual directory, you can publish stored procedures, user-defined functions, and templates as XML Web services. After a stored procedure is published, it can be called like any other XML Web service (set a Web reference, instantiate the class, and call the method name — usually the same as the name of the stored procedure). Now, create a stored procedure that looks like the following.

CREATE PROCEDURE spGetData
@sql NVARCHAR(4000)
AS
EXEC(@sql)
GO

When the stored procedure is published (using the virtual name soap of the type soap) with the name GetData, you can set it up by adding a a Web reference to any Visual Studio .NET project, and call it as follows.

WebServices.soap ws = new WebServices.soap();
int returnValue = 0;
DataSet ds = ws.GetData("SELECT * FROM Shippers", out returnValue);

Note that all XML Web services include the stored procedure's return code as the last parameter (must be declared out). With this simple arrangement to publish a stored procedure as an XML Web service, you have created a way to access and manipulate all your data in the database by using XML Web services. Although this arrangement is not recommended because of the security issues that it raises, it shows the power of the technology.

For the .NET developer, the toolkit also includes a number of managed classes. These classes resemble ADO.NET because they are implemented as a data adapter and a command object. Using these classes allows your .NET-connected application to use XML in a very natural way. However, these classes are not provided for .NET Compact Framework developers, so this article's sample includes the full source of a reusable .NET Compact Framework assembly that implements a subset of the managed classes for the full .NET Framework. The rest of this section is a walkthrough of this implementation and also an illustration of how these classes are used in the sample application.

The most important object in the implementation of the SQL XML managed classes is the SqlXmlCommand object. It represents an SQL command to execute against an SQL XML data source, and the constructor looks like the following.

private string connectionUrl;
public SqlXmlCommand(string connectionUrl)
{
    this.connectionUrl = connectionUrl;
}

The parameter (which is saved in the private variable, connectionUrl) is the URL of the database's virtual directory that is used in the main method of the class (ExecuteStream), which is implemented with the following code.

public Stream ExecuteStream()
{
    // Set up HTTP request
    HttpWebRequest request = (HttpWebRequest)
        WebRequest.Create(this.connectionUrl);
    request.ContentType = "text/xml";
    request.Method = "POST";

    // Set credentials (if supplied)
    if(this.credentials != null)
    {
        request.Credentials = this.credentials;
        request.AllowWriteStreamBuffering = true;
    }

    // Set up request content
    string content = "<" + this.rootTag +
        " xmlns:sql='urn:schemas-microsoft-com:xml-sql'";
    switch(this.commandType)
    {
        case SqlXmlCommandType.DiffGram:
            content += " sql:mapping-schema='" +
                this.schemaPath + "'>" + this.commandText;
            break;

        case SqlXmlCommandType.Sql:
            content += "><sql:query>" + this.commandText +
                "</sql:query>";
            break;
    }
    content += "</" + this.rootTag + ">";

    // Write content to request stream
    byte[] contentUTF8 = Encoding.Convert(Encoding.Unicode,
        Encoding.UTF8, Encoding.Unicode.GetBytes(content));
    request.ContentLength = contentUTF8.Length;
    Stream s = request.GetRequestStream();
    s.Write(contentUTF8, 0, contentUTF8.Length);
    s.Close();

    // Make HTTP request
    HttpWebResponse response = (HttpWebResponse)
        request.GetResponse();

    // Store response in memory (stream)
    StreamReader sr = new StreamReader(
        response.GetResponseStream());
    MemoryStream ms = new MemoryStream();
    byte[] responseBytesUTF8 = Encoding.UTF8.GetBytes(
        sr.ReadToEnd());
    response.GetResponseStream().Close();
    byte[] responseBytes = Encoding.Convert(Encoding.UTF8,
        Encoding.GetEncoding(this.outputEncoding),
        responseBytesUTF8);
    ms.Write(responseBytes, 0, responseBytes.Length);
    ms.Seek(0, SeekOrigin.Begin);

    // Check for errors, and throw managed if found
    XmlTextReader xr = new XmlTextReader(ms);
    while(xr.Read())
    {
        if(xr.NodeType == XmlNodeType.ProcessingInstruction)
            if(xr.Name == "MSSQLError")
            {
                SqlXmlException e = new SqlXmlException(
                    new Exception(xr.Value));
                e.ErrorStream = ms;
                throw e;
            }
    }

    // Reset memory stream and return it
    ms.Seek(0, SeekOrigin.Begin);
    return ms;
}

The code begins by setting up the HTTP POST request to the URL that is supplied with the constructor (content type is XML). If any credentials have been supplied (through the public Credentials property), these credentials are passed on to the HTTP request. Then, the content of the request is created, beginning with the root tag (specified by the public RootTag property) and including the command text (specified by the public CommandText property). The XML tags are inserted depending on the command type (specified by the public CommandType property). The only supported command types are Sql and DiffGram. The Sql type is used to send SQL queries to the database, and DiffGram types are used to manipulate data in the database. If the command is a DiffGram type, the schema to use for the data manipulation (specified by the public SchemaPath property) is included as an attribute in the root tag. The content is then converted to the encoding that SQL XML requires (UTF-8) and written to the request stream. The request is made, and the returned stream is converted to the desired output encoding (specified by the public OutputEncoding property) and saved in a memory stream. The stream is checked for errors that are thrown by means of a custom exception class (SqlXmlException) if they are found. If no errors are found, the memory stream is returned to the caller.

The SqlXmlCommand class also provides the functionality to execute a command that does not return any result, and the code looks like the following.

public void ExecuteNonQuery()
{
    this.ExecuteStream();
}

The command can also be executed to a supplied stream though the following code.

public void ExecuteToStream(Stream outputStream)
{
    MemoryStream ms = this.ExecuteStream();
    int size = 2048;
    byte[] buffer = new byte[2048];
    while(true) 
    {
        size = ms.Read(buffer, 0, size);
        if(size > 0) 
            outputStream.Write(buffer, 0, size);
        else 
            break;
    }
}

Although not included in the managed classes for the full .NET Framework, but in line with other managed (ADO.NET) providers, a method has also been supplied to handle requests that return only a single value.

public object ExecuteScalar()
{
    XmlTextReader xr = new XmlTextReader(this.ExecuteStream());
    DataSet ds = new DataSet();
    ds.ReadXml(xr);
    return ds.Tables[0].Rows[0][0];
}

This method simply uses an XML reader to load a DataSet object with the returned data, after which the first column of the first row is returned to the caller.

The SqlXmlAdapter object is the data adapter of the SQL XML managed classes implementation, and it is created as follows.

private SqlXmlCommand command;
public SqlXmlAdapter(SqlXmlCommand command)
{
    this.command = command;
}

The adapter depends on a SqlXmlCommand object, and that's why such an object must be passed to the constructor (which is saved in the private variable command) when an instance is created. The first use of the adapter is to fill a DataSet object with the returned data from the database, and the code for that looks like the following.

public void Fill(DataSet dataSet)
{
    XmlTextReader xr = new XmlTextReader(command.ExecuteStream());
    dataSet.ReadXml(xr);
    dataSet.AcceptChanges();
}

The command (SqlXmlCommand) object is used to make the request to the database, and it returns a stream that is read into the DataSet object through an XML reader (XmlTextReader). Because all imported data into the DataSet object is regarded as changes, you need to reset the DataSet object by using the AcceptChanges method to accept all changes that you made.

More interesting is probably the method that saves the changes in a DataSet object to the database. The method is implemented as follows.

public void Update(DataSet dataSet)
{
    MemoryStream ms = new MemoryStream();
    XmlTextWriter xw = new XmlTextWriter(ms, Encoding.UTF8);
    dataSet.WriteXml(xw, XmlWriteMode.DiffGram);
    ms.Seek(0, System.IO.SeekOrigin.Begin);
    StreamReader sr = new StreamReader(ms);
    SqlXmlCommandType commandType = command.CommandType;
    command.CommandType = SqlXmlCommandType.DiffGram;
    string commandText = command.CommandText;
    command.CommandText = sr.ReadToEnd();
    try
    {
        command.ExecuteStream();
    }
    finally
    {
        command.CommandText = commandText;
        command.CommandType = commandType;
    }
}

The nice thing about DiffGram command types is that the DataSet object natively supports them. You can extract the changes made to the DataSet object by using its WriteXml method with a special second parameter (XmlWriteMode.DiffGram). In the preceding code, this method is used to extract the changes made to the data (as a DiffGram type) and save the changes to a memory stream. When the command text and type of the command (SqlXmlCommand) object are saved, the type is set to DiffGram and the command text is retrieved from the memory stream. Finally, the request is made to the database (through the ExecuteStream method).

Before looking at the use of these classes, you should understand the main differences with the SQL XML managed classes for the full .NET Framework. The main differences are:

  • The SqlXmlCommand object is created with the URL of the database virtual directory. In the SQL XML managed classes for the full .NET Framework, this is a database connection string that is used with a specific provider.
  • The SqlXmlCOmmand method SchemaPath is a relative path to the database virtual directory. In the SQL XML managed classes for the full .NET Framework, this is either an absolute path or a relative path, depending on the BasePath property.
  • The only supported command types are Sql and DiffGram. The SQL XML managed classes for the full .NET Framework also support the UpdateGram and XPath templates.
  • The SqlXmlCommand object has a Credentials property to supply user credentials in the HTTP requests. The SQL XML managed classes for the full .NET Framework use the same model as any managed (ADO.NET) provider with user ID and password in the connection string or by using the credentials that the current user holds.
  • The SqlXmlCommand object supports the ExecuteScalar method (which is not supported in the SQL XML managed classes for the full .NET Framework).
  • The SqlXmlParameter class (from the SQL XML managed class for the full .NET Framework) is not provided.

Now it's time to look at how you can use these classes. One of the first things that happen after the login form is completed in the sample application is a test to determine whether the database connection (and credentials) is working (and valid). This test is accomplished through the following method in the CommonHandler class.

public void TestDatabaseConnection()
{
    SqlXmlCommand cmd = new SqlXmlCommand(Common.Values.ConnectionUrl);
    cmd.Credentials = new NetworkCredential(
        Common.Values.UserName, Common.Values.Password);
    cmd.CommandText = "SELECT COUNT(*) ProductsCount" +
        " FROM Products FOR XML RAW";
    int i = Convert.ToInt32(cmd.ExecuteScalar());
}

The basic structure is straightforward: the command is created, the credentials are set, the command text is set, and the database request is made. The similarity to using other managed (ADO.NET) providers is obvious. The reason for using a method call (ExecuteScalar) that returns a value (even if the value is not used) is deliberate to be sure that the query actually returns a value. Note how the URL of the virtual directory (ConnectionUrl), the user name (UserName), and the password (Password) are all supplied by the application's singleton class (only) instance (Common.Values). The connection URL and the user name are saved in the device registry when the application is closed (the password is not saved because it needs to be provided each time the application is started).

The way credentials are added to the command object is always the same as in the preceding code, and for the subsequent code snippets, it has been omitted. Also, if the user is connected to the company intranet by using a virtual private network (VPN) connection, and the credentials provided in the login form are used to log the user on to a Windows-based domain, the credentials are already there, and there's no need to supply them to each command object.

Now, you will see how the preceding code translates into HTTP. First is the HTTP request, as follows.

POST /NorthwindX/ HTTP/1.1
Content-Type: text/xml
Content-Length: 149
Connection: Keep-Alive
Host: server

<NewDataSet xmlns:sql='urn:schemas-microsoft-com:xml-sql'>
    <sql:query>
        SELECT COUNT(*) ProductsCount FROM Products
        FOR XML RAW
    </sql:query>
</NewDataSet>

The SQL command is simply embedded in the correct XML structure. Note that in the preceding code, some of the HTTP headers have been removed, and extra line breaks and indentation have been inserted to increase readability (the same is also true for all of the following HTTP requests and responses).

The HTTP response looks like the following.

HTTP/1.1 200 OK
Connection: close
Date: Sat, 12 Feb 2005 20:20:27 GMT
Server: Microsoft-IIS/6.0
Content-type: text/xml

<NewDataSet xmlns:sql="urn:schemas-microsoft-com:xml-sql">
    <row ProductsCount="77"/>
</NewDataSet>

The successful database query returns the number of rows in the products table. If an error occurs (for example, if the FOR was forgotten in the requested command), the response could look like the following.

HTTP/1.1 200 OK
Connection: close
Date: Sat, 12 Feb 2005 20:20:27 GMT
Server: Microsoft-IIS/6.0
Content-type: text/xml

<NewDataSet xmlns:sql="urn:schemas-microsoft-com:xml-sql">
    <?MSSQLError HResult="0x80040e14"
        Source="Microsoft OLE DB Provider for SQL Server"
        Description="Line 1: Incorrect syntax near 'RAW'."?>
</NewDataSet>

The ExecuteStream method on the SqlXmlCommand object captures the XML processing instruction tag (?MSSQLError), and a SqlXmlException exception is thrown. The exception (SqlXmlException) arguments include the error message (Message, from the XML tag's Description attribute) in addition to the complete response stream (ErrorStream).

One of the most important functions of the sample application is to update the number of items in stock, and the code in the product details form for doing that looks like the following.

using(ProductHandler productHandler = new ProductHandler())
{
    DataSet ds = productHandler.GetForID(productID);
    DataRow dr = ds.Tables[0].Rows[0];
    dr["UnitsInStock"] = Convert.ToInt32(unitsInStockTextBox.Text);
    productHandler.Save(ds);
}

If you have read the previous articles in this series (for links, see the Introduction section earlier in this article), you should recognize the interface to the business domain or handler classes. The product handler is first used to get a specific product (row), the new number of items in stock is updated to the product row, and the DataSet object is saved to the database. The difference shows only in the implementation of the product handler, so you can look at the method to get a DataSet object with a specific product.

public DataSet GetForID(string productID)
{
    SqlXmlCommand cmd = new SqlXmlCommand(
        Common.Values.ConnectionUrl);
    cmd.CommandText = Common.XmlQuery("SELECT * FROM" +
        " Products WHERE ProductID='" + productID + "'");
    DataSet ds = new DataSet();
    SqlXmlAdapter da = new SqlXmlAdapter(cmd);
    da.Fill(ds);
    return ds;
}

The structure is the same as before, but note that a common method (Common.XmlQuery) is used to add the XML addition (the string "FOR XML AUTO,ELEMENTS") to the command.

The HTTP request for this database call looks like the following.

POST /NorthwindX/ HTTP/1.1
Content-Type: text/xml
Content-Length: 193
Connection: Keep-Alive
Host: server

<NewDataSet xmlns:sql='urn:schemas-microsoft-com:xml-sql'>
    <sql:query>
        SELECT * FROM Products WHERE
        ProductID='D4B9EBEC-6B3A-4A1A-860F-0E546D585603'
        FOR XML AUTO,ELEMENTS
    </sql:query>
</NewDataSet>

Just as before, the SQL SELECT command is just embedded in the required XML structure.

The successful HTTP response is as follows.

HTTP/1.1 200 OK
Connection: close
Date: Sat, 12 Feb 2005 20:21:28 GMT
Server: Microsoft-IIS/6.0
Content-type: text/xml

<NewDataSet xmlns:sql="urn:schemas-microsoft-com:xml-sql">
    <Products>
        <ProductID>D4B9EBEC-6B3A-4A1A-860F-0E546D585603</ProductID>
        <ProductNo>12048</ProductNo>
        <ProductName>Chocolade</ProductName>
        <SupplierID>B7EF2643-5A35-4865-A1D4-06316B3DB145</SupplierID>
        <CategoryID>2086B2AA-114D-4774-B21C-19F2F0938AB0</CategoryID>
        <QuantityPerUnit>10 pkgs.</QuantityPerUnit>
        <UnitPrice>12.75</UnitPrice>
        <UnitsInStock>15</UnitsInStock>
        <UnitsOnOrder>70</UnitsOnOrder>
        <ReorderLevel>25</ReorderLevel>
        <Discontinued>0</Discontinued>
    </Products>
</NewDataSet>

The product row is returned with all columns in a normal hierarchical XML structure, and this structure can be read directly into a DataSet object.

The product handler's method to save the DataSet is implemented with the following code.

public void Save(DataSet dataSetToSave)
{
    SqlXmlCommand cmd = new SqlXmlCommand(Common.Values.ConnectionUrl);
    cmd.SchemaPath = Common.Values.SchemaPath;
    SqlXmlAdapter da = new SqlXmlAdapter(cmd);
    da.Update(dataSetToSave);
}

To update, you need to set the XML schema to use, and this is the HTTP request for this database call.

POST /NorthwindX/ HTTP/1.1
Content-Type: text/xml
Content-Length: 1349
Connection: Keep-Alive
Host: server

<NewDataSet xmlns:sql='urn:schemas-microsoft-com:xml-sql'
sql:mapping-schema='template/NorthwindX.xsd'>
<diffgr:diffgram
  xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"
  xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
    <NewDataSet>
        <Products diffgr:id="Products1" msdata:rowOrder="0"
          diffgr:hasChanges="modified">
            <ProductID>D4B9EBEC-6B3A-4A1A-860F-0E546D585603</ProductID>
            <ProductNo>12048</ProductNo>
            <ProductName>Chocolade</ProductName>
            <SupplierID>B7EF2643-5A35-4865-A1D4-06316B3DB145</SupplierID>
            <CategoryID>2086B2AA-114D-4774-B21C-19F2F0938AB0</CategoryID>
            <QuantityPerUnit>10 pkgs.</QuantityPerUnit>
            <UnitPrice>12.75</UnitPrice>
            <UnitsInStock>14</UnitsInStock>
            <UnitsOnOrder>70</UnitsOnOrder>
            <ReorderLevel>25</ReorderLevel>
            <Discontinued>0</Discontinued>
        </Products>
    </NewDataSet>
    <diffgr:before>
        <Products diffgr:id="Products1"
          msdata:rowOrder="0">
            <ProductID>D4B9EBEC-6B3A-4A1A-860F-0E546D585603</ProductID>
            <ProductNo>12048</ProductNo>
            <ProductName>Chocolade</ProductName>
            <SupplierID>B7EF2643-5A35-4865-A1D4-06316B3DB145</SupplierID>
            <CategoryID>2086B2AA-114D-4774-B21C-19F2F0938AB0</CategoryID>
            <QuantityPerUnit>10 pkgs.</QuantityPerUnit>
            <UnitPrice>12.75</UnitPrice>
            <UnitsInStock>15</UnitsInStock>
            <UnitsOnOrder>70</UnitsOnOrder>
            <ReorderLevel>25</ReorderLevel>
            <Discontinued>0</Discontinued>
        </Products>
    </diffgr:before>
</diffgr:diffgram>
</NewDataSet>

The content of the HTTP request contains the DiffGram type with the updated row as the first row (XML structure) followed by the original row (inside the DiffGram before tag).

The returned HTTP response looks like the following.

HTTP/1.1 200 OK
Connection: close
Date: Sat, 12 Feb 2005 20:21:29 GMT
Server: Microsoft-IIS/6.0
Content-type: text/xml

<NewDataSet xmlns:sql="urn:schemas-microsoft-com:xml-sql"></NewDataSet>

An empty root tag is an indication of a successful update of the database.

Included in the download sample is a help file of the SQL XML managed classes for the .NET Compact Framework, and it contains more details about the use of these classes. Of course, you can also find more details in the sample application's source code.

ValidationHandler

All enterprise mobile solutions need easy validation of user input. Therefore, the ValidationHandler class was developed to help validate input through regular expressions. Because validations can be different for different countries and languages, the validations are also localized. The ValidationHandler class can be used to validate complete forms, including all input controls, and it requires only the following single line of code in each form.

string s = ValidationHandler.Validate.Form(this);

The returned string includes any validation errors that occurred during the validation, and if empty, the validation of the form was successful.

Even custom text validations are supported, and they can be accomplished as follows.

string s = ValidationHandler.Validate.Text("LoginFormpasswordTextBox", passwordTextBox.Text);

The implementation of the ValidationHandler class begins as follows.

private const string resourcePrefix = "valid";
private ResourceManager resourceManager;
private CultureInfo currentCulture;
private Form form;

public static readonly ValidationHandler Validate =
    new ValidationHandler();

private ValidationHandler()
{
    string resourceSource = Path.GetFileNameWithoutExtension(
        this.GetType().Assembly.GetName().CodeBase) +
        "." + resourcePrefix;
    resourceManager = new ResourceManager(
        resourceSource, this.GetType().Assembly);
    currentCulture = (CultureInfo)
        CultureInfo.CurrentCulture.Clone();
}

The ValidationHandler class is implemented as a singleton class with the instance name Validate. So to access the singleton class, you use the format ValidationHandler.Validate.property/method. In the private constructor, the resource manager is created, and the current culture is saved in an instance variable. The first parameter of the resource manager constructor is the assembly name and the first part of the resource file name. In this example, the local resource files all begin with valid, and, therefore, the file name for the default locale would be "valid.resx". The file name of the Brazilian Portuguese locale would be "valid.pt-br.resx", and so on.

Each resource file is an XML file that has a specific format (XML schema), and the entries (after the schema definition) in the English file look like the following.

<data name="Date">
    <value>^(((0?[13578]|1[02])\/(0?[1-9]|[12]\d|3[01])\/
        ((1[6-9]|[2-9]\d)?\d{2}))|((0?[13456789]|1[012])\/
        (0?[1-9]|[12]\d|30)\/((1[6-9]|[2-9]\d)?\d{2}))|(0?2\/
        (0?[1-9]|1\d|2[0-8])\/((1[6-9]|[2-9]\d)?\d{2}))|(0?2\/
        29\/((1[6-9]|[2-9]\d)?(0[48]|[2468][048]|[13579][26])|
        ((16|[2468][048]|[3579][26])00)|00)))$</value>
    <comment>Valid date in the format MM/dd/yyyy including
        leap years.</comment>
</data>
<data name="Quantity">
    <value>^([0-9]{1,4})$</value>
    <comment>Quantity is a numeric value of at least 1 digit
        and no more than 4 digits.</comment>
</data>
<data name="Password">
    <value>^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,8}$</value>
    <comment>Password must be at least 4 characters, no more
        than 8 characters, and must include at least one upper
        case letter, one lower case letter, and one numeric
        digit.</comment>
</data>
<data name="ProductNo">
    <value>^([0-9]{1,9})$</value>
    <comment>Product number is a numeric value of at least 1
        digit and no more than 9 digits.</comment>
</data>
<data name="UnitsInStock">
    <value>^([0-9]{1,4})$</value>
    <comment>Units in stock is a numeric value of at least 1
        digit and no more than 4 digits.</comment>
</data>
<data name="LoginFormpasswordTextBox">
    <value>Password:Password must be 4-8 long with mixed case
        and digit!</value>
    <comment>Password must be 4-8 long with mixed case and
        digit!</comment>
</data>
<data name="ProductFormunitsInStockTextBox">
    <value>UnitsInStock:Units in stock must be 1-4
        digits!</value>
    <comment>Units in stock must be 1-4 digits!</comment>
</data>
<data name="ProductListFormproductNoTextBox">
    <value>ProductNo:Product No must be 1-9 digits!</value>
    <comment>Product No must be 1-9 digits!</comment>
</data>
<data name="SupplierProductFormquantityTextBox">
    <value>Quantity:Quantity must be 1-4 digits!</value>
    <comment>Quantity must be 1-4 digits!</comment>
</data>

The entries with the combination of form and control name (for example, LoginFormpasswordTextBox) have two parts (separated by a colon) in their values. The first part is a reference to the validation name (in this case, Password), and the second part is the validation error message in case the validation is not successful. The actual regular expression to use in the validation is in the referenced entry shown in the preceding code (named Password). Note that extra line spaces have been inserted to simplify reading (for the original format, please see the files in the sample code).

In the Brazilian Portuguese file, the same entries look like the following.

<data name="Date">
    <value>^(((0?[1-9]|[12]\d|3[01])\/(0?[13578]|1[02])\/
        ((1[6-9]|[2-9]\d)?\d{2}))|((0?[1-9]|[12]\d|30)\/
        (0?[13456789]|1[012])\/((1[6-9]|[2-9]\d)?\d{2}))|
        ((0?[1-9]|1\d|2[0-8])\/0?2\/((1[6-9]|[2-9]\d)?\d{2}))|
        (29\/0?2\/((1[6-9]|[2-9]\d)?(0[48]|[2468][048]|[13579]
        [26])|((16|[2468][048]|[3579][26])00)|00)))$</value>
    <comment>Data válida no formato dd/MM/yyyy inclusive
        anos bissexto.</comment>
</data>
<data name="Quantity">
    <value>^([0-9]{1,4})$</value>
    <comment>Quantidade é um valor numérico de pelo menos 1
        dígito e não mais que 4 dígitos.</comment>
</data>
<data name="ProductNo">
    <value>^([0-9]{1,9})$</value>
    <comment>Número de produto é um valor numérico de pelo
        menos 1 dígito e não mais que 4 dígitos.</comment>
</data>
<data name="Password">
    <value>^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,8}$</value>
    <comment>Senha deve ser pelo menos 4 letras, não mais que
        8 letras, e tem que incluir uma letra maiúscula, uma
        letra minúscula, e um dígito numérico.</comment>
</data>
<data name="UnitsInStock">
    <value>^([0-9]{1,4})$</value>
    <comment>Unidades em estoque são um valor numérico de pelo
        menos 1 dígito e não mais que 4 dígitos.</comment>
</data>
<data name="LoginFormpasswordTextBox">
    <value>Password:Senha deve ser 4-8 longo com caso
        misturado e dígito!</value>
    <comment>Senha deve ser 4-8 longo com caso misturado
        e dígito!</comment>
</data>
<data name="ProductFormunitsInStockTextBox">
    <value>UnitsInStock:Unidades em estoque devem ser 1-4
        dígitos!</value>
    <comment>Units in stock must be 1-4 digits!</comment>
</data>
<data name="ProductListFormproductNoTextBox">
    <value>ProductNo:Número de produto deve ser 1-9
        dígitos!</value>
    <comment>Product No must be 1-9 digits!</comment>
</data>
<data name="SupplierProductFormquantityTextBox">
    <value>Quantity:Quantidade deve ser 1-4
        dígitos!</value>
    <comment>Quantity must be 1-4 digits!</comment>
</data>

All of the validation messages are translated with the original English text in the comments used as guidance for the translator. Note that the date format is different for this locale, and this shows another reason for localizing the validation resource files.

For more information about creating resource files, read the section about the GlobalHandler class in the article Northwind Pocket Sales: Field Sales for Windows Mobile–based Pocket PCs.

Returning to the ValidationHandler class, the most basic function is the public Text method that is used to validate any text, as shown in the following code.

public string Text(string validationName, string subjectText)
{
    string returnValue = GlobalHandler.Translate.Text(
        "MsgCantReadValidationResource",
        "Could not read resource string!");
    string s;

    if((s = resourceManager.GetString(validationName,
        currentCulture)) != null)
    {
        int i = s.IndexOf(':');
        string generalName = s.Substring(0, i);
        string messageText = s.Substring(i + 1);
        if((s = resourceManager.GetString(generalName,
            currentCulture)) != null)
        {
            if(Regex.IsMatch(subjectText, s))
                returnValue = "";
            else
                returnValue = messageText;
        }
    }
    return returnValue;
}

The method is passed the entry to look for in the resource file (validationName), and the text to validate (subjectText). That entry is retrieved, and the entry name containing the actual regular expression is extracted (generalName), as is the validation error message (messageText). The evaluation occurs on the text string through the regular expression, and if everything goes well, the return value is empty. Otherwise, the returned string contains an error message (either a general error if the resource strings could not be read, or a specific error if the validation was not successful).

This method can be useful for custom validations, but the real value of the ValidationHandler class is the ability to validate complete forms. The implementation of the form validation looks like the following.

public string Form(Form form)
{
    this.form = form;
    string returnValue = Control(form);
    this.form = null;
    return returnValue;
}

public string Control(Control control)
{
    string returnValue = string.Empty;
    string s;

    foreach(Control ctrl in control.Controls)
    {
        // Find validation with form prefix
        string validationName = string.Empty;
        if(form != null)
        {
          // Find validation with form prefix
          if((s = resourceManager.GetString(form.GetType().Name +
                  getItemName(ctrl), currentCulture)) != null)
            validationName = s;
        }
        if(s == null)
          // Find validation with no prefix
          if((s = resourceManager.GetString(getItemName(ctrl),
                  currentCulture)) != null)
            validationName = s;

        // Validation exist?
        if(validationName.Length > 0)
        {
            // Get regular expression and validate
            int i = s.IndexOf(':');
            string generalName = validationName.Substring(0, i);
            string messageText = validationName.Substring(i + 1);
            if((s = resourceManager.GetString(
                    generalName, currentCulture)) != null)
                if(!Regex.IsMatch(ctrl.Text, s))
                    returnValue += messageText + "\r\n";
        }

        // Container with subcontrols to translate?
        if (ctrl.Controls.Count > 0)
            returnValue += Control(ctrl);
    }
    return returnValue;
}

The first method (Form) simply saves the passed form and calls the second method (Control) with the form as the argument. That method loops through the child controls of a control (the parameter), and in this first call, control is actually the form itself. It starts by checking whether there is an entry named with a combination of the form name and the control name. If not, the method tries to find an entry with the control name. If the method finds an entry, the same logic is processed as in the previously described method (Text). Then, if the current control contains any subcontrols, the same method (Control) is called recursively to validate them as well. For details about the method that retrieves the name of a control (getItemName), see the section about the GlobalHandler class in the article Northwind Pocket Sales: Field Sales for Windows Mobile–based Pocket PCs.

The typical use of the ValidationHandler class in a form is to include this code in the Closing event as follows.

if(this.DialogResult == DialogResult.OK)
{
    string s = ValidationHandler.Validate.Form(this); 
    if(s.Length > 1)
    {
        MessageBox.Show(s, this.Text);
        e.Cancel = true;
    }
}

The preceding code assumes that the dialog result is used in the events that close the form (that is, Click event for the Done and Cancel soft keys and menu options).

With the forms cache (FormCache) implemented (for details, see the Form Cache [and Stack] section of this article), the code would look like the following.

e.Cancel = true;
if(this.DialogResult == DialogResult.OK)
{
    string s = ValidationHandler.Validate.Form(this); 
    if(s.Length > 1)
    {
        MessageBox.Show(s, this.Text);
        return;
    }
}
FormCache.Instance.Pop(1);

Custom code is definitely needed for more intelligent validation (for example, to look up reference data), but validation through regular expressions should get you started with a localizable way of validating data entry.

Basic Bar-Code Scanner Support

As mentioned after Figure 8 earlier in this article, the user can enter a product number, and then press the Action key to directly select the product and display the product details screen shown in Figure 10, Figure 11, and Figure 12. The code for the KeyUp event of the product number text box (productNoTextBox) looks like the following.

private void productNoTextBox_KeyUp(object sender,
    System.Windows.Forms.KeyEventArgs e)
{
    if(e.KeyCode == Keys.Return)
    {
        int productNo = 0;
        try { productNo = Convert.ToInt32(productNoTextBox.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 the key pressed is the Action key, and the text box holds an integer value (presumably a product number), the product identity is retrieved, and the product detail form is opened.

This way, any bar-code scanner can be used that supports simulating the keyboard and finishing the scan by simulating a final press of the Action key. Correctly configured, most bar-code scanners have this capability.

However, most bar-code scanners have even more sophisticated capabilities (even with support for the .NET Compact Framework), like custom controls that are bar-code scanner aware. Such controls increase the control of the scans (with features like detailed exceptions when something goes wrong) but make the support for multiple bar-code scanners much more complicated.

XML Web Service Integration

Any enterprise Pocket PC solution that is intended to be used in cooperation with a back-office system or even connect to partner resources should probably use XML Web services for that integration. The sample connects to an XML Web service for retrieving product information directly from the suppliers. After the Web reference is created in the sample project, the code to use that XML Web service looks like the following.

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

The XML Web service scans the supplier's Web pages (the HTML code) to get the product information, but it can also use a supplier-provided XML Web service directly. The XML Web service is also used to place a purchase order (which in a real-world scenario would connect to a back-office system to create the purchase order).

For more details about the implementation of XML Web service integration, please see the article Northwind Pocket Inventory: Logistics for Windows Mobile–based Pocket PCs.

GlobalHandler

Any enterprise Pocket PC solution that will be used in more than one language needs to be easily translated. Therefore, you can use the GlobalHandler class 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 format.

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 that the forms, and the memory they consume, can be managed in an efficient way. Therefore, the FormCache class supports both the caching and stacking of forms. Briefly, the loading of a new form, or actually pushing a new form on the form stack, looks like the following.

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

The power of connecting Smartphones directly to back-end databases opens new possibilities. With all possibilities come new challenges, and an example in this case is input validation, which becomes more important. The reusable components for connecting to server databases by using standard XML and for validating input efficiently by using regular expressions will enable the new possibilities and help fight the new challenges. Make sure your enterprise-enabled Smartphones become a part of your core business processes by adding great .NET Compact Framework applications!