Click here to Skip to main content
Click here to Skip to main content

Quality Manager - An IAC 2013 Contest Entry

, 25 Oct 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
An application to help QA inspectors in the field

Please note

This article is an entry in our AppInnovation Contest. Articles in this sub-section are not required to be full articles so care should be taken when voting.

Vitals for the IAC 2013 Contest

  • This application targets the tablet platform
  • The application helps field agents and QC technicians perform in-the-field assessments.

  • This application is being submitted to the Retail category
  • This application will be written in .Net 4.5/WPF 

Introduction 

This article describes my entry into the Intel App Innovation Contest for 2013. My application is designed for field-based quality assurance technicians and is based on a need identified in my brother's corporate cleaning business. (See the background for more information.)  The goal of the application is to streamline the evaluation of work done in the field, the creation of reports about said work, and the submission of those reports to key decision makers to dramatically improve the business reaction times and improve overall customer satisfaction.

This application is targeted at Windows 8 Pro systems in desktop mode running on a tablet form factor. It is intended to be used in the field at a customer's location. 

The application is called "Inspection Manager" because that is, well, what it does. It helps inspection technicians and business managers keep track of on-site inspections that may be required as part of a line of business.  And while it was originally conceived to answer a need in a corporate cleaning business, the application is flexible enough that it can be applied to any quality inspection/quality control type of data collection and submission process across many vertical industries.

Key features of the application are: 

  • An inspector will be able to go to a customer location for a scheduled or one-off inspection. This could be a facility inspection, a product inspection, or any other sort of on-site inspection that may need to take place where the inspector has to rate something in one or more dimensions and categories.
  • The inspector will be able to (optionally) use the NFC hardware in a tablet or laptop (if equipped) to read a tag on the item being inspected or in key locations within a facility. The NFC tag ensures that the inspector was physically present at the time of the inspection. Tag reads may also be substituted with or accompanied by location acquisition at the time of the read as a second-level presence verification. (Again, if the device is properly equipped)   
  • The application, in response to the NFC tag or to user interaction, presents a blank inspection report tailored to the specific customer. This may come from local storage or from a cloud back end managed by the company. 
  • The inspector will be able to pick ratings for various inspection dimensions grouped into areas. If there are additional areas which must be inspected, the inspector can modify the report at inspection time by adding or removing groups from the report.  The inspection dimensions within a group are driven by the inspection template and cannot be modified at the time of an inspection to ensure the inspected dimensions within a group are consistent.  (For example, an "Office" area may be defined as a group in the inspection template with a number of dimensions to be inspected such as dusting, trash, windows, etc. At the time of the inspection, the dimensions for the office area are driven by the template but are locked down and may not be added or removed. The Office group, however, may be added to the report multiple times to cover all of the discreet office areas in a given building that may need inspected.)
  • If needed, the inspector will be able to take a picture with the built-in camera (if present) annotating and attaching that to a specific inspection dimension for documentation or for future follow-up.
  • If the inspector is interrupted, the report is saved in a WIP state and persisted to cloud storage for backup if appropriate.
  • When the inspection is complete, the inspector will "submit" the inspection. At this point the inspection may be e-mailed to internal or external contacts as well as being submitted to the cloud back end if appropriate.
  • The inspector will have the option of printing the report on-site via a portable Bluetooth printer or WiFi printer.
  • The application will also push notifications to the user that inspections are due when they have been previously scheduled.
  • A start tile for the application will show pending inspections that must be finished as well as upcoming inspections. 
  • Integration with the Search contract will allow the user to send search queries to the application via the search charm.
  • Integration with the Settings contract will allow the user to modify the application settings via the settings charm.
  • Integration with SkyDrive will give the ability to share inspection templates and store the resulting reports as PDFs.
  • Integration with an Azure-based back end will allow larger companies to centrally maintain and monitor inspection templates, customers, schedules, and results.

The application will be written in C# using WPF with project modifications to ensure it will only run on Windows 8. This is a requirement to access the WinRT sensor stack in a WPF desktop application. The sensor stack is needed for location data.

The program will follow modern UI design principles. Because it is designed for field use, it will be geared towards operation on a tablet using touch as the primary input but will support keyboard/mouse based interactions as well. While it will attempt to look like a WinRT app (opening full screen as topmost and running in a single window) it will be a desktop application which could be windowed and will have a start bar icon.

Background  

The inspiration for this came out of my brother's corporate cleaning business. They constantly struggle with "field intelligence"... knowing what is going on in the field. Their whole business is based on their employees doing their jobs (correctly) at a customer's facility. It is an extreme example of a distributed work environment.

The problem is keeping tabs on what is going on. The work is monotonous and it is easy for a cleaner's focus to stray and cause the quality of their work to suffer. Often, it is too late before the office is notified of an issue because they usually only become aware of a problem when the customer has called in to complain... often loudly... about the problem(s) in their building. 

In his case, he uses field inspectors to make regular and surprise inspection visits to these customer sites to evaluate the work of his cleaners. This information is invaluable as it provides early red flags to problems, documentation when there is an issue, and the ability to provide real, actionable feedback to his cleaners so they can do a better job. But, the inspection process is cumbersome because it is primarily manual and the time delay between a field inspection and the office becoming aware of a problem found in the field can often be days. The reaction time is often unacceptable to the customer reducing the overall benefits of the inspection program.

The solution, at least the one presented here, is to create an electronic version of the inspection reports and provide the ability to instantly submit them back to the main office for action. The tablet form factor is an ideal one for this type of operation. Inspectors can carry a tablet into the customer location and perform their inspections based on an inspection template presented to them for that facility.  This allows the report to be customized to the facility... something that could never be done with paper reports. The ability to easily enter comments and, more importantly, take pictures of problem areas gives the inspector the "ammo" needed to help get issues addressed and ensure the customer stays satisfied.

Performing the inspection is one thing, doing something with it is another. One of the primary features will be the ability to print the results of the inspection to a portable Bluetooth printer or a WiFi printer. This allows the inspector to give the customer an instant copy of the report. Since this application will run in desktop mode, any printer installed in Windows will technically be supported. The wireless or Bluetooth printers are simply the most convenient for this application and therefore the ones being targeted specifically.

Additionally, since many tablets are already equipped with mobile data or can be "activated" with mobile data via a hotspot or WiFi connection, the application will monitor Internet connectivity and will be able to immediately send the inspections to the central office for processing soon after the actual inspection is done. This closes the feedback loop between the time when a problem is identified to when the office becomes aware of it giving the company a jump to begin addressing issues. This reduces the time from days down to hours or even minutes between when a problem is identified and when the office becomes aware of it. 

The data connection will be a challenge. The intermittent nature of a field connection means that the application will have to be aware of the data connection status and gracefully degrade functionality in response. This also means that much of the information will need to be cached locally so the user is presented with it in a timely fashion while ensuring it is updated in the background when a data connection becomes available.  Asynchronous network support, uploading and downloading data in the background without the user's intervention, will be key to making the application responsive.

Broader Appeal

One of the most exciting aspects of this application is that, although it was born in response to a single vertical market, it is applicable across many possible segments. Anywhere that field inspections must be performed that entail rating one or more inspection dimensions and possibly annotating those dimensions or snapping a picture are possible applications for this program. Construction, manufacturing, and service are all industries where a program like this, running on a tablet form factor, could aid in the reduction of the time it takes to identify and correct issues in the field thereby increasing customer satisfaction.  Even facility or QC inspections in retail or restaurant settings are prime positions for this application.

Approaching the problem

The application is ambitious to be sure. So a schedule of implementation is going to be required so that I don't get sidetracked or overwhelmed by the immensity of the challenge.

The initial phase will be to create the UI framework and get the application to the point where it can take an inspection report and e-mail that to someone. The inspections should be configurable by the end user which means there must be a mechanism to define those inspections and an application flexible enough to read and display them as defined. An inspection will ultimately be a hierarchical data structure. At the top is the report and it contains customer information, date/time, location information, and other report-specific information like whether the report was user-initiated or launched from a location NFC tag.

The report will contain one or more inspection groups. Groups would be areas that may need inspected... a logical collection of dimensions that apply to a single thing or area. A report could be just a single area but more likely it will be several. For cleaning inspections, this may be bathrooms, office areas, lobby, etc.

The inspection groups will contain one or more inspection dimensions. These are the actual items on the inspection report. Take a Lobby area as a group example. Dimensions in the lobby may include trash cans, doors, windows, lamps, etc. Each dimension will have a rating. This would be something like poor, acceptable, above average. An office manager or some other administrative person would decide what these ratings should be and they will be applied to the inspection dimensions across the board for consistency.

The hierarchy will be driven by a template file which will define all of the possible values at each area as well as the form of the overall inspection. In the first phase, the application will simply open a template and allow the inspector to fill it in and e-mail the results.

A second phase of the application will be to add the support to save and recall the inspections on the device. This will require the addition of a work status flag to an inspection report so the application can know if an inspection is WIP or completed. I will also need a local database to store the template data and the inspection data in a relational manner. The second phase will also add support for printing the report via a Bluetooth or WiFi printer as well as exporting the report as a PDF.

The third phase will add SkyDrive and/or Google Drive support allowing the user to sync the storage with a cloud storage service for easier access as well as disaster recovery. Multiple copies of the program running on different devices will be able to access shared templates in a common, cloud-based storage location.

The last phase will be adding an Azure-based back end which would allow larger customers willing to pay a subscription to get automatically sync'd information and central management of all of their inspection tablets. Centralized scheduling and reporting will also become available. This phase is post-contest as there will be a significant amount of work to develop the back end and it isn't in-scope for the purposes of this contest.

Data Design 

The key is the design of the data schema to drive the whole system. It must be flexible enough to allow the end user to customize for their purposes but still fit within the hierarchy envisioned for the solution.

Here is a design of the data structure to give you an idea of how the report, group, and dimension entities relate to each other: 

The Data Model

Because the data structure of this application will need to be used across multiple platforms (WPF and Azure for sure) and because I'm a huge fan of MVVM, it makes sense to create a model for this data that exposes it in a platform-agnostic manner. That is where Portable Class Libraries come into play. While PCL's seem to still be an enigma in the .Net space, it is clear that this type of situation is where their power really comes through. To that end, the data model class(es) which will represent the report, group, and dimension objects in code will be written in a PCL project and linked into the main application. Functions for fetching and saving the data will be defined in a seperate PCL project which will specify an interface for the storage method. These interfaces will be implemented on each specific platform to hide the details of data storage on that specific platform. This way they can be re-used on WPF and the Azure platform as a way of exposing the model in a consistent manner.

User Interface

The UI is the key to the application. The data structure and the back end features can be amazing but if the app isn't intuitive and easy to use by people who may not be skilled in the use of computers, let alone tablets, the application will fall flat on its face. To this end, I'm adopting a modern UI design strategy and while the application is a desktop application, I want the look and feel of a Windows Store type application.  To get a jump on that, I plan to use the MahApps.Metro framework as a starting point to make the app look 'modern'. (XAML Spy, which I use and highly recommend, makes use of this framework for its interface.)

The Main Page:

The main page of the application acts as the portal into the information the system will collect and manage. From the main page the user will be able to create a new inspection, a new template, or will be able to edit an existing template. When looking at the new inspection view, the user will see the company's customers as tiles organized by name presenting some additional information such as the last time that location was inspected.  Touching a tile will open a new inspection on that customer. Pressing and holding the tile will open a menu for additional options such as passing the customer address to a mapping application for navigation to that customer.

The user will also be able to swipe right to left to switch to the WIP view where inspections that have been started but not yet finished will be displayed. The WIP inspection tiles will include a "glow" that will vary from green to yellow to red based on the age of the inspection. The list will be organized oldest to newest so the inspector can quickly see the most important WIP inspections. The press and hold on the WIP tiles will allow multiple selection and deletion of the WIP inspections.

The last main section the page supports is the Review view. The review section will allow the user to go back and look at inspections that have been previously submitted in a read-only manner. Once an inspection has been submitted, the application will 'lock' that inspection so that it can't be modified. This preserves the inspection as-is and prevents someone from going back and changing the results of the inspection after the fact. The idea is that the inspection that is printed for the customer should be the same that is stored in the system. The application wouldn't be very trustworthy if the stored report and the one printed for the customer didn't match up. By preventing changes to the submitted inspections, we preserve the 'forensic evidence' of that inspection. Picking an inspection tile on this view will simply return the user to the report page but the ability to edit the data will be removed. The user would be able to reprint a report from this page. 

The New Inspection: 

The new inspection page is where the user will land when they create a new inspection report. The report will have a bar at the top for adding a new group. Selecting the add new group button will give the user a list of the available inspection groups to choose from. Picking one of them will add that group to the report.

The groups are available via expanders which will reveal a grid of the dimensions in that group to be inspected. Each dimension has a name, a rating, an optional comment, and the ability to take a picture and tie it to that dimension. Touch navigation of the grids and touch selection of the ratings will make filling in the reports a breeze for the inspectors. A default rating is applied to all dimensions to save the inspector the time of manually selecting each one.

The action bar at the bottom is where the user will be able to submit the report, print it, and save the inspection report to the cloud. The print button will only be available once the report has been submitted. Again, this is to ensure the submitted version of the report and the printed version line up.

Taking pictures

The camera column on the report contains buttons which will launch the camera on the tablet and allow the inspector to take a picture which will be associated with that dimension. The color of the icon on the data grid will indicate the presence of a picture for that dimension.  Initially, the icon will be black showing that no image is associated it. Pressing the camera button will take the user to the image capture screen.

 

The program will use the CameraControl library from the XAML Toolkit (https://winrtxamltoolkit.codeplex.com/) to manage the camera interaction. The camera control is a nice jump start because everything is there to allow image capture in an async fashion. This library is WinRT-based but since Inspection Manager will be built using the WinRT stack, this won't be a problem for a WPF application running on Windows 8.

Provided the user has allowed Inspection Manager to access the camera (a small point but a very important one none the less!), the camera window will open and present a live view of the camera. The camera button overlaid on the image will snap a picture when pressed. The demo of the camera control uses a neat old-time movie countdown control to show when the image will be snapped. I will be using that paradigm as I really like how it helps the user out. So pressing the button will show the countdown timer then snap the picture. If the user decides they don't want that picture, pressing the capture button a second time will stop the capture in progress.

The double arrows to the left will be present on hardware that has both a front and a back camera. Pressing the button will switch between the cameras. While I doubt anyone would use the front camera on a device equipped with a back one, I'll make it available anyway.  Maybe the inspector will want to take a selfie with the cute receptionist in the lobby...

The magnifying glass button will allow the user to search the image gallery for an image that may have already been taken. This could come in handy when the inspection is being completed "off-line" or maybe in a situation where the customer has snapped a picture and does a bump-transfer to the inspector's tablet. Searching and picking the image from the gallery would allow the customer's image to be imported into the inspection.

The edit icon will only become available when an image is taken or loaded. If an image is snapped live or picked from the find image dialog, the page will automatically change to the edit screen. The button will become active but the image won't automatically become editable.

 

Once an image is snapped, the captured image will be shifted to the left and a splitter bar will appear. To the right of that, a scaled-down version of the live-image from the camera will be displayed. Again, this follows the camera control sample in the xaml toolkit. I really liked the concept so I'm adopting it here.

The user will be able to slide the splitter bar to 0%, 33%, 66% , or 100% of the way across the page similar to the way WinRT snaps two applications on one screen. This lets the user move between the entire captured image, a large captured image and a small live image (as seen above), a small captured image and a large live image, or go back to a full-size live image.  On the live image, if the user presses the capture button again, the capture will repeat (again, cancellable by pressing the button a second time) replacing the previously captured image.

When an image has been captured, the edit button on the left will become active. If the user does nothing and presses the back button, the untouched image is stored with the associated inspection dimension. If the user presses the edit button, the image will enter edit mode.

When the image is captured, it won't be put into a regular image control but rather into a GdPicture.Net image control. (http://www.gdpicture.com/) When the user presses the edit button, the GdPicture control will be put into edit mode and a toolbar will appear which will allow the user to annotate the image with markups such as lines and text. The GdPicture control is a real time saver here as it already supports the image editing and manipulation functions as well as touch manipulation such as two-finger-rotate and pinch-to-zoom. This allows me to embed a pretty capable image editor into the Inspection Manager application with a minimum of effort. Users will expect these features and the program will deliver them without me being forced to implement them.

When the user is done capturing and manipulating the image, pressing the back arrow will return them to the inspection report where they will see the camera icon on the subject dimension has turned red to indicate an image is captured for that dimension. Pressing the red camera will take the user back to the image edit screen (this time the live camera image will be hidden and the edit function will already be enabled) where they can edit the image or slide out the splitter and take a new image replacing the previous one.

On the printed version of the report. The camera column will resolve to a footnote number with the images printed at the end of the report and being referenced by that number. This will allow the images to be printed as a reasonable size but will require someone reading the report to refer back and forth between the report data and the referenced photos.

Interesting aspects 

So there are a number of things which I find interesting about this application that go beyond just the surface of a data collection application. At it's core, Inspection Manager is nothing more than a task-specific data collection tool. But to be useful, it needs to provide more functionality that just that. Data collection is fine and dandy but what you do with the data is what separates competitive companies from the wanna-bees. To that end a few things are priorities when developing this application:

  1. Touch, touch, touch!

    • The  tablet form factor and touch-screen capabilities are changing the way we interact with our computers. Windows 8 is a testimony to how we are undergoing this fundamental change. And videos of toddlers trying to swipe the pages of a magazine reinforce the idea that touch is going to be fundamental to our UI expectations more and more with each hardware release cycle. Developers need to keep this in mind and design their UX to not just incorporate touch, but to be designed from the ground-up with touch as the center of the UX universe. It should be touch with keyboard/mouse support... not the other way around.

      Fortunately, .NET 4.5 and more specifically the WinRT part of the stack, provide a lot of touch support. Designing the UI for touch takes a different thought process but the modern UI design principles are conducive to touch support so I don't think this is going to be a difficult part of the process. However, touch support in control libraries is still lacking. To implement a compelling touch experience in a WPF app will be challenging from the technical standpoint in that it will likely require some custom touch handling code. Hopefully some smart people have developed some libraries to help with this but at this point, I identify touch support as an area of risk for this project.

  2. Keep it simple.

    • The KISS principle is widely cited for a reason. And if you look at modern UI, the KISS principle is in full effect. It isn't coincidence... and for this application, I need to make sure I'm constantly challenging my assumptions and filtering them through this rule. Is there a better way to make this easier on the user? The fact is the target users for this application are not really technical people. They need something intuitive that fits with the paradigm they understand. Many of them may have smart phones but may be uncomfortable with computers in general. This application needs to be simple enough that someone can pick it up, and without instructions, use it to create an inspection report. Sounds simple but this is a challenge because I have to filter the functionality down to a convention (the smart phone) that the user is familiar with.
  3. Code reuse

    • We're all adults here... I don't think I need to make the case as to why we need to reuse code. But I will bring up the fact that Portable Class Libraries allow, for the first time, a truely cross-platform code library that isn't junked up with a bunch of #if compiler directives to force MSBuild to emit platform-specific binaries. With PCL's, I can write the library once and use it on several platforms. I've done this before and I'm excited to jump into it again on this project and use it on something with more teeth. It can be a challenge to write to the lowest-common-denominator sometimes but the upshot of it is that one library will satisfy the data model for several versions of the front end and back end of this application. 

  4. Provide features the business thinks should be there

    • Nothing drives me more crazy than looking for a function in an application which should be there but isn't there because the developers didn't think about or userstand their use cases. The businesses and users that will utilize this application are going to expect certain things. They will expect it to automatically detect the network connection. They will expect to be able to share and manage templates across multiple devices. They will expect that there will be some analytics and maybe an API for getting data out of the system. These are the types of things I need to think about, identify, and prioritize as I build out this application.
  5. Don't get too aggressive

    • This is the balance against the need to provide features businesses want. Everyone that uses the app will want to see something else added to it. Even I will be guilty of that. But allowing feature creep makes the application turn into something that can't possibly be delivered within the contest timeframe or worse, within a timeframe where it may be useful to the marketplace. They say you should strike while the iron is hot and the niche this app fills is a void in the marketplace right now. Limiting what the app does to a simple feature set that it executes well will help it get out in front and establish a reputation and traction before a competitor can establish a presence. In today's business environment, this is important and can determine an app's, and a company's, ultimate success or failure. 

 

Project Updates

(or how my hair is quickly turning gray)

Since we are well into the midst of the contest and I've got a lull at work, I've got some time to update this article on my progress. In a nutshell, it is progressing nicely and I'm very pleased with how the application is shaping up but time is ticking off very quickly and I certainly feel pressured to get it done. One thing I've always found is that no matter how much effort you put into thinking about and designing the project up front, some things change as get into the implementation phase and turning out a quality product always takes more time that you think it will up front.

One of the big decisions I made on this project was to create the data structure and objects in Portable Class Libraries. PCLs allow you to create libraries that are compatible across runtime stacks. Currently, you can write a library once that will run on WPF, Silverlight, ASP, etc without resorting to shared code files and compiler directives. For projects that may have a presence on multiple platforms, PCLs are now my go-to solution.  My intention is to bring this app to other runtimes, WinRT specifically, so using PCLs to model my data was a no-brainer to me.

 

Data Objects

For this project I've created two PCL projects. One of them, Inspection Manager Interfaces contains only the interface definitions for the various data objects the project will require. This creates a single library which exposes those interfaces and can be used across any platform that I would like. The interface library also exposes a custom exception that needs to be thrown by the implementations of these interfaces in the event the desired object can't be found in storage.

Here is what the interface for an inspection report looks like:

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace AerialBreak.InspectionManager.DAO.Interfaces
{
    /// <summary>
    /// Provides the interface for the Inspection Report
    /// </summary>
    public interface IInspectionReport : INotifyPropertyChanged
    {
        /// <summary>
        /// The ID number for this record
        /// </summary>
        int ID { get; set; }
        /// <summary>
        /// The name of the report
        /// </summary>
        string Name { get; set; }
        /// <summary>
        /// The ID of the associated customer record
        /// </summary>
        int CustomerID { get; set; }
        /// <summary>
        /// The ID of the associated customer location
        /// </summary>
        int LocationID { get; set; }
        /// <summary>
        /// The ID of the associated scoring system
        /// </summary>
        int ScoringID { get; set; }
        /// <summary>
        /// The name or username of the inspector creating the report
        /// </summary>
        string UserName { get; set; }
        /// <summary>
        /// The date & time the record was created
        /// </summary>
        DateTime? StartDateTime { get; set; }
        /// <summary>
        /// The date and time the record was last saved
        /// </summary>
        DateTime? LastSavedDateTime { get; set; }
        /// <summary>
        /// The date and time the report was submitted
        /// </summary>
        DateTime? SubmittedDateTime { get; set; }
        /// <summary>
        /// The tracking value for the current record state
        /// </summary>
        int CurrentState { get; set; }
        /// <summary>
        /// The data stored in the NFC tag read in association with the report
        /// </summary>
        string NfcTagValue { get; set; }
    }
}

I have a second interface which is a pluralized form of the first that declares the methods which will be required to work with these objects.

 /// <summary>
/// Provides the public interface for the management of the inspection report objects
/// </summary>
public interface IInspectionReports
{
    /// <summary>
    /// Gets all of the reports in the system
    /// </summary>
    /// <returns>Collection of report objects</returns>
    ObservableCollection<IInspectionReport> GetReports();
    /// <summary>
    /// Gets all of the reports in the system for the specified customer and optional location
    /// </summary>
    /// <param name="customerID">The customer ID to search for</param>
    /// <param name="locationID">(OPTIONAL) The location ID to filter the results with</param>
    /// <returns>Collection of the report objects that match the required criteria</returns>
    /// <remarks>If the customer ID does not exist in the system, this method will throw an InvalidIDException.
    /// If the location ID is specified and does not exist in the system or is not associated with the corresponding customer, the 
    /// method will thrown and InvalidIDException.</remarks>
    ObservableCollection<IInspectionReport> GetReports(int customerID, int locationID = 0);
    /// <summary>
    /// Gets all of the records in the system with the specified user name
    /// </summary>
    /// <param name="userName">The user name to search for</param>
    /// <returns>Collection of inspection records with the specified user name attached.</returns>
    /// <remarks>If there are no records with the specified user name, the method will return an empty collection.</remarks>
    ObservableCollection<IInspectionReport> GetReports(string userName);

    /// <summary>
    /// Gets all the reports that were opened during the specified date range.
    /// </summary>
    /// <param name="rangeBegin">(OPTIONAL) The start date and time to return records within. (Inclusive)</param>
    /// <param name="rangeEnd">(OPTIONAL) The ending date and time to return records within. (Exclusive)</param>
    /// <returns>Collection of inspection records which fall between the start and end dates.</returns>
    /// <remarks>The default begin and end times are the minimum and maximum date values causing the method to return all records in the system. 
    /// If no records match, an empty collection will be returned.</remarks>
    ObservableCollection<IInspectionReport> GetReportsByStartDate(DateTime? rangeBegin, DateTime? rangeEnd);
    /// <summary>
    /// Gets all of the reports that were saved during the specified date range.
    /// </summary>
    /// <param name="rangeBegin">(OPTIONAL) The start date and time to return records within. (Inclusive)</param>
    /// <param name="rangeEnd">(OPTIONAL) The ending date and time to return records within. (Exclusive)</param>
    /// <returns>Collection of inspection records which fall between the start and end dates.</returns>
    /// <remarks>The default begin and end times are the minimum and maximum date values causing the method to return all records in the system. 
    /// If no records match, an empty collection will be returned.</remarks>
    ObservableCollection<IInspectionReport> GetReportsByLastSavedDate(DateTime? rangeBegin, DateTime? rangeEnd);
    /// <summary>
    /// Gets all of the records that were submitted during the specified date range.
    /// </summary>
    /// <param name="rangeBegin">(OPTIONAL) The start date and time to return records within. (Inclusive)</param>
    /// <param name="rangeEnd">(OPTIONAL) The ending date and time to return records within. (Exclusive)</param>
    /// <returns>Collection of inspection records which fall between the start and end dates.</returns>
    /// <remarks>The default begin and end times are the minimum and maximum date values causing the method to return all records in the system. 
    /// If no records match, an empty collection will be returned.</remarks>
    ObservableCollection<IInspectionReport> GetReportsBySubmittedDate(DateTime? rangeBegin, DateTime? rangeEnd);
    /// <summary>
    /// Gets all of the records within the current state range
    /// </summary>
    /// <param name="lowerState">The lower value of the current state range (INCLUSIVE).</param>
    /// <param name="upperState">(OPTIONAL) The upper value of the state range. If this value is lower than the lower one, the maximum value will be used. (INCLUSIVE)</param>
    /// <returns>Collection of inspection records where the current state falls within the specified range. If no records match, an empty collection will be returned.</returns>
    ObservableCollection<IInspectionReport> GetReportsByState(int lowerState, int upperState = 0);
    /// <summary>
    /// Gets all of the records with the specified NFC Data
    /// </summary>
    /// <param name="nfcData">The NFC data to search for</param>
    /// <returns>Collection of inspection records with matching NFC data. If no records match, an empty collection will be returned.</returns>
    ObservableCollection<IInspectionReport> GetReportByNfcString(string nfcData);

    /// <summary>
    /// Returns the record with the corresponding ID
    /// </summary>
    /// <param name="reportID">The ID of the inspection record to return.</param>
    /// <returns>The specified inspection record. If the record ID does not exist, the method will thrown an InvalidIDException.</returns>
    IInspectionReport GetReport(int reportID);

    /// <summary>
    /// Adds or updates the system with the specified record.
    /// </summary>
    /// <param name="report">The inspection record to save or update.</param>
    /// <returns>The ID of the record.</returns>
    /// <remarks>If the ID parameter of the report is not set, the method will assume the record should be added to the system. 
    /// If the ID parameter is set, the method will attempt to update the specified record. If the record does not exist, an InvalidIDException will be thrown.</remarks>
    int SetReport(IInspectionReport report);

    /// <summary>
    /// Sets the LastSavedDate for the specified record to the current date & time.
    /// </summary>
    /// <param name="reportID">The ID of the record to update</param>
    /// <returns>True if successful</returns>
    /// <remarks>If the ID is not present in the system, the method will return an InvalidIDExpception.</remarks>
    bool SetReportLastSavedDate(int reportID);
    /// <summary>
    /// Sets the Submitted date for the report to the current date & time.
    /// </summary>
    /// <param name="reportID">The ID of the record to update</param>
    /// <returns>True if successful</returns>
    /// <remarks>If the ID is not present in the system, the method will return an InvalidIDExpception.</remarks>
    bool SetReportSubmittedDate(int reportID);
    /// <summary>
    /// Sets the current state for the specified report to the provided state value
    /// </summary>
    /// <param name="reportID">The ID of the record to update</param>
    /// <param name="newState">The new state value.</param>
    /// <returns>True if successful</returns>
    /// <remarks>If the ID is not present in the system, the method will return an InvalidIDExpception.</remarks>
    bool SetReportCurrentState(int reportID, int newState);
    /// <summary>
    /// Sets the NFC data for the specified record
    /// </summary>
    /// <param name="reportID">The ID of the record to update</param>
    /// <param name="nfcData">The NFC data string to save</param>
    /// <returns>True if successful</returns>
    /// <remarks>If the ID is not present in the system, the method will return an InvalidIDExpception.</remarks>
    bool SetReportNfcData(int reportID, string nfcData);

} 

This setup gives me two interfaces, one for the data object and one for the methods required to work with the data object. I did it this way because I've found it easier and more flexible with work with the objects this way. One thing I can do, specifically, with this setup is implement the actual data objects in another library or within a project then implement the handling methods seperately using whatever is appropriate for the platform. For example, on this project, I chose to use SQL Lite as the backing data store. Within the actual WPF project, I've implemented the data handling methods using the interfaces defined in the PCL but setup to use SQL Lite as the storage mechanism.  A WinRT version of this project may use Azure Mobile Services to store the data. From the application's standpoint, it doesn't matter because the interface (and the binary!) is the same for both projects.

One thing I've done with the data that I wish I hadn't was to create implementations of the data objects in a second PCL. I thought that would be a handy was to reuse them across projects and while that seems reasonable on paper, it ended up placing an extra level of inheritence on the objects that I didn't really need. When I got into the process of actually consuming the objects, I found that I needed to decorate them for SQL Lite to be able to automatically create tables with them. If I put the decorations on the properties in the PCL, I created a dependency on SQL Lite in the PCL library which may not be able to be fulfilled on one of the other platforms that a PCL could support. So my only option was to inherit from those objects and create an override property which included the decorations. Here is what the PCL implementation of a data object looks like:

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using AerialBreak.InspectionManager.DAO.Interfaces;
using AerialBreak.InspectionManager.Utility;

namespace AerialBreak.InspectionManager.DAO.Objects
{
    public class KeyedSetting : NotifyPropertyChangedBase, IKeyedSetting, INotifyDataErrorInfo
    {

        private int _id;
        public int ID
        {
            get
            {
                return _id;
            }
            set
            {
                if (_id != value)
                {
                    _id = value;
                    OnPropertyChanged();
                }
            }
        }

        private int _associatedRecordId;
        public int AssociatedRecordID
        {
            get
            {
                return _associatedRecordId;
            }
            set
            {
                if (_associatedRecordId != value)
                {
                    _associatedRecordId = value;
                    OnPropertyChanged();
                }
            }
        }

        private string _parentKey;
        public string ParentKey
        {
            get
            {
                return _parentKey;
            }
            set
            {
                if (_parentKey != value)
                {
                    _parentKey = value;
                    OnPropertyChanged();
                }
            }
        }

        private string _itemKey;
        public string ItemKey
        {
            get
            {
                IsItemKeyValid(_itemKey);
                return _itemKey;
            }
            set
            {
                if (IsItemKeyValid(value) && _itemKey != value)
                {
                    _itemKey = value;
                    OnPropertyChanged();
                }
            }
        }

        private string _value;
        public string Value
        {
            get
            {
                IsValueValid(_value);
                return _value;
            }
            set
            {
                if (IsValueValid(value) && _value != value)
                {
                    _value = value;
                    OnPropertyChanged();
                }
            }
        }

        private string _modifier;
        public string Modifier
        {
            get
            {
                return _modifier;
            }
            set
            {
                if (_modifier != value)
                {
                    _modifier = value;
                    OnPropertyChanged();
                }
            }
        }

        #region Data Validation
        private const string ITEMKEY_ERROR = "A name for the key must be provided.";
        private const string VALUE_ERROR = "A value for the key must be provided.";

        #region Property Validators
        // Validates the Name property, updating the errors collection as needed.
        public bool IsItemKeyValid(string value)
        {
            bool isValid = true;

            if (string.IsNullOrEmpty(value))
            {
                AddError("ItemKey", ITEMKEY_ERROR, false);
                isValid = false;
            }
            else RemoveError("ItemKey", ITEMKEY_ERROR);

            return isValid;
        }
        public bool IsValueValid(string value)
        {
            bool isValid = true;

            if (string.IsNullOrEmpty(value))
            {
                AddError("Value", VALUE_ERROR, false);
                isValid = false;
            }
            else RemoveError("Value", VALUE_ERROR);

            return isValid;
        }

        #endregion
        #region INotifyDataErrorInfo boilerplate
        private Dictionary<String, List<String>> errors = new Dictionary<string, List<string>>();


        // Adds the specified error to the errors collection if it is not
        // already present, inserting it in the first position if isWarning is
        // false. Raises the ErrorsChanged event if the collection changes.
        public void AddError(string propertyName, string error, bool isWarning)
        {
            if (!errors.ContainsKey(propertyName))
                errors[propertyName] = new List<string>();

            if (!errors[propertyName].Contains(error))
            {
                if (isWarning) errors[propertyName].Add(error);
                else errors[propertyName].Insert(0, error);
                RaiseErrorsChanged(propertyName);
            }
        }

        // Removes the specified error from the errors collection if it is
        // present. Raises the ErrorsChanged event if the collection changes.
        public void RemoveError(string propertyName, string error)
        {
            if (errors.ContainsKey(propertyName) &&
                errors[propertyName].Contains(error))
            {
                errors[propertyName].Remove(error);
                if (errors[propertyName].Count == 0) errors.Remove(propertyName);
                RaiseErrorsChanged(propertyName);
            }
        }

        public void RaiseErrorsChanged(string propertyName)
        {
            if (ErrorsChanged != null)
                ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
        }
        #endregion
        #region INotifyDataErrorInfo Members
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
        public IEnumerable GetErrors(string propertyName)
        {
            if (String.IsNullOrEmpty(propertyName) ||
                !errors.ContainsKey(propertyName)) return null;
            return errors[propertyName];
        }

        public bool HasErrors
        {
            get
            {
                return errors.Count > 0;
            }
        }
        #endregion
        #endregion
    }
} 

Because of my goof, I have a class within the WPF project which implements a "derived" version of this data object.

using AerialBreak.InspectionManager.DAO.Interfaces;
using AerialBreak.InspectionManager.DAO.Objects;

namespace InspectionManager_WPF.DerivedDataObjects
{
    public class DerivedKeyedSetting : KeyedSetting, IKeyedSetting
    {
        [SQLite.AutoIncrement, SQLite.PrimaryKey]
        public int ID
        {
            get
            {
                return base.ID;
            }
            set
            {
                base.ID = value;
            }
        }
    }
}

You can see that all it does is redeclare the ID property with two decorations. I should have implemented the data objects in the main project instead of the second PCL which would have let me skip the inheritence by decorating the implementation directly. If I get time, I'll go back and unwind this.

 

INotifyDataErrorInfo 

The other thing you might see in the implementation is the INotifyDataErrorInfo interface. I love this mechanism on data model classes like this one. It creates a standard interface for data validation and communication. The cool thing is that UI controls like text box and others are aware of this interface and will automatically tip the user if the provided values don't meet the requirement. Here you can see where the UI is tipping the user they need to enter a legal business name (this is on the customer setup screen). The amazing part is I wrote no code in the View Model or the View to make this happen! The text box is detecting the presence of the INotifyDataErrorInfo on the class it is bound to and handling the notification for me.

 

User Interface

I chose to go with a Metro Modern UI style for this project and I'm really glad I did. The main interface uses a "tab" view without the tabs. Here we can see the "manage" section (tab) showing the sub-sections (customers and templates) along with two tiles. (The empty tile is "add" and should have a plus in it.)

I used the MahApps metro toolkit to get the UI style and I've been really happy with it. It has a bit of a learning curve as the documentation is a bit thin but overall I've been super happy with it. The pano control that comes with it is nice and works very well. It certainly let me get to a UI that looks this good with little effort on my part. 

One of the other things I really like is the watermark attached control that comes in that toolkit. It allows you to attach watermarks to textboxes which look nice and give your user tips on what type of information you are looking for. A great example of that control is the customer setup page. The user has to enter information about the customer record they are creating and the watermarks give the app a professional look while also tipping the user as to what they should be providing.

Like all good data driven applications, I have a grid. Actually several of them but who is counting? Smile | :) For my program the user needs to setup a scoring system since that is really what the whole program is about. Elements of an inspection are scored using values from the defined scoring system. A grid is a natural choice for this. The implementation in my program looks like this:

 

One of the neat features I added is the data pager at the bottom of the grid. Since this program is going to be used on a tablet that limited vertical space, I didn't want the user to have to infinately scroll to find something they are looking for. So the data pager limits the grid entries to 10 per page automatically paging the data for me if the user has more than 10 rows in their data set. While this isn't a particular worry when defining the scoring system, it does become an issue when creating the actual inspection report. An office area or a bathroom or anything else really may easily have more than 10 elements that must be inspected and rated. To keep the UI compact and managable, the data pager comes in really handy!

Installment 1 wrap-up

So far, the project is going pretty close to plan. I know that I needed to create the data objects first, the UI (and application framework) next, then get all of the "overhead" stuff done. That is essentially complete at this point. The app will run and a user can create scoring systems, report templates, customers, and locations. Basically everything needed to actually start a report and perform an inspection. The next phase is bringing all of that together so the user can create an inspection report and submit it back to the office. After that, non-critical stuff like NFC support will be added provided I have time. However, testing is important and I need time to get this project into the hands of some people that can use it for real before I submit it. The time crunch is really putting pressure on that part of the project. So we'll see how it all washes out in the end. Now, if you'll excuse me I've got an app to finish up...

History      

  • Aug. 19, 2013 - Initial submission.
  • Aug. 21, 2013 - Added description and mockups of image capture and editing workflow.  
  • Oct. 25, 2013 - Added first update on project progress.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Jason Gleim
Software Developer (Senior) HealthEdge Software Inc
United States United States
I'm a Senior Software Developer with HealthEdge Software in Columbus, OH working primarily on the Silverlight portions of our platform. I've been working in .Net since version 1.0 and I am skilled in most aspects of the platform. I also speak fluent binary.
 
On the side I own Aerial Break Software under which I publish personal projects and hope to make some money... someday.
 
My fun job is shooting Fireworks professionally as an Ohio licensed fireworks exhibitor. I'm licensed for outdoor and indoor fireworks and I've been on the crew for Red, White, and Boom in Columbus, OH since 2002.
Follow on   Twitter   Google+

Comments and Discussions

 
GeneralMy vote of 5 PinprofessionalDrABELL14-Nov-13 9:11 
GeneralRe: My vote of 5 PinprofessionalJason Gleim14-Nov-13 9:19 
GeneralRe: My vote of 5 PinprofessionalDrABELL14-Nov-13 9:26 
QuestionHow's app development going? Will you be submitting on time? PinstaffKevin Priddle22-Oct-13 10:38 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.141022.1 | Last Updated 25 Oct 2013
Article Copyright 2013 by Jason Gleim
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid