The sample client scenario is a Pocket PC application that is written with Visual Studio 2005 in C# and targets the .NET Compact Framework 2.0. The sample uses the free community edition of the Smart Device Framework from OpenNETCF.
The sample application shows how to support the claims business process by using a Pocket PC. Some of the design choices are commented during the walkthrough of the sample application. Also note that the article describes parts of the code after the application's user interface design is described. In the following walkthrough, you are assumed to take the role of the user, the adjuster.
Splash
When the application starts, the first screen displayed is the Splash screen, as shown in Figure 10.
Figure 10. Splash screen
There could be many purposes of a splash screen, but the most common is to show something more interesting than the wait cursor while the application loads. Even if not shown here, it can also be used to show other information such as version number, copyright notices, announcements, and so on.
As you will see, the sample application is rotation-aware, and when the screen is rotated, the landscape version of the splash screen is shown in Figure 11.
Figure 11. Splash screen in landscape mode
Note that this is another part of the same image, and not merely the same image stretched to fit the new orientation. The image is designed in the format 320 x 320 pixels to look good even on a minimal square screen of 240 x 240 pixels, and using the same technique, even greater resolutions could be supported.
Login
When the application starts, the first screen is the Login screen, as shown in Figure 12.
Figure 12. Login screen
The Login screen is branded with an application image. This screen can also show notification messages about news, new features, and important changes. In addition, this screen could be a good place to inform the user about copyrights and license conditions. When choosing to put information on the splash or the login screen, you should consider that the splash screen will eventually go away, and the login screen will wait for the user to move on.
A rotated version of the same screen is shown in Figure 13.
Figure 13. Login screen in landscape
Note how the text boxes are adjusted to the new width, and that a completely different image is used that better matches the landscape orientation. Throughout this walkthrough, you will see screenshots in sometimes both portrait and landscape orientation (but most often either of the two) to demonstrate the support for both orientations.
When you enter the user name and password and then tap Login, the application confirms that the credentials are valid by connecting to the server by using Windows authentication. If the log on is successful, the application displays the Main menu screen (see Figure 14). If the log on is not successful, an error message appears, and then the application closes. It also closes if you tap Cancel.
Main Menu
After the user logs on to the application, the next screen displayed is the Main Menu screen, as shown in Figure 14.
Figure 14. Main menu screen
The functionality of the application aligns with the business process of the adjuster. The main steps in this process are to receive notifications about claim assignments, check out jobs from the server (use the Sync icon in Figures 14 and 15), open the claim, perform tasks that are related to that claim (Claims), and check jobs back into the server (Sync). The adjuster can also perform general tasks such as checking the Journal and changing application Options.
A ListView control presents the main menu commands as icons that a user can tap. Using icons to represent the various functionality of the application is a very useful approach that increases usability. The same screen in portrait mode is shown in Figure 15.
Figure 15. Menu commands
All of the options represented by icons on the Main Menu screen are also available in the Menu option (at the bottom of the screen). Providing several ways (icons and commands) to access the same functionality allows users to choose which approach is more efficient for them.
Claim Assignment
When a claim assignment is made in the back office, an SMS message is sent to the adjuster's mobile device, as shown in Figure 16.
Figure 16. Claim assignment notification
The message is intercepted and shown to the adjuster that can choose to accept the assignment. If the adjuster accepts the assignment, a request is queued to update the claim's status on the server. If (or when) the device has a live connection, the queued request is sent to the server to update the claim's status. When the status is updated on the server, a confirmation is sent back to the device, as shown in Figure 17.
Figure 17. Claim status update confirmation
This way of handling the claim assignments allows the back office (management) full control of the claims workflow. If an assigned adjuster does not accept the assignment within a specific time, the claim can be reassigned to another adjuster.
Claim Check Out
The first step in processing a claim is to check it out from the server to the device; this step is done by synchronizing with the server. On the Main menu screen, tap the Sync icon (or choose the Sync command in the menu).
The Synchronize screen allows you to synchronize claim data with the server. You can choose to check out or check in the selected claims. (The Check In option is covered in the Claim Check In section later in this article).
When you select Check out, the claims that are available for check out are retrieved from the server. Claims are available if another user does not have them checked out. The synchronization is implemented as a two-step wizard. First, you are required to select the claims that you want to check out, as shown in Figure 18.
Figure 18. Synchronize screen with the Check Out option selected
You also choose whether the media associated with the selected claims should also be checked out. The reason for this is that media files can be large, and, using a low bandwidth connection, it can be both slow and costly to download the media.
When you have selected at least one job, tapping the Start button initiates the check out. During the synchronization the progress is reported, as shown in Figure 19.
Figure 19. Synchronization completed (for a check-out request)
When the synchronization is complete, tap Done to return to the Main menu screen.
Open Claim
The next step in the business process is to open and work with a specific claim. On the Main Menu screen (shown in Figures 14 and 15), tap Claims.
Adjusters can use the Claims screen to search for claims that they are managing, and you can search for the name of the insured or the number of the claim. When you tap the Find option, the claims that match the search criteria appear in the list.
You can open a claim by tapping and holding a claim in the list, and then by tapping Open, as shown in Figure 20.
Figure 20. Opening a claim
You can also select the claim in the list, and use the Open option in the Menu. After you have opened a claim, the claim screen appears, as shown in Figure 21.
Figure 21. Claim screen
The Claim screen displays the name of the insured in the heading and lists the various artifacts that are associated with the claim in a tree. It can be electronic forms, text notes, photos, videos, voice recordings or other sounds, and estimates.
This screen also shows the current status of the claim. If the status is changed and the claim is saved (by selecting Done on the Menu), a request is queued to update the claim's status on the server. If (or when) the device has a live connection, the queued request is sent to the server to update the claim's status. When the status is updated on the server, a confirmation is sent back to the device (as shown in Figure 17). As already mentioned, this allows all interested parties to be updated on the current status of each claim.
By doing a tap-and-hold on any of the items in the tree, a pop-up menu appears with context-sensitive options. If it is a root item (such as Forms, Notes, and so on), only a New option will be available, and if it is any of the other items, the options to Edit and Delete are available. The same logic applies to the corresponding options in the Menu.
A summary of the claim is displayed when selecting the Summary option, as shown in Figure 22.
Figure 22. Claim summary
Selecting Artifacts takes you back to the artifacts tree (see Figure 21).
Claims Screen Options
The following five sections explain the various options on the Claims screen.
Forms
By selecting the New option on the Forms item in the artifacts tree on the claims screen (Figure 21), you can select which form to create, as shown in Figure 23.
Figure 23. Select form to create
All of the screens that you can access from the claims screen (shown in Figure 21) include the name of the insured below the screen heading (in Figure 23, the name of the insured is Fabrikam, Inc.).
You simply select one of the form types in the list and then choose Select to create the form, as shown in Figure 24.
Figure 24. Form entry
The form type is shown in the screen heading above the name of the insured, and each of the lines in the list is a form field. You select a field in the list, and then you enter the value for that field in the text box just above the list. When you want to save the value in the text box, you select the Save Row option. Selecting Done saves the form data and takes you back to the claims screen (Figure 21).
Note that this is the same screen that you will see when you select to Edit a form in the claims screen.
Notes
By selecting the New option on the Notes item on the claims screen (Figure 21), you create a new note, as shown in Figure 25.
Figure 25. Note entry
On this screen, you can enter a name of the note and simply write the note text. When you want to save the note, select Done and if not, select Cancel. Both options will take you back to the claims screen (Figure 21).
Note that this is the same screen that you will see when you select to Edit a note in the claims screen.
Photos & Videos
By selecting the New option on the Photos & Videos item on the claims screen (Figure 21), you can associate a photo or video to the current claim, and that is done on the Photo / Video screen, as shown in Figure 26.
Figure 26. New photo or video
On this screen, you usually start by either selecting an existing media file or creating a new one. The Photo and Video options on the New menu will bring up the camera application to allow you to take a photo or record a video. The From File option allows you to select an existing media file, as shown in Figure 27.
Figure 27. New photo or video
If you select a photo file, you are returned to the Photo / Video screen with a preview of the photo, as shown in Figure 28.
Figure 28. Photo selected
Now you can enter a name for the photo and save the photo by selecting Done (or Cancel to exit) and be returned to the claims screen (Figure 21).
Note that this is the same screen you will see when you select to Edit a photo or video in the claims screen.
Sounds
By selecting the New option on the Sounds item on the claims screen (Figure 21), you can associate a sound to the current claim, which is done on the Sounds screen, as shown in Figure 29.
Figure 29. Sound recording
On this screen, you can enter a name of the recording and record the sound using either the buttons in the middle of the screen or the same options at the bottom left of the screen. When you want to save the sound association, you select Done and if not, select Cancel. Both options will take you back to the claims screen (Figure 21).
Note that this is the same screen that you will see when you select to Edit a sound in the claims screen.
Estimates
By selecting the New option on the Estimates item on the claims screen (Figure 21), you will create a new estimate, as shown in Figure 30.
Figure 30. Estimate with time & material
On this screen, you can enter a name of the estimate, and then you can start adding time and material items. As you add items, a total is updated at the bottom of the screen.
You add items by doing a tap-and-hold in the respective list, and when you select New in the Time list, you will create a new time item, as shown in Figure 31.
Figure 31. Time entry
The name of the estimate is shown below the name of the insured. Note how the same screen is designed differently in Figure 32 to make use of the landscape mode.
Figure 32. Time entry in landscape mode
This is a simple way to enter estimated time to spend on a specific task. In a real-world application the tasks and rates are probably loaded from a back-office business system. The same goes for the entry of material, as shown in Figure 33.
Figure 33. Material entry
On the estimate screen (Figure 30), select Done to save or Cancel to exit. Both options will take you back to the claims screen (Figure 21).
Note that this is the same screen that you will see when you select to Edit an estimate in the claims screen.
Claim Check In
The last step in the claims business process is to check in claims from the device back to the server by synchronizing the device with the server. On the Main menu screen, tap Sync.
The Synchronize screen allows you to synchronize claim data with the server. You can choose to check out or check in the selected claims. The check out option was covered in the preceding Claim Check Out section.
When you select Check In, the claims that are available for check in are retrieved from the database on the device. The synchronization is implemented as a two-step wizard. First, you are required to select the claims that you want to check in, as shown in Figure 34.
Figure 34. Synchronize screen with the Check In option selected
When you have at least one claim selected, tapping the Start button to initiate the check in. During the synchronization, the progress is reported, as shown in Figure 35.
Figure 35. Synchronization completed (for a check in request)
When the synchronization is complete, tap Close to return to the main menu screen.
Journal
By selecting the Journal option on the Main Menu screen (as shown in Figures 14 and 15), you can view the claim journal, as shown in Figure 36.
Figure 36. Journal entries
Here you first select if you want to see the journal for only one claim or for all claims, and also delete journal entries. When a specific claim is selected, you can add journal entries by adding a note in the Entry text box and selecting Add.
Select Done to return to the main menu screen.
Options
When you select Options on the Main Menu screen (as shown in Figures 14 and 15), the Options screen appears, as shown in Figure 37.
Figure 37. Options
The application uses the URL in the Web Service (URL) box for the XML Web service to synchronize (check out and check in) jobs. In this case, the URL points to a Web service that uses encrypted communication (that is, Secure Sockets Layer).
Selecting Done will save any changes, selecting Cancel will discard them; both options take you back to the main menu screen.
About
All applications should include a screen with the product name, version, copyright, and other legal information. The About option on the Main menu screen displays this information, as shown in Figure 38.
Figure 38. About screen
This screen can also include a link to a Web page that has product and support information.
Code Walkthrough
The previous section provided an example scenario for the sample application, and now it is time to look at the source code.
Composite User Interface Application Block
The most interesting part of the CAB is the ability to build extensible applications. This means that different functionality is implemented in separate modules (assemblies) completely separated from each other.
The functionality in the sample application is divided into 10 separate modules, and they extend the core shell (the application .exe) in two levels. The first level consist of modules that extend the application's main menu (see Figures 14 and 15), and the second level extends the claims screen (Figure 21), as shown in Figure 39.
Figure 39. Functionality divided by module
Each module is responsible for extending the user interface with the functionality that it provides. For example, the journal module (JournalModule.dll) adds the icon and the menu option to the main menu screen as well as implement the journal screen (Figure 36). Another example is the form module (FormModule.dll) that add the root and child nodes to the claims screen as well as implements the form screens (Figures 23 and 24). The list of modules that is loaded should be included in the ProfileCatalog.xml file, and it looks like the following code:
|
<?xml version="1.0" encoding="utf-8" ?>
<SolutionProfile xmlns="http://schemas.microsoft.com/pag/cab-profile">
<Modules>
<ModuleInfo AssemblyFile="EstimateModule.dll" />
<ModuleInfo AssemblyFile="SoundModule.dll" />
<ModuleInfo AssemblyFile="PictureModule.dll" />
<ModuleInfo AssemblyFile="NoteModule.dll" />
<ModuleInfo AssemblyFile="FormModule.dll" />
<ModuleInfo AssemblyFile="AboutModule.dll" />
<ModuleInfo AssemblyFile="OptionsModule.dll" />
<ModuleInfo AssemblyFile="SyncModule.dll" />
<ModuleInfo AssemblyFile="JournalModule.dll" />
<ModuleInfo AssemblyFile="ClaimModule.dll" />
</Modules>
</SolutionProfile> |
As the different modules are unaware of each other (except that some are dependent on others) they can be deployed separately, and only this file needs to be updated for each deployment. For example, if a specific device does not have a camera and that functionality is not needed, that line should be removed from the file and the PictureModule.dll does not need to be deployed to that device.
The CAB introduces many new concepts such as the ObjectBuilder, WorkItems, Workspaces, Services, Commands, SmartParts, UI Extension Sites, and so on. It is a good idea to get to know all these concepts before you dig into this article’s sample as it will help considerably in understanding the code. The Mobile Client Software Factory documentation (included in the installation) is a good place to find such information.
The best way to get an idea of the benefits of the CAB is probably to look closer at one of the modules. We start by looking at the ModuleInitializer class, which inherits from the standard CAB class ModuleInit of the NoteModule, and first the declarations and the constructor.
|
private WorkItem workItem;
private ClaimItemsCatalog claimItemsCatalog;
public ModuleInitializer([ServiceDependency] WorkItem workItem,
[ServiceDependency] ClaimItemsCatalog claimItemsCatalog)
{
this.workItem = workItem;
this.claimItemsCatalog = claimItemsCatalog;
} |
The first odd thing to notice is the attributes of the constructor parameters, and they are used by the ObjectBuilder to perform dependency injection. It allows access to shared instances of these classes (in CAB named Services) that are defined somewhere else (in this or another module on which this module is dependent). There are also other ways (using properties, and so on) to share services between modules. The WorkItem service is important as it collects other instances related to a specific context. The constructor simply saves these services in private variables, and we move on to the method that is called when the module is loaded.
|
public override void Load()
{
base.Load();
ClaimItem claimItem = new ClaimItem(
Properties.Resources.NotesText,
Properties.Resources.NoteCommand,
Properties.Resources.NoteItemsCommand);
claimItemsCatalog.Add(claimItem);
} |
Now the ClaimItemCatalog service instantly comes into use as a new ClaimItem object is added that corresponds to the node "Notes" in the TreeView control (as shown in Figure 21). The actual node is created in the ClaimModule using the same ClaimItemCatalog service. Note that all constant strings are defined as resources to be easily translated to another language.
The last part of the ModuleInitializer class defines a number of commands.
|
[CommandHandler("NoteItems")]
public void OnNoteItems(object sender, EventArgs e)
{
if(!workItem.Services.Contains(typeof(NoteHandler)))
workItem.Services.AddNew<NoteHandler>();
NoteHandler noteHandler = workItem.Services.Get<NoteHandler>();
foreach(ClaimItem ci in claimItemsCatalog)
if(ci.ItemsCommand == Properties.Resources.NoteItemsCommand)
{
string claimID = workItem.RootWorkItemItems.Get(
"CurrentClaimID").ToString();
DataSet ds = noteHandler.GetList(claimID);
ci.SubItems.Clear();
foreach(DataRow dr in ds.Tables[0].Rows)
ci.SubItems.Add(dr["NoteID"].ToString(),
dr["Name"].ToString());
break;
}
} |
This command is used by the ClaimModule to populate the SubItems collection of the ClaimItem object. These SubItems will also be used by the ClaimModule to populate the nodes below the "Notes" node (the notes). The actual data access is handled by the NoteHandler service (see below). Note that both that service and the currently selected claim's identity (CurrentClaimID) are stored in WorkItems.
The command used when opening a note (the New and Edit menu options in the TreeView control's ContextMenu in the claim screen) is implemented like this:
|
[CommandHandler("Note")]
public void OnNote(object sender, EventArgs e)
{
using(WaitCursor wc = new WaitCursor())
{
WorkItem wi = workItem.WorkItems.AddNew(
typeof(ControlledWorkItem<NoteController>));
wi.Run();
}
} |
The command creates a new WorkItem object with the controller set to the NoteController class (see below). The following command is used when the Delete menu option is selected on a note in the claims screen:
|
[CommandHandler("DeleteNote")]
public void OnDeleteNote(object sender, EventArgs e)
{
using(WaitCursor wc = new WaitCursor())
{
string noteID = workItem.RootWorkItem.Items.Get(
"CurrentClaimItemID").ToString();
NoteHandler noteHandler =
workItem.Services.Get<NoteHandler>();
noteHandler.Delete(noteID);
}
} |
The selected note's identity (CurrentClaimItemID) is retrieved and used to delete the note from the database.
The code for the complete NoteHandler class (service) looks like this:
|
private Database database;
public NoteHandler([ServiceDependency] Database database)
{
this.database = database;
}
public DataSet GetList(string claimID)
{
return database.ExecuteDataSet(
"SELECT NoteID, Name FROM Note WHERE ClaimID='" + claimID + "'");
}
public void Delete(string noteID)
{
database.ExecuteNonQuery(
"DELETE Note WHERE NoteID='" + noteID + "'");
}
public DataSet GetForID(string noteID)
{
return database.ExecuteDataSet(
"SELECT * FROM Note WHERE NoteID='" + noteID + "'", "Note");
}
public DataSet GetEmpty()
{
return database.ExecuteDataSet(
"SELECT * FROM Note WHERE NOT 0=0", "Note");
}
public void Save(DataSet dataSetToSave)
{
database.UpdateDataSet(dataSetToSave, "Note");
} |
Using dependency injection, the centrally defined Database service is retrieved and saved. This handler can handle most standard database operations (queries, updates, and so on). The Delete method is included to show an alternative to using DataSets. The same functionality could be achieved by a call to the GetForID method followed by a delete of the row in the DataSet object, and a final call to the Save method.
The purpose of the NoteController class is to load the note form (as shown in Figure 25), and this is done with the following code:
|
noteDetailForm = WorkItem.Items.AddNew<NoteDetailForm>(
"NoteDetailForm");
shell.DialogWorkspace.SmartPartClosing += new
EventHandler<WorkspaceCancelEventArgs>(Workspace_SmartPartClosing);
shell.DialogWorkspace.Show(noteDetailForm, new
WindowSmartPartInfo(ControlBoxOptions.OkButton, false)); |
An instance of the NoteDetailForm class is added to the WorkItem, and after an event is set up to capture when the form is closed, the form is added as a SmartPart and shown to the user. The Workspace_SmartPartClosing event is implemented as follows:
|
if(e.SmartPart == noteDetailForm)
{
this.WorkItem.Terminate();
} |
The features of the CAB can easily cover several articles, but hopefully you have seen enough to be able to dig deeper yourself. Before we leave the CAB, let's have a look at an issue that is important to many mobile developers: form caching.
Most developers are faced with the tradeoff between performance and memory, and on a mobile device, this tradeoff is critical as both processor speed and memory is much more limited than on a normal desktop or laptop computer. Added to that is the fact that mobile users demand more responsive applications. Therefore, the navigation of the application needs to be highly optimized, and a common approach is to cache the forms in memory. Doing this using the CAB is not obvious, as the concept of modularity and extensibility does not match the concept of keeping things in memory that are not used. The approach selected in this article's sample is aligned with the use of WorkItems, but each form is loaded in the root WorkItem and stored there until the application ends (or programmatically removed). This means that other resources such as data handlers also need to be loaded into the root WorkItem to be accessible to the forms. However, some of these other items are unloaded from memory when the various WorkItems are terminated. This way, only the necessary resources (such as the forms) are stored throughout the lifetime of the application. The difference this means to the responsiveness of the user interface is significant.
Going back to the code in the NoteController class, the loading of the notes form using forms caching is implemented like this:
|
smartPartClosingEvent = new EventHandler<WorkspaceCancelEventArgs>(
Workspace_SmartPartClosing);
if(!shell.RootWorkItem.Items.Contains("NoteDetailForm"))
{
noteDetailForm = shell.RootWorkItem.Items.AddNew<NoteDetailForm>(
"NoteDetailForm");
shell.DialogWorkspace.SmartPartClosing += smartPartClosingEvent;
shell.DialogWorkspace.Show(noteDetailForm, new
WindowSmartPartInfo(ControlBoxOptions.OkButton, false));
}
else
{
noteDetailForm = shell.RootWorkItem.Items.Get<NoteDetailForm>(
"NoteDetailForm");
shell.DialogWorkspace.SmartPartClosing += smartPartClosingEvent;
shell.DialogWorkspace.Activate(noteDetailForm);
noteDetailForm.LoadComponent();
}
mainForm.SetTitle(false); |
If the root WorkItem does not include an instance of the NotesDetailForm class, it is created and shown just like before. If it already exists, it is activated and the LoadComponent method is called to restore the form to its original state. Note the last line of code that calls the main form to clear its title (Text property). The reason for that is to prevent more than one instance of the application to appear in the "Running Programs" list of the device (accessed on the Start menu by clicking Settings, pointing to System, pointing to Memory, and then clicking Running Programs). The Workspace_SmartPartClosing event now looks like this:
|
if(e.SmartPart == noteDetailForm)
{
mainForm.SetTitle(true);
shell.DialogWorkspace.SmartPartClosing -= smartPartClosingEvent;
shell.DialogWorkspace.Hide(noteDetailForm);
e.Cancel = true;
this.WorkItem.Terminate();
} |
The main form title is restored, the event handler is removed, the form is hidden, and the closing of the form is prevented by setting Cancel property of the event arguments before the WorkItem is terminated.
Orientation Aware Control Application Block
When designing forms that support screen rotation, this application block can be of great help. The important thing is to set the modifier of the controls that should be available to the form to public, and in the form you can use the following declarations.
|
private Label claimNameLabel
{
get { return this.noteDetailControl.ClaimNameLabel; }
}
private TextBox nameTextBox
{
get { return this.noteDetailControl.NameTextBox; }
}
private TextBox noteTextBox
{
get { return this.noteDetailControl.NoteTextBox; }
} |
These example lines of code come from the notes form discussed earlier, and show how private properties are created that correspond to the controls in the orientation-aware control noteDetailControl. This way, you can use the names of the controls just as if they were added to the form itself, and thereby you are minimizing the dependency on the orientation-aware control. A rule of thumb is therefore also not to put any code in the orientation-aware control class.
Also, any other initialization of controls (such as connecting event handlers) need to be set up manually (normally at the end of the constructor) with code like the following:
|
nameTextBox.GotFocus += new EventHandler(textBox_GotFocus);
nameTextBox.LostFocus += new EventHandler(textBox_LostFocus);
nameTextBox.ContextMenu = editMenu; |
Note that the two event handlers are used to show and hide the Soft Input Panel (SIP) or soft keyboard of the device, and the context menu is used to implement the standard edit menu functionality when doing a tap-and-hold in the TextBox control.
Configuration Application Block
This application block offers the most important functionality for reading configuration file information as there is no support for the "System.Configuration" namespace within the .NET Compact Framework. The application block is therefore used by several of the other application blocks when they need to read information in the application’s configuration file.
Connection Monitor Application Block
The most important use of the connection monitor block is to keep track of the current connection state of the device. There can be several connections live at a given time, and the application block enumerates and consolidates information to be easily accessible by the application developer as well as other application blocks.
An example of how to use the application block is taken from the login form, as shown in the following example:
|
if(connectionMonitor.IsConnected)
{
// Code that require a connection
} |
Here the application block is used to check whether there is any connection available. Events can also be set up that will be fired when the connection state changes. The Disconnected Service Agent Application Block (see below) makes more advanced use of this application block.
Password Authentication Application Block
When the application starts, the services handling the password authentication are loaded as follows:
|
RootWorkItem.Services.AddNew<AuthenticationService,
IAuthenticationService>();
RootWorkItem.Services.AddNew<PasswordAuthenticationService>(); |
The first service is important, as the CAB will look in the root WorkItem for a service of the type IAuthenticationService. If present, it will call the Authenticate method on that instance, and in the sample application, this method is implemented like this:
|
LoginForm loginForm = rootWorkItem.Items.AddNew<LoginForm>();
loginForm.ShowDialog();
´
PasswordIdentity identity =
rootWorkItem.Items.Get<PasswordIdentity>("Identity");
if(identity == null)
throw new QuitApplicationException(); |
First, the login screen is shown, and when it is closed (by the user, after entering login information) the root WorkItem is checked for a PasswordIdentity item (Identity). If it does not exist, the application is ended using a custom exception.
The PasswordAuthenticationService class is implemented with the following code (with the empty constructor excluded):
|
private AuthenticationToken token;
internal void SetTokenData(string tokenData)
{
token = new AuthenticationToken(tokenData);
}
public PasswordIdentity CheckCredentials(
string userName, string password)
{
using(RsaAesCryptographyProvider cryptoProvider =
new RsaAesCryptographyProvider("Claims2Go"))
{
return token.Authenticate(userName, password, cryptoProvider);
}
} |
This class simply holds a private instance of type AuthenticationToken (defined by this application block), and implements the SetToken method to set the token and another method, CheckCredentials, to authenticate a user name and password against that token.
With that in place, the following code is executed when the Login option is selected in the login form:
|
if(connectionMonitor.IsConnected)
{
// Check using Web Service URL
if(checkConnectionHelper.CheckHttp(common.WebServiceUrl))
{
// If new password, save new hash
PasswordIdentity passwordIdentity =
new PasswordIdentity(common.UserName, common.Password,
new RsaAesCryptographyProvider("Claims2Go"));
AuthenticationToken token =
new AuthenticationToken(passwordIdentity);
string hash = token.TokenData;
if(hash != common.PasswordHash)
{
common.PasswordHash = hash;
common.Save();
}
}
else
throw new Exception();
}
else
{
// Check if we have hash
if(common.PasswordHash.Length < 1)
{
// Show error message that you need to be connected
// first time you login (as hash need to be generated).
return;
}
}
passwordAuthenticationService.SetTokenData(common.PasswordHash);
PasswordIdentity identity =
passwordAuthenticationService.CheckCredentials(
common.UserName, common.Password);
if(identity != null && identity.IsAuthenticated)
rootWorkItem.Items.Add(identity, "Identity");
else
throw new Exception(); |
If the device is connected, the user provided user name and password is used to make a short connection attempt to the XML Web service. If successful, a password hash is generated and if it is different from the one stored locally on the device, the password has been changed on the server and therefore the locally stored hash is updated in the registry. If the device is not connected and there is not a password hash available, an error message is shown (see the preceding code comments).
Finally, the credential is checked using an instance of the PasswordAuthenticationService class, and if successful, that instance is added to the root WorkItem.
This way, the user can be authenticated whether connected or not, provided that at least one successful login on the server has taken place.
Data Access Application Block
This is the only application block that has been updated in this article's sample code. The reason is that all the functionality related to DataSets was excluded when the application block was ported from the desktop version. The updated application block now includes most of the functionality available in the desktop version to manipulate DataSets including the LoadDataSet, ExecuteDataSet, and UpdateDataSet methods, and their respective overloads. To support their functionality, the factory has also been updated to support the creation of data adapters and command builders.
The core of the all the overloads for the LoadDataSet and ExecuteDataSet methods is the following implementation of the LoadDataSet method.
|
public virtual void LoadDataSet(DbCommand command,
DataSet dataSet, string[] tableNames)
{
DbConnection connection = GetConnection();
PrepareCommand(command, connection);
using(DbDataAdapter adapter =
dbProviderFactory.CreateDataAdapter())
{
adapter.SelectCommand = command;
string systemCreatedTableNameRoot = "Table";
for(int i = 0; i < tableNames.Length; i++)
{
string systemCreatedTableName = (i == 0)
? systemCreatedTableNameRoot
: systemCreatedTableNameRoot + i;
adapter.TableMappings.Add(
systemCreatedTableName, tableNames[i]);
}
adapter.Fill(dataSet);
}
} |
Parameter validation has been removed for clarity, and except for the table mappings, the implementation is almost trivial.
A real time-saver that significantly simplifies the code is the use of the command builder in the UpdateDataSet.
|
public int UpdateDataSet(DataSet dataSet, string tableName,
string fields)
{
int rows = 0;
DbConnection connection = GetConnection();
using(DbDataAdapter adapter =
dbProviderFactory.CreateDataAdapter())
{
DbCommand selectCommand = dbProviderFactory.CreateCommand();
selectCommand.CommandText =
"SELECT " + fields + " FROM " + tableName;
PrepareCommand(selectCommand, connection);
adapter.SelectCommand = selectCommand;
DbCommandBuilder commandBuilder =
dbProviderFactory.CreateCommandBuilder();
commandBuilder.DataAdapter = adapter;
adapter.InsertCommand = commandBuilder.GetInsertCommand();
adapter.UpdateCommand = commandBuilder.GetUpdateCommand();
adapter.DeleteCommand = commandBuilder.GetDeleteCommand();
rows = adapter.Update(dataSet.Tables[tableName]);
}
dataSet.AcceptChanges();
return rows;
} |
Again, with the parameter validation removed, this is classic data access code. Many things can be said about the performance when using DataSets, but as always there is a tradeoff between performance and the time (and cost) it takes to write and maintain that code. There are definitely situations when other approaches need to be used, but explore them when needed and consider the tradeoff.
The following code shows how the database service is created and added to the root WorkItem.
|
string filename = Path.Combine(DirectoryUtils.BaseDirectory,
"Claims.sdf");
string connectionString = String.Format(
"Data Source=\"{0}\";Password={1}", filename,
"pe4eGaWR46a4e+UPR-c??&wa!uFu#asw");
Database dbService = new SqlDatabase(connectionString);
WorkItem.Services.Add<Database>(dbService);
return dbService; |
Note that it is a recommendation to use a password to encrypt the local database.
Disconnected Service Agent Application Block
As many of the other services, the disconnected service agent in the sample application is loaded into the root WorkItem when the application starts. The service agent is used to queue requests to the server, and the application block will then take care of dispatching the messages when there is a live connection available. The implementation of the service agent begins with the following code in the constructor.
|
IConnectionMonitor connections = new
ConnectionMonitorAdapter(connectionMonitor);
requestManager = RequestManager.Instance;
requestManager.Initialize(endpointCatalog, connections, database);
requestManager.StartAutomaticDispatch();
requestQueue = requestManager.RequestQueue; |
The RequestManager singleton instance is created and initialized using the catalog of endpoints (for more information, see the next section), a connection adapter, and the database service. The endpoint catalog is used to look up addresses and credential information, the connection adapter is used to check the connection status, and the database service is used for storing the queued requests and any unsuccessful calls. Then the automatic dispatch of requests is started and the request queue instance is saved in a private variable used at the end of the following method.
|
public void SetClaimStatus(string claimID, int status, int claimNo, string insured)
{
OfflineBehavior behavior = new OfflineBehavior();
behavior.MaxRetries = 0;
behavior.Stamps = 5;
behavior.Tag = "SetClaimStatus";
behavior.Expiration = DateTime.Now + new TimeSpan(2, 0, 0, 0);
behavior.ReturnCallback =
new CommandCallback(typeof(ServiceAgentCallback),
"OnSetClaimStatusReturn");
behavior.ExceptionCallback =
new CommandCallback(typeof(ServiceAgentCallback),
"OnSetClaimStatusException");
Request request = new Request();
request.MethodName = "SetClaimStatus";
request.Behavior = behavior;
request.CallParameters =
new object[] { claimID, status, claimNo, insured };
request.OnlineProxyType =
typeof(Microsoft.Samples.Claims2Go.WebServices.ClaimsWebService);
request.Endpoint = "ClaimsWebService";
requestQueue.Enqueue(request);
} |
When creating a disconnected request, you need to start with something called an offline behavior. The behavior object defines how the request will behave with regard to expiration, maximum number of retries, the number of "stamps" (the relative importance of the request), and the Tag value (a string property that allows you to categorize or otherwise identify requests). The OfflineBehavior class also exposes properties that provide information about the request, such as the date and time it was queued. This request will not be resent (no retries), it will expire in two days, and when the request is dispatched, the ServiceAgentCallback class will be called. A successful call will be to the OnSetClaimStatusReturn method, and if any exceptions occur, the OnSetClaimStatusException method will be called.
The request will use any connection with a "price" less than 5. In the sample application, the connections in the configuration file are defined as follows:
|
<Connections>
<ConnectionItems>
<add Type="CellConnection" Price="8"/>
<add Type="NicConnection" Price="2"/>
<add Type="DesktopConnection" Price="1"/>
</ConnectionItems>
</Connections> |
This means that this request will only use a network card or a desktop connection. In a real-world application, the status changes are both small and probably mission-critical, and should be sent on whatever available connection, that is, a very high "stamp" value should be assigned to the request.
Next, the request object is created with the behavior attached and specifying the ClaimWebService (proxy) class, the method to call, the parameters, and the endpoint to use for addressing and credentials (for more information, see the next section). Finally, the request is queued using the request queue.
Note that the application block uses the database service to store the requests in a table named Requests, and any requests that are not successfully dispatched are stored in a "dead letter queue" in a table named Dlq.
The method on the server side is implemented like this:
|
[WebMethod]
public string SetClaimStatus(string claimID, int status, int claimNo, string insured)
{
using(SqlConnection cn = new SqlConnection(connectionString))
{
cn.Open();
SqlHelper.ExecuteNonQuery(cn, CommandType.Text,
"UPDATE Claim SET Status=" + status.ToString() +
" WHERE ClaimID='" + claimID + "'");
return SqlHelper.ExecuteScalar(cn, CommandType.Text,
"SELECT Name FROM Status" +
" WHERE Status=(SELECT Status FROM Claim" +
" WHERE ClaimID='" + claimID +"')").ToString();
}
} |
It simply set the status of the claim, and returns the name of the new status.
The following code is for the callback methods.
|
public void OnSetClaimStatusReturn(Request request, object[] parameters, string returnValue)
{
string s = string.Format(
Properties.Resources.MsgSetClaimStatusReturn,
request.CallParameters[2].ToString(),
request.CallParameters[3].ToString(), returnValue);
MessageBox.Show(s, Properties.Resources.DefaultTitle);
}
public OnExceptionAction OnSetClaimStatusException(Request request, Exception ex)
{
string s = string.Format(
Properties.Resources.MsgSetClaimStatusException,
request.CallParameters[2].ToString(),
request.CallParameters[3].ToString(), ex.Message);
MessageBox.Show(s, Properties.Resources.ExceptionTitle);
re |