.NET Compact Framework 3.5 Data-Driven Applications Build robust and feature-rich mobile data-driven applications with the help of real-world examples
Edmund Tan
BIRMINGHAM - MUMBAI
.NET Compact Framework 3.5 Data-Driven Applications Copyright © 2010 Packt Publishing
All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews. Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, Packt Publishing, nor its dealers or distributors will be held liable for any damages caused or alleged to be caused directly or indirectly by this book. Packt Publishing has endeavored to provide trademark information about all the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information.
First published: April 2010
Production Reference: 1220410
Published by Packt Publishing Ltd. 32 Lincoln Road Olton Birmingham, B27 6PA, UK. ISBN 978-1-849690-10-2 www.packtpub.com
Cover Image by Vinayak Chittar (
[email protected])
[ FM- 2 ]
Credits Author
Editorial Team Leader
Edmund Tan
Gagandeep Singh
Reviewer
Project Team Leader
Greg Yap
Lata Basantani
Acquisition Editor
Project Coordinator
David Barnes
Srimoyee Ghoshal
Development Editor
Proofreader
Rakesh Shejwal
Sandra Hopper
Technical Editors
Graphics
Neha Damle
Geetanjali Sawant
Rukhsana Khambatta Production Coordinator Shantanu Zagade
Copy Editor Sanchari Mukherjee
Cover Work Shantanu Zagade
Indexer Monica Ajmera Mehta
[ FM-3 ]
About the Author Edmund Tan is the CTO and co-founder of a leading e-forms and workflow
solution vendor based in Singapore. He holds more than eight years of experience building performance-critical .NET e-forms and workflow solutions for smart devices on top of Oracle and Microsoft SQL Server databases for large companies and governmental institutions located in Singapore and Malaysia. Edmund is also a regular public speaker at various conferences held in Singapore and Malaysia on the topic of on-the-go BPM (Business Process Management) hosted on smart device technologies. During his free time, Edmund works on trying to create machines capable of emulating human thought. My first and heartfelt word of thanks goes out to David Barnes without whom this book would not have been possible at all. I also thank James Lumsden for giving my e-mail a chance, Rakesh Shejwal and Greg, my long-time collaborator, for their always insightful edits, Srimoyee Ghoshal for keeping me on schedule, Lata Basantani, as well as everyone else involved in this book at Packt. I extend my gratitude to my parents, Laumee and Obagi, for their undying love and support, my aunt Laumar for instilling the bookworm in me, my wife Shen for keeping the beverages flowing, my kid Sophie simply for being the pride of my life, and, last but not least, my brother Edwin for his jokes during the really, really late hours of the night.
[ FM- 4 ]
About the Reviewer Greg Yap is a tech-savvy person who's always interested in the latest technology. He has worked on .NET since its first release. He has experience in developing software for small, medium, and enterprise-sized companies around the world and has worked on Microsoft technologies for a variety of platforms including the .NET Compact Framework. He is also well versed in the C# and VB.NET programming languages. I would like to thank my family, especially my understanding wife Jennie who's given me all the support and time I needed to review this great book. To my two kids—thank you! I'm glad that both of you have shown the patience I knew you would. My thanks also go out to my friend Edmund for having considered me as the technical reviewer for this book—I owe you a coffee! Last but not the least, I would like to thank Srimoyee Ghoshal for always being there to keep me on time and the remaining Packt Publishing team for giving me the opportunity to review this book.
[ FM-5 ]
Table of Contents Preface Chapter 1: CRMLive.NET: An Overview
1 9
Tomorrow Inc. The mobile sales force application The mobile dashboard application The mobile support case application Data flow in CRMLive.NET Deciding on the type of mobile application Thin clients Thick clients Smart clients Messaging A brief summary Defining the mobile sales force application Capturing lead, opportunity, and customer information
10 10 11 12 12 13 13 14 14 15 15 16 17
Task management Full-text search functionality Integrating with Windows Mobile and the hardware
20 21 22
Data synchronization Dual database support Application maintenance and upgrades
25 25 25
Mobile screen design Creating reusable controls Maintaining global lists Data retrieval and manipulation on the mobile device Data transfer between multiple mobile devices
Detect incoming phone calls and SMS Integrating with the Windows Mobile Calendar and Contacts book Data exchange using Bluetooth and IrDA Capturing handwritten input
18 19 19 20 20
23 23 24 24
Table of Contents
Defining the mobile dashboard application Using stateless web services as a data source Rendering the dashboard
25 26 27
Defining the mobile support case application Building a messaging backbone with MSMQ Summary
28 29 30
Road show revenue Total monthly sales Sales for last three months
Chapter 2: Building the Data Tier
Designing the data tier SQL Server CE 3.5 versus Oracle Lite 10g Connecting the Windows Mobile emulator to ActiveSync Setting up SQL Server Compact 3.5 Installing SQL Server CE on the development machine Installing SQL Server CE on the Pocket PC device. Setting up Oracle Lite 10g Installing Oracle Lite 10g on the development machine Installing Oracle Lite 10g on the Pocket PC device Building the data tier Building the CRMLiveFramework project Defining the IDataLibPlugin interface Building the Plugin Manager UI An overview Implementing the PluginCollection class Implementing the PluginManager class Creating the SalesForceApp project Building the ConfigurePlugin form Building the PluginsSetup form Testing what you've built so far Building the database plugins Implementing the SQL Server CE Plugin Storing DDL in the resource file Building the plugin class Connecting to SQL Server CE Programmatically creating the SQL Server CE database Testing what you've done so far
27 28 28
31 31 32 33 35 35 36 37 37 38 38 39 40 43 44 46 46 50 50 54 58 59 60
60 63 64 65 67
Browsing the SQL Server CE database with Query Analyzer
68
Implementing the Oracle Lite plugin
75
Retrieving data from SQL Server CE Data manipulation in SQL Server CE Dealing with GUID values in SQL Server CE Storing DDL in the resource file
[ ii ]
70 71 74 75
Table of Contents Building the plugin class Connecting to Oracle Lite Programmatically creating the Oracle Lite database
Browsing the Oracle Lite database with Msql Retrieving data from Oracle Lite Data manipulation in Oracle Lite Dealing with GUID values in Oracle Lite
Summary
Chapter 3: Building the Mobile Sales Force Module
A brief walkthrough of what you will be building Building a form navigation class Building the main menu Using Main() as the startup object Creating the business objects to encapsulate your DataSets Validating data in your business objects Building the AccountViewer form Data binding .NET controls to your business objects Launching the AccountViewer form Testing the AccountViewer form Building the Tasks list Populating the Tasks Datagrid control Building the TaskDetailViewer form Launching the TaskDetailViewer form Testing the tasks list Handling file attachments Building a file manager class Building the FileUpload user control Building the FileDetailViewer form Testing file upload functionality Custom formatting and display in list controls Building a collection sorter using the IComparer interface Custom formatting and display in the list control Using the History list control Testing the History list control Building the ProductList control Using XML DOM to store and retrieve product selection Using the ProductList control Testing the ProductList control Building a paged listing of accounts Paging in SQL Server CE Paging in Oracle Lite [ iii ]
77 77 78
80
81 81 84
85
87
88 94 96 98 100 108 109 111 111 114 114 115 116 119 120 120 121 123 126 128 129 130 131 135 136 136 139 141 142 142 143 146
Table of Contents
Building a paging user control Creating a context menu for the paging user control Launching the accounts listing page Testing the accounts listing page Summary
Chapter 4: Building Search Functionality
A brief walk-through of parameterized search Building the parameterized search feature Creating the parameterized search query in SQL Server CE Creating the parameterized search query in Oracle Lite Encapsulating the retrieved Dataset using business objects Building the parameterized search forms Building the search form Building the search results listing form Trying out your code
A brief walk-through of full-text search Building the full-text search feature Creating the Keyword Extractor classes A sample keyword extractor—the HTML Keyword Extractor Indexing the file Creating the full-text search query for SQL Server CE Creating the full-text search query for Oracle Lite Encapsulating the retrieved dataset using business objects Creating the full-text search forms Trying out the full-text search Improving the full-text search engine Summary
Chapter 5: Building Integrated Services
Sending SMS and e-mail from your application Sending SMS and e-mail directly through code Delegating to the default Windows Mobile Compose UI Intercepting incoming SMS Intercepting an SMS message Placing phone calls from your application Detecting incoming phone calls Populating the History tab in the sales force application Creating the data tier functions to insert historical records Encapsulating SMS functionality Encapsulating phone functionality Intercepting incoming SMS messages and phone calls in the background [ iv ]
147 150 151 152 152
155 156 157 160 163 164 165
165 168 168
169 171 172 173 174 175 182 185 185 190 190 191
193 193 194 196 196 197 198 199 200 201 204 206 208
Table of Contents
Handling outgoing SMS messages and phone calls Testing your code Synchronizing with Windows Mobile Contacts Synchronizing with Windows Mobile Tasks Sharing an account between two devices Sharing an account between two devices using Infrared (IrDA) Sharing an account between two devices using Bluetooth Capturing handwritten input using the Smart Device Framework Summary
Chapter 6: Data Synchronization
Overview of the different data synchronization methods available for Microsoft SQL Server CE SQL Remote Data Access (SQLRDA) Merge replication Microsoft Synchronization Services Overview of the different data synchronization methods available for Oracle Lite Oracle Mobile Server A quick comparison between the various Synchronization frameworks Using Microsoft Synchronization Services Setting up Microsoft SQL Server and Microsoft Synchronization Services Creating the CRMLive server tables Creating the WCF service Configuring the WCF service library Setting filters for the Sync
Configuring the client project Writing the sync code Conflict resolution Using Oracle Mobile Server Installing Oracle Database Enterprise 11g and Oracle Mobile Server Creating an Oracle Mobile repository Creating the CRMLive server tables Creating a new publication using the Mobile Database Workbench Creating a new mobile project Adding publication items to your project Adding sequence items to your project Adding a publication to your project
Publishing the mobile application to the mobile server Setting up application users using the WebToGo portal
[v]
212 215 216 219 220 222 227 230 235
237 238 238 238 239 240 240 241 242 242 243 243 246
248
250 253 255 256 257 258 260 262
262 264 268 270
273 275
Table of Contents
Registering the mobile device with the mobile server Synchronizing with the mobile server Synchronizing files with the server Creating network-aware synchronization modules Summary
279 280 281 283 284
Chapter 7: Optimizing for Performance
285
Using BeginUpdate and EndUpdate Using SuspendLayout and ResumeLayout Load and cache forms in the background
302 302 303
Measuring performance Measuring .NET CF code performance Capturing application performance statistics Optimizing database performance Data caching Using database indexes to boost search performance Other database optimization tips Optimizing data transfer performance Managing better code Managing better strings Managing better Winforms
Managing better XML
Using XMLTextReader and XMLTextWriter XML serialization and deserialization thesis
Managing better files The .NET Compact Framework garbage collector Summary
286 286 289 292 293 295 297 297 300 301 302
303
303 304
304 304 306
Chapter 8: Securing the Application
307
Chapter 9: Globalization
323
Encrypting the database Encrypting the SQL Server CE database Encrypting the Oracle Lite database Authenticating the sales force application Performing one-way encryption using SHA256 Writing the code for authentication Loading the login form Encrypting data for inter-device transmission using AES Summary Supporting double-byte languages Supporting Japanese character input in Windows Mobile Supporting Unicode at the application level [ vi ]
307 308 309 309 310 312 317 319 322 323 324 324
Table of Contents
Designing culture-sensitive forms Retrieving culture information Summary
326 330 331
Chapter 10: Building the Dashboard
333
Chapter 11: Building the Support Case System
365
An overview of the dashboard Creating the web service Creating the dashboard smart client Connecting to the web service Creating the line chart Creating the round gauge Creating the bar chart Summary
Introduction to MSMQ and the support case system Setting up MSMQ on your mobile device Writing your first MSMQ application Setting up MSMQ on your server Creating a queue on the server manually using the computer management panel Sending a message from the server to a remote mobile device Sending data to a remote queue Creating the server-side application Creating the client-side application Sending a message from the mobile device to the server Writing the client-side code Writing the server-side code Summary
Chapter 12: Testing and Debugging
Overview of Power Toys for .NET CF 3.5 Installing Power Toys for .NET CF 3.5 Using the Remote Performance Monitor and GC Heap Viewer tool Memory leaks and their causes A sample application with memory leak Using the Remote Performance Monitor tool to view application statistics in real time Using PerfMon to graphically view runtime performance statistics Using the GC Heap Viewer tool to detect memory leaks Resolving the memory leak
[ vii ]
333 334 338 338 341 350 356 363 366 369 371 377
378 379 379 380 385 389 389 390 392
393 394 394 395 395 396 398 400 403 404
Table of Contents
Using the CLR Profiler tool A sample application with bad performance Launching the application with the CLR Profiler tool Inspecting the Histogram view Inspecting the Allocation Graph Inspecting the Time Line view Inspecting the Call Tree view Using the App Configuration tool Using the ServiceModel Metadata tool Using the Remote Logging Configuration tool Using the Network Log Viewer tool Summary
405 406 409 410 411 412 415 417 419 421 423 424
Chapter 13: Packaging and Deployment
425
Index
455
Deploying your solution as a CAB file Adding the SalesForce application files to your CAB project Configuring other miscellaneous settings Deploying your solution as an MSI file Creating an INI file Creating the custom action DLL Creating the MSI installer project Creating an automated update service Creating the server-side web service Creating the client-side updater tool Summary
[ viii ]
426 427 430 432 433 433 438 442 442 445 454
Preface As business systems become increasingly distributed, the mobile device becomes an increasingly important tool on the enterprise stage. The large amount of processing power available to mobile devices nowadays bring to it a whole new range of possibilities as a mobile extension to traditional server-based enterprise systems. Harnessing this power is the .NET Compact Framework, which has seen tremendous improvements over the last few versions. The .NET Compact Framework provides a rich set of managed classes that does away with a big chunk of the menial labor required to perform common tasks, leaving the developer to focus on building business logic instead. This book is not intended to be a complete reference tome of the .NET Compact Framework. There are numerous books and documentation online that serve this purpose. Rather, it will show you how to apply the .NET Compact Framework in interesting ways to solve real-world business problems. We will explore commonly encountered design decisions and technology comparisons along the way and ultimately build clean solutions that keep to best practices such as the three-tier design and the Model View Controller (MVC) model. Using a sales force application as the central example and theme in this book, you will have a clear step-by-step guide on building one of the most popular types of business applications in the market today from ground up. Through these pages, you will learn how to create robust data-driven mobile applications that work seamlessly with other mobile devices and database servers. You will get to explore the little nuances of .NET Compact Framework programming, and how to get around them using its advanced features. You will also get a firsthand look at how you can use third-party libraries such as the open source Smart Device Framework to add a host of rich functionality to your applications.
Preface
Towards the end of this book, you will have accumulated enough understanding of the capabilities and limitations of the .NET Compact Framework and its tools to confidently tackle an enterprise mobile application of any size or complexity. I hope in the process of getting there you will have as much fun reading this book and trying out the samples as I had writing it.
What this book covers
Chapter 1, CRMLive.NET: An Overview, provides a technical and scope overview of CRMLive.NET, a mobile customer relationship management suite comprising three individual applications (a mobile sales force, mobile dashboard, and mobile support case application). Chapter 1 also outlines the four different mobile client models and a comparison of their strengths and weaknesses. Chapter 2, Building the Data Tier, shows how a plugin-based data tier based on both the Microsoft SQL Server Compact and Oracle Lite databases can be created using ADO.NET. Chapter 3, Building the Mobile Sales Force Module, walks the reader through building the logic and presentation tiers of the mobile sales force application, illustrating various concepts along the way such as UI object reusability, validation, paging, record navigation, sorting, and grouping. Chapter 4, Building Search Functionality, illustrates how full-text search and parameterized-search functionality can be added to the mobile sales force application. Chapter 5, Building Integrated Services, illustrates how the sales force application can make use of the .NET Compact Framework and P/Invoke calls to access underlying Windows Mobile operating system and mobile device functionality such as the Bluetooth, Infrared, Calendar, and Telephony services. Chapter 6, Data Synchronization, covers one of the most important topics in the book—the process of data synchronization between the mobile device and the remote database. In this chapter, we will look at how the sales force application can perform bidirectional synchronization using Microsoft SQL Server Compact's SQL RDA and Oracle Lite's mSync technologies. Chapter 7, Optimizing for Performance, illustrates how the sales force application's performance can be measured and improved using various techniques such as data caching and data compression.
[2]
Preface
Chapter 8, Securing the Application, covers the various ways to secure locally stored data on the mobile device. It also covers the various authentication mechanisms available during data synchronization with the remote database. Chapter 9, Globalization, illustrates how the reader can globalize the sales force application to intrinsically support double-byte (Unicode) languages. Chapter 10, Building the Dashboard, walks the reader through the building of the second application in CRMLive.NET—the mobile dashboard. It will cover the use of stateless asynchronous web service calls to retrieve XML-based data from a remote server. Chapter 11, Building the Support Case System, walks the reader through the building of the third application in CRMLive.NET—the mobile support case application. It will cover how a messaging backbone based on Microsoft Messaging Queue (MSMQ) technology can be built to support disconnected-state messaging between two remote applications. Chapter 12, Testing and Debugging, looks at how the tools provided in the PowerToys for .NET CF 3.5 suite can assist in the testing and debugging process of the CRMLive. NET application. Chapter 13, Packaging and Deployment, walks through the packaging and deployment process of the CRMLive.NET application and how a network-aware, automated update service can be created to assist in the deployment of application upgrades.
What you need for this book
This book provides all source code in both VB.NET and C#. To run most of the code samples in this book, you will need the following basic tools: • • • • •
A suitable development workstation with Microsoft Visual Studio 2008 Windows Mobile 6 SDK (includes an emulator for you to test your .NET CF applications) Microsoft ActiveSync 4.5 (for Windows XP machines) or Microsoft Mobile Device Center (for Windows Vista machines) The .NET Compact Framework 3.5 redistributable The Microsoft SQL Server Compact 3.5 database
[3]
Preface
Throughout the book, we will also encounter certain technologies and products when we build our application. The following lists the other products used in the book: •
•
•
•
In Chapter 2, we will show how the data tier can also be built to support the Oracle Lite database. To run the Oracle Lite code samples, you will need to download and install Oracle Lite 10g. In Chapters 5 and 10, we make use of the Smart Device Framework to handle specific requirements in the CRMLive.NET application. To run the code samples in these chapters, you will need to install the Community Edition of the Smart Device Framework v. 2.3.0.39 (at the time of writing). In Chapter 11, we will be building a messaging backbone on top of the Microsoft Messaging Queue service. You will need to install the MSMQ service on the mobile device. In Chapter 12, we will cover the tools provided in the PowerToys for .NET CF 3.5 suite (provided by Microsoft) to test and debug the CRMLive.NET application. You will need to install this product to try out the samples in this chapter.
Who this book is for
This book is primarily targeted at developers who are new to the .NET Compact Framework and wish to embark on data-driven mobile application development in an enterprise scenario. All code samples included in this book are in VB.NET and C#. This book assumes you are familiar with either the Visual Basic.NET or C# language. It does not require you to have any prerequisite experience or knowledge of the .NET Compact Framework. This book is also targeted at: •
•
Developers who are already familiar with the .NET Compact Framework, but want to learn about how it can be effectively used to tackle commonly faced problems in real-life business scenarios Developers who want to learn how to use the .NET Compact Framework to access core Windows Mobile 6.0 and device functionality
Most of the samples in this book are targeted at the Windows Mobile operating system. Some basic knowledge of the Windows Mobile operating system would be beneficial, but not necessary.
[4]
Preface
Conventions
In this book, you will find a number of styles of text that distinguish between different kinds of information. Here are some examples of these styles, and an explanation of their meaning. Code words in text are shown as follows: "We can include other contexts through the use of the include directive." A block of code will be set as follows: public string PluginDatasource { get { return _PluginDatasource; } set { _PluginDatasource = value; } }
When we wish to draw your attention to a particular part of a code block, the relevant lines or items will be shown in bold: public bool SetAccountDetails(Guid AccountGUID, System.Data.DataSet Account) { SqlCeDataAdapter _adapter; SqlCeTransaction _transaction; }
New terms and important words are shown in bold. Words that you see on the screen, in menus or dialog boxes for example, appear in our text like this: " Open Windows Mobile Device Center and click on Connection Settings under Mobile Device Settings. " Warnings or important notes appear in a box like this.
Tips and tricks appear like this.
Reader feedback
Feedback from our readers is always welcome. Let us know what you think about this book—what you liked or may have disliked. Reader feedback is important for us to develop titles that you really get the most out of. [5]
Preface
To send us general feedback, simply drop an e-mail to
[email protected], and mention the book title in the subject of your message. If there is a book that you need and would like to see us publish, please send us a note in the SUGGEST A TITLE form on www.packtpub.com or email
[email protected]. If there is a topic that you have expertise in and you are interested in either writing or contributing to a book, see our author guide on www.packtpub.com/authors.
Customer support
Now that you are the proud owner of a Packt book, we have a number of things to help you to get the most from your purchase. Downloading the example code for the book Visit https://www.packtpub.com//sites/default/files/ downloads/0102_Code.zip to directly download the example code. The downloadable files contain instructions on how to use them.
Errata
Although we have taken every care to ensure the accuracy of our contents, mistakes do happen. If you find a mistake in one of our books—maybe a mistake in text or code—we would be grateful if you would report this to us. By doing so, you can save other readers from frustration, and help us to improve subsequent versions of this book. If you find any errata, please report them by visiting http://www.packtpub. com/support, selecting your book, clicking on the let us know link, and entering the details of your errata. Once your errata are verified, your submission will be accepted and the errata added to any list of existing errata. Any existing errata can be viewed by selecting your title from http://www.packtpub.com/support.
[6]
Preface
Piracy
Piracy of copyright material on the Internet is an ongoing problem across all media. At Packt, we take the protection of our copyright and licenses very seriously. If you come across any illegal copies of our works in any form on the Internet, please provide us with the location address or website name immediately so that we can pursue a remedy. Please contact us at
[email protected] with a link to the suspected pirated material. We appreciate your help in protecting our authors, and our ability to bring you valuable content.
Questions
You can contact us at
[email protected] if you are having a problem with any aspect of the book, and we will do our best to address it.
[7]
CRMLive.NET: An Overview Mobile device programming by itself can be a serious test of software engineering skill, where coders are usually forced to work with a fraction of the resources available compared to the full .NET Framework. Screen area, processor speed, memory, and disk space available stand at a fraction of what one would generally get with a desktop computer. In fact, the .NET Compact Framework implements only thirty percent of the full .NET Framework, doing away with a large subset of the classes deemed to be unsuitable for the small footprint required of a mobile application. In such a resource-tight environment, the wrong design decisions can often lead to an underperforming application, a clunky interface that irritates users, or worse—grinds to a halt when deployed in an enterprise scenario where hundreds of instances of the application need to exchange and sync data in synchrony. As you progress through this book, you will find that the three-tier model recurs throughout all chapters. In each approach, you will learn how to think a few steps ahead and adopt designs that allow the various components you are building to fit in together elegantly. We start this first chapter by exploring an overview of game developer Tomorrow Inc. and CRMLive.NET, a mobile customer relationship management suite that we will be building for this company. By the end of this chapter, you will obtain: •
A first-hand look at how the end product you are developing will look
•
A general overview of the functional scope of CRMLive.NET
•
A look at the various designs undertaken in CRMLive.NET and why they work best in each scenario
•
An understanding of the various classes in CRMLive.NET and their interactions
•
An overview of the various .NET Compact Framework technologies you are going to use along the way
CRMLive.NET: An Overview
Tomorrow Inc.
Tomorrow Inc. is a cutting edge gaming company of about 5,000 employees based in New York with branches around the world. They have recently developed a technology that immerses the gamer in a virtual world and are planning to launch their first MMIG (Massively Multiplayer Immersive Game), titled 'Rabbits from Hell' at a road show in New York and Tokyo at the same time. The products include a virtual reality chair, simply known as 'The Chair,' which is basically an interface to the game that allows full unrestricted physical motion from the player. Used together in conjunction with a set of Virtual Reality goggles, it brings gaming realism to a whole new level by immersing the player completely in the game. The Chair is about the size of an armchair and after purchase, a visit will be typically arranged by Tomorrow Inc. technicians to deliver and install the product in the customer's home. The road show is the biggest ever launch in the history of Tomorrow Inc., stationing about 100 salespersons at the event itself and another 50 on the streets for a total duration of three weeks. Tomorrow Inc. needs a suite of mobile applications: CRMLive.NET to cater to the needs of three different levels of staff—the salesperson, the boss, and the technician.
The mobile sales force application
Tomorrow Inc. needs a mobile sales force application that will allow its salespersons to capture and manage leads, opportunities, and customers at the event. Before we proceed with the technical requirements, let's explore Tomorrow Inc.'s business a little further in detail. A 'lead' is a sales term that applies to someone who shows an interest in the product, but how genuine he or she is about buying it has not yet been ascertained. It is usually the intention of the salesperson to convert a lead (a curious prospect) into an 'opportunity' (a serious potential buyer) and eventually into a buying customer. At any one time during and after the launch of the game, there will be a mix of people from all three categories. There are the curious onlookers at the road show who casually enquire about the game. These people fall under the leads category. Then there are those who have been hounded by salespersons for weeks. They have the money and do show a genuine interest to buy, but need a little more coaxing. These are 'opportunities'. Finally, there are the frenzied young kids who have camped outside the road show hall since 2 a.m., having saved up their entire life savings anticipating the game release. These are undoubtedly buying customers.
[ 10 ]
Chapter 1
Tomorrow Inc. is forecasting a huge flood of visitors to its road show, and has placed a strong emphasis on a lean UI design that can streamline quick data capture and access. The following list highlights the core requirements of this application in brief: •
Capable of lead, opportunity, customer, and task management
•
Must have quick and responsive UI
•
Must support offline access to stored information (when a network connection is unavailable)
•
Must support strong personalization features
•
Must be highly secure—Tomorrow Inc. is anticipating the possibility of lost mobile devices and wishes to protect locally stored information
•
Must be capable of fast full-text search (searching within file attachment content and the entire database)
•
Salespersons may decide to share leads at any point in time. The application must be able to transfer leads from a mobile device to another easily
•
Must have dual database support—due to incredibly poor IT budget management, half of Tomorrow Inc.'s servers are hosted on Oracle, with the other half hosted on Microsoft SQL Server Enterprise. Extending this to the mobile device, this means that Oracle Lite Edition and Microsoft SQL Server Compact Edition support will be necessary
•
Authentication must make use of Tomorrow Inc.'s existing ActiveDirectory setup
•
Must provide Japanese-language support for users at the road show in Tokyo
The mobile dashboard application
The boss of Tomorrow Inc. travels across country frequently by train; his trips usually take him through areas with little or no Internet connectivity. His requirements are primarily centered on a mobile dashboard application that allows real-time monitoring of sales figures and Key Performance Indices (KPIs). His requirements include: •
Must provide customizable dashboard views
•
Must have real-time access to the latest data from the server
•
Must be able to display information visually in the form of interactive charts and gauges
[ 11 ]
CRMLive.NET: An Overview
The mobile support case application
After the customer purchases a game account, the Tomorrow Inc. technician will need to make a visit to the customer's residence to install and deploy The Chair. Before that can happen, the following series of actions will first take place: 1. A data entry clerk at the Tomorrow Inc. office will create a job at the server. 2. Once a job is created, it will be broadcast to all technicians on the field over the Internet. 3. The technicians can then choose to accept a job via the mobile application. The accepted job is subsequently removed from the list of jobs at the server. The core requirements for this application include the following: •
Must be able to broadcast/push new jobs to individual mobile devices over the Internet even if these devices are offline
•
Must be able to propagate the technician's actions back to the server
Data flow in CRMLive.NET
Based on the requirements outlined in the previous section, we can roughly summarize the data flow of CRMLive.NET in the succeeding diagram. CRMLive. NET comprises three separate and distinct applications that tap into the same central data source. The sales force application is the key application that provides data to fuel the rest of the system. You will notice from the diagram that mobile devices running the sales force application can also share data directly with each other. This is because salespersons at the event may decide to transfer their leads and opportunities to their colleagues. At the end of the day, all data entered through the sales force application is eventually synced up to the central database. The support case system will make use of information captured from new customers, such as their residential addresses and phone numbers, to generate new jobs for the technicians. The mobile dashboard on the other hand will periodically retrieve sales-related data (keyed in when new customers are created) directly from this central database every minute. This sales data is converted into visual charts for display on the mobile dashboard.
[ 12 ]
Chapter 1
Customer info Tomorrow Inc CRMLive.com
Customers, Leads & Opportunities Internet
Support Case
SYNC UP Device-to-device data sharing Central Database Sales Force Internet
Sales info
Mobile Dashboard
Each of these three applications access data in a different way. In all three scenarios, the central database can be accessed over the Internet as well as over an Intranet. We will also explore the different ways to protect data transmission and access throughout CRMLive.NET in the later chapters of this book.
Deciding on the type of mobile application
There are four different ways to deploy a mobile application, each with their own benefits and downsides. Before you decide on the type of model to use in your applications, let's take a look at them in further detail.
Thin clients
Applications deployed on a thin client model are accessed through browsers on the mobile device, called mobile browsers or alternatively, mini browsers, or wireless Internet browsers. They are essentially scaled-down versions of a full-blown browser. For instance, the default preinstalled browser that comes with the Windows Mobile operating system, Internet Explorer Mobile, is a mobile browser.
[ 13 ]
CRMLive.NET: An Overview
In the thin client model, application code and data are not stored on the mobile device. Just like traditional web-based applications, all the work is done at the server, and each page is served up on-demand as they are accessed from the mobile device. Thin-client applications, therefore do not require the installation of any additional software other than the mobile browser.
Thick clients
Thick clients run entirely on the mobile device and store both code and data locally. It must be installed on the mobile device prior to usage. Thick clients typically utilize a mobile database installed on the same mobile device such as Microsoft SQL Server Compact or Oracle Lite. It is common for thick clients to transfer its data from the local database to a remote and central database periodically so that it can be shared with other units. This process is called data synchronization. The basic set of features you would find common across thick clients include: •
A responsive and interactive UI
•
Ability to store data locally in a mobile database
•
Ability to synchronize data with a database on a remote server
Smart clients
A smart client, sometimes known as a rich client lies somewhere between a thin client and a thick client. Code is still stored locally on the mobile device, but data is stored on a remote server. Data (commonly in the form of XML) is retrieved from or updated to the server in an on-demand fashion whenever needed. There are various ways over which this data exchange can take place, the most common being web service calls, made over the SOAP (Simple Object Access Protocol) interface. As smart clients still render the UI of the application locally, it has the benefit of a responsive and rich interface. However, smart clients do not have to worry about data synchronization as all data manipulation is done in real time (via web service calls). Smart clients are suitable for projects where: •
There are lists of frequently changing information, such as global pricelists that need to be frequently updated on the mobile device.
•
Developers need the flexibility of a customizable and responsive UI together with real-time data transactions.
[ 14 ]
Chapter 1
Messaging
Lastly, messaging-based mobile applications make use of services such as SMS (Short Messaging Service) and e-mail to drive the interaction between the user and the application. Like the thin client model, it does not require installation of any specific software other than the messaging application itself. For example, a stock price enquiry service may involve the following actions: •
A mobile user sends an SMS containing the stock symbol of the desired stock to a special number
•
This incoming SMS is processed by the server-side application, which then queries the database and retrieves the corresponding stock price for the symbol.
•
The stock price is then sent back to the user via SMS
A brief summary
The following table summarizes the various strengths and shortcomings of each type of mobile application: Feature
Thick client
Thin client
Smart client
Messaging
Requires installation/stores code locally on the mobile device
Y
N
Y
N
Requires live network connection to the application server
N
Y
N
N
Stores data locally and requires database on the mobile device
Y
N
N
N
Allows application to access low level device functions
Y
N
Y
N
Middleware requirements on the mobile device
.NET CF,
Mobile Browser
.NET CF
Messaging application
UI responsiveness
High
Low
Medium
High
UI richness/interactivity
High
Low
High
Low
Ease of development
Low
Medium
Medium
High
Security
High
Medium
Medium
Low
Ease of application update and maintenance
Low
High
Low
High
Mobile Database
[ 15 ]
CRMLive.NET: An Overview
Feature
Thick client
Thin client
Smart client
Messaging
Ideal for high transactional volume
Y
N
N
Y
Ideal for data transactions that are large in size (example : file attachments)
Y
N
N
N
Ideal for complex input data (example : forms involving multiple field input)
Y
Y
Y
N
Ideal for complex data input methods (example : digital signatures, barcode scanning)
Y
N
Y
N
Application data latency
Non real-time
Real-time
Real-time
Real-time
Support for different operating systems/devices
Low
High
Low
High
Defining the mobile sales force application
We will first take a look at the mobile sales force application and try to decide on a suitable client model. Looking at the earlier chart, a thick client model would best fit the requirements of this application due to the two key requirements of a responsive UI and offline access support. You may have also considered the smart client model for this task, but there is no requirement for real-time data transactions in this application. A local database works better in this case because it eliminates network latency that would otherwise be present on the smart client (due to remote database access). Furthermore, a smart client model would not work well in areas without a network connection.
[ 16 ]
Chapter 1
In the following sections, we walk through the main functionality of the application and outline the various .NET Compact Framework technologies that will be used in the process.
Capturing lead, opportunity, and customer information
Tomorrow Inc. is anticipating a very large turnout at the road show; your users will need 'at a glance' access to key information as well as easy navigation across the various data entry windows.
[ 17 ]
CRMLive.NET: An Overview
Mobile screen design
To cite an example of a screen design that is the product of objective thought, consider the following Lead Details form:
You might be used to the idea that phone numbers, street addresses, and e-mail addresses are related and should all go together under a single Contact Details tab. Thinking objectively though, the salesperson would likely spend more time trying to follow up on a lead making phone calls rather than holding face to face meetings at the lead's address. In this case, it makes for a better design to have the phone number and not the address on the first screen. When we build this application later on in the book, you will encounter other similar examples and learn how you can minimize the need to switch between tabs by placing the most commonly used fields in a single screen. We will also extend the idea of objective design to the toolbars and menus in the application, covering best practices and how efficient use of these controls can help reduce on-screen clutter.
[ 18 ]
Chapter 1
Creating reusable controls
You may have also noticed from the previous screenshots that we have placed various icons next to the phone number fields. They allow the user to initiate a phone call or send an SMS directly using the number in the adjacent text box. This control, called the PhoneNumber control (shown in the following screenshot), is one of the various Usercontrols that we will build and reuse throughout this application.
Maintaining global lists
You will also learn how to implement a simple checklist such as the Interested product(s) checklist shown in the succeeding screenshot using the .NET Compact Framework's Listview control.
To make things interesting, we will also feature a dynamic product list that can change any time at the remote server end. We will explore how a sync in the opposite direction (from server to mobile device) can be used to update global lists like this in your application.
[ 19 ]
CRMLive.NET: An Overview
Data retrieval and manipulation on the mobile device
As this is the first section dealing with data retrieval and manipulation, we will also get a first-hand look at the ADO.NET libraries and how we can make full use of the library to execute SQL queries against the local database to retrieve and manipulate sales force data. We will cover data access and ADO.NET in full detail when we build a generic data layer in Chapter 2, Building the Data Tier.
Data transfer between multiple mobile devices
Transferring data between one mobile device and another is a common requirement in most enterprise scenarios and our sales force application is no exception. At any point in time, a salesperson may decide to hand over a lead to another colleague, possibly because he or she is not interested in following up on that lead. Passing the lead to another person means that the receiving party needs to have the lead details and all other relevant data transferred to his mobile device. There are a few different approaches to data transfer, which we will cover in detail when the time comes, but for this application we will concentrate on using both Infrared and Bluetooth technology to transfer a lead record (in compressed XML format) directly from one device to another.
Task management
One of the reasons for building a task management module in this application is to allow the salesperson to manage tasks that are lead and opportunity specific. We will choose to implement the task listing using the .NET Compact Framework's DataGrid control due to its rich set of data formatting and data binding capabilities. You will also learn the basics of using the DataGrid control in this section and how to get around its limitations. We will also cover the following subtopics: •
How to data bind an ADO.NET data source to the DataGrid control
•
How to implement data paging at the SQL level
•
How to create columns with multiple sorting
•
How to handle data and cell formatting in the DataGrid control
[ 20 ]
Chapter 1
Full-text search functionality
Being able to search within the content of a file and entire databases via an incredibly simple UI comprising a text box and a button is indeed very attractive to any mobile user. We will build a similar search engine for the sales force application. It will be capable of searching within file content, file names, and database records all at one go.
[ 21 ]
CRMLive.NET: An Overview
Microsoft SQL Server Compact and Oracle Lite needs to operate with a small footprint and, unfortunately, does not provide any full-text search feature. We will take it upon ourselves to create a simplified version of the full-text search service of our own. You will learn how to do the following from this exercise: •
How to index the contents of a file for full-text search
•
How to handle wildcard and binary searches (the ability to combine multiple search phrases with AND and OR conditions)
•
How to create column indices in Microsoft SQL Server Compact and Oracle Lite to improve the performance of full-text search queries
The following screenshot shows what the search results listing will finally look like in your application:
Integrating with Windows Mobile and the hardware
The mobile device is usually packed with useful hardware and operating system features like telephony, GPS, SMS, and Bluetooth, just to name a few. You can use the libraries in the .NET Compact Framework and the open source Smart Device Framework to access a large range of such functionality from your application. Looking at an example of how this can work with your sales force application, consider phone number displays in your application. As you saw earlier, you will be creating a reusable PhoneNumber usercontrol that allows users to easily place a phone call or send an SMS message. You will learn how you can call the Windows Mobile classes in the .NET Compact Framework libraries to achieve this. Most of what follows in this section refers to application features discussed earlier. [ 22 ]
Chapter 1
Detect incoming phone calls and SMS
We will in fact extend this functionality further using the .NET Compact Framework Message Intercept classes to detect incoming phone calls and SMS. You will see how you can use these events to automatically generate historical log entries for every lead. This allows the salesperson to see a comprehensive history of all correspondence with a particular lead.
Integrating with the Windows Mobile Calendar and Contacts book
If you recall, we had to implement a lead and opportunity-specific task list in this application. What is also unique about this task list is that a copy of the tasks will be automatically pushed to the Windows Mobile Calendar. This allows you to tap into the reminder services in Windows Mobile to remind the user of an impending task. You will learn how to integrate with the Windows Mobile Calendar and Contacts services using the .NET Compact Framework libraries to achieve this functionality.
The benefits are twofold. In addition to not having to reinvent the wheel, this sort of integration opens up a host of functionality behind the scenes. For instance, by placing a copy of the lead- and opportunity-specific tasks in the Windows Mobile Calendar, the user will be able to eventually sync them to Microsoft Outlook on his desktop PC via ActiveSync. This feature is immediately available to your users without having to write any additional line of code!
[ 23 ]
CRMLive.NET: An Overview
Data exchange using Bluetooth and IrDA
We will also explore how we can transfer the lead details from one device to another directly using both the device's Bluetooth and IrDA (Infrared) capabilities. You will learn how you can efficiently package and compress XML-based data for transmission across these channels.
Capturing handwritten input
When new buyers sign up for a subscription to the game, Tomorrow Inc. needs their signature for record purposes. We can implement signature-capturing functionality using the Smart Device Framework's signature control. The signature will be captured as an image and saved together with the customer record in the database. In Chapters 3 and 6, we will cover more ground on file attachments and how they can be alternatively stored outside the database in the local filesystem. You will also learn of the different options available to the developer to sync files to a remote server.
[ 24 ]
Chapter 1
Data synchronization
Data synchronization for the sales force application is a mix of bidirectional (between mobile device and server) and unidirectional synchronization (mobile device to server only). The synchronization process is closely tied to the database systems that we are deploying for the application. For instance, Oracle Lite uses mSync technology while Microsoft SQL Server Compact uses the Microsoft Sync Framework. In Chapter 6, Data Synchronization, you will learn how to set up a mobile device and a server for data sync on these two databases. We will also go through the various approaches available to handle data concurrency and integrity during the sync process.
Dual database support
As part of Tomorrow Inc.'s requirements, we will ensure that our sales force application can support both the Oracle Lite 10g and Microsoft SQL Server Compact mobile databases. These two databases differ considerably in terms of their data types, SQL syntax, and synchronization processes.
Application maintenance and upgrades
As we've covered earlier on in this chapter, one of the biggest disadvantages of a thick client is the deployment process. Any subsequent updates to the application need to be redeployed to all devices. We will mitigate this drawback by building and integrating a network-aware Auto Update service into our sales force application. You will learn how to create a service that can periodically check and download the latest updates from a remote site on its own without the need to cradle the device.
Defining the mobile dashboard application
Our second application, the mobile dashboard, will be used by executive-level decision makers in Tomorrow Inc. on the move. The mobile dashboard will need to pull real-time data off the server over an Internet connection, which is then collated and presented on the dashboard in the form of a graphical chart. New data is periodically pulled down from the server every minute to refresh the chart displays.
[ 25 ]
CRMLive.NET: An Overview
You can see how a smart client model would probably work best in this scenario; a smart client allows you to render the charts you need on the device while keeping data on a remote server. The real-time nature of the data received will also ensure that your charts always display the latest information.
Using stateless web services as a data source With "on the move" scenarios, you can probably expect Internet service to be choppy or simply not available in some areas during travel. We will therefore use stateless web service calls to the remote server to access data in real time. Using stateless connections in our application allows our dashboard to resume from where it left off after an Internet service disruption without having to request the user to perform any additional action (such as a re-login). The mobile dashboard service will appear seamless to the end user. A stateless web service call is a web service call that does not maintain any state information in between calls.
[ 26 ]
Chapter 1
Web service calls are also, by default, blocking (synchronous) calls. What this means is that when a call is made to the server to retrieve data, your application will ignore all user input until the data is returned. This is certainly not acceptable, especially on a smart client! In Chapter 10, Building the Dashboard, when we build the dashboard, we will explore how we can make use of the multithreading features of the .NET Compact Framework to create asynchronous web service calls.
Rendering the dashboard
The planned dashboard design will look roughly similar to the following screenshot:
You will create the following charts in this application:
Road show revenue
This line chart sums up the total revenue collected from the road shows on a daily basis. The process of building this chart will introduce the basic drawing tools in the GDI + API that you can use in your application for visual data display.
[ 27 ]
CRMLive.NET: An Overview
Total monthly sales
In this chart, you will learn how you can make use of the Smart Device Framework's Gauge control to show the total monthly sales amount as a circular gauge chart.
Sales for last three months
Lastly, we will explore how you can make use of the GDI + API to render a bar chart showing the sales figures for the last three months.
Defining the mobile support case application
Our third and last application, the mobile support case application, involves tighter real-time collaboration between the server and the mobile units. We will use the .NET Compact Framework's messaging controls to build a messaging backbone across the server and the connected mobile devices so that Tomorrow Inc. technicians on the field can be instantly alerted when a new task is created in the server. As all job-related data is stored and managed centrally on the remote server, the smart client model would be the best fit for this application. We stop short of using the thin client model because the construction of the messaging backbone will require client-side coding.
[ 28 ]
Chapter 1
Building a messaging backbone with MSMQ This application is also unique in that it can be regarded as 'push' technology, whereas the earlier two applications were not. 'Push' technology, similar to the common meme 'push e-mail,' is proactive and means that newly available data is automatically pushed to the mobile device without the mobile device requesting for it.
Part of Tomorrow Inc.'s requirements for this application include instant notification to all mobile device units when a new job is created on the server. MSMQ (Microsoft Message Queuing) is a technology that allows an application to send a message to another application (not necessarily on the same machine) even if the other application is offline. These messages are stored in a queue and delivered when the targeted application resumes operation.
As you start building this application in Chapter 11, Buliding the Support Case System you will learn more about the intricacies of the MSMQ service and how you can write code to send and receive messages using the Messaging APIs.
[ 29 ]
CRMLive.NET: An Overview
Summary
From this chapter we've learned that there are four common client models used in mobile development, each with their own strengths and weaknesses. In this book, we will build three applications that cover the two models that utilize the .NET Compact Framework—thick and smart clients: •
The mobile sales force application is a thick-client application that allows users to key in data offline and to sync it with a remote server at a later time.
•
The mobile dashboard application is a smart-client application that will utilize stateless web services for data retrieval due to the limited availability of its Internet connection.
•
The mobile support case application demonstrates how a common messaging backbone can be built on top of the MSMQ framework to provide instant notifications to mobile devices.
Your journey through this book will also ultimately walk you through three important aspects of mobile application development: •
Learning the various methods of data transmission between the mobile device and a remote server and between mobile devices themselves
•
Learning the various ways to integrate Windows mobile and the device itself to provide an interactive and seamless solution
•
Designing an adaptable three-tier and MVC-based solution that makes future maintenance of the application easier.
With this overview, we have enough context to begin building the application. We will start by creating the data layer for the mobile sales force application in the next chapter.
[ 30 ]
Building the Data Tier The data tier handles all transactions between the database and the logic tier. In this chapter, we will explore how we can build a plugin-based data tier for the CRMLive. NET mobile sales force application. A plugin-based data tier has the benefit of not having to recompile the entire application when an underlying database change needs to be done. It also serves to further increase the decoupling of the data tier from the adjacent logic tier. Different database support is provided through the distribution of small DLL (Dynamic Link Library) plugin files, which can be installed or removed by the end user depending on his or her needs. By the end of this chapter, you will have learned: •
How to build an adaptable plugin-based data tier
•
How to set up Oracle Lite and SQL Server CE on the mobile device
•
The differences between Oracle Lite 10g and SQL Server CE 3.5
•
How to use interfaces to access Oracle Lite and SQL Server CE through the same set of method calls
•
How to connect, read, and write data to and from Oracle Lite and SQL Server CE using ADO.NET DataSets
Designing the data tier
As best practice, the logic tier only needs to describe at a higher level of abstraction, what it needs, and the data tier will do the work of retrieving the data from the database. Enter the IDataLibPlugin interface. This interface provides a set of method calls that must be implemented by both the SQL Server CE and Oracle Lite plugins. The logic tier will not need to concern itself with the low-level implementation for either database. It will make all its data requests through this interface.
Building the Data Tier
The classes and their relationships in the data tier can be summarized in the following class diagram. In this chapter, you will create two forms: PluginsSetup and ConfigurePlugin that lets the user manage the list of plugins in the sales force application. These forms utilize a global class called GlobalArea (which essentially contains a single instance of the PluginManager class). The PluginManager class contains a PluginCollection object, which is a list of all the plugins installed on the device. It also contains functions to add or remove plugins, but more importantly, to save and load this whole list to and from disk.
SQL Server CE 3.5 versus Oracle Lite 10g
In a thick client application, an important factor in deciding which mobile database to use depends on what you have installed on the server as your enterprise database. Data synchronization between the device and the enterprise database is usually a job that you want to leave entirely to the database engine. For instance, it doesn't make sense to use Oracle Lite on your mobile device when your enterprise database is SQL Server because they would not be able to sync between themselves. [ 32 ]
Chapter 2
In the sales force application, you will be implementing both databases. There are a few differences between these two databases, which you might be interested to know. They are listed in the following table: SQL Server CE
Oracle Lite
Microsoft provides the Microsoft Sync Framework (with SQL Server 2008) that allows you to sync a SQL Server CE database with any different database (any ADO.NET accessible database)
Oracle Lite syncs only with an Oracle database through the Oracle Mobile Server
SQL Server CE supports IDENTITY columns
Oracle Lite supports the use of SEQUENCES to implement running counters
Mobile database footprint is roughly half the size of Oracle Lite (~2.5MB)
Has a bigger mobile database footprint (~5MB)
Provides GUI-based db management tool to create and manage table schemas
Provides a GUI-based db management tool, but table schema creation/manipulation has to be done through SQL
Does not support database views
Supports database views
One important consideration to note is that both Microsoft SQL Server CE and Oracle Lite do not provide support for stored procedures. Your mobile applications can, however, use parameterized queries for data access.
Connecting the Windows Mobile emulator to ActiveSync
The Windows Mobile SDK provides an emulator kit that lets you test your mobile applications on a PC without the need for a real device. Although you can test your applications on a real mobile device connected to your PC, chances are you will most likely use the emulator at one point or another out of convenience. When using this emulator, you will most likely need to transfer files between your development machine and the emulator. Before you can do this, you must first establish an ActiveSync connection between the two by simulating a cradle on the emulated device. To do this, you must have ActiveSync 4.5 (for Windows XP users) or Windows Mobile Device Center (for Windows Vista users) installed on the development machine.
[ 33 ]
Building the Data Tier
Open Windows Mobile Device Center and click on Connection Settings under Mobile Device Settings. Ensure that the DMA option has been selected under Allow connections to one of the following.
After saving these settings, run the emulator either by running your project in Debug mode or by navigating to Tools | Connect to device in your Visual Studio IDE. After your emulator is up and running, navigate to Tools | Device Emulator Manager. You will be prompted with a window displaying a list of all device emulators registered on your system. Locate the active emulator (the one with an icon of a green arrow next to it). Right-click on this emulator and select the Cradle action from the pop-up menu that appears.
[ 34 ]
Chapter 2
Upon doing so, your emulator will begin to connect to ActiveSync/Mobile Device Center. Once it has connected successfully, your ActiveSync/Mobile Device Center will display a Connected message. You will now find that you can access the filesystem of the emulated device via Windows Explorer on your development machine.
Setting up SQL Server Compact 3.5
Now that you have connected the emulator to your development machine, you will need to install SQL Server CE on both your development machine and the mobile device.
Installing SQL Server CE on the development machine
The SQL Server Compact 3.5 SP1 package contains all the .cab files we need for deployment to the various supported devices and operating systems. To install SQL Server Compact 3.5 on your development machine, follow these instructions: •
Download SQL Server Compact 3.5 Service Pack 1 for Windows Mobile from the following URL: http://www.microsoft.com/Sqlserver/2005/en/us/compact-downloads.aspx
The other downloads on the web page are not relevant to this chapter, so we will leave them out for now. •
Run the downloaded file on your development machine and follow through the setup wizard to the end.
[ 35 ]
Building the Data Tier
Installing SQL Server CE on the Pocket PC device.
Now that we have installed SQL Server CE on the development machine, we will try to get a copy of the database engine running on the Pocket PC device. There are a two ways to get Microsoft SQL Server Compact installed on the Pocket PC device: •
MS Visual Studio automated deployment: If you have added a reference to SQL Server CE in your project, the SQL Server CE database engine will be automatically deployed to the mobile device when you run or debug your application from inside Microsoft Visual Studio.
•
Manual deployment: We can also manually copy the relevant SQL Server CE .cab files to the mobile device and deploy it directly from the device.
We will explore the second approach in the later section. The installation of SQL Server CE earlier creates a bunch of .cab files in the following location: \Program Files\Microsoft SQL Server Compact Edition\v3.5\Devices\ wce500\armv4i
Let's take a look at the SQL Server CE .cab files in detail. You will notice that there are generally three types of files: Feature sqlce.device.platform.processor
Description
sqlce.repl.device.platform.processor
This .cab file contains SQL Server CE replication functionality
sqlce.dev.language.device.platform. processor
This .cab file contains the developer tools such as the Query Analyzer 3.5
This .cab file contains the SQL Server CE database engine
To manually install these .cab files to your mobile device, follow these instructions: 1. Copy the three files (sqlce.dev.*.cab, sqlce.repl.*.cab, and sqlce.*.cab) that correspond to your target device, operating system, and processor to any folder on the mobile device manually. (Example: For English-based Windows Mobile 5/6 Pocket PC devices, the file extension would be ENU.ppc.wce5.armv4i)
[ 36 ]
Chapter 2
2. Run the sqlce.*.cab file from your mobile device or device emulator first, and the other two .cab files after that in any order. 3. In each installation a …successfully installed message will be displayed if the setup did not encounter any problems. SQL Server CE 3.1 and Windows Mobile 6 It is interesting to note that Windows Mobile 6 devices and emulators have the SQL Server CE 3.1 database engine already prebuilt into their ROM. This means that a manual database engine install will not be required if you are building your applications on top of SQL Server CE 3.1. SQL Server CE 3.1 has some limitations, mainly in that it cannot utilize the rich data features in Microsoft Visual Studio 2008. These rich data features include the Visual Database Tools, Data Source Configuration Wizard, and Data Designer Tools. The examples in this book will all be based on SQL Server CE 3.5.
Setting up Oracle Lite 10g
The Oracle Lite 10g database is Oracle's equivalent to Microsoft's SQL Server CE. It is provided free of charge, and can also sync with a standard Oracle database server. The Oracle Lite database consists of two main components: Mobile Server and the Mobile Development Kit.
Installing Oracle Lite 10g on the development machine To install Oracle Mobile server on your development machine, simply follow these instructions:
1. Download Oracle Database Lite 10g Release 3 (10.3.0.2.0) for Microsoft Windows (32-bit) from the following URL: http://www.oracle.com/technology/software/products/lite/ index.html.
2. Extract the contents of the downloaded file into any folder on your development machine and run the setup. 3. Install only the Oracle Mobile Development Kit. We will not need to install the Mobile Server at the moment (as this will require the installation of the full Oracle database). [ 37 ]
Building the Data Tier
Installing Oracle Lite 10g on the Pocket PC device
You will find two .cab files if you navigate to the folder below (where ORAHOME is the root location where you've installed Oracle Lite): \ORAHOME\Mobile\Sdk\wince\ppc60\cabfiles Feature
Description
olite.language.device.processor
This .cab file contains the main Oracle Lite 10g database engine
tools.language.device.processor
This .cab file contains the developer tools for Oracle Lite 10g such as Msql
There are two ways to get Oracle Lite 10g installed on the mobile device: Via Mobile Server: Oracle Mobile Server provides a way to invoke the setup of the Oracle database engine on the mobile device over ActiveSync. We will revisit Mobile Server in detail when we discuss data synchronization in Chapter 6, Data Synchronization. • Manual deployment: We can also manually copy the olite.language.device.processor.cab file and the tools.language.device.processor.cab file to the mobile device and deploy it directly from the device. •
We will use the second method of the previous section to install the .cab file to your mobile device: 1. Copy the olite.language.device.processor.cab and the tools.language.device.processor.cab files that correspond to your target language, device, and processor to any folder on the mobile device manually. 2. Run the .cab file from your mobile device or device emulator. 3. A …successfully installed message will be displayed if the setup did not encounter any problems.
Building the data tier
Now that you have both databases set up on your mobile device, you're ready to build the data tier. Let's take a first-hand look at the various Visual Studio projects you are going to build. There are four different Smart Device projects (all hosted within the same Visual Studio solution). [ 38 ]
Chapter 2
Let's take a look at an overview of these projects: Project name
Description
CRMLiveFramework
This is a light-weight Class Library project that contains the basic classes (PluginManager, PluginCollection, IDataLibPlugin) needed throughout the other projects. It is expected to be referenced by almost all the other projects.
SalesForceApp
This is the main Application project—the sales force application itself. It will hold all the forms (UI) and logic for the sales force application. For this chapter, we will only build the forms related to setting up and configuring the various database plugins.
SQLServerPlugin
This is the actual SQL Server CE plugin. It is a Class Library project that contains all the functions necessary to process SQL Server CE database access requests.
OracleLitePlugin
This is the Oracle Lite plugin. Just like the SQLServer plugin, it is a Class Library project that contains all the functions needed for Oracle Lite database access requests.
Building the CRMLiveFramework project
This project is meant to be a lightweight Class Library project (that, when compiled, produces a .DLL file). It contains all the basic, common, and shared classes that will need to be used throughout the Sales Force solution. It is therefore expected to be referenced by all other projects. You will be adding more classes and functionality to the CRMLiveFramework project as you progress through the book. In this chapter though, it will only contain the following items: Item name
Description
IDataLibPlugin interface
The IDataLibPlugin interface provides the rest of the sales force application with a consistent set of methods to retrieve data from the SQL Server CE and Oracle Lite databases without having to worry about its underlying implementation.
PluginCollection class
This class inherits from the System.Collections.Hashtable class. It is used to hold a list of plugin objects that implement the IDataLibPlugin interface.
PluginManager class
This class contains the functions to install and remove plugins from the framework. It will also handle the persisting of plugin information to disk (in the form of XML) and vice versa.
[ 39 ]
Building the Data Tier
Let's start by launching the Visual Studio application and creating a new Smart Device project. Enter CRMLiveFramework as the name of this project and click OK to proceed. You will be prompted with another window as shown in the following screenshot. Do ensure that you have chosen the Class Library project type, Windows Mobile 6 Professional SDK as the Target platform, and the .NET Compact Framework Version 3.5 as the target framework before continuing.
As a last step, you need to change the namespace of the project to a more consistent one. To do this, navigate to the Project | CRMLiveFramework Properties menu item and launch it. Change the Root namespace value to CRMLive. We will establish CRMLive as the namespace for all the projects in the Sales Force solution.
After this is done, you have created your very first Smart Device project! We will proceed to create some classes for this project in the next section.
Defining the IDataLibPlugin interface
At the heart of the data tier lies the IDataLibPlugin interface. It contains a set of method definitions that define how the sales force application communicates with the SQL Server CE and Oracle Lite databases.
[ 40 ]
Chapter 2
An interface is a collection of method definitions (without the implementation). A class that implements an interface has to provide implementations for each and every defined method of the interface. Interfaces are great for defining a consistent set of behavior that different classes have to provide, but leaves the details of the implementation to these classes.
Add a new Interface item to your CRMLiveFramework project by launching the Project | Add New Item menu item and choosing Interface as the item type. Name your Interface IDataLibPlugin when prompted. Let us take a look at the various methods we will define in this interface: public interface IDataLibPlugin { //========================================================= //The following properties are required for all plugins //so we define them in the interface //========================================================= string Datasource { get; set; } string PluginPath { get; set; } bool Active { get; set; } string PluginFullName { get; } //========================================================= //This method opens a connection to the database designated //by the Datasource property //========================================================= bool ConnectDatabase(); //=========================================================
[ 41 ]
Building the Data Tier //This method closes the connection opened via //ConnectDatabase() //========================================================= bool DisconnectDatabase(); //========================================================= //The CreateSalesForceDatabase() method defines a function //that generates an entirely new and empty Sales Force //database //========================================================= bool CreateSalesForceDatabase(); //========================================================= //The GetAccountDetails() method defines a function that //takes in an AccountGUID value and returns a Dataset //object containing the full details of the account //========================================================= DataSet GetAccountDetails(Guid AccountGUID); //========================================================= //The SetAccountDetails() method defines a function that //takes in a Dataset object (containing the updated account //details). The function must then write the updated //details to the underlying database //========================================================= bool SetAccountDetails(Guid AccountGUID, DataSet Account); //========================================================= //The RemoveAccount() method removes an account record and //all its related child records //========================================================= bool RemoveAccount(Guid AccountGUID); //========================================================= //The AccountExists checks if an account with the same //first name and last name already exists //========================================================= bool AccountExists(string FirstName, string LastName, Guid AccountGUID); //========================================================= //The ChangeAccountType() method defines a function that //changes the type of an account between a Lead, //Opportunity and Customer //========================================================= bool ChangeAccountType(Guid AccountGUID, int Type); [ 42 ]
Chapter 2 //========================================================= //The GetProductList() method retrieves the full list of //records from the Products table as a Dataset //========================================================= DataSet GetProductList(); //========================================================= //The GUIDToNative() method converts a .NET System.GUID //object into the database's native data type used to store //GUID values //========================================================= object GUIDToNative(Guid AccountGUID); //========================================================= //The NativeToGUID() method does the opposite – it converts //the database's native data type for GUID into a .NET //System.GUID object //========================================================= Guid NativeToGUID(object AccountGUID); }
What goes in an interface? As a rule of thumb, two functions that achieve the same output but do so in different ways are good candidates to include in an interface. For instance, consider the GetAccountDetails() method we used in the previous section. We know that regardless of the database used, if we pass in the GUID of an account, it will return a Dataset containing the matching account details. How this data retrieval is done in SQL Server CE and Oracle Lite, however, is entirely different; SQL Server CE uses the SqlCeCommand class, whereas Oracle Lite uses the OracleCommand class.
With that done, you will now proceed to build the Plugin Manager UI.
Building the Plugin Manager UI
The SalesForceApp project contains forms, which would usually be regarded as part of the presentation and logic-level tier, and so it may seem a little strange that we're covering this in a data tier chapter. The forms that you will be particularly looking at, however, have a lot to do with the managing, installing, and registering of the database plugins that you have created, and so constitute an important part of the data tier. Let us take a quick look at what these forms are supposed to do.
[ 43 ]
Building the Data Tier
An overview
When we first launch our SalesForceApp project, we will encounter the form as in the following screenshot. This form allows the user to install or remove a plugin. For instance, if the user wanted to have Oracle Lite and SQL Server CE support on his mobile device, he or she would have to install both the OracleLitePlugin and SQLServerPlugin plugins in this window.
When a user clicks on the New plugin button, this will open the window shown in the following screenshot. The user will have to key in the path of the plugin DLL (or browse for the file using the button next to the field). Upon selecting a plugin that conforms to the IDataLibPlugin interface, the plugin's full name will be shown in the same window. The user would then be required to specify the connection settings to an existing database in the Plugin datasource field. When installing a plugin for the first time, the sales force database does not exist. A Create now button will, therefore, be provided to allow the user to generate a new sales force database (together with its schema) on the mobile device. After that, it will automatically point the Plugin datasource field to this newly created database. When this has been done, the user will need to click on the Save button to finally install this plugin.
[ 44 ]
Chapter 2
Although the user can add multiple database plugins to his or her mobile device, only one plugin can be active at any one time. The user can select the active plugin by placing a tick in the checkbox next to the plugin, as shown in the following screenshot:
Before you start to create the forms shown previously, you will first need to create a couple of classes—a PluginCollection class to store this list of plugins and a PluginManager class to manage that list. Both classes will be created in the CRMLiveFramework project.
[ 45 ]
Building the Data Tier
Implementing the PluginCollection class
The PluginCollection is a small class that inherits from the System.Collections. HashTable object. The purpose of this class is to maintain a list of IDataLibPlugin objects. You need to inherit from the System.Collections.HashTable class because it allows you to refer to a stored object in its list using the plugin's full name. Add a new class to the CRMLiveFramework project and name it PluginCollection: public class PluginCollection : Hashtable { //========================================================= //This Add() function allows us to add a plugin object //to the collection, using the plugin name as the key //========================================================= public IDataLibPlugin Add(IDataLibPlugin PluginObject) { if (base.Contains(PluginObject.PluginFullName) == false) { base.Add(PluginObject.PluginFullName, PluginObject); return PluginObject; } else { return null; } } }
Implementing the PluginManager class
The PluginManager class will make use of a single instance of the PluginCollection object to hold a list of plugins that are installed on the framework. It will also contain functions that handle the installation of new plugins or the removing of existing ones. More importantly, this class will also need to be able to persist the PluginCollection to disk and to also load it back from disk. This class is expected to use .NET reflection and XML services, so you will need to import the references to these libraries. Let's take a look at the following skeleton code of this class: using System.Reflection; using System.Xml; public class PluginManager { [ 46 ]
Chapter 2 private PluginCollection _plugins; //========================================================= //This function simply returns the _plugins collection //========================================================= public PluginCollection GetPluginsList() { return _plugins; } //========================================================= //This function accepts a plugin name and returns the //corresponding plugin object from the _plugins collection //========================================================= public IDataLibPlugin GetPluginObject(string PluginName) { return _plugins.Item(PluginName); } }
Let's create an AddPlugin function that will load a DLL given its path, and add an instance of the plugin to the PluginCollection object. The AddPlugin function takes in the full path of the DLL plugin file and the plugin data source (connection string). You will use .NET reflection to dynamically load the DLL and create an instance of the plugin class. public IDataLibPlugin AddPlugin(string PluginPath, string PluginDatasource, bool Active) { Assembly _AssemblyObject; IDataLibPlugin _PluginObject; _AssemblyObject = Assembly.LoadFrom(PluginPath); _PluginObject = (CRMLive.IDataLibPlugin) (_AssemblyObject.CreateInstance("CRMLive.PluginClass")); _PluginObject.Datasource = PluginDatasource; _PluginObject.PluginPath = PluginPath; _PluginObject.Active = Active; _plugins.Add(_PluginObject); return _PluginObject; }
The Assembly class is part of the System.Reflection namespace. It provides the functions that allow you to load and instantiate a class contained in an external library file.
[ 47 ]
Building the Data Tier
Let's also create the opposite function, the RemovePlugin function. Its job is to simply remove the desired plugin from the PluginCollection object: public void RemovePlugin(string PluginName) { _plugins.Remove(PluginName); }
Your PluginManager class will also need to persist the PluginCollection to disk. You will create a SaveAllPlugins function that serializes the list of plugins in PluginCollection into XML and then saves it to the plugins.xml file. For this purpose, you will use the System.Xml library. The three important attributes of each plugin that you need to save to disk are: •
Plugin path
•
Data source
•
Status of the plugin (whether it is active or not)
public void SaveAllPlugins() { int _counter = 0; IDataLibPlugin _pluginObject; string _activePluginName = ""; XmlDocument _xmlDoc; XmlElement _xmlRoot; XmlElement _xmlPlugin; _xmlDoc = new XmlDocument(); _xmlRoot = _xmlDoc.CreateElement("Root"); _xmlDoc.AppendChild(_xmlRoot); foreach (IDataLibPlugin _pluginObject in _plugins.Values) { _xmlPlugin = _xmlDoc.CreateElement("Plugin"); _xmlPlugin.SetAttribute("PluginPath", _pluginObject.PluginPath); _xmlPlugin.SetAttribute("Datasource", _pluginObject.Datasource); _xmlPlugin.SetAttribute("Active", (string)_pluginObject.Active); _xmlRoot.AppendChild(_xmlPlugin); } _xmlDoc.Save("plugins.xml"); } [ 48 ]
Chapter 2
Now that you can save the collection to disk, you will need to create a LoadAllPlugins function that does the opposite task. It reads XML from the plugins.xml file and reconstructs the PluginCollection object using stored plugin information: public void LoadAllPlugins() { string _pluginPath = null; string _pluginDatasource = null; bool _active = false; int _counter = 0; XmlDocument _xmlDoc; XmlElement _xmlPlugin; XmlElement _xmlRoot; _plugins = new PluginCollection(); _xmlDoc = new XmlDocument(); try { _xmlDoc.Load("plugins.xml"); } catch (Exception ex) { return; } _xmlRoot = (System.Xml.XmlElement) _xmlDoc.ChildNodes.Item(0); for (_counter = 0; _counter <= _xmlRoot.ChildNodes.Count1; _counter++) { _xmlPlugin = (System.Xml.XmlElement) _xmlRoot.ChildNodes.Item(_counter); _pluginPath = _xmlPlugin.GetAttribute("PluginPath"); _pluginDatasource = _xmlPlugin.GetAttribute ("Datasource"); _active = bool.Parse(_xmlPlugin.GetAttribute("Active")); AddPlugin(_pluginPath, _pluginDatasource, _active); } }
[ 49 ]
Building the Data Tier
You're almost done. There is still one thing you need to do, which is to have your PluginManager class automatically call the LoadAllPlugins() function when it loads up. You can do this in the constructor of your class: public New() { //Load all the plugins from file to memory LoadAllPlugins(); }
Creating the SalesForceApp project
Now it's time for you to build the Plugin Manager UI. Add a new project to the existing solution and name the project SalesForceApp. Choose Smart Device Project as the project type and use the Device Application template when prompted. Lastly, add a reference to the CRMLiveFramework project and change the namespace of the project to CRMLive. You will need to have a global class to store a global PluginManager object that can be used throughout the SalesForceApp project. Add a new class named GlobalArea to your project: public class GlobalArea { private static PluginManager _PluginManager = new PluginManager(); public static PluginManager PluginManager { get { return _PluginManager; } } }
Building the ConfigurePlugin form
The ConfigurePlugin form is a dialog window that lets the user choose a plugin DLL and then subsequently allows him or her to set its data source (connection string). It also allows the user to create a new database if one does not exist. Navigate to the Project | Add New Item menu item and create a new item based on the Windows Form template. Name this form ConfigurePlugin. The ConfigurePlugin form uses the following controls:
[ 50 ]
Chapter 2
Control name txtLibraryPath
Description
btnLibraryPathBrowse
This button makes use of .NET's OpenFileDialog control to present a file selection window to the user.
ofdFileBrowser
This is a .NET OpenFileDialog control that you will need to drop onto your form.
lblPluginFullName
This is a label control that will display the full name of the plugin selected by the user using the btnLibraryPathBrowse button.
txtDatasource
This text control allows the user to key in the full data source (connection string) to an existing database if one exists. Similarly the user can use the adjacent btnDatasourceBrowse button to search for the database file directly.
btnDatasourceBrowse
This button makes use of .NET's OpenFileDialog control as well to present a file selection window to the user.
btnCreateDatabase
The Create Now button requests the plugin to generate a new database (together with schema) on the mobile device.
pnlPluginDetails
This is a panel that is initially invisible. Once a valid DLL plugin has been selected by the user, this panel will be made visible.
btnSave and btnCancel
We make use of the menu bar at the bottom of the form to provide a Save and Cancel feature.
This is the full path to the .DLL plugin library. The user can manually key in the full path or browse for the .DLL file through the btnLibraryPathBrowse button next to the control.
[ 51 ]
Building the Data Tier
We will now write code to imbue functionality to the form. Before we proceed further, let's take a brief look at the skeleton code for this preceding class. The _PluginObject variable will hold a live instance of the plugin when its respective DLL file has been selected by the user. using System.Reflection; public class ConfigurePlugin { private string _PluginDatasource; private string _PluginLibPath; private IDataLibPlugin _PluginObject; public string PluginDatasource { get { return _PluginDatasource; } set { _PluginDatasource = value; } } public string PluginLibPath { get { return _PluginLibPath; } set { _PluginLibPath = value; } } //========================================================= //The Save function saves the user's input and closes the //form with an 'OK' dialog result //========================================================= private void Save(System.Object sender, System.EventArgs e) { _PluginDatasource = txtDatasource.Text; _PluginLibPath = txtLibraryPath.Text; this.DialogResult = Windows.Forms.DialogResult.OK; this.Close(); } //========================================================= //The Cancel function ignores the user's input and simply //closes the form with a 'Cancel' dialog result //========================================================= private void Cancel(System.Object sender, System.EventArgs e) { this.DialogResult = Windows.Forms.DialogResult.Cancel; this.Close(); } } [ 52 ]
Chapter 2
Now, let's consider what happens when the user clicks on the btnLibraryPathBrowse button. This button should launch an OpenFileDialog control so that users can search for their DLL plugin files. Once located, it should also attempt to load an instance of the plugin class, and then access its full name through the IDataLibPlugin interface. You can achieve the above using the following piece of code: private void LibraryPathBrowse(System.Object sender, System.EventArgs e) { Assembly _AssemblyObject; ofdFileBrowser.Filter = "DLL files(*.dll)|*.dll"; ofdFileBrowser.FileName = txtLibraryPath.Text; if (ofdFileBrowser.ShowDialog == Windows.Forms.DialogResult.OK) { txtLibraryPath.Text = ofdFileBrowser.FileName; try { _AssemblyObject = Assembly.LoadFrom (txtLibraryPath.Text); _PluginObject = _AssemblyObject.CreateInstance ("CRMLive.PluginClass"); lblPluginFullName.Text = _PluginObject.PluginFullName; pnlPluginDetails.Visible = true; } catch (Exception ex) { MessageBox.Show(ex.Message, "Loading Plugin", MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1); } } }
[ 53 ]
Building the Data Tier
Next you will look at what happens when the user clicks on the btnCreateDatabase button. The idea of this function is to use the IDataLibPlugin interface to call the plugin's CreateSalesForceDatabase method. Depending on whether the Oracle Lite plugin or SQL Server CE plugin was loaded, the appropriate CreateSalesForceDatabase implementation would be called. You can achieve this using the following code snippet: private void CreateDatabase(System.Object sender, System.EventArgs e) { if (_PluginObject.CreateSalesForceDatabase() == true) { txtDatasource.Text = _PluginObject.Datasource; MessageBox.Show("Database successfully created", "Create database"); } }
Building the PluginsSetup form
The PluginsSetup form is the main startup form of the SalesForceApp project. The purpose of this form is to provide plugin management functionality. You can create the PluginsSetup form as shown in the following screenshot by navigating to the Project | Add New Item menu item and creating a new item based on the Windows Form template. The PluginsSetup form uses the following controls: Control name btnNewPlugin
Description
btnRemovePlugin
The Remove button that allows the user to remove the currently selected plugin from the list
lvPlugins
A ListView control that has its Checkboxes property set to true. It contains only a single text column displaying the name of the plugin
btnSave and btnCancel
We make use of the menu bar at the bottom of the form to provide a Save and Cancel feature
The New plugin button that allows the user to install a new plugin
[ 54 ]
Chapter 2
Let's take a look at the following skeleton code for this class. This form contains a single instance of the PluginManager class that you've created earlier, which handles all plugin-related logic. public class PluginsSetup { //======================================================== //The Save function on this form will write the full list //of plugins in memory (held in GlobalArea.PluginManager) //to disk via the SaveAllPlugins() method call //========================================================= private void Save(System.Object sender, System.EventArgs e) { GlobalArea.PluginManager.SaveAllPlugins(); this.DialogResult = Windows.Forms.DialogResult.OK; this.Close(); } //========================================================= //The cancel button simply closes the form (without saving //the list of plugins to disk) //========================================================= private void Cancel(System.Object sender, System.EventArgs e) { this.DialogResult = Windows.Forms.DialogResult.Cancel; this.Close(); [ 55 ]
Building the Data Tier } //========================================================= //When we launch this form, we will automatically populate //the lvPlugins ListView with the list of plugins in //GlobalArea.PluginManager //========================================================= public PluginsSetup() { // This call is required by the Windows Form Designer. InitializeComponent(); RefreshPluginsList(); } }
Let's take a look at how you can implement this RefreshPluginsList() function. By directly retrieving the PluginCollection object from the PluginManager, you can iterate through this collection and add the names of each plugin to the lvPlugins Listview control: private void RefreshPluginsList() { PluginCollection _pluginsCollection; IDataLibPlugin _pluginObject; int _counter = 0; ListViewItem _listviewItem; lvPlugins.Items.Clear(); _pluginsCollection = GlobalArea.PluginManager.GetPluginsList(); foreach (IDataLibPlugin _pluginObject in _pluginsCollection.Values) { _listviewItem = new ListViewItem(_pluginObject.PluginFullName); _listviewItem.Checked = _pluginObject.Active; lvPlugins.Items.Add(_listviewItem); } }
[ 56 ]
Chapter 2
Next we will explore what you need to do when the user clicks on btnNewPlugin. This button must launch the ConfigurePlugin form that you've created earlier. If ConfigurePlugin returns an OK dialog result, we will add the plugin to the PluginManager object. You can perform the operation with the following code. Take note that after adding the plugin, you can refresh the Listview control by issuing another call to RefreshPluginsList(). private void Newplugin(System.Object sender, System.EventArgs e) { ConfigurePlugin _ConfigurePlugin; _ConfigurePlugin = new ConfigurePlugin(); if (_ConfigurePlugin.ShowDialog() == Windows.Forms.DialogResult.OK) { GlobalArea.PluginManager.AddPlugin (_ConfigurePlugin.PluginLibPath, _ConfigurePlugin.PluginDatasource, true); RefreshPluginsList(); } _ConfigurePlugin.Dispose(); _ConfigurePlugin = null; }
You can also remove a plugin via the code shown as follows: private void RemovePlugin(System.Object sender, System.EventArgs e) { string _pluginFullName; if (lvPlugins.SelectedIndices.Count == 0) { MessageBox.Show("Please select a plugin from the list to remove", "Remove plugin", MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1); } else { _pluginFullName = lvPlugins.Items (lvPlugins.SelectedIndices.Item(0)).Text; GlobalArea.PluginManager.RemovePlugin(_pluginFullName); RefreshPluginsList(); } } [ 57 ]
Building the Data Tier
When the user checks or unchecks the checkbox next to each plugin in the Listview control, you must set the Active flag of the plugin to either true or false. You can do so with the following code snippet: private void PluginChecked(object sender, System.Windows.Forms.ItemCheckEventArgs e) { string _selectedPluginName; IDataLibPlugin _selectedPluginObject; _selectedPluginName = lvPlugins.Items(e.Index).Text; _selectedPluginObject = GlobalArea.PluginManager.GetPluginObject (_selectedPluginName); if (e.NewValue == CheckState.Checked) { _selectedPluginObject.Active = true; } else { _selectedPluginObject.Active = false; } }
Testing what you've built so far
At this point, you can try running your first .NET CF application! Run the SalesForceApp project. Visual Studio will build your project and prompt you to choose a target device to run the application (shown in the following screenshot):
[ 58 ]
Chapter 2
You should test your applications using the Windows Mobile 6 Professional Emulator. Choose this item and click Deploy. Your application files will be copied over to the emulator and run automatically. You should now be able to see the PluginsSetup form you've created earlier. Try clicking the New Plugin button to see the ConfigurePlugin form. You can also see the OpenFileDialog window in action by clicking the button next to the Plugin library path, though at this point you won't have any plugins to load yet. In the next section, you will learn how to build these plugins.
Building the database plugins
The two plugin projects (SQLServerPlugin and OracleLitePlugin) are standalone Class Library projects. They are compiled as DLL files and are meant to be deployed separately from the main sales force application. They are simply 'plugged in' to the framework when required. Both plugins implement the IDataLibPlugin interface that you've created earlier. Let's start by building the SQLServerPlugin project. Within the same CRMLiveFramework project, navigate to the File | Add | New Project menu item. Use the Smart Device Project type and Class Library template and create a project with the name SQLServerPlugin. Like before, change the namespace of the project to CRMLive. Your plugin would need to implement the IDataLibPlugin interface, so you need to add a reference to the CRMLiveFramework project. As both projects are created in the same solution, you can easily do this by navigating to the Projects tab in the Add Reference window and selecting the CRMLiveFramework project.
[ 59 ]
Building the Data Tier
In addition to this, we also need to add a reference to the System.Data. SqlServerCe library under the .NET tab in the same preceding window.
Implementing the SQL Server CE Plugin
Visual Studio provides a Resource Editor that allows you to store resources such as strings, bitmaps, and icons in an embedded resource file in your project. We can make use of this facility to store DDL (Data Definition Language) such as the CREATE TABLE statements in a resource file.
Storing DDL in the resource file
You can add a resource file to your project by navigating to the Project | Add New Item menu item and adding a new item based on the Resources File template. Name your resource TableSchema. Upon doing so, you will be prompted with the resource editor window, from which you can add your CREATE TABLE statements.
Let's add some of the SQL statements below to the resource editor. In the next section, we will explore how we can use .NET's Resourcemanager class to retrieve these SQL statements from the resource file. The UniqueIdentifier field type In the SQL code, we assign a UNIQUEIDENTIFIER field type to the primary key of the Accounts table. A UNIQUEIDENTIFIER is a 16-byte value that is guaranteed to be globally unique. You can generate this value in SQL using the NEWID() function or programmatically using System.Guid.NewGuid().
[ 60 ]
Chapter 2
Name Accounts_SQL
Description CREATE TABLE Accounts ( AccountGUID NOT NULL,
UNIQUEIDENTIFIER UNIQUE
AccountType
INT,
DateCreated
DATETIME,
FirstName
NVARCHAR(50),
LastName
NVARCHAR(50),
Status
INT,
Reception
INT,
Source
INT,
ResPhoneNo
NVARCHAR(50),
MobPhoneNo
NVARCHAR(50),
EmailAddress
NVARCHAR(100),
Street
NVARCHAR(255),
City
NVARCHAR(50),
State
NVARCHAR(50),
Zipcode
NVARCHAR(10),
Country
NVARCHAR(50),
Website
NVARCHAR(50),
InterestedProds
NVARCHAR(255),
OwnerID
NVARCHAR(255),
PRIMARY KEY(AccountGUID) AccountHistories_SQL
) CREATE TABLE AccountHistories ( AccountGUID
UNIQUEIDENTIFIER NOT NULL,
HistoryID
INT IDENTITY(1,1),
Originator
INT,
Subject
NVARCHAR(255),
Description
NTEXT,
Timestamp
DATETIME,
PRIMARY KEY(HistoryID) ) [ 61 ]
Building the Data Tier
Name
Description
AccountFiles_SQL
CREATE TABLE AccountFiles ( AccountGUID
UNIQUEIDENTIFIER NOT NULL,
AttachmentID
INT IDENTITY(1,1),
AttachmentName
NVARCHAR(255),
AttachmentSize
INT,
Attachment
NVARCHAR(255),
PRIMARY KEY(AttachmentID) AccountTasks_SQL
) CREATE TABLE AccountTasks ( AccountGUID
UNIQUEIDENTIFIER NOT NULL,
TaskID
INT IDENTITY(1,1),
TaskSubject
NVARCHAR(255),
TaskDescription
NTEXT,
TaskCreated
DATETIME,
TaskDate
DATETIME,
TaskStatus
INT,
PRIMARY KEY(TaskID) Products_SQL
) CREATE TABLE Products ( ProductID
INT IDENTITY(1,1),
ProductCode
NVARCHAR(5),
ProductName
NVARCHAR(100),
ProductPrice
MONEY,
PRIMARY KEY(ProductID) )
[ 62 ]
Chapter 2
Building the plugin class
We will now add the main plugin class to the project. Navigate to Project | Add New Item and add a new class named PluginClass to your project. You will first need to add a reference to the System.Data.SqlServerCE library. This library contains all the managed classes you will need to connect to SQL Server CE. Include the following imports at the top of your class: using using using using using
CRMLive; System.Data; System.Data.SqlServerCe; System.Resources; System.Reflection;
Now that we have all the library references we need, we can implement the IDataLibPlugin interface: public class DataLibPlugin : IDataLibPlugin { }
Upon typing this line of code, Visual Studio will automatically generate the entire skeleton of the interface in your class. We will need to implement some of the basic methods of the interface. The PlugInFullName method for instance, allows external code to retrieve the full name of the plugin. We can implement this function quite easily below: public string PluginFullName { get { return "SQL Server Compact 3.5 Plugin"; } }
We will also implement the Datasource, Active, and PluginPath properties of the interface. These three properties are used at run-time and need to be stored in the instance of the object. We simply create a member variable for each of these properties and expose them through the property: private string _connectionString; private string _pluginPath; private bool _active; public string Datasource { get { return _connectionString; } set { _connectionString = value; } }
[ 63 ]
Building the Data Tier public bool Active { get { return _active; } set { _active = value; } } public string PluginPath { get { return _pluginPath; } set { _pluginPath = value; } }
Connecting to SQL Server CE
Microsoft provides two different providers to connect to the SQL Server Compact database: •
Microsoft .NET Compact Framework Data Provider for SQL Server Mobile (SqlCeConnection)
•
Microsoft .NET Compact Framework Data Provider for OLEDB (OleDbConnection) It is recommended to use SqlCeConnection whenever possible, as these are native SQL Server CE providers. The OledbConnection provider is a generic provider that will perform data type conversions and other operations under the hood and so its performance will be slower than that of SqlCeConnection.
You can connect to your SQL Server CE database using the following code: SqlCeConnection _connection; _connection = new SqlCeConnection( "Data source='Salesforce.sdf';Password=admin123;"); _connection.Open(); . . . _connection.Close(); _connection.Dispose(); _connection = null;
[ 64 ]
Chapter 2
Programmatically creating the SQL Server CE database
Let's take a look at the CreateSalesForceDatabase method. This method is expected to create a database named SalesForce in SQL Server CE and to generate all the sales force application tables automatically. On implementing this function, we will employ the use of the following technologies: •
We will use the SqlCeEngine class to generate the new database.
•
We will use the SqlCeConnection class to connect to the database.
•
We will store all the SQL statements (that define the table schemas) in a Visual Studio generated resource file and retrieve them during realtime.
•
We will make use of .NET reflection to obtain the current folder path.
In the CreateSalesForceDatabase implementation, we first need to declare a few function-scope variables: SqlCeEngine _engine; SqlCeConnection _connection; SqlCeCommand _command; resourcemanager _ResourceManager; string _dbcreationstring; string _SDFPath; bool _dbcreationsuccess = false;
Next, we will use .NET reflection to retrieve the current path. We will use that to build your full connection string: _SDFPath = System.IO.Path.GetDirectoryName(Assembly. GetExecutingAssembly().GetName().CodeBase) + "\\salesforce.sdf"; _dbcreationstring = "Data Source='" + _SDFpath + "';LCID=1033;Password ='admin123';Encrypt=FALSE;"
We then use the SqlCeEngine class to create the database using the connection string you've built earlier. We put this whole block of code under a Try…Catch block so that if this part of the code fails, we will simply exit the function: _engine = new SqlCeEngine(_dbcreationstring); try { _engine.CreateDatabase(); _dbcreationsuccess = true; } catch (Exception ex) { [ 65 ]
Building the Data Tier MessageBox.Show(ex.Message, "Create database", MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1); } finally { _engine.Dispose(); } if (_dbcreationsuccess == false) return false;
Now that the database .SDF file has been created, we will attempt to connect to it and execute the SQL statements stored in the TableSchema.resx resource file to build the entire schema required for the sales force application. We start by first opening a connection to your newly created database: _connectionString = "Data source='" + _SDFPath + "';Password=admin123;"; _connection = new SqlCeConnection(_connectionString); try { _connection.Open(); } catch (Exception ex) { MessageBox.Show(ex.Message, "Connecting to database", MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1); return false; }
We latch .NET's ResourceManager class on to your TableSchema.resx resource file. You will need to provide the full base name for your resource file, which is CRMLive. TableSchema: _ResourceManager = new ResourceManager("CRMLive.TableSchema", Assembly.GetExecutingAssembly());
[ 66 ]
Chapter 2
Lastly, we create the SqlCeCommand object to run each of the SQL statements retrieved from the resource file. This will create all the tables we need for the application: _command = _connection.CreateCommand(); _command.CommandText = _ResourceManager.GetString("Accounts_SQL"); _command.ExecuteNonQuery(); _command.CommandText = _ResourceManager.GetString("AccountTasks_SQL"); _command.ExecuteNonQuery(); _command.CommandText = _ResourceManager.GetString("AccountHistories_ SQL"); _command.ExecuteNonQuery();
Also don't forget to close and dispose of the database connection at the end: _connection.Close(); _connection.Dispose(); _connection = null; return true;
Testing what you've done so far
It is now time to test your plugin! Compile your plugin project. This will output a DLL assembly. Copy the output DLL assembly to a folder on your mobile device. Alternatively, you can also add a project reference to SQLServerPlugin inside your SalesForceApp project. This will ensure that the DLL generated from the SQLServerPlugin project will always be deployed together with your SalesForceApp application when you run it. You can do so by right-clicking your SalesForceApp project, navigating to the Projects tab in your Add Reference menu, and selecting the SQLServerPlugin project.
[ 67 ]
Building the Data Tier
Run the SalesForceApp application. Click the New Plugin button, and then browse for your plugin DLL. Once you've selected it, the name of the plugin will appear. Without filling in anything else, click the Create now button. This will call the create CreateDatabase method of your plugin to create the SQL Server database.
Browsing the SQL Server CE database with Query Analyzer
When the SQL Server CE product is installed on your mobile device, it installs a tool called the Query Analyzer. You can think of it as a useful lightweight equivalent of the SQL Server Enterprise Manager tool. In this section, we will explore how we can use the Query Analyzer to browse the database and tables you've generated earlier. When you install the sqlce.dev.* .cab file, a tool called the Query Analyzer 3.5 is created in your device's Programs window. This tool is represented by the icon below:
After you launch the Query Analyzer tool, you will be presented with a window containing four tabs. The first tab (Objects) allows you to see the databases registered with the tool. You can register a database by clicking the button highlighted in the red box as follows:
[ 68 ]
Chapter 2
This will open up a window that allows you to browse for your database file (with the extension .SDF). Locate this file on your mobile device, and type in the password (which you've designated as admin123 at the moment). Click the Connect button to connect to the database.
Once you've done this, you will be able to see the list of tables shown in the main panel of the tool. You can browse the columns of each table by expanding the Columns folder.
You can also run SQL queries against the database via the SQL tab. The Query Analyzer tool is handy for situations when you need to debug your application. For example, you might want to check the database to see if data was correctly inserted from your application.
[ 69 ]
Building the Data Tier
Retrieving data from SQL Server CE
Let's revisit the SQLServerPlugin project. You will now explore how you can add functionality to retrieve data from the database you've created into an ADO.NET dataset object. The GetAccountDetails() method's job is to take in the unique identifier of a particular account (AccountGUID) and then retrieve all the relevant account details (including tasks, histories, and file attachments) from the database. It will place the data from each table in a separate Datatable object in the Dataset. To begin, let's declare some of the variables you will need to use: public DataSet GetAccountDetails(Guid AccountGUID) { SqlCeConnection _connection; SqlCeDataAdapter _adapter; SqlCeCommand _command; DataSet _Resultset; }
Assuming that a connection to the database has already been opened, you are now ready to create an ADO.NET command object. You can use the CreateCommand() method in the SqlCeConnection class to generate one. After doing so, assign the SQL statement you want to execute to the command object. Create an SqlCeDataAdapter object to run the command and fill the dataset created earlier with the retrieved data. Take note that you should specify the name of the resulting Datatable object to the Fill() method. This allows you to create multiple tables containing different data in the same Dataset object: _command = _connection.CreateCommand; _command.CommandText = "SELECT * FROM Accounts WHERE AccountGUID='" + AccountGUID + "'"; _Resultset = new DataSet(); _adapter = new SqlCeDataAdapter(_command); _adapter.Fill(_Resultset, "Accounts");
Repeat the same thing for the other three related tables, filling in different Datatables in the same DataSet object: _command.CommandText = "SELECT * FROM AccountTasks WHERE AccountGUID='" + AccountGUID + "' ORDER BY TaskDate DESC"; _adapter.Fill(_Resultset, "AccountTasks"); _command.CommandText = "SELECT * FROM AccountHistories WHERE AccountGUID='" + AccountGUID + "' ORDER BY TimeStamp DESC"; _adapter.Fill(_Resultset, "AccountHistories"); [ 70 ]
Chapter 2 _command.CommandText = "SELECT * FROM AccountFiles WHERE AccountGUID='" + AccountGUID + "' ORDER BY AttachmentID DESC"; _adapter.Fill(_Resultset, "AccountFiles");
Dispose of all of the used objects and return the Dataset object from the function. _adapter.Dispose(); _command.Dispose(); _adapter = null; _command = null; return _resultset;
You will be able to test your data retrieval function in Chapter 3, Building the Mobile Sales Force Module, when you create the rest of the application.
Data manipulation in SQL Server CE
Data manipulation requires a little bit more work. The logic tier will make use of the Dataset retrieved from GetAccountDetails() to display accounts data to the end user. The user can manipulate the data in this Dataset through the Sales Force UI (for instance adding a new task or deleting a file attachment). After that, the same Dataset is passed to the SetAccountDetails() method, which updates each table accordingly. You will use the ADO.NET command objects to achieve this. Another important consideration is that because you are updating four different tables, there may be cases where the database could be partially updated (for instance, the first three tables are updated successfully, but the last one fails). You can prevent this through the use of the SqlCeTransaction class. All four updates are collectively treated as a transaction—either all four tables are updated successfully or none at all. You will see how you can implement this in the following code. We first declare the variables we will need: public bool SetAccountDetails(Guid AccountGUID, System.Data.DataSet Account) { SqlCeDataAdapter _adapter; SqlCeTransaction _transaction; }
[ 71 ]
Building the Data Tier
Assuming that a connection to the database has already been opened, the next piece of code will mark the beginning of a transaction using the BeginTransaction() method call. _transaction = _connection.BeginTransaction();
You will need to then create an ADO.NET UpdateCommand, InsertCommand, and DeleteCommand object for each table. The reason or this is that when a Dataset arrives at your SetAccountDetails method, it may contain a list of changes that include updated fields, deleted rows, or newly inserted rows. The adapter object will therefore need to be fed the corresponding UPDATE, INSERT, and DELETE SQL statements. This also gives you some flexibility in the sense that your UPDATE statement, for instance, can selectively update desired fields in the table. You can use the Parameters collection of the command object to tell the adapter object which columns in the Dataset to retrieve the data from. The adapter object will plug the actual values into your UPDATE SQL statement when it commits the data. _adapter = new SqlCeDataAdapter(); try { //========================================================= //UPDATING THE ACCOUNTTASKS TABLE //========================================================= //Here we create an Update command for the AccountTasks //table. Take note that we include this command as part of //the transaction _adapter.UpdateCommand = new SqlCeCommand("UPDATE AccountTasks SET TaskSubject=@TaskSubject WHERE AccountGUID=@AccountGUID", _connection, _transaction); _adapter.UpdateCommand.Parameters.Add("@TaskSubject", SqlDbType.NVarChar, 255, "TaskSubject"); _adapter.UpdateCommand.Parameters.Add("@TaskDescription", SqlDbType.NText, 16, "TaskDescription"); _adapter.UpdateCommand.Parameters.Add("@TaskDate", SqlDbType.DateTime, 8, "TaskDate"); _adapter.UpdateCommand.Parameters.Add("@TaskStatus", SqlDbType.Int, 4, "TaskStatus"); [ 72 ]
Chapter 2 _adapter.UpdateCommand.Parameters.Add("@TaskID", SqlDbType.Int, 4, "TaskID");
//Here we create the Delete command for the AccountTasks table _adapter.DeleteCommand = new SqlCeCommand("DELETE FROM AccountTasks WHERE TaskID=@TaskID", _connection, _transaction); _adapter.DeleteCommand.Parameters.Add("@TaskID", SqlDbType.Int, 4, "TaskID");
//Finally we create the Insert command for the AccountTasks table _adapter.InsertCommand = new SqlCeCommand("INSERT INTO AccountTasks(AccountGUID, TaskSubject, TaskDescription, TaskCreated, TaskDate, TaskStatus) " + "VALUES (@AccountGUID, @TaskSubject, @TaskDescription, GETDATE(), @TaskDate, @TaskStatus)", _globalConnection, _transaction);
//Take note that you usually need to insert a foreign key //value into child tables (like AccountTasks). You can pass //the foreign key value in this case directly into your //Insert Command _adapter.InsertCommand.Parameters.Add("@AccountGUID", AccountGUID); _adapter.InsertCommand.Parameters.Add("@TaskSubject", SqlDbType.NVarChar, 255, "TaskSubject"); _adapter.InsertCommand.Parameters.Add("@TaskDescription", SqlDbType.NText, 16, "TaskDescription"); _adapter.InsertCommand.Parameters.Add("@TaskDate", SqlDbType.DateTime, 8, "TaskDate"); _adapter.InsertCommand.Parameters.Add("@TaskStatus", SqlDbType.Int, 4, "TaskStatus"); //Perform the update on the AccountTasks table. This //method goes through all changes in the specified table
[ 73 ]
Building the Data Tier //and applies the appropriate Insert, Update and Delete commands _adapter.Update(Account.Tables("AccountTasks")); //========================================================= //REPEAT FOR ALL OTHER TABLES //========================================================= . . .
The Commit() function of the transaction object will formally commit the transaction. You may have noticed that the entire chunk of code above was placed in a try…catch block. In the event that any part of the update operation fails, it will call the Rollback() function in the transaction object to roll back all changes made to the database. _transaction.Commit(); } catch (Exception ex) { _transaction.Rollback(); } _transaction.Dispose(); _transaction=null; _adapter.Dispose(); _adapter=null;
You will be able to test data manipulation in Chapter 3 when you create the rest of the application.
Dealing with GUID values in SQL Server CE
A GUID (Globally Unique IDentifier) is a 16-byte unique number generated by the system that is guaranteed to be globally unique. It is commonly used to uniquely identify records. As you will be using GUIDs frequently in your sales force application, you need to know how they will be stored in the database. SQL Server CE and Oracle Lite handles GUID storage differently. SQL Server CE can store a GUID using the UNIQUEIDENTIFIER data type. This data type is directly compatible with the .NET Compact Framework System.Guid object. This means that explicit conversion of data types is not required when reading from or writing to a UNIQUEIDENTIFIER column.
[ 74 ]
Chapter 2
As you've read earlier, the GUIDToNative() and NativeToGUID() methods defined in the IDataLibPlugin interface are used to convert the .NET Compact Framework System.Guid object to the native format used by the database to store GUID values, and vice versa. As no explicit conversion is necessary, we can simply return the objects in the same form as they are passed in: public object GUIDToNative(System.Guid AccountGUID) { return AccountGUID; } public System.Guid NativeToGUID(object AccountGUID) { return (System.Guid)AccountGUID; }
Implementing the Oracle Lite plugin
As with the SQL Server CE plugin, Oracle DDL statements can also be stored in the resource file.
Storing DDL in the resource file
You can add a resource file to your Oracle Lite plugin project by navigating to the Project | Add New Item menu item and adding a new item based on the Resources File template. You can use the same name for your resource—TableSchema. Upon doing so, you will be prompted with the same resource editor window, from which you can add your SQL statements. Take note that Oracle Lite SQL statements vary slightly from SQL Server CE SQL statements in terms of the data types available and how certain functionality is implemented. For example, Oracle Lite does not support the UNIQUEIDENTIFIER column. To store GUID values, a binary data type (RAW) with a size of 16 bytes is used instead. Oracle Lite also does not support IDENTITY columns, but provides similar auto-increment functionality through the use of SEQUENCES.
[ 75 ]
Building the Data Tier
Let's see how the AccountTasks table you've covered earlier under SQL Server CE can be declared in Oracle: Name AccountTasks_SQL
Description CREATE TABLE AccountTasks ( AccountGUID
RAW(16) NOT NULL,
TaskID
INTEGER,
TaskSubject
VARCHAR2(255),
TaskDescription
LONG,
TaskCreated
DATE,
TaskDate
DATE,
TaskStatus
INTEGER,
PRIMARY KEY(TaskID) AccountTasksSeq_SQL
) CREATE SEQUENCE AccountTasksSeq START WITH 1 INCREMENT BY 1
The first thing you will immediately notice is that you have an additional SQL statement named AccountTasksSeq_SQL. Because Oracle Lite does not support IDENTITY columns, you need a way to make the TaskID column an auto-incrementing number. Oracle Lite allows you to create independent SEQUENCES, which is a running counter that produces a running number starting with a defined value and a defined increment value. When a record is inserted into the AccountTasks table using SQL, you will have to explicitly insert AccountTasksSeq.NEXTVAL into the TaskID column. This is how we can make use of an Oracle sequence to generate auto-incrementing numbers. INSERT INTO AccountTasks(TaskID) VALUES(AccountTasksSeq.NEXTVAL)
The other things you will notice from the SQL above are the slight differences in data type names. For example, INT data types are declared as INTEGER in Oracle Lite, whereas NTEXT data types are defined as LONG. For the sake of brevity, we will not cover all the tables again in this section. However, to run the Oracle code samples in this chapter, you will need to define and store the SQL for the rest of the tables in the resource file.
[ 76 ]
Chapter 2
Building the plugin class
The Oracle plugin class should be named PluginClass (as the SQL Server CE plugin). You will now need to add a reference to the Oracle Lite managed library. This library is located at: \ORAHOME\Mobile\Sdk\ado.net\wince\v2.x\Oracle.DataAccess.Lite.dll
This DLL is available on your development machine when you install the Oracle Lite Mobile Development Kit. After doing so, you will need to include the following imports: using using using using using
CRMLive; System.Data; Oracle.DataAccess.Lite; System.Resources; System.Reflection;
You will implement the same IDataLibPlugin interface. The first function you need to implement is the PlugInFullName method, which identifies your class: public string PluginFullName { get { return "Oracle Lite 10g Plugin"; } }
We will also implement the Datasource, Active, and PluginPath properties of the interface the same way as we did for SQL Server CE.
Connecting to Oracle Lite
You can connect to Oracle Lite using the managed OracleConnection class provided by Oracle: OracleConnection _connection; _connection = new OracleConnection( "Database=SALESFORCE;DSN=SALESFORCE;uid=SYSTEM;pwd=admin123;"); _connection.Open(); . . . _connection.Close(); _connection.Dispose(); _connection = null; [ 77 ]
Building the Data Tier
Programmatically creating the Oracle Lite database
Let's take a look at the CreateSalesForceDatabase method equivalent for Oracle Lite. It does what it says, which is to create a database named SalesForce in Oracle Lite and to generate all the sales force application tables automatically. The differences now are that: •
You will be using the OracleEngine class to generate the new database
•
You will be using the OracleConnection class to connect to the database
In the CreateSalesForceDatabase implementation you will first need to declare a few function-scope variables: OracleConnection _connection; OracleCommand _command; ResourceManager _ResourceManager;
The CreateDatabase method in OracleEngine is a shared function, so you can call the method directly to create the salesforce DSN and database, and set the database password to admin123: try { OracleEngine.CreateDatabase("salesforce", "salesforce", "admin123"); } catch (Exception ex) { MessageBox.Show(ex.Message, "Create database", MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1); return false; }
Now that the database has been created, you will attempt to connect to it and execute the SQL statements stored in the TableSchema.resx resource file to build the entire schema required for the sales force application. You can start by first opening a connection to your newly created database: _connectionString = "Database=SALESFORCE;DSN=SALESFORCE;uid=SYSTEM;pwd=admin123;"; _connection = new OracleConnection(_connectionString); try { _connection.Open(); [ 78 ]
Chapter 2 } catch (Exception ex) { MessageBox.Show(ex.Message, "Connecting to database", MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1); }
Now latch .NET's ResourceManager class on to the TableSchema.resx resource file like this: _ResourceManager = new ResourceManager("CRMLive.TableSchema", Assembly.GetExecutingAssembly());
Lastly, create the OracleCommand object to run all the SQL statements retrieved from the resource file. This will create the tables together with their corresponding sequences needed for the application to run: _command = _connection.CreateCommand(); _command.CommandText = _ResourceManager.GetString("AccountTasks_ SQL"); _command.ExecuteNonQuery(); _command.CommandText = _ResourceManager.GetString("AccountTasksSeq _SQL"); _command.ExecuteNonQuery(); . . .
And, of course, don't forget to close and dispose of the database connection at the end! _connection.Close(); _connection.Dispose(); _connection = null; return true;
[ 79 ]
Building the Data Tier
Browsing the Oracle Lite database with Msql When you install the Oracle Lite tools.language.device.processor.cab file, a few tools are created in your device's Programs folder: Tool name
Description
Oracle Msql
Allows you to create databases, view tables, view data views, and execute SQL statements. This tool can be somewhat thought of as Oracle's equivalent of Microsoft's Query Analyzer tool.
Oracle DM
Allows the user to register the device with a Mobile Server (for data synchronization and centralized device management).
Oracle MSync
Allows the user to initiate a sync with a Mobile Server.
When you launch the Oracle Msql tool, a window will appear that allows you to select the database you wish to connect to. You should be able to see your SALESFORCE database in this list (shown in the following screenshot):
Once logged in, you can browse through the tables and columns in your SALESFORCE database via the Tables tab as shown in the following screenshot. (You need to first choose a table from the list and click the Describe button).
[ 80 ]
Chapter 2
The MSql tool also provides an area for you to run SQL queries against the database. This can be done via the SQL tab.
Retrieving data from Oracle Lite
You can try your hand now at retrieving data from the Oracle Lite database. We will look at the GetAccountDetails method again. To begin, you can declare some of the variables you will later use: OracleDataAdapter _adapter; OracleCommand _command; DataSet _Resultset;
The OracleConnection class also supports the CreateCommand method. The logic here is the same as that of the SQL Server CE plugin. The only difference here is that you need to use the OracleDataAdapter class instead of the SqlCeDataAdapter class to retrieve data. In this function, you can also assume that a connection has already been established to the database. _command = _connection.CreateCommand; _command.CommandText = "SELECT * FROM Accounts WHERE AccountGUID='" + AccountGUID + "'"; _Resultset = new DataSet(); _adapter = new OracleDataAdapter(_command); _adapter.Fill(_Resultset, "Accounts");
You will also need to repeat the same thing for the other three related tables, filling in different Datatables in the same DataSet object.
Data manipulation in Oracle Lite
Data manipulation logic in Oracle Lite behaves in much the same way as SQL Server CE. The only core differences are the syntax and data type differences between the two databases. We first declare the variables we will need in this section. Take note that the transaction class name is IDbTransaction. OracleDataAdapter _adapter; IDbTransaction _transaction;
[ 81 ]
Building the Data Tier
Assuming a connection to the database has already been established, the next piece of code marks the beginning of a transaction using the BeginTransaction() method call: _transaction = _connection.BeginTransaction();
You will now need to create the UpdateCommand, InsertCommand, and DeleteCommand objects for the AccountTasks table succeeding. Take note that Oracle Lite does not use the @fieldname notation. Instead, use the question symbol (?) to denote a parameter. _adapter = new OracleDataAdapter(); try { //========================================================= //UPDATING THE ACCOUNTTASKS TABLE //========================================================= //Here we create an Update command for the AccountTasks table. //Take note that we include this command as part of the transaction _adapter.UpdateCommand = new OracleCommand("UPDATE AccountTasks SET TaskSubject=?, TaskDescription=?, TaskDate=?, TaskStatus=? WHERE TaskID=?" , _connection, _transaction); _adapter.UpdateCommand.Parameters.Add("TaskSubject", DbType.String , "TaskSubject"); _adapter.UpdateCommand.Parameters.Add("TaskDescription", DbType.String, "TaskDescription"); _adapter.UpdateCommand.Parameters.Add("TaskDate", DbType.DateTime, "TaskDate"); _adapter.UpdateCommand.Parameters.Add("TaskStatus", DbType.Int32, "TaskStatus"); _adapter.UpdateCommand.Parameters.Add("TaskID", DbType.Int32, "TaskID");
//Here we create the Delete command for the AccountTasks table
[ 82 ]
Chapter 2 _adapter.DeleteCommand = new OracleCommand("DELETE FROM AccountTasks WHERE TaskID=?", _connection, _transaction); _adapter.DeleteCommand.Parameters.Add("TaskID", DbType.Int32, "TaskID");
//Notice that we insert AccountTasksSeq.NEXTVAL into the //TaskID column. We use the SEQUENCE we created earlier to //generate a running number of the TaskID column each time //we insert a new AccountTasks record _adapter.InsertCommand = new OracleCommand ("INSERT INTO AccountTasks(TaskID, AccountGUID, TaskSubject, TaskDescription, TaskCreated, TaskDate, TaskStatus) VALUES (AccountTasksSeq.NEXTVAL, ?, ?, ?, SYSDATE, ?, ?)", _connection, _transaction); _adapter.InsertCommand.Parameters.Add("AccountGUID", GUIDToHex(AccountGUID)); _adapter.InsertCommand.Parameters.Add("TaskSubject", DbType.String, "TaskSubject"); _adapter.InsertCommand.Parameters.Add("TaskDescription", DbType.String, "TaskDescription"); _adapter.InsertCommand.Parameters.Add("TaskDate", DbType.DateTime, "TaskDate"); _adapter.InsertCommand.Parameters.Add("TaskStatus", DbType.Int32, "TaskStatus"); //Perform the update on the AccountTasks table. This //method goes through all changes in the specified table //and applies the appropriate Insert, Update and Delete //commands _adapter.Update(Account.Tables("AccountTasks")); //========================================================= //REPEAT FOR ALL OTHER TABLES //========================================================= . . .
[ 83 ]
Building the Data Tier _transaction.Commit(); } catch (Exception ex) { _transaction.Rollback(); }
As the last step, we dispose of the objects we have created: _transaction.Dispose(); _transaction=null; _adapter.Dispose(); _adapter=null;
Dealing with GUID values in Oracle Lite
In Oracle Lite, the RAW(16) binary data type is typically used to store a GUID value. When you try to retrieve data from a column defined as RAW and apply the TO_CHAR() function to it, the data retrieved will be returned in the form of a hexadecimal string. The following is an example of such an SQL statement: SELECT TO_CHAR(AccountGUID) FROM Accounts
The retrieved data cannot be directly read into a System.Guid object. The hexadecimal string must first be converted into a byte array, and then fed into the System.Guid (byte()) constructor to obtain a System.Guid object. Likewise, if you wanted to write to a RAW column in the database, you have to first convert your System.Guid object into a byte array and then into a hexadecimal string. We will not go into the details of converting between hexadecimal strings and byte arrays. (These functions GUIDToHex() and HexToGUID() are provided in this book's companion source code.) The GUIDToNative() and NativeToGUID() implementations will need to call the conversion functions in the following fashion: public object GUIDToNative(System.Guid AccountGUID) { return GUIDToHex(AccountGUID); } public System.Guid NativeToGUID(object AccountGUID) { return HexToGUID(AccountGUID); }
[ 84 ]
Chapter 2
Summary
In this chapter, you've seen how you can design an adaptable plugin framework for the data tier and how interfaces can be used to construct a layer of abstraction on top of the database. You then learned how to set up the Oracle Lite and SQL Server CE databases on the mobile device and had a feel of the developer tools provided with these database installations. You've also learned how you can use the various ADO.NET providers to connect to these databases, and run SQL statements to retrieve and update data to and from these databases. In building the two database plugins, you've learned how you can use ADO.NET datasets, command objects, and transactions to communicate effectively with the underlying databases. Building a formidable data tier goes a long way in providing a robust foundation on top of which you can build your logic and presentation tier. In the next chapter, we will look at how we can make further use of the data tier we have built from the logic tier.
[ 85 ]
Building the Mobile Sales Force Module In this chapter, you will begin to build the logic and presentation tiers of the sales force application itself. The Microsoft .NET Compact Framework offers many different data driven controls to help you get your application up in the shortest possible time. We will cover most of these controls and learn to use them effectively in this chapter. With these controls, there are also numerous design pitfalls that you can potentially encounter if you're not careful. This chapter will provide tips to help you avert these pitfalls and ensure a clean segregation of code between the three tiers. You will also be making use of the data libraries you've built in Chapter 2 to supply data to the sales force application. By the end of this chapter, you will have learned: •
How to build a robust logic tier around the data objects generated from the data tier
•
How to use the .NET data-binding features to bind presentation-layer controls to your business objects
•
How to build a flexible form navigation class
•
How to include data validation in your business objects
•
How to handle master-detail data
•
How to create .NET user controls
•
How to create custom controls that inherit from the .NET Listview control
•
How to handle and parse XML-based data using the XML Document Object Model
•
How to handle file attachments
Building the Mobile Sales Force Module
A brief walkthrough of what you will be building
In this chapter, the first screen your users will see after launching the SalesForceApp application is the main menu as shown in the next screenshot. The Setup datasources icon probably sounds familiar to you. It should, because clicking this icon will bring the user to the PluginsSetup form you built in the previous chapter. The Leads, Opportunities, and Customers buttons will let the user browse a listing of the respective accounts stored on the mobile device, while the New Account button will let the user create a new account immediately.
In the accounts listing screen (shown in the following screenshot), the first thing you will notice is that the data is displayed in pages. Each page of data will display up to ten records. Four buttons at the top of the screen allow the user to navigate between pages of data. The accounts by default will be sorted in descending order by the date and time the account was created. We will, however, provide a context menu to the user so that he or she can re-sort the accounts by name if desired.
[ 88 ]
Chapter 3
In the menu bar, clicking the Leads item will pop up a menu that lists a few actions the user can perform on the selected lead. For instance, the user can choose to create an entirely new lead or to edit or remove the selected lead. He or she can also opt to promote or demote the selected account between the Lead, Opportunity, and Customer types.
Removing a lead is straightforward. The user highlights an account on the list and chooses the Remove selected Lead menu item. A confirmation box will appear and, upon confirmation, the account is deleted from the database immediately. The list of accounts will then refresh itself to reflect the latest data.
When a user decides to create a new lead, he or she will be presented with the lead details screen as shown in the next screenshot. There are a total of six tabs in the lead details screen. The first tab, the General tab, allows the user to key in general lead information. Compulsory fields are marked with an asterisk and presented in red. You will be using the data-binding features provided in the .NET Compact Framework to bind the controls on this screen to your business objects.
[ 89 ]
Building the Mobile Sales Force Module
The second tab, the Interest tab, displays all of Tomorrow Inc.'s products in a checklist format. The user ticks the items that the lead or opportunity is interested in. To implement the checklist functionality, you will be creating a custom control that inherits from the ListView control. Our approach to this feature will also be interesting—the list of products are retrieved as rows of data from the Products table, but the user's checklist selection is stored and retrieved in XML.
The next tab, the Address tab, is another simple data-gathering screen similar to the General tab. It allows the user to specify the lead's mailing correspondence and website address (if available).
[ 90 ]
Chapter 3
The fourth tab, the Tasks tab, will provide the user with a means to create and manage tasks for the particular account. This will be an example of the classic master-detail form.
Upon clicking the New or Edit buttons, it will display the task detail window, where the user will be able to either create a new task or edit an existing one. It is important to take note that at this step, any changes to the list of tasks are not immediately committed to the database. The changes are held in the DataSet in memory and are only committed when the user saves the main lead record.
[ 91 ]
Building the Mobile Sales Force Module
The next tab, the History tab, is a specialized list control that will display historical data of all correspondence with a customer grouped by date in the fashion shown in the following screenshot. As direct data binding will not allow you to achieve this type of grouping, you will build a custom control that inherits from the Listview control to handle the desired formatting. This control will also be purely read only. The data input to this control will be automatically generated when your application detects incoming/outgoing phone calls and SMS messages. The generating of the input data will be covered in Chapter 5, Building Integrated Services. In this chapter, we will focus on building the control responsible for its display.
The last tab, the Files tab, allows the user to manage the file attachments for the particular account. This is another set of master-detail data, similar to that of the Task list.
[ 92 ]
Chapter 3
Clicking on the New or Edit buttons launches the file details screen shown in the next screenshot. Through this screen, the user will be able to browse for a file attachment (of any type) on the mobile device and upload it to the sales force system. Upon selecting a file, the file size is automatically retrieved and displayed in the same screen. What is interesting is that you will be building a reusable FileUpload user control to handle the uploading and launching of the file.
Your FileUpload user control will also feature a clickable link that will automatically launch the file in its viewer (if the viewer application is installed on the mobile device) when the user clicks on it. In the next screenshot, Windows Mobile's default image viewer is used to open a JPEG photo of the customer's living room (so that the technicians have a good idea where to place The Chair).
[ 93 ]
Building the Mobile Sales Force Module
When the user clicks the Save button in the menu bar, the entire lead together with the task and file attachment records will be committed to the database all in one go. You will be implementing some level of validation on the business objects, so that your users will be prompted to fill in compulsory fields if they've left them out, or be notified when an account with the same name already exists.
The Opportunity and Customer screens will behave exactly like the Lead screen, so reusability of UI will be one of the core objectives in building this application.
Building a form navigation class
Using a form navigation class is a good approach to UI navigation in any application. By placing the code to launch a form in a single location, changes to the form in the future will be an easier task. For instance, consider the following code to launch the AccountViewer form: public void btnLaunch_Click(System.Object sender, System.EventArgs e) { AccountViewer _accountViewer; _accountViewer = new AccountViewer (); _accountViewer.ShowDialog (); _accountViewer.Dispose(); _accountViewer=null; }
[ 94 ]
Chapter 3
Suppose your boss asked you to replace the AccountViewer form with an entirely different form. You would have to go through your code to look for the preceding code to make your change. Now imagine if you did the same thing in twenty different places. Making such a change would be a messy affair. If you centralize form navigation, launching the form would be possible in a single call to the navigation service, passing in a tag to indicate which form you wish to display: public void btnLaunch_Click(System.Object sender, System.EventArgs e) { NavigationService.ShowDialog("AccountViewer"); }
The actual form objects used would be transparent to the rest of your application. To implement the change your boss requested, all you would need to do is to switch the form objects associated with the AccountViewer tag in the NavigationService class. Let's add the NavigationService class to your project now. The ShowDialog() function takes in the name of the form that you wish to display as well as an argument (if available) to supply to the instantiated form object. We can launch the forms that correspond to the specified name using a switch.case statement. In the following code, we launch the PluginsSetup form you've created in Chapter 2 when the SetupDatasources tag is specified. using System.Windows.Forms; namespace CRMLive { public class NavigationService { public static DialogResult ShowDialog(string DialogName, object Arg1) { DialogResult _dialogResult = DialogResult.Cancel; switch (DialogName) { case "SetupDatasources": PluginsSetup _PluginsSetup = new PluginsSetup(); _dialogResult = _PluginsSetup.ShowDialog(); _PluginsSetup.Close(); _PluginsSetup.Dispose(); _PluginsSetup = null; break; } return _dialogResult; } } } [ 95 ]
Building the Mobile Sales Force Module
The fact that it is declared as a static method allows you to call this method from anywhere without having to specifically instantiate the NavigationService class. That's all there is to it! You now have a form navigation class. We will add the ability to launch other forms as we proceed further in this chapter. Now let's take a look at how we can create the main menu for the application and launch it using the NavigationService class.
Building the main menu
The main menu serves as a launch pad to the various modules in your sales force application. We will implement an icon-based menu using the ListView control. Let's start by adding a new form named MainMenu to your project.
Drag a ListView control to the form and set the Dock property to Fill. This will expand your ListView control to fill up the screen. You will need to represent the various modules of your application using different icons. Drag an ImageList control to the form. Highlight the ImageList instance and set the ImageSize property to 32 (indicating an icon width and height of 32 pixels respectively). Click on the Images property, and add five icon files of the same size to this list. You can use your own set of icons or the ones provided in the downloadable source code for this book. After doing that, select the ListView instance on the form, and set its LargeImageList property to the name of the ImageList instance you've just created. This provides the ListView control with a library of icons that it can use for display. Next, set the Activation property of the ListView control to TwoClick. This means that the user has to click or tap the icon twice to indicate that he has selected that item. [ 96 ]
Chapter 3
You should now add some items to your main menu. Expand the Items property of your ListView control, and add five items. We will use the Tag property to identify each item. Use the same name for both the Tag and Text properties. Make sure you select the appropriate icon for each item.
When a user double-clicks on one of these five menu items, we will check to see which item was selected, read its Tag property, and then call the NavigationService class to launch the corresponding form associated with the tag. The following shows how this is done in code: public void lstMainMenu_ItemActivate(object sender, System.EventArgs e) { if (lstMainMenu.SelectedIndices.Count > 0) { string _tag = System.Convert.ToString (lstMainMenu.Items [lstMainMenu.SelectedIndices[0]].Tag); NavigationService.ShowDialog(_tag, null); } }
[ 97 ]
Building the Mobile Sales Force Module
You will now need to create a button that allows your users to exit the application. In the menu bar of this form, add a menu item with the caption Logout. Double-click on this menu item, and add some code to close the window: public void mnuLogout_Click(System.Object sender,System.EventArgs e) { this.DialogResult = DialogResult.Cancel; this.Close(); }
You will now add code to launch this main menu in your Navigationservice class: public static DialogResult ShowDialog(string DialogName,object Arg1) { DialogResult _dialogResult = DialogResult.Cancel; switch (DialogName) { case "SetupDatasources": PluginsSetup _PluginsSetup = new PluginsSetup(); _dialogResult = _PluginsSetup.ShowDialog(); _PluginsSetup.Close(); _PluginsSetup.Dispose(); _PluginsSetup = null; break; case "MainMenu": MainMenu _MainMenu = new MainMenu(); _dialogResult = _MainMenu.ShowDialog(); _MainMenu.Close(); _MainMenu.Dispose(); _MainMenu = null; break; } return _dialogResult; }
Using Main() as the startup object
In Chapter 2, the SalesForceApp project would automatically run the PluginsSetup form every time you ran the project. You will now change the startup object for this project from a form to a class instead. The .NET framework supports using a Main() method as the initial point of entry to an application, so let's create this method.
[ 98 ]
Chapter 3
First add a new class called Application to your project. This class will provide the methods to manage the application lifecycle as well as to house global-scope functions. using System; using System.Windows.Forms; using System.Data; namespace CRMLive { public class Application { //Application entry point public static void Main() { InitializeApp(); NavigationService.ShowDialog("MainMenu", null); DeInitializeApp(); } //Initializes the application public static void InitializeApp() { try { GlobalArea.PluginManager.GetActivePlugin.ConnectDatabase(); } catch (Exception ex) {} } //Deinitializes the application public static void DeInitializeApp() { try { GlobalArea.PluginManager.GetActivePlugin.DisconnectDatabase(); } catch (Exception ex) {} } } } [ 99 ]
Building the Mobile Sales Force Module
You will also notice in the InitializeApp() method that we just saw, you requested the active plugin object to establish a connection to the database. The sales force application will open and maintain a single connection to the database throughout its entire life. Connected or disconnected state? A constantly connected database state is possible and, in fact, better suited for a mobile application. This is due in part to the expectation that there is only one user using the local database on the mobile device at any one time.
The Main() method must be declared as a static method so that it can be used as the startup object for this project. To change the startup object, navigate to the Project Properties menu. In the Application tab, select Sub Main in the Startup object drop-down list. Let's test what you have built so far. Run the SalesForceApp project. You will immediately see your main menu form show up instead of the PluginsSetup form. Click on the Logout menu item to exit the application. Now let's set up the logic tier—the business objects that encapsulate the raw datasets retrieved from the database.
Creating the business objects to encapsulate your DataSets
Using raw DataSet objects directly in the presentation layer is not a good practice and should be avoided whenever possible. It makes maintenance a relatively difficult job later on when there are changes to the underlying data tier. Consider the following code snippet. It retrieves a dataset from the data layer and references the data inside using table column names. DataSet _accountsDataset= GetAccountsList(); Datarow _row = _accountsDataset.Tables["Accounts"].Rows[0]; txtFirstName.text = _row["FirstName"]; txtLastName.text = _row["LastName"];
[ 100 ]
Chapter 3
Now, try to imagine this same code snippet peppered all over your application across your forms, custom controls, and user controls. This will translate into a lot of trouble when the underlying database column name or type changes in the future. Many programmers commonly make the mistake of passing dataset objects directly to the presentation tier (a classic example of this is the Datagrid control, which readily takes in and renders a DataSet object). A logic or business object tier shields the presentation tier from having to worry about the underlying data tier.
The great thing about data binding is that it not only works on datasets but also on custom business object classes. This allows you to wrap or encapsulate the datasets within your own custom class and to expose each field as properties in your class. The following diagram best illustrates this concept:
[ 101 ]
Building the Mobile Sales Force Module
Let's take a look at what happens when the underlying database column or type changes this time. If the Database Administrator decided to rename the FirstName column as IndividualFirstName one day, you would only need to change the property definition in your logic tier to the following: public string FirstName { get {return_row["IndividualFirstName"]; }}
The following table lists the various business objects you will need to build in the sales force application: Class name Baseobject
Description
LeadAccount
The LeadAccount represents a lead. This account inherits from the BaseAccount class.
OpportunityAccount
The OpportunityAccount represents an opportunity. This account inherits from the BaseAccount class.
CustomerAccount
The CustomerAccount represents a customer. This account inherits from the BaseAccount class.
Task
The Task class represents a task record.
History
The History class represents a history record. Historical records are usually auto-generated by the application.
File
A File class represents a file attachment object.
Product
The Product class represents a product item.
TaskCollection
This is a collection class that stores Task objects.
HistoryCollection
This is a collection class that stores History objects.
FileCollection
This is a collection class that stores File objects.
ProductCollection
This is a collection class that stores Product objects.
This is the base class for the LeadAccount, OpportunityAccount, and CustomerAccount business objects. Internally, it holds all the properties that are common across leads, opportunities, and customers. It also holds a collection of Task, File, and History objects related to the account.
[ 102 ]
Chapter 3
Let's take a look at the skeleton code for the BaseAccount class. As each sales force account is represented by a Datarow in the dataset, you will need to pass in the data row to the constructor of the BaseAccount class. namespace CRMLive { public class BaseAccount { public enum AccountTypes { Lead, Opportunity, Customer } private TaskCollection _Tasks; private HistoryCollection _Histories; private FileCollection _Files; private DataSet _MappedDataSet; private DataRow _MappedDataRow; private string _ValidationResult; public DataRow MappedDatarow { get { return _MappedDataRow; } } public DataSet MappedDataset { get { return _MappedDataSet; } } public BaseAccount(DataRow MappedDatarow) { _MappedDataRow = MappedDatarow; _MappedDataSet = _MappedDataRow.Table.DataSet; _Tasks = new TaskCollection(this, _MappedDataSet.Tables["AccountTasks"]);
[ 103 ]
Building the Mobile Sales Force Module _Histories = new HistoryCollection(this, _MappedDataSet.Tables["AccountHistories"]); _Files = new FileCollection(this, _MappedDataSet.Tables["AccountFiles"]); } }
The BaseAccount class would need to expose each column in the Datarow as a property. The following code shows an example on how you can expose the FirstName field as a property. public string FirstName { get { if (Information.IsDBNull(_MappedDataRow["FirstName"]) == true) { return ""; } else { return _MappedDataRow["FirstName"].ToString(); } } set { _MappedDataRow["FirstName"] = value; } }
Let's take a look now at both the Task class and TaskCollection classes. A data row is also passed in to the constructor of the Task class, which can then be exposed and accessed via properties in the same fashion. public class Task { private DataRow _MappedRow; public string TaskSubject { get { if (Information.IsDBNull(_MappedRow["TaskSubject"]) == true) { return ""; [ 104 ]
Chapter 3 } else { return _MappedRow["TaskSubject"].ToString(); } } set { _MappedRow["TaskSubject"] = value; } } public DateTime TaskDate { get { if (Information.IsDBNull(_MappedRow["TaskDate"])) { return DateTime.Now; } else { return System.Convert.ToDateTime (_MappedRow["TaskDate"]); } } set { _MappedRow["TaskDate"] = value; } } . . . public DataRow MappedRow { get { return _MappedRow; } } [ 105 ]
Building the Mobile Sales Force Module public Task(DataRow DataRowItem) { _MappedRow = DataRowItem; } }
Collection classes will be created differently. Most of the collection classes that you create in this application will need to inherit from the System.Collections. Generic.List class. The reason for this is that the .NET Datagrid only recognizes and binds to objects that implement either the IList or IListView interface. The DataSet object, for example implements the IList interface. Most of the methods required of a collection object such as the Count() and Contains() method are already provided in the List base class. You only need to create your own custom AddTask() and RemoveTask() functions. These custom
functions do the additional step of adding the new data rows to the data table of the internally stored dataset. The following code shows how the TaskCollection class can be created. public class TaskCollection : System.Collections.Generic.List
{ private DataTable _TaskDataTable; private BaseAccount _parent; public BaseAccount Parent { get { return _parent; } } public Task AddTask(Task TaskItem) { base.Add(TaskItem); _TaskDataTable.Rows.Add(TaskItem.MappedRow); return TaskItem; } public void RemoveTask(Task TaskItem) { [ 106 ]
Chapter 3 TaskItem.MappedRow.Delete(); base.Remove(TaskItem); } public TaskCollection(BaseAccount Parent, DataTable TableItem) { int _counter; Task _task; _parent = Parent; _TaskDataTable = TableItem; for (_counter = 0; _counter <= _TaskDataTable.Rows.Count - 1; _counter++) { _task = new Task(_TaskDataTable.Rows[_counter]); base.Add(_task); } } }
Adding a new task to a BaseAccount object, for example, is a two-step process usually done in the following fashion. The first step creates the new task object and the second step adds it to the Tasks collection. Task _task = _myBaseAccount.NewTask(); . . . _myBaseAccount.Tasks.AddTask(_task);
The NewTask() method can be implemented in the BaseAccount class using the following code snippet: public Task NewTask() { DataRow _row = _MappedDataSet.Tables["AccountTasks"].NewRow(); Task _task = new Task(_row); return _task; }
[ 107 ]
Building the Mobile Sales Force Module
Validating data in your business objects
Another important role of the logic tier is to validate its data before it reaches the data tier. You can provide validation functionality to the rest of the application by exposing a Validate() function in the business object class. This function will run through all the necessary validations required and finally return true or false depending on the outcome of the validation. The following code shows how this function might look in the BaseAccount class. private string _ValidationResult; public string GetValidationResult() { return _ValidationResult; } public bool Validate() { _ValidationResult = ""; //We ensure that the Account GUID is not empty if (AccountGUID.ToString().Length == 0) { _ValidationResult = "Account GUID cannot be empty"; return false; } //We ensure that at least one phone number have been filled in if (ResPhoneNo.Trim().Length == 0 && MobPhoneNo.Trim().Length == 0) { _ValidationResult = "Please fill in at least one contact number"; return false; }
//We ensure that both the first name and last name fields //have been filled in if (FirstName.Length == 0 || LastName.Length == 0) { _ValidationResult = "Please fill in both the first name and last name fields"; return false; } [ 108 ]
Chapter 3 //Make sure that an account with the same name does not //already exist if (GlobalArea.PluginManager.GetActivePlugin.AccountExists (FirstName, LastName, this.AccountGUID) == true) { _ValidationResult = "An account with the same name already exists. Please use a different name"; return false; } return true; }
Building the AccountViewer form
Now that you have your business objects set up, you will begin to build the AccountViewer form. The AccountViewer form is the main form in the sales force application. It presents the full details of an account in the sales force application to the user. This form is a rather large form, so you will need to split its interface into six different tabs using the .NET Compact Framework Tab control. Design a form similar to the next screenshot. Create the General and Address tabs first.
[ 109 ]
Building the Mobile Sales Force Module
Define the following list of items in the Status, Reception, and Source Combobox controls. Combobox
List of items
Status
Contacted Qualified Unqualified
Reception
Cold Warm Hot
Source
Referrals Advertisement
In the menu bar of this form, you will need to add the Save and Cancel menu items to save and discard changes made to the account record. In the Save item click event, call the BaseAccount.Validate() function to validate the data keyed in. If the validation fails, display the error message to the user. The Cancel button click event simply discards changes made to the account details. public void btnSave_Click(System.Object sender, System.EventArgs e) { AccountBindingSource.EndEdit(); if (_lead.Validate()==false) { String _validationMessage = _lead.GetValidationResult(); MessageBox.Show(_validationMessage,"Validation error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation ,MessageBoxDefaultButton.Button1); } } public void btnCancel_Click(System.Object sender, System.EventArgs e) { AccountBindingSource.CancelEdit(); this.DialogResult = System.Windows.Forms.DialogResult.Cancel; this.Close(); }
[ 110 ]
Chapter 3
Data binding .NET controls to your business objects
The .NET Compact Framework provides the BindingSource control, which allows you to set up data binding on the form. Drag a BindingSource control to the AccountViewer form. You can set up data binding using the following code. The first two lines of code pass the business object to the BindingSource control. The subsequent lines of code define the mapping between your business objects and your controls. public void SetupBindings() { if (_account != null) { AccountBindingSource.DataSource = typeof(BaseAccount); AccountBindingSource.Add(_account); txtFirstName.DataBindings.Add(new Binding("Text", AccountBindingSource, "FirstName", true)); txtLastName.DataBindings.Add(new Binding("Text", AccountBindingSource, "LastName", true)); //Data binding a combobox txtLastName.DataBindings.Add(new Binding("SelectedIndex", AccountBindingSource, "Status", true)); . . . } }
In the same fashion, establish the data bindings for the other controls on the AccountViewer form.
Launching the AccountViewer form
At this point, you have an AccountViewer form that is bound to the BaseAccount class. In order to use the form for data entry, you will need to feed it a live BaseAccount object.
[ 111 ]
Building the Mobile Sales Force Module
As you found out earlier, the BaseAccount class also acts as a wrapper for the Dataset object. You can't simply instantiate a new BaseAccount object without having the Dataset in the first place. The global NewLead() method solves this problem for us. It grabs an empty dataset (containing the data schema) from the data tier. You can use the GetAccountDetails() function you've created in Chapter 2 to do this. It creates a new row in that dataset and encapsulates it using the LeadAccount object. public LeadAccount NewLead() { DataSet _newAccountDataset; LeadAccount _newAccount; DataRow _newRow; _newAccountDataset = GlobalArea.PluginManager.GetActivePlugin.GetAccountDetails(null); _newRow = newAccountDataset.Tables["Accounts"].NewRow(); _newAccount = new LeadAccount(_newRow); _newAccount.AccountGUID = Guid.NewGuid(); return _newAccount; }
The global SaveNewAccount() method does the opposite. It takes a Baseaccount object containing the user's changes and commits it to the database via the SetAccountDetails() function you created in Chapter 2. public bool SaveNewAccount(BaseAccount AccountObject) { DataSet _accountSet; AccountObject.MappedDataset.Tables["Accounts"].Rows.Add (AccountObject.MappedDatarow); _accountSet = AccountObject.MappedDataset; return GlobalArea.PluginManager.GetActivePlugin .SetAccountDetails (AccountObject.AccountGUID, _accountSet); }
[ 112 ]
Chapter 3
You need to launch the AccountViewer form, create a new lead object, pass it to the form, and upon the user clicking the Save button, commit the changes in this object back to the database. You can do all this in the ShowDialog() function of your NavigationService class: public static DialogResult ShowDialog(string DialogName, object Arg1) { DialogResult _dialogResult = DialogResult.Cancel; switch (DialogName) { . . . Case "New Account": AccountViewer _lead = new AccountViewer(); _lead.Account = GlobalArea.Application.NewLead(); _lead.SetupBindings(); _lead.TitleBar = "New lead"; _dialogResult = _lead.ShowDialog(); if (_dialogResult == DialogResult.OK) { GlobalArea.Application.SaveNewAccount (_lead.Account); } _lead.Close(); _lead.Dispose(); _lead = null; break; } return _dialogResult; }
[ 113 ]
Building the Mobile Sales Force Module
Testing the AccountViewer form
You can test the AccountViewer form at this point, assuming you have set up and registered your data source appropriately. Launch the sales force application. When you double-click on the New Account icon, you will see the AccountViewer form appear. Test validation by skipping the required fields (such as First Name) and attempting a save. You should see the appropriate validation error message appear. If you key in all the required fields and attempt a save, you will notice that the form will automatically close. If you query your database using the Query Analyzer or Msql tool, you will notice a new record created in the Accounts table.
Building the Tasks list
Master-detail forms are a common occurrence in any software application. Using the tasks list as an example, the main lead record would be the Master, and the list of associated tasks would comprise the Detail. Master-detail records have a 1:many relationship, and detail records are usually represented in a grid or table, which is usually editable. Desktop applications provide specialized grid controls that support in-grid editing, allowing the user to edit data directly on the grid itself. Unfortunately, the Datagrid control provided by the .NET Compact Framework does not support such a feature. You will therefore need to launch a separate Details form whenever the user needs to edit or create a detail record. Let's take a look at how you can build a Datagrid to display the list of tasks for the account.
Add a Tasks tab to the AccountViewer form and place a Datagrid control in the tab frame. You will also need to place three buttons: New, Edit, and Remove at the top of this Datagrid. The New and Edit buttons will launch the TaskDetailViewer form that you will create later. [ 114 ]
Chapter 3
Populating the Tasks Datagrid control
To populate the Tasks Datagrid control, simply bind the BaseAccount. Tasks property to the Datasource property of the Datagrid. This will feed a TaskCollection object to the grid, which will render every row of data in this collection nicely for you. public void SetupBindings() { if (_account != null) { AccountBindingSource.DataSource = typeof(BaseAccount); AccountBindingSource.Add(_account); . . . dgTasks.DataBindings.Add(new Binding("DataSource", AccountBindingSource, "Tasks", true)); } }
Notice that you have not yet specified which columns of the Task object to show (and how to format them) on the grid. You can do this by selecting the Datagrid in the Visual Studio designer and expanding the TableStyles property. Upon doing so, you will be presented with the following window:
[ 115 ]
Building the Mobile Sales Force Module
Specify the type name of the data source for the Datagrid in the MappingName field. This would be your TaskCollection object. Expand the GridColumnStyles property. You will now be prompted with yet another window that allows you to define column styles and mappings. In this window you can define the columns that are visible in the grid. You can map each column to any property in the Task object by providing the exact property name in the MappingName field. You can also specify a format mask (for date or numerical fields).
Building the TaskDetailViewer form
The TaskDetailViewer form takes in a Task object; you will need to write code to bind the controls of this form to the properties of the Task object. You can use a DateTimePicker control for the Due On field (which is the date and time the task is due), a Combobox control (with some preloaded data in the Items property) for the Status field, and text boxes for the Subject and Description fields. Drag a BindingSource control to the form, and name it TaskBindingSource. The binding source control allows you to manage the editing lifecycle of the data bound controls. Finally, add the Save and Cancel menu buttons to allow the user to either save his or her changes or to discard them.
[ 116 ]
Chapter 3
The following is the code you have to write for this form. In the constructor of the form, call the SetupBindings() method to setup the data binding. The Save and Cancel menu buttons will call the EndEdit() and CancelEdit() methods of the TaskBindingSource control respectively to either save or discard the user's changes. using System; using System.Windows.Forms; namespace CRMLive { public partial class TaskDetailViewer { private Task _Datasource; private void SetupBindings() { this.TaskBindingSource.DataSource = typeof(Task); this.TaskBindingSource.Add(_Datasource); dtpDueOn.DataBindings.Add(new Binding("Value", TaskBindingSource, "TaskDate", true)); txtSubject.DataBindings.Add(new Binding("Text", TaskBindingSource, "TaskSubject", true)); txtDescription.DataBindings.Add(new Binding("Text", TaskBindingSource, "TaskDescription", true));
[ 117 ]
Building the Mobile Sales Force Module cbStatus.DataBindings.Add(new Binding("SelectedIndex", TaskBindingSource, "TaskStatus", true)); dtpDueOn.Value = _Datasource.TaskDate; } public void btnSave_Click(System.Object sender, System.EventArgs e) { this.TaskBindingSource.EndEdit(); this.DialogResult = System.Windows.Forms.DialogResult.OK; this.Close(); } public void btnCancel_Click(System.Object sender, System.EventArgs e) { this.TaskBindingSource.CancelEdit(); this.DialogResult = System.Windows.Forms.DialogResult.Cancel; this.Close(); } public TaskDetailViewer(Task Datasource) { // This call is required by the Windows Form //Designer. InitializeComponent(); // Add any initialization after the //InitializeComponent() call. _Datasource = Datasource; SetupBindings(); } } }
[ 118 ]
Chapter 3
Launching the TaskDetailViewer form When the user decides to create a new task, you will need to:
1. Create a new task object using the BaseAccount.NewTask() method 2. Pass the task object to the TaskDetailViewer form, and launch the form 3. If the form returns an OK dialog result, call the AddTask() method to add the task object to the BaseAccount.Tasks collection The following code shows how this is done: public void btnNewTask_Click(System.Object sender, System.EventArgs e) { Task _task; _task = _account.NewTask(); TaskDetailViewer _EditView = new TaskDetailViewer(_task); if (_EditView.ShowDialog() == DialogResult.OK) { _account.Tasks.AddTask(_task); dgTasks.DataSource = null; dgTasks.DataSource = _account.Tasks; } _EditView.Close(); _EditView.Dispose(); _EditView = null; }
To edit a selected task, you will need to: 1. Get the CurrentRowIndex property of the Datagrid to get the currently selected row index 2. Use this row index to retrieve the corresponding Task object 3. Pass in this Task object into the TaskDetailViewer form for editing 4. If the form returns an OK dialog result, refresh the Datagrid (to reflect the latest changes)
[ 119 ]
Building the Mobile Sales Force Module
The following code shows how this is done: public void btnEditTask_Click(System.Object sender, System.EventArgs e) { if (dgTasks.CurrentRowIndex == - 1) { return; } Task _task = _account.Tasks[dgTasks.CurrentRowIndex]; TaskDetailViewer _EditView = new TaskDetailViewer(_task); if (_EditView.ShowDialog() == DialogResult.OK) { dgTasks.DataSource = null; dgTasks.DataSource = _account.Tasks; } _EditView.Close(); _EditView.Dispose(); _EditView = null; }
Testing the tasks list
Run the sales force application again. This time, you should be able to see the Tasks tab. Try creating a new task by clicking the New Task button. As soon as you've keyed in the task details and clicked Save, your task will be displayed in the Datagrid. You can edit it by selecting the Datagrid row and clicking the Edit Task button. After you're done, fill in the details in the General tab, and click the Save button. If you query your database again using the database tools provided, you should be able to see your task records successfully created in the AccountTasks table.
Handling file attachments
When you build mobile applications, it is quite likely that you will have to handle file attachments one way or another. When deciding on a strategy for file handling on the mobile platform, it is usually a good idea to consider how these files will be synced to your enterprise server at the end of the day. There are generally two approaches: •
Storing physical files as binary data in the database
•
Storing physical files in the filesystem with a link to the file in the database
[ 120 ]
Chapter 3
Generally speaking, if your intention is to make file handling as painless as possible without much regard to performance, then you're probably better off with the first approach. Storing physical files in the database has the benefit of not having to do any additional work during sync—the file is treated like any other ordinary table column. The following table lists the main considerations for each approach: Storing physical files in the database
Storing physical files in the filesystem
Use this approach if performance is not a strong consideration and you want to employ a file handling approach that is quick and easy to set up
Use this approach if performance is a concern and you need more control over how files are synced
Typically limited to the database size limit of 4 GB
Storage size is not limited by database but by device
Security is provided via database encryption
Files stored on disk are not protected—you will usually need to implement your own encryption routines
As performance is an important consideration for the sales force application, we will use the second approach. We will start by first building a file manager class to handle the uploading of files to the sales force application and their internal storage.
Building a file manager class
The core concept of a file manager has to do with maintaining a file repository (usually a folder or drive) on the mobile device that is internally used to store all files uploaded via the application. This file repository is never accessed directly from the rest of the application; only the file manager can access this repository. In this sense, the file manager serves as a broker that services all file upload/download requests initiated by the application. Let's take a look at the skeleton code for this file manager class. When this class is created, the first step would be to ensure that the file repository (in this case the \\Attachments folder) exists, and if it does not, to create it. using using using using
System.Diagnostics; System; System.Data; System.IO;
namespace CRMLive { public class FileManager [ 121 ]
Building the Mobile Sales Force Module { public FileManager() { if (System.IO.Directory.Exists("\\Attachments") == false) { try { System.IO.Directory.CreateDirectory ("\\Attachments"); } catch (Exception) {} } } } }
You will now create the Store() function, which allows users to upload an on-disk file to the repository. You cannot simply make a copy of the uploaded file in the root repository folder because it is highly likely that two files with the same name will be uploaded on the same device. This will lead to one file overwriting the other in the shared repository folder. We must therefore assign each uploaded file a unique GUID of its own. Every time a new upload is made, a folder is created with this GUID as its name. The physical file is then copied to this folder, and the full path returned (so that it can be used as a link to refer back to this file). In other words, when a file named Contract.docx is uploaded to the sales force application, it would end up in: \\Attachments\2a2c1726-24b3-4c3f-82ba-893933971f9f\Contract.docx
The code to do this is as follows: public string Store(string FilePath) { string _newFileGUID; string _finalPath; _newFileGUID = Guid.NewGuid().ToString(); _finalPath = "\\Attachments\\" + _newFileGUID; try { Directory.CreateDirectory(_finalPath); _finalPath = _finalPath + "\\" + Path.GetFileName(FilePath); File.Copy(FilePath, _finalPath); [ 122 ]
Chapter 3 return _finalPath; } catch (Exception) { return ""; } }
The sales force user would likely need to pull out documents from the repository via the application, edit them, and then subsequently save them (and hopefully not have to re-upload the file again). The easiest way to achieve this is to simply open the file directly from the repository using its associated viewer. This means that opening a .docx file would automatically launch Microsoft Word, a .pdf file would launch Adobe Reader, and so on. Doing this is much easier than it sounds. The Process class in the System.Diagnostics namespace allows you to do all that with a single line of code: public void Launch(string FilePath) { Process.Start(FilePath, ""); }
Building the FileUpload user control
When you build mobile applications, it is typical to provide file upload functionality in many different screens in your application. So it would make a lot of sense to create a FileUpload user control that is reusable. A user control, unlike an inherited custom control, can be designed using the Visual Studio designer. If you intend to create a control that is a composite of multiple controls, creating it as a user control would be a better alternative than an inherited custom control.
The FileUpload user control consists of a LinkLabel, a Remove and Browse button, and an OpenFileDialog control. You will need to set the Filter property of the OpenFileDialog control to All files|*.* so that it will display all file types in the file dialog window.
[ 123 ]
Building the Mobile Sales Force Module
Let's take a look at the following skeleton code for this user control. You will need to create a custom event called Changed, which is raised by the user control every time the file attachment is changed. To do this, you will need to declare an event handler for the Changed event using the delegate keyword. Take note that this is usually done outside the class declaration. Inside the class, we declare the event as follows: using System; using System.Windows.Forms; namespace CRMLive { public delegate void ChangedEventHandler(string NewFilepath); public partial class Fileupload { private bool _editMode; private FileManager _FileManager; private string _filepath; public event ChangedEventHandler Changed; public bool EditMode { get { return _editMode; } set { _editMode = value; } } public string Datasource { get { return _filepath; } set { _filepath = value; llFilepath.Text = System.IO.Path.GetFileName(_filepath);
[ 124 ]
Chapter 3 //Here we raise the changed event Changed(_filepath); } } public Fileupload() { // This call is required by the Windows Form //Designer. InitializeComponent(); // Add any initialization after the //InitializeComponent() call. _FileManager = new FileManager(); } } }
You will now need to implement the code for the various constituent controls. The following code will make use of the FileManager class you have created earlier to store and launch uploaded files. //Click event handler for the Link Label public void llFilepath_Click(System.Object sender, System.EventArgs e) { if (MessageBox.Show ("Are you sure you wish to launch the file now?","Launch file?",MessageBoxButtons.YesNo ,MessageBoxIcon.Question ,MessageBoxDefaultButton.Button1 )==DialogResult.Yes ) { _FileManager.Launch(_filepath); } } //Click event handler for the Browse button public void btnBrowse_Click(System.Object sender, System.EventArgs e) { if (ofdFileDialog.ShowDialog() == DialogResult.OK) { Datasource = _FileManager.Store(ofdFileDialog.FileName); } [ 125 ]
Building the Mobile Sales Force Module } //Click event handler for the Remove button public void btnRemove_Click(System.Object sender, System.EventArgs e) { Datasource = ""; }
Building the FileDetailViewer form
Finally, you need to set up a Datagrid to show a listing of the files for each account. Add a new Files tab to your AccountViewer form.
Drop a Datagrid control onto this tab and add another line of code in the SetupBindings() method to bind the Datagrid to the BaseAccount.Files property. public void SetupBindings() { if (_account != null) { AccountBindingSource.DataSource = typeof(BaseAccount); AccountBindingSource.Add(_account); . . . dgFiles.DataBindings.Add(new Binding("DataSource", AccountBindingSource, "Files", true)); } }
[ 126 ]
Chapter 3
You will now need to build the FileDetailViewer form, which shows the detail for each file attachment record. This form is shown as follows:
This form consists of a text box for the file name, a label to display the file size, and the FileUpload control you created earlier. Let's take a look at the following code required to set up data binding for this form. The path of the uploaded file will need to be stored in the database. You can achieve this by data-binding the FileUpload.Datasource property to the Attachment field of the File class. private void SetupBindings() { this.FileBindingSource.DataSource = typeof(File); this.FileBindingSource.Add(_Datasource); txtFilename.DataBindings.Add(new Binding("Text", FileBindingSource, "AttachmentName", true)); fuAttachment.DataBindings.Add(new Binding("Datasource", FileBindingSource, "Attachment", true)); DisplayAttachmentSize(_Datasource.AttachmentSize); }
[ 127 ]
Building the Mobile Sales Force Module
In the event handler of the Changed event for the FileUpload field, retrieve the file size of the file and display it in the same window using the following code: public void fuAttachment_Changed(string NewFilepath) { if (System.IO.File.Exists(NewFilepath) == true) { System.IO.FileInfo _file = new System.IO.FileInfo(NewFilepath); DisplayAttachmentSize(_file.Length); } } private void DisplayAttachmentSize(long AttachmentSize) { lblFileSize.Tag = AttachmentSize; lblFileSize.Text = String.Format("{0:###,###,###,###}", AttachmentSize) + " bytes"; }
Testing file upload functionality
Run the sales force application again. Navigate to the Files tab, and try uploading a new file by clicking the New button. You should be able to see the FileDetailViewer form. After uploading the file and clicking the Save button, you should be able to see the file item displayed in the Datagrid. If you navigate to the \\Attachments folder on your device, you should be able to see that a new folder has been created by the file manager, with your uploaded file stored inside.
[ 128 ]
Chapter 3
Custom formatting and display in list controls
We will now take a look at how you can set up a specialized list control to display the historical data of an account. This control is unique in that it implements grouping in the list; historical records are grouped by date in the following fashion:
The default ListView control provided by Microsoft is unable to handle this type of layout. You will need to build a custom control to achieve this by inheriting from the ListView class. To effectively group data in the layout as just shown, the historical data must already be sorted by date. The following pseudocode outlines how you can roughly achieve this: Loop through each historical record Retrieve the date portion of the timestamp If the date portion is a new day Create a blue title bar with the day printed on it Else Create a standard entry (print the time and activity name)
Implementing the sort in SQL would not be a good idea because you wouldn't want to call the GetAccountDetails() method every time you sort the historical records. You will, therefore, implement the sorting in the HistoryCollection object itself. The .NET Compact Framework provides the IComparer interface, which you can implement to attach your own custom sorting algorithm to the HistoryCollection object.
[ 129 ]
Building the Mobile Sales Force Module
Building a collection sorter using the IComparer interface
The IComparer interface allows you to specify the class type of the objects that are being compared. It defines the Compare method, which you can implement using your own algorithm. Ultimately, you need only define the outcome of a comparison between two objects and the .NET Compact Framework will do the rest of the sorting for you. In the following code snippet, you are comparing two History objects via their timestamp values: using System; namespace CRMLive { public class HistoryComparer : IComparer { private GlobalVariables.SortingOrder _sortOrder; public HistoryComparer (GlobalVariables.SortingOrder sortOrder) { _sortOrder = sortOrder; } public int Compare(History x, History y) { if (x.TimeStamp == y.TimeStamp) { return 0; } else { if (x.TimeStamp > y.TimeStamp) { if (_sortOrder == GlobalVariables.SortingOrder.Ascending) { return - 1; } else { return 1; }
[ 130 ]
Chapter 3 } else { if (_sortOrder == GlobalVariables.SortingOrder.Descending) { return 1; } else { return - 1; } } } } } }
In the next section, you will build the HistoryList control and make use of the HistoryComparer class preceding to sort history data.
Custom formatting and display in the list control
To build a custom control that inherits from the ListView class, add a new class to your SalesForceApp project and name it HistoryList. Let's take a look at the skeleton code for the HistoryList class. Notice that this class has a ListView class as its base class. In the constructor, we set up the columns for the list view by code. We add two columns—one to display the timestamp of the historical record and another to display the description. As this class inherits from ListView, it contains all the methods and properties available in the ListView control. To access them, use the base pointer. using System; using System.Windows.Forms; using System.Drawing; namespace CRMLive { public class HistoryList : System.Windows.Forms.ListView
[ 131 ]
Building the Mobile Sales Force Module { internal System.Windows.Forms.ColumnHeader ColumnHeader1; internal System.Windows.Forms.ColumnHeader ColumnHeader2; private HistoryCollection _Datasource; private GlobalVariables.SortingOrder _SortDirection = GlobalVariables.SortingOrder.Descending; private void SetupListView() { base.CheckBoxes = false; ColumnHeader1 = new ColumnHeader(); ColumnHeader2 = new ColumnHeader(); ColumnHeader1.Text = "Date/time"; ColumnHeader1.Width = 79; ColumnHeader2.Text = "Activity"; ColumnHeader2.Width = 133; base.Columns.Add(this.ColumnHeader1); base.Columns.Add(this.ColumnHeader2); base.Font = new System.Drawing.Font("Tahoma", 8.0F, System.Drawing.FontStyle.Regular); base.FullRowSelect = true; base.View = System.Windows.Forms.View.Details; } public HistoryList() { SetupListView(); } } }
[ 132 ]
Chapter 3
Based on the pseudocode mentioned earlier, you can write code to first sort the historical data, then go through each historical record and either add a title bar or a standard entry. private void LoadData() { int _counter; DateTime _dateInProcess = Convert.ToDateTime(null); ListViewItem _dateHeaderItem; ListViewItem _ActivityItem; History _history; //Re-sort the data first _Datasource.Sort(new HistoryComparer(_SortDirection)); base.Items.Clear(); //After sorting the data, we loop through each record for (_counter = 0; _counter <= _Datasource.Count - 1; _counter++) { _history = _Datasource[_counter]; //If it is a new day, we create a header entry if (_history.TimeStamp.DayOfYear != _dateInProcess.DayOfYear || _history.TimeStamp.Year != _dateInProcess.Year) { _dateHeaderItem = NewDateHeaderRow(_history.TimeStamp); base.Items.Add(_dateHeaderItem); _dateInProcess = _history.TimeStamp; } _ActivityItem = NewActivityRow(_Datasource[_counter]); base.Items.Add(_ActivityItem); } } //This function adds a standard entry row to the ListView //control.Take note that we pad the date with whitespace to //achieve an Indented effect private ListViewItem NewActivityRow(History HistoryItem) [ 133 ]
Building the Mobile Sales Force Module { ListViewItem _activityItem; _activityItem = new ListViewItem(" " + String.Format("{0:hh:mm tt}", HistoryItem.TimeStamp)); _activityItem.SubItems.Add(HistoryItem.Subject); _activityItem.BackColor = Color.White; _activityItem.ForeColor = Color.DarkGreen; return _activityItem; } //This function adds a date header row to the ListView control private ListViewItem NewDateHeaderRow(DateTime DateValue) { ListViewItem _dateheaderItem; _dateheaderItem = new ListViewItem(String.Format("{0:ddMMM-yyyy}",DateValue)); _dateheaderItem.BackColor = Color.DeepSkyBlue; _dateheaderItem.ForeColor = Color.White; return _dateheaderItem; }
You can also provide a Datasource property to facilitate data binding from the AccountViewer form. public HistoryCollection Datasource { get { return _Datasource; } set { _Datasource = value; if (_Datasource != null) { LoadData(); } } }
[ 134 ]
Chapter 3
You can also make the headers clickable to dynamically change sorting direction using the following code snippet: private void HistoryList_ColumnClick(object sender, System.Windows.Forms.ColumnClickEventArgs e) { if (e.Column == base.Columns.IndexOf(ColumnHeader1)) { if (_SortDirection == GlobalVariables.SortingOrder.Ascending) { _SortDirection = GlobalVariables.SortingOrder.Descending; } else { _SortDirection = GlobalVariables.SortingOrder.Ascending; } LoadData(); } }
Using the History list control
Create the History tab in the AccountViewer form and drag the HistoryList control you've created into the tab frame. You only need to bind the BaseAccount. Histories property to the control for the data to show up. You can do this in the SetupBindings() method of the AccountViewer form: public void SetupBindings() { if (_account != null) { AccountBindingSource.DataSource = typeof(BaseAccount); AccountBindingSource.Add(_account); . . . hlHistories.DataBindings.Add(new Binding("DataSource", AccountBindingSource, "Histories", true)); } }
[ 135 ]
Building the Mobile Sales Force Module
Testing the History list control
You might not be able to test this control at the moment as there is no data. You will use this control later on in Chapter 5, Building Integrated Services, to view captured phone and SMS communication records.
Building the ProductList control
The ProductList control is yet another specialized list control that displays a checklist of products. It allows the user to select products that a lead, opportunity, or customer might be interested in by ticking this checklist.
This control will first retrieve the latest list of products from the Products table in the database via the GetProductList() method you've created in Chapter 2. When the user attempts to save the account record, the product codes of the selected items in the ProductList control will be saved in XML format using Microsoft's XML libraries. This XML object is then serialized into a single string, which is eventually committed to the BaseAccount.InterestedProds field. A similar process also happens in the opposite direction. When an account record is loaded, the XML object is rebuilt from the BaseAccount.InterestedProds field. A code routine runs through this XML object to retrieve the selected product codes and correspondingly places a tick next to each item in the checklist. Let's begin to build this control. Add a new class to your SalesForceApp project and make it inherit the ListView class. The SetupListView() method sets up the columns and settings for this list. using using using using
System; System.Windows.Forms; System.Xml; System.IO; [ 136 ]
Chapter 3 namespace CRMLive { public class ProductList : System.Windows.Forms.ListView { private string _Datasource; internal System.Windows.Forms.ColumnHeader ColumnHeader1; internal System.Windows.Forms.ColumnHeader ColumnHeader2; internal System.Windows.Forms.ColumnHeader ColumnHeader3; private XmlDocument _xmlDoc; private XmlElement _xmlRoot;
private void SetupListView() { base.CheckBoxes = true; ColumnHeader1 = new ColumnHeader(); ColumnHeader2 = new ColumnHeader(); ColumnHeader3 = new ColumnHeader(); ColumnHeader1.Text = "Code"; ColumnHeader1.Width = 59; ColumnHeader2.Text = "Product Name"; ColumnHeader2.Width = 100; ColumnHeader3.Text = "Product Price"; ColumnHeader3.Width = 50; base.Columns.Add(this.ColumnHeader1); base.Columns.Add(this.ColumnHeader2); base.Columns.Add(this.ColumnHeader3); base.Font = new System.Drawing.Font("Tahoma", 8.0F, System.Drawing.FontStyle.Regular); base.FullRowSelect = true; base.View = System.Windows.Forms.View.Details; }
private void MarkProduct(string ProductCode) { int _counter; for (_counter = 0; _counter <= base.Items.Count - 1; _counter++) [ 137 ]
Building the Mobile Sales Force Module { if (String.Compare(base.Items[_counter].Text, ProductCode,true) == 0) { base.Items[_counter].Checked = true; return; } } } public ProductList() { SetupListView(); LoadProducts(); } } }
The LoadProducts() method will loop through the retrieved ProductCollection and generate the corresponding list view items. private void LoadProducts() { ProductCollection _products; int _counter; ListViewItem _listViewItem; Product _product; try { base.Items.Clear(); _products = GlobalArea.Application.GetProductList(); for (_counter = 0; _counter <= _products.Count – 1; _counter++) { _product = _products[_counter]; _listViewItem = new ListViewItem(_product.ProductCode); _listViewItem.SubItems.Add (_product.ProductName); _listViewItem.SubItems.Add (_product.ProductPrice.ToString()); [ 138 ]
Chapter 3 base.Items.Add(_listViewItem); } } catch (Exception) { } }
At this point we have a list view control that can display a checklist of all products stored in the database, but without the capability to save or load any user selection yet.
Using XML DOM to store and retrieve product selection
The .NET Compact Framework provides a set of classes that allow you to manipulate XML. These classes are contained in the System.Xml namespace. The XMLDocument class is the first class you need to get familiar with. It provides the methods to create new XML elements and attributes. In the Get() method of the Datasource property shown in the following snippet, we loop through all ListView items and for those items that are checked, create a new XML element to store the corresponding Product Code. For example, if the user ticked two products from the checklist, you will end up with the following XML:
The code snippet to achieve the above is shown as follows: public string Datasource { get { XmlElement _xmlProduct; string _productCode; XmlTextWriter _xmlWriter; StringWriter _stringWriter; int _counter; //Loop through the ListView items and create the //corresponding XML object [ 139 ]
Building the Mobile Sales Force Module _xmlDoc = new XmlDocument(); _xmlRoot = _xmlDoc.CreateElement("Root"); _xmlDoc.AppendChild(_xmlRoot); for (_counter = 0; _counter <= base.Items.Count - 1; _counter++) { if (base.Items[_counter].Checked == true) { _productCode = base.Items[_counter].Text; _xmlProduct = _xmlDoc.CreateElement("Product"); _xmlProduct.SetAttribute("ProductCode", _productCode); _xmlRoot.AppendChild(_xmlProduct); } } //Serialize the XML into a single string _stringWriter = new StringWriter(); _xmlWriter = new XmlTextWriter(_stringWriter); _xmlDoc.WriteTo(_xmlWriter); _xmlWriter.Close(); _xmlWriter = null; _xmlDoc = null; return _stringWriter.ToString(); } set { int _counter; XmlElement _xmlProduct; string _productCode; _Datasource = value; if (_Datasource.Length > 0) { //Rebuilds the XML object from a single string _xmlDoc = new XmlDocument(); _xmlDoc.LoadXml(_Datasource); //Loops through each XML element and retrieves the //stored product codes. The MarkProduct() //method is called to place a tick next to each //corresponding ListView item
[ 140 ]
Chapter 3 _xmlRoot = (XmlElement) (_xmlDoc.ChildNodes.Item(0)); for (_counter = 0; _counter <= _xmlRoot.ChildNodes.Count - 1; _counter++) { _xmlProduct = (XmlElement) (_xmlRoot.ChildNodes.Item(_counter)); _productCode = _xmlProduct.GetAttribute("ProductCode"); MarkProduct(_productCode); } _xmlDoc = null; } } }
Using the ProductList control
Create the Interest tab in the AccountViewer form and drag the ProductList control you've created into the tab frame. As the ProductList control is capable of retrieving the list of Products from the database on its own, the only data you need to bind to this control would be the user selection XML, contained in the BaseAccount. InterestedProds field. You can do this in the SetupBindings() method of the AccountViewer form: public void SetupBindings() { if (_account != null) { AccountBindingSource.DataSource = typeof(BaseAccount); AccountBindingSource.Add(_account); . . . plProducts.DataBindings.Add(new Binding("DataSource", AccountBindingSource, "InterestedProds", true)); } }
[ 141 ]
Building the Mobile Sales Force Module
Testing the ProductList control
Before you can test this control, you will need to have a few sample products in your database. Open the Query Analyzer or MSql tool (depending on the database you are using), and run the following SQL: INSERT INTO Products(ProductCode, ProductName, ProductPrice) VALUES('C1', 'The Chair', 2500) INSERT INTO Products(ProductCode, ProductName, ProductPrice) VALUES('C2', 'Annual Subscription', 500) INSERT INTO Products(ProductCode, ProductName, ProductPrice) VALUES('C3', '6-month subscription', 300)
For the Oracle Lite database, don't forget to explicitly specify the sequence number source for the ProductID column.
Run the sales force application again. Navigate to the Interest tab. You will find the list of products showing in the Datagrid. Tick any number of items you wish and click Save to save the account details together with your selection. If you browse the database using Query Analyzer or MSql, you will find that your selection has been saved as XML in the InterestedProds column of the Accounts table.
Building a paged listing of accounts
In the previous sections, you've built a form that allows you to create and edit the details of a single account. In the sales force application, a user would also need to be able to browse through the list of created accounts. You will learn how to implement a form to show a paged list of accounts in this section. Paging is important when you present lists of data that are expected to grow in size over time to the end user. It allows you to build scalable applications that can handle large amounts of data without decrement to application performance. Imagine having to load thousands of records every time the user tries to access the list of active accounts! In this chapter you will implement paging at the SQL level. This involves writing SQL queries that can retrieve data within a certain range (for example: From the 10th record to the 20th record).
[ 142 ]
Chapter 3
The benefit of SQL-based paging is that it never retrieves more rows than it needs from the database. An SQL that is configured to retrieve 10 rows per page will always return 10 rows (or less) in the dataset. This can lead to better performance compared with other paging methods. To implement SQL-based paging, we will usually need to implement two functions at the data tier—one to return the count of all records, and another to return the actual data given the desired page number and page size. You can define these two additional functions in the IDataLibPlugin interface in your CRMLiveFramework project: int GetAccountsCountByType(int Type); DataSet GetAccountsByType(int Type, int TotalRecords, int PageNumber, int PageSize, string SortColumn, GlobalVariables.SortingOrder SortDirection);
Paging in SQL Server CE
Now that you have defined those additional functions, you will need to implement them. You can retrieve the record count using the following code snippet: public int GetAccountsCountByType(int Type) { SqlCeCommand _command; int _recordcount = 0; _command = _globalConnection.CreateCommand(); _command.CommandText = "SELECT COUNT(*) AS RecordCount FROM Accounts WHERE AccountType=" + Type; try { _recordcount = (int) (_command.ExecuteScalar()); } catch (Exception ex) { _recordcount = 0; throw (ex); } _command.Dispose(); _command = null; return _recordcount; } [ 143 ]
Building the Mobile Sales Force Module
Getting a range of data for a page from SQL Server CE is a bit tricky because, unlike the full SQL Server edition, it does not provide support for the ROW_NUMBER function, which would have made numbering and dividing sets of records into pages easier. The SQL statement to retrieve a specific page of data can be written as a composite of three individual SQL statements: Statement 1: Statement 2: Statement 3:
SELECT TOP(LastRowOfPage) FROM Table ORDER BY SortColumn ASC SELECT TOP(RowsInPage) FROM {Statement1} ORDER BY SortColumn DESC SELECT * FROM {Statement2} ORDER BY SortColumn ASC
The first SQL Statement sorts all records in ascending order and retrieves a chunk of records up to the last row of the desired page from the table. For instance, if we wanted to retrieve page 3, and assuming there are 10 rows per page, LastRowOfPage would be 30. The second SQL statement takes the result set generated from the first SQL statement and reorders them in descending order. It then returns an amount of rows equivalent to the number of rows in the page from the bottom. This means that if I wanted page 3, this second SQL statement will effectively produce rows 20 to 30 for me in descending order. The last SQL statement simply takes the resultset generated from the second one and reorders them in the original ascending direction. Translating these SQL statements into code, you can create the function this way: public DataSet GetAccountsByType(int Type, int TotalRecords, int PageNumber, int PageSize, string SortColumn, GlobalVariables.SortingOrder SortDirection) { string _sortColumn; string _sortDirection; string _sortOppDirection; SqlCeCommand _command; SqlCeDataAdapter _adapter; DataSet _resultSet; int _initialSelectSize; int _pageRecordCount; _resultSet = new DataSet(); _sortColumn = SortColumn; if (_sortColumn.Length == 0) [ 144 ]
Chapter 3 { _sortColumn = "DateCreated"; } if (SortDirection == GlobalVariables.SortingOrder.Ascending) { _sortDirection = "ASC"; _sortOppDirection = "DESC"; } else { _sortDirection = "DESC"; _sortOppDirection = "ASC"; } _initialSelectSize = PageNumber * PageSize; if (PageNumber * PageSize >= TotalRecords) { //It's the last page _pageRecordCount = TotalRecords - ((PageNumber - 1) * PageSize); } else { _pageRecordCount = PageSize; } _command = _globalConnection.CreateCommand(); _command.CommandText = "SELECT * FROM (SELECT TOP(" + _pageRecordCount + ") * FROM (SELECT TOP(" + _initialSelectSize + ") * FROM Accounts WHERE AccountType=" + Type + " ORDER BY " + _sortColumn + " " + _sortDirection + ", AccountGUID DESC) AS [mytable] ORDER BY " + _sortColumn + " " + _sortOppDirection + ", AccountGUID ASC) AS [mytable2] ORDER BY " + _sortColumn + " " + _sortDirection + ",AccountGUID DESC"; _adapter = new SqlCeDataAdapter(_command); _adapter.Fill(_resultSet, "Accounts"); _adapter.Dispose(); _command.Dispose(); return _resultSet; } [ 145 ]
Building the Mobile Sales Force Module
Paging in Oracle Lite
The code to retrieve the count of records is the same in Oracle Lite, so we'll just look at the code to retrieve page data. Fortunately, Oracle Lite provides a ROWNUM function that can dynamically tag each row of the result set with a running number. This makes segregation of the data into pages easier. For example, if you wanted to retrieve all the rows in page 2, you could simply run the following SQL: SELECT * FROM Table WHERE ROWNUM>=10 AND ROWNUM<20
You can make the call to Oracle Lite to retrieve paged data in the following fashion: public System.Data.DataSet GetAccountsByType(int Type, int TotalRecords, int PageNumber, int PageSize, string SortColumn, GlobalVariables.SortingOrder SortDirection) { string _sortColumn; string _sortDirection; OracleCommand _command; OracleDataAdapter _adapter; DataSet _resultSet; int _lowerlimit; int _upperlimit; _resultSet = new DataSet(); _sortColumn = SortColumn; if (_sortColumn.Length == 0) { _sortColumn = "DateCreated"; } if (SortDirection == GlobalVariables.SortingOrder.Ascending) { _sortDirection = "ASC"; } else { _sortDirection = "DESC"; } _lowerlimit = ((PageNumber - 1) * PageSize) + 1; _upperlimit = _lowerlimit + PageSize - 1; _command = (Oracle.DataAccess.Lite.OracleCommand) (_globalconnection.CreateCommand());
[ 146 ]
Chapter 3 _command.CommandText = "SELECT ROWNUM, TO_CHAR(AccountGUID) AS AccountGUID, AccountType, DateCreated, Firstname, Lastname, Status, Reception, Source, ResPhoneNo, MobPhoneNo, EmailAddress, Street, City, State, Zipcode, Country, Website, InterestedProds FROM Accounts WHERE AccountType=" + Type + " AND ROWNUM>=" + _lowerlimit + " AND ROWNUM<=" + _upperlimit + " ORDER BY " + _sortColumn + " " + _sortDirection; _adapter = new OracleDataAdapter(_command); _adapter.Fill(_resultSet, "Accounts"); _adapter.Dispose(); _command.Dispose(); _adapter=null; _command=null; return _resultSet; }
Building a paging user control
You can build the paged listing of accounts as a user control. Using a user control would be a good idea, as you will likely need to use paging again when you display search results in Chapter 4, Building Search Functionality. You should also build a couple of business object classes to encapsulate the raw datasets generated from the data layer. You can name these classes AccountSummary and AccountSummaryCollection. Create a user control with the following layout shown. You will probably also need to add a few column styles to the DataGrid object (as you did earlier for the Tasks DataGrid).
[ 147 ]
Building the Mobile Sales Force Module
The first thing you must do in your user control is to set up the navigation panel itself. You need to get the total number of records, and then decide to enable or disable the navigation buttons accordingly. For instance, when the user is currently browsing page 1 of 3, the Back and First buttons should be disabled, and the Next and Last buttons enabled. However, if there is only 1 page of data in total, then all navigational buttons should be disabled. Let's take a look at the navigation panel setup: private void SetupNavigationPanel() { _totalRecords = GlobalArea.Application.GetAccountsCountByType (_type); //Initialize the paging mechanism _pageCount = ((_totalRecords - 1) / _recordsPerPage) + 1; if (_pageCount == 0) { btnNavigateBack.Enabled = false; btnNavigateFirst.Enabled = false; btnNavigateLast.Enabled = false; btnNavigateNext.Enabled = false; txtPageInfo.Text = "No records found"; } else { RefreshPageInfoDisplay(); } }
Every time the user navigates from one page to another, you need to redetermine if any of the navigational buttons need to be disabled or enabled. We can do this in a RefreshPageInfoDisplay() function: private void RefreshPageInfoDisplay() { txtPageInfo.Text = "Page " + _currentPage.ToString() + " of " + _pageCount.ToString(); if (_currentPage == _pageCount) { btnNavigateNext.Enabled = false; btnNavigateLast.Enabled = false; [ 148 ]
Chapter 3 } else { btnNavigateNext.Enabled = true; btnNavigateLast.Enabled = true; } if (_currentPage == 1) { btnNavigateBack.Enabled = false; btnNavigateFirst.Enabled = false; } else { btnNavigateBack.Enabled = true; btnNavigateFirst.Enabled = true; } }
The RefreshPage() function calls the GetAccountsByType() function, passes the dataset to the AccountSummaryCollection business object, and then passes the business object to the Datagrid for display: private void RefreshPage() { try { _Accounts = GlobalArea.Application.GetAccountsByType (_type, _totalRecords, _currentPage, _recordsPerPage, _SortColumn, _SortOrder); dgAccounts.DataSource = _Accounts; dgAccounts.Refresh(); } catch (Exception ex) { GlobalArea.ErrorHandler.DisplayError (ex, "Get page data"); } }
[ 149 ]
Building the Mobile Sales Force Module
Navigating between pages is easy. You simply increase or decrease the page number and call both the RefreshPage() and RefreshPageInfoDisplay() functions preceding to update the interface: public void btnNavigateBack_Click(System.Object sender, System.EventArgs e) { _currentPage--; RefreshPage(); RefreshPageInfoDisplay(); } public void btnNavigateNext_Click(System.Object sender, System.EventArgs e) { _currentPage++; RefreshPage(); RefreshPageInfoDisplay(); }
Creating a context menu for the paging user control Let's add a context menu to your paging user control by dragging one from the Visual Studio toolbox into your user control. You will find that you can now design your context menu:
Add a menu item with the caption Sort to your context menu, and add two submenu items under this menu. This will allow the user to sort the page data by name or by date created. You will also need to associate the context menu you have just created with the Datagrid control for it to show up. Set the ContextMenu property of the Datagrid control to the name of the context menu you've created. [ 150 ]
Chapter 3
Writing the code to re-sort page data isn't difficult either. We simply set the sorting column and sorting order and the RefreshPage() function will pass them down to the database plugins where they will eventually get incorporated into SQL as an ORDER BY statement. public void mnuSortByName_Click(System.Object sender, System.EventArgs e) { _SortColumn = "FirstName"; _SortOrder = GlobalVariables.SortingOrder.Ascending; RefreshPage(); } public void mnuSortByDateCreated_Click(System.Object sender, System.EventArgs e) { _SortColumn = "Datecreated"; _SortOrder = GlobalVariables.SortingOrder.Descending; RefreshPage(); }
Launching the accounts listing page
To launch the accounts listing page, simply add the following highlighted section to the NavigationService class: using System.Windows.Forms; namespace CRMLive { public class NavigationService { public static DialogResult ShowDialog(string DialogName, object Arg1) { DialogResult _dialogResult = DialogResult.Cancel; switch (DialogName) { case "Leads": GlobalArea.Application.ShowBusyCursor(); AccountsListing _Leads = new AccountsListing(); GlobalArea.Application.ShowDefaultCursor(); _Leads.SetupNavigator (BaseAccount.AccountTypes.Lead, 10); [ 151 ]
Building the Mobile Sales Force Module _dialogResult = _Leads.ShowDialog; _Leads.Close(); _Leads.Dispose(); _Leads = null; . . . } return _dialogResult; } } }
Testing the accounts listing page
Run the sales force application now. Double-click the Leads icon. You will be able to see the listing of accounts shown in a DataGrid. Create enough records so that you have more than 10 accounts on this page. You will be able to browse through the different pages of records using the navigation buttons at the top. Take note that these navigational buttons will be enabled or disabled accordingly when you have reached the maximum or minimum page number.
Summary
In this chapter, you have seen how you can design a logic tier comprising of business objects that encapsulate the raw datasets retrieved from the data tier. You have also designed reusable UI components by inheriting from existing .NET Compact Framework controls and also via user controls. Through the TaskDetailViewer form, you've learned how to build the interfaces to handle master-detail data. You've learned the various approaches to handling file attachments and how you can build a repository to store uploaded files through the FileManager class. Through the HistoryList control, you've learned how you can use an inherited ListView control to achieve custom formatting and sorting. And through the ProductList control, you've learned how to load and store XML data in the database. You've also made use of the data tier you've built in the previous chapter to commit an entire dataset to the database and vice versa from the main AccountViewer form. More importantly, you have used one of the core features of data-driven programming—data binding to bind presentation-layer controls to your business objects. [ 152 ]
Chapter 3
You have also used SQL-based paging for both the Oracle Lite and SQL Server CE databases to return result sets of a fixed size and explored how to build a user control to handle the navigation between pages of data. At this point you have already built more than half of the sales force application, and you now have a functional sales force application working on your mobile device that can at least store and retrieve data! The next few chapters aim to add some additional functionality to your sales force application. In the next chapter, you will see how you can build a robust search facility to search through file content and data.
[ 153 ]
Building Search Functionality It is important to understand that most mobile device users are busy people—they are likely to be talking to a customer or conversing on the phone while they use your application. The last thing they want is to spend too much time interfacing with the application rather than getting at the information they need. A well-designed search feature attempts to reduce, as much as possible, the number of keystrokes (or stylus taps) required to get to the desired data. In this chapter, we take a look at the two most common types of searches— parameterized search and full-text search. Parameterized search is specific—it allows you to perform a search on a combination of specific data fields (such as the account name or account ID) and is usually quite precise. Full-text search is more general—it searches for a phrase or keyword within a body of text, such as file content. It is relatively easy to build parameterized search functionality on the mobile platform. However, full-text search on the mobile device has remained a largely neglected area. On the desktop, developers had the convenience of relying on the database for full-text search support. In the mobile environment, both the SQL Server CE and Oracle Lite databases do not support this functionality, leaving the developer little choice but to resort to workarounds. As a result, most mobile developers choose to either omit full-text search functionality entirely, use a third-party search engine, or simply fall back to the thin-client model (and leave full-text search support to the full SQL Server database on the server). In this chapter, you will attempt to build your own full-text search functionality with what is readily available in SQL Server CE and Oracle Lite. Before that, let's take a look at an overview of what you are going to build.
Building Search Functionality
A brief walk-through of parameterized search You will be adding two new forms to the main menu in this chapter—one for full-text search and another for parameterized search.
Double-clicking the Parameter Search icon produces the following screen. The parameterized search form you are going to build will support the following features: •
The user will be able to search for an account using a combination of any of the fields in this form.
•
The search form will also allow the user to search across leads, opportunities, and customers.
•
The search form will allow the user to perform partial searches on the First name and Last name fields.
[ 156 ]
Chapter 4
After clicking Search, the application will build a search query from the parameters specified in this window and use it to search against the database. The search results returned are then displayed in the following window:
You will reuse the paging control you've created in the previous chapter to provide paging functionality for the search results listing. Take note that this listing also has an additional column (Type) on the right to denote the type of the account. The end user can choose to sort the search results by name, date created, or type of the account. The user can also choose to launch into the details screen for a selected account from this window using the Accounts | Edit selected item menu item on the menu bar. The Accounts menu will also provide an option for users to delete a selected item.
Building the parameterized search feature
The concept of parameterized search is simple. At its core it is nothing more than a SELECT SQL statement with a WHERE clause built from the user's search parameters. It allows the user to combine multiple search parameters to narrow down the search results.
[ 157 ]
Building Search Functionality
Choosing which fields to expose to the user as a search parameter is an important consideration in building your search form. As screen space is a precious commodity, you don't want to clutter up your search form unnecessarily. Include only fields that are commonly used by your users to lookup their data. For instance, searching for an account using the account ID or account name is useful, but not by phone number or e-mail address.
The first thing you will need to do is to create a class to hold the search parameters keyed in by the user. This is necessary to transport these search parameters between the different tiers of the application. Passing search parameters around Although you could also pass search parameters around through function arguments, using a class is generally better; if, in the future, you had to add new search parameters to your application, you would not have to change these function declarations in any way. Aesthetically, GetAccountsByParameters (mySearchParams) also looks a lot better than GetAccountsByParameters (AcctFirstName, AcctLastName, Status, AccountType…).
Add a new class named AccountSearchParameters to the CRMLiveFramework project. You should place it in this project because the database plugin projects will also need access to this class type. public class AccountSearchParameters { private string _FirstName; private string _LastName; private int _Status; private string _AccountTypes; public string FirstName { Get {return _FirstName;} set {_FirstName = value;} } public string LastName { Get {return _LastName;} set {_LastName = value;}
[ 158 ]
Chapter 4 } public string AccountType { Get {return _AccountTypes;} set {_AccountTypes = value;} } public int Status { Get {return _Status;} set {_Status = value;} } }
Keeping in mind that a long list of search results might be returned, you need to implement paging when you display these search results. You will need to create two functions at the data tier—one to return the search result record count and the other to return a dataset filled with the search results for a specific page. Does this sound familiar? It should—if you recall, you've built two similar functions in the paging example in the previous chapter. Define these two new functions in IDataLibPlugin so that you can write separate implementations for SQL Server CE and Oracle Lite. The function definitions follow: DataSet GetAccountsByParameters(AccountSearchParameters SearchParam, int TotalRecords, int PageNumber, int PageSize, string SortColumn, GlobalVariables.SortingOrder SortDirection); int GetAccountsCountByParameters(AccountSearchParameters SearchParam);
Now let's take a look at how you can implement these two functions in SQL Server CE and Oracle Lite.
[ 159 ]
Building Search Functionality
Creating the parameterized search query in SQL Server CE
Both the GetAccountsCountByParameters() and GetAccountsByParameters() function calls require you to dynamically translate the user's search parameters (held in an AccountSearchParameters object) into an SQL WHERE clause. You will first need to create the function that does this. Let's call this function BuildWhereClause(). private string BuildWhereClause(AccountSearchParameters SearchParam) { string _clause = ""; if (SearchParam.FirstName.Length > 0) { _clause += ((_clause.Length > 0) ? " AND " : "") + "FirstName LIKE \'%" + SearchParam.FirstName + "%\'"; } if (SearchParam.LastName.Length > 0) { _clause += ((_clause.Length > 0) ? " AND " : "") + "LastName LIKE \'%" + SearchParam.LastName + "%\'"; } if (SearchParam.Status != 0) { _clause += ((_clause.Length > 0) ? " AND " : "") + "Status = " + SearchParam.Status; } if (SearchParam.AccountTypes.Length > 0) { _clause += ((_clause.Length > 0) ? " AND " : "") + "AccountType IN (" + SearchParam.AccountTypes + ")"; } if (_clause.Length > 0) { _clause = "WHERE " + _clause; } return _clause; }
[ 160 ]
Chapter 4
Now that you have done this, let's take a look at the GetAccountsCountByParameters() function. This function calls the BuildWhereClause() function you've just created and attaches the generated WHERE clause to the main SQL statement. public int GetAccountsCountByParameters (AccountSearchParameters SearchParam) { SqlCeCommand _command; int _recordcount = 0; string _whereclause; _command = _globalConnection.CreateCommand(); _whereclause = BuildWhereClause(SearchParam); _command.CommandText = "SELECT COUNT(*) AS RecordCount FROM Accounts " + _whereclause; try { _recordcount = (int) (_command.ExecuteScalar()); } catch (Exception ex) { _recordcount = 0; throw (ex); } _command.Dispose(); _command = null; return _recordcount; }
The second function, GetAccountsByParameters(), is similar to the page-retrieving function (GetAccountsByType()) you've created in the previous chapter. The generated SQL statement follows the same method of embedding multiple SQL statements to implement paging. public DataSet GetAccountsByParameters(AccountSearchParameters SearchParam, int TotalRecords, int PageNumber, int PageSize, string SortColumn, GlobalVariables.SortingOrder SortDirection) { string _sortColumn; string _sortDirection; string _sortOppDirection;
[ 161 ]
Building Search Functionality SqlCeCommand _command; SqlCeDataAdapter _adapter; DataSet _resultSet; int _initialSelectSize; int _pageRecordCount; string _whereclause; _resultSet = new DataSet(); _sortColumn = SortColumn; if (_sortColumn.Length == 0) { _sortColumn = "DateCreated"; } if (SortDirection == GlobalVariables.SortingOrder.Ascending) { _sortDirection = "ASC"; _sortOppDirection = "DESC"; } else { _sortDirection = "DESC"; _sortOppDirection = "ASC"; } _initialSelectSize = PageNumber * PageSize; if (PageNumber * PageSize >= TotalRecords) { _pageRecordCount = TotalRecords - ((PageNumber - 1) * PageSize); } else { _pageRecordCount = PageSize; } _command = _globalConnection.CreateCommand(); _whereclause = BuildWhereClause(SearchParam); _command.CommandText = "SELECT * FROM (SELECT TOP(" + _pageRecordCount + ") * FROM (SELECT TOP(" + _initialSelectSize + ") * FROM Accounts " + _whereclause + " ORDER BY " + _sortColumn + " " + _sortDirection + ", AccountGUID DESC) AS [mytable] ORDER BY " + _sortColumn + " " + _sortOppDirection + ", AccountGUID ASC) AS [ 162 ]
Chapter 4 [mytable2] ORDER BY " + _sortColumn + " " + _sortDirection + ",AccountGUID DESC"; _adapter = new SqlCeDataAdapter(_command); _adapter.Fill(_resultSet, "Accounts"); _adapter.Dispose(); _command.Dispose(); return _resultSet; }
Let us now take a look at how you can do the equivalent in Oracle Lite.
Creating the parameterized search query in Oracle Lite
The GetAccountsByParameters() function is similar to the page-retrieving function (GetAccountsByType()) you've created for Oracle Lite in the previous chapter, with the only changes required highlighted in the following code snippet: public DataSet GetAccountsByParameters(AccountSearchParameters SearchParam, int TotalRecords, int PageNumber, int PageSize, string SortColumn, GlobalVariables.SortingOrder SortDirection) { . . . _whereClause = BuildWhereClause(SearchParam); _command.CommandText = "SELECT ROWNUM, TO_CHAR(AccountGUID) AS AccountGUID, AccountType, DateCreated, Firstname, Lastname, Status, Reception, Source, ResPhoneNo, MobPhoneNo, EmailAddress, Street, City, State, Zipcode, Country, Website, InterestedProds FROM Accounts " + _whereClause + " AND ROWNUM>=" + _lowerlimit + " AND ROWNUM<=" + _upperlimit + " ORDER BY " + _sortColumn + " " + _sortDirection; . . .
You can reuse the previous BuildWhereClause() function for the Oracle Lite example, as there is no change in code for this function.
[ 163 ]
Building Search Functionality
The code for the GetAccountsCountByParameters() function is also similar, with the only changes required highlighted in the following code snippet: public int GetAccountsCountByParameters (AccountSearchParameters SearchParam) { . . . _whereclause = BuildWhereClause(SearchParam); _command.CommandText = "SELECT COUNT(*) AS RecordCount FROM Accounts " + _whereclause; . . . }
Encapsulating the retrieved Dataset using business objects
Now that you've laid out all the groundwork for parameterized search at the data tier, let's move on to the logic (business objects) tier. You can probably recall that in the previous chapter you've created the AccountSummary and AccountSummaryCollection business object classes. To refresh your memory, these two classes encapsulate a DataSet containing data from the Accounts table and expose two properties—the FirstName and LastName of the account. The AccountSummaryCollection class can be plugged directly into a DataGrid control for display. You can reuse these classes as the DataSet retrieved via the search is structurally the same. You can add the code to do this in the global Application class of your SalesForceApp project. public AccountSummaryCollection GetAccountsByParameters (AccountSearchParameters SearchParameters, int TotalRecords, int PageNumber, int PageSize, string SortColumn, GlobalVariables.SortingOrder SortDirection) { IDataLibPlugin _plugin = GlobalArea.PluginManager.GetActivePlugin; DataSet _accountsSummaryDataset = _plugin.GetAccountsByParameters (SearchParameters,
[ 164 ]
Chapter 4 TotalRecords, PageNumber, PageSize, SortColumn, SortDirection); DataTable _accountsSummaryDatatable = _accountsSummaryDataset.Tables["Accounts"]; return new AccountSummaryCollection (_accountsSummaryDatatable); } public int GetAccountsCountByParameters (AccountSearchParameters SearchParameters) { IDataLibPlugin _plugin = GlobalArea.PluginManager.GetActivePlugin; return _plugin.GetAccountsCountByParameters (SearchParameters); }
If you wish to show more columns in the search results listing besides the FirstName and LastName columns, you can add more columns of your own by adding the respective properties to the AccountSummary class.
Building the parameterized search forms
At this point, you have all the functions you need to search for data and to retrieve it as business objects. You will now need to build two screens—one to let the user specify their search parameters and another to display the search results. Let's move on to the final presentation tier.
Building the search form
The parameterized search form allows the user to search using a combination of the following fields: •
Account first name
•
Account last name
•
Account status
•
An option to include any of the three account types in the search—leads, opportunities, and customers
This form lets the user key in his or her search parameters and then saves it in an AccountSearchParameters object. This object can thereafter be easily passed around to the other tiers for processing.
[ 165 ]
Building Search Functionality
To begin, create a new form called SearchAccount and design it based on the following screenshot:
You would probably also want to add a Bindingsource control to the form to bind the controls on the form to your AccountSearchParameters object. You should place the binding code in the form load event. private AccountSearchParameters _AccountParams = new AccountSearchParameters(); public void SearchAccounts_Load(System.Object sender, System.EventArgs e) { SearchAccountsBindingSource.DataSource = typeof(AccountSearchParameters); SearchAccountsBindingSource.Add(_AccountParams); txtFirstName.DataBindings.Add(new Binding("Text", SearchAccountsBindingSource, "FirstName", true)); txtLastName.DataBindings.Add(new Binding("Text", SearchAccountsBindingSource, "LastName", true)); cbStatus.DataBindings.Add(new Binding("SelectedIndex", SearchAccountsBindingSource, "Status", true)); }
[ 166 ]
Chapter 4
The Search button simply signals to the binding control that editing is done. It then passes the entire AccountSearchParameters object to the next screen, which is the search results listing form. public void btnSearch_Click(System.Object sender, System.EventArgs e) { . . . SearchAccountsBindingSource.EndEdit(); NavigationService.ShowDialog("AccountsSearchSummary", ((object) _AccountParams)); } }
The code to launch the search results listing form can be added to the ShowDialog() function in the NavigationService class. Take special note that you need to pass the AccountsSearchParameters object to the search results listing form (following the code highlighted): public static DialogResult ShowDialog(string DialogName, object Arg1) { DialogResult _dialogResult = 0; switch (DialogName) { case "AccountsSearchSummary": AccountsSearchSummary _AccountsSearch = new AccountsSearchSummary(); _AccountsSearch.SearchParameters = (AccountSearchParameters) Arg1; _AccountsSearch.SetupNavigator(10); _dialogResult = _AccountsSearch.ShowDialog(); _AccountsSearch.Close(); _AccountsSearch.Dispose(); _AccountsSearch = null; break; . . .
[ 167 ]
Building Search Functionality
Building the search results listing form
You can reuse the paging control you've built in the previous chapter to browse through pages of search results. The search results listing form is similar to the AccountsListing form you've built in the last chapter. You can reuse the form design and code from that form. The only difference is that you need to pass the search parameters to this form. You can easily achieve this via an AccountSearchParameters object. You will also need to change the SetupNavigator() and RefreshPage() functions (highlighted) to call the search methods you've created earlier. private AccountSearchParameters _SearchParameters; public AccountSearchParameters SearchParameters { get {return _SearchParameters;} set {_SearchParameters = value;} } public void SetupNavigator(int RecordsPerPage) { _recordsPerPage = RecordsPerPage; _totalRecords = GlobalArea.Application.GetAccountsCountByParameters (_SearchParameters); pgPager.SetupPager(_totalRecords, RecordsPerPage); RefreshPage(); } private void RefreshPage() { _accounts = GlobalArea.Application.GetAccountsByParameters (_SearchParameters, _totalRecords, pgPager.CurrentPage, _recordsPerPage, _SortColumn, _SortOrder); dgAccounts.DataSource = _accounts; dgAccounts.Refresh(); }
Trying out your code
And that's it! You've built your first parameterized search function. Before you can try your code, you will need to create a new icon in the main menu and a corresponding entry in the NavigationService class to launch the SearchAccount form. I'll leave that as an exercise for you.
[ 168 ]
Chapter 4
In the next section, we will move on to an overview of full text search functionality and how to implement it.
A brief walk-through of full-text search
Now that you've seen what a parameterized search looks like, let's turn to full-text search. Full-text search functionality involves two different operations: •
Indexing: It is the process by which metadata or keywords are extracted from a file and stored elsewhere in a more easily accessible format for searching. Take an encyclopedia for example. (The type that is printed on paper, not the electronic one!). If you wanted to look up a section on grasshoppers, you wouldn't need to search through the book page by page looking for it. You could just flip to the glossary, look up the word, and it would tell you which page to turn to. Indexing is the process of creating this glossary.
•
Searching: It is the process of searching your document content for a specific phrase. After you've indexed a document, you do not need to search every line in your file for a particular phrase. You only need to search the created index. This leads to better performance by many orders of magnitude.
In your application, it makes good sense for the indexing to be performed automatically every time a user uploads a file. The best place to do this would be the File details pop up window in the Account Details area you've created in the previous chapter (shown in the following screenshot). When the user clicks the Save button, you could index the file right there and then.
[ 169 ]
Building Search Functionality
Moving on to the search interface itself, you will need to provide the user with a place to key in the search phrase and start the search. The full-text search window can be launched from the main menu by double-clicking the Search icon in the main menu. The full-text search screen features a single text box that allows the user to specify a search phrase and a Search button to run the search.
Your full-text search engine will also need to support basic Boolean searches. Boolean searches allow you to specify how multiple words in a search phrase are used in a search. In the previous screenshot, for instance, the search phrase Medical OR Certificate will indicate that documents containing either the word Medical or the word Certificate will be returned. In contrast, a search phrase consisting of two words Medical Certificate would indicate an AND relationship between these words, and would only return documents that contains both words. The following is a list of basic Boolean search operators commonly found in most search engines: Boolean operator
Description
Keyword1 AND Keyword2
The AND operator (usually omitted) specifies that *both* the keywords around this operator must exist in the document for it to be returned in the search results. Examples: "Medical AND Certificate" "Medical Certificate"
Keyword1 OR Keyword2
The OR operator specifies that all documents containing either one of the keywords should be returned.
Keyword1 NOT Keyword2
The NOT operator specifies that all documents containing Keyword1 but not keyword2 should be returned.
[ 170 ]
Chapter 4
The full-text search functionality runs the search phrase (with the Boolean logic applied) against the stored Keywords metadata for all documents. This can be done at the database level. All documents with matching results are returned in a paged list (again making use of the paging control you've created in the previous chapter).
The summary details window will display a summary of the text containing the search phrase (which is highlighted in bold in the previous screenshot). The name and size of the document is also displayed, with an Open file link at the side to let the user open the file directly. The name of the account is displayed as a link so that end users can easily jump into the details window of the account if they wish to.
Building the full-text search feature
As you've learned earlier, full-text search consists of two parts—indexing and searching. To index a file, you will need to be able to extract keywords from the file. As files come in all types and formats (for example: .PDF, .DOCX, .XLSX, .CAD), it will be difficult to try to extract keywords from every type of file out there. However, what you can do is build an extensible framework that lets you easily add support for more file formats in the future. You will build a common interface for Keyword Extractor classes—the IKeywordExtractor interface. Each keyword extractor will handle a specific file format, and its sole function will be to retrieve a set of keywords from that file.
[ 171 ]
Building Search Functionality
Creating the Keyword Extractor classes
The Keyword Extractor interface defines three abstract methods. These methods are self-explanatory, a Keyword Extractor class must be able to open a file, extract keywords (into a string), and subsequently close the file. public interface IKeywordExtractor { bool Open(string filePath); string ExtractKeywords(); bool Close(); }
We also create a KeywordExtractorBase class that offers common functionality across all keyword extractors. When you extract keywords from a file, you would most likely need to throw away common words that you don't need to index. For example, words like 'a', 'the', and 'an' do not need to be indexed. These words are called stop words. We can strip away stop words using Regular Expressions (as shown in the following highlighted function): public class KeywordExtractorBase { private string[] _stopWords; public string RemoveStopWords(string RawText) { string _regexPattern; int _counter; for (_counter = 0; _counter <= (_stopWords.Length - 1); _counter++) { _stopWords[_counter] = "\\b" + _stopWords[_counter] + "\\b"; } _regexPattern = "(" + string.Join("|", _stopWords) + ")"; return Regex.Replace(RawText, _regexPattern, "", RegexOptions.IgnoreCase); } public KeywordExtractorBase() { _stopWords = new string[] {"a", "and", "the", "of", "but"}; } }
[ 172 ]
Chapter 4
A sample keyword extractor—the HTML Keyword Extractor
When the user uploads a HTML file (for instance, a downloaded web page) to your application, it is readily indexable as it contains mostly text instead of binary information. However, a simple web page may contain lots of HTML tags that don't need to be indexed. Take a look at the following sample: This is my sample website, there are simply too many tags here
In a heavily formatted web page, the HTML tags not only add up to a lot of wasted space but also increase search inaccuracy, as these tags are picked up by the search as well. The HTML Keyword Extractor is an implementation of a keyword extractor that strips away the HTML tags from a document that is about to be indexed. This can be quite easily done via pattern matching using regular expressions. The following snippet of code shows how the HTMLKeywordExtractor class can be implemented. public class HTMLKeywordExtractor : KeywordExtractorBase, IKeywordExtractor { private StreamReader _reader; public bool Open(string filePath) { _reader = File.OpenText(filePath); return false; } public bool Close() { _reader.Close(); return false; } private string RemoveHTMLTags(string RawText) { return Regex.Replace(RawText, "<(.|\\n)*?>", string.Empty); [ 173 ]
Building Search Functionality } public string ExtractKeywords() { string _rawText; _rawText = _reader.ReadToEnd(); _rawText = RemoveHTMLTags(_rawText); _rawText = MyBase.RemoveStopWords(_rawText) return _rawText; } }
You can also build your own keyword extractors to handle other file formats. For example, a PDF file is not readily indexable as its content is stored in binary form. It's a great boon to users if they can search the content within their PDF files. To implement this, you could build another keyword extractor to extract the text from a PDF file. This is out of the scope of this book of course, but with the Keyword Extractor interface and base classes, you have the infrastructure to easily add support for more file formats in the future.
Indexing the file
We can have the application automatically index a file each time the user uploads one via the FileDetailViewer window created in the previous chapter (shown in the following screenshot).
You need to add the IndexFile() function to the File class that you've created in the previous chapter. This function checks the extension of the uploaded file and instantiates the corresponding keyword extractor (if one is available). The extracted keywords are then saved to the Keywords property, which eventually ends up being written to the Keywords column of the AccountFiles table. [ 174 ]
Chapter 4
You need to first add a new Keywords column to the AccountFiles table in your database. You should make it a TEXT field (in SQL Server CE) or a CLOB field (in Oracle Lite) as you might end up with a very large set of keywords. public void IndexFile() { string _attachmentName; IKeywordExtractor _keywordExtractor; _attachmentName = Attachment; switch (System.IO.Path.GetExtension(_attachmentName).ToLower()) { case "html": _keywordExtractor = new HTMLKeywordExtractor(); break; default: return; } _keywordExtractor.Open(_attachmentName); Keywords = _keywordExtractor.ExtractKeywords(); _keywordExtractor.Close(); }
As a last step, you can call the IndexFile() function from the click event handler of the Save button in the FileDetailViewer window to invoke the indexing process. Now that you have managed to extract keywords from uploaded files and save them to the database, we will look at how you can create an SQL query that can make use of these keywords to perform a Boolean search.
Creating the full-text search query for SQL Server CE
As you've learned earlier, SQL Server CE does not support any full-text search functionality and, therefore, does not come with the CONTAINS() and FREETEXT() T-SQL functions that would otherwise have made full-text search an easier task.
[ 175 ]
Building Search Functionality
The closest equivalent you are given is the CHARINDEX(sequence,expression) and PATINDEX(pattern,expression) T-SQL functions. These functions search text-based SQL fields for a given sequence or pattern and return the starting point of the pattern in the text (if it is found). Consider the following table for instance: TABLE [Fruits] ---------------------------FruitName FruitDescription ---------------------------Apple I love apples
Running the following SQL would produce the result 8 (the starting position of the word 'Apple' in the preceding text) SELECT CHARINDEX('apple',FruitDescription) FROM [Fruits]
The PATINDEX() function gives you a little more control, in that you can specify a small subset of regular expression patterns. An example follows: SELECT CHARINDEX('%apple[0-9]%',FruitDescription) FROM [Fruits]
So, how does the CHARINDEX() function translate to the full-text search feature? Let's take a look at an example. If the user, for instance, wanted to search for all documents containing either the Medical or Certificates keyword, he or she would key in Medical OR Certificates as the search phrase. You could basically retrieve a listing of the uploaded files matching these keywords using the following SQL: SELECT * FROM AccountFiles WHERE CHARINDEX( 'Medical', Keywords)>0 OR CHARINDEX('Certificates', Keywords)>0
The CHARINDEX()and PATINDEX() T-SQL functions, unlike the LIKE keyword, work well on large data types such as TEXT or NTEXT.
You now need to find a way to translate a Boolean search phrase into an SQL WHERE clause. The user can key in search phrases in many different ways. A few are shown in the following table: Search phrase example Medical OR Certificates OR Receipts Medical OR Certificates NOT Clinic Medical Certificates OR Medical Receipts NOT Medical
Description Containing any of the 'Medical', 'Certificates' or 'Receipts,' keywords Containing either the 'Medical' or the 'Certificates' keywords but must not contain the 'Clinic' keyword Containing either the 'Medical Certificates' phrase or the 'Medical Receipts' phrase Any document that does not contain the 'Medical' keyword [ 176 ]
Chapter 4
You can create a BuildWhereClause() function that takes in a search phrase keyed in by the user and converts it into SQL. The code is as follows: private string BuildWhereClause(string SearchPhrase) { int _counter; int _counter2; string[] _NOTPhrases; string[] _ORPhrases; string _NOTPhrase; string _ORPhrase; string _WhereClause; string _SubWhereClause; _WhereClause = ""; //Split the search phrase using 'NOT' as the delimiter //The \b Regex special character specifies a word boundary //and basically tells Regex to only consider occurrences of //'NOT' that are standalone words _NOTPhrases = Regex.Split(SearchPhrase, "\\bNOT\\b", RegexOptions.IgnoreCase); //Loop through the split parts for (_counter = 0; _counter <= (_NOTPhrases.Length - 1); _counter++) { _NOTPhrase = _NOTPhrases[_counter].Trim(); if (_NOTPhrase.Length > 0) { //Split this part further using 'OR' as the delimiter _ORPhrases = Regex.Split(_NOTPhrase, "\\bOR\\b", RegexOptions.IgnoreCase); _SubWhereClause = ""; //Here we enter another loop for (_counter2 = 0; _counter2 <= (_ORPhrases.Length – 1); _counter2++) { _ORPhrase = _ORPhrases[_counter2].Trim(); if (_ORPhrase.Length > 0) {
[ 177 ]
Building Search Functionality //Generate the CHARINDEX() expression for each //word in the search phrase and attaches them //together using the OR operator _SubWhereClause += ((_SubWhereClause.Length > 0) ? " OR " : "") + "CHARINDEX(\'" + _ORPhrase + "\',Keywords)>0"; } } if (_SubWhereClause.Length > 0) { //As we are still inside a loop that runs through //phrases separated by the NOT operator, //we reattach each formatted portion together //using the NOT operator _WhereClause += ((_WhereClause.Length > 0) ? " AND " : "") + (_counter > 0 ? "NOT " : "") + _SubWhereClause; } } } if (_WhereClause.Length > 0) { //If the WHERE clause is not empty, we append the //'WHERE' SQL keyword at the front _WhereClause = "WHERE " + _WhereClause; } return _WhereClause; }
As an example, passing the search phrase "Medical OR Certificate NOT Clinic" through the function above will produce the following: WHERE CHARINDEX('Medical',Keywords)>0 OR CHARINDEX('Certificate',Keywo rds)>0 AND NOT CHARINDEX('Clinic',Keywords)>0
As you've seen in the brief walk-through earlier on in this chapter, the full-text search results are displayed differently. Instead of the usual DataGrid listing, we choose to display in similar fashion to online search engines, which is to display each search result item with a text summary at the bottom.
[ 178 ]
Chapter 4
The text summary displayed isn't just a random snippet of text from the Keywords field. It must contain at least one of the search phrase keywords typed in by the user for the display to be meaningful. These keywords are highlighted in bold in the text summary. To retrieve these text summaries, you can use another T-SQL function available in SQL Server CE—the SUBSTRING(expression, starting_position, length) function. This function simply extracts a chunk of text (with a specified length) from a specified field and starting position. Hence, assuming you have the following data: TABLE [AccountFiles] --------------------------------------Keywords --------------------------------------This Medical Certificate proves that...
If you combine the CHARINDEX() function with the SUBSTRING() function in the following manner: SELECT SUBSTRING(Keywords,CHARINDEX('Medical',Keywords),12) FROM AccountFiles
You will get the following result: Medical Cert
You can use these two functions to extract the first chunk of text that contains the search phrase keywords. Keeping in mind that a search phrase may consist of more than one set of keywords (attached via the OR operator), you need to call these two functions for every OR keyword in the search phrase. The search phrase Medical OR Certificate NOT Clinic will hence translate to the following: SELECT SUBSTRING(Keywords,CHARINDEX('Medical',Keywords),100) AS [TextSummary1], SUBSTRING(Keywords,CHARINDEX('Certificate',Keywords), 100) AS [TextSummary2] FROM AccountFiles
Let's write some code to do this translation: private string BuildSelectClause(string SearchPhrase) { int _counter; int _counter2 = 1; string[] _NOTPhrases; string[] _ORPhrases; string _ActivePhrase; string _ORPhrase;
[ 179 ]
Building Search Functionality string _SelectClause; _SelectClause = ""; //Split the search phrase at the NOT operator _NOTPhrases = Regex.Split(SearchPhrase, "\\bNOT\\b", RegexOptions.IgnoreCase); //Everything after the NOT is not needed. We only need the //part on the left of the NOT operator _ActivePhrase = _NOTPhrases[0].Trim(); if (_ActivePhrase.Length > 0) { //Split the phrase using the OR operator _ORPhrases = Regex.Split(_ActivePhrase, "\\bOR\\b", RegexOptions.IgnoreCase); for (_counter = 0; _counter <= (_ORPhrases.Length - 1); _counter++) { _ORPhrase = _ORPhrases[_counter].Trim(); if (_ORPhrase.Length > 0) { //For each OR operator, we call the SUBSTRING and //CHARINDEX functions. Take note that we deduct 30 //characters from the starting point so that the //keyword does not appear exactly as the first //word in the text summary. This is purely for //aesthetic value _SelectClause += ((_SelectClause.Length > 0) ? "," : "") + "SUBSTRING(Keywords,CHARINDEX(\'" + _ORPhrase + "\',Keywords)-30,120) AS [TextSummary" + _counter2.ToString() + "]"; _counter2++; } } } if (_SelectClause.Length == 0) { //If no keyword was specified in the search phrase, we //simply return the first 120 characters of the text [ 180 ]
Chapter 4 _SelectClause = "SUBSTRING(Keywords,0,120) AS TextSummary1"; } return _SelectClause; }
Now that you've created this function, you can proceed to build the full-text search queries. The full-text search function is expected to return paged data, so this means that you will again implement two functions—one to return the search result count and another to return paged data. The code is very similar to the previous ones you've done. The code for the count function follows. It makes use of the BuildWhereClause() function you've created earlier. public int GetAccountFilesCountBySearchPhrase(string SearchPhrase) { . . . _whereClause = BuildWhereClause(SearchPhrase); _command.CommandText = "SELECT COUNT(*) AS RecordCount FROM AccountFiles " + _whereClause; . . . }
The code to retrieve the paged data follows next. It makes use of both the BuildWhereClause() and BuildSelectClause() functions you've created. public DataSet GetAccountFilesBySearchPhrase(string SearchPhrase, int TotalRecords, int PageNumber, int PageSize, string SortColumn, GlobalVariables.SortingOrder SortDirection) { . . . _command = _globalConnection.CreateCommand(); _whereClause = BuildWhereClause(SearchPhrase); _textSummaryClause = BuildSelectClause(SearchPhrase); _command.CommandText = "SELECT * FROM (SELECT TOP(" + _pageRecordCount + ") * FROM (SELECT TOP(" + _initialSelectSize + ") b.FirstName, b.LastName, b.AccountType, a.AccountGUID,a.AttachmentID, [ 181 ]
Building Search Functionality a.AttachmentName, a.AttachmentSize, a.Attachment," + _textSummaryClause + " FROM AccountFiles a LEFT JOIN Accounts b ON a.AccountGUID=b.AccountGUID " + _whereClause + " ORDER BY " + _sortColumn + " " + _sortDirection + ", AttachmentID DESC) AS [mytable] ORDER BY " + _sortColumn + " " + _sortOppDirection + ", AttachmentID ASC) AS [mytable2] ORDER BY " + _sortColumn + " " + _sortDirection + ",AttachmentID DESC"; . . . }
Let's now take a look at the equivalent for Oracle Lite.
Creating the full-text search query for Oracle Lite
In Oracle Lite, the equivalent of the CHARINDEX() and SUBSTR() functions are the INSTR() and SUBSTR() functions. Unfortunately, these two functions can only work on the CHAR or VARCHAR data types and not on the LONG or CLOB data types. This presents a problem for us. CHAR or VARCHAR data types are small by nature (they can only fit up to a maximum of 4,096 bytes), whereas the LONG and CLOB data types allow us to store data up to roughly 2 gigabytes in size. If we don't use the LONG and CLOB data types, we cannot
store keywords of any significant size. (A long text document, for instance, can easily contain thousands of unique words).
So, how do we use the CLOB data type to store keywords and yet retain the ability to perform a full-text search? Fortunately, Oracle Lite allows you to use the SQL LIKE operator to search for an expression within a CLOB field. We will use this in place of the CHARINDEX() function. However, there is no other equivalent workaround to obtain the location of a phrase in a CLOB field. So, in other words, we can retrieve a list of files matching the search phrase but we can't determine where the search phrase occurs in the file.
[ 182 ]
Chapter 4
Hence, for the text summary that is displayed with the search results, we will simply display the first 120 bytes of the text in the matching file. Let's take a look at the BuildWhereClause() function for Oracle Lite. The differences in the Oracle Lite code are highlighted in the following code: private string BuildWhereClause(string SearchPhrase) { int _counter; int _counter2; string[] _NOTPhrases; string[] _ORPhrases; string _NOTPhrase; string _ORPhrase; string _WhereClause; string _SubWhereClause; _WhereClause = ""; //Split the search phrase using the NOT operator _NOTPhrases = Regex.Split(SearchPhrase, "\\bNOT\\b", RegexOptions.IgnoreCase); for (_counter = 0; _counter <= (_NOTPhrases.Length - 1); _counter++) { _NOTPhrase = _NOTPhrases[_counter].Trim(); if (_NOTPhrase.Length > 0) { //Split the phrase further using the OR operator _ORPhrases = Regex.Split(_NOTPhrase, "\\bOR\\b", RegexOptions.IgnoreCase); _SubWhereClause = ""; for (_counter2 = 0; _counter2 <= (_ORPhrases.Length – 1); _counter2++) { _ORPhrase = _ORPhrases[_counter2].Trim(); if (_ORPhrase.Length > 0) { //We use the LIKE operator to do the comparison _SubWhereClause += ((_SubWhereClause.Length > 0) ? " OR " : "") + "a.Keywords LIKE \'%" + _ORPhrase + "%\'"; } } if (_SubWhereClause.Length > 0) { [ 183 ]
Building Search Functionality _WhereClause += ((_WhereClause.Length > 0) ? " AND " : "") + (_counter > 0 ? "NOT " : "") +_SubWhereClause; } } } if (_WhereClause.Length > 0) { _WhereClause = "WHERE " + _WhereClause; } return _WhereClause; }
The GetAccountFilesBySearchPhrase() function in Oracle is similar to the standard paged data retrieval function. The main differences are shown below. For Oracle Lite, you do not have to create a corresponding BuildSelectClause() function. It will always return the first 120 characters in the matching file. SQL joins in Oracle Lite Take note that Oracle Lite does not support the ANSI join syntax (a LEFT JOIN b). It uses the ODBC join syntax instead. ({OJ a LEFT JOIN b}). public DataSet GetAccountFilesBySearchPhrase(string SearchPhrase, int TotalRecords, int PageNumber, int PageSize, string SortColumn, GlobalVariables.SortingOrder SortDirection) { . . . _whereClause = BuildWhereClause(SearchPhrase); _command.CommandText = "SELECT ROWNUM, TO_CHAR(a.AccountGUID) AS AccountGUID, b.FirstName, b.LastName, b.AccountType, a.AttachmentID, a.AttachmentName, a.AttachmentSize, a.Attachment,SUBSTR(CAST(a.Keywords AS VARCHAR(120)),1,120) AS TextSummary1 FROM {OJ AccountFiles a LEFT JOIN Accounts b ON a.AccountGUID=b.AccountGUID} " + _whereClause + ((_whereClause.Length > 0) ? " AND " : " WHERE ") + " ROWNUM>=" + _lowerlimit + " AND ROWNUM<=" + _upperlimit + " ORDER BY " + _sortColumn + " " + _sortDirection; . . . } [ 184 ]
Chapter 4
The GetAccountFilesCountBySearchPhrase() function is also similar in many respects. The main differences are shown in the following code snippet: public int GetAccountFilesCountBySearchPhrase(string SearchPhrase) { . . . _whereclause = BuildWhereClause(SearchPhrase); _command.CommandText = "SELECT COUNT(*) AS RecordCount FROM AccountFiles " + _whereclause; . . . }
Encapsulating the retrieved dataset using business objects
As usual, you will need to encapsulate the retrieved dataset using a custom business object class (and corresponding collection object). It's fairly easy to create two new classes: AccountFileSummary and AccountFileSummaryCollection based on what you've learned in this chapter and the previous one. As a last step, you also need to add the two corresponding functions in the global
Application class to retrieve the dataset and pass them to the instantiated business
objects above.
Creating the full-text search forms
There are two forms you will need to build for the full-text search. The first form, the FullTextSearch form is simple—it presents a text box and a button for the user to key in his search phrase and run the search.
[ 185 ]
Building Search Functionality
On clicking the Search button, it simply passes the search phrase to the second form (which is the full-text search results summary page). public void btnSearch_Click(System.Object sender, System.EventArgs e) { NavigationService.ShowDialog("FullTextSearchSummary", txtSearch.Text); }
In your second form, the FullTextSearchSummary form, we no longer use a Datagrid control. The easiest way to display the search results as shown in the next screenshot would be to use HTML. To display HTML in our application, we will need to use the .NET Compact Framework's Webbrowser control.
You will still need to use the Pager control as you need to be able to flip between multiple pages of data. You can generate HTML code from the retrieved dataset using the following code: private void RefreshPage() { string _webOutput; string _textSummary; //Get a business object containing the list of account //files matching the search phrase _accountFiles = GlobalArea.Application.GetAccountFilesBySearchPhrase (_searchPhrase, _totalRecords, pgPager.CurrentPage, _recordsPerPage, "", GlobalVariables.SortingOrder.Ascending); [ 186 ]
Chapter 4 _webOutput = ""; foreach (AccountFileSummary _accountFile in _accountFiles) { //Retrieve the text summary and highlight the //keywords in bold _textSummary = _accountFile.TextSummary; _textSummary = HighlightKeywords(_textSummary, _searchPhrase); //Display a link that holds the account GUID and the //account type of the account _webOutput = _webOutput + "" + _accountFile.FirstName + " "; _webOutput = _webOutput + _accountFile.LastName + "
"; //Display a link with the full path of the file attachment _webOutput = _webOutput + "" + _accountFile.AttachmentName + " (" + Strings.Format(_accountFile.AttachmentSize, "###,###,###") + ") bytes" + "" + " Open file
"; _webOutput = _webOutput + "" + _textSummary + "
"; _webOutput = _webOutput + "
"; }
[ 187 ]
Building Search Functionality _webOutput = _webOutput + ""; //Assign the HTML string to the WebBrowser control, and //call the Show() function to display the HTML wbSearchSummary.DocumentText = _webOutput; wbSearchSummary.Show(); }
To highlight the list of keywords in the text summary, create the HighlightKeywords() function as in the following code: Private Function HighlightKeywords(ByVal TextSummary As String, ByVal SearchPhrase As String) As String Dim arrKeywords() As String Dim _keyword As String Dim _counter As Integer arrKeywords = Regex.Split(SearchPhrase, "\bOR\b|\bNOT\b", RegexOptions.IgnoreCase) For _counter = 0 To UBound(arrKeywords) _keyword = Trim(arrKeywords(_counter)) If Len(_keyword) > 0 Then TextSummary = Regex.Replace(TextSummary, _keyword, "" & _keyword & "",RegexOptions.IgnoreCase) End If Next _counter Return TextSummary End Function
When the user clicks on any of the links on this page, the WebBrowser control will receive a Navigating event notification. We can use this mechanism to determine which link the user clicked on by inspecting the URL of the link (which contains the desired information written by the RefreshPage() function shown previously). public void wbSearchSummary_Navigating(object sender, System.Windows.Forms.WebBrowserNavigatingEventArgs e) { string _temp; System.Guid _accountGUID; BaseAccount _account; string _filePath; string[] _info; BaseAccount.AccountTypes _type; [ 188 ]
Chapter 4 //User clicks on the 'Account Name' link if (e.Url.ToString().ToLower().StartsWith ("http://accountinfo/")) { e.Cancel = true; _temp = e.Url.ToString().Substring ("http://accountinfo/".Length); _info = Regex.Split(_temp, ",", RegexOptions.IgnoreCase); if ((_info.Length - 1) == 1) { _accountGUID = new System.Guid(_info[0]); _type = (CRMLive.BaseAccount.AccountTypes) (_info[1]); _account = GlobalArea.Application.GetAccount(_accountGUID, _type); NavigationService.ShowDialog("Edit" + BaseAccount.AccountTypeToString(_type), ((object) _account)); } } //User clicks on an 'Open File' link if (e.Url.ToString().ToLower().StartsWith ("http://fileid/")) { e.Cancel = true; _filePath = e.Url.ToString().Substring ("http://fileid/".Length); if (MessageBox.Show("Are you sure you wish to open this file","Open file",MessageBoxButtons.YesNo ,MessageBoxIcon.Question, MessageBoxDefaultButton.Button1 )==DialogResult.Yes) { Process.Start(_filePath, ""); } } }
[ 189 ]
Building Search Functionality
Trying out the full-text search
To try out the full-text search form, you will need to add another icon to the main menu form. You will also need to create a new entry in the NavigationService class to launch the FullTextSearch form. Once you have this setup, try launching the full-text search form. You need to of course, index a file before you can search for it, so create a new lead account and upload a few text-based files (such as HTML files) in the file attachments tab. Once you have done that, navigate to the full-text search form, and type in a search phrase. You can use the OR, AND, and NOT operators to narrow down your search. Assuming the search keywords exist in the indexed files, you will be able to see a list of the matching files.
Improving the full-text search engine
The search engine you have created so far works, but there is certainly much room for improvement. A few ways you can further improve this search engine are as follows: •
Creating a DOCX and PDF keyword extractor: The Microsoft Word DOCX and Adobe PDF formats are popular file attachment types. Letting your users search through DOCX and PDF content can indeed provide a lot of business value. You can easily create new keyword extractors for these file formats by leveraging the framework you have created.
•
Supporting nested Boolean queries: You can also build support for nested Boolean queries. A sample nested Boolean query might look like this: ((Medical OR Certificate) NOT Clinic) OR Hospital.
•
Creating a more comprehensive list of stop words to reduce the size of generated keywords: By increasing the size of the stop words list, you can increase the accuracy of your search by stripping away irrelevant text. Take note, however, that increasing the size of the stop words list means your application will need to incur more processing cycles to strip away this data. You will need to find the right balance between accuracy and performance for your project.
[ 190 ]
Chapter 4
•
Implementing search result ranking: You can also easily implement a search-result ranking system roughly similar to that used by Google. By keeping track of a popularity counter for each search result, and incrementing it every time the user opens that particular item, you can sort the search results by this popularity counter in descending order to show the most popular result at the top.
Summary
In this chapter, you've covered ground on how to build a parameterized search and full-text search engine from the ground up using the SQL Server CE and Oracle Lite databases. You saw how you could dynamically generate SQL statements based on the user's search parameters. You've also created an indexing framework and built a HTML keyword extractor to extract keywords from HTML files. Finally, you've seen how you can use SQL Server CE's CHARINDEX() and SUBSTR() T-SQL functions and the SQL LIKE function in Oracle Lite to perform a full-text search against these stored keywords in the database. In the next chapter, it gets a little more interesting. You will explore how you can write code to interact with the mobile device and the Windows Mobile operating system.
[ 191 ]
Building Integrated Services I still remember those days when mobile phones were just mobile phones. Nowadays, they are loaded with tons of hardware features that extend their use as full-blown life organizers and enterprise tools. As mobile device manufacturers find ingenious ways to squeeze in ever more hardware functionality, the .NET Compact Framework and other third-party vendors have been busy trying to write the software to keep up. Through Windows Mobile 6, a whole range of functionality has been added to let you, the developer, tap into the various hardware features of the mobile device. Third-party libraries like the Smart Device Framework have also been active in this area, releasing classes and controls that make .NET Compact Framework development easier than ever. In this chapter, you will learn how to add the following functionalities to the sales force application: •
Placing phone calls and sending SMS messages/e-mails to a lead directly
•
Detecting and logging all incoming and outgoing communication (SMS messages and phone calls)
•
Using the Bluetooth and Infrared capabilities of your mobile device to transfer a lead account between devices
•
Capturing a customer's signature
Sending SMS and e-mail from your application
As you are probably aware, Windows Mobile uses the integrated Outlook Mobile software to manage your tasks, appointments, e-mails, SMS, and MMS messages. Microsoft provides the Microsoft.WindowsMobile.PocketOutlook namespace, which allows you to utilize Outlook Mobile to programmatically send an SMS or e-mail.
Building Integrated Services
The POOM (Pocket Outlook Object Model) allows you to automate the Windows Mobile messaging application's UI. This means that you can use POOM to send SMS and e-mail messages, create tasks, iterate through your appointments, and so on. There are two ways to include SMS- and e-mail-sending capability in your application. One way is to build your own UI to let the user key in his or her message and use POOM to programmatically send the message. Alternatively, you could also delegate this functionality to the default Windows Mobile Compose window (and, to make it more convenient for the end user, prefill some of the fields in this window with the minimum data required—such as the intended recipient of the message). Let's take a look at both approaches in the following sections:
Sending SMS and e-mail directly through code
Sending an SMS through POOM is straightforward. Before you can use POOM, however, you must first import a reference to the Microsoft.WindowsMobile. PocketOutlook library.
All the code that you need to send an SMS is shown as follows: using Microsoft.WindowsMobile.PocketOutlook; public void SendSMS(string phoneNumber, string message) { [ 194 ]
Chapter 5 SmsMessage _msg; _msg = new SmsMessage(phoneNumber, message); _msg.Send(); } public static void main() { SendSMS("+6598532715","Hello world!"); }
You can send an e-mail through POOM using the EmailMessage class. The code to do this is similarly straightforward. public void SendEmail() { EmailMessage _em = new EmailMessage(); //Define recipients _em.To.Add(New Recipient("John Mayer", "[email protected]")); _em.To.Add(New Recipient("Ed Stables", "[email protected]")); //Define CC or BCC list, if any _em.CC.Add(New Recipient("Greg Yap", "[email protected]")); //Define attachments – you can attach multiple files _em.Attachments.Add(New Attachment("C:\documents\contract.docx")); _em.Attachments.Add(New Attachment("C:\documents\contract2.docx")); //Define other e-mail attributes _em.Importance = Importance.High; _em.Sensitivity = Sensitivity.Confidential; //Define the main e-mail subject and body _em.Subject = "The contract documents you asked for"; _em.BodyText = "Hey guys, as requested..."; //Send out the e-mail using an e-mail account on Mobile Outlook _em.Send("AcmeSMTPAccount"); }
[ 195 ]
Building Integrated Services
Delegating to the default Windows Mobile Compose UI
Outlook Mobile by default provides Compose windows that let you compose an SMS or e-mail message in Windows Mobile (accessible under the Messaging menu). These windows can be seen as follows:
Your application could use POOM to launch these windows (prefilled with some minimal data). For example, if you wanted to provide functionality to send an SMS using a phone number available in your application, you could launch the Compose Text Message window with the To phone number field filled in and the content of the SMS defaulted to some value. Let's see how this could be done in code: SMSMessage _msg = new SMSMessage(); _msg.To.Add(New Recipient("Ed", "+6598532715")); _msg.Body = "This message was sent from CRMLive!"; MessagingApplication.DisplayComposeForm(_msg);
You could do the same thing with e-mails by creating an EmailMessage object and feeding it to the same MessagingApplication.DisplayComposeForm() function.
Intercepting incoming SMS
In Chapter 3, Building the Mobile Sales Force Module, you've created a History tab in the Account Details window that displays logs of all communication with a lead, opportunity, or customer. These logs include incoming phone calls and SMS messages. In this section, you will write the code that generates these logs.
[ 196 ]
Chapter 5
To perform this, you would need to be able to detect SMS messages as they come in. Your application would then need to do the following: 1. Look at the origin (phone number) of these messages 2. Locate the lead account with the matching phone number in the sales force application 3. The log entry is then created under this account (if it is found)
Intercepting an SMS message
Intercepting an SMS message requires a bit more work compared to sending one. The Microsoft.WindowsMobile.PocketOutlook.MessageInterception namespace provided by Microsoft allows you to define a call back function that is automatically called when a new SMS message arrives. Let's take a look at how this is done in code. The first thing to do is to import the namespace we need. This is shown as follows: using Microsoft.WindowsMobile.PocketOutlook.MessageInterception;
Next, you need to create a MessageInterceptor object. You can specify one of these two values for the InterceptionAction argument in the constructor of this object: •
InterceptionAction.Notify This allows your application to 'peek' at the incoming SMS message without deleting it—this message is still available to other applications.
•
InterceptionAction.NotifyAndDelete If you choose this option, the SMS message will not be available to other applications after your application has received it.
The second argument defines whether to use the form thread to process the events. If your application does not use any form (for example, in the case of a console application), set this value to false. MessageInterceptor _SmsInterceptor; _SmsInterceptor = new MessageInterceptor(InterceptionAction.Notify, True); //You must then add an event handler for the MessageReceived() event //in the interceptor object. _SmsInterceptor.MessageReceived+= SmsReceivedEventHandler;
[ 197 ]
Building Integrated Services
Through the event arguments of the event handler, you can retrieve an SmsMessage object, which you can then use to retrieve the originating phone number (or SMS content). public void SmsReceivedEventHandler(ByVal sender As Object, ByVal e As MessageInterceptorEventArgs) { SmsMessage _msg = (SmsMessage) e.Message; MessageBox.Show("Received SMS from the following number: " + _msg.From.Address.ToString(),""); }
The MessageInterceptor object seen previously is typically created when your application starts, and you will only receive SMS receipt notifications when your application remains open. The moment you close your application, the object goes out of scope and you stop receiving SMS receipt notifications as well. If you are planning to process each and every single SMS message that comes into your phone over extended periods of time, it would make more sense to create a window-less application that runs constantly in the background, listening to the events raised by the MessageInterceptor class. You will see how you can do exactly this in the next few sections. Unfortunately, the MessageInterceptor object cannot be used to intercept an e-mail message. To intercept an e-mail, you would need to invoke MAPI (Messaging API) functionality on the device, which is out of the scope of this book.
Placing phone calls from your application
In most mobile development projects, convenience is a central theme. User friendliness is measured by how quick a user can get to the information that he or she needs, or how quickly he or she can execute a desired action. With this in mind, let's consider the following example. John might have created an application that manages a list of customers and their phone numbers. When viewing the details of a particular customer, the end user might wish to make a phone call to the customer. It wouldn't make much sense to have the user key in the phone number of the customer again as it is already onscreen. We could make it more convenient for the end user by placing a button called Make a phone call that automatically dials the onscreen number. [ 198 ]
Chapter 5
The Telephony library provided in the Microsoft.WindowsMobile namespace allows you to easily place a phone call directly from your application. Let's see how this can be done: Using Microsoft.WindowsMobile.Telephony; public object MakePhoneCall(string phoneNumber) { Phone _phone = new Phone(); _phone.Talk(phoneNumber); }
Detecting incoming phone calls
You saw earlier how you could detect incoming SMS messages using the MessageInterception class. How about incoming phone calls? You can do the same thing for phone calls but you'll need to use a different class—the SystemState class. The SystemState class is a useful class that allows you to query almost every type of state information on the mobile device. It allows you to query information such as battery life, missed phone calls, ActiveSync status, Bluetooth status, calendar events, appointment events, notification events, camera status, and phone signal strength, just to name a few. Not only can you query these statuses but you can also receive events in your code when these values change.
Let's take a look at the code you can use to do this. The method used to detect phone calls is roughly the same as the one used to detect incoming SMS messages. You need to set it to listen in the background for a specific status field and to register a call back function that can be automatically invoked when the status value changes. In the following code, we set it to listen to a status change in the incoming phone number: using Microsoft.WindowsMobile.Status; public void StartDetector() { _phoneCaller = new SystemState(SystemProperty.PhoneIncomingCallerNumber, true); _phoneCaller.Changed += phoneCaller_Changed; } [ 199 ]
Building Integrated Services
In the event handler, simply print the incoming phone number. private void phoneCaller_Changed(object sender, ChangeEventArgs args) { _IncomingphoneNumber = Strings.Trim(SystemState.PhoneIncomingCallerNumber); MessageBox.Show("The incoming call is from this number: " + _IncomingphoneNumber,""); }
The format of the phone number retrieved depends on the numbers you have stored in your Windows Mobile Contacts area. For instance, if you have a Windows Mobile Contact named My wife with the phone number +60163176148 and you receive a call from this number, the function preceding would yield: My Wife <+60163176148>
If the number does not exist in your Windows Mobile Contacts area, it would simply return the number (without any brackets): +60163176148
You can easily use regular expressions to parse and strip away all the unnecessary text if you want to extract only the phone number from this string.
Populating the History tab in the sales force application
Now that you know how to use the various mobile phone and OS features, you will need to build it into your sales force application. To jog your memory a little, you've created the HistoryList control in the AccountViewer form to view a list of all incoming and outgoing communication (SMS messages/phone calls) for each account. This screen is shown as follows:
[ 200 ]
Chapter 5
To populate this list, you will need to be able to detect SMS messages and phone calls as and when they are sent or received, and then generate the corresponding historical records in the database. You will need to do the following tasks: •
Create the relevant functions in the data tier to insert new historical records
•
Encapsulate SMS and phone functionality in a class each
•
Create the background application to intercept incoming SMS messages and phone calls—this will generate the corresponding historical records
•
Handle outgoing SMS messages and phone calls—this will also generate the corresponding historical records
Creating the data tier functions to insert historical records
The first thing you need to do is to create the Oracle Lite and SQL Server CE functions to insert historical records into the AccountHistories table. You'll need to define the following functions in the IDataLibPlugin interface: bool InsertHistoricalRecord(Guid AccountGUID, int OriginatingSource, string Subject, string Description); bool InsertHistoricalRecordByPhone(string phoneNumber, int OriginatingSource, string Subject, string Description);
The following list describes what each function can do: •
InsertHistoricalRecord(): This function generates a history record for a
•
InsertHistoricalRecordByPhone(): This function allows you to pass in
specified account.
the incoming phone number. It locates an account with the matching phone number and then calls the InsertHistoricalRecord() function to generate the history record under that account.
You will also notice in the preceding functions that you need to insert an
OriginatingSource value into the AccountHistories table. This is an integer value that takes on the value of 0 (incoming) or 1 (outgoing). It would be more
intuitive to use an enumerated type to represent this in your code. Add the following enumerated type to the GlobalVariables class in the CRMLiveFramework project. public enum Communications { Incoming=0, Outgoing=1 } [ 201 ]
Building Integrated Services
Now let's take a look at the SQL Server CE implementation for these two functions: public bool InsertHistoricalRecord(System.Guid AccountGUID, int OriginatingSource, string Subject, string Description) { SqlCeCommand _command; bool _result; _command = _globalConnection.CreateCommand(); _command.CommandText = "INSERT INTO AccountHistories(AccountGUID,OriginatingSource,Subject, Description,Timestamp) VALUES (@AccountGUID, @OriginatingSource, @Subject, @Description, GETDATE())"; _command.Parameters.Add("@AccountGUID", AccountGUID); _command.Parameters.Add("@OriginatingSource", OriginatingSource); _command.Parameters.Add("@Subject", Subject); _command.Parameters.Add("@Description", Description); try { if (_command.ExecuteNonQuery() == 1) { _result = true; } else { _result = false; } } catch (Exception ex) { throw (ex); _result = false; } _command.Dispose(); _command = null; return _result; } public bool InsertHistoricalRecordByPhone(string phoneNumber, int OriginatingSource, string Subject, string Description) { SqlCeCommand _command; SqlCeDataReader _datareader; [ 202 ]
Chapter 5 Guid _accountGUID; bool _result; //First we attempt to retrieve an account that matches the //incoming phone number passed in _command = _globalConnection.CreateCommand(); _command.CommandText = "SELECT AccountGUID FROM Accounts WHERE MobPhoneNo LIKE @PhoneNo OR ResPhoneNo LIKE @PhoneNo"; _command.Parameters.Add("PhoneNo", phoneNumber); try { _datareader = _command.ExecuteReader(); //If an account is found, we create the historical record for it if (_datareader.Read() == true) { _accountGUID = _datareader.GetGuid (_datareader.GetOrdinal("AccountGUID")); InsertHistoricalRecord(_accountGUID, OriginatingSource, Subject, Description); } _result = true; } catch (Exception ex) { _result = false; throw (ex); } _command.Dispose(); _command = null; return _result; }
[ 203 ]
Building Integrated Services
Encapsulating SMS functionality
You could of course access the POOM classes directly from your application, but it is a good idea to encapsulate its functionality in a custom class of your own. The MessagingService class handles both outgoing and incoming SMS messages. Add this class to the CRMLiveFramework project. Let's take a look at the following class: using using using using
System; System.Data; Microsoft.WindowsMobile.PocketOutlook; Microsoft.WindowsMobile.PocketOutlook. MessageInterception; using System.Text.RegularExpressions; namespace CRMLive { public class MessagingService { private SmsMessage _msg; private MessageInterceptor _SMSInterceptor; private string _IncomingSource; public delegate void IncomingSMSReceivedEventHandler(string IncomingPhoneNumber); private IncomingSMSReceivedEventHandler IncomingSMSReceivedEvent; public event IncomingSMSReceivedEventHandler IncomingSMSReceived { add { IncomingSMSReceivedEvent = (IncomingSMSReceivedEventHandler) System.Delegate.Combine (IncomingSMSReceivedEvent, value); } remove { IncomingSMSReceivedEvent = (IncomingSMSReceivedEventHandler) System.Delegate.Remove (IncomingSMSReceivedEvent, value); } } [ 204 ]
Chapter 5 public string IncomingSource { get { return _IncomingSource; } } public void CreateSMS(string phoneNumber, string message) { _msg = new SmsMessage(phoneNumber, message); } public void StartDetector() { _SMSInterceptor = new MessageInterceptor(InterceptionAction.Notify, true); _SMSInterceptor.MessageReceived += new Microsoft.WindowsMobile.PocketOutlook. MessageInterception.MessageInterceptorEventHandler (SMSReceivedEventHandler); }
You've seen earlier that the incoming phone number retrieved might look something like this: Ed <+6598532715>
You can use a simple Regular Expression to extract just the phone number (+6598532715) from between the < > brackets. Let's take a look at this function in detail: private void SMSReceivedEventHandler(object sender, MessageInterceptorEventArgs e) { _msg = (SmsMessage)e.Message; _IncomingSource = _msg.From.Address.ToString().Trim(); if (_IncomingSource.Length > 0) { if (Regex.IsMatch(_IncomingSource, "\\<(.*)\\>") == true) { [ 205 ]
Building Integrated Services _IncomingSource = Regex.Match(_IncomingSource, "\\<(.*)\\>").Groups[1].Value; } if (IncomingSMSReceivedEvent != null) IncomingSMSReceivedEvent(_IncomingSource); } } public void SendSMS() { _msg.Send(); } public void LaunchSMSComposeWindow() { MessagingApplication.DisplayComposeForm(_msg); } } }
Encapsulating phone functionality
The Telephony class is the equivalent class for phone functionality in the sales force application. Add the following class to the CRMLiveFramework project: using using using using using
System; System.Data; Microsoft.WindowsMobile.Telephony; Microsoft.WindowsMobile.Status; System.Text.RegularExpressions;
namespace CRMLive { public class Telephony { private SystemState _IncomingCallState; private SystemState _phoneCaller; private string _IncomingphoneNumber; public delegate void IncomingCallReceivedEventHandler (string incomingPhoneNumber); private IncomingCallReceivedEventHandler IncomingCallReceivedEvent; public event IncomingCallReceivedEventHandler IncomingCallReceived [ 206 ]
Chapter 5 { add { IncomingCallReceivedEvent = (IncomingCallReceivedEventHandler) System.Delegate.Combine (IncomingCallReceivedEvent, value); } remove { IncomingCallReceivedEvent = (IncomingCallReceivedEventHandler) System.Delegate.Remove (IncomingCallReceivedEvent, value); } } public object IncomingPhoneNumber { get { return _IncomingphoneNumber; } } public object MakePhoneCall(string phoneNumber) { Phone _phone = new Phone(); _phone.Talk(phoneNumber); } public void StartDetector() { _phoneCaller = new SystemState (SystemProperty.PhoneIncomingCallerNumber, true); _phoneCaller.Changed += new Microsoft.WindowsMobile.Status.ChangeEventHandler (phoneCaller_Changed); } private void phoneCaller_Changed(object sender, ChangeEventArgs args) {
[ 207 ]
Building Integrated Services _IncomingphoneNumber = SystemState.PhoneIncomingCallerNumber.Trim(); if (_IncomingphoneNumber.Length > 0) { if (Regex.IsMatch(_IncomingphoneNumber, "\\<(.*)\\>") == true) { _IncomingphoneNumber = Regex.Match(_IncomingphoneNumber, "\\<(.*)\\>").Groups[1].Value; } if (IncomingCallReceivedEvent != null) IncomingCallReceivedEvent (_IncomingphoneNumber); } } } }
Intercepting incoming SMS messages and phone calls in the background
As hinted earlier, your sales force application would likely need to detect incoming SMS messages and phone calls even if it is not running. The best way to achieve this is to write a separate program that constantly runs as a background service in memory. You should also have this program automatically run when the mobile device starts up. If you are wondering why you need to write a separate program, that's because your sales force application is a heavy application—if you take a look at the file size of the generated SalesForce.exe file, you can see it's somewhere around 150Kb by now. It wouldn't be a good idea to keep this whole application constantly running in the background. A separate program on the other hand, only has a 10Kb foot print.
[ 208 ]
Chapter 5
Let's take a look at how you can create such a program. Create a new Windows Forms project (named CRMLiveInterceptor) and add a form called MainForm to your project. You will need to add a reference to the CRMLiveFramework project because this application will make use of the database plugins. using using using using using
System; System.Windows.Forms; System.Reflection; System.IO; CRMLive;
public class MainForm : Form {
You will of course need to create a few objects that you will be using in this application: private PluginManager _PluginManager = new PluginManager(); private MessagingService _Interceptor = new MessagingService(); private Telephony _Telephony = new Telephony(); //The constructor for the class. In this constructor we //setup some of the event handlers public MainForm() { this.Activated +=new EventHandler(Form_Activated); _Interceptor.IncomingSMSReceived += new MessagingService.IncomingSMSReceivedEventHandler (Interceptor_IncomingSMSReceived); _Telephony.IncomingCallReceived += new _Telephony.IncomingCallReceivedEventHandler (Telephony_IncomingCallReceived); //Here we create a shortcut to this application in the //Windows\Startup folder if it does not exist yet try { CreateStartupShortCut(); _Interceptor.StartDetector(); _Telephony.StartDetector(); } catch (Exception ex) { MessageBox.Show(ex.Message, "Initializing app"); [ 209 ]
Building Integrated Services } } //Because we want this form to run constantly in the //background, you will need to hide it when it is activated public void Form_Activated(object sender, EventArgs e) { this.Hide(); }
To get your application to automatically run when the Windows Mobile OS starts up, there are generally two methods that most developers consider: •
Placing the path to your application in the registry in the HKLM\Init key: This method works well for native applications. For .NET CF applications, it may or may not execute successfully. This is because there is a possibility your application may run before the .NET CF classes are loaded. For .NET CF applications, this method is not desirable.
•
Placing a link to your application in the \Windows\Startup folder: The better way to get your application to automatically start up is by placing a link to it in the \Windows\Startup folder. You can create your own .LNK (link) file quite easily. The format of the .LNK file follows: #
Let's take a look at the function to create this .LNK file: private void CreateStartupShortCut() { string _startupPath = Environment.GetFolderPath (Environment.SpecialFolder.Startup); string _data = ""; string _path = ""; _startupPath = _startupPath.TrimEnd('\\') + "\\CRMLiveInterceptor.lnk"; if (File.Exists(_startupPath) == false) { _path = "\"" + Assembly.GetExecutingAssembly() .GetName().CodeBase + "\""; _data = Convert.ToString (_path.Length) + "#" + _path; SaveTextToFile(_data, _startupPath); } [ 210 ]
Chapter 5 } public bool SaveTextToFile(string strData, string FullPath) { bool _result = false; StreamWriter objReader = default(StreamWriter); try { objReader = new StreamWriter(FullPath, false); objReader.Write(strData); objReader.Close(); _result = true; } catch (Exception ex) { MessageBox.Show(ex.Message, "Creating lnk file"); } return _result; }
The final two functions in this form are event handlers that connect to the database and write the corresponding historical record to the database using the functions you've created earlier. This is done for every incoming SMS and phone call. private void Interceptor_IncomingSMSReceived(string IncomingPhoneNumber) { try { _PluginManager.GetActivePlugin.ConnectDatabase(); _PluginManager.GetActivePlugin. InsertHistoricalRecordByPhone (IncomingPhoneNumber, (int)GlobalVariables.Communications.Incoming, "SMS received", ""); _PluginManager.GetActivePlugin. DisconnectDatabase(); } catch (Exception ex) { MessageBox.Show(ex.ToString(), "Incoming SMS received"); } }
[ 211 ]
Building Integrated Services private void Telephony_IncomingCallReceived(string incomingPhoneNumber) { try { _PluginManager.GetActivePlugin.ConnectDatabase(); _PluginManager.GetActivePlugin. InsertHistoricalRecordByPhone(incomingPhoneNumber, (int) GlobalVariables.Communications.Incoming, "Phone call received", ""); _PluginManager.GetActivePlugin.DisconnectDatabase(); } catch (Exception ex) { MessageBox.Show(ex.ToString(), "Incoming call received"); } } }
Handling outgoing SMS messages and phone calls When the user sends an SMS message or places a phone call through the sales force application, you would also need to generate a corresponding historical record.
There is no equivalent way to 'detect' an outgoing SMS message or phone call using POOM or the SystemState class without resorting to the MAPI functions, so we will look at an easier approach. As you only need to log all outgoing phone calls and SMS messages sent through your application, you can create a general user control to do this (as shown in the following screenshot). This user control will be used in place of the normal text box in your application whenever you display phone numbers.
[ 212 ]
Chapter 5
The Call button allows the application to dial the number contained in the adjacent text box, while the SMS button launches a custom SMS Compose screen. Create the SMS Compose form as shown in the following screenshot and name it SendSMS.
Whenever the user sends an SMS message through this window, you will generate the corresponding history record. Let's take a look at the following code for this user control (called FlexiControl). The highlighted code generates the historical records. Take note that this user control needs a valid AccountGUID. You will need to pass in this value before using the user control. using System; using System.Windows.Forms; using System.Drawing; namespace CRMLive { public partial class FlexiControl { private Guid _AccountGUID; public Guid AccountGUID { get { return _AccountGUID; } set { _AccountGUID = value; } } [ 213 ]
Building Integrated Services public override string Text { get { return txtData.Text; } set { txtData.Text = value; } } public FlexiControl() { // This call is required by the Windows Form //Designer. InitializeComponent(); } public void FlexiControl_Resize(object sender, System.EventArgs e) { this.Height = txtData.Height; } public void btnMakeCall_Click(System.Object sender, System.EventArgs e) { Telephony _call = new Telephony(); GlobalArea.PluginManager.GetActivePlugin. InsertHistoricalRecord(_AccountGUID, System.Convert.ToInt32 (GlobalVariables.Communications.Outgoing), "Outgoing phone call", ""); _call.MakePhoneCall(txtData.Text); } public void btnSendSMS_Click(System.Object sender, System.EventArgs e) { //Here we launch the SendSMS Custom Compose window SendSMS _sendSMSForm = new SendSMS(); if (_sendSMSForm.ShowDialog() == DialogResult.OK) { GlobalArea.PluginManager.GetActivePlugin. InsertHistoricalRecord(_AccountGUID, System.Convert.ToInt32 [ 214 ]
Chapter 5 (GlobalVariables.Communications.Outgoing), "Outgoing SMS", ""); MessagingService _sms = new MessagingService(); _sms.CreateSMS(txtData.Text, sendSMSForm.Message); _sms.SendSMS(); } } } }
Now that you have done this, you can replace all phone number text boxes in the AccountViewer form with this new user control. As this control still exposes the Text property, data binding will work as usual. The only extra step you need to do is to initialize this control with the current AccountGUID in the form load event of the AccountViewer form (as shown in the following code snippet): fcLandPhone.AccountGUID = _account.AccountGUID fcMobilePhone.AccountGUID = _account.AccountGUID
The following screenshot shows what the new AccountViewer screen will look like after your changes:
Testing your code
You can try what you've built so far! Now, obviously you can only test your application with a real device. You will not be able to send or receive an SMS/phone call via the mobile device emulator.
[ 215 ]
Building Integrated Services
After compiling the relevant projects, run the CRMLiveInterceptor project. You won't be able to see much (as the form is hidden). You will know that the application has started after the Windows Mobile Busy icon goes away. Create a new lead account and set the Phone (HP) field to your friend's number. (Take care to specify the + and country code before the number and also not to type in any whitespaces in between numbers). You can create more robust code by handling whitespaces, country and area codes, number separators (such as brackets), and so on.
Now, get your friend to call your phone (you will need to pick up the call) or send your phone an SMS message. Again, you won't see much happen on your phone. If you go back into the lead account and click on the History tab, you will be able to see that the incoming SMS or phone call has been captured:
Now try using the AccountViewer form to place a phone call directly from a lead account. After the phone call has been successfully made, you can check the History tab again. You will notice a new entry—denoting that the outgoing call has been logged. You can also repeat your experiment with SMS messages.
Synchronizing with Windows Mobile Contacts
Windows Mobile Contacts is a pretty useful tool provided with Windows Mobile that allows you to store a list of all your contacts. These contacts can also be readily synced with Microsoft Outlook (via ActiveSync) on the desktop to provide you with an always updated list of contacts. [ 216 ]
Chapter 5
If your users use the sales force application to create a lead, opportunity, or customer, why should it be any different? These are also your contacts and should, therefore, make their way into the Windows Mobile Contacts area. Using the OutlookSession class in the Microsoft.WindowsMobile.PocketOutlook namespace, you can access all the Contacts stored on your mobile device. Let's see how we can write the following function code to sync the latest information in the sales force application to the Windows Mobile Contacts area: using Microsoft.WindowsMobile.PocketOutlook; public void SyncToWindowsMobileContacts(string mobilePhoneNumber, string firstName, string lastName, string address, string emailAddress) { OutlookSession _outlookapp = new OutlookSession(); PropertyDescriptor _contact; int _contactIndex; Contact _contactItem;
You can search through your Windows Mobile Contacts using the Find() function as follows. This function is flexible enough to allow you to search by any property in the PocketOutlook.Contact class. _contact = TypeDescriptor.GetProperties(typeof(Contact)) ["MobileTelephoneNumber"]; _contactIndex = _outlookapp.Contacts.Items.Find(_contact, mobilePhoneNumber);
If a result returned from the search is -1, this means that a contact matching the mobile phone number passed in cannot be found, you should proceed to create the contact in this case. If the result was a value other than -1, you can retrieve the contact and update its details with the latest data from the sales force application. if (_contactIndex == - 1) { _contactItem = new Contact(); _contactItem.FirstName = firstName; _contactItem.LastName = lastName; _contactItem.MobileTelephoneNumber = mobilePhoneNumber; _contactItem.AccountName = firstName + " " + lastName + "@CRMLive"; _contactItem.Email1Address = emailAddress; _outlookapp.Contacts.Items.Add(_contactItem); } else [ 217 ]
Building Integrated Services { _contactItem = _outlookapp.Contacts.Items[_contactIndex]; _contactItem.FirstName = firstName; _contactItem.LastName = lastName; _contactItem.Update(); } }
Now that you have created this function, a suitable place to call it is when the user clicks on the Save button of the AccountViewer form. This is because you need to update the Windows Mobile Contacts area with the latest first name and last name of the lead account. Let's take a look at the btnSave_Click() function in the AccountViewer form. The changes in this function are highlighted as follows: private void btnSave_Click(System.Object sender, System.EventArgs e) { AccountBindingSource.EndEdit(); if (Account.Validate == false) { Interaction.MsgBox(Account.GetValidationResult(), MsgBoxStyle.Exclamation, "Save error"); } else { Telephony _telephony = new Telephony(); _telephony.SyncToWindowsMobileContacts (txtMobPhone.Text, txtFirstName.Text, txtLastName.Text, txtStreet.Text, txtEmail.Text); this.DialogResult = Windows.Forms.DialogResult.OK; this.Close(); } }
You can test this functionality by running the application and subsequently creating or updating an existing lead, opportunity, or customer. You will notice that the account's information is automatically propagated to the Windows Mobile Contacts area (as shown in the next screenshot).
[ 218 ]
Chapter 5
Synchronizing with Windows Mobile Tasks
The Windows Mobile Tasks area (accessible through Programs | Tasks) allows you to keep track of your personal tasks. You can also synchronize the sales force tasks with the tasks in this area. Each Windows Mobile Task item contains a unique ItemId property value. By retrieving these values and saving them together in the AccountTasks table, you can associate each Windows Mobile Task item with a task in your sales force application. Let's take a look now at how you can create the code to access Windows Mobile tasks. Through the same OutlookSession class, you can find a particular Windows Mobile Task items using its ItemId property. using Microsoft.WindowsMobile.PocketOutlook; public string SyncToWindowsMobileTasks(string taskSubject, string taskDescription, DateTime taskDueDate, string taskID) { OutlookSession _outlookapp = new OutlookSession(); PropertyDescriptor _task; int _taskIndex; Task _taskItem; _task = TypeDescriptor.GetProperties(typeof(Task))["ItemId"]; _taskIndex = _outlookapp.Tasks.Items.Find(_task, taskID);
[ 219 ]
Building Integrated Services
If a task with the matching item ID cannot be found, you will need to create a new task. However, if a task with a matching ID is found, you must then update the subject, body, and due date of that task item with the latest data from your sales force application. if (_taskIndex == - 1) { _taskItem = new Task(); _taskItem.Body = taskDescription; _taskItem.Subject = taskSubject; _taskItem.DueDate = taskDueDate; _taskItem.ReminderDialog = true; _taskItem.ReminderRepeat = true; _taskItem.ReminderSound = true; _taskItem.ReminderVibrate = true; _outlookapp.Tasks.Items.Add(_taskItem); } else { _taskItem = _outlookapp.Tasks.Items[_taskIndex]; _taskItem.Body = taskDescription; _taskItem.Subject = taskSubject; _taskItem.DueDate = taskDueDate; _taskItem.Update(); } return _taskItem.ItemId.ToString(); }
A good place to call this function would be in the btnSave_Click() function in your AccountViewer form. You need to iterate through all the task items and run the SyncToWindowsMobileTasks() function on each task. This will be left to you as an exercise.
Sharing an account between two devices In any sales force application, it is a common requirement to share leads, opportunities, and customers with other mobile devices. Without any form of data-sharing capability, the data is locked in your device, and the only way to get it to another device is the unattractive option of rekeying in the data piece by piece. There are a few ways to send raw data from one device to another. Infrared (IrDA) and Bluetooth are two examples. Before we go into the details of each implementation, let's first decide on a transmission format for your data. [ 220 ]
Chapter 5
As you know, an account (together with its file attachments, historical records, and tasks) can be conveniently represented using a single dataset object. The easiest way to transmit this account to another device would be to serialize this dataset into a byte array for easy transmission and then to deserialize it back into a dataset at the targeted device. Let's take a look at the code that you can use to do this: public static byte[] SerializeDataset(DataSet Data) { StringWriter strWriter = new StringWriter(); string _result; ASCIIEncoding _encoding = new ASCIIEncoding(); Data.WriteXml(strWriter); _result = strWriter.GetStringBuilder().ToString(); return _encoding.GetBytes(_result); } public static DataSet DeserializeDataset(byte[] Data) { ASCIIEncoding _encoding = new ASCIIEncoding(); string _stringData; _stringData = _encoding.GetString(Data, 0, Data.Length); StringReader _reader = new StringReader(_stringData); DataSet _ds = new DataSet(); _ds.ReadXml(_reader); return _ds; }
Take note that this Accounts dataset contains only the file attachment metadata and not the actual file attachments themselves. You will need to write additional code to transmit these files one by one to the targeted device as well via Infrared or Bluetooth.
Now that you have decided on a suitable format for data transmission, let's see how you can transmit these byte arrays across Infrared and Bluetooth channels.
[ 221 ]
Building Integrated Services
Sharing an account between two devices using Infrared (IrDA)
Although new devices in the market nowadays don't really come with Infrared capability anymore, it is still a quick and cheap form of data transmission for older devices. Infrared requires line of sight during sending and receiving but, unlike Bluetooth, does not require the time-consuming process of device pairing. Infrared functionality is based on a client-server model and is provided through the IrDAListener and IrDAClient classes. You can place both the client side and server side code in the same class. Let's take a look at the following code for this class: using using using using using using
System; System.Data; System.Net; System.Net.Sockets; System.IO; System.Threading;
namespace CRMLive { public class InfraredService { private IrDAListener _listener; private string _serviceName; private int _maxRetryCount; private IrDAClient _client = null; private Thread _serverThread; private byte[] _receivedData; public delegate void DataReceiveEndedEventHandler(); private DataReceiveEndedEventHandler DataReceiveEndedEvent; public event DataReceiveEndedEventHandler DataReceiveEnded { add { DataReceiveEndedEvent = (DataReceiveEndedEventHandler) System.Delegate.Combine(DataReceiveEndedEvent, value); } remove [ 222 ]
Chapter 5 { DataReceiveEndedEvent = (DataReceiveEndedEventHandler) System.Delegate.Remove(DataReceiveEndedEvent, value); } }
Because the server listens for Infrared connections in a blocking call, it's a good idea to run it in a thread. The following code shows how this is done: public bool StartServer() { _serverThread = new Thread(new System.Threading.ThreadStart(ReceiveFile)); _serverThread.Start(); } public bool StopServer() { try { if (_serverThread != null) { _serverThread.Abort(); _serverThread = null; } } catch (Exception ex) { throw(ex); } }
This is the function that sets up the Infrared device to listen for connections. Once data is received via this connection, it is written into a byte array. private void ReceiveFile() { int _bytesRead = 0; IrDAListener listener = new IrDAListener(_serviceName); IrDAClient client = null; System.IO.Stream _stream = null; byte[] _byteArray = new byte[1048576]; [ 223 ]
Building Integrated Services string _str = string.Empty; _receivedData = null; try { listener.Start(); client = listener.AcceptIrDAClient(); _stream = client.GetStream(); int counter = 0; do { _bytesRead = _stream.Read(_byteArray, counter, 8192); counter += _bytesRead; } while (!(_bytesRead == 0)); } catch (Exception ex) { throw(ex); } if (_stream != null) { _stream.Close(); } if (client != null) { client.Close(); } listener.Stop(); if (DataReceiveEndedEvent != null) DataReceiveEndedEvent(); _receivedData = _byteArray; } public bool Initialize(string ServiceName) { _serviceName = ServiceName; } public byte[] GetReceivedData() { return _receivedData; }
[ 224 ]
Chapter 5
Now let's take a look at the client-side code. You can connect to a listening IrDA server by creating a new IrDA connection with the same service name as the server. public bool StartClient() { int _retryCount = 0; do { try { _client = new IrDAClient(_serviceName); } catch (Exception ex) { if (_retryCount >= _maxRetryCount) { throw (ex); } } _retryCount++; } while (_client == null && _retryCount < _maxRetryCount); if (_client == null) { return false; } else { return true; } }
You can serialize a dataset using the functions you've created earlier into a byte array, which can then be transmitted through this IrDA channel using a stream object. public bool SendData(byte[] byteArray) { System.IO.Stream _stream = null; try { _stream = _client.GetStream(); _stream.Write(_byteArray, 0, byteArray.Length); } [ 225 ]
Building Integrated Services catch (Exception ex) { throw(ex); } if (_stream != null) { _stream.Close(); } } public int MaxRetryCounts { get { return _maxRetryCount; } set { _maxRetryCount = value; } } public bool StopClient() { try { if (_client != null) { _client.Close(); } } catch (Exception ex) { throw(ex); } } } }
To use this class, you can call the following code on the client side to send the dataset across: InfraredService client = new InfraredService(); client.Initialize("MYSERVICE"); client.StartClient(); client.SendData(Generic.SerializeDataset(myDataset));
[ 226 ]
Chapter 5
At the server side, you can use the following code to set up the Infrared connection. Once the dataset is received, the DataReceiveEnded event is raised. You can then obtain the received byte array using the GetReceivedData() function. InfraredService server = new InfraredService(); server.Initialize("MYSERVICE"); server.StartServer();
Sharing an account between two devices using Bluetooth
Bluetooth technology has gained widespread support ever since its inception and remains the best way to transfer large amounts of data wirelessly to another device. Bluetooth devices can exchange information anywhere from 10 meters to 100 meters apart. Unlike Infrared, there is no built-in support for Bluetooth transmission in the .NET Compact Framework. Bluetooth, however, supports the serial port profile, which allows the data to be exchanged over serial communications. Fortunately, the latest version of the .NET Compact Framework provides the SerialPort class in the System.IO.Ports namespace, which allows you to easily do this. Let's take a look at what is needed in the server-side code for Bluetooth communications. using using using using using
System.Diagnostics; System; System.Data; System.IO; System.IO.Ports;
namespace CRMLive { public class BluetoothService { public delegate void DataReceiveEndedEventHandler(); private byte[] _ReceivedData; private SerialPort _port; private DataReceiveEndedEventHandler DataReceiveEndedEvent; public event DataReceiveEndedEventHandler DataReceiveEnded { add { [ 227 ]
Building Integrated Services DataReceiveEndedEvent = (DataReceiveEndedEventHandler) System.Delegate.Combine(DataReceiveEndedEvent, value); } remove { DataReceiveEndedEvent = (DataReceiveEndedEventHandler) System.Delegate.Remove(DataReceiveEndedEvent, value); } }
At the server side, we only need to create a new SerialPort object and assign an event handler for the DataReceived event. Whenever data is received through the port, your event handler is called. public void StartServer() { _port = new SerialPort(); _port.DataReceived += new SerialDataReceivedEventHandler(DataReceived); } private void DataReceived(object sender, SerialDataReceivedEventArgs e) { int _counter; int _bytesRead; byte[] Buffer = new byte[1048577]; _bytesRead = 0; try { _counter = 0; do { _bytesRead = _port.Read(Buffer, _counter, 8192); _counter += _bytesRead; } while (!(_bytesRead == 0)); } catch (Exception ex) { throw (ex); [ 228 ]
Chapter 5 } _ReceivedData = Buffer; if (DataReceiveEndedEvent != null) DataReceiveEndedEvent(); } public byte[] GetReceivedData() { return _ReceivedData; }
On the client side, you can just as easily send data to the server. You must first create a SerialPort object, set up its connection parameters, and then use the Write() function to write binary data to the port. public bool SendData(byte[] Data) { try { _port = new SerialPort(); _port.PortName = "COM1"; _port.BaudRate = 9600; _port.Parity = Parity.None; _port.DataBits = 8; _port.StopBits = StopBits.One; _port.Open(); _port.Write(Data, 0, Data.Length); _port.Close(); } catch (Exception ex) { throw (ex); } } } }
To use this class, you could call the following code on the client side: BluetoothService client = new BluetoothService(); client.SendData(Generic.SerializeDataset(myDataset));
[ 229 ]
Building Integrated Services
As for the server side, you can use the following code. Once the dataset is received, the DataReceiveEnded event is raised. You can then obtain the received byte array using the GetReceivedData() function. BluetoothService server = new BluetoothService(); server.StartServer();
Sending files across devices As the preceding code allows you to transfer binary data across devices, you can reuse the same code to send any type of object (including files) with a little extra work.
Capturing handwritten input using the Smart Device Framework
The Smart Device Framework consists of a set of controls and classes that exposes a broad range of mobile device functionality to the .NET programmer. One of these controls is the Signature control, which allows the end user to perform freehand drawing (using the mobile device stylus). Let's take a look at how you can use this control in your sales force application to capture the customer's signature. When the end user saves the signature, it will be saved as a PNG image file, and will eventually need to be committed to the AccountFiles table. As you already have a facility to load and save file attachments in your AccountViewer form, let's take advantage of this existing functionality. You will need to make some changes so that when the user clicks on New in the Files tab, the user is prompted to choose if he or she wants to upload a signature or a normal file. If the end user chooses No, it will launch the default FileDetailViewer form that you've built. If the user chooses Yes, it will launch the new signature-capturing form.
[ 230 ]
Chapter 5
The signature-capturing form will display the Smart Device Framework's Signature control in the bottom half of the screen, with some text and images at the top. When the user clicks on Save, the signature is saved into a PNG image file. It will output a File object just like any other ordinary file attachment and can, therefore, be easily integrated into your sales force application.
Now that you have an idea how this works, let's start building the functionality. The first thing you need to do is to install the Smart Device Framework. You can download the Smart Device Framework from the following URL by clicking the Download the Community Edition (free) link: http://opennetcf.com/CompactFramework/Products/SmartDeviceFramework/ tabid/65/Default.aspx
[ 231 ]
Building Integrated Services
After you've installed the Smart Device Framework, add a reference to the OpenNETCF.Windows.Forms.dll library. This is typically located in the installation folder of the Smart Device Framework.
Once you have done this, you should now create the signature-capturing form. Name this form GetSignature. Visual designing support for the Smart Device Framework If you are using the free version of the Smart Device Framework, one of its limitations is that you will not be able to visually drag and drop the control onto the form as you normally would. You will have to create and add the control to the form using code.
Let's take a look at the code for this form: using using using using using using
System; System.Windows.Forms; System.Drawing.Imaging; System.IO; System.Reflection; OpenNETCF.Windows.Forms;
namespace CRMLive [ 232 ]
Chapter 5 { public partial class GetSignature { private File _File; private FileManager _FileManager; private Signature _signature = new Signature(); //Constructor for the form. Take note that we will pass //in a File object to this form public GetSignature(File FileObject) { InitializeComponent(); _File = FileObject; _FileManager = new FileManager(); }
The following code in the form load event generates the control and places it at the desired coordinates: public void GetSignature_Load(object sender, System.EventArgs e) { _signature.Name = "Signature1"; _signature.Location = new System.Drawing.Point (20, 160); _signature.Size = new System.Drawing.Size(200, 90); this.Controls.Add(_signature); }
When the user clicks the Save button, you will first need to save the contents of the Signature control to a PNG image file. After that, you will use the FileManager class you've created earlier in Chapter 2 to upload the file and return a relative file path. You will need to then fill in the File object passed in to this form with the details of the signature file. public void mnuSave_Click(System.Object sender, System.EventArgs e) { string _filePath; FileInfo _fileinfo; string _tempPath; _tempPath = System.IO.Path.GetTempPath().TrimEnd('\\');
[ 233 ]
Building Integrated Services _filePath = _tempPath + "\\" + Guid.NewGuid().ToString() + ".png"; _signature.ToBitmap().Save(_filePath, ImageFormat.Png); _fileinfo = new System.IO.FileInfo(_filePath); _File.Attachment = _FileManager.Store(_filePath); _File.AttachmentName = "Signature"; _File.AttachmentSize = (int) _fileinfo.Length; this.DialogResult = System.Windows.Forms.DialogResult.OK; this.Close(); } public void mnuCancel_Click(System.Object sender, System.EventArgs e) { this.DialogResult = System.Windows.Forms.DialogResult.Cancel; this.Close(); } } }
Now, let's take a look at the changes required in the AccountViewer form. Look at the btnNewFile_Click function in the AccountViewer form. The changes required for this function are highlighted in the following code snippet: public void btnNewFile_Click(System.Object sender, System.EventArgs e) { File _File; DialogResult _dialogResult; _dialogResult=MessageBox.Show("Choose Yes to upload a signature, choose No to upload a normal file", "Add a file", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1); switch (_dialogResult) { case DialogResult.Yes: _File = _account.NewFile(); GetSignature _Signature = new GetSignature(_File); if (_Signature.ShowDialog() == System.Windows.Forms.DialogResult.OK) { _account.Files.AddFile(_File); dgFiles.DataSource = null; [ 234 ]
Chapter 5 dgFiles.DataSource = _account.Files; } _Signature.Close(); _Signature.Dispose(); _Signature = null; break; case DialogResult.No: _File = _account.NewFile() FileDetailViewer _EditView = new FileDetailViewer(_File); if (_EditView.ShowDialog() == DialogResult.OK) { _account.Files.AddFile(_File); dgFiles.DataSource = null; dgFiles.DataSource = _account.Files; } _EditView.Close(); _EditView.Dispose(); _EditView = null; break; } }
After you have done this, try creating a new file in your AccountViewer form, and choose Yes to capture your signature. You will find that it is saved to the database like any other file attachment.
Summary
In this chapter, you've taken a look at various forms of integration with Windows Mobile and the mobile device. You have learned how to do the following: •
Detect incoming SMS messages and phone calls
•
Send outgoing SMS and e-mail messages
•
Place phone calls
•
Synchronize with Windows Mobile Contacts and Tasks
•
Transfer binary data using Infrared
•
Transfer binary data using Bluetooth
•
Capture and store signatures using the Smart Device Framework's Signature control
In the next chapter, you'll take a look at the all-important task of synchronizing data between your mobile device and a remote repository. [ 235 ]
Data Synchronization If you are a mobile device user, it is likely that you would have performed a sync at one point in time (with or without being aware of it). We are all familiar with the convenience of being able to just dock our PDA devices and have our calendars, tasks, and contacts automatically synced to our desktop machines. The synchronization process is a necessity for any type of mobile device, whether it's a Smart Phone, iPhone, or a Pocket PC. The core of this necessity is simple—people need to have access to their data when they're on the move and when they're back at the office, and this data needs to be consistent—wherever they're accessing it from. In a business scenario, the importance of this necessity increases manifold—it's not just about your personal data anymore. The data you've keyed in on your PDA needs to be synced to the server so that it can be shared with other users, used to generate reports, or even sent for number-crunching. With hundreds of mobile users synchronizing their data and server-side applications updating this data at the same time, things can quickly get messy. The synchronization process has to ensure that conflicts are gracefully handled, auto-generated numbers don't overlap, that each user only syncs down the data they're meant to see, and so on. Fortunately for us, the folks at Microsoft and Oracle have gone through much of the brainstorming for us and have created rich frameworks which we can easily use to initiate a sync job. In this chapter, we study two such frameworks in detail—the Microsoft Synchronization Services Framework and the Oracle Mobile Server suite. We will take a look at the following topics in detail: •
An overview of the various synchronization methods available for both SQL Server CE and Oracle Lite
•
How to sync the Sales Force database tables using the Microsoft Synchronization Services Framework and the Oracle Mobile Server
•
How to synchronize externally stored files to the server
•
How to create a network-aware sync module
Data Synchronization
Overview of the different data synchronization methods available for Microsoft SQL Server CE
Microsoft provides various methods to sync data between the mobile device and the database server. Let's take a look at these methods in detail:
SQL Remote Data Access (SQLRDA)
SQLRDA was one of the earliest methods of data synchronization between a mobile device and an SQL Server database. It allowed the mobile application to issue push or pull commands to sync data upwards or downwards from the SQL Server database. Through the Client Agent, the mobile application could issue these commands to the Server Agent (which sits behind the IIS web server). The Server Agent then executes the requested commands directly on the SQL Server database. It is important to note that SQLRDA is being phased out by Microsoft in favour of the newer Microsoft Synchronization Services Framework. You should use this new framework instead when you implement your synchronization projects.
Merge replication
Merge replication provides synchronization capability via a publisher-subscriber model. The database at the server side is called the Publisher. It makes a publication (a set of database objects) available for replication. The mobile application is the Subscriber. The setting up of merge replication is a two-step process: 1. A publication is created on the database. 2. A subscriber subscribes to the publication. After that, any changes made to the data on the client-side can be synced to the publisher through this replication framework. The architecture of the merge replication framework is depicted as follows:
[ 238 ]
Chapter 6
In the previous diagram, the mobile application uses the replication objects to communicate with the SQL Server CE Client Agent, which passes the changes made to the corresponding Server Agent via IIS. The SQL Server Reconciler is the component that will merge all changes from the Subscriber to the Publisher.
Microsoft Synchronization Services
The Microsoft Synchronization Services Framework allows a server database to not only sync with mobile device clients but also with desktop clients. It is also able to sync data from different types of data sources to the server. For instance, this framework provides the Sync Services for Filesystems component that allows you to easily sync folders or files with another system. Microsoft uses certain terminologies in this framework. The client-side database is called the Local Database Cache and the server-side database is called the Remote Database.
The Microsoft Synchronization Services Framework provides sync functionality in the form of Sync providers, in much the same way as database providers provide data access functionality. The client-side Sync provider and server-side Sync provider communicate with each other through a Windows Communication Framework (WCF) service. This setup is depicted as follows:
[ 239 ]
Data Synchronization
The Microsoft Synchronization Services Framework is extensible—through the use of different providers, you can implement sync functionality for different types of data and data sources. You can even create your own custom sync providers to latch on to the framework.
Overview of the different data synchronization methods available for Oracle Lite
The Oracle Lite suite provides synchronization services through the Oracle Mobile Server. Let's take a look at how this software works in the following section.
Oracle Mobile Server
The Oracle Lite Mobile Server provides synchronization services between the Oracle Lite database on the mobile device and an Oracle database at the backend server. In fact, the Mobile Server does much more than that. It handles the management of all aspects of mobile application deployment, including user management for each application. The WebToGo portal The web-based WebToGo portal is used to manage the deployment of your mobile applications. This portal is included in the Oracle Mobile Server installation.
The Oracle Lite installation on the mobile device provides the Oracle MSync tool, which can be used to initiate a sync with the Oracle Mobile Server. The following diagram shows how these various components are connected to each other:
[ 240 ]
Chapter 6
The great thing about using Oracle Mobile Server is that you can set up a sync without writing a single line of code. Most of the configuration work is done using the Mobile Database Workbench, a tool that allows you to define how the data and tables are synced down. There are certain Oracle terminologies to watch out for. In the Oracle world, the client-side database is called the Snapshot and the server-side database the Master.
A quick comparison between the various Synchronization frameworks The following table lists the main differences between each Synchronization framework covered so far and their support for various features. Feature
SQL RDA
Merge Repl.
MS Sync Services
Oracle Mobile Server
Supports heterogeneous data sources
N
N
Y
N
Supports Incremental change tracking
N
Y
Y
Y
Supports conflict detection and resolution
N
Y
Y
Y
Able to sync schema changes
N
Y
N
Y
Ability to create data views on the client
N
N
Y
N
Allows to programmatically control the Sync
Y
Y
Y
Y (through COM or C/C++)
[ 241 ]
Data Synchronization
Using Microsoft Synchronization Services
The following sections describe how you can set up the Sales Force application to sync with a similar set of tables on the server side using the Microsoft Synchronization Services Framework. The steps involved in performing this sync are roughly outlined as follows: 1. Create the CRMLive tables in the server-side database. 2. Create a WCF Service Library hosted at the server side to listen for requests from the mobile device. 3. Create a local database cache in the WCF Service Library project. 4. Use the Microsoft Synchronization Services Framework wizards to generate the relevant .NET classes required for the sync in the Sales Force and WCF Service Library projects. You will also need to tweak the default settings for some of these generated classes. 5. Define appropriate filters for the sync—so that only a subset of the data is synced (instead of all records in a table). 6. Add some code to your Sales Force project to initiate the sync.
Setting up Microsoft SQL Server and Microsoft Synchronization Services
To perform a sync, you need to have a server-side database. If you don't have the full standard version of Microsoft SQL Server, you can install the freely available Microsoft SQL Server Express 2008 database for your testing. You can download and install this database engine using the following URL: http://www.microsoft.com/downloads/details.aspx?FamilyID=B5D1B8C3FDA5-4508-B0D0-1311D670E336&displaylang=en
The next step is to install the Microsoft Synchronization Services for ADO.NET package. This package allows you to synchronize between a server database and an SQL Server CE database running on Windows Mobile 5 or 6. You can download this package using the following URL: http://www.microsoft.com/downloads/details.aspx?FamilyID=75FEF59F1B5E-49BC-A21A-9EF4F34DE6FC&displaylang=en
[ 242 ]
Chapter 6
Creating the CRMLive server tables
You will need to first recreate the CRMLive tables at the server side. Using the SQL Server Management Studio tool on your desktop, create a new database named crmlive. You can reuse the CREATE TABLE DDL statements you've created in Chapter 2 in the SQLServerPlugin project to recreate your tables in this database. Copy and paste each CREATE TABLE statement in the SQL Server Management Studio and run them against the crmlive database. After you've done this, you should have a total of five tables in your crmlive database.
Creating the WCF service
As you've read earlier, the local database cache on the mobile device can be made to sync with the backend server through a WCF service. This WCF service will run off the server (or in your case, your development machine). You can begin by adding a new WCF Service Library project to your Sales Force solution. Name this project CRMLiveServiceLibrary.
[ 243 ]
Data Synchronization
After doing this, add a new Local Database Cache item to this new project (shown in the following screenshot). Name this item CRMLiveDataCache. This step creates a local SDF database in your project, after which you will be able to set up synchronization settings through a wizard.
Once you have added the Local Database Cache item, you will immediately be prompted with the Configure Data Synchronization wizard. The first thing you need to do in this window is to configure the server and client connections. Configure the server connection to point to the crmlive database you've created earlier. As for the client connection, leave it as the default (CRMLIVE.sdf). This will let the wizard generate a new database in the mobile device project. After configuring these connections, you should have the following settings in the Database Connections section.
[ 244 ]
Chapter 6
You will now need to include some tables in the sync. Click on the Add button at the bottom of the cached tables list. A new dialog window will appear. Tick all the five tables in the crmlive database in this window (shown as follows).
Although you can use the default values for most of the settings in this screen, the Data to download field is worthy of note. This field allows you to choose between two refresh options: •
New and incremental changes after first synchronization This is an incremental refresh—only the changes are synced down to the local database.
•
Entire table each time This option syncs down the entire table during each sync. You can use this option on small tables that change frequently.
[ 245 ]
Data Synchronization
After you've added the tables to the cached list, you need to expand the Advanced section. You will notice that both the server and client project location are set to the same project. Change the client project location to the SalesForceApp project. The client project is usually a WinForms application. This step allows the wizard to generate the database .SDF file in the SalesForceApp project.
Click on the OK button to start generating the files required for the sync. You may be prompted to Update the server for incremental changes and to Save the SQL scripts in the project for later use. Select both options if prompted. At the end of this process, you will find new files added to both your CRMLiveServiceLibrary and SalesForceApp projects. You will also immediately notice that two new fields—LastEditDate and CreationDate have been automatically added to each table in your crmlive database. You don't have to worry about this—these two fields are managed by SQL Server and will allow the Synchronization Framework to decide on the records that have changed and need to be synced down.
Configuring the WCF service library
The first thing you can do is to delete both the Service1.cs and IService1.cs files from your CRMLiveServiceLibrary project. These two files are default files generated together with the project, and will not be used. After that, open the CRMLiveDataCache.SyncContract.cs file. Look for the ICRMLiveDataCacheSyncContract interface definition. You will need to add the XMLSerializerFormat() attribute here (highlighted as follows). This is important
because the .NET Compact Framework does not support the WCF default DataContractSerializer when serializing data for transfer over the network. The XMLSerializer, on the other hand is supported by the .NET Compact Framework. [ 246 ]
Chapter 6 [ServiceContractAttribute() , XmlSerializerFormat ()] public interface ICRMLiveDataCacheSyncContract { [OperationContract()] SyncContext ApplyChanges(SyncGroupMetadata groupMetadata, DataSet dataSet, SyncSession syncSession); . . .
Serializers are classes that convert an object and its current state into a stream or buffer of bytes (for storage or transport purposes).
Next, scroll to the top of this same file. There is a bunch of commented app.config settings that you need to copy and paste into the app.config file in this project. You need to perform the following changes to your app.config file: 1. Copy the commented <service> block into the app.config file, overwriting the existing <service> entry under the <system. serviceModel><services> tag. Don't forget to uncomment this block after that! 2. Copy the commented block into the app.config file, overwriting the existing entry under the <service Behaviors> tag. Uncomment this block of code as well. 3. Change the binding="wsHttpBinding" property to binding="basicHttpB inding" .This is important because the .NET Compact Framework does not support wsHttpBinding. 4. Change the base address from http://localhost:8080 to reflect your actual server name or server IP (for example, http://192.168.2.4:8080). Your app.config file should now look something like this:
Data Synchronization Info=True;User ID=sa;Password=admin123" providerName="System.Data.SqlClient" /> <system.web> <system.serviceModel> <services> <service name="CRMLiveServiceLibrary. CRMLiveDataCacheSyncService" behaviorConfiguration="CRMLiveServiceLibrary. CRMLiveDataCacheSyncServiceBehavior"> <endpoint address ="" binding="basicHttpBinding" contract="CRMLiveServiceLibrary. ICRMLiveDataCacheSyncContract"/> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> <serviceBehaviors> <serviceMetadata httpGetEnabled="True" /> <serviceDebug includeExceptionDetailInFaults="False" />
Setting filters for the Sync
When you sync down Accounts data from the server to the mobile device, you obviously don't need to sync down every single record from the server. Only the accounts owned [ 248 ]
Chapter 6
by the mobile user need to be synced down. This same rule extends to all the accounts-related tables—Accounts, AccountTasks, AccountHistories, and AccountFiles. As each account record has an OwnerID column, we can easily filter the records using this column. Open the CRMLiveDataCache.Designer.cs file in Visual Studio. This file contains all the DataAdapter classes (one for each table to be precise), which determines how your data is extracted for the sync job. Let's take a look at how we can implement this filter, starting with the Accounts table. Navigate to the InitializeCommands() method of the AccountsSyncAdapter in the Visual Studio designer, shown as follows :
Look for the SelectIncrementalInsertsCommand object. The default SQL for this command object retrieves newly created records from the Accounts table. You will need to make the following changes to the SQL (highlighted as follows). Take note that you can use the @OwnerID parameter in your SQL. The actual value will be passed in from the client front-end later. this.SelectIncrementalInsertsCommand = new System.Data.SqlClient.SqlCommand(); this.SelectIncrementalInsertsCommand.CommandText = @"SELECT [OwnerID], [AccountGUID], [AccountType], [DateCreated], [FirstName], [LastName], [Status], [Reception], [Source], [ResPhoneNo], [MobPhoneNo], [EmailAddress], [Street], [City], [State], [Zipcode], [Country], [Website], [InterestedProds], [LastEditDate], [CreationDate] FROM dbo.Accounts WHERE ([CreationDate] > @sync_last_received_anchor AND [CreationDate] <= @sync_new_received_anchor AND OwnerID=@OwnerID)";
Now let's move on to a child table—the AccountFiles table. You need to filter these records by the OwnerID field as well, but the AccountFiles table does not contain this field. So how do we get around this? [ 249 ]
Data Synchronization
As the OwnerID is located in the Accounts table, you can do an inner join with the Accounts table to create this filter. The highlighted code below illustrates how this can be done: this.SelectIncrementalInsertsCommand = new System.Data.SqlClient.SqlCommand(); this.SelectIncrementalInsertsCommand.CommandText = "SELECT a.[AccountGUID], a.[AttachmentID], a.[AttachmentName], [AttachmentSize], a.[Attachment], a.[Keywords], a.[LastEditDate], a.[CreationDate] FROM dbo.AccountFiles a, dbo.Accounts b WHERE (a.AccountGUID = b.AccountGUID AND a.[CreationDate] > @sync_last_received_anchor AND a.[CreationDate] <= @sync_new_received_anchor AND b.OwnerID=@OwnerID)";
You will need to repeat this for the AccountTasks and AccountHistories tables. As for the Products table, you will not need to change the query. This is because every mobile user will need to see the same list of products. No filter is required for this table. Take note that if you change any settings through the Configure Data Synchronization Wizard, you will lose all changes you've made to the CRMLiveDataCache.Designer.cs file.
Configuring the client project
Now that you've successfully configured the WCF service, you need to configure the smart device client project. In the SalesForceApp project, you can access the WCF service like any normal web service. You can therefore add a reference to the WCF service in the same way (through the Add Web Reference menu in Visual Studio). Before you can add the reference, the WCF service must be running in the first place. We can easily do this by first running the WCF service library project in non-debug mode. Select the CRMLiveServiceLibrary project in the Solution Explorer window, and press Ctrl+F5. This will launch the WCF Test Client, which will host the WCF service for as long as it is open.
[ 250 ]
Chapter 6
Keep the window open, and add a web reference to your SalesForceApp project. Specify the full URL of your WCF library in the URL field and click on the Go button. If the WCF service was found, you will see the screen shown as follows. Specify CRMLiveProxy as the web reference name and click on Add Reference to add the reference.
[ 251 ]
Data Synchronization
After adding the WCF reference, you will need to make some changes to the Reference.cs file of the generated CRMLiveProxy. To see this file, you will need to use the Show All Files option in the Solution Explorer for this project.
Open the Reference.cs file and import the Microsoft.Synchronization.Data namespace (highlighted): namespace SalesForceApp.CRMLiveProxy { using System.Diagnostics; using System.Web.Services; using System.ComponentModel; using System.Web.Services.Protocols; using System; using System.Xml.Serialization; using System.Data; using Microsoft.Synchronization.Data;
After you have done that, you will need to scroll down the same file and remove everything beginning from (and including) the following section: //REMOVE ALL CODE BELOW [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] [System.Xml.Serialization.XmlTypeAttribute (Namespace="http://tempuri.org/")] public partial class SyncGroupMetadata { . . . [System.Xml.Serialization.XmlTypeAttribute (Namespace="http://tempuri.org/")] [ 252 ]
Chapter 6 public enum SyncDirection { /// DownloadOnly, /// UploadOnly, /// Bidirectional, /// Snapshot, }
This step is necessary because the Add Web Reference wizard has generated a set of Sync Services classes that are already defined in the Sync Service assemblies referenced by the client project. This conflict can be fixed by removing the previous code. Take note that if you re-update the reference, you will lose all changes you've made to this file.
Writing the sync code
To perform a sync you will need to write the following code. Since you only need to sync down the records owned by the current mobile user, you can filter the data by passing in the username of the mobile user to the following function. This username (the OwnerID) is passed in as a SyncParameter to the server, where it will be used in the SQL filters you've modified earlier. The code to do this is highlighted in the following: using Microsoft.Synchronization.Data; private void Synchronize(string userName) { try { var webSvcProxy = new CRMLiveProxy.CRMLiveDataCacheSyncService(); var serverProvider = new ServerSyncProviderProxy (webSvcProxy); var syncAgent = new CRMLiveDataCacheSyncAgent(); [ 253 ]
Data Synchronization syncAgent.RemoteProvider = serverProvider; //Take note that we only need unidirectional sync for //the Products table syncAgent.Accounts.SyncDirection = SyncDirection.Bidirectional; syncAgent.AccountTasks.SyncDirection = SyncDirection.Bidirectional; syncAgent.AccountHistories.SyncDirection = SyncDirection.Bidirectional; syncAgent.AccountFiles.SyncDirection = SyncDirection.Bidirectional; syncAgent.Products.SyncDirection = SyncDirection.DownloadOnly; //Pass in the username of the mobile device user //as the OwnerID parameter value. This will only //sync down records belonging to this user. syncAgent.Configuration.SyncParameters.Add (new SyncParameter("@OwnerID", userName)); //Synchronize the database var stats = syncAgent.Synchronize(); MessageBox.Show("Sync complete!"); MessageBox.Show("Changes Downloaded: " + stats.TotalChangesDownloaded.ToString() + Constants.vbCrLf + "Changes Uploaded: " + stats.TotalChangesUploaded.ToString()); } catch (Exception ex) { Interaction.MsgBox(ex.ToString); } }
To test the sync code you've created, run the SalesForceApp project (in debug mode). You will notice that the crmlive database SDF file is automatically deployed to the project folder on your mobile device. The WCF service is also automatically started in the background.
[ 254 ]
Chapter 6
Configure the connection string for your Sales Force application to point to this new SDF file. Create a new lead account in your Sales Force application, and then run the sync code. A message box will pop up showing the number of changes uploaded to the server. You can verify that the data was uploaded to the server by running an SQL SELECT query against the crmlive database on the backend server (your development machine) through SQL Server Management Studio. If the sync was successful, you should be able to see the record you've created from your mobile device.
Conflict resolution
There may be occasional scenarios where you have made changes to the same record at both the server and client side. A conflict will arise when you attempt a sync on this record. In such a case, you can write your own custom code to handle this conflict. The CRMLiveDataCacheServerSyncProvider class in the CRMLiveDataCache. Designer.cs file provides an event that is raised whenever a conflict arises. This event is the ApplyChangeFailed event. Let's take a look at how you can write an event handler to handle this conflict. //Constructor for CRMLiveDataCacheServerSyncProvider public CRMLiveDataCacheServerSyncProvider() { . . . //Add the event handler for the ApplyChangeFailed event this.ApplyChangeFailed += new System.EventHandler<Microsoft.Synchronization.Data. ApplyChangeFailedEventArgs> (ApplyChangeFailedEventHandler); } //The ApplyChangeFailed event handler public void ApplyChangeFailedEventHandler(object sender, Microsoft.Synchronization.Data.ApplyChangeFailedEventArgs e) { System.Data.DataTable clientChanges = e.Conflict.ClientChange; System.Data.DataTable serverChanges = e.Conflict.ServerChange;
[ 255 ]
Data Synchronization //Here you can write your own custom logic to compare the //server-side and client-side record if ((System.DateTime)clientChanges.Rows[0]["ModifiedDate"] > (System.DateTime)serverChanges. Rows[0]["ModifiedDate"]) { //If you want the client record to overwrite the server //record, set the RetryWithForceWrite value as the //final action. e.Action = Microsoft.Synchronization.Data.ApplyAction. RetryWithForceWrite; } }
You can control how the conflict is handled by setting the e.Action variable to one of the following values: •
RetryWithForceWrite The client "wins"—this action will overwrite the server record with the client record.
•
Continue The server "wins"—the client changes are not applied to the server
•
RetryApplyingRow This action will attempt a re-sync of the record. This option is usually used when you've made changes to the data and wish to try a re-sync in the hope that a conflict no longer occurs.
Using Oracle Mobile Server
The Oracle Mobile Server can be a bit tedious to set up for first time beginners. Once you get going, however, it can be a powerful tool that can manage not only database synchronization but also mobile application deployment. In the following steps, you will see how you can configure Oracle Mobile Server to perform a database sync without writing a single line of code in your program.
[ 256 ]
Chapter 6
Installing Oracle Database Enterprise 11g and Oracle Mobile Server
The Oracle Mobile Server requires an existing Oracle Database server to function. You can install the latest version of the Oracle Database software on your desktop for testing purposes. Download the latest Oracle Database software (for Windows) from the following location: http://www.oracle.com/technology/software/products/database/index.html
Install the database software using the default settings and create a database with the SID crmlive. After that is done, you will need to install the Oracle Mobile Server. The Oracle Mobile Server is included in the Oracle Lite 10g installation package that you've downloaded earlier in Chapter 2. During the setup, you may be prompted to select the type of installation. In Chapter 2, you selected the Mobile Development Kit option. This time, you will need to select the Mobile Server option.
[ 257 ]
Data Synchronization
After this step, you can use the default settings throughout most of the installation. There are a few settings you should take note of: •
Mobile Server Standalone TCP/IP Port configuration OC4J TCP/IP Port : 81: This is the port used to host the mobile server. For the rest of this chapter, we will assume you are using port 81
•
Launch Repository Wizard? YES: You will need to set up a mobile repository after your installation
•
Install the Demo applications? YES
Creating an Oracle Mobile repository
An Oracle Mobile repository makes use of an existing Oracle database to store the server-side data for your mobile applications. After you install Oracle Mobile Server, you will need to create at least one repository. The Mobile Server Repository Wizard will automatically run after your first installation of Oracle Mobile Server.
You can also manually run the Mobile Server Repository wizard from the following location: \OLITE10GHOME\Mobile\Server\admin\ repwizard.bat
In the first screen of the Oracle Mobile Repository Wizard, specify the details of an existing and valid Oracle database.
[ 258 ]
Chapter 6
In the next few screens, you will be brought through the configuration of the following details. You will need to specify an appropriate password for each setting. •
Mobile Server Repository Password Schema name : MOBILEADMIN Deploy demo applications : YES
•
Mobile Server Administrator Administrator name : Administrator
•
Username and Password for Demo Schema Schema name : master
At the end of the wizard, your mobile repository will be generated. To ensure that it was generated successfully, run the Mobile Server from the Start | All Programs | Oracle Database Lite 10g | Mobile Server menu. A command prompt window will open. Since this is your first time running the Mobile Server, it will request you to specify the password for the OC4J administrator. Specify an appropriate password.
You should not see any errors displayed in this command prompt window. Keep this window open—the moment you close it, the Mobile Server service goes offline. To double-check that your mobile repository is set up successfully, navigate to the following URL in any web browser (replace the section within the brackets with your server name or IP address): http://:81/webtogo
[ 259 ]
Data Synchronization
You should be able to see the WebToGo portal login screen (shown as follows):
Creating the CRMLive server tables
As you've read earlier, the tables at the server side are called the master tables, and the copy in the database on the mobile device is called the snapshot. As you have not setup the master tables yet, let's create them now. You can reuse all the CREATE TABLE DDL statements you've created in Chapter 2. Let's take a look at how we can create these tables using the SQL*Plus tool. Managing the Oracle database The Oracle suite of products provides many ways to manage your database. You can either do it visually through the desktop-based SQL Developer tool or the web-based Oracle Enterprise Manager tool. You can also do it via the text-based SQL*Plus tool.
Here's the SQL for the Accounts table from Chapter 2. There are a few things you need to consider. You need to specify the Master schema when creating each table. This is because Oracle uses the schema tied to the user account (used to log in to SQL*Plus) by default. For instance, if you have logged in to SQL*Plus under the SYSTEM account, the table would be created under the System schema, which is not appropriate from a security viewpoint. CREATE TABLE Master.Accounts ( AccountGUID RAW(16) NOT NULL, AccountType INTEGER, DateCreated DATE, FirstName VARCHAR2(50), LastName VARCHAR2(50), Status INTEGER, [ 260 ]
Chapter 6 Reception INTEGER, Source INTEGER, ResPhoneNo VARCHAR2(50), MobPhoneNo VARCHAR2(50), EmailAddress VARCHAR2(100), Street VARCHAR2(255), City VARCHAR2(50), State VARCHAR2(50), Zipcode VARCHAR2(10), Country VARCHAR2(50), Website VARCHAR2(50), InterestedProds VARCHAR2(255), PRIMARY KEY(AccountGUID));
Launch SQL*Plus from Start | All Programs | Oracle11g_home | Application Development | SQL*Plus. Log in to SQL*Plus using your SYSTEM account and password. After login, you will see the SQL> prompt. You can now copy and paste each CREATE TABLE statement in succession at this prompt to execute them. Take note that SQL*Plus doesn't execute each statement until it encounters the semicolon (;) character. You will need to ensure that each statement is terminated using the semicolon (highlighted in the sample SQL above).
The following screenshot shows the SQL*Plus output:
[ 261 ]
Data Synchronization
You should also take note that you do not have to manually create any sequences at the server using the CREATE SEQUENCE statement. Sequences are created and configured in a mobile application publication. This will be covered in detail in the next section.
Creating a new publication using the Mobile Database Workbench
A publication represents an application (and its database) in the Oracle mobile server. You can create a publication through the Mobile Database Workbench tool provided with Oracle Mobile Server.
Creating a new mobile project
Launch the Mobile Database Workbench tool from Start | All Programs | Oracle Database Lite 10g | Mobile Database Workbench. Create a new project by clicking on the File | New | Project menu item in the Mobile Database Workbench window. A project creation wizard will run. Specify a name for your project and a location to store the project files.
The next screen will request you to key in mobile repository particulars. Specify your mobile repository connection settings, and use the mobile server administrator password you specified earlier to log in.
[ 262 ]
Chapter 6
In the next step, specify a schema to use for the application. As you've created the master tables in the MASTER schema, you can specify your MASTER account username and password here.
The next screen will show a summary of what you've configured so far. Click the Finish button to generate the project. If your project is generated successfully, you should be able to see your project and a tree list of its components in the left pane.
[ 263 ]
Data Synchronization
Adding publication items to your project
Each publication item corresponds to a database table that you intend to publish. For example, if your application contained five tables, you will need to create five publication items. Let's create the publication items now for the Accounts, AccountTasks, AccountHistories, AccountFiles, and Products tables. Click on the File | New | Publication Item menu item to launch the Publication Item wizard. In the first step of the wizard, specify a name for the publication item (use the table name as a rule of thumb). There are two options here worth noting: •
Synchronization refresh type This refers to the type of refresh used for a particular table: °°
Fast This is a type of incremental refresh—only the changes are synced down from the server during a sync. This is the most common mode of refresh used.
°°
Complete In this type of refresh, all content is synced down from the server during each sync. It is comparatively more time consuming and resource intensive. You might use this option with tables containing small lists of data that change very frequently.
°°
Queue based This is a custom refresh in that the developer can define the entire logic for the sync. It can be used for custom scenarios that may not exactly require synchronization—for instance you might need to simply collect data on the client and have it stored at the server. In such a case, the queue-based refresh works better because you can bypass the overhead of conflict detection.
•
Enable automatic synchronization Automatic synchronization allows a sync to be initiated automatically in the background of the mobile device when a set of rules are met. For example, you might decide to use automatic synchronization if you wanted to spread out synchronization load over time and reduce peak-out on the server.
[ 264 ]
Chapter 6
In the next step, choose the table that you want to map the publication item to. Select the MASTER schema, and click the Search button to retrieve a list of the tables under this schema. Locate the Accounts table and highlight it.
In the next screen, you will need to select all the columns you need from the Accounts table. As you need to sync every single column from the snapshot to the master table, include all columns. Move all columns from the Available list to the Selected list using the arrow buttons and click on the Next button to proceed.
[ 265 ]
Data Synchronization
The next step is one of the most important steps in creating a publication item. The SQL statement shown here basically defines how data is retrieved from the Accounts table at the server and synced down to the snapshot on the mobile device. This SQL statement is called the Publication Item Query. The first obvious thing you need to do is to edit the default query. You need to include a filter to sync down only the accounts owned by the specific mobile device user. You can easily use a filter that looks like the following: WHERE OwnerID = :OwnerID
The following screenshot shows how your Publication Item Query will look after editing. If any part of it is defined or formatted incorrectly, you will receive a notification. Click on Next after that to get to the summary screen, then click on the Finish button to generate the publication item.
After creating the publication item for the Accounts table, let's move on to a child table—the AccountTasks table. Create another publication item in the same fashion that maps to the AccountTasks table. At Step 4 of the wizard, the Publication Item Query that you need to specify will be a little bit different. The AccountTasks table does not contain the OwnerID field, so how do we filter what gets synced down to each specific mobile device? You obviously don't want to sync down every single record in this table—including those that are not meant to be accessible by the specific mobile device user. One way to still apply the OwnerID filter is to use a table join with the Accounts table. You can easily specify a table join in the following manner:
[ 266 ]
Chapter 6 SELECT "TASKID", A."ACCOUNTGUID", "TASKSUBJECT", "TASKDESCRIPTION", "TASKCREATED", "TASKDATE", "TASKSTATUS" FROM MASTER.ACCOUNTTASKS A, MASTER.ACCOUNTS B WHERE A.ACCOUNTGUID=B.ACCOUNTGUID AND B.OWNERID = :OwnerID
If you try to save the Publication Item Query above in the Edit Query box, it may prompt you to select the primary base object for the publication item (as shown in the following screenshot). This should be set to AccountTasks because we are creating a publication item that maps to this table.
If you choose the Accounts table again, you will end up with two publication items that map to the same Accounts table. This will cause problems when you attempt to add both items to a publication.
If you have typed in everything correctly, you will be able to see your Publication Item Query show up in the Query tab shown as follows. You can then click on the Next and Finish buttons to complete the wizard.
[ 267 ]
Data Synchronization
Now that you've seen how to create a publication item based on a child table, repeat the same steps above for the other child tables – AccountFiles and AccountHistories. The last table—the Products table deserves a special mention because it's different. You do not need a filter for this table, simply because every mobile device user will need to see the full list of products. You can, therefore, use the default Publication Item Query for the Products table: SELECT "PRODUCTID", "PRODUCTCODE", "PRODUCTNAME", "PRODUCTPRICE" FROM MASTER.Products
After you've done this, you can now move on to creating the "sequences" necessary in this mobile application.
Adding sequence items to your project
When you are working with multiple mobile devices, you have to keep in mind that records from multiple devices will be synced into one single table on the server. If each mobile device uses its own set of running numbers, you may end up with duplicate numbers when the records from each device are synced to the server. Oracle Mobile Server helps you manage sequence numbers by allocating a set of numbers that never overlap for each separate mobile device user. Let's see how this can be configured for the AccountTasks sequence number for example. Launch the Create Sequence window by clicking on the File | New | Sequence menu item. Fill in the name of the sequence (use the same name as the sequence names you've created in Chapter 2). There are three fields worth noting: •
Window Size The window size defines the size of each new block of numbers that is allocated to each mobile user.
•
Threshold Once the allocated number has reached the threshold, a new block of numbers of size defined by the Window size property is allocated. The threshold value is usually a few hundred numbers smaller than the Window size.
[ 268 ]
Chapter 6
•
Generate Server side Sequence In some cases, you might also be using sequences on the server side to create new records. In such a case, Oracle Mobile Server prevents any conflict by assigning all even numbers to the server and odd numbers to the mobile clients. If you don't need to use sequences on the server side, avoid selecting this option, because the mobile user will only be able to use half the numbers allocated to him or her.
The following screenshot shows the Create Sequence user interface:
[ 269 ]
Data Synchronization
After you've created three sequences—one for each of the AccountTasks, AccountHistories, and AccountFiles tables, your project should now contain the following items:
Adding a publication to your project
Now that you have created all the items you need, you can begin to create the publication itself. Click on the File | New | Publication menu item. In the General tab of the ensuing pop-up window, specify the name of the publication and the client database. The Client Database Name field will become the name of the database generated on the mobile device when the mobile application is synced down to the device.
[ 270 ]
Chapter 6
In the Publication Item tab, you can add a list of existing publication items to this publication. Click the Add button to add a publication item. You will be presented with a screen similar to the one shown below. There are a few fields worth noting in this screen: •
Name You can see the list of all your publication items in the Name field. Select the Accounts publication item.
•
Updateability An updatable publication item means that the sync for this item is bidirectional. A read-only item means that the sync for this item is from master table (server) to snapshot (client) only.
•
Conflict resolution Client Wins means that in the event of a conflict (updates done on the same record at both the server side and client side), the client-side changes will always override the server-side changes. Server Wins means exactly the opposite.
Add the publication item in the same fashion for each of the Accounts, AccountTasks, AccountHistories, and AccountFiles tables. [ 271 ]
Data Synchronization
For the Products table, you will never need to update the server with the list of products from the client device, so the sync will usually be unidirectional (from server to client). You will, therefore, need to set Updatebility to read-only. Since Conflict Resolution is also no longer applicable, set its value to Custom. When you're done, you should have the following in your publication items list.
It is now time for you to add the sequences you've created earlier to the Sequence tab of the publication. Click on the Add button to add an existing sequence to the publication. When you're done, you should see three sequences show in the list as follows:
[ 272 ]
Chapter 6
The other tabs are not relevant and will not need to be filled in. Click on the OK button to create the publication item. You can also opt to save your publication at this point by clicking on the File | Save menu item.
Publishing the mobile application to the mobile server
Your mobile application will not be accessible without first publishing it. Click on the Tool | Package menu item to launch the Packaging Wizard. In the first step, choose to create a new application definition. Click Next to advance to the next window. In the next window, choose the target platform for your mobile application. As you are targeting Windows Mobile 6 on the Pocket PC, choose the Oracle Lite PPC60 ARMV4I;US option.
In the next window, choose a name for your application. This will be the name displayed in Oracle Mobile Server. Specify a virtual path, description, and local application directory. You must then specify the name of the publication you've created earlier in the Publication Name field.
[ 273 ]
Data Synchronization
In the next window, you can add any additional files you wish to include with the mobile application. These can be images, resource files, and so on. You would usually include the .CAB installation files of your application here. The idea is that when an application is synced to the mobile device, it would not only sync the database, but also everything the mobile device needs to get your application up and running. We will cover more on generating .CAB installation files when we touch on application deployment in Chapter 13. For now, you can just leave this section empty.
When you're done, click on the Finish button. You will now see a pop-up window that allows you to choose from a list of actions. Choose to Publish the current application. You will be prompted with the following window. Specify the URL of your WebToGo site as well as the Mobile Server Administrator username and password to proceed.
[ 274 ]
Chapter 6
When that's all done, your application will be published to the mobile server and you will see the following message.
Setting up application users using the WebToGo portal
You will now need to define the users that can access your mobile application. You can do this via the WebToGo portal. The first thing you need to do before you can log in to the WebToGo portal is to start the Oracle Mobile Server service. The Mobile Server service can be started by clicking on the Start | All Programs | Oracle Database Lite 10g | Mobile Server menu item. A command prompt window will appear. You will need to have this command prompt window running in the background.
[ 275 ]
Data Synchronization
Navigate to the WebToGo portal by directing your browser to the following location: http://<server_name>:<mobileserverportnumber>/webtogo
You can log in to the WebToGo portal using the Mobile Server Administrator password. After login, click on the Mobile Manager to view the list of registered mobile servers. Click on the link with your server name. In the ensuing window, click on the Users tab. You will be presented with a list of registered mobile users. Add a new user by clicking on the Add User link. You can key in the details for a new user (shown as follows).
After clicking OK, you will be able to see your user account added to the list (shown as follows).
[ 276 ]
Chapter 6
The next thing you need to do is to make sure that this user has access to your CRMLIVE mobile application. Click on the main Applications tab. You will be able to see a list of all the mobile applications registered with Oracle Mobile Server. You should be able to spot the application you've just published.
Click on your application. You will now be brought to the application details page. Navigate to the Access tab. Here you will be presented with a checklist of user accounts which have access to your application. Ensure that the user account you've just added is granted access by placing a tick in the checkbox next to it. Grant access to a few other users (sample users installed together with the Mobile Server demo) as well as the Administrator.
[ 277 ]
Data Synchronization
The last step you need to carry out is to configure Data Subsetting. Data Subsetting does what it says—it allows you to set filters for each user account so that only subsets of the data are synced down for each user. You probably recall that you've used an OwnerID parameter earlier when creating the Publication Item Queries for each table. This section is where you will set the value for the OwnerID parameter for each user account. Click on the Data Subsetting tab. You will see a list of every mobile user that has access to your application.
Click on the account you've just created. You will be presented with the following screen. Notice that your OwnerID parameter shows up automatically in this list. In fact, any number of parameters that were configured in your Publication Item Queries will show up as a list in this area. Set the parameter value to a value that identifies the owner of the record. We might decide to use the username field. In the following example, we use the username edzehoo. What you've configured is a bit like saying: "For the EDZEHOO user account, sync down all records where the OWNERID is equal to edzehoo." Each mobile user for your application can have the OwnerID value configured this way. This, therefore, allows you to sync down different sets of data for different user accounts.
[ 278 ]
Chapter 6
Data Subsetting In fact, some developers may use different fields to subset their data. For example, some may decide to use department names to subset their data. The effect is that all user accounts that have the same department name would be able to share each other's data.
Registering the mobile device with the mobile server
You will need to register the mobile device with the Oracle Mobile Server before you can perform the initial sync-down. Launch your Mobile Device Emulator and establish an ActiveSync connection between your desktop and the emulator. Do ensure that you have also enabled the NE2000 PCMCIA network adapter on the emulator by going to the File | Configure | Network tab screen in the emulator window. This allows your emulator to connect to the WebToGo server hosted on your desktop. Ensure that any firewall used is turned off as well. (You will be using port 81 to communicate with the WebToGo server)
You can test if your connection to the WebToGo server is running fine by opening the Mobile Internet Browser on the device and navigating to the following location: http://:81/webtogo.
If you are able to see the login page of the WebToGo portal, then you're all set.
[ 279 ]
Data Synchronization
In the programs area of the emulator, launch the Oracle DM tool. Fill in the details of the mobile user that will be using your application (shown in the following screenshot's left screen). Take note to fill in the correct WebToGo server URL as well. Click the OK button when you're done. This will initiate the registration process. If the registration has succeeded, you will see the screen shown at the right the next time you launch the Oracle DM tool.
Synchronizing with the mobile server
You can finally perform your first sync down from the Oracle Mobile server. In the Programs area, launch the Oracle MSync tool. You will need to key in the mobile user account details again. Tick the Save Password checkbox so that you don't have to specify the same details again for the next sync.
[ 280 ]
Chapter 6
When you're done, launch the sync by clicking the Sync button. The Sync progress bars will show you the progress of the sync.
When the sync has completed, you can check to see if the Oracle tables have been created on the mobile device. If you launch the Oracle Msql tool, you will see that a new database, crmlive, has been created on the device. You can connect to it using the SYSTEM user account and password. All the five tables with their corresponding columns will have also been created in this database. You can try a sync by pointing the connection string of your Sales Force application to the crmlive database and then creating a new Lead account in this database. Run the Oracle MSync tool again to sync this record to the server. If you run a SELECT query on the MASTER.Accounts table on your desktop through SQL*Plus, you will be able to see the record you've keyed in from the mobile device. Managing a sync programmatically in Oracle Lite One question that's probably lingering in your mind by now is: Am I able to programmatically manage a sync from within my application? The answer is yes, but unfortunately, you need to use the Mobile Sync Client APIs, which are only accessible through COM or C/C++. This will not be covered in this book.
Synchronizing files with the server
If you have chosen to store your files in the database in a Binary Large Object (BLOB) field, then file synchronization is effortless—binary data is handled just like any other field. No additional code needs to be written. (For SQL Server CE, this is the image data type, and for Oracle, this is the BLOB data type).
[ 281 ]
Data Synchronization
If you are storing files externally in the file system, you will need to write additional code to sync them to the server. The Microsoft Synchronization Services Framework does the bulk of the work for you. You can map a folder on the server to a folder on the mobile device and the Sync framework will do the rest. External file synchronization in an Oracle setup Oracle Mobile Server does not provide an equivalent facility, so if you are using an Oracle setup, you will most likely need to write your own file synchronization service (or use the Microsoft Synchronization Services Framework).
Let's take a look at how this can be done. The first thing you need to do is to download and install the full Microsoft Sync Framework package (which contains the Sync Services for Filesystems components) from this location: http://www.microsoft.com/downloads/details.aspx?FamilyId=C88BA2D1CEF3-4149-B301-9B056E7FB1E6&displaylang=en
After installing this component, you will then need to add references to the following libraries: • •
Microsoft.Synchronization.Files.dll Microsoft.Synchronization.dll
In your class, you will also need to import both the Microsoft.Synchronization and the Microsoft.Synchronization.Files namespaces. Setting up and running the sync is relatively straightforward. The following code shows how this can be easily achieved. using Microsoft.Synchronization; using Microsoft.Synchronization.Files; public void Synchronize() { string _sourceFolder = "\\My Documents"; string _destFolder = "d:\\TargetFolder"; // Create the source and destination providers FileSyncProvider sourceProvider = new FileSyncProvider(Guid.NewGuid(), _sourceFolder); FileSyncProvider destProvider = new FileSyncProvider(Guid.NewGuid(), _destFolder); // We request the providers to detect any changes in the folder
[ 282 ]
Chapter 6 sourceProvider.DetectChanges(); destProvider.DetectChanges(); // We do a bidirectional sync of files in both folders SyncOrchestrator agent = new SyncOrchestrator(); agent.LocalProvider = _sourceProvider; agent.RemoteProvider = _destProvider; agent.Direction = SyncDirectionOrder.UploadAndDownload; agent.Synchronize(); }
Creating network-aware synchronization modules In a Microsoft SQL Server CE setup, you may want to initiate a sync automatically when a network connection is detected. You can do this quite easily by tapping into the WindowsMobile.Status namespace to be automatically notified whenever a change in the number of network connections occurs on the mobile device. The SystemProperty.ConnectionsCount property increments whenever a GPRS/EDGE, Wi-fi, or any other connection is established. Using the SystemState class that you've learned in the last chapter, you can listen in on this property and be automatically notified once its value changes. You can achieve this with the following code snippet. using Microsoft.WindowsMobile.Status; SystemState _detector; //Setup the event handler public void StartDetector() { _detector = new SystemState(SystemProperty.ConnectionsCount, true); _detector.Changed += detector_Changed; } //The event handler will check if there is more than 1 //connection available. If there is, a sync is initiated private void detector_Changed(object sender, ChangeEventArgsargs) { if (SystemState.ConnectionsCount>0) { //Run the sync here PerformTheSync(); } } [ 283 ]
Data Synchronization
Summary
In this chapter, you've taken an in-depth look at how to set up the mobile device and the server for synchronization in both the SQL Server CE and Oracle Lite setups. You have learned how to do the following: •
Set up the Microsoft Synchronization Services Framework for synchronization
•
Set up the Oracle Mobile Server for synchronization
•
Synchronize externally stored files with the server
•
Create a network-aware synchronization module
In the next chapter, you'll take a look at how you can tweak and fine-tune the performance of your application and the database.
[ 284 ]
Optimizing for Performance In web-based server applications, performance optimization is understandably a very necessary part of the software development lifecycle—after all, you are writing code to service hundreds of users at the same time. However, in a single user environment, such as the mobile device, you might wonder if this is necessary. The truth is, sometimes mobile users may not need applications that run faster, but applications that respond faster. A mobile device has hardware limitations that will always place it behind the PC in terms of power and speed, so it will never be expected to number crunch or load data as fast as the PC can. By designing your mobile application so that it will be constantly and highly responsive to user input, it can make up for its limitations by providing a more fluid user experience. In this chapter, you will learn how to write faster .NET CF code that facilitates responsive user interfaces. You will cover the following: • How to measure code performance • How to measure application performance statistics • How to optimize database performance through data caching and indexes • How to use the GZIP compression library provided by Microsoft to compress data for more efficient data transfer • The best practices to adopt when manipulating strings, WinForms, XML, and files in the .NET Compact Framework • An overview of how garbage collection in the .NET Compact Framework works
Optimizing for Performance
Measuring performance
Before you can optimize your code for performance, you obviously need to know if it is underperforming in the first place. You can measure performance at two basic levels—at the code level and at the application level. At the code level, developers typically use timers in order to time sections of code and to then display the time taken for the piece of code to execute. At the application level, the developer might need to know, for instance, how much memory an application has been consuming or how many times garbage was collected during its lifetime. In the following sections, we will explore how you can set up your code and application to collect this diagnostic information.
Measuring .NET CF code performance
The most basic way to measure the performance of a piece of code is to: 1. Capture the start time. 2. Perform the action. 3. Capture the end time. 4. Calculate the difference (delta). There are a few ways to capture time. First, you have the Environment.TickCount .NET method (which is based on the Win32 API GetTickCount() method). It returns the amount of time (in milliseconds) that has passed since the device booted. However, there are two immediate problems with Environment.TickCount: • It has poor accuracy (the variance can be up to 500 milliseconds) • It has a limited size—for devices that have been running for a long time, the tick count may overflow, and end up being a negative number The better (and more accurate) way to measure time would be to use a high resolution timer. The QueryPerformanceCounter method returns the current value of the high resolution performance counter, and the QueryPerformanceFrequency method the number of counts per second. To measure performance, you can: 1. Call QueryPerformanceCounter to capture the start time. 2. Perform the action. 3. Call QueryPerformanceCounter again to capture the end time.
[ 286 ]
Chapter 7
4. Calculate the difference (delta) between these two values and divide it by the frequency retrieved via QueryPerformanceFrequency. This will return the amount of seconds elapsed. You can also retrieve the time in milliseconds by performing a simple conversion. Let's build a class to do this. Create a new class called PerfTimer and add it to the CRMLiveFramework project. You will need two functions: Start and Stop, to signal the capturing of the start time and corresponding stop time. The Stop function will return the amount of time elapsed (in milliseconds). The QueryPerformanceCounter and QueryPerformanceFrequency functions are Win32 API functions and do not have any .NET framework equivalent. You will thus need to declare these external functions using the DllImport keyword. The code for the PerfTimer class follows: using System; using System.Runtime.InteropServices; public class PerfTimer { [DllImport("coredll.dll", EntryPoint = "QueryPerformanceCounter")]public static extern int QueryPerformanceCounter(long perfCounter); [DllImport("coredll.dll", EntryPoint = "QueryPerformanceFrequency")]public static extern int QueryPerformanceFrequency(long frequency); private long _timerFreq; private long _startTime; private long _endTime; public PerfTimer() { if (QueryPerformanceFrequency(_timerFreq) == 0) { throw (new Exception("Could not retrieve timer frequency")); } } public void StartTimer() { if (QueryPerformanceCounter(_startTime) == 0) [ 287 ]
Optimizing for Performance { throw (new Exception("Could not retrieve timer start time")); } } public long StopTimer() { if (QueryPerformanceCounter(_endTime) == 0) { throw (new Exception("Could not retrieve timer end time")); } return ((_endTime - _startTime) * 1000) / _timerFreq; } }
Now let's see how you can use this class. In the SalesForceApp project, open the AccountDetails form. Look for the RefreshPage method. The GetAccountsByType method is called here—let's measure the time it takes for the dataset to be retrieved from this method call. Add the lines highlighted in the following code: private void RefreshPage() { try { PerfTimer _timer = new PerfTimer(); _timer.StartTimer(); _accounts = GlobalArea.Application.GetAccountsByType(_type, _totalRecords, pgPager.CurrentPage, _recordsPerPage, _SortColumn, _SortOrder); MessageBox.Show("Data retrieved in :" + _timer.StopTimer() + " milliseconds"); dgAccounts.DataSource = _accounts; dgAccounts.Refresh(); } catch (Exception ex) { MessageBox.Show(ex.ToString()); } }
[ 288 ]
Chapter 7
Run the application and try opening the Leads Listing page. The message shown in the next screenshot will be immediately displayed.
Capturing application performance statistics
In the preceding section, you've learned how to measure performance at the code level. However, you can also retrieve performance statistics at the application's working-set level via internal counters maintained by the .NET Compact Framework. This includes, for example, the amount of memory allocated or deallocated during the application's execution. To activate these performance counters, you will first need to enable a key in the registry of your mobile device. Windows Mobile does not come with a tool to edit the registry, so you will either need to get a third-party tool or do it remotely from your desktop using the bundled CE Remote Tools package from Microsoft. Launch the ccregedt.exe application from this location on your desktop: \Program Files\CE Remote Tools\5.01\bin\ccregedt.exe
[ 289 ]
Optimizing for Performance
You should be able to see a window that allows you to choose a target device. Choose the Windows Mobile 6 Professional Emulator device.
This will launch the emulator. At the same time, you will be able to edit the registry details for the emulator device using an interface that is similar to the desktop-based regedit.exe tool.
Expand the Windows Mobile 6 Professional Emulator node and create a new key named PerfMonitor under the following key: HKEY_LOCAL_MACHINE\Software\Microsoft\.NET CompactFramework
[ 290 ]
Chapter 7
In this key, create a DWORD value with the name Counters, and set its value to 1. This will signal the framework to generate a .Stat file whenever a managed application executes. If you wish to disable capturing of performance statistics, set this value to 0. This key and value can be seen in the following screenshot:
You can save the emulator's state at this point. Run the sales force application now by navigating to its folder. After the application launches, close and exit the application. You will now notice that a new file with the name of your application and the .stat extension (for example, SalesForceApp.stat) has been automatically generated in the root folder of the device. Copy this file to your desktop and open it using Microsoft Excel. You will be able to see a list of performance counters and their values. Through this set of statistics, you can determine the total amount of different resources used throughout your application. If, for instance, you wanted to reduce the memory footprint size of your application, you could use this set of statistics as a gauge for memory consumption. You can also view the .stat file using the .NET Compact Framework Remote Performance Monitor tool at the following location: \Program Files\Microsoft.NET\SDK\CompactFramework\v2.0\bin\NetCFRPM. exe
[ 291 ]
Optimizing for Performance
You can open a .stat file from the File | Open menu in the tool. The Remote Performance Monitor displays a more comprehensive view of the counters together with brief explanations of each counter.
You can read about each of these counters listed in the previous section in detail at the following location: http://msdn.microsoft.com/en-us/library/ms172525.aspx
Optimizing database performance
There are a few things you could do to optimize database performance. In this section, we explore two approaches: • Caching data lists to improve loading performance • Using database indexes to improve search performance
[ 292 ]
Chapter 7
Data caching
When you cache data in memory, you do not need to retrieve the same data from the database each time you need to use it. This can lead to considerably better performance as it cuts down the database round trip required to retrieve that data. Having said that, you shouldn't go around caching everything you use. For example, short lists of data that don't change frequently would benefit more from caching compared to large (for example, 100 records and above) amounts of data. The following are general guidelines to follow in selecting suitable candidates for data caching: • Short lists of data (< 100 records) • Data that is not expected to change frequently • Data that is used repeatedly throughout the application (for example, data in drop-down lists) Now let's take a look at how you can apply data caching to the sales force application. If you recall, you've created the Products table, which contains the full list of products in your application. This same list is retrieved from the database each time you view the details of an existing lead, and is displayed in the Interest tab of the Account Details window. As the list of products is seldom expected to change once it has been synced down from the server, we can cache it in memory to improve performance. All you need to do to cache the list of products in memory is the following: 1. Retrieve the full list of products from the database into a ProductCollection object when viewing account details for the first time. 2. Store this ProductionCollection object in a global variable. 3. On all subsequent requests to view account details, retrieve the list of products from this cached object instead of the database. Let's make some changes to your code. Declare a public static ProductCollection variable named CachedProducts to the GlobalArea class in your SalesForceApp project (as highlighted in the following code snippet). public class GlobalArea { private static PluginManager _PluginManager = new PluginManager(); private static Application _Application = new Application(); private static CustomErrorHandler _ErrorHandler = new CustomErrorHandler("\\My [ 293 ]
Optimizing for Performance Documents\\Personal\\error.log"); private static ProductCollection _CachedProducts = null; public static ProductCollection CachedProducts { get { return _CachedProducts; } set { _CachedProducts = value; } } . . . }
Now let's make some changes to the function that retrieves the list of products from the database. This is the GetProductList() function in the Application.cs file. To see the difference in terms of performance, you can also use the PerfTimer class you've created earlier to display the time elapsed in retrieving the list of products. All these changes are highlighted in the following code snippet: public ProductCollection GetProductList() { DataSet _productsDataset; ProductCollection _products; PerfTimer _timer = new PerfTimer(); _timer.StartTimer(); if (GlobalArea.CachedProducts == null) { _productsDataset = GlobalArea.PluginManager.GetActivePlugin. GetProductList(); _products = new ProductCollection (_productsDataset.Tables["Products"]); } else { _products = GlobalArea.CachedProducts; } MessageBox.Show("Loading the product list took :" + _timer.StopTimer() + " milliseconds"); return _products; } [ 294 ]
Chapter 7
If you now run the sales force application and try to view an existing account or create a new account, you will see the following pop up. As this is the first time you are accessing this screen, the cache has not yet been loaded and so, a round trip is made to the database to retrieve the list of products. After that, this list is then stored in the cache.
If you close the Account Details window and reopen it, you will see that the load time has now been reduced to a mere 19 seconds (only 10 percent of the original time taken!). This is because it is now retrieving the list of products from the cache.
If you use these types of lists frequently in your application, you can realize huge performance cost savings by caching these lists in the same preceding fashion.
Using database indexes to boost search performance
In Chapter 4, you've built a search engine capable of searching through the list of accounts in the Accounts table. When the list of accounts increases into the hundreds or thousands, you will find that the time it takes to perform a search starts to increase. It is not unheard of to have users wait 10 seconds or more for a search to complete. This is, of course, unacceptable in cases where the application is time critical. You can easily boost search performance without changing a single line of code just by creating an index on regularly searched columns in the database. Indexes allow the database engine to build an internal "map" of your data that allows the database to zoom and narrow down on the exact desired data. Without an index, the database would have to do a full table scan—reading each table row by row to find the matching record.
[ 295 ]
Optimizing for Performance
For example, consider the following SQL statement. Without an index, the database engine would have to scan the entire Accounts table row by row to check if the FirstName matches "John." SELECT * FROM Accounts WHERE FirstName = 'John'
If you create an index on the FirstName column in the Accounts table, any and all SQL statements that filter data based on the FirstName column will experience a large performance increment. You can create an index in SQL Server CE using the following DDL statement: CREATE INDEX FirstNameIdx ON Accounts(FirstName ASC)
For the Oracle Lite database, you can create an index using the following DDL: CREATE INDEX FirstNameIdx ON Accounts(FirstName)
Run these DDL statements using the Query Analyzer or MSql tools provided by SQL Server CE and Oracle Lite respectively. You can also create indexes on multiple columns at the same time. For example, if you know that 80 percent of your users will search using both the FirstName and the AccountType columns, you can create a single index that maps to both of these columns. The following DDL shows how this can be done on SQL Server CE: CREATE INDEX FirstName_TypeIdx ON Accounts(FirstName ASC, AccountType ASC)
The following DDL shows how this can be done on Oracle Lite. CREATE INDEX FirstName_TypeIdx ON Accounts(FirstName, AccountType)
Multi-column indexes such as the one we just saw would yield the most optimal performance if users specify both the first name and account type in the search. If the user specifies only the first name or only the account type, the index will not be optimally utilized, and this will lead to little or no performance increment. Too many indexes Take note that when you create an index on a table, the performance of any SQL INSERT or UPDATE done on that table decreases slightly—this is because the database engine has to now store additional index data for each record written in the database. If you create too many indexes, the INSERT and UPDATE performance decrement may build up undesirably. You should, therefore, build indexes only on columns or column groups that are anticipated to be frequently used in a search. For example, if you know that most of your users will run a search using the first name of the account rather than the last name, create an index only on the FirstName column. [ 296 ]
Chapter 7
Other database optimization tips
Mobile devices don't come with a lot of memory (at least not as much as a desktop PC). A large part of database optimization on the mobile device focuses on improving performance by reducing memory consumption as much as possible. The following guidelines will help you write better code to this effect: • In your SQL queries, avoid using the SELECT * notation. SELECT * returns all columns in a table, which for a large table would be an unnecessary waste of memory space. Specify the columns you need to use explicitly in the SQL statement. • Minimize the number of open objects, such as cursors or record sets held in memory. Use them when you need to, and then close and dispose of them immediately afterwards. • Avoid using DISTINCT or GROUP BY clauses if possible. These SQL keywords are relatively slow operations in the database, especially if you have a large number of records in the table. • Avoid using cursors if possible, but if you need to, try to use forward-only cursors.
Optimizing data transfer performance
If you recall from Chapter 5, Building Integrated Services, you've built a facility to transfer an account from one device to another via Infrared and Bluetooth. In these cases, you are transferring the dataset as is without any sort of compression. This can lead to slow transfer, especially for larger datasets or larger numbers of records. Microsoft offers data compression functionality via the System.IO.Compression namespace. The GZipStream class allows you to compress a byte array based on the GZIP standard. The GZipStream class cannot be used to compress files larger than 4 GB in size.
After compressing data, the GZipStream class does not store the original size of the uncompressed data. You must, therefore, implement your own method to "remember" and store this original size somewhere so that it can be used later during decompression. A common approach is to store this size as the first four bytes of the compressed stream.
[ 297 ]
Optimizing for Performance
Let's take a look at the following code to do this. You can also print the size of the data before and after compression to see the performance difference with and without using GZIP compression. Make the following changes to your SerializeDataset function (highlighted in bold): public static byte[] SerializeDataset(DataSet Data) { StringWriter strWriter = new StringWriter(); string _result; ASCIIEncoding _encoding = new ASCIIEncoding(); Data.WriteXml(strWriter); _result = strWriter.GetStringBuilder().ToString(); byte[] _buffer = _encoding.GetBytes(_result); MessageBox.Show("Buffer length before compression :" + _buffer.Length.ToString()); //We create a GZipStream object or compression MemoryStream _stream = new MemoryStream(); GZipStream compressedzipStream = new GZipStream(_stream, CompressionMode.Compress, true); //We calculate the uncompressed data length and write it as //the first four bytes of the stream. We will need this //length during decompression int _totalLength = _buffer.Length; compressedzipStream.Write(BitConverter.GetBytes (_totalLength), 0, 4); //We now write the remaining data to the stream compressedzipStream.Write(_buffer, 0, _totalLength); compressedzipStream.Close(); _buffer = _stream.ToArray(); MessageBox.Show("Buffer length after compression :" + _buffer.Length.ToString()); return _buffer; }
Now, let's take a look at the opposite equivalent. The changes you need to make to the DeserializeDataset method are highlighted in bold in the following code snippet: public static DataSet DeserializeDataset(byte[] Data) { //Use the GZipStream class to decompress the incoming data MemoryStream _stream = new MemoryStream(Data); GZipStream compressedzipStream = new GZipStream(_stream, [ 298 ]
Chapter 7 CompressionMode.Decompress); //Read the first four bytes from the stream – this is the //size (in bytes) of the rest of the stream byte[] _totalLength = new byte[4]; compressedzipStream.Read(_totalLength, 0, 4); int _dataSize = BitConverter.ToInt32(_totalLength, 0); //Create a byte array that has the exact size of the //decompressed data, and read from the GZipStream object byte[] _data = new byte[_dataSize]; compressedzipStream.Read(_data, 0, _dataSize); compressedzipStream.Close(); //Convert the decompressed stream into a Dataset ASCIIEncoding _encoding = new ASCIIEncoding(); string _stringData; _stringData = _encoding.GetString(_data, 0, _data.Length); StringReader _reader = new StringReader(_stringData); DataSet _ds = new DataSet(); _ds.ReadXml(_reader); return _ds; }
As it is probably tedious to test using two mobile devices running Bluetooth at the same time, you can create a simple form to see the effect of GZIP compression. Create a new form with any name, place a button on this form, and write the following code in the click event of the button: private void button1_Click(object sender, EventArgs e) { DataSet _testSet= new DataSet(); _testSet.Tables.Add (new DataTable("TestTable")); _testSet.Tables[0].Columns.Add (new DataColumn ("TestCol", System.Type.GetType ("System.String"))); //Generate 1000 dummy records for (int i = 0; i <= 1000; i++) { DataRow myrow = _testSet.Tables[0].NewRow(); myrow["TestCol"] = "Test"; _testSet.Tables[0].Rows.Add(myrow); }
[ 299 ]
Optimizing for Performance //Serialize and compress the dataset byte[] _testArray= SerializeDataset (_testSet); //Decompress and deserialize the dataset _testSet= DeserializeDataset (_testArray); //Ensure the dataset has been decompressed correctly - show //the number of records in the decompressed dataset MessageBox.Show("Total Records: " + _testSet.Tables[0].Rows.Count.ToString ()); }
If you run this form and click on the button, you will be greeted with this pop-up message box. You can see that the serialized dataset before compression is about 54,081 bytes in size.
After compression you will notice that the length of the data is now 736 bytes. This is about a mere 1.4 percent of the original size of the data!
To compress or not to compress GZIP compression works exceptionally well with text-based data. You will get better compression rates with larger text data. In fact, for smaller bits of text data (for example, 500 bytes or less), the compression may even yield sizes larger than the original data itself. You should implement routines in your code to check the size of your input data before deciding whether to run your data through compression or not.
Managing better code
There are many other good practices to adopt when writing code in the .NET Compact Framework. They can help drive your application towards a smaller memory footprint and ultimately better performance. We will cover some of these practices here. [ 300 ]
Chapter 7
Managing better strings
String manipulation is a common operation in any application. Throughout the sales force application you have been using the + operator to concatenate strings. The String class is immutable. This means that each time you concatenate strings in the preceding fashion it creates a new string object. This can lead to inefficient memory utilization (and more work for the garbage collector). You can improve the memory utilization of strings in your application using the
StringBuilder class. Let's take a look at the performance differences between these
two methods shown next. Create a new form and place two buttons on the form. Write the following code in the click event of the two buttons: private void btnStringConcat_Click(object sender, EventArgs e) { String _temp=""; for (int i = 0; i <= 10000; i++) { _temp = _temp + "test"; } MessageBox.Show("Done!"); } private void btnStringBldr_Click(object sender, EventArgs e) { StringBuilder _temp = new StringBuilder(); for (int i = 0; i <= 10000; i++) { _temp.Append("test"); } MessageBox.Show("Done!"); }
The first function in the preceding code performs 10,000 string concatenations while the second function uses the StringBuilder to perform 10,000 appends. Run the application and click on the first button (btnStringConcat). After that, close the form. A .stat file will be generated in the root folder of the mobile device. Take note of the following counters (which show the amount of string objects allocated, their size in bytes, and so on). Managed String Objects Allocated Bytes of String Objects Allocated Garbage Collections (GC) Bytes Collected By GC
[ 301 ]
10027 400140456 438 407222100
Optimizing for Performance
Now run the same application again and click the second button (btnStringBldr). Another .stat file will be generated. Now take note of this second set of counters. Managed String Objects Allocated Bytes of String Objects Allocated Garbage Collections (GC) Bytes Collected By GC
39 262584 1 296332
The first thing you will immediately notice is that with the StringBuilder class, fewer managed string objects are allocated. The size of the allocated objects is also much smaller. This equates to less memory traffic. It is, therefore, a good idea to use the StringBuilder class whenever possible for strings that change frequently.
Managing better Winforms
There are a few good practices to keep in mind when manipulating Windows Forms. Let's take a look at some of them in the following sections.
Using BeginUpdate and EndUpdate
You should generally use the BeginUpdate and EndUpdate methods for controls that offer them, such as the ListBox, ComboBox, ListView, and TreeView controls. The BeginUpdate method prevents the control from repainting itself until the EndUpdate method is called. It is typically used to prevent the control from repainting itself when it is being populated. For example, the following code snippet prevents a ListBox from repainting itself while it is populated with 3,000 items. public void PopulateListBox() { _listBox.BeginUpdate(); for(int i = 1; i < 3000; i++) { _listBox.Items.Add("Test"); } _listBox.EndUpdate(); }
Using SuspendLayout and ResumeLayout
You should similarly use the SuspendLayout and ResumeLayout methods when you plan to reposition any controls on the form. SuspendLayout will suspend any layout logic while changes are made to the controls on a form until ResumeLayout is called. Let's take a look at how these functions can be used:
[ 302 ]
Chapter 7 public void ChangeControls() { this.SuspendLayout(); button1.Location = new Point(50,50); button1.Size = new Size(100,100); button1.Text = "Test"; this.ResumeLayout(); }
Load and cache forms in the background
Forms with large numbers of controls can sometimes take some time to load on the mobile device. If a form is expected to be reused many times throughout the application, you should consider caching the form and using the Show and Hide methods of the form to control its visibility. For this same reason, you should not perform lengthy operations (such as populating data on the form) in the Show event of the form. Doing so will cause the UI to experience a period of inactivity (the form becomes visible only after the Show event has executed). This isn't good for your users because they will experience lower UI responsiveness. In the same vein, you should also abstain from making calls that block the UI in any way. Consider using threads or asynchronous calls when you need to make extended function calls.
Managing better XML
When manipulating large XML data files, there are a few ways to improve memory utilization and performance. They are outlined in the following sections.
Using XMLTextReader and XMLTextWriter
You should consider using XMLTextReader and XMLTextWriter instead of XMLDocument. This is because XMLDocument uses more memory and also builds an untyped object model using a tree, which performs inefficiently. When to use XMLDocument XMLDocument can be used, however, for smaller XML documents (smaller than 64KB).
[ 303 ]
Optimizing for Performance
You can also set the IgnoreWhiteSpace and IgnoreComments property values to true in order to increase the performance of the XMLTextReader and XMLTextWriter. You can set these settings in the following fashion: XmlReaderSettings _settings = new XmlReaderSettings(); _settings.IgnoreWhitespace = true; XmlReader reader = XmlReader.Create("test.xml", _settings);
XML serialization and deserialization thesis
When serializing or deserializing large amounts of XML data, you should consider building a custom binary serialization mechanism (using the BinaryReader and BinaryWriter class). This is because XML serialization produces large amounts of textual data (tag names, attribute names, and so on) that takes up more memory than necessary. You can also consider Microsoft's GZIP compression (covered earlier) to compress XML data for storage or transmission. XML that is more attribute-centric (uses attributes to store data) rather than element-centric also performs faster during deserialization.
Managing better files
When reading from files locally on the mobile device, it is a good practice to use the FileStream class. The FileStream class outperforms the StreamReader class because it does not look for line breaks, whereas the StreamReader does. This contributes to the slow performance of StreamReader.
The .NET Compact Framework garbage collector
The .NET Compact Framework, just like the full .NET framework, features a garbage collector that automatically runs on its own to free up unused memory to the operating system. The garbage collector runs when any of the following conditions are met: • The application is moved to the background: When an application is moved to the background, a garbage collection is run. • After an allocation threshold: A garbage collection is automatically initiated after 1 MB of memory is allocated since the last garbage collection.
[ 304 ]
Chapter 7
• An explicit GC.Collect() call: You can call the GC.Collect() function in managed code to force a garbage collection. • Out-of-memory condition: When the system fails to allocate or reallocate memory, a garbage collection is automatically initiated by the .NET Compact Framework to attempt to free up more memory. The following diagram describes the lifecycle of an object and how it is eventually collected by the garbage collector:
1. When an object is created and memory is allocated for it, the object is considered a live object. 2. When the object can no longer be accessed by code (for example, when there are no more references to the object), the object is considered no longer in use and is eligible for finalization. 3. The garbage collector runs based on any of the four conditions mentioned earlier being met. 4. The garbage collector brings all threads to a "safe point"—bringing all threads to a state where they cannot modify the Garbage Collection (GC) heap further in any way. 5. All live objects that are reachable by the garbage collector are marked.
[ 305 ]
Optimizing for Performance
6. Among those objects that are not reachable (unmarked objects), the ones that do not have finalizers are freed by the garbage collector. The ones that do have finalizers are placed in a finalization queue. 7. The garbage collector runs the finalizers on all objects in the finalization queue. After the unused objects are freed, the collection is considered complete, and all threads are allowed to resume. Garbage collection is a somewhat expensive process—it needs to suspend threads, mark live objects, and so on. Forcing a GC.Collect() call frequently to free up memory is, therefore, not a good practice and may even lead to a significant decrease in performance. If your application runs out of memory more often than usual, you should ensure that your objects have been properly disposed (call the .Dispose() method if it is available in the object). The garbage collector should be left to run automatically.
Summary
In this chapter, you've taken a look at some of the approaches and methods to optimize your application for performance. You've covered how to: • Use data caching to cache frequently used data lists • Use database indexes to boost search performance • Use GZIP compression to compress textual data • Write faster code to manipulate strings, WinForms, XML, and files in the .NET Compact Framework More importantly you've managed to try your hand at measuring code performance in your application and also to use the application performance counters as a gauge of the memory footprint of your application. In the next chapter, you will look at how you can write code to increase security in the sales force application and database.
[ 306 ]
Securing the Application Security is something that tends to be overlooked, especially in the case of mobile device projects. One likely reason is that mobile applications are not expected to be used in a multiuser environment. Mobile developers often take the shortcut of not implementing any authentication at all. Their argument is that "it's your own device and it's only meant for a single user." Some even see the login step for the mobile device as counter-productive. The truth is that mobile devices tend to get misplaced a lot, and most of the time the data within is left unprotected. If the device contains sensitive information such as the financial or medical records of a customer, a security compromise (such as data theft) could lead to serious consequences. In this chapter, we explore why encryption and authentication are equally important, and how to implement them in your sales force application. We will cover these topics as follows: •
How to encrypt and password protect your database
•
How to implement authentication for the sales force application
•
How to secure data transfer between devices
Encrypting the database
When you choose to encrypt a database, you are encrypting the entire content of the database file including the tables, data, and other database objects. This means that even if the .SDF or .ODF file was stolen from your mobile device, the content would be undecipherable. This is an important aspect of mobile device programming—the portable nature of the mobile device makes it easy for users to misplace their devices. If the database is left unencrypted, an unauthorized user could retrieve your data with relatively little effort.
Securing the Application
Encrypting the SQL Server CE database
The SQL Server CE database comes with an encryption utility that allows the entire content of the .SDF file to be encrypted. Should I worry about performance? The performance cost of activating encryption is negligible, so you don't have to worry about enabling it.
You can specify to encrypt the SQL Server CE database by specifying the Encrypt Database=true setting when creating the database using the SqlCeEngine class. _dbcreationstring = "Data Source='" + _SDFPath + "'; LCID=1033;Password='admin123'; Encrypt database= TRUE;"; _engine = new SqlCeEngine(_dbcreationstring); try { _engine.CreateDatabase(); } catch (Exception ex) { throw ex; } finally { _engine.Dispose(); }
You can modify the code in the CreateSalesForceDatabase method that you've created during Chapter 2 in SQLServerPlugin.PluginClass. When you enable encryption, you must also ensure that a valid Password has been specified. The database will be encrypted using this password value. There is no difference in terms of the code used to connect to the database. As long as you've included the same password in your connection string, you will be able to access the database: Data source='\My Documents\salesforce.sdf'; Password=admin123;
[ 308 ]
Chapter 8
Encrypting the Oracle Lite database
For Oracle Lite, it is equally simple to set up an encrypted database. You just need to call the EncryptDatabase method after the CreateDatabase() method. try { OracleEngine.CreateDatabase("salesforce", "salesforce", "admin123"); OracleEngine.EncryptDatabase("salesforce", "salesforce", "admin123", "admin123"); } catch (Exception ex) { Interaction.MsgBox(ex.ToString, MsgBoxStyle.Exclamation, "Create database"); return false; }
Like the SQL Server CE database, the encryption of the underlying database is transparent to the accessing application.
Authenticating the sales force application
You have probably noticed by now that you did not build an authentication mechanism for the sales force application. Users need to be authenticated before they access their hand-held mobile devices for of the following reasons: •
Changes are made to the master database at the server during synchronization. If the synchronization source was unauthorized, this may lead to data compromise or theft.
•
Unauthorized persons (for example, in the case of a stolen device) can access the data on the mobile device.
[ 309 ]
Securing the Application
To implement authentication, you must first create the login screen. Create the following form and lay out the controls appropriately on the form.
The login screen works in the following way: 1. Upon the first-time use of this application, it will show a message requesting the user to key in a username and password. This becomes the master account. 2. On subsequent runs, the user will be able to log in with this master account. 3. The password for this master account will be stored in the database as a one-way encrypted string (a hash).
Performing one-way encryption using SHA256 One-way encryption, as opposed to two-way encryption, produces a value that cannot be decrypted (in the other direction) to produce the original value. It is hence useful for password verification. This is done in the following steps:
1. The master account is created—a one-way encrypted value for the password is produced and stored in the database. 2. Every time the user wishes to verify the authenticity of his password, the password will be encrypted using the same one-way encryption process, and will produce a hash value. This value is compared to the value in the database. If they match, access is granted. This way, you never need to know what the original password was in the first place. [ 310 ]
Chapter 8
To produce a one-way encrypted value or a hash value from a string of bytes, you can use SHA256, which is a commonly known hashing algorithm. SHA, short for Secure Hashing Algorithm is a set of algorithms developed by the National Security Agency (NSA) to produce a hash output (or "fingerprint") from an input message of any size. SHA256 is a variant of SHA that outputs a 256-bit sized hash.
There are other algorithms too. A few of them are listed as follows, together with their differences. Algorithm name
Details
CRC32
Low security, fast speed
SHA1
Moderate security, medium speed
SHA256
High security, low speed
SHA384
High security, low speed
SHA512
Very high security, low speed
MD5
Moderate security, medium speed. Take note that MD5 is already obsolete, and should no longer be used.
As we will focus on SHA256, let's see how you can write a function to encrypt a string in SHA256. Before you can use SHA256, you must first import the following namespaces: using System.Security.Cryptography; using System.Text; public string SHA256Encrypt(string EncString) { string SHAString = null; byte[] EncStringBytes = null; UTF8Encoding Encoder = new UTF8Encoding(); EncStringBytes = Encoder.GetBytes(EncString); //Create the SHA256 hash SHA1CryptoServiceProvider SHAHasher = new SHA1CryptoServiceProvider(); EncStringBytes = SHAHasher.ComputeHash(EncStringBytes); SHAString = BitConverter.ToString(EncStringBytes); SHAString = SHAString.Replace("-", ""); return SHAString; } [ 311 ]
Securing the Application
Create an empty form, place a button on it, and in the click event of this button write the following code: MessageBox.Show(SHA256Encrypt("HELLO TEST"));
When you launch this form and click the button, you will see that the message has been encrypted using SHA256. Take note that whatever the length of the original message, it will always produce a hash of the same length. The great thing about SHA256 encryption is that small changes in the original text will translate to large changes in the hash value. This makes it especially sensitive to any data change.
Writing the code for authentication
You will need to store the one-way encrypted password for the master account so that it can be used for future password verification. Although the password is in encrypted form, it is not a good idea to leave it around in a file on the mobile device. The reason for this is that someone could potentially acquire this hash value, and then write a separate program to run a brute-force comparison by running words in a dictionary through the same SHA256 encryption and then comparing it with the hash value. It is a good idea to store this password in the database. Your database has the added encryption functionality provided by both Oracle Lite and SQL Server C—this will make it a bit more difficult for unauthorized persons to retrieve the hash values. Let's first create a new table in the database. You can do this via the Oracle mSQL tool or the QueryAnalyzer 3.5 tool. CREATE TABLE UserLogons ( Username NVARCHAR(20), Password NVARCHAR(50) )
We will just use this table to store our master account details. It will be expected to store only one single record at any point in time. You will now need to add a few more functions (for the login process) to the IDataLibPlugin interface.
[ 312 ]
Chapter 8 public interface IDataLibPlugin { . . . //This function will attempt to login the user by comparing //his or her password against the hashed value in the //database bool Login(string Username, string Password); //This method will check if this is the first login attempt //after the application installation bool IsFirstTimeLogin(); //This method will create the master login account and save //its details to the UserLogons table bool CreateLogin(string Username, string Password); }
The next step would be to implement these functions, in both the Oracle Lite and SQL Server CE plugins. Let's take a look at the code for the SQL Server CE plugin (I'll leave you to do the same for the Oracle Lite plugin): public bool Login(string Username, string Password) { SqlCeCommand _command; int _recordCount; _command = _globalConnection.CreateCommand(); //Do a compare against the hashed password stored in the //database. _command.CommandText = "SELECT COUNT(*) AS RecordCount FROM UserLogons WHERE Username=@Username and Password=@Password"; _command.Parameters.Add("@Username", Username); _command.Parameters.Add("@Password", Password); try { _recordCount = (int) (_command.ExecuteScalar()); } catch (Exception ex) { _recordCount = 0; throw (ex); } [ 313 ]
Securing the Application _command.Dispose(); _command = null; if (_recordCount == 0) { return false; } else { return true; } } public bool IsFirstTimeLogin() { SqlCeCommand _command; int _recordCount; _command = _globalConnection.CreateCommand(); //Checks if there is already a record in UserLogons table. _command.CommandText = "SELECT COUNT(*) AS RecordCount FROM UserLogons"; try { _recordCount = (int) (_command.ExecuteScalar()); } catch (Exception ex) { _recordCount = 0; throw (ex); } _command.Dispose(); _command = null; if (_recordCount == 0) { return true; } else { return false; } } public bool CreateLogin(string Username, string Password) { [ 314 ]
Chapter 8 bool returnValue; SqlCeCommand _command; int _recordCount; _command = _globalConnection.CreateCommand(); //The password is encrypted outside and passed in to this //function _command.CommandText = "INSERT INTO UserLogons (Username, Password) VALUES (@Username, @Password)"; _command.Parameters.Add("@Username", Username); _command.Parameters.Add("@Password", Password); try { _recordCount = _command.ExecuteNonQuery(); if (_recordCount > 0) { returnValue = true; } else { returnValue = false; } } catch (Exception ex) { returnValue = false; throw (ex); } _command.Dispose(); _command = null; return returnValue; }
Now write the code for the Login form. The highlighted code shows the code for the main logic of the login form. public partial class Login { public Login() { InitializeComponent(); } private bool _firstTimeLogin = false;
[ 315 ]
Securing the Application public void mnuExit_Click(System.Object sender, System.EventArgs e) { System.Windows.Forms.Application.Exit(); } public void mnuLogin_Click(System.Object sender, System.EventArgs e) { if (txtUsername.Text.Length == 0 || txtPassword.Text.Length == 0) { MessageBox.Show ("Please ensure you have specified both the login username and password"); return; } //Encrypt the password string _encryptedPassword; _encryptedPassword = SHA256Encrypt(txtPassword.Text); if (_firstTimeLogin == true) { GlobalArea.Application.CreateLogin(txtUsername.Text, _encryptedPassword); this.DialogResult = System.Windows.Forms.DialogResult.Cancel; this.Close(); NavigationService.ShowDialog("MainMenu", null); } else { if (GlobalArea.Application.Login(txtUsername.Text, _encryptedPassword) == false) { txtUsername.Text = ""; txtPassword.Text = ""; MessageBox.Show("The username and password combination is not correct"); } else { this.DialogResult = System.Windows.Forms.DialogResult.Cancel; this.Close(); NavigationService.ShowDialog("MainMenu", null); } } [ 316 ]
Chapter 8 } public string SHA256Encrypt(string EncString) { string SHAString = null; byte[] EncStringBytes = null; UTF8Encoding Encoder = new UTF8Encoding(); EncStringBytes = Encoder.GetBytes(EncString); SHA1CryptoServiceProvider SHAHasher = new SHA1CryptoServiceProvider(); EncStringBytes = SHAHasher.ComputeHash(EncStringBytes); SHAString = BitConverter.ToString(EncStringBytes); SHAString = SHAString.Replace("-", ""); return SHAString; }
public void Login_Load(object sender, System.EventArgs e) { //Only display the first-time login message if it is a //first time login if (GlobalArea.Application.IsFirstTimeLogin() == true) { _firstTimeLogin = true; } else { _firstTimeLogin = false; } lblFirstTimeMsg.Visible = _firstTimeLogin; } }
Loading the login form
Instead of going straight to the menu, you must now load the login form as the first form of your application. You can easily do that by first adding a new navigation target in the SalesForceApp.NavigationService class (highlighted as follows). public static DialogResult ShowDialog(string DialogName, object Arg1) { DialogResult _dialogResult = 0; switch (DialogName) { [ 317 ]
Securing the Application case "SetupDatasources": PluginsSetup _PluginsSetup = new PluginsSetup(); _dialogResult = _PluginsSetup.ShowDialog(); _PluginsSetup.Close(); _PluginsSetup.Dispose(); _PluginsSetup = null; break; case "Login": Login _login = new Login(); _dialogResult = _login.ShowDialog(); _login.Close(); _login.Dispose(); _login = null; break; case "MainMenu": MainMenu _MainMenu = new MainMenu(); _dialogResult = _MainMenu.ShowDialog(); _MainMenu.Close(); _MainMenu.Dispose(); _MainMenu = null; break; . . . } }
As the last step, you will now need to modify the Main() method in the SalesForceApp.Application class. This will enable the application to launch the Login form first whenever you start the application. public class Application { public static void Main() { InitializeApp(); NavigationService.ShowDialog("Login", null); DeInitializeApp(); } . . . }
[ 318 ]
Chapter 8
That's all there is to it! Now you can try your Login form by running the SalesForceApp project. The first time, it will prompt you for your password. You can see the record for the master account generated in the UserLogons table if you use the Query Analyzer 3.5 tool to run a query off the SQL Server CE database. On all subsequent runs, you will be required to key in that same username and password to log on.
Encrypting data for inter-device transmission using AES
You've learnt how to transmit data via Bluetooth and Infrared from one device to another in Chapter 5, Building Integrated Services. Just to jog your memory a little, you placed the data for a lead account in a Dataset object, serialized it into an array of bytes, transmitted it across to the other device, and then de-serialized the stream of bytes back into a Dataset object). It is unsafe to leave your data unprotected over wireless transmission. For instance, Bluetooth technology works over a distance wide enough for an unauthorized device (and an adept hacker) to intercept the stream of data. The System.Security.Cryptography libraries in the .NET CF provide a number of algorithms that allow you to encrypt and decrypt your data. Unlike the SHA algorithm you've seen earlier, which was one-way, these algorithms are two-way. A key (password) is usually applied during the encryption. An encrypted stream can only be decrypted when the same key is supplied. One of these algorithms is the Rijndael algorithm. Rijndael encryption is also another name for AES (Advanced Encryption Standard). Rijndael encryption comes in three types—128-bit, 192-bit, and 256-bit. The bit size denotes the strength of the key.
In Rijndael encryption, three pieces of information usually need to be supplied: •
The text to encrypt (input)
•
Initialization Vector (IV)
•
Key (password)
[ 319 ]
Securing the Application
The IV is an arbitrary number that can be used along with the key for data encryption. The purpose of an IV is to prevent repetition in data encryption. For example, if every occurrence of the word "hello" in the original text became the sequence "ABC789" after encryption, without an IV, an attacker could assume that every occurrence of "ABC789" meant that it represented the same identical sequence. This way an attacker may be able to identify patterns in the ciphertext. An IV prevents the appearance of corresponding duplicate character sequences in the ciphertext. Let's take a look at how you can apply Rijndael encryption to a simple text message. Create a new C# Smart Device project, add a new form to the project and in this form, import the following library: using System.Security.Cryptography;
The next thing you need to do is write the EncryptAES function. This function (shown as follows) will pass in the plain text, key, and IV into the .NET CF Rijndael cryptography library and set up the stream objects required for the encryption process. The function will return a stream of bytes representing the encrypted text (ciphertext). public byte[] EncryptAES(string plainText, byte[] Key, byte[] IV) { RijndaelManaged _aesObject = null; MemoryStream _encStream = null; // Create the Rijndael object _aesObject = new RijndaelManaged(); _aesObject.Key = Key; _aesObject.IV = IV; // Create an encryptor object ICryptoTransform encryptor = _aesObject.CreateEncryptor(_aesObject.Key, _aesObject.IV); // Create the stream objects used for the encryption _encStream = new MemoryStream(); using (CryptoStream csEncrypt = new CryptoStream(_encStream, encryptor, CryptoStreamMode.Write)) { using (StreamWriter swEncrypt = new StreamWriter(csEncrypt)) { // Write data to the stream swEncrypt.Write(plainText); } } [ 320 ]
Chapter 8 _aesObject.Clear(); return _encStream.ToArray(); }
The DecryptAES function is roughly similar. You will need to pass in the same key and IV (used during encryption) to decrypt the ciphertext successfully. This function will output the original plain text as a string object. public string DecryptAES(byte[] cipherText, byte[] Key, byte[] IV) { RijndaelManaged _AESObject = null; string _decText = null; // Create the Rijndael object _AESObject = new RijndaelManaged(); _AESObject.Key = Key; _AESObject.IV = IV; // Create the decryptor object ICryptoTransform decryptor = _AESObject.CreateDecryptor(_AESObject.Key, _AESObject.IV); // Setup the stream objects used for decryption using (MemoryStream msDecrypt = new MemoryStream(cipherText)) { using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) { using (StreamReader srDecrypt = new StreamReader(csDecrypt)) { // Read data from the stream _decText = srDecrypt.ReadToEnd(); } } } _AESObject.Clear(); return _decText; }
It's time to try out these routines. Place a button on the form, and in the click event of this button, write the following code: private void button1_Click(object sender, EventArgs e) { string plainText = "HELLO WORLD!"; // You can get a new IV and key simply by instantiating a // new RijndaelManaged class RijndaelManaged myRijndael = new RijndaelManaged(); [ 321 ]
Securing the Application // Run the encryption routine byte[] _encryptedData = EncryptAES(plainText, myRijndael.Key, myRijndael.IV); // Run the decryption routine string _decryptedData = DecryptAES(_encryptedData, myRijndael.Key, myRijndael.IV); MessageBox.Show("Decrypted text:" + _decryptedData); }
If you run the form and click the button, you should be able to see your original text after it has been run through the encryption and decryption process:
You can now apply these same routines to your sales force project by encrypting the stream of bytes obtained right after serializing a dataset (before it gets sent out via Bluetooth or Infrared). You must also correspondingly decrypt the stream of bytes immediately once it is retrieved on the device, and then finally de-serialize it back into a dataset.
Summary
In this chapter, you've taken a look at some of the simple approaches you can take to implement security. You've covered how to do the following: •
Activate encryption for the SQL Server CE database
•
Activate encryption for the Oracle Lite database
•
Secure data during transfer between devices using the Rijndael (AES) algorithm.
•
Add authentication to your application for both the Oracle Lite and SQL Server CE databases
In the next chapter, you will look at how you can globalize your application—to make the sales force application double-byte ready and able to support Japanese characters.
[ 322 ]
Globalization You've come a long way, and you now have a fully functional mobile sales force application. The last step is to make this application available to an international audience. Your application needs to display dates, currencies, and numbers in the right format when used in different countries or locales. For example, dates are displayed in Japan year first, followed by the month, and then the day. In the U.S. (and most parts of the world), the decimal point is a dot (.), whereas in France, it is a comma (,). Therefore, $1,500 may mean entirely different things in these two countries. The need to present locale-accurate data is even more important when you deal with financial information. Globalizing your application is actually easier than it sounds. The .NET Compact Framework automatically handles most of the intrinsic formatting of dates and currencies based on the culture used in the regional settings of the device. In fact, you can globalize most of your application without even writing a single line of code at all! In this chapter, you will explore the following: •
How double-byte data can be stored in the database
•
How to design culture-sensitive forms within the Visual Studio IDE
•
How to programmatically retrieve device culture information
Supporting double-byte languages
The first thing you need to do to globalize your application would obviously be to support a particular locality's language. Let's take a look at how we can support the display and entry of foreign languages such as East Asian languages. Most East Asian languages are stored in double-byte format. This means that each Chinese or Japanese character takes up two bytes in the database. Let's take a look at how SQL Server CE and Oracle Lite handles double-byte languages.
Globalization
Supporting Japanese character input in Windows Mobile
To type East Asian characters (such as Chinese, Japanese, or Korean) on a mobile device, you need to have an Input Method Editor (IME) for each language. An IME allows you to type East Asian characters via various methods. For example, a Chinese IME would allow you to use Pinyin, or the phonetic value (in Romanized form), of a character as input. Another method would be to input the strokes of the character. The following screenshot shows an example of a Japanese IME:
Unfortunately for us, Microsoft does not ship or provide for any East Asian IME on the Windows Mobile platform. There is no other way but to download or buy a third-party IME. There are a few open-source and commercial IMEs online that you can explore, such as CEStar (http://www.mobem.com/products/CE-Star.php), ookii.org, and Bagoj's Japanese IME. We will not go into the details of their setup as it would be out of the scope of this book. It is recommended for you to set up an IME on your emulator or device before proceeding with the examples in this chapter.
Supporting Unicode at the application level
You will notice that unlike the desktop-based SQL Server or Oracle versions, which makes distinctions between the VARCHAR and NVARCHAR data types, SQL Server CE and Oracle Lite do not make any such distinction. Both the SQL Server CE and Oracle Lite databases readily support double-byte data.
[ 324 ]
Chapter 9
All you need to remember is that when you pass parameters to the DataAdapter or DataReader objects, you need to declare the VARCHAR parameters using the SqlDbType.NVarChar data type and TEXT parameters using the SqlDbType.NText data types (highlighted as follows). _adapter.UpdateCommand = new SqlCeCommand("UPDATE AccountTasks SET TaskSubject=@TaskSubject, TaskDescription=@TaskDescription, TaskDate=@TaskDate, TaskStatus=@TaskStatus WHERE TaskID=@TaskID", _globalConnection, _transaction); _adapter.UpdateCommand.Parameters.Add("@TaskSubject", SqlDbType.NVarChar, 255, "TaskSubject"); _adapter.UpdateCommand.Parameters.Add("@TaskDescription", SqlDbType.NText, 16, "TaskDescription"); _adapter.UpdateCommand.Parameters.Add("@TaskDate", SqlDbType.DateTime, 8, "TaskDate"); _adapter.UpdateCommand.Parameters.Add("@TaskStatus", SqlDbType.Int, 4, "TaskStatus"); _adapter.UpdateCommand.Parameters.Add("@TaskID", SqlDbType.Int, 4, "TaskID"); _adapter.DeleteCommand = new SqlCeCommand("DELETE FROM AccountTasks WHERE TaskID=@TaskID", _globalConnection, _transaction); _adapter.DeleteCommand.Parameters.Add("@TaskID", SqlDbType.Int, 4, "TaskID"); _adapter.InsertCommand = new SqlCeCommand("INSERT INTO AccountTasks(AccountGUID, TaskSubject, TaskDescription, TaskCreated, TaskDate, TaskStatus) " + "VALUES (@AccountGUID, @TaskSubject, @TaskDescription, GETDATE(), @TaskDate, @TaskStatus)", _globalConnection, _transaction); _adapter.InsertCommand.Parameters.Add("@AccountGUID", AccountID); _adapter.InsertCommand.Parameters.Add("@TaskSubject", SqlDbType.NVarChar, 255, "TaskSubject"); _adapter.InsertCommand.Parameters.Add("@TaskDescription", SqlDbType.NText, 16, "TaskDescription"); _adapter.InsertCommand.Parameters.Add("@TaskDate", SqlDbType.DateTime, 8, "TaskDate"); _adapter.InsertCommand.Parameters.Add("@TaskStatus", SqlDbType.Int, 4, "TaskStatus"); _adapter.Update(Account.Tables["AccountTasks"]);
[ 325 ]
Globalization
If you try keying in East Asian characters as the first name or last name of a new account, you will find that you can successfully save East Asian characters to the database for storage and, likewise, retrieve them correctly, as shown in the following screenshot:
The great thing about working with mobile databases is that they're double-byte ready, and this helps remove a great deal of programming otherwise needed to support Unicode formats.
Designing culture-sensitive forms
Up to this point, you've successfully built the English version of the sales force application so far—the menu bars, logos, captions, and messages are all displayed in English. Your next task is to change your application so that these items are displayed in Japanese instead. Instinctively, you might think of storing each caption, logo, and message as strings in a resource file, with a separate resource file for each different language supported in your application. This is the correct approach, and Microsoft has even automated the creation of these resource files within their Integrated Development Environment (IDE). Each form in your application comes with a property called Localizable. You must first set this property to true. This indicates that the form can support languages other than the system default. Let's take the Login.cs form as an example. As you need to design a Japanese version of the form, you will need to set its Localizable property to true. Next, change the current language of the form by setting the Language property to Japanese(Japan). [ 326 ]
Chapter 9
Once you have done this, Visual Studio will generate a new resource file under the Login form. The name of this resource file indicates ja-JP (short for Japanese (Japan)). You can now make changes directly on your form. Change all the captions to display Japanese. (You can either use an IME to type Japanese characters, or you could simply grab some dummy Japanese text off the Internet). You can even change the entire logo on the login page to a different one. When you make these changes, you're probably wondering if you are overwriting the English version of your form. Don't worry, because it's still there. The Language property of the form allows you to switch back and forth between different language versions of the same form. To switch back to the English version of the form, you can just set the Language property back to Default. And to switch back to the Japanese version of the form, set the Language property back to Japanese(Japan). The following screenshot shows the extra resource file generated in Visual Studio when you set the Language and Localizable properties.
[ 327 ]
Globalization
Similarly, you can create as many language versions of the form as you wish. Each new language you set will generate a new resource file. For example, the following screenshot shows that there are English (default), German, French, and Japanese versions of the Login form.
If you were to open any of the resource files in Visual Studio, you can see that Visual Studio has neatly organized the culture-specific captions, images, and messages in the resource file. These files are maintained by Visual Studio—you would almost never need to directly edit these files by hand.
[ 328 ]
Chapter 9
Now that you've managed to create different language versions of the same form, how does the .NET Compact Framework know which version to display? The .NET Compact Framework automatically decides for you—it looks at the regional settings of the mobile device. For instance, if you were to change the regional settings of the device to the Japanese (Japan) culture, this would invoke the Japanese (Japan) version of the Login form you have created.
If you run the sales force application with the regional settings set to Japanese (Japan), you will see the following login page:
[ 329 ]
Globalization
Now try changing your regional settings to English (US), you will find that the English (default) version of the Login form will be displayed instead. If you change the regional settings of the device to a language that you have not accounted for in your application, the default language version of your forms will be used. Changing the device culture programmatically? Take note that the .NET Compact Framework does not provide you with any facility to set the culture of a device programmatically. You can only change the device culture through the regional settings of the device.
Retrieving culture information
The .NET Compact Framework platform intrinsically handles all date, time, numbers, and string formatting whenever the mobile user changes the current culture of the device. However, there might be cases when your code needs to know the current culture of the device. For example, currency conversion is something the platform does not do for you automatically. You might wish to find out what the default currency of the mobile device is set to and use that to automatically calculate and display its equivalent value in US dollars. The following code snippet allows you to retrieve the current culture of the mobile device. Using the CultureInfo object, you can query a rich set of information about the language, date/time format, numerical format, and currency of the mobile device. using System.Globalization; . . . CultureInfo _culture = CultureInfo.CurrentCulture; string _summary; _summary = "Culture: " + _culture.EnglishName + "\r\n"; _summary += "Date: " + _culture.DateTimeFormat.LongDatePattern + "\r\n"; _summary += "Lang: " + _culture.ThreeLetterISOLanguageName + "\r\n"; _summary += "Currency: " + _culture.NumberFormat.CurrencySymbol + "\r\n"; _summary += "Decimal Separator: " + _culture.NumberFormat.NumberDecimalSeparator + "\r\n"; _summary += "Group separator: " + _culture.NumberFormat.NumberGroupSeparator + "\r\n"; _summary += "Negative symbol: " + _culture.NumberFormat.NegativeSign + "\r\n"; MessageBox.Show(_summary);
[ 330 ]
Chapter 9
If you run the previous code snippet, you can see a pop-up message box similar to the one shown as follows:
Summary
After nine chapters, your sales force application is finally complete. In this chapter, you've taken a look at how you can support double-byte languages such as Japanese in your application. You've covered the specifics of how to do the following: •
Support the storage of double-byte data in SQL Server CE and Oracle Lite
•
Use the features of the Visual Studio IDE to build culture-sensitive forms
•
Programmatically retrieve culture-specific information from the device
In the next chapter, we will depart from the sales force application and move to build an entirely new type of application—the dashboard.
[ 331 ]
Building the Dashboard In this chapter, we take a look at another common type of .NET Compact Framework application—the smart client. Smart clients have the advantage of giving the developer full control over the user interface and at the same time not having to concern the developer with local data storage. This is one application where you don't have to do any database development on the mobile device! Smart clients are frequently deployed nowadays in applications that takes their data feed off the Internet, but run complex data manipulation locally on the device. Mobile stock-market analysis software is one such example. The latest data is always retrieved from a remote server, but complex processing is still done locally on the mobile device to present the data in different charts and diagrams. Throughout this chapter, you will learn how to create three different types of charts using .NET Compact Framework's GDI+ graphics library employing the smart client approach. You will learn how to do the following: •
Consume remote web services asynchronously from within your .NET Compact Framework application
•
Use basic GDI+ functions in the .NET Compact Framework
•
Use the gauge control in the Smart Device Framework
An overview of the dashboard
Compared to the sale force application, the dashboard application is a much lighter application. It is a single-form application that displays three charts representing sales data in different views: •
Daily sales (as a line chart)
•
Total sales for the month (as a round gauge)
•
Sales for the last three months (as a bar chart)
Building the Dashboard
You will be using the GDI+ capabilities of the .NET Compact Framework to render the line and bar charts, and the Smart Device Framework to render the round gauge. The following screenshot shows how the three charts are laid out in the main application form:
The data for these charts is stored on a remote server, and will be retrieved (in XML format) asynchronously via web services. You will also be able to resize each panel using the splitter bars and have the charts resized automatically.
Creating the web service
The web service that supplies the data for the dashboard is hosted on the Internet Information Server (IIS) on a remote server. This means that it will be developed as an ASP.NET Web Service Application project. As ASP.NET web services are not the focus of this book, you will not need to retrieve the data from an actual database. Rather, you can write some code to return hardcoded dummy data. Create a new ASP.NET Web Service Application project. Rename the main service from Service1.asmx to DataService.asmx. Take note that you might also need to reflect this new name in the Class attribute of the web service declaration in the DataService.asmx mark-up file. You can view the mark-up file by right-clicking the DataService.asmx file and choosing View Markup from the pop-up menu.
[ 334 ]
Chapter 10
The DataService.asmx file will only contain one web method, named GetChartData, which takes in two arguments—the year and month of the requested sales data. This method will return the data for all three charts in a single XML block. The code to generate this XML follows: [WebMethod()]public string GetChartData(int Year, int Month) { XmlDocument _xml = new XmlDocument(); XmlElement _xmlRoot = _xml.CreateElement("ChartData"); XmlElement _xmlDay; XmlElement _xmlRoadShowRoot; XmlElement _xmlTotalSalesRoot; _xml.AppendChild(_xmlRoot); VBMath.Randomize(); //====================================================== //Generate road show sales data. //You will use the Rnd() function to generate random //sales figures for each day of the month. //====================================================== _xmlRoadShowRoot = _xml.CreateElement("RoadShowSales"); _xmlRoot.AppendChild(_xmlRoadShowRoot); for (var _counter = 1; _counter <= DateTime.DaysInMonth(Year, Month); _counter++) { _xmlDay = _xml.CreateElement("Day" + _counter); _xmlDay.SetAttribute("Value", System.Convert.ToString(Math.Round(VBMath.Rnd() * 50000))); _xmlRoadShowRoot.AppendChild(_xmlDay); } //====================================================== //Generate total sales for the month //There are two values that need to be returned: //a) Actual sales achieved for the month //b) The sales target for the month //====================================================== _xmlTotalSalesRoot = _xml.CreateElement("TotalSales"); _xmlRoot.AppendChild(_xmlTotalSalesRoot); XmlElement _xmlSalesAmount = _xml.CreateElement("TotalSalesAmount"); _xmlSalesAmount.SetAttribute("Value", "800000"); _xmlSalesAmount.SetAttribute("MonthlySalesTarget", "1000000");
[ 335 ]
Building the Dashboard _xmlTotalSalesRoot.AppendChild(_xmlSalesAmount); //====================================================== //Generate total sales for the last three months //====================================================== XmlElement _xmlLastThreeMonthSalesRoot; _xmlLastThreeMonthSalesRoot = _xml.CreateElement("LastThreeMonthSales"); _xmlRoot.AppendChild(_xmlLastThreeMonthSalesRoot); XmlElement _xmlMonth1 = _xml.CreateElement("Month1"); _xmlMonth1.SetAttribute("Value", "1840000"); _xmlLastThreeMonthSalesRoot.AppendChild(_xmlMonth1); XmlElement _xmlMonth2 = _xml.CreateElement("Month2"); _xmlMonth2.SetAttribute("Value", "1200000"); _xmlLastThreeMonthSalesRoot.AppendChild(_xmlMonth2); XmlElement _xmlMonth3 = _xml.CreateElement("Month3"); _xmlMonth3.SetAttribute("Value", "800000"); _xmlLastThreeMonthSalesRoot.AppendChild(_xmlMonth3); //Write the XML block into a string StringWriter _stringWriter = new StringWriter(); XmlTextWriter _xmlWriter = new XmlTextWriter(_stringWriter); _xml.WriteTo(_xmlWriter); return _stringWriter.ToString(); }
After creating this web service, you will need to host it in IIS so that it can be consumed remotely. Add a new application under the Default Web Site via IIS Manager.
[ 336 ]
Chapter 10
To test if your web service was set up successfully, open your browser, and navigate to the following address: http://localhost/DashboardService/DataService.asmx
You should be able to see the following page. Notice that your web method is displayed on this page. You can even try invoking this web method by clicking on the GetChartData link, filling in the year and month values and pressing the Invoke button (in the page that follows).
The XML data that is retrieved from this web method will have the following structure:
[ 337 ]
Building the Dashboard
Creating the dashboard smart client
The next thing you need to do is create a new .NET 3.5 Framework Smart Device project. Name this project SalesForceDashboard. Use the Device Application template and set the target platform to Windows Mobile 6 Professional SDK. You will now need to create the main dashboard form. Add a new form to your application and name it Dashboard.cs. Using the Windows Forms Splitter and Panel controls, divide the form into three panels (as shown in the following screenshot).
When this form loads up, you will call the remote web service you've created earlier to retrieve the appropriate chart data. Once the data has been retrieved, you will proceed to render the various charts in their respective frame panels. The Refresh button allows you to refresh the displayed data by making another call to the remote web service to retrieve the latest data. The Close button simply quits the whole application via the following code: Application.Exit();
Connecting to the web service
To connect to a web service, you must first add a web reference to your project. You've done this earlier in Chapter 6 when you had to connect to a WCF service to synchronize data with a remote server. Right-click on your project, and click on the Add Web Reference menu item. [ 338 ]
Chapter 10
Browse to the location of your web service's .asmx file and click the Go button to load it. Once the web service is found, name the web reference DashboardDataservice and click the Add Reference button to add this reference to your project.
Now that you have a reference to your web service, let's see how you can call this web service asynchronously from your application. An asynchronous web service call is a non-blocking call that returns immediately after you execute the request. You specify the address of a call-back function that is automatically invoked when the request completes. Asynchronous web services are commonly used to improve UI responsiveness—the user is free to manipulate the UI while it waits to load data from a web service.
[ 339 ]
Building the Dashboard
In the code behind the Dashboard form, declare and instantiate a new instance of this web reference: private DashboardDataService.DataService _service = new DashboardDataService.DataService();
In the load event of the form, call the RefreshAllCharts method: public void Dashboard_Load(object sender, System.EventArgs e) { RefreshAllCharts(); }
The BeginGetChartData method will initiate the asynchronous web service call for the GetChartData web method. You will need to pass in the two arguments for this web method, as well as the address of the call back function (as an AsyncCallback object). You can pass in null for the last parameter of this function. private void RefreshAllCharts() { _service.BeginGetChartData(DateTime.Now.Year, DateTime.Now.Month, new AsyncCallback(BeginGetChartDataCallback), null); }
Asynchronous and synchronous web service calls An asynchronous web service method call has a Begin prefix. To make synchronous (blocking) web service calls, simply use the original web method name itself. Example :_service. GetChartData (DateTime.Now.Year, DateTime.Now.Month)
After the GetChartData web method has completed, your callback function will be invoked. You can declare the callback function shown as follows. You must call the EndGetChartData method to retrieve the return value from the web method. private void BeginGetChartDataCallback(IAsyncResult Result) { String _data; _data = _service.EndGetChartData(Result); MessageBox.Show("Data retrieved : " + _data); }
[ 340 ]
Chapter 10
With these few lines of code, you can try connecting to the web service now by running your smart client! Build and deploy this project to the Windows Mobile 6 emulator. Ensuring your emulator environment is set up correctly You must first ensure that your emulator is able to connect to the IIS server in your development environment. For this to happen, an ActiveSync session must first be set up between your development machine and the emulator. Please refer to the Connecting the Windows Mobile emulator to ActiveSync section in Chapter 2 for more details on this. You can test if the emulator can reach the IIS server by navigating to your web service from IE Mobile.
After your application loads, you should be able to see the data retrieved from the web service shown in your emulator.
Creating the line chart
You're now ready to create your first chart. The first chart is a line chart, which shows the amount of sales for each individual day of the specified month. You can encapsulate the functionality of the line chart in a usercontrol.
[ 341 ]
Building the Dashboard
Add a new usercontrol named LineChart to your project, and set its background color to pure white. This line chart control will need the data you've retrieved from the web service. You can simply pass in the entire XML node to this usercontrol, via the Datasource property. private XmlElement _dataSource = null; public XmlElement Datasource { get { return _dataSource; } set { _dataSource = value; if (_dataSource != null) {AnalyzeData();} this.Refresh(); } }
When XML data is passed in via this property, you can call the AnalyzeData() function, which iterates through the XML nodes and retrieves all the data into an array (highlighted as follows). //The highest value to be displayed in the chart private int _HighestValue; //The lower range of the dayscale to display in the chart private int _DayStart; //The upper range of the dayscale to display in the chart private int _DayEnd; //An array containing values for each day in the dayscale private long[] _DataValues; //Extract data from the XML object into an array private void AnalyzeData() { int _counter; XmlElement _xmlDay; int _value; _HighestValue = 0; _DayStart = 1; [ 342 ]
Chapter 10 _DayEnd = _dataSource.ChildNodes.Count; Array.Resize(ref _DataValues, _DayEnd + 1); for (_counter = 0; _counter <= _dataSource.ChildNodes.Count - 1; _counter++) { _xmlDay = (System.Xml.XmlElement) (_dataSource.ChildNodes.Item(_counter)); _value = int.Parse(_xmlDay.GetAttribute("Value")); _DataValues[_counter] = _value; _HighestValue = Math.Max(_value, _HighestValue); } }
The next thing you need to do is to use the GDI+ capability of the .NET Compact Framework to draw the line chart. Add a reference to the System.Drawing library to your project. Make sure you also import this library in your usercontrol. using System.Drawing;
You can implement your own paint routines by overriding the OnPaint event of the usercontrol. Every time the usercontrol display is changed, for example, during the first time it is loaded, or during a resize, or when another window is moved over it, the OnPaint event is fired. public void LineChart_Paint(object sender, System.Windows.Forms.PaintEventArgs e) { LoadLineChart(e.Graphics); } public void LineChart_Resize(object sender, System.EventArgs e) { this.Refresh(); }
Before you create the LoadLineChart method, you will first need to declare a few variables that will be used throughout the line chart control. //Horizontal margin of the chart private int _marginX = 20; //Vertical margin of the chart private int _marginY = 20; //Vertical spacing between each gridline
[ 343 ]
Building the Dashboard private int _gridlineSpacing = 9; //Vertical spacing between each label on the Y Axis private int _YAxisLabelSpacing = 18; //Number of horizontal pixels to render for each day in the dayscale private int _XPixelsPerDay; //Bottom coordinate of the chart area private int _chartBottom; //Top coordinate of the chart area private int _chartTop; //Left coordinate of the chart area private int _chartLeft; //Right coordinate of the chart area private int _chartRight; //Height of the chart area private int _chartHeight; //Width of the chart area private int _chartWidth; //The value that each pixel in the chart area represents vertically private double _ValuePerChartPixel; //The base of the number (If in thousands, BaseOfValue = 1000) private int _BaseOfValue = 1000; //Some pens and brushes that will be used for the line chart private Pen _blackPen = new Pen(Color.Black); private Pen _bluePen = new Pen(Color.Blue); private Pen _grayPen = new Pen(Color.LightGray); private SolidBrush _blackBrush = new SolidBrush(Color.Black); //Fonts private private private
that Font Font Font
will be used for the line chart _stdFont = new Font("Tahoma", 8, FontStyle.Regular); _boldFont = new Font("Tahoma", 8, FontStyle.Bold); _smallFont = new Font("Tahoma", 6, FontStyle.Regular);
[ 344 ]
Chapter 10
The Pen object is used to draw lines and polygons (for instance rectangles) while the Brush object is used to fill these rectangles (and other enclosed patterns) with colors or patterns.
Before we proceed, it is useful to take note of the following GDI+ methods: GDI+ Method name DrawString (String s, Font f, Brush brush, int X, int Y)
Description
DrawLine (Pen pen, int X1, int Y1, int X2, int Y2)
This method draws a line using the specified pen from (X1,Y1) to (X2,Y2)
FillRectangle (Brush brush, int X, int Y, int Width, int Height)
This method draws a filled rectangle (using the specified brush) at (X,Y) with the specified width and height
This method writes the specified text (using the specified font and brush) to a location in the usercontrol specified by X and Y
Now let's take a look at the main bulk of the line chart functionality in LoadLineChart(). private void LoadLineChart(Graphics Graphics) { int _counter; //Calculate chart dimensions _chartTop = _marginY; _chartBottom = this.Height - _marginY; _chartLeft = _marginX; _chartRight = this.Width - _marginX; _chartHeight = _chartBottom - _chartTop; _chartWidth = _chartRight - _chartLeft; _ValuePerChartPixel = (_HighestValue)/ _chartHeight; //If no datasource was loaded, show a message to //indicate that data is currently loading if (_dataSource == null) { Graphics.DrawString("Loading data...", _stdFont, _blackBrush, 15, 15); return; [ 345 ]
Building the Dashboard } //Draw Chart Title Graphics.DrawString("Revenue ($K) from NY Roadshow", _boldFont, _blackBrush, 2, 2); //Draw Y axis Graphics.DrawLine(_blackPen, _chartLeft, _chartTop, _chartLeft, _chartBottom); //Draw X axis Graphics.DrawLine(_blackPen, _chartLeft, _chartBottom, _chartRight, _chartBottom); //Draw gray horizontal gridlines _counter = _gridlineSpacing; while (_counter < _chartHeight) { Graphics.DrawLine(_grayPen, _chartLeft + 1, _chartBottom - _counter, _chartRight, _chartBottom - _counter); _counter = _counter + _gridlineSpacing; } //Draw Y axis labels _counter = 0; double _LabelValue; while (_counter < _chartHeight) //Draw the label hair Graphics.DrawLine(_blackPen, _chartLeft - 3, _chartBottom - _counter, _chartLeft - 1, _chartBottom - _counter); //Get the value at the label, and convert to the correct base _LabelValue = _ValuePerChartPixel * _counter; _LabelValue = _LabelValue / _BaseOfValue; //Draw the label Graphics.DrawString(String.Format("{0:#0.00}", _LabelValue ), _smallFont, _blackBrush, 0, VAlignMiddle(_chartBottom - _counter)); _counter = _counter + _YAxisLabelSpacing; }
[ 346 ]
Chapter 10 //Draw X axis labels _XPixelsPerDay = (int) ((_chartWidth)/ _DayEnd - _DayStart + 1); int _x = _chartLeft; int _fromIndex = 0; int _toIndex = 1; int _YPos1; int _YPos2; for (_counter = _DayStart; _counter <= _DayEnd; _counter++) { if (_counter % 2 == 0) { //We display a shorter hair line for even-numbered days Graphics.DrawLine(_blackPen, _x, _chartBottom + 1, _x, _chartBottom + 3); } else { //We display a slightly longer hair line for odd-numbered days Graphics.DrawLine(_blackPen, _x, _chartBottom + 1, _x, _chartBottom + 6); //We display labels only for odd-numbered days Graphics.DrawString(_counter.ToString(), _smallFont, _blackBrush, _x - 2, _chartBottom + 6); } //Draw the lines representing the data if (_counter < _DayEnd) { _YPos1 = ConvertValueToYCoordinate((int) (_DataValues[_fromIndex])); _YPos2 = ConvertValueToYCoordinate((int) (_DataValues[_toIndex])); Graphics.DrawLine(_bluePen, _x, _YPos1, _x + _XPixelsPerDay, _YPos2); _fromIndex++; _toIndex++; } _x += _XPixelsPerDay; } }
[ 347 ]
Building the Dashboard //This method converts a sales amount into a Y coordinate on //the line chart private int ConvertValueToYCoordinate(int Value) { return _chartBottom - (int) ((Value)/ _ValuePerChartPixel); } //This method aligns the Y-axis labels vertically by moving //them up 5 pixels private int VAlignMiddle(int Y) { return Y - 5; }
That's it for the usercontrol. Back in the dashboard.cs form, add a LoadDashboard() method to the load event. This method will instantiate a new instance of your line chart usercontrol and will place it in the top-most panel of your form. //Declare an instance of the line chart usercontrol private LineChart _lineChartControl; public void Dashboard_Load(object sender, System.EventArgs e) { LoadDashboard(); RefreshAllCharts(); } private void LoadDashboard() { _lineChartControl = new LineChart(); _lineChartControl.Dock = DockStyle.Fill; _lineChartControl.Visible = true; pnlLineChart.Controls.Add(_lineChartControl); }
[ 348 ]
Chapter 10
In the BeginGetChartDataCallback() method, you will need to write additional code to parse the XML data retrieved from the web service, look for the node representing the road show sales figures and then pass it to the line chart usercontrol. The changes that you need to make are highlighted as follows: private delegate void PopulateLineChartControlDelegate(XmlElement Data); private void BeginGetChartDataCallback(IAsyncResult Result) { string _data; XmlDocument _xml; XmlElement _xmlRoot; XmlElement _xmlRoadShowSales; XmlElement _xmlTotalSales; XmlElement _xmlLastThreeMonthSales; _data = _service.EndGetChartData(Result); _xml = new XmlDocument(); _xml.LoadXml(_data); _xmlRoot = (System.Xml.XmlElement) (_xml.FirstChild); //Look for the road show sales node and load it into the //line chart control _xmlRoadShowSales = (System.Xml.XmlElement) (_xmlRoot.GetElementsByTagName("RoadShowSales")[0]); PopulateLineChartControlDelegate _linechartDelegate = new PopulateLineChartControlDelegate (PopulateLineChartControl); this.BeginInvoke(_linechartDelegate, _xmlRoadShowSales); } private void PopulateLineChartControl(XmlElement Data) { _lineChartControl.Datasource = Data; }
[ 349 ]
Building the Dashboard
Why use BeginInvoke You might notice from the code above that we use BeginInvoke() to indirectly pass the XMLElement object to the line chart control. This is necessary because the callback runs on a different thread than the one used to create the usercontrol. The .NET Compact Framework will raise an exception if you try to access the control directly.
Now try running your application to see your line chart spring to life. Notice that you can see the Loading data… message before the chart appears. During this time, the application has asynchronously invoked the remote web service to retrieve the data required for the chart.
Creating the round gauge
The Smart Device Framework (OpenNETCF), as you've encountered earlier in Chapter 5 of this book, provides an out-of-the-box round gauge control that you can readily use in your application. We will now explore how you can make use of this control to display the total monthly sales achieved against the expected sales target. You first used the Smart Device Framework in Chapter 5. If you haven't installed it yet, you can do so now by downloading the Smart Device Framework from the following URL (via the Download the Community Edition (free) link). http://opennetcf.com/CompactFramework/Products/ SmartDeviceFramework/tabid/65/Default.aspx
[ 350 ]
Chapter 10
First, add a reference to the OpenNETCF.Windows.Forms.dll library. You might need to browse for this DLL, which is located in the following folder: \Program Files\Smart Device Framework\Bin
After this, create a new usercontrol named GaugeChart. This usercontrol will encapsulate the functionality of the Smart Device Framework's RoundGauge control. You will also load data into your usercontrol by way of a Datasource property, shown as follows: private XmlElement _dataSource = null; public XmlElement Datasource { get { return _dataSource; } set { _dataSource = value; LoadGauge(); } }
[ 351 ]
Building the Dashboard
The main properties you need to work with on the Smart Device Framework's RoundGauge control are listed as follows: RoundGauge property name MinValue
Description
MaxValue
This property defines the maximum value shown in the gauge control
Value
This property defines the value that the needle points to in the gauge control.
This property defines the minimum value shown in the gauge control.
Now let's declare some variables that will be used throughout your usercontrol. //An instance of the Smart Device Framework's Round Gauge control private RoundGauge _gaugeControl = null; //The achieved sales amount for the month private long _totalSalesAmount; //The expected sales target amount for the month private long _MonthlySalesTarget; //The minimum % of sales of which an amount below this value //is considered critically low private int _MinimumSalesPercentage = 20; //The base number for all values on the chart (eg: 1000 means //in thousands) private int _BaseOfValue = 10000; //Fonts private private private private
and brushes used throughout the GaugeChart usercontrol SolidBrush _blackBrush = new SolidBrush(Color.Black); Font _smallFont = new Font("Tahoma", 6, FontStyle.Regular); Font _stdFont = new Font("Tahoma", 8, FontStyle.Regular); Font _boldFont = new Font("Tahoma", 8, FontStyle.Bold);
[ 352 ]
Chapter 10
You can now write the LoadGauge() method, which initializes the gauge control and sets the display values on the gauge. private void LoadGauge() { if (_dataSource != null) { //Extracts the data from the XML node XmlElement _xmlSalesAmount = (System.Xml.XmlElement) (_dataSource.FirstChild); _totalSalesAmount = int.Parse(_xmlSalesAmount.GetAttribute("Value")); _MonthlySalesTarget = int.Parse(_xmlSalesAmount.GetAttribute ("MonthlySalesTarget")); if (_gaugeControl == null) { //Create the gauge control _gaugeControl = new RoundGauge(); _gaugeControl.Name = "TotalSalesGauge"; _gaugeControl.Visible = true; _gaugeControl.Font = _smallFont; _gaugeControl.Dock = DockStyle.None; //Leave some space at the top for the chart title _gaugeControl.Location = new Point(0, 20); _gaugeControl.Size = new Size(this.Width, this.Height - 20); _gaugeControl.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; _gaugeControl.ShowTickLabels = true; //Set the gauge values _gaugeControl.MinValue = 0; _gaugeControl.MaxValue = ConvertToBaseValue((int) _MonthlySalesTarget); _gaugeControl.Value = ConvertToBaseValue((int) _totalSalesAmount); //Set other gauge settings _gaugeControl.ValueDigits = - 1; _gaugeControl.LineBezelSpacing = 2;
[ 353 ]
Building the Dashboard _gaugeControl.TickLabelPadding = 2; //Sets the warning/critical zone for the gauge _gaugeControl.LowWarnValue = ConvertToBaseValue((int) ((_MinimumSalesPercentage * _MonthlySalesTarget)/100)); _gaugeControl.LowWarnColor = Color.Red; _gaugeControl.HighWarnValue = ConvertToBaseValue((int) _MonthlySalesTarget); _gaugeControl.HighWarnColor = Color.Black; this.Controls.Add(_gaugeControl); //Creates the gauge chart title Label _gaugeTitle = new Label(); _gaugeTitle.Text = DateAndTime.MonthName(DateTime.Now.Month, true) + " Sales ($0,000)"; _gaugeTitle.Visible = true; _gaugeTitle.Font = _boldFont; _gaugeTitle.Location = new Point(2, 2); _gaugeTitle.Size = new Size(this.Width, 20); _gaugeTitle.BringToFront(); this.Controls.Add(_gaugeTitle); } } } //This method converts the sales amount into the correct //figure on the chart using the base number private int ConvertToBaseValue(int Amount) { return (int) ((Amount)/ _BaseOfValue); }
Now override the OnPaint() event so that you can display the Loading data… message when the gauge chart loads. public void GaugeChart_Paint(object sender, System.Windows.Forms.PaintEventArgs e) { if (_gaugeControl == null) { e.Graphics.DrawString("Loading data...", _stdFont, _blackBrush, 15, 15); } } [ 354 ]
Chapter 10
Back in dashboard.cs, you need to make the following (highlighted) changes to the LoadDashboard() method. private GaugeChart _gaugeChartControl; private void LoadDashboard() { _lineChartControl = new LineChart(); _lineChartControl.Dock = DockStyle.Fill; _lineChartControl.Visible = true; pnlLineChart.Controls.Add(_lineChartControl); int _GaugeSize = Math.Min(pnlGauge.Width, pnlGauge.Height); _gaugeChartControl = new GaugeChart(); _gaugeChartControl.Dock = DockStyle.None; _gaugeChartControl.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; _gaugeChartControl.Location = new Point(0, 0); _gaugeChartControl.Size = new Size(_GaugeSize, _GaugeSize); _gaugeChartControl.Visible = true; pnlGauge.Controls.Add(_gaugeChartControl); }
In the BeginGetChartDataCallback() method, you need to add the following additional code: private delegate void PopulateGaugeChartControlDelegate(XmlElement Data); private void BeginGetChartDataCallback(IAsyncResult Result) { . . . //Look for the total sales amount node and load it into the //gauge chart control _xmlTotalSales = (System.Xml.XmlElement) (_xmlRoot.GetElementsByTagName("TotalSales")[0]); PopulateGaugeChartControlDelegate _gaugechartDelegate = new PopulateGaugeChartControlDelegate (PopulateGaugeChartControl); this.BeginInvoke(_gaugechartDelegate, _xmlTotalSales); } private void PopulateGaugeChartControl(XmlElement Data) { _gaugeChartControl.Datasource = Data; }
[ 355 ]
Building the Dashboard
Run your application again to test the gauge chart in action. You will see a display similar to the following diagram. The red zone of the gauge chart indicates the "critical" area, and the label in the middle of the gauge displays the value the needle is pointing to.
Creating the bar chart
The bar chart is yet another example of a GDI+ chart. This chart displays the amount of sales for the last three months, and is a good demonstration of the GDI+ FillRectangle method. Add a new usercontrol named BarChart to your project, and set its background color to pure white. You will again pass in XML data via the Datasource property. private XmlElement _dataSource = null; public XmlElement Datasource { get { return _dataSource; } set { _dataSource = value; if (_dataSource != null) {AnalyzeData();} this.Refresh(); } } private void AnalyzeData() { int _counter; XmlElement _xmlDay; int _value;
[ 356 ]
Chapter 10 //Extracts the values from the XML element into an array _HighestValue = 0; for (_counter = 0; _counter <= _dataSource.ChildNodes.Count - 1; _counter++) { _xmlDay = (System.Xml.XmlElement) (_dataSource.ChildNodes.Item(_counter)); _value = int.Parse(_xmlDay.GetAttribute("Value")); _DataValues[_counter] = _value; _HighestValue = Math.Max(_value, _HighestValue); } }
You also need to declare the following variables that will be used throughout the bar chart. //The highest value to be displayed in the chart private int _HighestValue; //An array containing values for each month in the monthscale private long[] _DataValues = new long[4]; //Horizontal margin of the chart private int _marginX = 20; //Vertical margin of the chart private int _marginY = 20; //Vertical spacing between each gridline private int _gridlineSpacing = 9; //Vertical spacing between each label on the Y Axis private int _YAxisLabelSpacing = 18; //Number of horizontal pixels to render for each day in the dayscale private int _XPixelsPerDay; //Bottom coordinate of the chart area private int _chartBottom; //Top coordinate of the chart area private int _chartTop;
[ 357 ]
Building the Dashboard //Left coordinate of the chart area private int _chartLeft; //Right coordinate of the chart area private int _chartRight; //Height of the chart area private int _chartHeight; //Width of the chart area private int _chartWidth; //The value that each pixel in the chart area represents vertically private double _ValuePerChartPixel; //The base of the number private int _BaseOfValue = 1000000; //Pens and brushes used throughout the bar chart private Pen _blackPen=new Pen(Color.Black); private SolidBrush _blackBrush= new SolidBrush(Color.Black); private SolidBrush _redBrush= new SolidBrush(Color.Red); private SolidBrush _blueBrush= new SolidBrush(Color.Blue); private SolidBrush _greenBrush= new SolidBrush(Color.Green); //Fonts private private private
used Font Font Font
throughout the bar chart _stdFont = new Font("Tahoma", 8, FontStyle.Regular); _smallFont = new Font("Tahoma", 6, FontStyle.Regular); _boldFont = new Font("Tahoma", 8, FontStyle.Bold);
You will need to override the OnPaint() event for this usercontrol. public void BarChart_Paint(object sender, System.Windows.Forms.PaintEventArgs e) { LoadBarChart(e.Graphics); } public void BarChart_Resize(object sender, System.EventArgs e) { this.Refresh(); }
[ 358 ]
Chapter 10
The LoadBarChart() method does the bulk of the drawing for the bar chart usercontrol. private void LoadBarChart(Graphics Graphics) { int _counter; //Calculate chart dimensions _chartTop = _marginY; _chartBottom = this.Height - _marginY; _chartLeft = _marginX; _chartRight = this.Width - _marginX; _chartHeight = _chartBottom - _chartTop; _chartWidth = _chartRight - _chartLeft; _ValuePerChartPixel = (_HighestValue)/ _chartHeight; //If no datasource was loaded, we display a message to //indicate that data is loading if (_dataSource == null) { Graphics.DrawString("Loading data...", _stdFont, _blackBrush, 15, 15); return; } //Draw Chart title Graphics.DrawString("Monthly sales ($M)", _boldFont, _blackBrush, 2, 2); //Draw Y axis Graphics.DrawLine(_blackPen, _chartLeft, _chartTop, _chartLeft, _chartBottom); //Draw X axis Graphics.DrawLine(_blackPen, _chartLeft, _chartBottom, _chartRight, _chartBottom); //Draw Y axis labels _counter = 0; double _LabelValue; while (_counter < _chartHeight) { //Draw the label hair Graphics.DrawLine(_blackPen, _chartLeft - 3,
[ 359 ]
Building the Dashboard _chartBottom - _counter, _chartLeft - 1, _chartBottom - _counter); //Get the value at the label, and convert them to the correct base _LabelValue = _ValuePerChartPixel * _counter; _LabelValue = _LabelValue / _BaseOfValue; //Draw the label Graphics.DrawString(Strings.Format(_LabelValue, "#0.0"), _smallFont, _blackBrush, 0, VAlignMiddle(_chartBottom - _counter)); _counter = _counter + _YAxisLabelSpacing; } //Draw X axis labels _XPixelsPerDay = (int) ((_chartWidth)/ 3); int _x = _chartLeft; int _YPos1; string _monthName; SolidBrush[] _bars = new SolidBrush[4]; //Initialize different colors for each bar _bars[0] = _blueBrush; _bars[1] = _greenBrush; _bars[2] = _redBrush; for (_counter = 0; _counter <= 2; _counter++) { //Draw the X-Axis hairs Graphics.DrawLine(_blackPen, _x, _chartBottom + 1, _x, _chartBottom + 6); Graphics.DrawLine(_blackPen, _x + _XPixelsPerDay, _chartBottom + 1, _x + _XPixelsPerDay, _chartBottom + 6); //Draw X-Axis labels _monthName = DateAndTime.MonthName(DateTime.Now.Month + _counter - 2, true); Graphics.DrawString(_monthName, _smallFont, _blackBrush, _x + 5, _chartBottom + 6); //Draw the data bars _YPos1 = _chartBottom - ConvertValueToPixels((int) (_DataValues[_counter]));
[ 360 ]
Chapter 10 Graphics.FillRectangle(_bars[_counter], _x, _YPos1, _XPixelsPerDay, ConvertValueToPixels((int) (_DataValues[_counter]))); _x += _XPixelsPerDay; } } //This method converts a sales amount into a Y coordinate on //the line chart private int ConvertValueToYCoordinate(int Value) { return _chartBottom - (int) ((Value)/ _ValuePerChartPixel); } //This method middle-aligns the Y-axis labels vertically private int VAlignMiddle(int Y) { return Y - 5; }
As with the other charts, you need to modify the LoadDashboard() method to load the bar chart usercontrol. private BarChart _barChartControl; private void LoadDashboard() { . . . _barChartControl = new BarChart(); _barChartControl.Dock = DockStyle.Fill; _barChartControl.Visible = true; pnlBarChart.Controls.Add(_barChartControl); }
As a last step, you will need to call the BeginInvoke() method to pass the XML node into the bar chart usercontrol. private delegate void PopulateBarChartControlDelegate(XmlElement Data); private void BeginGetChartDataCallback(IAsyncResult Result) { . . . [ 361 ]
Building the Dashboard //Look for the node of the last three month of sales and //load it into the bar chart control _xmlLastThreeMonthSales = (System.Xml.XmlElement) (_xmlRoot.GetElementsByTagName ("LastThreeMonthSales")[0]); PopulateBarChartControlDelegate _barchartDelegate = new PopulateBarChartControlDelegate (PopulateBarChartControl); this.BeginInvoke(_barchartDelegate, _xmlLastThreeMonthSales); } private void PopulateBarChartControl(XmlElement Data) { _barChartControl.Datasource = Data; }
If you try running your application again, you will be able to see the bar chart rendered in the following fashion:
Now, try something different—try resizing the chart areas by dragging the splitters; all three charts will resize automatically (shown as follows):
[ 362 ]
Chapter 10
You can additionally provide a manual refresh function to your end users so that clicking on the Refresh menu item will initiate a reconnection to the remote web service for the latest data. public void mnuRefresh_Click(System.Object sender,System.EventArgs e) { RefreshAllCharts(); }
Summary
In this chapter, you've seen how you were able to tap into the power of remote web services to provide data to your .NET Compact Framework application. You've also seen how you can use basic GDI+ function calls to draw basic shapes on the Windows Mobile platform. You've covered the following: •
Asynchronously calling remote web services using the BeginXXX and EndXXX methods
•
Using the DrawLine, DrawString, and FillRectangle GDI+ methods
•
Using the round gauge control in the Smart Device Framework
In the next chapter, you will create another type of mobile application—a message-driven application.
[ 363 ]
Building the Support Case System In the real world, it is difficult to have two devices (the server and mobile device) remain connected online throughout the use of an application. Mobile devices are meant to be, as the term suggests, mobile—end users will use the application you've developed underground in subways, in airplanes thousands of feet in the air, or during train rides miles away from civilization. As you've seen in the previous chapter, web services work great in disconnected scenarios. Your mobile application could work with local data, and only needed to go online when it had to receive or send updates to/from the server. In other words, you had to be online to make a web service call. But what if your application needs to still send updates to the server, even when you are not connected to your server? Consider the following example—as a technician on the move, you are given a mobile application that lists down, in real time, the available jobs that you can choose to accept. Let's say you are entering an area where the local Internet Wi-Fi connection is choppy at best. If you use web services, accepting a job would quickly become a frustrating affair—you find that you can't accept a job unless you're online. In such scenarios, some form of disconnected messaging is required. When you accept a job, and you are not connected to the server, your application should store this request as a message on the device temporarily. Whenever the device has the chance to get online, it should automatically send these messages on their way again to the server. Microsoft provides a technology called the Microsoft Messaging Queue Service (MSMQ) that does precisely this, all "under the hood," and it is completely transparent to your application.
Building the Support Case System
In this chapter, you will create a mobile support case system (where users can accept a job from a list of open jobs) that relies on MSMQ technology for all communications between the mobile device and server. You will learn: •
How to set up MSMQ on the mobile device and the server
•
How to use MSMQ to provide reliable disconnected messaging services—send data to a remote device even when it is offline
•
How to use MSMQ in a "broadcast" model—broadcasting data to multiple mobile devices
•
How to send entire business objects between devices through MSMQ
•
How to use the accompanying MSMQ tools on the mobile device and server to manage your messaging queues and their messages
Introduction to MSMQ and the support case system
The MSMQ service is a component provided free of charge by Microsoft, and is available to both the PC and mobile device platforms. It is essentially a Windows service—you can see it in your list of running services on your development PC.
[ 366 ]
Chapter 11
At the core of MSMQ lies the concept of messages, which are packets that hold information (typically represented in XML), and queues, the objects that temporarily hold messages before they are processed. A client application would typically send a message to a server, where it would end up in a queue on the server. The application on the server would inspect this queue periodically, and upon finding any messages inside, it would remove them from the queue for processing. You can create as many queues as you like, and each message can be designated for a specific queue (by name). In the support case application, for example, the mobile device and the server will have a queue each. This allows both mobile device and server to send and receive data. MSMQ supports the following protocols for data transmission: •
TCP
•
HTTP
•
HTTPS
This is depicted in the following diagram:
[ 367 ]
Building the Support Case System
The diagram shows what happens in an always connected environment. However, there are times when a message cannot be delivered due to the recipient device being offline. When this happens, MSMQ will internally create an Outgoing queue, and place the undelivered message in this queue. When the recipient device reconnects, the message is sent out and subsequently removed from the Outgoing queue. This facility happens "under the hood" and is transparent to your application. The following diagram depicts this functionality:
In the support case system that you are going to build in this chapter, you will use MSMQ to broadcast a job to all the devices every time the user adds a new job at the server side. The client application running on the mobile device will update its display of open jobs, and the user can then choose to accept a job from this list. The client application will then use MSMQ to transmit this command back to the server, where the job list display will also be updated. Let's start by taking a look at how you can get MSMQ set up on your mobile device.
[ 368 ]
Chapter 11
Setting up MSMQ on your mobile device
MSMQ is not installed by default on the Windows Mobile platform. This section will guide you on how to install MSMQ on your mobile device or device emulator. You will first need to download the Redistributable Server Components for Windows Mobile 5.0 package (which can also be used for Windows Mobile 6.0) from this location: http://www.microsoft.com/downloads/details.aspx?FamilyID=cdfd2bb2fa13-4062-b8d1-4406ccddb5fd&displaylang=en
After downloading and unzipping this file, you will have access to the MSMQ.arm.cab file in the following folder: \Optional Windows Mobile 5.0 Server Components\msmq
Copy this file via ActiveSync to your mobile device and run it on the device. This package contains two applications (and a bunch of other DLL components) that you will be using frequently on the device: •
msmqadm.exe:
This is the command line tool that allows you to start and stop the MSMQ service on the mobile device and also configure MSMQ settings. It can also be invoked programmatically from code. •
visadm.exe:
This tool does the same thing as above, but provides a visual interface. These two files will be unpacked into the \Windows folder of your mobile device. The following DLL files will also be unpacked into the \Windows folder: • •
msmqd.dll msmqrt.dll
Verify that these files exist. The next thing you need to do is to change the name of your device (if you haven't done so earlier). In most cases, you are probably using the Windows Mobile Emulator, which comes with an unassigned device name by default. To change your device name, navigate to Settings | System | About on your mobile device. You can change the device name in the Device ID tab. At this point, you have the files for MSMQ unpacked, but it isn't exactly installed yet. To do this, you must invoke either msmqadm.exe or visadm.exe. Launch the following application: \Windows\visadm.exe
[ 369 ]
Building the Support Case System
A pop-up window will appear. This window contains a text box and a Run button that allows you to type in the desired command and to execute it. The first command you need to issue is the register install command. Type in the command and click the Run button. No message will be displayed in the window. This command will install MSMQ (as a device driver) on your device.
Run the following commands in the given order next (one after the other): MSMQ Command Name register
Purpose
enable binary
This command enables the proprietary MSMQ binary protocol to send messages to remote queues.
enable srmp
This command enables SRMP (SOAP Reliable Messaging Protocol), for sending messages to remote queues over HTTP.
start
This command starts the MSMQ service.
You will need to run the register command one more time (without the install keyword) to create the MSMQ configuration keys in the registry.
Verify that the MSMQ service has been installed successfully by clicking on the Shortcuts button and then clicking the Verify button in the ensuing pop-up window. You will be presented with a pop-up dialog as shown in the following screenshot:
[ 370 ]
Chapter 11
MSMQ log information If you scroll down in this same window above, you will find the Base Dir path, which contains the MSMQ auto-generated log file. This log file, named MQLOGFILE by default, contains useful MSMQ related information and error messages.
After you've done the preceding steps, you will need to do a soft-reset of your device. The MSMQ service will automatically start upon boot up.
Writing your first MSMQ application
Your first MSMQ application will do something really simple—create a local queue (on the device), send a message to the queue, and then read it back from the queue immediately after. Create a new Smart Device Application project named SupportCase, and add a new form to your project. Add a reference to the System.Messaging library in your project. This library contains the MSMQ routines.
[ 371 ]
Building the Support Case System
Create a new class named Generic.cs. This class will contain the initialization routines for MSMQ. Write the following code: using using using using using
System.Diagnostics; System; System.Windows.Forms; System.Messaging; System.Runtime.InteropServices;
namespace SupportCase { public class Generic { //Import the following API functions in CoreDLL.dll [DllImport("CoreDll.dll")]private static extern int CloseHandle(IntPtr hProcess); [DllImport("CoreDll.dll")]private static extern IntPtr ActivateDevice(string lpszDevKey, int dwClientInfo); //This function will initialize MSMQ and ensure that the //service is running. It will ultimately return true if //MSMQ is running public static bool InitializeMSMQ() { bool returnValue; returnValue = RunMSMQCommand("status"); if (returnValue == false) { //MSMQ service is not started yet, so we will //attempt to recreate the registry keys and start //the service RunMSMQCommand("register cleanup"); RunMSMQCommand("register install"); RunMSMQCommand("register"); RunMSMQCommand("enable binary"); RunMSMQCommand("enable srmp"); returnValue = RunMSMQCommand("start"); if (returnValue == false) { //You have to perform ActivateDevice //(specifically for Pocket PC devices)
[ 372 ]
Chapter 11 //to load the MSMQ device drivers. RunActivateDevice(); //Check the status of the MSMQ service again returnValue = RunMSMQCommand("status"); } } return returnValue; } //This function loads the MSMQ device drivers public static void RunActivateDevice() { try { IntPtr handle = ActivateDevice("Drivers\\BuiltIn\\MSMQD", 0); CloseHandle(handle); } catch (Exception ex) { MessageBox.Show(ex.ToString()); } } //This function allows you to run an msmqadm command //programmatically. It is functionally similar to //executing statements in visadm.exe public static bool RunMSMQCommand(string Command) { try { Process _process = new Process(); _process.StartInfo.FileName = "\\windows\\msmqadm.exe"; _process.StartInfo.Arguments = Command; _process.StartInfo.UseShellExecute = true; _process.Start(); _process.WaitForExit(); return (_process.ExitCode == 1 ? false : true); } catch (Exception ex) {
[ 373 ]
Building the Support Case System MessageBox.Show(ex.ToString()); } return false; } } }
Now let's take a look at the code required to send a message to a queue. The first thing you need to do is to create the queue. You can create a queue programmatically using the following code: string _queueName = ".\\private$\\mylocalqueue"; if (! MessageQueue.Exists(_queueName)) { MessageQueue.Create(_queueName); }
The queue name usually takes on the following format. The private$ keyword indicates that this queue is a private queue (that is not published on the network). .\\private$\\queueName
You must now create the message object. The Message class allows you to create one in the following manner: Message _m = new Message(); _m.Label = "My first message"; _m.Body = "Hello, this is my first message";
The Body property of the message takes in an object, which can be a string, or an entire business object. Take note that serialization of business objects is done automatically by the queue. Now that you have the message object, you need to send it to a queue. You must first create a MessageQueue object in your code and pass in the name of the queue you want it to represent. This object contains a Send method that you can use to send the Message object to the queue. This can be done via the following code: MessageQueue _mq = new MessageQueue(_queueName); _mq.Send(_m); _mq.Close();
[ 374 ]
Chapter 11
To read from a queue, you must first inform MSMQ which data type you're using in the Body property of the message. You can do this by setting the MessageQueue. Formatter property to an appropriate formatter. In the following code, you are specifying a String data type for the formatter: _mq.Formatter = new XmlMessageFormatter(new Type[]{typeof(string)}) ;
The next step would be to do the actual read. You can do this by calling the MessageQueue.Receive method, which will then return a Message object. The following code shows how you can retrieve and display the Body of a received message: Message _receivedMsg = _mq.Receive(); MessageBox.Show(_receivedMsg.Body.ToString());
Now, let's put it all together. Add a button to the form you've created earlier. In the click event of the button, write the following code to send a message to a queue and to read it back from the same queue right after: public void btnSendLocalMessage_Click(System.Object sender, System.EventArgs e) { //First we initialize MSMQ if (Generic.InitializeMSMQ() == false) { MessageBox.Show("Could not initialize MSMQ"); return; } string _queueName = ".\\private$\\mylocalqueue"; try { //Create the queue if it does not exist if (! MessageQueue.Exists(_queueName)) { MessageQueue.Create(_queueName); } //Send the message to the queue MessageQueue _mq = new MessageQueue(_queueName); Message _m = new Message(); _m.Label = "My first message"; _m.Body = "Hello, this is my first message"; _mq.Send(_m); [ 375 ]
Building the Support Case System _mq.Close(); MessageBox.Show("MSMQ message sent!"); //Set the formatter to use when reading the incoming //message back from the queue. Typeof(string) indicates //that you wish to read the body of the message as a //string _mq.Formatter = new XmlMessageFormatter(new Type[] {typeof(string)}); //Retrieve the message from the queue and display it Message _receivedMsg = _mq.Receive(); MessageBox.Show(_receivedMsg.Body.ToString()); } catch (Exception ex) { MessageBox.Show(ex.ToString()); } }
If you run this sample, you will see that the message is first sent to the local queue, and then is displayed.
[ 376 ]
Chapter 11
Setting up MSMQ on your server
To send messages remotely from a mobile device to a server, your server must also have queues (and, therefore, MSMQ) set up. Not all PCs or servers come with MSMQ installed by default. To install MSMQ on your PC, you simply have to follow these steps: 1. Navigate to the Control Panel of your operating system, and launch Add/Remove Programs (in Windows XP) or Programs and Features (in Windows Vista). 2. On the left pane, you will be able to see the Add/Remove Windows Components shortcut (in Windows XP) or Turn Windows Features on or off shortcut (in Windows Vista). Click on the shortcut. 3. In the ensuing pop-up window, ensure that all the Microsoft Message Queue (MSMQ) Server options are checked. (The naming of the various subcomponents might also differ between Windows XP and Vista.)
4. Click OK to begin installation. You might be prompted for the Windows setup CD in the process.
[ 377 ]
Building the Support Case System
5. After a successful installation, you will have access to the various message queuing features in the Control Panel | Administrative Tools | Computer Management tool. You will see a Message Queuing node under the Services and Applications node. Through this area, you can manually create/remove a queue and also browse through messages sitting in these queues.
Creating a queue on the server manually using the computer management panel
You will now create a local private queue in your server by right-clicking the Private Queues node and then choosing New | Private Queue from the context menu. Specify private$\jobqueue as the name for this queue. Do not check the Transactional checkbox. You will now see your queue added to the Private Queues section. Right-click on the queue and navigate to its properties window. Under the Security tab, ensure that you have granted the ANONYMOUS_LOGON, IUSR_MachineName, and IWAM_MachineName accounts at least the Receive Message and Peek Message rights.
[ 378 ]
Chapter 11
Sending a message from the server to a remote mobile device
Now that you have everything set up on both sides, it's time to create the support case application. As explained earlier in the overview, your server-side application will attempt to broadcast a new job to all connected mobile devices when it is created. For this purpose, you will be sending data from the server to a queue on each mobile device remotely.
Sending data to a remote queue
The code to send a message to a remote queue remains the same as the one you've written earlier for the local queue. The only difference is that your queue name now takes on the following format: FormatName:DIRECT=TCP:192.168.0.128\\private$\\devicejobqueue
The preceding IP address shown is the target device's IP address on the network. If you want to refer to the device by name, you can also use the following format: FormatName:DIRECT=OS:MYPOCKETPC\\private$\\devicejobqueue
For your testing purposes, you will most likely need the IP address of your mobile device. You can retrieve this information via the Windows Mobile Network Analyzer PowerToy tool, which can be downloaded from the Microsoft website at the following location: http://www.microsoft.com/downloads/details.aspx?FamilyID=081c640149d4-4506-a03b-c41bc76c2f51&DisplayLang=en
This tool allows you to view useful network settings such as the IP address of the device (a rough equivalent of the ipconfig tool on the PC).
[ 379 ]
Building the Support Case System
Creating the server-side application
Create a new project, named SupportCaseServerApp, and add a new class to your project, named JobClass. This will be the business object that holds the details of each job. using System; public class JobClass { private private private private private
string _JobID; string _JobSubject; DateTime _JobDate; string _JobRemarks; string _JobStatus;
public string JobStatus { get { return _JobStatus; } set { _JobStatus = value; } } public string JobSubject { get { return _JobSubject; } set { _JobSubject = value; } } public string JobRemarks { get { return _JobRemarks; } set [ 380 ]
Chapter 11 { _JobRemarks = value; } } public DateTime JobDate { get { return _JobDate; } set { _JobDate = value; } } public string JobID { get { return _JobID; } set { _JobID = value; } } }
Add a form to your project named AddNewJob. This form is the interface that allows you to modify the properties of a JobClass object.
[ 381 ]
Building the Support Case System
Write the following code in the code behind of this form: using System; using System.Windows.Forms; public partial class AddNewJob { private JobClass _jobObj; public JobClass Datasource { get { return _jobObj; } set { _jobObj = value; } } public void OK_Button_Click(System.Object sender, System.EventArgs e) { _jobObj.JobID = txtJobID.Text; _jobObj.JobSubject = txtJobSubject.Text; _jobObj.JobRemarks = txtJobRemarks.Text; _jobObj.JobDate = datJobDate.Value; _jobObj.JobStatus = "OPEN"; this.DialogResult = DialogResult.OK; this.Close(); } public void Cancel_Button_Click(System.Object sender, System.EventArgs e) { this.DialogResult = DialogResult.Cancel; this.Close(); } }
[ 382 ]
Chapter 11
Now, add another form (named Main) to your project. This will be the main form of your application. Place a Datagrid, listbox, and two buttons on the form in the following fashion. Key in the IP address of your mobile device/emulator (retrieved earlier) in the Recipients listbox.
In the click event of the Broadcast a new job button, you will need to launch the AddNewJob form, obtain the details of the new job from the user (and place it in the JobClass object), then broadcast it to the list of recipients. //The list of jobs on the server private List<JobClass> _Jobs = new List<JobClass>(); public void btnBroadCastNewJob_Click(System.Object sender, System.EventArgs e) { AddNewJob _frmAddNewJob = new AddNewJob(); JobClass _jobObject = new JobClass(); _frmAddNewJob.Datasource = _jobObject; if (_frmAddNewJob.ShowDialog() == DialogResult.OK) { _Jobs.Add(_jobObject); RefreshGrid(); //Broadcast this new job to all devices SendJobToAllQueues(_jobObject); } _frmAddNewJob.Dispose(); _frmAddNewJob = null; [ 383 ]
Building the Support Case System } //Loop through all recipient IP addresses and sends the same //message for each one private void SendJobToAllQueues(JobClass JobObject) { int _counter; string _subject; _subject = "Job ID : " + JobObject.JobID; for (_counter = 0; _counter <= lbRecipients.Items.Count – 1; _counter++) { SendMSMQMessage(_subject, JobObject, lbRecipients.Items[_counter].ToString()); } } //Sends an MSMQ message containing a JobClass object to a //target IP address private void SendMSMQMessage(string Label, JobClass JobObject, string TargetIP) { string _targetAddress = "FormatName:DIRECT=TCP:" + TargetIP + "\\private$\\devicejobqueue"; try { MessageQueue _mq = new MessageQueue(_targetAddress); Message _m = new Message(); _m.Label = Label; _m.Body = JobObject; _mq.Send(_m); _mq.Close(); } catch (Exception ex) { MessageBox.Show(ex.ToString()); } } //Refreshes the list of jobs displayed on the grid private void RefreshGrid() { dgJobs.DataSource = null; dgJobs.DataSource = _Jobs; } [ 384 ]
Chapter 11
You can try running the preceding code at this point. Ensure that your mobile device/emulator is not connected to your PC. After creating a new job (via the Broadcast a new job button), you will be able to see the job appear in your Datagrid. More importantly, you will notice that MSMQ has internally created an outgoing queue (via the Computer Management tool). You can see the details of the message by double-clicking on it in the right pane of the tool.
When the mobile device reconnects with your PC, the message will be automatically sent out to the remote queue, and will be removed from this Outgoing messages area.
Creating the client-side application
Now you will need to create the client-side application so that you can retrieve these incoming messages from the devicejobqueue queue on the mobile device. Add the same JobClass class to the same SupportCase smart application project you've created earlier in this chapter.
[ 385 ]
Building the Support Case System
Add a new form named JobPool to your project. This form will contain a Datagrid control and some menu options. The following is the layout:
Upon loading, this form should check to see if the private$\devicejobqueue queue is present, and if not, create it. It should then check to see if there are any messages in this queue. You can do this via the MessageQueue.GetAllMessages function, which returns an array of Message objects found in the queue. Take note that this function will not remove the message from the queue. To remove it, you must explicitly call the MessageQueue.ReceiveByID method. Let's see how it all looks in the following code: //The list of jobs retrieved from the server private List<JobClass> _Jobs = new List<JobClass>(); //The currently selected row in the Datagrid private int _currentRow = - 1; public void JobPool_Load(object sender, System.EventArgs e) { //Initialize MSMQ service on the device if (Generic.InitializeMSMQ() == false) { MessageBox.Show("Could not initialize MSMQ"); return; } //Creates the private$\devicejobqueue queue if its not //present CreateJobQueue(); //Reads from the Job Queue ReadFromJobQueue(); } [ 386 ]
Chapter 11 //Creates the private$\devicejobqueue queue if it does not //exist private void CreateJobQueue() { string _queueName = ".\\private$\\devicejobqueue"; try { if (! MessageQueue.Exists(_queueName)) { MessageQueue.Create(_queueName); } } catch (Exception ex) { MessageBox.Show(ex.ToString()); } } //Reads any messages (if available) from the queue. Notice //that you need to inform MSMQ that the payload of the message //is no longer a String object, but a JobClass object private void ReadFromJobQueue() { string _queueName = ".\\private$\\devicejobqueue"; try { MessageQueue _mq = new MessageQueue(_queueName); _mq.Formatter = new XmlMessageFormatter(new Type[] {typeof(@JobClass)}); //Retrieve all messages from the queue Message[] _messages = _mq.GetAllMessages(); int _counter; for (_counter = 0; _counter <= (_messages.Length - 1); _counter++) { Message _currentMsg = _messages[_counter]; JobClass _jobObject = (JobClass)(_currentMsg.Body); _Jobs.Add(_jobObject); //You must do a Receive for each message to remove it //from the queue _mq.ReceiveById(_currentMsg.Id); } RefreshGrid(); [ 387 ]
Building the Support Case System } catch (Exception ex) { MessageBox.Show(ex.ToString()); } } //Refreshes the grid display with the latest list of jobs private void RefreshGrid() { dgJobs.DataSource = null; dgJobs.DataSource = _Jobs; } //store the index of the currently selected row public void dgJobs_CurrentCellChanged(object sender, System.EventArgs e) { _currentRow = dgJobs.CurrentCell.RowNumber; } //Refreshes the grid by reading again from the message queue public void mnuRefresh_Click(System.Object sender, System.EventArgs e) { ReadFromJobQueue(); }
You can try running your application at this point. Ensure that your mobile device/emulator is connected to your PC. Start both the server-side and client-side applications. Create a new job at the server side. After you have done that, click on the Refresh menu in the client-side application on your mobile device. You will be able to see the newly created job show up in the data grid.
[ 388 ]
Chapter 11
Sending a message from the mobile device to the server
Now, we need to pass messages the other way around. The end user will "accept" a job, and this will constitute a message that must be passed back to the server.
Writing the client-side code
In the same JobPool form, write the following code for the Mark selected job as accepted menu-click event: public void mnuMarkAccepted_Click(System.Object sender, System.EventArgs e) { if (_currentRow < 0) { MessageBox.Show("You must select at least one item from the grid"); return; } //Get the currently selected job JobClass _selectedJobObject = _Jobs[_currentRow]; //Mark job status as Accepted _selectedJobObject.JobStatus = "ACCEPTED"; //Notify the server using an MSMQ message if (NotifyServerJobAccepted(_selectedJobObject) == true) { MessageBox.Show("Job accepted notification message sent"); } } //This function sends the accepted job to the server. Take //note that you will need to specify the IP address of your //server in the target Queue name private bool NotifyServerJobAccepted(JobClass JobObject) { string _QueueName = "FormatName:DIRECT=TCP:192.168.140.1\\private$\\jobqueue";
[ 389 ]
Building the Support Case System try { MessageQueue _mq = new MessageQueue(_QueueName); Message _m = new Message(); _m.Label = "Job Accept Notification"; _m.Body = JobObject; _mq.Send(_m); _mq.Close(); return true; } catch (Exception ex) { MessageBox.Show(ex.ToString()); return false; } }
Writing the server-side code
Back at the server side, open the Main form. In the click event of the Refresh Job Statuses button, you will need to retrieve any messages in the jobqueue queue (if available) sent by the mobile device, and then update the statuses of the corresponding jobs in the Datagrid. public void btnRefreshJobStatus_Click(System.Object sender, System.EventArgs e) { string _queueName = ".\\private$\\jobqueue"; try { MessageQueue _mq = new MessageQueue(_queueName); _mq.Formatter = new XmlMessageFormatter(new Type[] {typeof(@JobClass)}); //Retrieve all messages from the queue System.Messaging.Message[] _messages = _mq.GetAllMessages(); int _counter; for (_counter = 0; _counter <= (_messages.Length - 1); _counter++) { Message _currentMsg = _messages[_counter]; JobClass _jobObject = (JobClass) (_currentMsg.Body);
[ 390 ]
Chapter 11 //Update the job record UpdateJobsList(_jobObject); //You must do a Receive for each message to remove it //from the queue _mq.ReceiveById(_currentMsg.Id); } RefreshGrid(); } catch (Exception ex) { MessageBox.Show(ex.ToString()); } } //Look for the job in the _Jobs list with the same Job ID, and //update its status field private void UpdateJobsList(JobClass Job) { int _counter; for (_counter = 0; _counter <= _Jobs.Count - 1; _counter++) { JobClass _jobObj = _Jobs[_counter]; if (_jobObj.JobID == Job.JobID ) { //The job record was found. We update the status of //the job with the latest status information from the //Job object passed in _jobObj.JobStatus = Job.JobStatus; return; } } }
[ 391 ]
Building the Support Case System
Now try running this application. Open both the server-side and client-side applications again. Create a job at the server side and retrieve it from the client-side application. At the client-side application, select a job from the data grid, and choose the Mark selected job as accepted menu item. A message will be sent back to the server. If you now click on the Refresh Job Statuses button on the server application, you will see that the status of the matching job now displays Accepted instead of Open.
Summary
In this chapter, you've developed two applications (client-server model) that communicate entirely via the Microsoft's messaging queue framework. You've seen how this framework can provide a facility for reliable disconnected messaging. Throughout this chapter, you've learned: •
How MSMQ and offline messaging works internally
•
How to set up message queues on the mobile device and server for remote messaging
•
How to use the XMLMessageFormatter class to serialize/deserialize business objects during transmission via MSMQ
•
How to use the two core classes in the MSMQ framework—the
MessageQueue and Message class
In the next chapter, we will take a look at the PowerToys for .NET CF 3.5 suite of tools provided by Microsoft and how to use them to help you debug your .NET CF applications. [ 392 ]
Testing and Debugging Most of us have encountered cases where an application would crash and burn after running for extended periods of time and then see error messages (such as Out of handles exceptions) that don't seem to shed any light on where or how the problem occurred. Troubleshooting issues such as memory leaks using the debugging features of Visual Studio alone can be a frustratingly difficult affair, particularly because these types of problems cannot be traced by the debugger to any specific line in code but rather, arise from a build-up of memory that is not released. Microsoft has released a suite of diagnostic tools to help .NET CF developers troubleshoot these types of issues in their applications. The Power Toys for .NET CF 3.5 suite is provided free of charge, and gives you the ability to peek "under the .NET CF hood" while your mobile application runs. In this chapter, you will learn how to use each tool to resolve commonly encountered .NET CF problems. You will specifically learn about the following: •
The Power Toys for .NET CF 3.5 package and what it contains
•
How to use the Remote Performance Monitor and GC Heap Viewer tools to detect and resolve memory leaks
•
How to use the CLR Profiler tool to investigate the memory allocation profile of your application
•
How to use the App Configuration tool to configure the specific .NET CF version used for an application
•
How to use the ServiceModel Metadata tool to generate client code to connect to WCF or web services
•
How to log and view network events using the Remote Logging Configuration/Viewer Tools
Testing and Debugging
Overview of Power Toys for .NET CF 3.5 In a nutshell, the Power Toys for .NET CF 3.5 package is a set of tools commonly used to gauge application performance and collect diagnostic information for applications developed on .NET CF 3.5. The following table lists down each tool in the package and what it does. Tool name
Description
Remote Performance Monitor and GC Heap Viewer
The Remote Performance Monitor allows you to capture and view performance statistics in real time while your mobile application is running. The GC Heap Viewer allows you to capture "snapshots" of the memory heap at different points in time and to analyze the differences between each "snapshot."
CLR Profiler
The CLR Profiler tool is a memory profiler that provides details on how your application allocates managed objects.
App Configuration tool
This relatively simple tool allows you to view the contents of the Global Assembly Cache (GAC) on your mobile device. It also allows you to configure the specific .NET CF version used for each application on the mobile device.
ServiceModel Metadata tool
This tool allows you to generate client-side source code (in the language of your choice) to connect to a WCF or web service. You can immediately import these generated files into your Visual Studio project.
Remote Logging Configuration tool
This tool allows you to enable or disable logging of network events on the mobile device. Once enabled, all network messages and events (including WCF sessions) will be captured to a log file on the mobile device.
Network Logging Viewer tool
This tool allows you to view the log files generated by the Remote Logging Configuration tool.
Installing Power Toys for .NET CF 3.5
You can download the Power Toys for .NET CF 3.5 package from this location: http://www.microsoft.com/downloads/details.aspx?FamilyID=c8174c14a27d-4148-bf01-86c2e0953eab&displaylang=en.
[ 394 ]
Chapter 12
After running the setup, you will see a new shortcut in your All Programs area (as shown in the following screenshot). Some of the tools mentioned in the preceding table might not be visible in this area because they are command-line based. Nevertheless, you will see how you can launch and use each of these tools later in this chapter.
Using the Remote Performance Monitor and GC Heap Viewer tool
The .NET CF Remote Performance Monitor and GC Heap Viewer tool, as its name suggests, consist of two parts: •
The Remote Performance Monitor, which allows you to view application statistics (such as the amount of memory allocated and so on) in real time. It is also able to project these statistics to the Windows PerfMon tool so that the data can be viewed as a real-time graph.
•
The GC Heap Viewer tool, which allows you to capture snapshots of the GC heap area (the memory area where managed objects are allocated) at certain times and then analyze their differences.
Memory leaks and their causes
Before we proceed to use this tool, it is appropriate to understand why you will need it in the first place. One of the main reasons is memory leaks—a condition where memory allocated is not released after use. In the next section, we will take a look at how memory leaks can arise as a result of careless programming. Many developers still think that memory leak issues are only "real" or significant problems in multiuser environments such as web applications where leaks build up over a large number of requests, and that it is less of a concern for single-user applications. On the contrary, mobile applications suffer more when memory leaks occur, due to the lesser amount of memory present on the device, and the likely possibility that applications are kept open for long periods of time. [ 395 ]
Testing and Debugging
Although the .NET CF's garbage collector does a good job cleaning up managed objects that gone out of scope, certain programming mistakes can cause these objects to remain referenced in memory, and thus go uncollected. The following are some mistakes commonly encountered: •
Registering for events and forgetting to unregister them after you're done: This is by far the most common source of memory leaks for the C# developer. Any event handlers (and their objects) registered with an event are not released until they are unregistered from the event.
•
Forgetting to invoke the Dispose method: Many controls and classes have Dispose methods. For example, the OleDBCommand and ODBCCommand objects have Dispose methods that must be called to free up unmanaged resources.
•
Using static references: When you add objects to an array declared as a static variable, for example, these objects (and all other objects it references) are kept alive until the static array itself is destroyed.
A sample application with memory leak
To effectively demonstrate the tools in this section, you will need to create a small application that intentionally causes a memory leak by instantiating a class, registering it as an event handler, and failing to unregister it after use. Create a new project, add a new form to the project, and place a single button on the form.
In the click event of the button, we want to instantiate a business object, register one of its methods as an event handler and then raise the event. Most developers would write something similar to the following code: // Declare the delegates public delegate void OnCertainEventOccured(object sender, EventArgs e); [ 396 ]
Chapter 12 public event OnCertainEventOccured OnCertainEvent;
// The Click event of the button on the form private void btnInvokeHandler_Click(object sender, EventArgs e) { //Register the event handler MyLargeBusinessObject _bizObj = new MyLargeBusinessObject(); this.OnCertainEvent += new OnCertainEventOccured(_bizObj.TheEventHandler); //Call the event handler OnCertainEvent(this, null); //Kill the business object _bizObj = null; }
The definition of the business object follows: //A large business object class MyLargeBusinessObject { //Let's assume this business object allocates a large //amount of memory space char[] _bigVariable = new char[100000]; //The event handler in the business object public void TheEventHandler(object sender, EventArgs e) { //Do something here... } }
The (incorrect) perception is that the _bizObj instance would automatically go out of scope once the method completes (as it was declared inside the method). To be safe, you might even decide to enforce a _bizObj = null at the end of the method.
[ 397 ]
Testing and Debugging
You will find that despite your efforts, _bizObj remains uncollected by the garbage collector. This is because there is still one item that holds a reference to the _bizObj object—the event registration itself. To ensure that there are no references left, you must unregister the event handler in _bizObj from the event at the end of the method. Nevertheless, let's try subjecting the preceding code sample through the Remote Performance Monitor. Create the sample project, name it as SampleProject, and deploy it to your mobile device/emulator.
Using the Remote Performance Monitor tool to view application statistics in real time Before you can use the .NET CF Remote Performance Monitor tool, you must first ensure that you have set up an ActiveSync connection between the mobile device/emulator and your development PC. After doing so, you can launch the Remote Performance Monitor tool from Start | All Programs | .NET Compact Framework Power Toys 3.5 on your development machine. You will encounter a window similar to the one shown in the following screenshot:
Click on the Launch Application button (represented by the green arrow icon). This will allow you to specify the application that you want to monitor (shown in the next screenshot). Choose the appropriate device and type in the full path to your application (as it exists on the mobile device). Ensure that the SampleProject.exe application is already deployed to the mobile device/emulator, but don't launch the application yet. You can leave the other fields empty.
[ 398 ]
Chapter 12
When you click the OK button, the Remote Performance Monitor tool will launch the application on your device and, at the same time, start collecting statistics immediately. You will see a window similar to the one shown in the next screenshot. A number of counters are displayed, with a description of each one on the right.
[ 399 ]
Testing and Debugging
Most of the counters relevant to memory management are displayed under the GC category. The Managed Bytes In Use After GC counter is a good indicator of whether a memory leak has occurred. This counter shows the number of live objects remaining since the last collection run by the Garbage Collector. If this number keeps increasing over time, it is a good chance that there is a memory leak somewhere preventing your managed objects from being collected by the Garbage Collector.
You will probably notice that the Managed Bytes In Use After GC counter is initially set to 0. Now, try clicking the Invoke button in the SampleProject application window. You will notice the value in the Managed Bytes Allocated counter increase, indicating that memory was allocated (due to the instantiation of the MyLargeBusinessObject class). Click on the same button a few more times. Eventually, a GC run will occur, and you will realize that the Managed Bytes In Use After GC counter will show a number greater than 0. Clicking the button further will increase this value. This indicates that the objects created from the button click action are not being collected by the GC run.
Using PerfMon to graphically view runtime performance statistics
The Remote Performance Monitor tool can also output its data (in real time) to the Windows PerfMon tool so that you can see the generated data in a scrolling time graph. To do this, you first need to ensure that the Publish to PerfMon setting under the Options menu is ticked. Launch the PerfMon tool by navigating to Start | Control Panel | Administrative Tools | Reliability and Performance Monitor. Select the Performance Monitor item under the Monitoring Tools item category in the left pane.
[ 400 ]
Chapter 12
A time chart will be seen instantly in the right pane. You now need to add the .NET CF counters to the chart so that they can be displayed. Right-click on the right pane and choose Add Counters. You will see a window similar to the following screenshot:
[ 401 ]
Testing and Debugging
Look for the .NET CF GC category, expand the list, select the counters you wish to include in the chart display, and click the Add >> button to include it in the list. When you are done, click the OK button. You will see the counters appear in the chart (as shown in the following screenshot). Take note that you might need to adjust the vertical scale of the chart appropriately to properly view the data in all the counters. You can set the vertical scale by navigating to Action | Properties, clicking on the Graph tab in the window that pops up, and changing the Maximum and Minimum values under the Vertical Scale section.
If you have the Managed Bytes In Use After GC counter added, you can see how the chart responds by clicking the Invoke button on your SampleProject form a few times.
[ 402 ]
Chapter 12
Using the GC Heap Viewer tool to detect memory leaks
Another way to detect memory leaks is via the GC Heap Viewer tool. In the Remote Performance Monitor tool, click the GC Heap button. This will generate a snapshot of the heap (at the time when you pressed the button). You will see the display shown in the following screenshot. The GC Heap Viewer shows you a list of all the managed objects that were allocated. If you look for the SampleProject.MyLargeBusinessObject class, you will notice that there are multiple instances of it allocated in memory. This is because each time you pressed the Invoke button on your form, a new instance of the class was generated. The objects are not released from memory because they were not properly unregistered from the OnCertainEvent event.
Try clicking the Invoke button on your SampleProject form once. Now, without closing the current heap window, navigate to the main tool window, and click the GC Heap button one more time. This will capture a second snapshot of the heap. You will notice in this latest snapshot that the number of SampleProject. MyLargeBusinessObject instances have increased by 1. [ 403 ]
Testing and Debugging
Now, do the same thing as above one more time so that you now have three GC Heap View windows open. In any one of the GC Heap View windows, navigate to the View | Compare Heap menu item. This will compare the heaps across all open GC Heap View windows. You will see a snapshot similar to the following screenshot:
This comparison view shows you the delta (of the number of instances) for each managed object between each GC snapshot. You can see that the number of instances of the SampleProject.MyLargeBusinessObject object has increased by one across each snapshot.
Resolving the memory leak
Now that we know for sure that there's a memory leak in SampleProject, let's resolve the leak. Add the following highlighted line of code to unregister the _bizObj object from the event. private void btnInvokeHandler_Click(object sender, EventArgs e) { //Register the event handler MyLargeBusinessObject _bizObj = new MyLargeBusinessObject(); this.OnCertainEvent += new OnCertainEventOccured(_bizObj.TheEventHandler); //Call the event handler OnCertainEvent(this, null);
[ 404 ]
Chapter 12 //Unregister the event handler this.OnCertainEvent -= new OnCertainEventOccured(_bizObj.TheEventHandler); }
Now, compile and deploy the application to the mobile device. Launch the application from the Remote Performance Monitor tool again. You will notice that clicking the Invoke button now no longer causes the value in the Managed Bytes In Use After GC counter to rise. If you try capturing a GC snapshot, you will not even see the SampleProject.MyLargeBusinessObject object in the list anymore, because the object goes out of scope as soon as the method completes. If you run it through the same test as earlier, and do a comparison across the various GC snapshots, you will see the message shown in the following screenshot:
This confirms that there are no more objects left unallocated—the memory leak is resolved.
Using the CLR Profiler tool
The CLR Profiler tool is another powerful tool that allows you to view the memory allocation profile of your application. It provides the details of how your application is allocating and using managed objects. You can look at the heap at various points in time and even walk through the method calls involved in allocating each managed object. While the Remote Performance Monitor tool is useful in detecting memory leaks, the CLR Profiler tool allows you to zoom in on the particular method and line of code that caused it. To describe the features of the CLR Profiler tool, we will run it on another sample application—one with visibly bad performance.
[ 405 ]
Testing and Debugging
A sample application with bad performance
Let's assume we have an application that needs to populate a DataGrid control with a list of Customer objects. Each Customer object holds a Product code (the product he or she is interested in), which is used to fetch the actual product name from a hash table. Create a new project and add a new form to the project. Add a DataGrid control to the form and create a menu item with the caption Refresh.
Add a class named Product to the project. This class represents a product, and also contains the function to generate a lookup table of all available products. class Product { private string _productName; private string _productCode; private byte[] _productImage = new byte[500]; public Product(string ProductName, string ProductCode) { _productName = ProductName; _productCode = ProductCode; } public string ProductName { get { return _productName; } set { _productName = value; } } public string ProductCode { get { return _productCode; } set { _productCode = value; } } [ 406 ]
Chapter 12 //Generate 20 sample products with the codes P1,P2,P3... //and returns them as a hashtable public static Hashtable LoadAllProducts() { Hashtable _products = new Hashtable(); for (int i = 0; i <20; i++) { Product _prod = new Product("Product" + i.ToString(), "P" + i.ToString()); _products.Add(_prod.ProductCode, _prod); } return _products; } }
Add another class named Customer to the project. This class represents a customer. You will notice that the constructor for this class attempts to compare the specified ProductCode against the lookup table generated from the LoadAllProducts function. class Customer { //Full name of the interested product private string _interestedProduct; //Customer name private string _customerName; public string InterestedProduct { get { return _interestedProduct; } set { _interestedProduct = value; } } public string CustomerName { get { return _customerName; } set { _customerName = value; } } public Customer(string CustName, string ProductCode) { Hashtable _allProducts = Product.LoadAllProducts(); Product _matchingProduct = (Product)_allProducts[ProductCode]; _customerName = CustName; _interestedProduct = _matchingProduct.ProductName; } } [ 407 ]
Testing and Debugging
In the form you've created earlier, write the following code. Upon loading the form, we will allocate about 50 KB of memory. The reasons for this will be clear in the next section. The Datagrid is populated with 20 customer objects when the user clicks the Refresh menu item. public partial class frmBadPerformanceSample : Form { byte[] _workBuffer; private void RefreshPage() { List _allCustomers= new List(); for (int i = 0; i < 20; i++) { Customer _cust = new Customer("Cust" + i.ToString (), "P" + i.ToString ()); _allCustomers.Add(_cust); } dgCustomers.DataSource = _allCustomers; } private void DoSomeIntensiveWork() { //Allocate some byte[] memory _workBuffer = new byte[50000]; //Do something here //... } private void frmBadPerformanceSample_Load(object sender, EventArgs e) { DoSomeIntensiveWork(); } private void mnuRefresh_Click(object sender, EventArgs e) { RefreshPage(); } }
[ 408 ]
Chapter 12
The loading of all 20 sample products was placed in the constructor of the Customer object to intentionally simulate bad performance. So for 20 customer records, there would be 20 * 20 = 400 product objects allocated in memory. You will now use the CLR Profiler tool to see if you can get to the root of the problem.
Launching the application with the CLR Profiler tool
Compile and deploy the sample project above to your mobile device/emulator. You can launch the .NET CF CLR Profiler tool from the Start | All Programs | .NET Compact Framework Power Toys 3.5 menu. You will immediately see the CLR Profiler control panel, similar to the following screenshot:
Click on the Start Application... button, and type in the full path to your Bad Performance sample application (as it exists on your mobile device). You can leave the Parameters field blank as there aren't any. Click on the Connect button to start recording information. The mobile application will run.
[ 409 ]
Testing and Debugging
Ensure that you wait a few seconds after the form has loaded, then click the Refresh menu item. A few seconds later, the Datagrid will be populated with data. After that click the Kill Application button in the CLR Profiler control panel to stop the recording. You will see the following window appear:
Inspecting the Histogram view
From this window, you can launch a number of different views of the collected statistics. The first you will look at is the Histogram view. Click on the Histogram button next to the Allocated bytes field. A window similar to the one shown as follows will appear. This window shows you the amount of memory allocated for each different type of managed object used in your application. You can also fine-tune the chart to see more or less detail by changing the vertical and horizontal scales.
[ 410 ]
Chapter 12
The first thing you will notice is that a surprisingly large amount of Byte objects were allocated (212 Kb). This tells you immediately that something isn't right, considering that all that your application is doing is just populating a Datagrid. You can use the Histogram view to find out (at first glance) if there is anything "unusual" about the proportion of allocated objects.
Now that you're on to something—the Byte object—let's take a look at the allocation history of this object. Close the Histogram view, and click on the Allocation Graph button in the main window.
Inspecting the Allocation Graph
The Allocation Graph is a visual walkthrough of your entire application from the main thread all the way down to each individual method call along with its allocated objects. The physical size of each item indicates the amount of memory it takes up. Even for a simple application, it might look really messy when it first loads up, but you can change the level of detail to show only allocations of a significant size. You can also quickly locate an item by navigating to the Edit | Find routine menu. You can use this to look for the Byte object.
[ 411 ]
Testing and Debugging
When you've found the Byte object, you will notice that two methods contribute to its allocation (as highlighted in the following screenshot): •
The constructor of the Product class
•
The DoSomeIntensiveWork method
By looking at this chart, you can further conclude that the constructor of the Product class was called in the LoadAllProducts method. Using the Allocation Graph, you can "walk backwards" starting from an allocated object. This way, you can discover the sequence of function calls that brought about its allocation.
Inspecting the Time Line view
You now know that two methods contribute to the unusual allocation of the Byte object. Usually, with some guesswork, that is good enough for you to know which method caused the problem. In the lifetime of a complex application, however, you will usually have much more than two leads and you may need to zoom down in further detail.
[ 412 ]
Chapter 12
The Time Line view allows you to view the GC heap over a time scale representing the duration of your application's execution. With this feature, you can inspect the allocation graph at any point (in time) in its execution! Close the Allocation Graph view and click the Timeline button on the main CLR Profiler Tool window. The following screenshot shows the Time Line view:
Now, move the time window to the first few seconds (when you haven't yet clicked the Refresh button to populate the Datagrid) by left-clicking any area within the first few seconds in the chart. A thin vertical line will appear denoting the currently selected time window. Right-click on the chart and choose Show Who Allocated.
[ 413 ]
Testing and Debugging
The familiar Allocation Graph will appear again, but this time, it will only show the objects allocated during that point in time. If you look for the Byte object again, you can see it was allocated solely by the DoSomeIntensiveWork method (shown next).
Now, close the allocation graph and move the time window to a later point in time (after you have clicked the Refresh button to populate the Datagrid). If you look at the allocation graph in that time window, you will be able to see the Product object's constructor contributing to the allocation of the Byte object (shown next).
[ 414 ]
Chapter 12
As the Product object is only created when you click the Refresh button to populate the Datagrid, it doesn't show up in the allocation graph before that. We can also conclude from this view that the problem lies in the Product object's constructor rather than the DoSomeIntensiveWork method, because the bad performance was observed only when you clicked the Refresh button.
Inspecting the Call Tree view
Knowing that the Product object's constructor is the culprit might not be enough—if you've gone to look at the source code of the constructor, you'll find that each Product allocates 500 bytes to store an image, but you already knew that. You need to know how 20 products can give rise to an allocation of 212,000 bytes. The Call Tree view is another view that shows every allocation made by every method call in your application (in a tree-list format). Click the Call Tree button in the main CLR Profiler Tool window. A new window will open, and you will see a single item in the tree list. This represents the main function thread of your application. Method calls are shown in black, and object allocations are shown in green. You can follow the trail of execution by expanding each node. At each level, there will be a highlighted node. The highlighted node shows the method that allocated the most memory at that level. By walking through this trail of highlighted nodes, you will eventually end up at the object you are looking for.
[ 415 ]
Testing and Debugging
At one point as you are expanding the nodes, you will arrive at a rather strange observation. Twenty Customer objects are generated, but each object makes roughly 500 method calls, and the size of each Customer object comes up to about 15 KB in size, which is alarmingly huge. This is shown in the following screenshot:
If you expand each Customer object further, you finally find the root cause of the problem—the constructor of each Customer object makes a call to the LoadAllProducts method, which generates 20 Product objects in return. This makes a total of 400 Product objects!
[ 416 ]
Chapter 12
From this point, you can conclude that the next logical step is to move the LoadAllProducts method out of the constructor of the Customer class into a global area so that it is only called once—resolving the bad performance problem. The CLR Profiler is way too slow! You might have noticed that the CLR Profiler generates a huge amount of data—even from a simple application. I've heard developers complain that a reasonably complex application started with the CLR Profiler can take minutes to load up. There is a workaround to this, which is to use the CLR Profiler API, a set of libraries that allow you to specifically control when to generate diagnostic data programmatically from within your application. This topic is out of the scope of this book.
Using the App Configuration tool
If you write mobile applications targeting different versions of .NET CF, you will need to explicitly configure the application to run using that specific .NET CF version. The .NET CF App Configuration tool allows you to do this. It is located in this folder on your development machine: \Program Files\Microsoft.NET\SDK\CompactFramework\v3.5\WindowsCE\ wce500 \armv4i\NetCFCfg.exe
You will need to copy this file into your mobile device/emulator. You can place the file in any folder you wish. When you run this tool from your mobile device, you will see a window with a number of tabs displayed at the bottom. The About tab shows the currently installed versions of .NET CF on the mobile device.
[ 417 ]
Testing and Debugging
The GAC tab shows all the assemblies registered in the Global Assembly Cache of the mobile device/emulator. You can use this list to check if an assembly you are using in your application is registered with the GAC.
The Device Policy tab shows the default .NET CF version that is used to launch your .NET CF applications.
[ 418 ]
Chapter 12
The Application Policy tab (shown in the next screenshot) allows you to configure the .NET CF version that is used for each individual application. The setting here overwrites the setting in the Device Policy tab.
Using the ServiceModel Metadata tool
If you have created a WCF/web service to consume from your mobile application, you can easily connect to it by adding a web reference to it in your mobile application project in Visual Studio (as you've done in Chapter 10, Building the Dashboard). There is, however, another way to do this. The ServiceModel Metadata tool allows you to quickly generate the classes that are required to connect to a WCF/web service. You can even specify the language of the generated source code. To test this tool, you can use the DashboardService web service you've created in Chapter 10. Make sure that the web service is properly set up and hosted in IIS. Open a command prompt window and navigate to the following folder: \Program Files\Microsoft.NET\SDK\CompactFramework\v3.5\bin
You can run the ServiceModel Metadata tool using the following command and syntax: netcfsvcutil.exe /language: <serviceURL
For example, you can generate C# client code files for the DashboardService web service by running the following command: netcfsvcutil.exe /language:cs http://localhost/DashboardService/ Dataservice.asmx
[ 419 ]
Testing and Debugging
The output of this command is shown in the following screenshot:
Now, create a new Smart Device Windows Application project, and import the following libraries to the project: • •
System.ServiceModel System.Runtime.Serialization
Add the two generated files to your project. After that, add a new form to your project. Place a multiline text box and a button on this form (named btnInvoke). In the click event of this button, write the following code: private void btnInvoke_Click(object sender, EventArgs e) { System.ServiceModel.Channels.Binding _binding = DataServiceSoapClient.CreateDefaultBinding(); string _serverAddress = DataServiceSoapClient.EndpointAddress.Uri.ToString(); //Replace with the correct address – keep in mind that this //code executes on your mobile device – it needs to connect //to the webservice sitting on the server _serverAddress = _serverAddress.Replace("localhost", "edzehoo-pc"); EndpointAddress _endPoint = new EndpointAddress(_serverAddress); //Create the client DataServiceSoapClient _client = new DataServiceSoapClient(_binding, _endPoint); try { [ 420 ]
Chapter 12 //Get chart data for December 2009 txtResults.Text = _client.GetChartData(2009, 12); } catch (Exception ex) { MessageBox.Show(ex.Message); } }
That's all it takes. Make sure that an ActiveSync connection has been set up between the mobile device and your development machine. When you run this application and click the Invoke button, it will successfully connect to your web service and retrieve the data into the multiline text box (as shown in the following screenshot).
Using the Remote Logging Configuration tool If you have developed a mobile application that makes use of remote WCF or web services and you experience problems communicating with the service, you can use Remote Logging to capture all network events that occur between the mobile device/emulator and the remote service.
[ 421 ]
Testing and Debugging
The Remote Logging Configuration tool allows you to activate the capturing of network event logs. You can run this tool from the Start | All Programs | .NET Compact Framework Power Toys 3.5 menu on your development machine. In the pop-up window, check the Network item (as we are only interested in network events). As indicated in the following screenshot, the network events will be stored in a file named netcf_network.log. Click the Apply button to apply these changes to your mobile device/emulator.
You can try launching the sample project you've created earlier (for the ServiceModel Metadata tool). You will notice that launching any .NET CF application on the mobile device will now prompt the following dialog message:
[ 422 ]
Chapter 12
Choose Yes to proceed. In the sample application, try connecting to the web service. After successfully retrieving data, close the application. You will notice that a log file (named netcf_Network.log) has been generated in the same folder as your application.
It will be difficult to read this log file on the mobile device, so you will need to copy the file to your development machine.
Using the Network Log Viewer tool
Once you've used the Remote Logging Configuration tool to enable logging, you can use the Network Log Viewer tool to view the generated logs. You can launch the Network Log Viewer tool from the Start | All Programs | .NET Compact Framework Power Toys 3.5 menu on your development machine. Navigate to the File | Open menu, and browse for the netcf_Network.log file. Once you've opened it, you should be able to see a screen similar to the following screenshot:
[ 423 ]
Testing and Debugging
The log viewer shows each network event and packet of data that is sent or received between the mobile device/emulator and the remote service, in sequential order. This is particularly useful when you need to know at which stage exactly an error occurs in a network communication failure.
Summary
In this chapter, you've dealt with all the tools in the Power Toys for .NET CF 3.5 suite. You've specifically learned: •
How to use the Remote Performance Monitor and GC Heap Viewer to detect and resolve memory leaks
•
How to use the Histogram, Allocation Graph, Time Line, and Call Tree views of the CLR Profiler tool to inspect how your managed objects are being allocated under the hood
•
How to use the Remote Logging Configuration tool to capture network event logs and view them using the Remote Logging Viewer tool
•
How to connect to a web service using client code files generated from the ServiceModel Metadata tool
•
How to configure the specific .NET CF version to use for each mobile application
In the next and final chapter of this book, we will take a look at how you can package your applications into .MSI and .CAB files for deployment.
[ 424 ]
Packaging and Deployment You've spent all your time sculpting and perfecting your applications—it's now time to get them from the development machine into the hands of your users (literally!). Packaging and deploying an application is an important part of the user experience. It is essentially the first interaction between your users and your application, and a troublesome installation process can quickly leave your users unimpressed and frustrated. Imagine the pain of a systems administrator who has to repeat this for every mobile device that requires an installation. If you're a mobile user, you will have noticed that mobile software applications are generally deployed in two forms—CAB files and MSI files. CAB files consist of multiple compressed files stored as a single file. Due to their convenience and small size, they are ideal for distribution of a mobile application. CAB files come with a disadvantage, however, and that is they involve a steeper learning curve for first-time users. To install a CAB file from a Compact Disc, for example, the user must first establish an ActiveSync connection between the mobile device and the development machine, manually copy the CAB file into the device, and then run it off the device. MSI files make the deployment process slightly easier—they provide an installation wizard (on the PC) that automatically copies the CAB files onto the mobile device and executes them, but an ActiveSync connection still needs to be set up first. In this chapter, you will learn the following: •
How to deploy your solution as a CAB file
•
How to deploy your solution as an MSI file
•
How to create a service that checks for updates on a remote server and automatically downloads and installs them onto the mobile device
Packaging and Deployment
Deploying your solution as a CAB file
Microsoft Visual Studio provides the necessary project templates to help you build CAB and MSI packages for the smart device. You can create a CAB project by choosing the Smart Device CAB Project template under the Other Project Types | Setup and Deployment category in Visual Studio (shown as follows):
It is a good idea to have the CAB suffix in your project name, as you are also going to build an MSI installer in the same solution later.
After you've done so, you need to add the project containing your sales force application to the solution. Go to the File menu and click on the Add | Existing Project menu item. Browse for your sales force application project, and include it in the solution. You should also consider changing the name of your solution to a more generic one (like SalesForceAppInstaller), as you are going to house the MSI project later in the same solution.
[ 426 ]
Chapter 13
Adding the SalesForce application files to your CAB project You now need to tell the CAB project to include the output files of the sales force application project. To do so, right click on the CAB project and choose the Add | Project Output menu item (shown as follows).
When you've done that, you will see a screen similar to the one shown as follows. Here you can choose to include the output files of a particular project in the solution. There are several types of project output groups. You can choose to include the Primary Output of the selected project (the EXE and DLL files of the project), the Documentation Files (XML documentation files of the project), or Content Files (External content files, such as images, attached to the project), to name a few.
[ 427 ]
Packaging and Deployment
As we're only interested in the SalesForceApp.exe executable file and its DLL dependencies, select the SalesForceApp item as the project, and Primary Output as the project output group.
Click OK to continue. After you have done this, you will see your selection appear within your CAB project (shown as follows):
The next step is to include the other files required by the SalesForceApp project. If you recall from the first few chapters of this book, you've created a few other DLLs that are required for the proper functioning of the sales force application: • • •
CRMLiveFramework.dll SQLServerPlugin.dll OracleLitePlugin.dll [ 428 ]
Chapter 13
You can manually include these files in the CAB project by right-clicking on the CAB project and navigating to the View | File System menu item (shown below).
This will show the CAB File System folder hierarchy in the right pane of the Visual Studio IDE. The folders shown here correspond to where the actual files will be installed during deployment on the mobile device. You will also notice that the Primary output option you've selected appears in the Application Folder item, denoting that SalesForceApp.exe and its DLL dependencies will be installed in the application folder on the mobile device. You can now individually add your own files to the list. Right-click on the Application Folder item, and choose Add | File.
[ 429 ]
Packaging and Deployment
Browse for the OracleLitePlugin.dll and SQLServerPlugin.dll files and add them to the list. Take note that adding each DLL file automatically includes their dependency files as well. In this case, both plug-in files reference the CRMLiveFramework.dll file. The CRMLiveFramework.dll file is, therefore, automatically included in the list.
Configuring other miscellaneous settings
When creating the CAB file, you should also remember to set a number of other settings, such as the Product name, Manufacturer name, and so on. You can access these properties from the project properties window of your CAB project.
[ 430 ]
Chapter 13
You should also remember to change the build type of all your projects to Release mode instead of the default Debug mode. Doing this reduces the size of the executable file on the device. You can perform this task by right-clicking on the solution and choosing Configuration Manager from the pop-up menu. You will see a pop-up window similar to the following screenshot. Change the active solution configuration from Debug to Release.
You are now ready to build your CAB project! Build the solution. This will create the CAB file in the project output folder (shown as follows). At this point, you can also try to copy the CAB file into your mobile device (over an ActiveSync connection) and run it from there. This will automatically deploy the files you've included in the CAB.
[ 431 ]
Packaging and Deployment
This CAB file is useful, but your users still need to manually copy it into the mobile device to run it. After running the CAB file in the mobile device, you will see the screen shown as follows, indicating that the installation has successfully completed.
In the following section, let's take a look at how you can create an automatic installer that automatically installs the CAB file into the mobile device without doing a manual copy. This can be achieved through an MSI file.
Deploying your solution as an MSI file
An MSI file has the .msi extension. This deployment method, unlike a CAB file, is initiated from a PC. An MSI installation still requires an ActiveSync connection to be readily established between the mobile device and the PC, but it does away with the tedious process of having to manually copy the CAB files to your mobile device. The MSI installer also provides the standard wizards that make the deployment of your mobile application "feel" more like a typical PC application installation. The process involved in creating an MSI file is a bit different from that of a CAB file. In a nutshell, you need to do the following: •
Create an INI file ActiveSync handles mobile application installations initiated from the PC via a tool called CeAppMgr.exe. In this first step, you must create an INI file that informs CeAppMgr.exe about your application.
•
Create a Custom Action DLL You need to programmatically launch CeAppMgr.exe and pass your INI file to it in order to register your application with ActiveSync. One way to do it is via a custom action DLL. The MSI project allows you to insert custom actions (through DLL files) so that you can run custom code at certain points in time during the installation.
•
Create an MSI project The last step is to create the MSI project itself, and to include all the necessary files. [ 432 ]
Chapter 13
Creating an INI file
As mentioned earlier, all mobile application installations initiated from the PC are handled by ActiveSync (or more particularly, a component called CeAppMgr.exe). Your deployment project must make your application known to ActiveSync by passing an INI file to CeAppMgr.exe. You can create this file manually using any text editor program. The contents of this file should look something like the following: [CEAppManager] Version = 1.0 Component = SalesForceApp [SalesForceApp] Description = Sales Force Application CabFiles = SalesForceApp.cab
The first two lines denote that this is a CEAppManager INI file, and are usually fixed. The Component keyword specifies the name of your application. This same name must then be declared in its own section (on the fifth line). The Description keyword specifies the description of your application (this will be shown on the screen when you run the installation wizard). In the last line, CabFiles denote the CAB files included in this installation. Save this INI file as SalesForceApp.ini. In the next section, you will pass this file to the CeAppMgr.exe program in a custom installation action DLL.
Creating the custom action DLL
The MSI installer, like most installer tools, allows you to write custom code that runs at certain points in time during the installation. This allows the developer to do additional things like registering a freshly installed web site with IIS, or to register a file with the Global Assembly Cache (GAC), or even pop up your own configuration windows after an installation is complete. This custom code is usually written as a DLL and then "hooked" onto MSI installation events such as BeforeInstall (before an installation) or AfterInstall (after an installation). In your case, before the installation proceeds, you need to programmatically launch the CeAppMgr.exe file and pass it to the INI file you've created earlier. You can make use of the custom action feature of the MSI installer to do this.
[ 433 ]
Packaging and Deployment
Your first step is to create the custom action DLL (your code will go in there). Add a new Class Library project to the SalesForceAppInstaller solution you've created earlier. Name this library SalesForceCustomAction.
Instead of the usual class, you will need to add a new type of class to your project—an Installer Class. Name this class CustomActionInstaller.
[ 434 ]
Chapter 13
Remember to also remove the auto-generated Class1.cs file from the project. Now, open the CustomActionInstaller class. You will first notice that this class inherits from the Installer class, denoting that this is an MSI installer class. In the constructor of this class, you need to register your own handlers against the various MSI installation events: public CustomActionInstaller() { InitializeComponent(); //Before an installation this.BeforeInstall += new InstallEventHandler(BeforeInstallationHandler); //After an installation this.AfterInstall += new InstallEventHandler(AfterInstallationHandler); //Before an uninstallation this.BeforeUninstall += new InstallEventHandler(BeforeUninstallationHandler); }
Let's first take a look at the first handler: BeforeInstallationHandler. What you need to do in this event handler is: 1. Get the path of the ActiveSync folder from the registry of the PC 2. Copy your installation files into the ActiveSync folder 3. Get the path of the CeAppMgr.exe program from the registry of the PC 4. Launch CeAppMgr.exe and pass it your INI file as an argument The path of the ActiveSync folder is stored in the following registry entry: SOFTWARE\Microsoft\Windows CE Services\InstalledDir
To retrieve the path of the ActiveSync folder, you can write the following code. Take note that when you return the installation path, you include the name of your application (highlighted as follows). This name must be the same name that you've configured under the Component keyword of the INI file earlier. private string GetInstallationPath() { RegistryKey _ActiveSyncKey = Registry.LocalMachine.OpenSubKey [ 435 ]
Packaging and Deployment ("SOFTWARE\\Microsoft\\Windows CE Services"); if (_ActiveSyncKey != null) { string _ActiveSyncPath =(string) _ActiveSyncKey.GetValue ("InstalledDir"); _ActiveSyncKey.Close(); return _ActiveSyncPath.TrimEnd('\\') + "\\SalesForceApp"; } else { return ""; } }
You also need to import the following libraries to work with registry keys and file paths: using Microsoft.Win32; using System.IO;
Take note that the full path of the CeAppMgr.exe tool is stored in the following registry entry: SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\CEAPPMGR.EXE
Write the following code in the BeforeInstallationHandler event: void BeforeInstallationHandler(object sender, InstallEventArgs e) { string _installationPath = GetInstallationPath(); //Create a folder for your application in the ActiveSync folder Directory.CreateDirectory(_installationPath); //Copy the installation files into the ActiveSync folder foreach (string _fileItem in Directory.GetFiles(Environment.SystemDirectory + "\\TEMP\\SalesForceApp")) { string _fileName = Path.GetFileName(_fileItem); string _targetPath = Path.Combine(_installationPath, _fileName); File.Copy(_fileItem, _targetPath, true);
[ 436 ]
Chapter 13 } //Get the full path of the CeAppMgr.exe program RegistryKey _CeAppMgrKey = Registry.LocalMachine.OpenSubKey ("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\CEAPPMGR.EXE"); string appMgrPath = (string)_CeAppMgrKey.GetValue(null); _CeAppMgrKey.Close(); //Launch CEAppMgr.exe and pass in the full path of your INI //file as an argument System.Diagnostics.Process.Start(appMgrPath, "\"" + Path.Combine(_installationPath, "SalesForceApp.ini") + "\""); }
You're done! Now, for the AfterInstallationHandler event. In this event, you just need to do some cleaning up after yourself—you need to delete the temporary files from the following folder: Environment.SystemDirectory + "\\TEMP\\SalesForceApp"
You can write the AfterInstallationHandler event as follows: void AfterInstallationHandler(object sender, InstallEventArgs e) { foreach (string _fileItem in Directory.GetFiles(Environment.SystemDirectory + "\\TEMP\\SalesForceApp")) { File.Delete(_fileItem); } }
Last is the BeforeUninstallationHandler event. When someone uninstalls your MSI file from the PC, it doesn't automatically remove the files from the ActiveSync folder. This makes sense, considering that those files were put there via custom code. You must, therefore, provide custom code to do the opposite—removing these files from the ActiveSync folder during an uninstallation of the MSI. This can be done using the following code: void BeforeUninstallationHandler(object sender, InstallEventArgs e) { string _installationPath = GetInstallationPath(); foreach (string _fileItem in Directory.GetFiles(_installationPath))
[ 437 ]
Packaging and Deployment { File.Delete(_fileItem); } Directory.Delete(_installationPath); }
We now move on to the last section—creating the MSI project itself.
Creating the MSI installer project
Visual Studio provides a template to create MSI installers called the Setup Project. In the same solution that you've created earlier, add another project. Look for the Setup Project template under the Other Project Types | Setup and Deployment category. Name the project SalesForceAppInstallerMSI. Again, try to add an MSI suffix to the project name to differentiate it from the CAB project.
[ 438 ]
Chapter 13
Add the SalesForceCustomAction project you've created in the previous section to the solution— you will need to reference this project later. Now you must define what to include in your MSI project. Right-click on the MSI project and choose the Add | Project Output menu item.
Select the SalesForceAppInstallerCAB project from the drop-down list in the ensuing pop-up dialog. Ensure the Built Outputs item is selected, and click OK to add it to the list. This tells the MSI project to include the CAB output from the CAB project you've created earlier. After this, you need to hook the MSI project to the custom action DLL you've created earlier. You can do this by right-clicking on the MSI project, and choosing the View | Custom Actions menu item (shown as follows):
[ 439 ]
Packaging and Deployment
When you've done this, you will see a new dialog window pop up. Open the Application Folder item, and click the Add Output… button. The familiar Add Project Output Group dialog window will pop up. Choose the SalesForceCustomAction project, and choose the Primary output output group. When you've clicked the OK button, you should now see the following window:
Click the OK button again to close this window. You will now see that Visual Studio has automatically hooked up the custom action handlers in your DLL to the various installation events. You can see this in the Custom Actions tab:
[ 440 ]
Chapter 13
The last step is to ensure that your INI file is included together with the MSI project. To do this, right-click on your MSI project, choose the Add | Existing Item menu item and browse for your INI file. After doing this, you should see the following screen:
Your MSI installer is now complete! Build the solution. If you navigate to the output folder of your MSI project, you will now find your generated SalesForceAppInstaller.msi file. You can try out your MSI file by first ensuring that your mobile device/emulator is connected to your PC through ActiveSync, and then running the MSI file. You should see the wizard shown as follows, which will automatically install your .CAB file to the mobile device over an ActiveSync connection.
[ 441 ]
Packaging and Deployment
Creating an automated update service
Many applications on the mobile device today come with tools that let the user download patches or latest updates from a remote server (when an Internet connection is available) and directly install them on the mobile device. This can help to take the load of maintenance and upkeep of the mobile application off the systems administrator. An automated update service ensures that mobile end users can freely apply patches and updates to their mobile device at the click of a button. Your application should be no different. In a nutshell, this auto-update service will consist of two parts, one part being the server side and the other the mobile device side. At the server side, you will use web services. Your web service will do the following: •
Host the latest updates (in the form of CAB files)
•
Provide a method that the mobile device code can call to retrieve information on the latest updates (such as version number, download size of the update, and so on)
At the mobile device side, your updater service should do the following: 1. Call the web service to retrieve information on the latest updates 2. Check the current version number of the application against the latest version number retrieved from the web service and decide whether an update is required 3. If an update is required, download the new CAB file and automatically deploy it 4. Update the application's current version number on the mobile device
Creating the server-side web service
We choose web services because it is relatively easy to set up and connect to. Create an ASP.NET web service project and name it SalesForceAutoUpdateService. Add a new Web Service (.asmx) file to your project named UpdateService.asmx, and delete the automatically created Service1.asmx file. You will need to store information about the latest application updates somewhere on the server. Setting up a whole database for this seems like overkill, so the best way is to store it in an XML file. Add a new XML file (named SalesForceAppVersion.xml) to your project. The XML file only needs to store information about the latest version of the application. It is recommended to store at least the following details: [ 442 ]
Chapter 13
•
CAB filename
•
Latest version number
•
Latest version date
•
Name of application
Write the following contents in the XML file: <Applications> <SalesForceApp UpdatesCabFile="salesforceapp2.0.cab" LatestVersion="2" VersionDate="12/Jan/2010" />
Every time you need to make a new version of the application available to all your mobile users, you will need to place a copy of the latest CAB file on your server and then update this XML file. Now let's create a web method that reads from this XML file and returns the information to the calling program. Take note that this web method assumes that the CAB file containing the updates is stored in a folder named \UpdateFiles in your web service folder. The information about the latest updates is returned as a comma-separated string of values. [WebMethod] public string GetApplicationVersionInfo(string ApplicationName) { try { String _data=""; //Load the XML file XmlDocument _xml = new XmlDocument(); _xml.Load (AppDomain.CurrentDomain.BaseDirectory.TrimEnd('\\') + "\\SalesForceAppVersion.xml"); //Retrieve the application node XmlNodeList _xmlNodeList = _xml.GetElementsByTagName("Applications"); XmlElement _xmlAppRootNode = (XmlElement)_xmlNodeList.Item(0); XmlNodeList _xmlAppNodeList = _xmlAppRootNode.GetElementsByTagName (ApplicationName); [ 443 ]
Packaging and Deployment if (_xmlAppNodeList.Count == 0) return ""; XmlElement _xmlApplicationNode = (XmlElement)_xmlAppNodeList.Item(0); //Get the full path to the CAB file on the server String _cabFileName = _xmlApplicationNode.GetAttribute("UpdatesCabFile"); String _cabFilePath = AppDomain.CurrentDomain.BaseDirectory.TrimEnd('\\') + "\\UpdateFiles\\" + _cabFileName; //Calculate the filesize of the CAB file FileInfo _file = new FileInfo(_cabFilePath); String _packageSizeInBytes = Convert.ToString(_file.Length); //Place all the details in a comma-separated string and //return this string _data += _xmlApplicationNode.GetAttribute("LatestVersion") + "," + _packageSizeInBytes + "," + _xmlApplicationNode.GetAttribute("VersionDate") + "," + _cabFileName; return _data; } catch (Exception) { return ""; } }
That's all you need for the web service. Your next step is to place a dummy .CAB file in the <Webservicefolder>\UpdateFiles folder. Copy the CAB file you've created earlier into the UpdateFiles folder. Rename this CAB file salesforceapp2.0.cab. Register this web service with IIS by creating a virtual folder that points to this web service. Test your web service by navigating to the following URL in any browser on your PC. http://localhost/SalesForceAutoUpdateService/UpdateService.asmx
You should be able to see your web service and its list of methods show up in your browser. You can even test out your web method. Click on the GetApplicationVersionInfo link, key in SalesForceApp as the Application argument, and click the Invoke button. You will see the following data returned: [ 444 ]
Chapter 13
In the next section, you will learn how to create the client-side part of the equation.
Creating the client-side updater tool
The updater service at the client-side is a separate application with an interface that allows the user to trigger the update process. Create a new Smart Device Application project, and design the following form:
[ 445 ]
Packaging and Deployment
The gray panel (pnlRemoteFiles) is initially invisible. When a user clicks the Check for Updates button, this will initiate a check against the web service on a remote server. It will check if the local copy of the application needs an update (by checking the local version number against the latest version number available from the web service). If an update is needed, the gray panel is shown (together with the details of the latest updates). The user can then download and install these updates by clicking the Download & install updates button. There is also a progress bar below this button that shows the file download progress. You will also need a place to store the current application version on the mobile device. You can use an XML file for this purpose. Add a new XML file to your project and name it SalesForceAppDeviceVersion.xml. Write the following contents in this file: <Applications> SalesForceApp CurrentVersion="1" />
In your project, ensure that you set the Copy to Output Directory property to Copy always in the property window of the XML file (as shown below).
As the updater service will need to connect to the remote web service on your PC, you need to add a web reference to the web service. Right-click on the project and click the Add Web Reference menu item. Key in the full URL to the web service in the pop-up window. Take note that this web service will be called from your mobile device, so you should avoid using localhost in the URL. Refer to the web service using the full machine name of your PC (shown in the next screenshot). Name this web reference AutoUpdater.
[ 446 ]
Chapter 13
In your form, you first need to declare a few global variables to store version details: using System.Net; using System.IO; //Local version information private int _currentVersion; //Remote version information private int _remoteVersionNo; private DateTime _remoteVersionDate; private string _updatesCabFile; private int _updatesCabFileSize;
[ 447 ]
Packaging and Deployment
In the load event of the form, you need to parse the XML file and retrieve the local version information. public void Updater_Load(object sender, System.EventArgs e) { //Get Local Version information LoadLocalVersion(); } private void LoadLocalVersion() { XmlDocument _xml = new XmlDocument(); _xml.Load(GetCurrentApplicationPath().TrimEnd("\\".ToCharAr ray()) + "\\SalesForceAppDeviceVersion.xml"); XmlNodeList _xmlNodeList = _xml.GetElementsByTagName("Applications"); XmlElement _XmlAppRootNode = (System.Xml.XmlElement) (_xmlNodeList.Item(0)); XmlNodeList _xmlAppNodeList = _XmlAppRootNode.GetElementsByTagName("SalesForceApp"); XmlElement _xmlApplicationNode = (System.Xml.XmlElement) (_xmlAppNodeList.Item(0)); _currentVersion = int.Parse( _xmlApplicationNode.GetAttribute("CurrentVersion")); lblCurrentVersion.Text = _currentVersion.ToString(); }
In the click event of the Check for Updates button on your form, you need to call the web service, retrieve the latest version information, and then compare it against the local version information retrieved earlier. You can do this via the following code: public void btnCheckUpdates_Click(System.Object sender, System.EventArgs e) { AutoUpdater.UpdateService _service = new AutoUpdater.UpdateService(); string _data; string[] _versionInfo; //Call the web service _data = _service.GetApplicationVersionInfo("SalesForceApp"); //Since the returned data is a comma-separated string, we //split it into its constituent data fields [ 448 ]
Chapter 13 _versionInfo = _data.Split(','); _remoteVersionNo = int.Parse(_versionInfo[0]); _updatesCabFileSize = int.Parse(_versionInfo[1]); _remoteVersionDate = DateTime.Parse(_versionInfo[2]); _updatesCabFile = _versionInfo[3]; //Checks the version retrieved from the webservice against //the current version if (_remoteVersionNo > _currentVersion) { //If a latest update is available, display its //details in the gray panel and make it visible lblLatestVersion.Text = _remoteVersionNo.ToString(); lblDownloadSize.Text = String.Format("{0:0,0}",_updatesCabFileSize) + " bytes"; lblVersionDate.Text = _remoteVersionDate.ToString(); pnlRemoteFiles.Visible = true; } else { //If an update is not required, display a message MessageBox.Show("Your SalesForce application is already up to date"); } }
When the user clicks the Download & install updates button, you will need to download the update CAB file from the remote server. You can programmatically download a file using the System.Net.HttpWebRequest class. Using this class consists of the following steps: 1. Call HttpWebRequest.Create(URL) to create a request object on the desired file specified by the URL 2. Call HttpWebRequest.GetResponse() to get a response object 3. Call WebResponse.GetResponseStream().BeginRead() and WebResponse. GetResponseStream().EndRead() repeatedly to download chunks of the file asynchronously
[ 449 ]
Packaging and Deployment
You first need to declare a few additional global variables for the web request. //Variables used to download the updates CAB file private byte[] _downloadBuffer; private int _downloadChunkSize = 65536; private int _totalBytesDownloaded = 0; private FileStream _fileStreamObj; private WebResponse _webResponse; private HttpWebRequest _webRequest; private string _localCabPath;
In the click event of the Download and install updates button, write the following code: public void btnDownloadAndInstall_Click(System.Object sender, System.EventArgs e) { _webRequest = (HttpWebRequest) (HttpWebRequest.Create("http://edzehoopc/SalesForceAutoUpdateService/updatefiles/" + _updatesCabFile)); _webResponse = _webRequest.GetResponse(); //Initialize the download buffer. The download buffer //ideally should be a 64Kb chunk _totalBytesDownloaded = 0; _downloadBuffer = new byte[_downloadChunkSize + 1]; //Initialize the progressbar pbDownload.Minimum = 0; pbDownload.Maximum = _updatesCabFileSize; pbDownload.Value = 0; //Initialize the File stream object that receives the file //data _localCabPath = GetCurrentApplicationPath().TrimEnd("\\".ToCharArray()) + "\\" + _updatesCabFile; _fileStreamObj = new FileStream(_localCabPath, FileMode.Create); //Start the asynchronous file download – download the first //64Kb chunk _webResponse.GetResponseStream().BeginRead(_downloadBuffer, 0, _downloadChunkSize, new AsyncCallback(DataReceived), this); }
[ 450 ]
Chapter 13
In the previous code, you are referencing an asynchronous callback function named DataReceived. This callback function is automatically invoked when the amount of data specified (via _downloadChunkSize) has been downloaded successfully. In this callback function, you retrieve the downloaded data, write it to the file stream object, update the progress bar, and then initiate another asynchronous read (of the next 64 KB chunk of data). This process repeats itself until there is no data left to read. private void DataReceived(IAsyncResult Data) { int _downloadedSize = _webResponse.GetResponseStream().EndRead(Data); //Write the downloaded data into the file stream object _fileStreamObj.Write(_downloadBuffer, 0, _downloadedSize); _totalBytesDownloaded += _downloadedSize; //Update the progress bar pbDownload.Invoke (new EventHandler (UpdateProgress)); //Check if all bytes downloaded if (_totalBytesDownloaded < _updatesCabFileSize) { //Download the next chunk _webResponse.GetResponseStream().BeginRead (_downloadBuffer, 0, _downloadChunkSize, new AsyncCallback(DataReceived), this); } else { //Close the file _fileStreamObj.Close(); //Asks the user for confirmation if he or she wants to //install the downloaded updates if (MessageBox.Show("Updates downloaded. Would you like to install the updates now?", "Install?", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1) == System.Windows.Forms.DialogResult.Yes) { //Launch the cab file Process.Start(_localCabPath, "");
[ 451 ]
Packaging and Deployment //Update local version information UpdateLocalVersion(); } } } //Updates the progress bar private void UpdateProgress(Object sender, EventArgs e) { pbDownload.Value = _totalBytesDownloaded; Application.DoEvents(); } //Retrieves the current application path private string GetCurrentApplicationPath() { string _path = System.Reflection.Assembly.GetExecutingAssembly() .GetName().CodeBase; return System.IO.Path.GetDirectoryName(_path); }
Your updater service is now complete! To test this application, ensure that an ActiveSync connection is properly set up first. When your application loads, you will notice that the current version is set to 1. Clicking the Check for Updates button retrieves the latest version information from the web service and displays it in the gray panel (shown as follows):
[ 452 ]
Chapter 13
When you click the Download & install updates button, this will initiate the download process. Once the download is complete, you will be prompted to install the updates (shown in the following screenshot).
After installing the CAB updates, the current version number in your XML file is automatically updated. If you launch the application again from your mobile device, you will notice that the current version now displays the value 2. Don't launch the application from Visual Studio this time, as doing so will deploy the original XML file over to your mobile device and overwrite the copy on the device.
If you click Check for Updates now, it will prompt you with a message notifying that the application is already up to date.
[ 453 ]
Packaging and Deployment
Summary
You've come all the way from scratch across the full spectrum of development, testing, and deployment, and you're now fully equipped to build rich data-driven applications for the .NET Compact Framework! In this final chapter of the book, you've learned how to create robust packaging and deployment solutions that make deployment an easier task for your end users. You've learned the following in this chapter: •
How to create a CAB installer project
•
How to create an MSI installer project
•
How to create custom actions (in a class library) and link it up to various installation events in an MSI project
•
How to create an automated update web service, and an update tool (on the mobile device) that is version aware
[ 454 ]
Index Symbols
A
.Dispose() method 306 .NET CF 3.5 App Configuration tool 417, 418 CLR Profiler tool 405 GC Heap Viewer tool 403, 404 Network Log Viewer tool 423, 424 power toys 394 power toys, installing 394, 395 Remote Logging Configuration tool 421-423 Remote Performance Monitor tool 395 ServiceModel Metadata tool 419-421 tools 394 .NET CF 3.5, tools App Configuration tool 394 CLR Profiler tool 394 Network Logging Viewer tool 394 Remote Logging Configuration tool 394 Remote Performance Monitor tool 394 ServiceModel Metadata tool 394 .NET Compact Framework about 323 culture-sensitive forms, designing 326-330 culture information, retrieving 330 garbage collector 304 Smart Device project, creating 338 _PluginObject variable 52 HTML Keyword Extractor 173, 174 Microsoft SQL Server and Microsoft Synchronization Services setting up 242
account sharing, between devices 220, 221 sharing between two devices, Bluetooth used 227-229 sharing between two devices, Infrared (IrDA) used 222-226 accounts listing page, mobile sales force application building 142 context menu, creating for paging user control 150, 151 launching 151, 152 Oracle Lite, paging in 146, 147 paging user control, building 147-150 SQL Server CE, paging in 143-146 testing 152 AccountSummary class 165 AccountSummaryCollection class 164 AccountViewer form, mobile sales force application building 109, 110 data binding .NET controls, to business objects 111 launching 111-113 testing 114 AddPlugin function 47 ADO.NET command object 70 ADO.NET dataset object 70 Advanced Encryption Standard. See AES AES used, for encrypting inter-device transmission 319
file class 102 FileCollection class 102 history class 102 HistoryCollection class 102 LeadAccount class 102 OpportunityAccount class 102 product class 102 ProductCollection class 102 task class 102 TaskCollection class 102
algorithm CRC32 311 MD5 311 SHA1 311 SHA256 311 SHA384 311 SHA512 311 App Configuration tool, .NET CF 3.5 Application Policy tab 419 Device Policy tab 418 uses 417 Application class 185 asynchronous web service call 339 automated update service client-side updater tool, creating 445-453 creating 442 server-side web service, creating 442-444
C
B BaseAccount.InterestedProds field 136 BaseAccount.Validate() function 110 BaseAccount class 108 BeginGetChartDataCallback() method 349, 355 BeginInvoke() method 361 BeginTransaction() method 72, 82 BeginUpdate method 302 Binary Large Object (BLOB ) field 281 BinaryReader class 304 BinaryWriter class 304 BindingSource control 111, 116 Bluetooth used, for transferring account between two devices 227-229 btnNewFile_Click function 234 btnSave_Click() function 218 BuildSelectClause() function 181, 184 BuildWhereClause() function 160-163, 177, 181, 183 business objects, mobile sales force application BaseAccount class, code 105 Baseobject class 102 creating, to encapsulate DataSets 100-107 CustomerAccount class 102 data, validating 108, 109
CAB file about 425 miscellaneous settings, configuring 430-432 SalesForce application files, adding 427-430 solution, deploying as 426 callback function 451 CancelEdit() method 117 CHARINDEX() function 176, 182 CHARINDEX(sequence,expression) T-SQL function 176 Class attribute 334 client-side updater tool, automated update service 445-453 CLR Profiler tool, .NET CF 3.5 about 405 Allocation Graph, inspecting 411, 412 application, launching 409 Call Tree view, inspecting 415-417 Histogram view, inspecting 410, 411 sample application 406-409 Time Line view, inspecting 412-415 code performance .NET CF code performance, measuring 286, 288 application performance statistics, capturing 289-292 measuring 286 Commit() function 74 Computer Management panel used, for creating queue manually 378 connected state 100 Copy to Output Directory property 446 CRC32 algorithm 311 CreateCommand method 81
[ 456 ]
CreateDatabase method 68, 78 CreateSalesForceDatabase method 54, 65, 78 CRMLive.NET data flow 12, 13 CRMLiveDataCacheServerSyncProvider class 255 CRMLiveFramework project about 39 building 39, 40 IDataLibPlugin interface 39, 43 IDataLibPlugin interface, defining 40, 41 namespace, changing 40 PluginCollection class 39 PluginManager class 39 custom action DLL, MSI file ActiveSync folder, path 435 AfterInstallationHandler event 437 BeforeUninstallationHandler event 437 creating 433-435 CustomActionInstaller class 435
D dashboard charts 334 overview 333, 334 rendering 27 road show revenue 27 sales, for last three months 28 total monthly sales 28 web service, creating 334-337 XML data, structure 337 dashboard smart client about 338, 340 AnalyzeData() function 342 AsyncCallback object 340 asynchronous web service call 339 asynchronous web service method call 340 bar chart, creating 356-358 BeginGetChartDataCallback() method 349 BeginGetChartData method 340 BeginInvoke() 350 BeginInvoke() method 361-363 emulator environment set up, ensuring 341 EndGetChartData method 340 GDI+ methods 345 GetChartData web method 340
line chart, creating 341-345 LoadLineChart() 345-348 LoadLineChart method 343 OnPaint() event 358, 361 RefreshAllCharts method 340 round gauge, creating 350-355 synchronous web service method call 340 web service, connecting to 338-341 data encrypting for inter-device transmission, AES used 319-322 subsetting 279 data, mobile sales force application exchanging, bluetooth and IrDA used 24 manipulating, on mobile device 20 retrieving, on mobile device 20 transferring, between mobile devices 20 transferring, between mobile devices 20 DataAdapter class 249 database encrypting 307 Oracle Lite database, encrypting 309 SQL Server CE database, encrypting 308 database performance database optimization, tips 297 data caching 293-295 optimizing 292 search performance boosting, database indexes used 295, 296 database plugins building 59, 60 Oracle Lite database browsing, Msql used 80 Oracle Lite plugin, implementing 75 SQL Server CE Plugin, implementing 60 DataGrid control, mobile sales force application 20 DataService.asmx file GetChartData method 335 Datasource property 139, 342 data synchronization about 25 frameworks, differences 241 methods 238 data tier building 38 designing 31, 32
[ 457 ]
data transfer performance optimizing 297-300 DateTimePicker control 116 DeserializeDataset method 298 disconnected state 100 Dispose method 396 DLL 31 DoSomeIntensiveWork method 414 double byte languages Japanese character input, supporting in Windows Mobile 324 supporting 323 Unicode, supporting at application level 324, 326 DrawLine (Pen pen, int X1, int Y1, int X2, int Y2), GDI+ methods 345 DrawString (String s, Font f, Brush brush, int X, int Y), GDI+ methods 345 dual database support, mobile sales force application 25 Dynamic Link Library. See DLL
E EmailMessage class 195 EndEdit() method 117 EndGetChartData method 340 EndUpdate method 302 Environment.TickCount .NET method 286
F file attachments, mobile sales force application FileDetailViewer form, building 126-128 file manager class, building 121-123 FileUpload user control, building 123-125 handling 120, 121 physical files, storing in database 121 physical files, storing in filesystem 121 upload functionality, testing 128 FileDetailViewer form building 126-128 FileManager class 125 files managing 304 synchronizing, with server 281, 283
FileStream class 304 FileUpload.Datasource property 127 FileUpload user control 123 building 123 Fill() method 70 FillRectangle (Brush brush, int X, int Y, int Width, int Height), GDI+ methods 345 FillRectangle method 356 Find() function 217 form navigation class, mobile sales force application 94-94 full text search about 21, 22, 155, 169-171 building 171 file, indexing 174, 175 forms, creating 185-189 HTML Keyword Extractor 173, 174 indexing operation 169 Keyword Extractor classes, creating 172 query, creating for Oracle Lite 182-185 query, creating for SQL Server CE 175-182 retrieved Dataset encapsulating, business objects used 185 search engine, improving 190, 191 searching operation 169 trying 190
G garbage collector, .NET Compact Framework about 305 allocation threshold 304 application, moving to background 304 explicit GC.Collect() call 305 object, lifeycle 305, 306 GC.Collect() call 305 GC.Collect() function 305 GC Heap Viewer tool, .NET CF 3.5 about 395 memory leak, resolving 404 using, to detect memory leaks 403, 404 GDI+ chart bar chart, creating 356-363 line chart, creating 341-350
[ 458 ]
round gauge, creating 350-356 GDI+ methods DrawLine (Pen pen, int X1, int Y1, int X2, int Y2) 345 DrawString (String s, Font f, Brush brush, int X, int Y) 345 FillRectangle (Brush brush, int X, int Y, int Width, int Height) 345 Get() method 139 GetAccountDetails() method 81, 129 GetAccountFilesBySearchPhrase() function 184 GetAccountFilesCountBySearchPhrase() function 185 GetAccountsByParameters() function 160-163 GetAccountsByType method 288 GetAccountsCountByParameters() function 160-164 GetChartData method 335 GetProductList() function 294 GetProductList() method 136 GetReceivedData() function 227 GlobalArea class 32, 293 global lists, mobile sales force application maintaining 19 Globally Unique IDentifier. See GUID GlobalVariables class 201 GUID 74 GUIDToHex() function 84 GUIDToNative() method 75 GZipStream class 297
H HexToGUID() function 84 Hide method 303 HighlightKeywords() function 188 HistoryList class 131 History tab, mobile sales force application about 200 code, testing 215 data tier functions, creating to insert historical records 201-204 incoming SMS messages, intercepting in background 208-211
InsertHistoricalRecord() function 201 InsertHistoricalRecordByPhone() function 201 outgoing SMS messages, handling 212-215 phone calls, intercepting in background 208-211 phone functionality, encapsulating 206, 207 SMS functionality, encapsulating 204-206 HTMLKeywordExtractor class 173
I IComparer interface collection sorter, building 130, 131 IDataLibPlugin interface 31, 39, 143 IDE 326 IgnoreComments property 304 IgnoreWhiteSpace property 304 IIS 334 IKeywordExtractor interface 171 ImageList instance 96 IME 324 IndexFile() function 174 indexing 169 Infrared (IrDA) used, for transferring account between two devices 222-226 INI file, MSI file creating 433 Initialization Vector (IV), Rijndael encryption 320, 321 InitializeCommands() method 249 Input Method Editor. See IME Installer Class 434 Integrated Development Environment. See IDE interface 41 Internet Information Server. See IIS IrDAClient class 222 IrDAListener class 222 ItemId property 219
J JobClass class 385 JobClass object 381
[ 459 ]
K Key Performance Indices. See KPIs KeywordExtractorBase class 172 Keyword Extractor interface 172 KPIs 11
L LargeImageList property 96 LeadAccount object 112 list control, mobile sales force application collection sorter building, IComparer interface used 130, 131 custom control, building 131-135 History list control, suing 135 History list control, testing 136 ListView control 90 LoadAllPlugins function 49, 50 LoadAllProducts function 407 LoadAllProducts method 416 LoadBarChart() method 359 LoadDashboard() method 348, 355 LoadGauge() method 353 LoadLineChart method 343 LoadProducts() method 138 Local Database Cache 239 Localizable property 326
M Main() method using, as startup object 98, 100 main menu, mobile sales force application building 96-98 Main() method, using as startup object 98-100 MAPI 198 Massively Multiplayer Immersive Game. See MMIG Master 241 MD5 algorithm 311 message sending, from mobile device to server 389 sending, from server to remote mobile device 379 Message class 374 MessageInterception class 199
MessageInterceptor class 198 Message object 374 MessageQueue.GetAllMessages function 386 MessageQueue.ReceiveByID method 386 MessageQueue.Receive method 375 MessageQueue object 374 message sending, mobile device to server client-side code, creating 389, 390 server-side code, creating 390, 392 message sending, server to remote mobile device client-side application, creating 385-388 data, sending to remote queue 379 server-side application, creating 380-385 messaging 15 Messaging API. See MAPI MessagingApplication.DisplayComposeForm() function 196 Microsoft .NET Compact Framework 87 Microsoft.Synchronization.Files namespace 282 Microsoft.Synchronization namespace 282 Microsoft Message Queuing. See MSMQ Microsoft Messaging Queue Service. See MSMQ Microsoft SQL Server CE data synchronization, methods 238 merge replication 238 Microsoft Synchronization Services 239 SQL Remote Data Access (SQLRDA) 238 Microsoft Synchronization Services about 239-242 and Microsoft SQL Server setting up 242 client project, configuring 250 conflict, resolution 255, 256 CRMLive server tables, creating 243 sync code, writing 253, 254 WCF service, creating 243 WCF service library, configuring 246-248 MMIG 10 mobile application database, encrypting 307 default Windows Mobile Compose UI, delegating to 196 deploying, ways 13 e-mail sending, POOM used 195, 196 [ 460 ]
incoming phone calls, detecting 199, 200 messaging 15 Oracle Lite database, encrypting 309 phone calls, placing 198, 199 publishing, to mobile server 273, 274 smart clients 14 SMS sending, POOM used 194 SQL Server CE database, encrypting 308 strengths 15, 16 thick clients 14 thin clients 13, 14 types 13 users setting up, WebToGo portal used 275-278 mobile dashboard application about 11, 25 dashboard, rendering 27, 28 stateless web services, using as data sources 26, 27 Mobile Database Workbench new mobile project, creating 262, 263 publication, adding to project 270-272 publication, creating 262 publication items, adding to project 264-266 sequence items, adding to project 268, 270 mobile device message, sending to server 389 registering, Oracle Mobile Server used 279, 280 mobile sales force application about 10, 16 accounts listing page, building 142, 143 AccountViewer form, building 109, 110 application, maintenance 25 application, upgrades 25 authenticating 309 authentication code, writing 312-317 building 88 business objects, creating to encapsulate DataSets 100-107 culture-sensitive forms, designing 326-330 culture information, retrieving 330 data, manipulating on mobile device 20 data, retrieving on mobile device 20 data, transferring between multiple mobile devices 20
data exchanging, bluetooth and IrDA (Infrared) used 24 data synchronization 25 dual database support 25 file attachments, handling 120 form navigation class, building 94 full-text search functionality 21, 22 globalizing 323 global lists, maintaining 19 handwritten input, capturing 24 History tab, populating 200, 201 incoming phone calls, detecting 23 incoming SMS, detecting 23 list controls 129 login form, loading 317-319 login screen, creating 310 main menu 88-93 main menu, building 96-98 mobile screen, design 18 one-way encryption performing, SHA256 used 310-312 ProductList control, building 136-139 requisites 11 reusable controls, creating 19 task management 20 Tasks list, building 114 Windows Mobile and hardware, integrating with 22 Windows Mobile Calendar and Contacts book, integrating with 23 mobile screen design, mobile sales force application 18 mobile server mobile application, publishing 273, 274 mobile support case application about 12, 28 messaging backbone building, MSMQ used 29 requisites 12 MSI file about 425 creating, steps 432 custom action DLL, creating 433-438 INI file, creating 433 MSI installer 432 MSI installer project, creating 438-441 solution, deploying as 432
[ 461 ]
MSI installer project creating 438-441 MSMQ about 29, 365, 366 application, writing 371-376 data transmission, protocols 367 login information 371 messages 367 outgoing queue 368 queue creating on server, Computer Management panel used 378 queues 367 setting up, on mobile device 369-371 setting up, on server 377, 378 used, for building messaging backbone 29 MSMQ Command Name enable srmp 370 register 370 start 370 MyLargeBusinessObject class 400
N National Security Agency. See NSA NativeToGUID() method 75 NavigationService class 95, 96, 113, 167 network aware synchronization modules creating 283, 284 Network Log Viewer tool, .NET CF 3.5 using 423, 424 NewLead() method 112 NewTask() method 107 non-blocking call 339 NSA 311
O OpenFileDialog control 123 OracleCommand class 43 OracleConnection class 77, 81 OracleDataAdapter class 81 Oracle Lite data synchronization, methods 240 double byte languages, handling 323 Oracle Lite Mobile Server 240, 241 Oracle Lite 10g installing, on development machine 37 installing, on Pocket PC device 38
Oracle Lite 10g, installing on Pocket PC device 38 setting up 37 versus SQL Server CE 3.5 32 Oracle Lite database browsing, Msql used 80, 81 data, manipulating 81-84 data, retrieving 81 encrypting 309 GUID values 84 Oracle Lite plugin class, building 77 DDL, storing in resource file 75, 76 Oracle Lite, connecting to 77 Oracle Lite database, creating 78, 79 OracleLitePlugin project 39 Oracle Mobile Server about 240, 241 CRMLive server tables, creating 260, 261 Oracle Database Enterprise 11g, installing 257 Oracle Mobile repository, creating 258, 259 publication creating, Mobile Database Workbench used 262 synchronizing with 280, 281 used, for registering mobile device 279, 280 using 256 OutlookSession class 217-219
P paging user control building 147, 148 parameterized search about 155, 156 building 157, 159 features 156 query, creating in Oracle Lite 163, 164 query, creating in SQL Server CE 160-163 retrieved Dataset encapsulating, DataSets used 164, 165 parameterized search forms building 165-167 code 168 search results listing form, building 168 PATINDEX() function 176
[ 462 ]
PATINDEX(pattern,expression) T-SQL function 176 phone calls detecting 199, 200 placing 198, 199 plugin class 63 PluginCollection class 45, 46 PluginCollection object 32 PlugInFullName method 63, 77 PluginManager class 32, 46, 47 plugin manager UI building 43 ConfigurePlugin form, building 50 overview 44-46 PluginCollection class, implementing 46 PluginManager class, implementing 46-50 PluginsSetup form, building 54-58 SalesForceApp project, creating 50 testing 58 PluginsSetup form 95 PocketOutlook.Contact class 217 Pocket Outlook Object Model. See POOM POOM used, for sending e-mail 195, 196 used, for sending SMS 194 Product class 412 ProductList control, mobile sales force application about 136 product selection storing, XML DOM used 139-141 testing 142 using 141 Publisher 238
RefreshPluginsList() function 56 Remote database 239 Remote Logging Configuration tool, .NET CF 3.5 using 422, 423 Remote Performance Monitor tool, .NET CF 3.5 about 395 application statistics, viewing in real time 398-400 issues 396 memory leak, sample application 396, 398 memory leaks 395 memory leaks, causes 395 PerfMon, using to graphically view runtime performance statistics 400, 402 RemovePlugin function 48 RemoveTask() function 106 ResourceManager class 60, 66 ResumeLayout method 302 reusable controls, mobile sales force application creating 19 Rijndael encryption. See also AES Rijndael encryption applying, on text message 320 Initialization Vector (IV) 320 Key (password) 320 types 319 RoundGauge property name MaxValue 352 MinValue 352 Value 352 ROW_NUMBER function 144
Q
S
QueryPerformanceCounter function 287 QueryPerformanceCounter method 286 QueryPerformanceFrequency function 287
SalesForceApp project 39 SaveAllPlugins function 48 SaveNewAccount() method 112 searching 169 search parameters passing 158 Secure Hashing Algorithm. See SHA Send method 374 SerializeDataset function 298 Serializers 247
R RefreshAllCharts method 340 RefreshPage() function 149, 168, 188 RefreshPageInfoDisplay() function 150 RefreshPage method 288
[ 463 ]
SerialPort class 227 server message, sending to remote mobile device 379 MSMQ, setting up 377, 378 queue creating manually, Computer Management panel used 378 server-side web service, automated update service creating 442 ServiceModel Metadata tool, .NET CF 3.5 running 419 Smart Device Windows Application project, creating 420 testing 419 SetAccountDetails() function 112 SetAccountDetails method 72 SetupBindings() method 117, 126, 135 SetupListView() method 136 SetupNavigator() function 168 SHA 311 SHA1 algorithm 311 SHA256 algorithm 311 SHA384 algorithm 311 SHA512 algorithm 311 Short Messaging Service. See SMS ShowDialog() function 95, 113 Show method 303 smart clients about 14, 333 uses 14 Smart Device Framework handwritten input, capturing 230-235 round gauge, creating 350-355 SMS 15 SMS message intercepting 197 InterceptionAction.Notify 197 InterceptionAction.NotifyAndDelete 197 MessageInterceptor object 198 MessageInterceptor object, creating 197, 198 Snapshot 241 SOAP (Simple Object Access Protocol) interface 14
SQL-based paging benefits 143 implementing 143 SqlCeCommand class 43 SqlCeConnection class 70 SqlCeDataAdapter class 81 SqlCeEngine class 65 SqlCeTransaction class 71 SQLRDA 238 SQL Remote Data Access. See SQLRDA SQL Server CE double byte languages, handling 323 SQL Server CE 3.1 and Windows Mobile 6 37 SQL Server CE 3.5 versus Oracle Lite 10g 32, 33 SQL Server CE database browsing, Query Analyzer used 68, 69 data, manipulating 71-74 data, retrieving 70 encrypting 308 GUID values 74, 75 SQL Server CE plugin class, building 63 DDl, storing in resource file 60 SQL Server CE database, creating 65, 66 SQL Server Compact database, connecting to 64 testing 67, 68 SQL Server Compact 3.5 installing, on development machine 35 installing, on Pocket PC device 36 setting up 35 SQLServerPlugin project 39 SQL WHERE clause 176 Start function 287 stateless web service, mobile dashboard application using as data source 26, 27 Stop function 287 stop words 172 Store() function 122 StreamReader class 304 StringBuilder class 301 String class 301
[ 464 ]
string manipulation managing 300, 302 Subscriber 238 SUBSTR() function 182 SUBSTRING() function 179 SuspendLayout method 302 synchronous web service calls 340 Sync Services classes 253 SyncToWindowsMobileTasks() function 220 System.Collections.Generic.List class 106 System.Diagnostics namespace 123 System.Guid object 74 System.Net.HttpWebRequest class 449 System.Xml namespace 139 SystemProperty.ConnectionsCount property 283 SystemState class 199, 212, 283
T TaskCollection object 115 TaskDetailViewer form building 116-118 launching 119, 120 task management, mobile sales force application 20 Tasks Datagrid control populating 115 Tasks list, mobile sales force application building 114 TaskDetailViewer form, building 116-118 Tasks Datagrid control, populating 115, 116 testing 120 Telephony class 206 Text property 215 text to encrypt (input), Rijndael encryption 319 The Chair 10 thick clients about 14 features 14 thin clients 13 Tomorrow Inc. about 10 mobile dashboard application 11 mobile sales force application 10, 11 mobile support case application 12
U UNIQUEIDENTIFIER data type 74
V Validate() function 108 Visual Studio IDE culture-sensitive forms, designing 326-330
W WCF service 239 WCF service library configuring 246-248 filters, setting for Sync 248-250 web method 443 WebToGo portal 240 used, for setting up application users 275-278 Win32 API GetTickCount() method 286 Windows Communication Framework service. See WCF service Windows Mobile integrating with 22 Japanese character input, supporting 324 MSMQ, setting up 369-371 Windows Mobile, integration handwritten input, capturing 24 incoming phone calls, detecting 23 SMS, detecting 23 with Calendar and Contacts book 23 WindowsMobile.Status namespace 283 Windows Mobile 6 and SQL Server CE 3.1 37 Windows Mobile Contacts synchronizing with 216-218 Windows Mobile emulator ActiveSync, connecting to 33, 35 Windows Mobile Tasks synchronizing with 219, 220 Winforms BeginUpdate method, using 302 EndUpdate method, using 302 load and cache forms 303 managing 302 ResumeLayout method, using 302, 303 SuspendLayout method, using 302, 303
[ 465 ]
X XML data files deserialization thesis 304 managing 303 XML serialization 304 XMLTextRead method, using 303, 304 XMLTextWriter method, using 303, 304
XMLDocument 303 XML DOM using, to store product selection 139-141 XML serialization 304 XMLTextReader method 303 XMLTextWriter method 303
[ 466 ]
Thank you for buying
.NET Compact Framework 3.5 Data-Driven Applications About Packt Publishing
Packt, pronounced 'packed', published its first book "Mastering phpMyAdmin for Effective MySQL Management" in April 2004 and subsequently continued to specialize in publishing highly focused books on specific technologies and solutions. Our books and publications share the experiences of your fellow IT professionals in adapting and customizing today's systems, applications, and frameworks. Our solution based books give you the knowledge and power to customize the software and technologies you're using to get the job done. Packt books are more specific and less general than the IT books you have seen in the past. Our unique business model allows us to bring you more focused information, giving you more of what you need to know, and less of what you don't. Packt is a modern, yet unique publishing company, which focuses on producing quality, cutting-edge books for communities of developers, administrators, and newbies alike. For more information, please visit our website: www.packtpub.com.
About Packt Enterprise
In 2010, Packt launched two new brands, Packt Enterprise and Packt Open Source, in order to continue its focus on specialization. This book is part of the Packt Enterprise brand, home to books published on enterprise software – software created by major vendors, including (but not limited to) IBM, Microsoft and Oracle, often for use in other corporations. Its titles will offer information relevant to a range of users of this software, including administrators, developers, architects, and end users.
Writing for Packt
We welcome all inquiries from people who are interested in authoring. Book proposals should be sent to [email protected]. If your book idea is still at an early stage and you would like to discuss it first before writing a formal book proposal, contact us; one of our commissioning editors will get in touch with you. We're not just looking for published authors; if you have strong technical skills but no writing experience, our experienced editors can help you develop a writing career, or simply get some additional reward for your expertise.
ASP.NET 3.5 CMS Development ISBN: 978-1-847193-61-2
Paperback: 284 pages
Build, Manage, and Extend your own Content Management System 1.
Create your own Content Management System with the understanding needed to expand it and add new functionality as your needs grow
2.
Learn to build a fully functional application with very little code and set up users and groups within your application
3.
Manage the layout of your site using Master Pages, Content Placeholders, Themes, Regions, and Zones
4.
A step-by-step guide with plenty of code snippets and screen images
ODP.NET Developer's Guide ISBN: 978-1-847191-96-0
Paperback: 328 pages
A practical guide for developers working with the Oracle Data Provider for .NET and the Oracle Developer Tools for Visual Studio 2005 1.
Application development with ODP.NET
2.
Dealing with XML DB using ODP.NET
3.
Oracle Developer Tools for Visual Studio .NET
Please check www.PacktPub.com for information on our titles
ASP.NET 3.5 Application Architecture and Design ISBN: 978-1-847195-50-0
Paperback: 260 pages
Build robust, scalable ASP.NET applications quickly and easily 1.
Master the architectural options in ASP.NET to enhance your applications
2.
Develop and implement n-tier architecture to allow you to modify a component without disturbing the next one
3.
Design scalable and maintainable web applications rapidly
4.
Implement ASP.NET MVC framework to manage various components independently
BlackBerry Enterprise Server for Microsoft® Exchange ISBN: 978-1-847192-46-2
Paperback: 188 pages
Installation and Administration 1.
Understand BlackBerry Enterprise Server architecture
2.
Install and configure a BlackBerry Enterprise Server
3.
Implement administrative policies for BlackBerry devices
4.
Secure and plan for disaster recovery of your server
Please check www.PacktPub.com for information on our titles