Northwind Pocket Service: Field Service for Windows Mobile-based Smartphones

 

Andy Sjostrom and Christian Forsberg

Business Anyplace

Contribution from: Odyssey Software

July 2004

Applies to:
   Windows Mobile™-based Smartphones
   Windows Mobile 2003 Second Edition software for Smartphones
   Microsoft® Visual Studio® .NET 2003
   Microsoft .NET Compact Framework version 1.0

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

Download Field Service for Smartphone.msi from the Microsoft Download Center.

Contents

Introduction
Smartphones in Enterprise Mobile Solutions
Northwind Field Service Business Process
"Northwind Pocket Service" Client Walkthrough
Code Walkthrough
Conclusion

Introduction

This article is about designing and developing a mobile field service application based on the Microsoft® .NET Framework, the Windows Mobile™ platform, and the Microsoft .NET Compact Framework. This article follows Northwind Pocket Service: Field Service for Pocket PC, which explained how to design and develop a Pocket PC–based field service application. It is recommended that you read the first article before continuing with this one. This article is about implementing support for the same key field service scenarios but for Windows Mobile–based Smartphone users.

The sample solutions address the needs of the fictitious company Northwind Traders. A sample database containing customers, products, employees, orders, suppliers, shippers, and related tables is shipped with Microsoft Access and Microsoft SQL Server. The related samples provide Northwind Traders with mobile solutions, targeting both Windows Mobile-based devices, supporting the company's core business processes. The sample application features, on both client and server, will be sufficiently complex to cover key .NET Framework and .NET Compact Framework technologies and illustrate how to meet the primary mobile enterprise application objectives.

Figure 1 shows the mobile solution infrastructure including the Pocket PC and Smartphone clients, server components, common services, and the Northwind database. It also shows additional business processes that might be mobilized.

Click here for larger image

Figure 1. Common system architecture supporting many scenarios. Click the thumbnail for a larger image.

The preceding figure shows that some client features are commonly shared on both the Pocket PC and Smartphone clients, some client features are exclusively delivered on Smartphones, and other features are exclusively delivered on Pocket PCs. The driving factors behind the division are different use cases, scenarios, and users, combined with the fact that the two form factors are both very different while sharing some similarities. In solution design and development, it is important to identify what features are best deployed to each form factor.

More information on mobile application development can be found on the following pages:

Microsoft MSDN Mobility & Embedded Development Center

Microsoft .NET Compact Framework

Smartphones in Enterprise Mobile Solutions

Companies have deployed Pocket PC mobile devices to increase efficiency and improve competitive edge. Programmability, data connectivity, and a rich feature set are factors that have played major roles in this adoption.

With the arrival of Windows Mobile software for Smartphone, smaller and more voice-centric devices have become another alternative. Windows Mobile has made the Smartphone a stronger platform from a mobile enterprise application perspective. Programmability is on par with Pocket PCs, primarily because the .NET Compact Framework is built into ROM. More memory and improved data connectivity are also aspects that appeal to developers and application development decision makers.

Following are some key guidelines for deciding which platform to choose:

  • Always there — People carry their phones almost everywhere they go, and it is a smaller and more portable form factor. The Pocket PC does not enjoy such penetration into peoples lives so would not always be with the user.
  • Amount of required data input — When users need to input less data in the field, the Smartphone is the better choice because it is even more portable.
  • Importance of phone feature integration — Applications that need to integrate SMS, MMS, and voice calls are very well supported on -based Smartphones.
  • Complexity of the user interface — Can the application user interface be designed in a clear and logical way for the smaller Smartphone screen? Does the user interface lend itself to a "deck" approach, common for phone applications?
  • Importance of voice communication versus data communication — Pocket PCs have a slight advantage over Smartphones in a data communication perspective, with stronger support for local wireless networks (WiFi).
  • Processing power — Your application may be very processor-intensive, in which case a Pocket PC may be a better choice because it typically has more RAM and processor power than a Smartphone.

In the case of Northwind Traders, field service workers can choose to use either Pocket PC or Smartphone clients to perform the main activities of their field service business process.

Northwind Field Service Business Process

Note If you have read the Field Service for Windows Mobile-based Pocket PCs article, you will be familiar with this business process.

No More Empty Vending Machines

Northwind Traders owns a number of vending machines that are placed in malls, supermarkets, train stations, airports, and so on. The company has a field service staff responsible for making sure these vending machines are refilled when needed. Take a look in the Northwind Trader's database, and you'll find that some of their products are chocolate, Grandma's Boysenberry Spread, Uncle Bob's Organic Dried Pears, and Ipoh Coffee. The challenge is to keep these vending machines up and running with a solid supply of products. The clients of Northwind Traders rent vending machines and expect a good return on their investments.

Field Service Process

The Northwind Traders field service process is fundamentally built on planned and urgent vending machine refills. The company has set up a maintenance contract with each vending machine client. Under this contract, the field service technicians visit each site at a defined schedule. The client can also contact Northwind Traders with an urgent refill service order.

Process Without Mobile Solution

The Northwind Traders field service process without a mobile solution involves paper and pencil, paper-based service orders, and manual work at the head office. Each day, the technicians go to the head office and pick up service orders that are printed on paper forms. The technicians spend time discussing who goes where and does what. Urgent service orders are manually prioritized. When all is done at the head office, the field workers drive away for another day of vending machine refills.

If an urgent call comes in during the day, the head office staff has to call out to the field workers and hope one can come back to pick up the paper-based service order. Otherwise, the service order cannot be addressed until the next business day.

Process with Mobile Solution

The Northwind Traders field service process with a mobile solution is digital from beginning to end. The service orders are digitally created in the back office system and transferred to each and every service technician. This goes both for urgent and for planned refills. There is no need to travel to the head office to pick up the service orders because the service orders are downloaded to the Windows Mobile-based Smartphone. The service orders are filled out in the field and transferred directly to the back office system. No need for double data entry or paper handling. This leads to quicker invoicing and less need for staff to manage paper forms.

The Pocket PC field service article is about how to support the business process with a Pocket PC-based solution. This article is about how to support the same business process with a Windows Mobile-based Smartphone. The idea is not to stack the two solutions against each other in a competing situation nor is it to show how to migrate a Pocket PC application to a Windows Mobile-based Smartphone platform. Both platforms can successfully support most business scenarios, and it is important for any mobile application developer to master both platforms. The deciding factors that each individual company uses to choose one or the other, or even both, are numerous and include form factor (size and appearance), data input requirements, storage and connectivity requirements, and so on.

"Northwind Pocket Service" Client Walkthrough

The client application is developed using Microsoft Visual Studio® .NET 2003 and the .NET Compact Framework 1.0. The client application user interface is designed to match the following fundamental business process steps:

  1. Plan service
  2. Perform service
  3. Report service

The user interface challenge is addressed by implementing key data input as logical wizards. The wizards make it easy for users to enter data and ensure that all required data is collected.

Use Cases

The diagram in Figure 2 illustrates the main use cases that the Northwind Pocket Service for Smartphone supports. The Service boundary encompasses the use cases that actually address core business process steps. Synchronize and Manage settings are technical features that do not directly correlate to business process.

Figure 2. Supported services and processes

First Run

The application enters an initialization phase the first time the application is run, because, as indicated in Figure 3, the application cannot connect to the local database, which is a local XML file.

Figure 3. No database found on first run

Upon acknowledgment of this first message, the user is automatically taken to the Options dialog, shown in Figure 4, where required connectivity settings can be entered before the first data transfer from server to client. The user selects Local.

Note   The end user, in this case the field service engineer, would not be expected to perform these configuration steps. These steps would be carried out by an IT person during configuration of the device or would be deployed as part of the solution.

Figure 4. Options

In the Local Options dialog, shown in Figure 5, the user sets the location and name of the local DataSet XML file.

Figure 5. Local options with suggested database name

The Local database setting points to a local DataSet XML file that implements the actual database for this application. The user selects Create database and moves to the Server Options dialog, shown in Figure 6.

Figure 6. Server options

The three server options are as follows:

  • Server database (URL) — URL for the DataSet Server CE Web Service, which is used to initialize a new database and to pull down reference data.
  • Server login (User) — The default user name used when connecting the server.
  • Web Service (URL) — URL for the XML Web service, which is used to synchronize service orders.

The first time the application is used, it is recommended that the user initialize a new database. A wizard leads the user through the steps. The first page of the Synch wizard, shown in Figure 7, informs the user that a new database is about to be initialized.

Figure 7. Synchronize wizard for creating new database

In the second step, as shown in Figure 8, the user enters a valid user name and password. These network credentials are used when connecting to the server side DataSet Server CE XML Web Service. The DataSet Server CE XML Web Service impersonates the given user when connecting to the database. The new database (local DataSet XML file) is created when the Finish soft key button is selected.

Figure 8. Server authentication

The user is kept updated on the progress of the synchronization. Synchronization is completed when the Reference tables pulled message appears, as indicated in Figure 9.

Figure 9. Synchronization done

Functionality is aligned with the business process, as shown in Figure 2. As shown in Figure 10, the main menu provides easy access to the process steps: Plan, Service, and Report.

Figure 10. Main menu

The other main menu options include support functionality for synchronization, entering application settings (options), and information about the application. As more support functionality could be added, care should be taken not to include too much functionality. The user expects a Smartphone application to be as easy to use as possible, and more functionality usually means increased complexity.

Synchronization

In the Synch wizard, as shown in Figure 11, the feature set related to synchronization supports three use cases:

  • Synchronize services — Synchronizes service orders to and from the Smartphone.
  • Get reference data — Pulls reference data such as product information to the Smartphone.
  • Initialize new database — Creates a new database and pulls down data from the server.

Figure 11. Synchronization wizard to get new service items

When the user selects Synchronize services and provides user name and password, the wizard completes the action by reporting the progress of the actual synchronization, as shown in Figure 11. The ServiceWS Web service is called during the synchronization, and it returns open service orders that are assigned to the Smartphone user.

The last step of the wizard, shown in Figure 12, informs the user that the service orders have been synchronized.

Figure 12. Services synchronized

Planning

Assigned service orders can be viewed by the service technician, who can thereby plan the efforts involved in addressing the orders. Like the database synchronization, the planning features are also implemented by a wizard. The first step, shown in Figure 13, allows the user to filter out relevant service orders that have been pulled down to the Smartphone.

Figure 13. Search criteria for service items to plan

The search criteria are used when searching the service orders. Service orders matching the criteria are displayed in step two of the wizard, shown in Figure 14. The user selects one of the open service orders and continues by selecting the Next soft key button. Notice that the Date from and Date to fields have been prefilled by the application. This allows the user to save valuable time by not having to input the dates. By default, the application searches for the last month, up to and including today.

Note   The wizard menu shown in Figure 14 includes the vital Back and Cancel options, allowing the user to go back to previous wizard steps or to cancel the entire wizard process.

Figure 14. Found service items to plan

In step three, shown in Figure 15, the user is presented with more detailed information about the service order.

Figure 15. Service item details

At this point, as shown in Figures 16 and 17, the user can also choose to view customer details by using the Action hardware key when the customer name has the focus. This is useful in route planning and when the service technician is expected to call the customer prior to arriving to the customer's site. The customer data is presented in a scrollable form much like the inbuilt Smartphone applications. Figure 16 shows the initial (upper) part of the form. When the Down hardware navigation key is pressed, the second (lower) part of the form is shown.

Figures 16 and 17. Customer details

As shown in Figure 18, vending machine details are also available when using the Action hardware key when the focus is on the machine name. This information is vital in making sure it is the correct machine.

Figure 18. Machine details

The fourth step of the wizard, shown in Figure 19, indicates the vending machine items that have to be refilled and the quantity of each item. Note that the product names in the Northwind Traders product table, although truncated, are still readable so that the user can easily discern what the unique product name is. If the list entries for your own application are too long, you might want to consider displaying only one item or maximizing the width of the name column. This is how the inbuilt Smartphone applications work.

Figure 19. Service item products

The last step of the wizard, shown in Figure 20, allows the service technician to set a new status on the service order. The default choice is Planned. All planned service orders are available to the service technician in the next step of the core business process: Service.

Figure 20. Set new service item status

Service

The service features are used during the actual service work. All planned service orders can be viewed, and the service orders can be updated for any refills that have been performed. The first step of the Service wizard, shown in Figure 21, is similar to the first step of the Plan wizard shown in Figure 13. The user first enters search criteria.

Figure 21. Search criteria for service items to complete

As shown in Figure 22, the previously planned service order appears in the list and is now ready for updating.

Figure 22. Found service items to complete

As shown in Figure 23, the user can now view service order details.

Figure 23. Service item details

In step four, as shown in Figure 24, the user selects a product in the list and selects the menu option Update.

Figure 24. Service item products

The wizard then allows entering delivered quantity, as shown in Figure 25.

Figure 25. Deliver a product

It is also possible to view the product details by selecting the product name, as shown in Figures 26 and 27. Product details can be useful during the service work if article number, supplier name, and so on need to be verified.

Figure 26 and 27. Products details

The fourth step of the wizard, shown in Figure 28, provides a list of products delivered.

Figure 28. Products delivered

The last step of the wizard, shown in Figure 29, allows the user to close the service by setting it to the status Delivered.

Figure 29. Set new service item status

Report

The final step of the service order process that is performed out in the field is the reporting. The service technician uses this step to mark the service order as ready for invoice. Actual invoice text can be viewed and shown to the client on site. The service technician can also decide how the client should receive a receipt.

As in the first two process wizard, the first step of the Report wizard is the search criteria form, shown in Figure 30.

Figure 30. Search criteria for service items to report

As shown in Figure 31, the wizard finds all service orders marked with the status Delivered.

Figure 31. Found service items to report

As shown in Figures 32 and 33, a summary report of the service order is displayed. This can be shown to the client before the service technician leaves the site.

Figure 32 and 33. Service item summary report

In step four, shown in Figure 34, the user can decide how the receipt should be delivered.

Figure 34. Client name and copy delivery

In the last step, shown in Figure 35, the user closes the service order by setting it to status Reported.

Figure 35. Set new service item status

Options

The Options form, shown in Figure 36, enables the user to set options related to local and server settings. In real-world applications, it is sometimes wise to put some form of lock on the options menu. This prevents the user from accidentally changing settings that would prevent the application from functioning correctly. For example, on a Smartphone, you might want to require a certain sequence of button presses to modify settings on a particular screen.

Figure 36. Options menu

The Local Options dialog, shown in Figure 37, includes the name of the employee who is currently using the Smartphone. This setting is used when pulling down assigned service orders. Path and file name of the local database can be set at this point.

Figure 37. Local options

On the Server Options dialog, shown in Figure 38, the three server options are as follows:

  • Server database (URL) — URL for the DataSet Server CE XML Web Service, which is used to initialize a new database and to pull down reference data.
  • Server login (User) — The default user name used when connecting the server.
  • Web Service (URL) — URL for the XML Web service that is used to synchronize service orders.

Figure 38. Server options

About

The About form in Pocket Service, shown in Figure 39, is provided purely as a sample. The sample application is not copyrighted. Feel free to download and use the source code!

Figure 39. About the application

Code Walkthrough

This walkthrough will provide overview explanations to key segments of the code. However, before moving on to the walkthrough and if you plan to download the source code, it is recommended that you first set up the physical environment correctly.

Physical Environment

The physical environment is composed of the following:

  • A Windows Mobile-based Smartphone or emulator.
  • Visual Studio .NET 2003.
  • SQL Server 2000 SP3.
  • Internet Information Services (IIS) 6.
  • DataSet Server CE XML Web Service. (See DataSet Server CE: Database Connectivity for Smartphone 2003 and Pocket PC 2003 for more information.)
  • The ServiceWS XML Web service (source code included in this article's source code download).
  • Installed NorthwindX database (included in this article's source code download).

Additionally, the following conditions must be applied to the environment:

  • For integrated security to work, the DataSet Server CE XML Web Service and the ServiceWS must not use "Allow Anonymous" authentication.

  • Observe the Web.config in the two Web services:

    <authentication mode="Windows" />
    <identity impersonate="true" />
    
  • The Windows user that you intend to use must be added to the database.

The diagram shown in Figure 40 illustrates the DataSet Server CE architecture. The DataSet Server CE component architecture is designed to mimic the SQL Server CE client and server components for the Smartphone platform. DataSet Server CE is a covered in a separate article in MSDN. DataSet Server CE is used in Pocket Service for Smartphone because SQL Server CE is not available on this platform. Pocket Service for Pocket PC uses SQL Server CE, and therefore most lines of code can stay the same across the two applications, with different references to only two different components.

Click here for larger image

Figure 40. DataSet Server CE

The diagram in Figure 41 shows how closely DataSet Server CE mimics SQL Server CE.

Note   Please refer to the disclaimers in the DataSet Server CE article concerning performance, support, and scalability for DataSet Server CE.

Click here for larger image

Figure 41. DataSet Server CE functionality

Client Walkthrough Code Highlights

When the application is run, the MainForm form is loaded and its constructor begins as follows:

try
{
    // Read settings (from registry).
    string s = Common.Values.LocalDatabase;
}
catch (Exception)
{
    MessageBox.Show("Could not read settings from registry!",
                     this.Text);
    this.Close();

}

The dummy string (variable s) is set only to load the singleton class (Common) into memory. Here's a look at the Common class constructor:

public static readonly Common Values = new Common();
private Common()
{
    Load();
}

The Common class includes only a private constructor, and the first time the public and static Values variable is accessed, it will be loaded with the only (single) instance that will be available to all classes in the application.

The private constructor's only purpose is to call the private Load method that includes the following code to load application settings from the registry:

RegistryKey key = Registry.LocalMachine.CreateSubKey(this.registryKey);
this.localDatabase = key.GetValue("LocalDatabase",
    @"\NorthwindX.xml").ToString();
this.remoteDatabaseURL = key.GetValue("RemoteDatabaseURL",
    " https://server/dsrda/datasetserveragent.asmx").ToString();
this.remoteLogin = key.GetValue("RemoteLogin", @"ssceuser").ToString();

When the single instance is disposed (when the application ends), the application settings are saved to the registry in the Save method:

RegistryKey key = Registry.LocalMachine.OpenSubKey(this.registryKey,
    true);
key.SetValue("LocalDatabase", this.localDatabase);      
key.SetValue("RemoteDatabaseURL", this.remoteDatabaseURL);
key.SetValue("RemoteLogin", this.remoteLogin);

Both the Load method and the Save method use the OpenNETCF.Win32.Registry class included in the Smart Device Framework from OpenNETCF that is also available with source code.

Note   OpenNETCF is a third party, open source, initiative that provides .NET Compact Framework classes. The OpenNETCF registry component is not provided by or supported by Microsoft.

The Common class also includes application definitions such as enumerations and other constants. The following example shows the different status values a service item can have:

public enum ServiceStatus : int
{
    Open, Planned, Delivered, Reported, Invoiced, Closed
}
public static readonly string[] ServiceStatusText = new string[]
    { "Open", "Planned", "Delivered", "Reported", 
     "Invoiced", "Closed" };

The string array is used to provide text for each of the ServiceStatus enum values. The Common class also provides properties for each application setting based on private variables:

private DataSet databaseDataSet;
public DataSet DatabaseDataSet
{
    get { return databaseDataSet; }
    set { databaseDataSet = value; }
}

private string localDatabase;
public string LocalDatabase
{
    get { return localDatabase; }
    set { localDatabase = value; }
}

Some of the properties are derived (calculated):

public string AppPath
{
    get {return Path.GetDirectoryName(
System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase );}
}

The preceding example provides the current application path as a property that is retrieved by using a very interesting technique called reflection, included in the .NET (Compact) Framework. Reflection allows code to retrieve meta information about the code currently executing in runtime. This includes the ability to examine the name of the currently loaded assembly, types of class members and method parameters, and so on.

Now, returning to the MainForm constructor:

try
{
    // Open database dataset.
    Common.Values.OpenDatabase();
}
catch (Exception)
{
    MessageBox.Show("Could not connect to local database!", this.Text);
    // Go to Options!
    Options(false);
}

In a real-world application, you should try to provide your users with more helpful information. Examples might include an error code that can be easily traced by a support person. You might want to include the support number or e-mail address for support and, if possible, suggest some possible actions the user can perform to fix the error.

Next, an attempt is made to open the database, but if no database exists, the Options dialog that allows a new database to be created is opened. The code that creates the new database looks like this:

DataSetEngine engine = new
    DataSetEngine(Common.Values.LocalConnectionString);
engine.CreateDatabase();

The DataSetEngine class is included in the DataSet Server CE component, and implementation details are covered in another article.

The initialization of the new database includes the download of reference data and transactional data. In the synchronization form, the tables are defined as two hash tables (one for reference data tables and one for transactional data tables):

// Reference tables.
rdaTables.Add("Categories", "Categories");
rdaTables.Add("Customers", "Customers");
rdaTables.Add("Employees", "Employees");
rdaTables.Add("Machines", "Machines");
rdaTables.Add("Products", "Products");
rdaTables.Add("Suppliers", "Suppliers");

// Transactional tables.
webserviceTables.Add("Services", "Services");
webserviceTables.Add("ServiceDetails", "ServiceDetails"); 

The actual download (pull) of the table data looks like this:

DataSetRemoteDataAccess rda = new DataSetRemoteDataAccess();
DataSet ds = Common.Values.DatabaseDataSet;
string sql = "";

rda.InternetLogin = remoteLogin;
rda.InternetPassword = remotePassword;
rda.InternetUrl = Common.Values.RemoteDatabaseURL;
rda.LocalConnectionString = Common.Values.LocalConnectionString;

if (!data)
    sql = " WHERE 'A'='B'";

// Loop through hash table and pull tables from server.
foreach (DictionaryEntry table in tables)
{
    rda.Pull(table.Key.ToString(), "SELECT * FROM " +
             table.Value.ToString() + sql,
             Common.Values.RemoteConnectionString,
           RdaTrackOption.TrackingOffWithIndexes, ref ds);
}

The parameter tables is either the reference or the transactional hash table, and the parameter data is a flag indicating whether data should be included in the download. For the transactional data tables, this value is false.

Now, back to the MainForm, where the menu is implemented as a custom control called SelectionList. The menu is initialized in the constructor after the call to the private InitializeComponent method:

selList = new SelectionList();
selList.Size = new System.Drawing.Size(176, 180);
selList.ItemSelected +=new ItemSelectedEventHandler(selList_ItemSelected);
this.Controls.Add(selList);

In the form's Load event, the menu options are created:

// Add menu options to SelectionList.
selList.Items.Add("Plan");
selList.Items.Add("Service");
selList.Items.Add("Report");
selList.Items.Add("Sync");
selList.Items.Add("Options");
selList.Items.Add("About");
selList.Focus();

Note that the control implementation requires the Focus method to be called at runtime. The code that is executed when the first menu option (Plan) is selected and looks like this:

private void selList_ItemSelected(object sender, ItemSelectedEventArgs e)
{
    switch (selList.SelectedIndex)
    {
        case 0 : // Plan.
            Cursor.Current = Cursors.WaitCursor;
            PlanForm planForm = new PlanForm(this);
            planForm.ShowDialog();
            selList.Focus();
            break;

        case 1 : // Service.
            // ...

During the load of the new form, the wait cursor is shown. Also note that the current form instance is passed as a parameter to the child form (PlanForm) to allow the child form to control the parent form's visibility.

This is what the PlanForm constructor looks like:

public PlanForm(Form parentForm)
{
    InitializeComponent();
    this.parentForm = parentForm;
}

The parent form instance is saved in a private variable (parentForm) that is used in the form's load event:

private void PlanForm_Load(object sender, EventArgs e)
{
    // ...
    parentForm.Hide();
    Cursor.Current = Cursors.Default;
}

It is used to hide the parent form and remove the wait cursor. Also, it is used when the form closes:

private void PlanForm_Closing(object sender, CancelEventArgs e)
{
    parentForm.Show();
}

The parent form is shown again. The reason for doing this is that there will be only one form visible for the application, which will prevent any other form from receiving focus and thereby confusing the user.

The PlanForm is implemented as a wizard form using panels, and reasons for doing this in preference to having a separate form for each wizard step include the following:

  • The user experience is better because the form load time is removed.
  • Because the nature of a wizard is that you should be able to go back and forth, the control and variable values do not need to be transferred between the forms.
  • As each form takes a considerable amount of memory, this memory is saved.

However, an obvious drawback of this approach is that the form design and implementation in general are a bit more complex.

This is what the code for the Cancel and Back soft keys look like:

private void mitCancel_Click(object sender, System.EventArgs e)
{
    this.Close();
}

private void mitBack_Click(object sender, System.EventArgs e)
{
    switch (currentStep)
    {
        case 4:
            // Go to fourth step.
            this.Text = "Plan 4/5";
            mitNext.Text = "Next";
            pnlFirst.Visible = false;
            pnlSecond.Visible = false;
            pnlThird.Visible = false;
            pnlFourth.Visible = true;
            pnlFifth.Visible = false;
            lvwProducts.Focus();
            currentStep = 3;
            break;

        case 3:
            // Go to third step.
            this.Text = "Plan 3/5";
            pnlFirst.Visible = false;
            pnlSecond.Visible = false;
            pnlThird.Visible = true;
            pnlFourth.Visible = false;
            pnlFifth.Visible = false;
            txtCustomer2.Focus();
            currentStep = 2;
            break;
        
        case 2:
            // Go to second step.
            this.Text = "Plan 2/5";
            pnlFirst.Visible = false;
            pnlSecond.Visible = true;
            pnlThird.Visible = false;
            pnlFourth.Visible = false;
            pnlFifth.Visible = false;
            lvwServices.Focus();
            currentStep = 1;
            break;

        case 1:
            // Go to first step.
            this.Text = "Plan 1/5";
            pnlFirst.Visible = true;
            pnlSecond.Visible = false;
            pnlThird.Visible = false;
            pnlFourth.Visible = false;
            pnlFifth.Visible = false;
            mitBack.Enabled = false;
            txtDateFrom.Focus();
            currentStep = 0;
            break;
    }
}

Note how the panels are shown and hidden to simulate the change of steps in the wizard. Initially (in the form's load event), they are all moved to the same position because they are more easily designed if placed side-by-side in the forms designer.

The code for the Next button, with application logic stripped, looks like this:

private void mitNext_Click(object sender, System.EventArgs e)
{
    switch (currentStep)
    {
        case 0:
            // Go to second step.
            this.Text = "Plan 2/5";
            pnlFirst.Visible = false;
            pnlSecond.Visible = true;
            pnlThird.Visible = false;
            pnlFourth.Visible = false;
            pnlFifth.Visible = false;
            mitBack.Enabled = true;
            lvwServices.Focus();
            currentStep = 1;
            break;

        case 1:
            // Go to third step.
            this.Text = "Plan 3/5";
            pnlFirst.Visible = false;
            pnlSecond.Visible = false;
            pnlThird.Visible = true;
            pnlFourth.Visible = false;
            pnlFifth.Visible = false;
            txtCustomer2.Focus();
            currentStep = 2;
            break;
        
        case 2:
            // Go to fourth step.
            this.Text = "Plan 4/5";
            pnlFirst.Visible = false;
            pnlSecond.Visible = false;
            pnlThird.Visible = false;
            pnlFourth.Visible = true;
            pnlFifth.Visible = false;
            lvwProducts.Focus();
            currentStep = 3;
            break;

        case 3:
            // Go to fifth step.
            this.Text = "Plan 5/5";
            mitNext.Text = "Finish";
            pnlFirst.Visible = false;
            pnlSecond.Visible = false;
            pnlThird.Visible = false;
            pnlFourth.Visible = false;
            pnlFifth.Visible = true;
            cboStatus.Focus();
            currentStep = 4;
            break;

        case 4:
            this.Close();
            break;
    }
}

Note how the buttons sometimes change visibility and also names.

In the PlanForm first step (panel), the Next button is used to search for service items, and the code looks like this:

Cursor.Current = Cursors.WaitCursor;
try
{
    // Get service list data.
    using (ServiceHandler serviceHandler = new ServiceHandler())
           drs = serviceHandler.GetList((int)Common.ServiceStatus.Open,
           txtDateFrom.Text, txtDateTo.Text, txtCustomer.Text,
           chkType.Checked);

    // Fill ListView.
    lvwServices.BeginUpdate();
    lvwServices.Items.Clear();
    foreach (DataRow dr in drs)
    {
        lvi = new ListViewItem(((DateTime) 
            dr["RequiredDate"]).ToShortDateString());
        lvi.SubItems.Add(dr.GetParentRow(
            "Machines2Services").GetParentRow(
            "Customers2Machines")["CompanyName"].ToString());
        lvi.SubItems.Add(dr["ServiceType"].ToString() == 
                         "1" ? "*" : "");
        lvi.SubItems.Add(dr["ServiceID"].ToString());
        lvi.SubItems.Add(dr.GetParentRow(
            "Machines2Services").GetParentRow(
            "Customers2Machines")["CustomerID"].ToString());
        lvi.SubItems.Add(dr.GetParentRow(
            "Machines2Services")["MachineID"].ToString());
        lvi.SubItems.Add(dr.GetParentRow(
            "Machines2Services")["Description"].ToString());
        lvi.SubItems.Add(dr.GetParentRow(
            "Machines2Services")["Location"].ToString());
        lvwServices.Items.Add(lvi);
    }
    lvwServices.EndUpdate();
}
catch (Exception)
{
    MessageBox.Show("Could not find any service items!", this.Text);
}
Cursor.Current = Cursors.Default;

The search arguments (date interval, customer name, and so on) are passed to a business logic class—ServiceHandler (GetList method). The result of the search is returned in an array of DataRows, and the ListView is filled with the rows (and subrows, because the rows also include relational data in other tables).

This is the ServiceHandler.GetList method implementation:

public DataRow[] GetList(int status, string dateFrom, string dateTo,
                         string customer, bool urgent)
{
    string sql;

    sql = "Status = " + status.ToString();
    if (dateFrom.Length  > 0)
        sql += " AND RequiredDate >= '" + dateFrom + "'";
    if (dateTo.Length > 0)
        sql += " AND RequiredDate <= '" + dateTo + "'";
    if (customer.Length > 0)
    {
        if (!ds.Tables["Machines"].Columns.Contains("CustomerName"))
        {
            DataColumn dc = 
                ds.Tables["Machines"].Columns.Add("CustomerName");
            dc.Expression = "Parent(Customers2Machines).CompanyName";
        }
        sql += " AND Parent(Machines2Services).CustomerName 
            LIKE '%" + customer + "%'";
    }
    if (urgent)
        sql += " AND S.ServiceType=1";
    DataRow[] drs = ds.Tables["Services"].Select(sql, "RequiredDate");

    return drs;
}

A SQL statement is dynamically built, depending on which parameters contain any values. Because a selection is required on the customer name, a derived field is needed in the Machines table that holds the company name of the customer. If that field does not exist (that is, it is the first time the search is made), the derived field is created with an expression getting the customer name (CompanyName), using a secondary relation (Machines2Services). Then the SQL condition is added with a reference to the newly created field by using the primary relation (Machines2Services).

The ServiceHandler class holds a private reference (ds) to the application's main DataSet (database) that is retrieved from the Common singleton class:

private DataSet ds;

public ServiceHandler()
{
  ds = Common.Values.DatabaseDataSet;
}

Returning to the PlanForm, here's a look at the code executed after the last step in the wizard and when you have selected a new status:

using (ServiceHandler serviceHandler = new ServiceHandler())
{
    DataRow dr = serviceHandler.GetForID(serviceID);
    dr["Status"] = Common.ServiceStatus.Planned;
    dr.EndEdit();
    serviceHandler.Save();
}

Once again, a ServiceHandler instance is used to get the current service item and also to save it with an updated status.

The ServiceHandler.GetForID method looks like this:

public DataRow GetForID(string serviceID)
{
    DataView dv = new DataView(ds.Tables["Services"]);
    dv.RowFilter = "ServiceID='" + serviceID + "'";
    return dv[0].Row;
}

A DataView with a filter is used to return the requested service item as a DataRow.

This is the code to save the changes after the update (ServiceHandler.Save method):

public void Save()
{
    ds.AcceptChanges();
}

All changes are simply accepted in the application database (actually DataSet).

Here's a look at how the service items are synchronized. This synchronization uses a Web service that implements the synchronization logic that is called from the client with the following code:

WebServices.Services servicesWebService = new WebServices.Services();

// Set URL of Web service.
servicesWebService.Url = Common.Values.RemoteWebServiceURL;

using (ServiceHandler serviceHandler = new ServiceHandler())
{
    DataSet ds = serviceHandler.GetReportedServices();

    // Transfer reported services to server and get new services back.
    servicesWebService.Credentials = new NetworkCredential(remoteLogin,
      remotePassword);
    DataSet dsReturned = 
      servicesWebService.Sync(Common.Values.EmployeeID, ds);

    // Since call to server went OK, assume services were transferred
    // and can therefore delete them from the local database.
    serviceHandler.DeleteReportedServices();

    // Insert new services (and details) into database.
    serviceHandler.AddNewServices(dsReturned);
}

The Web service is instantiated, and the URL is set. The reported (completed) service items are retrieved using the serviceHandler object's GetReportedServices method and sent to the Web service method (Sync) when the credentials are set. When the call to the Web service completes successfully, the reported (completed) service items are removed from the local database (DeleteReportedServices). Finally, the returned (new) service items are added to the local database (AddNewServices).

Here's the serviceHandler.GetReportedServices method:

public DataSet GetReportedServices()
{
    DataSet dsRep = new DataSet();
    dsRep.Tables.Add(ds.Tables["Services"].Clone());
    dsRep.Tables.Add(ds.Tables["ServiceDetails"].Clone());
    DataRow[] drs = ds.Tables["Services"].Select("Status=" +
        ((int)Common.ServiceStatus.Reported).ToString());
    foreach (DataRow dr in drs)
    {
        dsRep.Tables["Services"].Rows.Add(dr.ItemArray);
        foreach (DataRow drDetail in 
                 dr.GetChildRows("Services2Details"))
           dsRep.Tables["ServiceDetails"].Rows.Add(drDetail.ItemArray);
    }
    dsRep.Tables["Services"].ChildRelations.Add("Services2Details",
        dsRep.Tables["Services"].Columns["ServiceID"],
        dsRep.Tables["ServiceDetails"].Columns["ServiceID"], true);
    return dsRep;
}

Note that the relation between the Services and ServiceDetails tables are added to the DataSet, and the code for the DeleteReportedServices method is as follows:

public void DeleteReportedServices()
{
    DataRow[] drs = ds.Tables["Services"].Select("Status=" +
        ((int)Common.ServiceStatus.Reported).ToString());
    foreach (DataRow dr in drs)
    {
        foreach (DataRow drDetail in 
                 dr.GetChildRows("Services2Details"))
            drDetail.Delete();
        dr.Delete();
    }
    ds.AcceptChanges();
}

The two tables are emptied of reported service items, and following is the AddNewServices implementation:

public void AddNewServices(DataSet dsNew)
{
    foreach (DataRow dr in dsNew.Tables["Services"].Rows)
        ds.Tables["Service"].Rows.Add(dr.ItemArray);
    foreach (DataRow dr in dsNew.Tables["ServiceDetails"].Rows)
        ds.Tables["ServiceDetail"].Rows.Add(dr.ItemArray);
    ds.AcceptChanges();
}

Note that the DataRow ItemArray property is used to add row data because the DataRow can belong to only one DataTable.

The code for the server Web service method looks like this:

[WebMethod]
public DataSet Sync(string employeeID, DataSet dsClient)
{
    SqlConnection cn;
    SqlDataAdapter da;
    SqlDataAdapter daDetail;
    SqlCommandBuilder cb;
    SqlCommandBuilder cbDetail;
    DataSet dsServer;
    DataRow[] selectedRows;
    DataRow drServer;
    DataSet dsReturn;

    // Thread.CurrentPrincipal.Identity.Name

    // Set up database connection.
    cn = new SqlConnection("data source=(local);initial" +
        " catalog=NorthwindX;integrated security=SSPI;");
    cn.Open();
    da = new SqlDataAdapter("SELECT * FROM Services", cn);
    cb = new SqlCommandBuilder(da);
    da.InsertCommand = cb.GetInsertCommand();
    da.UpdateCommand = cb.GetUpdateCommand();
    da.DeleteCommand = cb.GetDeleteCommand();
    dsServer = new DataSet();
    da.Fill(dsServer, "Services");
    daDetail = new SqlDataAdapter("SELECT * FROM ServiceDetails", cn);
    cbDetail = new SqlCommandBuilder(daDetail);
    daDetail.InsertCommand = cbDetail.GetInsertCommand();
    daDetail.UpdateCommand = cbDetail.GetUpdateCommand();
    daDetail.DeleteCommand = cbDetail.GetDeleteCommand();
    daDetail.Fill(dsServer, "ServiceDetails");
    dsServer.Tables["Services"].ChildRelations.Add("Services2Details",
        dsServer.Tables["Services"].Columns["ServiceID"],
        dsServer.Tables["ServiceDetails"].Columns["ServiceID"], true);

    // Update Services.
    foreach (DataRow drClient in dsClient.Tables["Services"].Rows)
    {
        selectedRows = dsServer.Tables["Services"].Select("ServiceID='" 
            + drClient["ServiceID"].ToString() + "'");
        drServer = selectedRows[0];
        drServer["DeliveredDate"] = drClient["DeliveredDate"];
        drServer["Status"] = drClient["Status"];
        drServer["CustomerCopy"] = drClient["CustomerCopy"];
        drServer["ContactName"] = drClient["ContactName"];
        if (dsClient.Tables["Services"].Columns.Contains("Signature"))
            drServer["Signature"] = drClient["Signature"];

        // Update ServiceDetails.
        foreach (DataRow dr in 
                 drClient.GetChildRows("Services2Details"))
        {
            selectedRows = dsServer.Tables["ServiceDetails"].Select(
                "ServiceDetailID='" + 
                dr["ServiceDetailID"].ToString() + "'");
            drServer = selectedRows[0];
            drServer["DeliveredQuantity"] = dr["DeliveredQuantity"];
        }
    }

    // Update server database.
    da.Update(dsServer, "Services");
    daDetail.Update(dsServer, "ServiceDetails");

    // Use EmployeeID and status to select services to return.
    da = new SqlDataAdapter("SELECT * FROM Services 
        WHERE EmployeeID='" + employeeID + "' AND Status=0", cn);
    dsReturn = new DataSet();
    da.Fill(dsReturn, "Services");
    da.SelectCommand.CommandText = "SELECT D.* FROM ServiceDetails D" +
        " INNER JOIN Services S ON D.ServiceID=S.ServiceID WHERE" +
        " S.EmployeeID='" + employeeID + "' AND S.Status=0";
    da.Fill(dsReturn, "ServiceDetails");

    // Update status flags (to avoid duplicate transfer).
    SqlCommand cmd = cn.CreateCommand();
        cmd.CommandText = "UPDATE Services SET Status=1 WHERE 
        EmployeeID='" + employeeID + "' AND Status=0";
    cmd.ExecuteNonQuery();

    return dsReturn;
}

First, the database connection and data adapters for the two tables are set up, and the reported service items (included in the dsClient DataSet) are used to update the Services and ServiceDetails tables in the server database. Then the new service items (with status set to zero—that is, Open) are filled into a DataSet that is returned to the client when the status in the server database is updated to prevent duplicate transfer.

Conclusion

Windows Mobile-based Smartphones put business at the fingertips of customers, partners, and employees. A solid platform and development tools are available to developers facing the challenge of designing and developing mobile solutions based on Smartphones. This article provided a first glimpse of what a mobile enterprise application based on the Smartphone platform can look like. Download the source code and hit the ground running!