Click to Rate and Give Feedback
MSDN
MSDN Library
Windows Mobile
Technical Articles
Sample Solutions
 
Pocket PC (General) Technical Articles
Northwind Pocket Analyze: Decision Support for Windows Mobile-based Pocket PCs
 

Christian Forsberg
business anyplace

March 2006

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

Summary: Learn about mobile decision support and how to design and develop solutions for Windows Mobile 2003–based Pocket PCs using Visual Studio .NET and the .NET Compact Framework. The download code sample for this article implements a server XML Web service, database, and a Pocket PC client. (67 printed pages)


Download Northwind_Traders_Decision_Support_PPC.msi from the Microsoft Download Center.

Contents

Introduction
Northwind Traders's Decision Support Business Process
Application Design
Data Analysis
Connection Aware
Screen-Orientation Aware
Data Compression
Northwind Pocket Analyze Application Walkthrough
Code Walkthrough
Conclusion

Introduction

This article continues on the topics presented in these articles:

The first and second articles describe key elements about how to develop mobile field service applications. The third and fourth articles focus on developing mobile field sales applications. The fifth and sixth articles describe the design and implementation of mobile logistic solutions, and the last two articles focus on solutions for supporting a transportation business process.

This article is about designing and developing a mobile decision support application that is based on the .NET Framework, the Windows Mobile platform for Pocket PC, and the .NET Compact Framework. The sample solution addresses the needs of the fictitious company Northwind Traders from a decision support process and field data analysis points of view. From a technological point of view, the sample demonstrates how to implement an orientation-aware user interface, implement basic pivot tables, use GDI+ to do basic charting, use the hardware keys and stylus for user interface navigation, use compression in remote communication (XML Web service calls) and optimize local storage, make applications be connection aware, capture screen shots, and more.

Northwind Traders's Decision Support Business Process

As previous articles have described, the customers of Northwind Traders have vending machines that sell various products from Northwind Traders. The other information systems that the organization uses create many pieces of valuable information. To maintain competitive strength and also a productive workforce, a company must focus on making the right decisions. Decision makers can make correct decisions if the decisions are based on up-to-date facts and, most often, figures.

Because Northwind Traders has invested in a healthy digital nervous system—a solid information infrastructure—this company has many possibilities readily available for data analysis (for more information, see the Data Analysis section in this article). Although parts of solutions that are created with these tools (like Reporting Services reports that are adapted for Pocket Internet Explorer for Pocket PC) can be made available for the mobile workforce, the decision makers at Northwind Traders wanted something more powerful. They wanted something that could make use of the central data and would still work disconnected (offline). The following list shows the steps that such a business process would include:

  1. Retrieve the data to analyze.
  2. Create a view of the data to analyze.
  3. Analyze the data as a table.
  4. Analyze the data as a chart.
  5. Report a decision using the table and chart.

That business process can be illustrated as shown in Figure 1.

Figure 1. Business process that helps to make correct decisions

As shown in Figure 1, the main steps in the business process involve a number of sub-activities that function as main requirements for the new solution. Assuming this is a good way to define the new process, the next step would be to look at the design of the application.

Application Design

The article, Northwind Pocket Service: Field Service for Windows Mobile-based Pocket PCs, shows a good introduction to the architectural work in a mobile solution. The article, Northwind Pocket Inventory: Logistics for Windows Mobile-based Pocket PCs, describes the most important deliverables (artifacts) in the application's design. The design begins with the definitions 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 dialogs. Figure 3 illustrates the dialog model for the sample application.

Figure 3. Dialog model

The dialog model gives an overview of the dialogs (forms) that are included in the application, and the model also shows the navigation between these dialogs.

Figure 4 shows some sample dialogs.

Figure 4. Sample dialogs

These dialog samples were drawn in a general drawing tool (in this case, Microsoft PowerPoint). Someone who is knowledgeable in user interface design should be involved in creating the dialogs. Visualizing the application's dialogs early in the process gives the 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 Northwind_Traders_Decision_Support_PPC.msi, you can find all of the previous figures in a PowerPoint presentation that you can reuse when you create your own diagrams.

Data Analysis

With the data stored in SQL Server, its Analysis Services and Reporting Services, and client tools like Microsoft Office and the Office Web Components provide almost endless possibilities for data analysis.

You can retrieve data directly from the relational database or from data warehouse cubes of precalculated data in multiple dimensions. Using the above tools, the precalculated data can even be stored in an offline cube file (.cub) to be used for disconnected analysis.

You can manipulate data in Excel by using pivot tables and pivot charts, and the data can be an online data source (relational database or data warehouse cube) or an offline cube. When you make an analysis in Excel, you can export it as a Web page, which you can publish on a Web server. That Web page would use Office Web Components to enable interactive analysis on any client that has a license for Microsoft Office. Figure 5 shows an example of a Web page that was created in this way.

Click here for larger image

Figure 5. Sample Web page that uses Office Web Components. Click the thumbnail for a larger image.

On this Web page, the chart and table are connected, so any changes that the user makes to the table is reflected in the chart—and vice versa. Many of the functions you can perform in Excel are available on this Web page as well. This Web page is clearly an alternative for organizational-wide light-weight analysis. For more information about data analysis, see the previously listed Web pages in the beginning of this section.

Although these tools are not available to Pocket PC users, this article's sample shows how you can add similar functionality—however basic—to an enterprise analysis application.

Connection Aware

Because most Pocket PC devices are sometimes connected (online), there is a need for enterprise applications to work both connected and disconnected. When the device is connected, the device should retrieve data from the sources because this data is probably the most up to date. When a connection is not available, data should be available offline to enable the application to function. Figure 6 shows the two data retrieval options.

Figure 6. Data retrieval options

The application's first priority is to retrieve the data from the server through the network (indicated by the arrow with number 1 in Figure 6). If network connectivity is available (the device is online), the application retrieves data in this way. When the application retrieves the data, it also stores the data on the device to enable the application to function without a connection. When a connection is not available—and provided that the data is available on the device—the application retrieves the data from the local storage (indicated by the arrow with number 2 in Figure 6). To be sure that the application functions the first time it is opened, the user can install the local data either with the application, or the application can retrieve the data the first time the server is accessed after the application is installed.

This is how the sample application accompanying this article is implemented to function properly independent of a network connection.

Screen-Orientation Aware

Because most devices allow the user to rotate the screen's orientation, an enterprise application should support this feature. Although the operating system manages any application that is not screen-orientation aware (that is, putting scroll bars on the parts of the form that cannot be displayed), it will not deliver a great user experience.

Because the Code Walkthrough section of this article reveals that it is not very difficult to implement orientation awareness in a .NET Compact Framework application, there is no good excuse (except maybe a smaller amount of time and cost) for excluding such support.

There are a number of options available to make forms orientation aware:

  • Design for the upper-left square (240 x 240) of the form.
  • Change the content of the form.
  • Change the layout of the form.

Even though the first option is the simplest, it is not possible in all situations. A consequence of this design approach leaves the user somewhat troubled because the screen has wasted space. And even if a developer can avoid wasting screen space, the second option is clearly not recommended because the goal is not to change the user's experience due to screen orientation. Therefore, the third option is recommended. You should make the form's controls adapt to the current screen orientation and try to make use of the screen space in the most efficient way.

In the Northwind Pocket Analyze Application Walkthrough section of this article, there are comments about how the orientation awareness was done in the different screens and why.

Note   When you run the sample application (which you can find in this article's Northwind_Traders_Decision_Support_PPC.msi), you can rotate the screen in the emulator by pressing the F2 key (on your computer's keyboard) or in the emulator screen settings (by clicking Start, Settings, and then Screen).

For more general details about screen-orientation awareness, see Developing Screen Orientation-Aware Applications.

Data Compression

Developers can use data compression in enterprise applications for a number of reasons:

  • Reduce storage space.
  • Reduce communication time.
  • Reduce communication cost.
  • Increase performance.

The fact that data compression saves storage space hardly surprises any developer. Despite that fact, many developers do not make use of this fairly common technology in their mobile enterprise applications to decrease the demand for storage space on their mobile devices. On any mobile device, storage is a scarce resource, and anything that developers do to reduce the space that any application requires is welcome.

XML is an attractive standard to use due to its general acceptance and the support that is built into most development tools. The XML file format is far from space efficient—XML files contain extensive metadata (such as schemas and tags), and the fact that they are text based, which creates large files. However, the same facts make them very suitable for data compression. All of the white space (for example, spaces and tabs) and the repeating patterns in the files match the compression algorithms very well, as shown in Figure 7.

Figure 7. Compression of medium-sized payload

The chart in Figure 7 shows a typical DataSet instance (which the sample application uses) that is stored as XML. The original file size (raw) is about 190 KB, and the size is reduced to about 12 KB when the file is compressed—a storage save of more than 93 percent! The difference is not that interesting for small payloads, but as payloads get larger, the gain increases.

Also, the reduced communication time and cost comes as obvious consequences for smaller sizes. In scenarios where the cost is related directly to the size rather than the time (like with General Packet Radio Services [GPRS]), the gains are generally larger.

The fact that compression increases performance may come as a surprise because compression generally requires a certain amount of processing power. For smaller payloads, compression logically decreases the performance (however relatively marginal). However, the fact that using larger data loads is processing intensive ultimately means that the overall performance is increased as data is compressed.

Numerous standards exist for data compression, but the most commonly used standards are gzip (RFC 1952) and deflate (RFC 1951). The deflate standard is more commonly known as the zip format because this is the format used in normal zip files (.zip). The gzip format is the preferred format for compressing Web content, and the deflate (.zip) format is the most commonly used format for storage compression. Their respective compression rate is about the same—as are their performance demands.

When it comes to the compression of Web content, the HTTP 1.1 standard (RFC 2616) includes a specification for content compression. In the latest version of Internet Information Services 6.0 (IIS), there is built-in support for this compression. You can enable compression for all of your ASP.NET and ASP.NET XML Web service applications without changing a single line of code.

Northwind Pocket Analyze Application Walkthrough

The sample client scenario is a Pocket PC application that is written with Microsoft Visual Studio .NET 2003 in C# and targets the Microsoft .NET Compact Framework. The sample uses the Smart Device Framework from OpenNETCF, which is also available as source code.

The application shows how to support the decision support business process by using a Pocket PC. The design choices align to the process as much as possible and also maximize efficiency for the decision maker. Some of the design choices are commented in this walkthrough. Also note that this article explores parts of the code after it describes the application user interface design.

The functionality of the application aligns with the business process of a decision maker. The main steps in this process are to get the data to analyze, create a view of that data, analyze the view of the data by using a pivot table and charts, and report the result of the analysis in a report.

Feeds Screen

When the user starts the application, the first screen is the main screen, which is called the Feeds screen, to manage the feeds of data from the server, as shown in Figure 8.

Figure 8. Feeds screen

The main purpose of this screen is to retrieve data to be analyzed. By tapping the Find button, a user can search for available feeds. If the device is online (connected to a network and thereby to a server), an XML Web service call is made to the server to retrieve the list of available feeds. The application locally saves this feed list to be used for later requests when the device is offline.

When a user selects a feed in the list, there are two commands available for that feed: the View and Analyze commands. Selecting View shows the details about the feed, and selecting Analyze initiates a retrieval and analysis of the feed data. The same commands are also available in a shortcut menu (if the user taps and holds a feed in the list). This article describes more details about these menu commands in the Analyze section.

The Offline command indicates that the user wants to work in an offline mode (when there is not a network connection). One reason to work offline is to save network traffic (and thereby cost when using an expensive connection) because the feeds that the application saved locally may have sufficiently current information.

The Feeds screen in landscape mode is shown in Figure 9.

Figure 9. Feeds screen in landscape mode

In this mode, the Find button moves to remain right-aligned, and note how the Name box was resized to allow the user to enter more data. The feed list is also resized to allow more data to be shown. Now the application shows the full date and time. However, not shown in Figure 9, the feed list now allows less number of rows compared to the amount of rows that display in portrait mode.

When the user selects the Offline command for the first time (to remove the check mark) and puts the application in online mode (if a network connection is available), the Login screen appears, as shown in Figure 10.

Figure 10. Login screen

The Username box is populated (if the user enters this information in the Options screen as described in the Options section of this article), and the application uses this information when it connects to the XML Web service.

When the user selects the View command on the Feed screen or from the list's shortcut menu, the Feed details screen appears, as shown in Figure 11.

Figure 11. Feed details screen

On this screen, the user can review information about the feed, like when the feed was last downloaded from the server, view the size of the feed data, the local space that is required to store the feed (this information is in parentheses), and read a description about the feed's information. The difference between the two sizes is the result of the data compression used for storing local data.

The Feed details screen in landscape mode is shown in Figure 12.

Figure 12. Feed details screen in landscape mode

Most of the content remains intact, but the description text is resized to fit the new screen dimensions.

Analyze

When the user selects the Analyze command, the application initiates a retrieval of the selected feed. If the device is online, an XML Web service call is made to the server to retrieve the feed. The application also saves this feed locally to be used for later requests when the device is offline. When the application retrieves the feed, the View screen displays and allows the user to define the view, as shown in Figure 13.

Figure 13. View screen

The user defines the view by selecting what dimensions should be analyzed. The visual guide below the Formula box shows how the user's selections will be shown in the pivot table. The selections in Figure 13 will create a pivot table with the quarters as columns and the employees as one row each. The data will come from the amount, which in this example is the total sales, and the data will be calculated as sums for each cross-section (there is also an average formula option available).

In Figure 14, the View screen in landscape mode is shown.

Figure 14. View screen in landscape mode

Figure 14 shows a typical example of a design for a square screen that doesn't require any modifications to the form layout that depends on the screen's orientation. The lists could be resized to cover the width of the form, but that would probably make the user experience worse because the distance between the short dimension names and the drop-down arrow would be too long to be intuitive.

When the user taps OK on the View screen, the Analyze screen with the pivot table is displayed, as shown in Figure 15.

Figure 15. Analyze screen displaying a pivot table

Figure 15 shows a pivot table with the data organized according to the user's selections on the View screen in Figure 13. The sales figures are summed for each employee for each time period (in this example, quarters). For each quarter, the pivot table also calculates a total. To return to the definition of the view on the View screen (Figure 13), the user selects the View command. This article describes the Chart command and the Copy menu later in this article.

The user can scroll through the pivot table by tapping the scroll bar, but the user can also use the direction keys. If the user presses the Action key (usually found at the center of the direction keys), the user can toggle the keyboard navigation between small and large steps. If the user does not press the Action key, the direction keys scroll the pivot table in small steps; if the user presses it once, the direction keys scroll the pivot table in large steps. If the user scrolls through the pivot table to the right, the screen looks like Figure 16.

Figure 16. Analyze screen scrolled to the right

Each row also has a total, and the last cell at the bottom of the pivot table is the grand total.

The Analyze screen in landscape mode is shown in Figure 17.

Figure 17. Analyze screen in landscape mode

Figure 17 shows the pivot table resized to cover the new screen dimensions.

When the user selects the Copy menu, the application copies the current content of the pivot table on the clipboard in a tab-separated format; this format enables pasting into other tabular-aware applications, such as Pocket Excel. The data in Figure 17 copied from the pivot table and pasted into a Pocket Excel worksheet looks like Figure 18.

Figure 18. Data copied to Pocket Excel

Figure 19 shows the same Pocket Excel worksheet in landscape mode.

Figure 19. Pocket Excel worksheet in landscape mode

The user can further analyze the data in Pocket Excel by using the formula functions. The user can also save a Pocket Excel file and transfer (by means of ActiveSync) or attach it to an e-mail message to be further manipulated on a desktop computer or server.

Chart

The Chart screen is displayed, as shown in Figure 20, when the user selects the Chart command from the Analyze screen's menu.

Figure 20. Chart screen

When the application opens the Chart screen, the default chart type is a line chart. The scale of the y-axis is adjusted to fit the data, and in Figure 20, the data is divided by 100 making the top of the y-axis equal to $70,000. In Figure 21, the Legend command is selected.

Figure 21. A chart with a legend

The user shows the legend by tapping Menu, and then by tapping Legend. By tapping this command again, the legend is removed. The legend shows a guide to each of the series in the chart. In this example, the series corresponds to the rows in the pivot table (which correspond to employees) as shown in Figures 15, 16, and 17. Each point on the x-axis corresponds to a category, which are the columns of the pivot table (which correspond to quarters).

Because the legend might cover important information in the chart, the user can move the legend by using the direction keys. Pressing the Action key toggles the legend to move in large and small movements. If the user does not press the Action key, the direction keys move the legend in large steps (10 pixels); if the user presses it once, the direction keys move the legend in small steps (1 pixel). The user can also drag the legend by using the stylus, as shown in Figure 22.

Figure 22. Dragging the legend

If the user taps and holds the legend and then moves the stylus, the user can freely position the legend over the chart. By releasing the stylus, the user chooses the legend's position, as shown in Figure 23.

Figure 23. Trend chart with a legend

The Trend command calculates a linear trend for each of the chart series. The application calculates the trend by using linear regression and the least square method. A trend chart is a very effective way to view the information because it often says much more than the raw data. Compare the trend chart in Figure 23 with the exact same raw data in Figure 20—you can see the difference.

When the user selects Save, the application saves the chart that is currently displayed (including the legend) as a file. A confirmation message appears, as shown in Figure 24.

Figure 24. Confirmation message to save a chart as a file

The application saves the file in the My Documents folder, and the application automatically generates a unique name (for example, chart0.bmp or chart1.bmp). The user can transfer the chart image file (by means of ActiveSync) or attach it to an e-mail message to be further manipulated on a desktop computer or server.

The user can also flip data by selecting the Flip command. The series and categories (x-axis and y-axis) switch locations on the chart. The legend updates accordingly, as shown in Figure 25.

Figure 25. A flipped chart and an updated legend

Because the application can flip charts numerous times, this could be a quick way for the user to see the x-axis values (because the values are not shown to save screen space). A flip generates a new chart (which is not the case for Legend, Trend, and Save), and each time the application generates a new chart, the application generates a new set of colors for the series. The colors are generated randomly, so it can be practical for a user to flip the chart a number of times until a satisfying color combination is achieved.

The Bar command toggles the chart type between a line chart (which this article discussed previously) and a bar chart, as shown in Figure 26.

Figure 26. Bar chart

Figure 26 shows a stacked bar chart where all of the series values are added, and those values show the total sales per employee. Though not shown in the chart, the fifth x-axis value is the total sales for Margaret Peacock (refer to the legend in Figure 23 for the order of employees). Again, the application adjusts the y-axis to fit the data this new chart displays; in Figure 26, the data is divided by 1000, making the top of the y-axis equal to $300,000. The application also generates a new set of colors. Note how the colors that are generated for bar charts are lighter than the colors for line charts—these colors improve the appearance of the chart. When the application shows a bar chart, the application changes the Bar command to Line to allow the user to switch to a line chart.

Figure 27 shows the bar chart in landscape mode.

Figure 27. Bar chart in landscape mode

The application resizes the chart to fit the whole screen and aligns the legend to the top-right corner of the chart.

Figure 28 shows a line chart in landscape mode.

Figure 28. Line chart in landscape mode

Note how the user can move the legend by using the keyboard or stylus. The corresponding trend chart is shown in Figure 29.

Figure 29. Trend chart in landscape mode

The ability to switch between portrait and landscape modes gives the user more alternative ways of looking at the data.

Report

As previously mentioned, the user can copy the analysis data from the pivot table to Pocket Excel, save the data in a file, and transfer the file to a desktop computer. A user can also save a chart as an image file that can be transferred to the desktop computer. With those two files, the user can easily put together a nice looking sales report in Microsoft Word, as shown in Figure 30.

Click here for larger image

Figure 30. Sales report using the chart and pivot table data. Click the thumbnail for a larger image.

The user can simply insert the chart image in the document and import the table from Microsoft Excel—the program that opens the Pocket Excel file.

Options

The Options screen, as shown in Figure 31, is displayed when the user selects the Options command on the Feed screen.

Figure 31. Options screen

The Web Service (URL) box shows the URL for the XML Web service that is used to retrieve both the list of available feeds and the individual feeds. The Server Login (Username) box shows the default user name when the device connects to an XML Web service.

Figure 32 shows the Options screen in landscape mode.

Figure 32. Options screen in landscape mode

The boxes are resized to fit the new width, allowing the Web Service (URL) box to show more data.

About

All applications should include a screen with the product name, version, copyright, and other legal information. On the Feed screen, the About command 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. Figure 34 shows the About screen in landscape mode.

Figure 34. About screen in landscape mode

Note how the text moved upward and the picture moved to the right and rescaled. The purpose of these moves is to make sure the important text information is presented to the user; the lack of space when the application is in portrait mode only affects the graphic.

Because the application supports globalization (and localization), the next section shows some translated screens.

World Ready

When the user changes the regional settings (by tapping Start, Settings, and then Regional Settings) to Portuguese (Brazil) and then restarts the application, the complete sample application is translated. The following figures are the Brazilian Portuguese versions of the screens that are shown in Figures 8, 11, 13, 15, 23, and 33.

Figure 35 shows the translated Feed screen. The form's title and controls (like the Alimentos and Nome labels, the Busca button, and the list headings) are translated—as are the commands.

Figure 35. Translated Feed screen

Figure 36 shows the translated Feed details screen. Like Figure 35, the form's title and controls (labels) are translated. Also note that the date format has changed according to the current locale.

Figure 36. Translated Feed details screen

Figure 37 shows the translated View screen. The labels, the visual guide (below the lists), and the formula options are translated.

Figure 37. Translated View screen

Figure 38 shows the translated Analyze screen. The title, labels, and commands are translated.

Figure 38. Translated Analyze screen

Figure 39 shows the translated Chart screen. Again, the title, labels, and commands are translated.

Figure 39. Translated Chart screen

Finally, Figure 40 shows the translated About screen.

Figure 40. Translated About screen

All of the screens are translated in landscape mode as well.

This concludes the walkthrough of the client application, and now this article describes some of the source code.

Code Walkthrough

The previous section provided an example client scenario for the Pocket Analyze application, and now it's time to look at the source code for that sample. The general parts of the code are covered in the article Northwind Pocket Service: Field Service for Windows Mobile-based Pocket PCs, so this article focuses on some of the topics that are unique for this sample.

Analysis Data

A good place to start is to look at how the analysis data is stored, created, and distributed. The actual data to be analyzed is included in the sample database, which is an enhanced version of the Northwind sample database. It includes basic information about employees, sales orders, service engagement, requested deliveries, and so on.

To structure the analysis data, a new Feeds table was created with the following code example to hold the feeds (queries).

CREATE TABLE Feeds (
    FeedID NVARCHAR(20) PRIMARY KEY NOT NULL,
    FeedName NVARCHAR(50) NOT NULL,
    [Description] NVARCHAR(256) NULL,
    Query NVARCHAR(4000) NOT NULL
)

It includes an identifier name in the FeedID column, a name of the feed in the FeedName column, and a long description in the Description column. The most interesting column is the actual specification of the feed named Query. This column holds the SQL SELECT statement to be submitted to extract the feed from the database. A typical feed specification looks like the following code example.

SELECT (E.FirstName + ' ' + E.LastName) Employee,
    C.CompanyName Customer, S.CompanyName Shipper,
    CONVERT(NVARCHAR(10), O.OrderDate, 120) [Date],
    RIGHT(CONVERT(NVARCHAR(4), DATEPART(year, O.OrderDate)), 2) [Year],
    RIGHT(CONVERT(NVARCHAR(4), DATEPART(year, O.OrderDate)), 2) + '-Q' +
    CONVERT(NVARCHAR(2), DATEPART(quarter, O.OrderDate)) [Quarter],
    RIGHT(CONVERT(NVARCHAR(10),DATEPART(year, O.OrderDate)), 2) + '-' +
    RIGHT('0' + CONVERT(NVARCHAR(2), DATEPART(month, O.OrderDate)), 2)
    + ' ' + CONVERT(NVARCHAR(3), DATENAME(month, O.OrderDate)) [Month],
    ROUND(SUM(D.UnitPrice * D.Quantity * (1 - D.Discount)) +
    O.Freight, 0) Amount
FROM Orders O JOIN OrderDetails D ON O.OrderID=D.OrderID
    JOIN Customers C ON O.CustomerID=C.CustomerID
    JOIN Employees E ON O.EmployeeID=E.EmployeeID
    JOIN Shippers S ON O.ShipVia=S.ShipperID
GROUP BY E.LastName, E.FirstName, C.CompanyName, S.CompanyName,
    O.OrderDate, O.Freight

Table 1 shows a typical output from this feed.

Table 1. Feed data output

Employee Customer Shipper Date Year Quarter Month Amount
Steven Buchanan Berglunds snabbköp Speedy Express 1997-09-02 97 97-Q3 97-09 Sep 657
Steven Buchanan Berglunds snabbköp Speedy Express 1998-02-03 98 98-Q1 98-02 Feb 1205
Steven Buchanan Blondesddsl père et fils United Package 1996-09-04 96 96-Q3 96-09 Sep 1426
Steven Buchanan Bon app' Speedy Express 1997-11-05 97 97-Q4 97-11 Nov 504
Steven Buchanan Chop-suey Chinese United Package 1996-07-11 96 96-Q3 96-07 Jul 580
Steven Buchanan Familia Arquibaldo Federal Shipping 1997-08-29 97 97-Q3 97-08 Aug 1956
Steven Buchanan Folk och fä HB Federal Shipping 1996-12-10 96 96-Q4 96-12 Dec 109

All of the columns, except the Amount column, is the categorization of the analysis data—commonly referred to as the dimensions. The dimensions are used to compare different groupings of information. The last column in Table 1 holds the value to be analyzed. This value is the total sales per date (calculated as unit price x quantity–discount for all order details + freight).

To access the analysis data in a structured way, the stored procedure spGetFeed is created by the following code example.

CREATE PROCEDURE spGetFeed @feedID NVARCHAR(20)
AS
DECLARE @sql NVARCHAR(4000)
SELECT @sql = Query FROM Feeds WHERE FeedID = @feedID
EXEC(@sql)
GO

When the stored procedure is supplied with the feed identifier feedID, the feed specification (Query) is retrieved from the Feeds table and is executed to produce the returned result set. Although this stored procedure could be published directly from SQL Server 2000 by using SQLXML a straightforward XML Web service is created with the following code example.

[WebMethod]
public DataSet GetFeedList()
{
    DataSet ds = new DataSet();
    using(SqlConnection cn = new SqlConnection(this.connectionString))
    {
        cn.Open();
        SqlDataAdapter da = new SqlDataAdapter("SELECT FeedID,
            FeedName, Description FROM Feeds ORDER BY FeedName", cn);
        da.Fill(ds);
    }
    return ds;
}

[WebMethod]
public DataSet GetFeed(string feedID)
{
    DataSet ds = new DataSet();
    using(SqlConnection cn = new SqlConnection(this.connectionString))
    {
        cn.Open();
        SqlCommand cmd = cn.CreateCommand();
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.CommandText = "spGetFeed";
        cmd.Parameters.Add("@feedID", feedID);
        SqlDataAdapter da = new SqlDataAdapter(cmd);
        da.Fill(ds);
    }
    return ds;
}

The first method is used to get the list of available feeds by using a direct query on the database Feeds table, and the second method is used to retrieve the feed result by calling the stored procedure spGetFeed. The private class variable connectionString holding the connection string is declared as shown in the following code example.

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

With a reference to the correct namespace ("System.Configuration"), this declaration extracts the following application setting from the configuration file (Web.config), as shown in the following code example.

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

These lines of code make it easier for you to manage the connection string.

On the client side, the following code calls the two Web methods, which are in the RemoteHandler class.

public DataSet GetFeedList()
{
    WebServices.Analyze analyzeWebService = new WebServices.Analyze();

    // Set the URL of the Web service
    analyzeWebService.Url = Common.Values.WebServiceUrl;

    // Set the credentials
    analyzeWebService.Credentials =
        new NetworkCredential(Common.Values.UserName,
        Common.Values.Password);

    // Get the list of feeds
    DataSet ds = analyzeWebService.GetFeedList();

    return ds;
}

public DataSet GetFeed(string feedID)
{
    WebServices.Analyze analyzeWebService = new WebServices.Analyze();

    // Set the URL of the Web service
    analyzeWebService.Url = Common.Values.WebServiceUrl;

    // Set the credentials
    analyzeWebService.Credentials =
        new NetworkCredential(Common.Values.UserName,
        Common.Values.Password);

    // Get the feed
    DataSet ds = analyzeWebService.GetFeed(feedID);

    return ds;
}

The settings on the Options screen (see Figures 31 and 32) set the URL of the XML Web service. The user enters credentials once on the Login screen (see Figure 10).

A DataSet instance is used for the data transport. But when a DataSet is used with an XML Web service, it is actually the XML representation of the DataSet. Because XML is text based and also includes extensive metadata, it adds up quickly to produce large chunks of information that need to be transferred and later stored.

To optimize communications, a very interesting option is available in most Web servers (like IIS 6.0)—use HTTP (1.1) compression. With this technology, both requests and responses to the server can be compressed by using the gzip or deflate (zip) formats. For more information, see the XML Web Service Compression section in this article.

To store XML data more efficiently, you can also use the same compression (deflate) technology for saving (DataSet) XML in archive files (.zip files). Because the savings are drastic and the loss of performance is negotiable, this is a valid option.

XML Web Service Compression

Although the HTTP 1.1 standard (RFC 2616) allows content from a Web server to be compressed by using a number of algorithms, the client requests the desired compression format by including a HTTP message header, named Accept-Encoding, that contains the compressed formats it supports in a comma-separated list. As mentioned previously, the mostly used formats are gzip (RFC 1952) and deflate (RFC 1951), and according to the HTTP 1.1 standard, gzip is the preferred format. If the server supports compression, the response is compressed according to the formats that the request header.

The compression is implemented on the client by adding the following code (overload) to the file that is generated when a Web reference is added.

protected override System.Net.WebRequest GetWebRequest(Uri uri)
{
    System.Net.WebRequest request = base.GetWebRequest(uri);
    request.Headers.Add("Accept-Encoding", "gzip, deflate");
    return request;
}

The request header asks the Web server to compress the response in a gzip or deflate format. Because gzip occurs first, gzip is the preferred format. If the Web server does not support gzip compression, the Web server will use the deflate format.

When the compressed response is returned to the client, another overload in the same file is needed to decompress the response, which the following code does.

protected override System.Net.WebResponse GetWebResponse(System.Net.WebRequest request)
{
    HttpWebResponseDecompressed response =
        new HttpWebResponseDecompressed(request);
    return response;
}

It uses the utility class HttpWebResponseDecompressed to do the actual decompression, and the following code example shows that class.

internal class HttpWebResponseDecompressed : WebResponse 
{
    private HttpWebResponse response;

    public HttpWebResponseDecompressed(WebRequest request)
    {
        response = (HttpWebResponse)request.GetResponse();
    }
    public override void Close()
    {
        response.Close();
    }
    public override Stream GetResponseStream()
    {
        Stream compressedStream = null;
        if(response.ContentEncoding == "gzip")
        {
            compressedStream =
                new GZipInputStream(response.GetResponseStream());
        }
        else if(response.ContentEncoding == "deflate")
        {
            compressedStream =
                new ZipInputStream(response.GetResponseStream());
        }
        if(compressedStream != null)
        {
            // Decompress
            MemoryStream decompressedStream = new MemoryStream();
            int size = 2048;
            byte[] buffer = new byte[2048];
            while(true)
            {
                size = compressedStream.Read(buffer, 0, size);
                if(size > 0)
                    decompressedStream.Write(buffer, 0, size);
                else
                    break;
            }
            decompressedStream.Seek(0, SeekOrigin.Begin);
            return decompressedStream;
        }
        else
            return response.GetResponseStream();
    }
    public override long ContentLength
    {
        get { return response.ContentLength; }
    }
    public override string ContentType
    {
        get { return response.ContentType; }
    }
    public override System.Net.WebHeaderCollection Headers
    {
        get { return response.Headers; }
    }
    public override System.Uri ResponseUri
    {
        get { return response.ResponseUri; }
    }
}

The excellent (and free) SharpZipLib library was used to handle the decompression, and fellow MVP Alex Yakhnin provided the basis of this class. The original response stream (GetResponseStream) is read through one of the decompression streams (GZipInputStream for gzip and ZipInputStream for deflate) and is written to an uncompressed response stream, which is returned as the new response stream (GetResponseStream). The decompression is transparent to the classes that handle the XML Web service and also to a user of the Web reference proxy.

For more details, see the article Web Service Compression with .NET CF. It includes instructions about how to make it all work by correctly setting up your Web server, development tools, and so on. It also includes some measurement results.

Local Storage Compression

The same compression library, SharpZipLib (see the previous section), was also used to optimize local storage of the feed list and the various feeds.

When a DataSet instance is stored locally, the download code sample uses the following code.

private void saveLocalData(string dataName, DataSet dataSet)
{
    ZipOutputStream fs;
    ZipEntry entry;
    Crc32 crc = new Crc32();

    if(File.Exists(Common.Values.DataFilesZip))
    {
        string oldFileName = Common.Values.ApplicationPath +
            Path.DirectorySeparatorChar + "_old.zip";
        File.Move(Common.Values.DataFilesZip, oldFileName);
        ZipInputStream ts = new ZipInputStream(File.OpenRead(oldFileName));
        fs = new ZipOutputStream(File.Create(Common.Values.DataFilesZip));
        ZipEntry oldEntry = ts.GetNextEntry();
        while(oldEntry != null)
        {
            if(oldEntry.Name != dataName + ".xml")
            {
                // Copy an entry to a new archive
                entry = new ZipEntry(oldEntry.Name);
                entry.DateTime = oldEntry.DateTime;
                entry.Size = oldEntry.Size;
                byte[] buffer = new byte[oldEntry.Size];
                ts.Read(buffer, 0, buffer.Length);
                crc.Reset();
                crc.Update(buffer);
                entry.Crc = crc.Value;
                fs.PutNextEntry(entry);
                fs.Write(buffer, 0, buffer.Length);
            }
            oldEntry = ts.GetNextEntry();
        }
        ts.Close();
        File.Delete(oldFileName);
    }
    else
        fs = new ZipOutputStream(File.Create(Common.Values.DataFilesZip));

    if(true)
    {
        // Get the DataSet (with schema) into buffer
        MemoryStream ms = new MemoryStream();
        XmlTextWriter xtw = new XmlTextWriter(ms, Encoding.UTF8);
        dataSet.WriteXml(xtw, XmlWriteMode.WriteSchema);
        ms.Seek(0, System.IO.SeekOrigin.Begin);
        BinaryReader br = new BinaryReader(ms);
        byte[] buffer = new byte[ms.Length];
        br.Read(buffer, 0, buffer.Length);

        // Create an entry with the date/time, length, and CRC
        entry = new ZipEntry(dataName + ".xml");
        entry.DateTime = DateTime.Now;
        entry.Size = ms.Length;
        crc.Reset();
        crc.Update(buffer);
        entry.Crc = crc.Value;
        
        // Add an entry
        fs.PutNextEntry(entry);
        fs.Write(buffer, 0, buffer.Length);
        fs.Close();
    }
}

If the local data storage file (defined by the Common.Values.DataFilesZip class as data.zip) exists, it is renamed to _old.zip. Then, all of the entries in that file, except the one to add, are copied to a newly created file (data.zip) before the old file is deleted and the new entry is added. The new entry's data is the XML representation of the DataSet (dataSet parameter) including the schema information. The name of the new entry (dataName parameter) is set on the new entry with an .xml extension, and before the entry is added, it is updated with the date and time for creation, the length, and the Cyclic Redundancy Check (CRC) checksum.

The code to retrieve a locally stored DataSet looks like the following code example.

private DataSet getLocalData(string dataName)
{
    ZipInputStream fs =
        new ZipInputStream(File.OpenRead(Common.Values.DataFilesZip));
    ZipEntry entry = fs.GetNextEntry();
    while(entry.Name != dataName + ".xml")
    {
        entry = fs.GetNextEntry();
        if(entry == null)
            throw new Exception("Not available!");
    }
    DataSet ds = new DataSet();
    XmlTextReader xtr = new XmlTextReader(fs);
    ds.ReadXml(xtr, XmlReadMode.ReadSchema);
    xtr.Close();
    fs.Close();
    return ds;
}

The archive file is searched to find the requested entry (specified by the dataName parameter with an .xml extension added), and if found, a DataSet is created and initiated with the entry data.

Connection Awareness

The connection awareness is encapsulated in a separate class (DataHandler) to simplify for the developer who designs the user interface. In the form, when the list of feeds is requested, the following code is used.

DataSet ds;
using(DataHandler dataHandler = new DataHandler())
    ds = dataHandler.GetFeedList();

The implementation of the GetFeedList method is shown in the following code example.

public DataSet GetFeedList()
{
    DataSet ds;

    // Connected?
    if(CheckConnectionHandler.CheckConnectedState())
    {
        // Get the lastest feed list from the Web service, and save it         // locally
        using(RemoteHandler remoteHandler = new RemoteHandler())
            ds = remoteHandler.GetFeedList();
        saveLocalData("feedlist", ds);
    }
    else
    {
        // Get the local feed list
        ds = getLocalData("feedlist");
    }
    ds = updateFeedData(ds);
    return ds;
}

A check finds out if a network connection exists, and if so, the feed list is retrieved from the XML Web service (for details, see the XML Web Service Compression section) and is stored locally in the local archive file. If no connection exists, the feed list is retrieved from the local store. Finally, the feed list DataSet is updated with some information from the local data store, and the code looks like the following.

private DataSet updateFeedData(DataSet dataSet)
{
    // Add columns to the DataSet
    DataColumn dc = dataSet.Tables[0].Columns.Add(
        "LastUpdatedDateTime", typeof(DateTime));
    dc = dataSet.Tables[0].Columns.Add("Size", typeof(long));
    dc = dataSet.Tables[0].Columns.Add("CompressedSize", typeof(long));
    
    string feedID;
    DataRow dr;
    ZipInputStream fs =
        new ZipInputStream(File.OpenRead(Common.Values.DataFilesZip));
    ZipEntry entry = fs.GetNextEntry();
    while(entry != null)
    {
        feedID = Path.GetFileNameWithoutExtension(entry.Name);
        if(feedID != "feedlist")
        {
            DataRow[] drs = dataSet.Tables[0].Select(
                "feedID='" + feedID + "'");
            if(drs.Length > 0)
            {
                dr = drs[0];
                dr["LastUpdatedDateTime"] = entry.DateTime;
                dr["Size"] = entry.Size;
                dr["CompressedSize"] = entry.CompressedSize;
            }
        }
        entry = fs.GetNextEntry();
    }
    dataSet.AcceptChanges();
    return dataSet;
}

Three new columns are added to the dataSet instance with the feed list, and for all entries (except the feed list entry), the DataSet instance is updated with the metadata (date and time; size; and compressed size) from the archive file.

The logic for retrieving a feed is very similar, and it looks like the following code example.

public DataSet GetFeed(string feedID)
{
    DataSet ds;

    // Connected?
    if(CheckConnectionHandler.CheckConnectedState())
    {
        // Get the lastest feed from the Web service, and save it         // locally
        using(RemoteHandler remoteHandler = new RemoteHandler())
            ds = remoteHandler.GetFeed(feedID);
        saveLocalData(feedID, ds);
    }
    else
    {
        // Get the local feed
        ds = getLocalData(feedID);
    }
    return ds;
}

Again, if a network connection exists, the feed is retrieved from the server and is stored locally. If no connection exists, the feed is retrieved from the local store.

The application functions as expected—independent of the connection state. The prerequisites are that the local data is available and that either it can be installed with the application or it can be retrieved immediately following installation.

Orientation Awareness

To make an application aware of the screen's orientation, the application first needs to be notified that a change in orientation has occurred, and then the new size of the screen needs to be retrieved to make adjustments to the user interface.

In a .NET Compact Framework application, each form receives a Resize event as a notification about a change in screen orientation (examples about how you can implement such events follows). The current size of the screen can be retrieved from the Screen class in the "System.Windows.Forms" namespace. You can use its singleton instance PrimaryScreen to get information about the screen, as shown in Figure 41.

Figure 41. Bounds and WorkingArea properties

The Bounds property (of type Rectangle) specifies the whole area of the screen as the two arrows that are marked with 1 in Figure 41 indicate, and the WorkingArea property (also of type Rectangle) specifies the screen area except the title bar as the two arrows that are marked with 2 in Figure 41 indicate.

In landscape mode, the same two properties are defined, as shown in Figure 42.

Figure 42. Bounds and WorkingArea properties in landscape mode

Because the desired return value is the actual area that is reserved for the user interface elements (the white areas in Figure 41 and 42), you also need to remove the size of the menu bar.

To simplify for the user interface developer, you can create a common property (on the Common class) that returns the current size of the screen, as shown in the following code example.

private const int MENUBAR_HEIGHT = 26;
public static Size Screen
{
  get
  {
    Size s = new
      Size(System.Windows.Forms.Screen.PrimaryScreen.WorkingArea.Width,
      System.Windows.Forms.Screen.PrimaryScreen.WorkingArea.Height –
      MENUBAR_HEIGHT);
    return s;
  }
}

With this code in place, you can write the code to rearrange the forms—beginning with the resize event for the main form, as shown in the following code example.

private void MainForm_Resize(object sender, System.EventArgs e)
{
    if(headingPanel.Width != Common.Screen.Width)
    {
        headingPanel.Width = Common.Screen.Width;
        nameTextBox.Width = Common.Screen.Width - nameTextBox.Left –
            findButton.Width - 16;
        findButton.Left = Common.Screen.Width - findButton.Width - 8;
        itemsPanel.Width = Common.Screen.Width;
        itemsPanel.Height = Common.Screen.Height - itemsPanel.Top;
        itemsListView.Width = Common.Screen.Width + 2;
        itemsListView.Height = Common.Screen.Height - itemsPanel.Top + 1;
        dateColumnHeader.Width =
            Common.Screen.Width - nameColumnHeader.Width - 17;
    }
}

A check finds out if the form controls need to be adjusted. This check is a good idea because the form sometimes gets several resize events without there being a change in the screen's orientation. The thin line below the form's headingPanel is used for that test. The button is moved to the right of the form, but a margin that is 8 pixels on the right side remains. The text box is resized to the maximum width excluding the button and margins (2 x 8 pixels) on both sides of the button. The list view is resized to fit the new width and height of the form. Note that a panel wraps the list view to conform to the user interface design guidelines and to hide the left and right borders of the list view. Finally, the second column header of the list view is resized to the maximum size possible excluding the width of the first column header and the width of the list view's vertical scroll bar (17 pixels).

In the About form, you can see a more advanced approach, as the following code example shows.

private void AboutForm_Resize(object sender, System.EventArgs e)
{
    if(headingPanel.Width != Common.Screen.Width)
    {
        headingPanel.Width = Common.Screen.Width;
        int offset = 0;
        if(Common.Screen.Height > Common.Screen.Width)
        {
            aboutPictureBox.Width = Common.Screen.Width - 16;
            aboutPictureBox.Left = 8;
            offset = 96;
        }
        else
        {
            aboutPictureBox.Width = 64;
            aboutPictureBox.Left =
                Common.Screen.Width - aboutPictureBox.Width - 8;
        }
        productNameLabel.Top = offset + 32;
        copyrightLabel.Top = offset + 48;
        commentLabel.Top = offset + 80;
        visitUsLabel.Top = offset + 152;
        urlPanel.Top = offset + 152;
    }
}

After the initial check to find out if an update is necessary, comparing the width and the height of the screen determines the current screen orientation. If the screen is in portrait mode, the picture is placed at the top with a fixed margin (8 pixels) on each side, and the labels are placed below the picture. In landscape mode, the labels are placed at the upper-left corner, and the picture is resized and moved to the upper-right corner. For more details about the resize events in the other forms, see this article's Northwind_Traders_Decision_Support_PPC.msi.

To find out if you can rotate a screen, see David Kline's blog post Checking for Screen Rotation Support Using NetCF and Checking for Screen Rotation Support in NetCF version 2. To rotate the screen from code, see the first listing in the article Put Your Apps on the Landscape with Windows Mobile 2003 Second Edition.

The .NET Compact Framework version 2.0 has better support for making forms screen-orientation aware.

Pivot Table

Implementing a pivot table in the Analyze form (AnalyzeForm) makes use of a standard grid (DataGrid) control. First, when creating a pivot table, the selected dimensions are captured from the drop-down lists in the View screen (see Figures 13 and 14). The following code example shows these dimensions being captured.

bool average = false;
if(formulaComboBox.SelectedIndex > 0)
    average = true;
string columnName = columnComboBox.Text;
string rowName = rowComboBox.Text;
string dataName = dataComboBox.Text;
ArrayList columns = new ArrayList();
ArrayList rows = new ArrayList();
string s;
foreach(DataRow dar in dataSet.Tables[0].Rows)
{
    s = dar[columnName].ToString();
    if(!columns.Contains(s))
        columns.Add(s);
    s = dar[rowName].ToString();
    if(!rows.Contains(s))
        rows.Add(s);
}
columns.Sort();
rows.Sort();

The selected formula is determined (where Sum is default), and the names for the various dimensions are saved. Then, each of the possible values for each of the column and row dimensions are retrieved from the feed DataSet (dataSet), and they are also sorted.

Because a grid control does not enable any data to be added to the control itself, there must always be a data source (a DataTable) available to hold the information to display. The following code is how you can create such a data source.

DataTable dt = new DataTable();
dt.Columns.Add("_RowColumn");
dataGrid.TableStyles[0].GridColumnStyles.Clear();
DataGridTextBoxColumn tc = new DataGridTextBoxColumn();
tc.MappingName = "_RowColumn";
dataGrid.TableStyles[0].GridColumnStyles.Add(tc);
for(int i = 0; i < columns.Count; i++)
{
    s = columns[i].ToString();
    dt.Columns.Add(s);
    tc = new DataGridTextBoxColumn();
    tc.MappingName = s;
    tc.HeaderText = s;
    dataGrid.TableStyles[0].GridColumnStyles.Add(tc);
}
dt.Columns.Add("_RowTotal");
tc = new DataGridTextBoxColumn();
tc.MappingName = "_RowTotal";
tc.HeaderText = "Total";
dataGrid.TableStyles[0].GridColumnStyles.Add(tc);

The grid control's column definitions are also added while the data source is created. The first column (_RowColumn) in the data table (dt) holds the row values. Note how the data table's column name and the column styles mapping name in the MappingName property must match. All of the column values (columns) are added as columns in the data table and the grid, and finally a totals column (_RowTotal) is added.

The following code example calculates the values in the pivot table.

// Insert rows and columns with the values and totals
DataRow[] drs;
double colValue;
double rowTotal;
double[] colTotals = new double[columns.Count];
DataRow dr;
for(int i = 0; i < rows.Count; i++)
{
    dr = dt.NewRow();
    dr["_RowColumn"] = rows[i].ToString();
    rowTotal = 0;
    for(int j = 0; j < columns.Count; j++)
    {
        drs = dataSet.Tables[0].Select(rowName + "='" +
            rows[i].ToString().Replace("'", "''") + "' AND " +
            columnName + "='" + columns[j].ToString().Replace(
            "'", "''") + "'");
        colValue = 0;
        foreach(DataRow r in drs)
            colValue += Convert.ToDouble(r[dataName]);
        if(average)
            colValue /= drs.Length;
        dr[j + 1] = string.Format("{0:0}", colValue);
        rowTotal += colValue;
        colTotals[j] += colValue;
    }
    // Row total
    if(average)
        rowTotal /= columns.Count;
    dr["_RowTotal"] = string.Format("{0:0}", rowTotal);
    dt.Rows.Add(dr);
}
// Column totals and the grand total
dr = dt.NewRow();
dr["_RowColumn"] = "Total";
double grandTotal = 0;
for(int i = 0; i < columns.Count; i++)
{
    if(average)
        colTotals[i] /= rows.Count;
    dr[i + 1] = string.Format("{0:0}", colTotals[i]);
    grandTotal += colTotals[i];
}
if(average)
    grandTotal /= columns.Count;
dr["_RowTotal"] = string.Format("{0:0}", grandTotal);
dt.Rows.Add(dr);

dt.AcceptChanges();
dataGrid.DataSource = dt;

For each row, the row values (rows) are added to the first column in the data table (dt), and for all values (dataName) that match the row value and the column value (columns), the sum or average is calculated. In this process, the row and column totals are also captured. The row total is added to the last column and when all of the rows are added to the data table, a final row is added with the column totals and the table's grand total. Finally, the changes are saved to the data table, and it is set as the data source of the grid.

Line and Bar Chart

There are several commercial products available that provide very advanced charting capabilities. You should browse the market when you consider charting in an enterprise application. Some commercial products allow you to control the source code. While you might be waiting for someone to port the excellent open source charting library ZedGraph to the .NET Compact Framework, you can find some basic charting functionality in this sample code. All of the charting is done by using managed code and the GDI+ functionality that the .NET Compact Framework offers.

In the chart form (ChartForm), a chart instance is created as the following code example shows.

chart = new Chart(pictureBox.Width, pictureBox.Height);

The size of the picture box (pictureBox) that receives the chart is used to set the size of the bitmap that the chart generates. The following code example initializes the chart.

chart.Clear();
if(!flipMenuItem.Checked)
{
    for(int i = 0; i < dataSource.Rows.Count - 1; i++)
    {
        double[] series = new double[dataSource.Columns.Count - 2];
        for(int j = 1; j < dataSource.Columns.Count - 1; j++)
            series[j - 1] = Convert.ToDouble(dataSource.Rows[i][j]);
        chart.AddValues(series, dataSource.Rows[i][0].ToString());
    }
}
else
{
    for(int i = 1; i < dataSource.Columns.Count - 1; i++)
    {
        double[] series = new double[dataSource.Rows.Count - 1];
        for(int j = 0; j < dataSource.Rows.Count - 1; j++)
            series[j] = Convert.ToDouble(dataSource.Rows[j][i]);
        chart.AddValues(series, dataSource.Columns[i].ColumnName);
    }
}
updateChart();

After any previous data in the chart is removed, a check finds out if the series (rows) and categories (columns) should be flipped, and the data in the data source (this is the data table that holds the data for the pivot table grid as the previous section describes) is added as a series to the chart. This article looks at the method to add the series to the chart (AddValues) in a moment. First, look at the method in the following code example that updates the chart and legend picture box controls.

private void updateChart()
{
    chart.ShowTrend = trendMenuItem.Checked;
    chart.BarChart = (barMenuItem.Text != "Bar");
    pictureBox.Image = chart.Paint();
    Bitmap b = chart.Legend();
    legendPictureBox.Size = b.Size;
    legendPictureBox.Top = 28;
    legendPictureBox.Left = Common.Screen.Width –
        legendPictureBox.Width - 4;
    legendPictureBox.Image = b;
}

The attributes of the chart are set depending on the current state of the various menu options. The properties control if a trend should be calculated (ShowTrend) and if the chart generated should be a bar chart (BarChart). The chart bitmap is generated (Paint) and inserted into the picture box. The legend is also inserted into the legend picture box after it is resized to fit the generated legend bitmap.

Next, this article describes the Chart class. The following code example shows the private class declarations.

private Bitmap bitmap;
private Graphics graphics;
private ArrayList values;
private ArrayList colors;
private ArrayList seriesLabels;

The constructor looks like the following code example.

public Chart(int width, int height)
{
    this.width = width;
    this.height = height;
    bitmap = new Bitmap(width, height);
    graphics = Graphics.FromImage(bitmap);
    Clear();
}

A bitmap and a graphics (GDI+) context are initialized with the provided size (the width and height parameters) before the chart data is initialized, as shown in the following code example.

public void Clear()
{
    values = new ArrayList();
    colors = new ArrayList();
    seriesLabels = new ArrayList();
}

The arrays to hold the series data (values), each series color (colors), and each series label (seriesLabels) are created and initialized.

The following code example adds a series to the chart.

public void AddValues(double[] series, string label)
{
    values.Add(series);
    if(label != null)
        seriesLabels.Add(label);
    else
        seriesLabels.Add("Series " + values.Count.ToString());
    if(barChart)
        colors.Add(anyLightColor());
    else
        colors.Add(anyColor());
}

The series data parameter is added to the values array, and if a label is provided, it is added to the labels array seriesLabels. Depending on the chart's type, a color is generated and saved for the series. The following code example uses two methods for randomly generating colors.

private Random random = new Random();
private Color anyColor()
{
   return Color.FromArgb(random.Next(256),
        random.Next(256), random.Next(256));
}
private Color anyLightColor()
{
    return Color.FromArgb(random.Next(8) * 16 + 128,
        random.Next(8) * 16 + 128, random.Next(8) * 16 + 128);
}

The anyColor method generates any color that is in the full range of colors, and the anyLightColor method only allows values for the red, green, and blue parts of a color to be a multiple of 16 starting at 128 and larger.

The Paint method is at the heart of the Chart class; this method draws the chart bitmap, and it begins with the following code.

int xMin = -1;
int xMax = ((double[])values[0]).Length;
int xStep = 1;
int yMin = 0;
int yMax = 0;
if(barChart)
{
    double[] totSeries = new double[xMax];
    foreach(double[] v in values)
        for(int i = 0; i < v.Length; i++)
            totSeries[i] += v[i];
    for(int i = 0; i < totSeries.Length; i++)
        yMax = (int)Math.Max(Math.Ceiling(totSeries[i]), yMax);
}
else
{
    foreach(double[] v in values)
        for(int i = 0; i < v.Length; i++)
            yMax = (int)Math.Max(Math.Ceiling(v[i]), yMax);
}
int log10 = (int)Math.Floor(Math.Log10(yMax));
int yLog = 1;
if(log10 > 2)
    yLog = (int)Math.Pow(10, log10 - 2);
double log = Math.Pow(10, log10);
if((int)(Math.Ceiling(yMax / log) * log / yLog) == 1000)
    yLog *= 10;
yMax = (int)(Math.Ceiling(yMax / log) * log / yLog);
int yStep = (int)(Math.Pow(10, Math.Floor(Math.Log10((yMax - yMin) / 10)) + 1) / 2);
if(yStep < 1)
    yStep = 1;
if(((yMax - yMin) / yStep) > 10)
    yStep *= 2;
int xTransform = -xMin;
int yTransform = -yMin;
Rectangle chartRectangle = new Rectangle(24, 8, width - 33, height - 16);
float xFactor = ((float)chartRectangle.Width) / (xMax - xMin);
float yFactor = ((float)chartRectangle.Height) / (yMax - yMin);

The minimum and maximum values for both axes are calculated along with the y-axis's divisor (yLog) and tick mark size (yStep). Also, the movement of all values to the positive ranges of the x and y axes (xTranform and yTransform) and the pixels/unit conversion factors (xFactor and yFactor) are calculated. With these values set, the application can now draw the chart. The application begins by clearing the background by using the following code example.

graphics.Clear(backColor);

The background color (backColor) is provided as a property, as the following code example shows.

private Color backColor = SystemColors.Window;
public Color BackColor
{
    get { return backColor; }
    set { backColor = value; }
}

There are a number of other similar properties for foreground color (foreColor): the color that is used to draw the grid lines (gridColor) and the font that is used to draw text in the legend (font).

The following code example shows how the grid lines are drawn.

SolidBrush foreBrush = new SolidBrush(foreColor);
for(int i = yMin; i <= yMax; i += yStep)
{
    if(showGridY)
    {
        graphics.DrawLine(new Pen(gridColor), chartRectangle.X - 3,
            Convert.ToInt32(chartRectangle.Y + chartRectangle.Height –
            ((i - yStep / 2) + yTransform) * yFactor),
            chartRectangle.X + chartRectangle.Width,
            Convert.ToInt32(chartRectangle.Y + chartRectangle.Height –
            ((i - yStep / 2) + yTransform) * yFactor));
        graphics.DrawLine(new Pen(gridColor), chartRectangle.X - 3,
            Convert.ToInt32(chartRectangle.Y + chartRectangle.Height –
            (i + yTransform) * yFactor), chartRectangle.X +
            chartRectangle.Width, Convert.ToInt32(chartRectangle.Y +
            chartRectangle.Height - (i + yTransform) * yFactor));
    }
    string s = Math.Abs(i).ToString();
    graphics.DrawString(s, font, foreBrush, 21 –
        graphics.MeasureString(s, font).Width,
        chartRectangle.Y + chartRectangle.Height - 7 –
        (i + yTransform) * yFactor);
}
if(showGridX)
{
    for(int i = xMin; i <= xMax; i += xStep)
    {
        graphics.DrawLine(new Pen(gridColor),
            Convert.ToInt32(chartRectangle.X + (i + xTransform) * xFactor),
            chartRectangle.Y, Convert.ToInt32(chartRectangle.X +
            (i + xTransform) * xFactor),
            chartRectangle.Y + chartRectangle.Height + 3);
    }
}

If the grid lines for the y-axis should be shown, there are two lines drawn: one for the tick mark distance and one for half of the tick mark distance. Each tick mark gets a text label.

The bars in a bar chart are drawn as shown in the following code example.

double[] totSeries = new double[xMax];
for(int i = 0; i < values.Count; i++)
{
    Pen framePen = new Pen(foreColor);
    SolidBrush seriesBrush = new SolidBrush((Color)colors[i]);
    double[] series = (double[])values[i];
    for(int x = 0; x < series.Length; x++)
    {
        totSeries[x] += series[x];
        double y = totSeries[x];
        graphics.FillRectangle(seriesBrush,
            Convert.ToInt32(chartRectangle.X +
            (x + xTransform) * xFactor - xFactor / 3),
            Convert.ToInt32(chartRectangle.Y +
            chartRectangle.Height - (y / yLog + yTransform) * yFactor), 
            Convert.ToInt32(xFactor/1.5),
            Convert.ToInt32((series[x] / yLog + yTransform) * yFactor) +
           (i > 0 ? 1 : 0));
        graphics.DrawRectangle(framePen,
            Convert.ToInt32(chartRectangle.X +
            (x + xTransform) * xFactor - xFactor / 3),
            Convert.ToInt32(chartRectangle.Y +
            chartRectangle.Height - (y / yLog + yTransform) * yFactor),
            Convert.ToInt32(xFactor/1.5),
            Convert.ToInt32((series[x] / yLog + yTransform) * yFactor) +
            (i > 0 ? 1 : 0));
    }
}

The bars are calculated as the sum of the series for each x value (stacked bars). The bars are drawn by first filling the bar with the assigned color and then drawing a frame in the foreground color (foreColor), which is normally black.

The following code example shows how the lines in a line chart are drawn.

for(int i = 0; i < values.Count; i++)
{
    Pen seriesPen = new Pen((Color)colors[i]);
    double[] series = (double[])values[i];
    for(int x = 0; x < series.Length - 1; x++)
    {
        double y1 = series[x];
        double y2 = series[x + 1];
        graphics.DrawLine(seriesPen,
            Convert.ToInt32(chartRectangle.X + (x + xTransform) * xFactor),
            Convert.ToInt32(chartRectangle.Y + chartRectangle.Height –
            (y1 / yLog + yTransform) * yFactor),
            Convert.ToInt32(chartRectangle.X +
            ((x + 1) + xTransform) * xFactor),
            Convert.ToInt32(chartRectangle.Y + chartRectangle.Height –
            (y2 / yLog + yTransform) * yFactor));
        graphics.DrawEllipse(seriesPen,
            Convert.ToInt32(chartRectangle.X +
            (x + xTransform) * xFactor - 2),
            Convert.ToInt32(chartRectangle.Y + chartRectangle.Height –
            (y1 / yLog + yTransform) * yFactor - 2), 4, 4);
    }
    graphics.DrawEllipse(seriesPen,
        Convert.ToInt32(chartRectangle.X +
        (series.Length - 1 + xTransform) * xFactor - 2),
        Convert.ToInt32(chartRectangle.Y + chartRectangle.Height –
        (series[series.Length - 1] / yLog + yTransform) * yFactor - 2),
        4, 4);
}

Each part of the line is drawn from the current value to the next, and a small circle is also drawn for each point.

The following code example shows how the axes are drawn.

Pen forePen = new Pen(foreColor);
graphics.DrawLine(forePen, chartRectangle.X, chartRectangle.Y +
    chartRectangle.Height, chartRectangle.X + chartRectangle.Width,
    chartRectangle.Y + chartRectangle.Height);
graphics.DrawLine(forePen, chartRectangle.X, chartRectangle.Y +
    chartRectangle.Height, chartRectangle.X, chartRectangle.Y);

These axes are drawn last, so the grid or any lines or bars are drawn over them. Now the Paint method is finished off with the return of the generated bitmap, as shown in the following code example.

return bitmap;

The bitmap of the legend is created with the following code example.

public Bitmap Legend()
{
    int height = 12 * values.Count + 1;
    if(height > bitmap.Height - 10)
        height = bitmap.Height - 10;
    float yStep = Math.Min(height / values.Count, 12);
    int width = 0;
    for(int i = 0; i < seriesLabels.Count; i++)
        width = (int)Math.Max((int)graphics.MeasureString(
            seriesLabels[i].ToString(), font).Width, width);
    width += 15;
    Bitmap b = new Bitmap(width + 10, height + 10);
    Graphics g = Graphics.FromImage(b);
    g.Clear(backColor);
    Pen framePen = new Pen(foreColor);
    g.DrawRectangle(framePen, 0, 0, width + 9, height + 9);
    SolidBrush foreBrush = new SolidBrush(foreColor);
    for(int i = 0; i < values.Count; i++)
    {
        SolidBrush seriesBrush = new SolidBrush((Color)colors[i]);
        g.FillRectangle(seriesBrush, 5, (int)(i * yStep) + 5, 10, 10);
        g.DrawRectangle(framePen, 5, (int)(i * yStep) + 5, 10, 10);
        g.DrawString(seriesLabels[i].ToString(), font, foreBrush,
            15 + 5, i * yStep - 1 + 5);
    }
    g.Dispose();
    return b;
}

Care is taken when deciding the size of the legend. First, the legend is assumed to allow space for all of the series' colored squares and label texts. If this size is larger than the size of the chart (minus the margin), the height is derived from the allowed maximum size. Then, the height of each item is calculated. The width is calculated as the size of the longest label when drawn as text. With these values calculated, the legend squares and labels are drawn. Then, the drawn bitmap is returned.

If a trend should be shown in the chart, the series values (in the values array) are used to calculate the trend values for each series like the following code example shows.

ArrayList trendValues = null;
trendValues = new ArrayList();
for(int i = 0; i < values.Count; i++)
    trendValues.Add(trend((double[])values[i]));

When the lines are drawn, these values are used instead.

Finally, the function that uses linear regression and the least square method to calculate the trend for a series is shown in the following code example.

private double[] trend(double[] values)
{
    double sumX = 0, sumY = 0, sumXSq = 0, sumYSq = 0, sumCod = 0;
    int count = values.Length;

    for(int x = 0; x < count; x++) 
    {
        double y = values[x];
        sumCod += x * y;
        sumX += x;
        sumY += y;
        sumXSq += x * x;
        sumYSq += y * y;
    }
    double slope = (sumCod - sumX * sumY / count) /
        (sumXSq - sumX * sumX / count);
    double intercept = (sumY / count) - slope * (sumX / count);

    double[] trendValues = new double[count];
    for(int x = 0; x < count; x++)
        trendValues[x] = x * slope + intercept;

    return trendValues;
}

This code creates a linear curve that is derived from the provided series values. For more details about the theory behind this method, search for mathematical resources using keywords like linear regression.

The Chart class is just the beginning of what developers could ask for in a chart control, but it shows that, thanks to GDI+ and the support for it in the .NET Compact Framework, basic charting capabilities can be added without enormous efforts.

Screen Capture

With thanks to fellow MVP Alex Feinman, some of his code is included in the sample to capture the image of a control (in this case, the picture box that holds the chart) and save it to a file.

The code for the menu option (Save) looks like the following.

string fileName;
int i = 0;
while(true)
{
    fileName = @"\My Documents\chart" + i.ToString() + ".bmp";
    if(!File.Exists(fileName))
        break;
    i++;
}
BitmapHelper.SaveControlToFile(pictureBox, fileName);
MessageBox.Show("Chart saved to file" + " '" + fileName + "'!",
    this.Text);

When a unique file name is found, the bitmap helper (BitmapHelper) method (SaveControlToFile) is called with the picture box control and the unique file name as parameters. That method is implemented like the following code example shows.

public static void SaveControlToFile(Control control, string fileName)
{
    byte[] bitmapData = GetControlBitmapArray(control);
    FileStream fs = new FileStream(fileName, FileMode.Create);
    fs.Write(bitmapData, 0, bitmapData.Length);
    fs.Flush();
    fs.Close();
}

The file is saved using the byte array of the bitmap, which is created like the following code example shows.

private const int PelsPerMeter = 0xb12; // 72 dpi, 96 (0xec4) also // possible
private static byte[] GetControlBitmapArray(Control control)
{
    control.Capture = true;
    IntPtr hWnd = GetCapture();
    control.Capture = false;
    IntPtr hDC = GetDC(hWnd);
    IntPtr hMemoryDC = CreateCompatibleDC(hDC);

    BITMAPINFOHEADER bih = new BITMAPINFOHEADER();
    bih.biSize = Marshal.SizeOf(bih);
    bih.biBitCount = 24;
    bih.biClrUsed = 0;
    bih.biClrImportant = 0;
    bih.biCompression = 0;
    bih.biHeight = control.Height;
    bih.biWidth = control.Width;
    bih.biPlanes = 1;
    int cb = (int)(bih.biHeight * bih.biWidth * bih.biBitCount / 8);
    bih.biSizeImage = cb;
    bih.biXPelsPerMeter = PelsPerMeter; 
    bih.biYPelsPerMeter = PelsPerMeter;

    IntPtr pBits = IntPtr.Zero;
    IntPtr pBIH = LocalAlloc(GPTR, bih.biSize);
    Marshal.StructureToPtr(bih, pBIH, false);
    IntPtr hBitmap = CreateDIBSection(hDC, pBIH, 0, ref pBits, IntPtr.Zero, 0);

    BITMAPINFOHEADER bihMem =
        (BITMAPINFOHEADER)Marshal.PtrToStructure(pBIH,
        typeof(BITMAPINFOHEADER));
    IntPtr hPreviousBitmap = SelectObject(hMemoryDC, hBitmap);
    BitBlt(hMemoryDC, 0, 0, bih.biWidth, bih.biHeight, hDC, 0, 0,
        SRCCOPY);
    byte[] bits = new byte[cb];
    Marshal.Copy(pBits, bits, 0, cb);

    BITMAPFILEHEADER bfh = new BITMAPFILEHEADER();
    bfh.bfSize = (uint)cb + 0x36;
    bfh.bfType = 0x4d42;
    bfh.bfOffBits = 0x36;
    int headerSize = 14;
    byte[] header = new byte[headerSize];
    BitConverter.GetBytes(bfh.bfType).CopyTo(header, 0);
    BitConverter.GetBytes(bfh.bfSize).CopyTo(header, 2);
    BitConverter.GetBytes(bfh.bfOffBits).CopyTo(header, 10);
    byte[] data = new byte[cb + bfh.bfOffBits];
    header.CopyTo(data, 0);
    header = new byte[Marshal.SizeOf(bih)];
    IntPtr pHeader = LocalAlloc(GPTR, Marshal.SizeOf(bih));
    Marshal.StructureToPtr(bihMem, pHeader, false);
    Marshal.Copy(pHeader, header, 0, Marshal.SizeOf(bih));
    LocalFree(pHeader);
    header.CopyTo(data, headerSize);
    bits.CopyTo(data, (int)bfh.bfOffBits);

    DeleteObject(SelectObject(hMemoryDC, hPreviousBitmap));
    DeleteDC(hMemoryDC);
    ReleaseDC(hDC);

    return data;
}

After the handle to the control's device context, hDC, has been retrieved, a compatible bitmap (meaning, a same-size bitmap) is created in memory, and the screen bits are copied to that new bitmap. Then, a bitmap file header is created before the complete array (header and bitmap bits) is returned.

The helper class also includes code to capture and save the complete screen to a file. To use that code, you can add the following code to a form.

BitmapHelper.SaveScreenToFile(@"\My Documents\screenshot.bmp");

This code example saves the current screen in a file, and the application can make its own screenshots by assigning this code to a hardware key. This technique can be very useful when writing user documentation for support (the user can create a screenshot of a strange looking screen) or for creating figures for articles.

Because the files are stored in raw bitmap format, they end up quite large on discs, and you might consider investing in some component or library to compress the captured images to a more effective format (such as .jpeg or .gif). One example is the JpegTest sample by Sergey Bogdanov.

In the .NET Compact Framework 2.0, there is support for saving a bitmap (Image.Save) in many formats (such as .gif, .jpeg, .tiff, and .png).

Hardware Key Navigation

The first form that implements keyboard navigation is the Analyze form (AnalyzeForm); the application uses keyboard navigation to help the user navigate the grid. To enable navigation of the grid, you first need to find the grid's scroll bars, which you can do by declaring two private form class variables for the scroll bar instances, as shown in the following code example.

private HScrollBar dataGridHScrollBar;
private VScrollBar dataGridVScrollBar;

When the variables are defined, the following code example in the form's Load event gets the instances of the scroll bars.

dataGridHScrollBar = (HScrollBar)dataGrid.GetType().GetField(
    "m_sbHorz", BindingFlags.NonPublic | BindingFlags.GetField |
    BindingFlags.Instance).GetValue(dataGrid);
dataGridVScrollBar = (VScrollBar)dataGrid.GetType().GetField(
    "m_sbVert", BindingFlags.NonPublic | BindingFlags.GetField |
    BindingFlags.Instance).GetValue(dataGrid);

This code example uses reflection to get the hidden members (m_sbHorz and m_sbVert) of the data grid control. If the internal implementation of the data grid changes, this code breaks. With the scroll bars in place, the following code example handles the keyboard navigation in the form's KeyDown event.

private bool dataGridScrollBarLargeChange = false; 
private void AnalyzeForm_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e)
{
    int hStep = dataGridHScrollBar.SmallChange;
    int vStep = dataGridVScrollBar.SmallChange;
    if(dataGridScrollBarLargeChange)
    {
        hStep = dataGridHScrollBar.LargeChange;
        vStep = dataGridVScrollBar.LargeChange;
    }

    switch(e.KeyCode)
    {
        case Keys.Left:
            if(dataGridHScrollBar.Value > dataGridHScrollBar.Minimum)
                dataGridHScrollBar.Value -= hStep;
            break;

        case Keys.Right:
            if(dataGridHScrollBar.Value < dataGridHScrollBar.Maximum)
                dataGridHScrollBar.Value += hStep;
            break;

        case Keys.Up:
            if(dataGridVScrollBar.Value > dataGridVScrollBar.Minimum)
                dataGridVScrollBar.Value -= vStep;
            break;

        case Keys.Down:
            if(dataGridVScrollBar.Value < dataGridVScrollBar.Maximum)
                dataGridVScrollBar.Value += vStep;
            break;

        case Keys.Enter:
            dataGridScrollBarLargeChange = !dataGridScrollBarLargeChange;
            break;
    }
}

The private class variable (dataGridScrollBarLargeChange) is used to indicate whether the movement should be small (the default size) or large. Small movements correspond to tapping the arrows at the end of the scroll bars, and large movements correspond to tapping the scroll bar above or below the scroll bar's scroll box. The Action key corresponds to the user pressing the Enter key (Keys.Enter) on a normal keyboard; the application uses the Action key to allow the user to toggle between small and large movements. Depending which direction keys the user presses (left, right, up, or down), the user navigates the grid accordingly.

The second form that implements keyboard navigation is the Chart form (ChartForm); keyboard navigation moves the legend. The code to handle the navigation looks like the following code example (in the form's KeyDown event).

private bool legendMoveSmallStep = false; 
private void ChartForm_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e)
{
    if(!legendPictureBox.Visible)
        return;

    int step = 10;
    if(legendMoveSmallStep)
        step = 1;

    switch(e.KeyCode)
    {
        case Keys.Left:
            if(legendPictureBox.Left > 4)
                legendPictureBox.Left -= (int)Math.Min(step,
                    legendPictureBox.Left - 4);
            break;

        case Keys.Right:
            if(legendPictureBox.Left < Common.Screen.Width –
                    legendPictureBox.Width - 4)
                legendPictureBox.Left += (int)Math.Min(step,
                    Common.Screen.Width - legendPictureBox.Width - 4 –
                    legendPictureBox.Left);
            break;

        case Keys.Up:
            if(legendPictureBox.Top > 28)
                legendPictureBox.Top -= (int)Math.Min(step,
                  legendPictureBox.Top - 28);
            break;

        case Keys.Down:
            if(legendPictureBox.Top < Common.Screen.Height –
                legendPictureBox.Height - 4)
                legendPictureBox.Top += (int)Math.Min(step,
                    Common.Screen.Height - legendPictureBox.Height - 4 –
                    legendPictureBox.Top);
            break;

        case Keys.Enter:
            legendMoveSmallStep = !legendMoveSmallStep;
            break;
    }
}

The private class variable (legendMoveSmallStep) indicates whether the movement should be small or large (which is the default size). Small movements are one pixel at a time, and large movements are ten pixels at a time. The user presses the Action key to toggle between small and large movements. Depending on which of the direction keys the user pressed (left, right, up, or down), the user navigates the legend accordingly. Note that the boundaries of where the user can place the legend are within an area that is the size of the chart picture box minus a four pixel margin on each side.

Stylus Navigation

To enable the user to drag the legend freely over the chart area by using the stylus, three events on the form (MouseDown, MouseMove, and MouseUp) need to be defined. Because these events are not available in the form designer, you should add the following code to the form's constructor event.

legendPictureBox.MouseDown +=
    new MouseEventHandler(legendPictureBox_MouseDown);
legendPictureBox.MouseMove +=
    new MouseEventHandler(legendPictureBox_MouseMove);
legendPictureBox.MouseUp +=
    new MouseEventHandler(legendPictureBox_MouseUp);

Also, you need to declare a number of private form class variables, as shown in the following code example.

private Point mousePos;
private Point controlPos;
private bool mouseDownOnLegend = false;

The first event (MouseDown) occurs when the user places the stylus anywhere on the legend picture box. Its code looks like the following.

private void legendPictureBox_MouseDown(
    object sender, MouseEventArgs e)
{
    mouseDownOnLegend = true;
    controlPos = new Point(legendPictureBox.Left, legendPictureBox.Top);
    mousePos = new Point(legendPictureBox.Left + e.X,
    legendPictureBox.Top + e.Y);
}

After the flag variable that indicates that the application is in a navigational state (mouseDownOnLegend) is set, the current position of both the legend and where the user placed the stylus are saved.

When the user moves the stylus, the second event (MouseMove) is raised for each movement, and the implementation looks like the following code example.

private void legendPictureBox_MouseMove(
    object sender, MouseEventArgs e)
{
    if(!mouseDownOnLegend)
        return;

    Point mouseP = new Point(
        legendPictureBox.Left + e.X, legendPictureBox.Top + e.Y);

    legendPictureBox.Left =
        (int)Math.Min(Math.Max(controlPos.X + mouseP.X - mousePos.X, 4),
        Common.Screen.Width - legendPictureBox.Width - 4);
    legendPictureBox.Top  =
        (int)Math.Min(Math.Max(controlPos.Y + mouseP.Y - mousePos.Y, 28),
        Common.Screen.Height - legendPictureBox.Height - 4);
}

This method exits if the application is not in navigational state and saves the new position of the legend. Then, the legend (picture box) is reallocated according to the new position. Note that the same boundaries apply as mentioned for the direction keys (see the Hardware Key Navigation section).

The MouseUp event is raised as soon as the user lifts the stylus off the device screen. The code for the MouseUp event looks like the following.

private void legendPictureBox_MouseUp(object sender, MouseEventArgs e)
{
    mouseDownOnLegend = false;
}

This code simply ends the navigational state.

The previously discussed approach is a fast and efficient way of visually manipulating objects, and, with further enhancements, you can enhance this approach to allow more advanced visual manipulations, like drag and drop capabilities. Although there may be exceptions to any rule, the acts of dragging and dropping are not aligned with Pocket PC visual design guidelines.

GlobalHandler

Any mobile enterprise application that is intended to display 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 only requires a single line of code in each form, which the following code example shows.

GlobalHandler.Translate.Form(this);

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

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

For more details about the management and code related to globalization, 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, to 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 code.

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

The push implicitly loads the form (if it is not already loaded). 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, see the article Northwind Pocket Sales: Field Sales for Windows Mobile-based Smartphones.

Help

Any professional enterprise application should provide online help. On Pocket PC, the standard way to support help is by means of the Start menu's Help command. This command should be context sensitive to align with design guidelines. This article's sample application shows how to implement context-sensitive help according to the standard, and it only requires a single line of code in each form.

Common.Values.HelpTopic = "OptionsForm";

For more details about the management and code related to providing online help, see the article Northwind Pocket Inventory: Logistics for Windows Mobile-based Pocket PCs.

Conclusion

Data analysis is the key to making the correct decision. With a corporate information infrastructure in place, the facts are in place. By implementing basic pivot tables that use grid controls and use GDI+ to do basic charting, you can enable basic analysis on a mobile device. The streamlining of remote communication, like XML Web services calls and the optimization of local storage using standard compression technology, enables the extension of data analysis to mobile devices that increases the probability of correct decision making—anywhere!

© 2009 Microsoft Corporation. All rights reserved. Terms of Use | Trademarks | Privacy Statement
Page view tracker