Wednesday, December 22, 2010

Scanning Images to PDF

In this post I will show how to use Windows Image Acquisition (WIA) interface to scan images and convert them to PDF using converter from my previous post.

There is a nice intro article on WIA written by Scott Hanselman several years ago. Please read it, I referred to it several times over the years. There is also some information available on Wikipedia.

So what is WIA:

WIA is a Microsoft driver model and application programming interface (API) for Microsoft Windows Me and later Windows operating systems that enables graphics software to communicate with imaging hardware such as scanners, digital cameras and Digital Video-equipment. It was first introduced in 2000 as part of Windows Me, and continues to be the standard imaging device and API model through successive Windows versions. It is implemented as an on-demand service in Windows XP and later Windows operating systems.

If you are developing for Windows XP you will need to download this tool. It requires one time administrative installation on a client machine. I created a little batch file to install these files:

echo off

cls

echo Copying WIA Files ...

cd c:\

copy \\YOUR_INSTALL_FOLDER\WIA\wiaaut.chi c:\windows\help
copy \\YOUR_INSTALL_FOLDER\WIA\wiaaut.chm c:\windows\help
copy \\YOUR_INSTALL_FOLDER\WIA\wiaaut.dll c:\windows\system32

RegSvr32 WIAAut.dll

echo DONE!!!

If you are developing for Windows Vista or Windows 7 you need to know this:

“In Windows XP, WIA runs in the LocalSystem context. Because of the security ramifications of running a service as LocalSystem whereby a buggy driver or malicious person would have unrestricted access to the system, the WIA service in Windows Server 2003 and Windows Vista operates in the LocalService context. This can result in compatibility issues when using a driver designed for Windows XP.”

“Windows Vista has the WIA Automation library built-in. Also, WIA supports push scanning and multi-image scanning. Push scanning allows initiating scans and adjusting scanning parameters directly from the scanner control panel. Multi-image scanning allows you to scan several images at once and save them directly as separate files. However, video content support is removed from WIA for Windows Vista. Microsoft recommends using the newer Windows Portable Devices API.”

Now on to the hardware. Since in my project I needed to scan multiple pages at once, I used Fujitsu fi-5110C and newer model (works just as well) fi-6110. The problem is that in Windows XP for some reason these scanners don’t report correctly the “Empty Tray” status. So I had to assume some things for multi page scanning. The logic is simple:

- scan until no exception.
- when exception occurs checks if anything is scanned.
- if pages were scanned then simply exit.
- if no pages were scanned then report a problem.

Here is a snippet responsible for this:


// if we reached this point, then scanner is probably initialized. 
bool isTransferring = true;
foreach (string format in item.Formats)
{
while (isTransferring)
{
try
{
WIA.ImageFile file = (item.Transfer(format)) as WIA.ImageFile;
if (file != null)
{
Stream stream = new MemoryStream();
stream.Write(file.FileData.get_BinaryData() as Byte[], 0, (file.FileData.get_BinaryData() as Byte[]).Length);

// resetting stream position to beginning after data was written into it.
stream.Position = 0;
Bitmap bitmap = new Bitmap(stream);
images.Add(bitmap);
}
else
isTransferring = false; // something happend and we didn't get image
}
catch (Exception ex)
{
// most likely done transferring
// I was not able to find a way to pole scanner for paper feed status.
isTransferring = false;

// scanner's paper feeder was not loaded with paper.
if (images.Count() == 0)
throw new ImageScannerException("Scanner is not loaded with paper or not ready.");

}
}
}

That’s multi page scanning in Windows XP, unlike earlier statement from Wiki that it is supported only in Vista.

Another not so straight forward part is how to initialize the scanner. By default when you call WIA API to scan it loads manufacturers interface. In my case I wanted to save all the settings using internally developed user form and from then on pass these settings on to a scanner every time scanner is used.

I created a class to store scanner settings.

ScannerSettings:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.Security.Permissions;

namespace Bmec.ScanLibrary.Scan
{
public enum ScanningSize
{
Letter,
Legal
}

public enum Color
{
GrayScale=0,
Color=1,
BlackWhite=4
}

public class ScannerSettings: ISerializable
{
#region Fields
/// <summary>
/// Width of the scanned image.
/// Default is 8500.
/// </summary>
private double _width = 8.5;

/// <summary>
/// Height of the scanned image.
/// Default is 11000.
/// </summary>
private double _height = 11;

/// <summary>
/// Optical Resolution.
/// </summary>
private int _resolution = 120;

/// <summary>
/// Color setting, default is 0 - grayscale.
/// </summary>
private int _color = 0; // grayscale

/// <summary>
/// Horizontal Cropping.
/// Default is 0.5 in
/// </summary>
private double _horizontalCrop = 0.5; // if cropping required it can be set in here for horizontal.

/// <summary>
/// Vertical Cropping
/// Default is 0.5 in
/// </summary>
private double _verticalCrop = 0.5; // if cropping required it can be set in here for vertical.

/// <summary>
/// Standard scanning size
/// </summary>
private ScanningSize _size = ScanningSize.Letter;

#endregion //Fields

#region Constructors

/// <summary>
/// Default Constructor
/// </summary>
public ScannerSettings()
{

}

/// <summary>
/// Creates settings object for WIA scanner.
/// </summary>
/// <param name="size">Standard paper size.</param>
/// <param name="resolution">scanning resolution (e.g. for 300x300 pass 300).</param>
/// <param name="color">Color setting, default is gray scale.</param>
public ScannerSettings(ScanningSize size, int resolution, Color color)
{

Size = size;
_color = (int)color;
_resolution = resolution;
}

/// <summary>
/// Creates settings object for WIA scanner and resolution of 150x150 pixels.
/// </summary>
/// <param name="size">Standard paper size.</param>
public ScannerSettings(ScanningSize size)
{
Size = size;
}

/// <summary>
/// Creates customized settings for WIA scanner
/// </summary>
/// <param name="width">Scanner's sheet feed width.</param>
/// <param name="height">Scanner's sheet feed height.</param>
/// <param name="resolution">Optical resolution.</param>
public ScannerSettings(double width, double height, int resolution, double horizontalCrop, double verticalCrop)
{
Width = width;
Height = height;
_resolution = resolution;
_horizontalCrop = horizontalCrop;
_verticalCrop = verticalCrop;
}


#endregion //Constructors

#region Properties

/// <summary>
/// Color setting, default is 0 - grayscale.
/// </summary>
public int Color
{
get
{
return _color;
}
set { _color = value; }
}

/// <summary>
/// Height of the scanned image.
/// Default is 11 in.
/// </summary>
public double Height
{
get
{
return _height;
}
set
{
_height = value;
}
}

/// <summary>
/// Horizontal Cropping.
/// Default is 0.5 in.
/// </summary>
public double HorizontalCrop
{
get
{
return _horizontalCrop;
}
set
{
_horizontalCrop = value;
}
}

/// <summary>
/// Optical Resolution. Default is 120 pixels per inch.
/// </summary>
public int Resolution
{
get
{
return _resolution;
}
set
{
_resolution = value;
}
}

/// <summary>
/// Standard scanning size
/// </summary>
public ScanningSize Size
{
get
{
return _size;
}
set
{
_size = value;
Width = GetWidth(_size);
Height = GetHeight(_size);
}
}

/// <summary>
/// Vertical Cropping
/// Default is 0.5 in.
/// </summary>
public double VerticalCrop
{
get
{
return _verticalCrop;
}
set
{
_verticalCrop = value;
}
}

/// <summary>
/// Width of the scanned image.
/// Default is 8.5 in.
/// </summary>
public double Width
{
get
{
return _width;
}
set
{
_width = value;
}
}
#endregion //Properties

#region Methods
public static double GetWidth(ScanningSize size)
{
if (size == ScanningSize.Legal)
{
return 8.5;
}
else if (size == ScanningSize.Letter)
{
return 8.5;
}
else return 8.5;
}

public static double GetHeight(ScanningSize size)
{
if (size == ScanningSize.Legal)
{
return 14;
}
else if (size == ScanningSize.Letter)
{
return 11;
}
else return 11;
}
#endregion //Methods

#region ISerializable Members

/// <summary>
/// Constructor for serializer.
/// </summary>
/// <param name="info">Serialization data.</param>
/// <param name="context">Serialization streaming context.</param>
public ScannerSettings(SerializationInfo info, StreamingContext context)
{

Color = info.GetInt32("Color");
Height = info.GetDouble("Height");
HorizontalCrop = info.GetDouble("HorizontalCrop");
Resolution = info.GetInt32("Resolution");
Nullable<ScanningSize> size = info.GetValue("Size", typeof(Nullable<ScanningSize>)) as Nullable<ScanningSize>;
Size = ((size != null)? size.Value : ScanningSize.Letter);
VerticalCrop = info.GetDouble("VerticalCrop");
Width = info.GetDouble("Width");
}

/// <summary>
/// Implementation for ISerializable.
/// </summary>
/// <param name="info">Serialization data.</param>
/// <param name="context">Serialization streaming context.</param>
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Color", Color);
info.AddValue("Height", Height);
info.AddValue("HorizontalCrop", HorizontalCrop);
info.AddValue("Resolution", Resolution);
Nullable<ScanningSize> size = Size;
info.AddValue("Size", size);
info.AddValue("VerticalCrop", VerticalCrop);
info.AddValue("Width", Width);

}

#endregion
}
}

And then apply these settings in the following manner:

// setting properties (dimensions and resolution of the scanning.)
setItem(item, "6146", _scannerSettings.Color); // color setting (default is gray scale)
setItem(item, "6147", _scannerSettings.Resolution); //horizontal resolution
setItem(item, "6148", _scannerSettings.Resolution); // vertical resolution
setItem(item, "6149", _scannerSettings.HorizontalCrop); // horizontal starting position
setItem(item, "6150", _scannerSettings.VerticalCrop); // vertical starting position
setItem(item, "6151", (int)((double)_scannerSettings.Resolution * (_scannerSettings.Width - _scannerSettings.HorizontalCrop))); // width
setItem(item, "6152", (int)((double)_scannerSettings.Resolution * (_scannerSettings.Height - _scannerSettings.VerticalCrop))); // height

Another annoying window is selecting the scanner itself:

clip_image002

This is taken care by saving device id into local user settings and then reading it off:

#region Private Methods

private void setItem(IItem item, object property, object value)
{
WIA.Property aProperty = item.Properties.get_Item(ref property);
aProperty.set_Value(ref value);
}

private Device GetDevice(Settings settings)
{
Device device=null;
CommonDialogClass dialog = new CommonDialogClass();
if (String.IsNullOrEmpty(settings.DeviceId))
{
device = dialog.ShowSelectDevice(WiaDeviceType.ScannerDeviceType, true, false);
if (device != null)
{
settings.DeviceId = device.DeviceID;
settings.Save();
}
}
return device;
}

private Device GetDevice(string deviceId)
{
WIA.DeviceManager manager = new DeviceManager();
Device device=null;
foreach( DeviceInfo info in manager.DeviceInfos)
{
if(info.DeviceID == deviceId)
{
device = info.Connect();
break;
}
}
return device;
}
#endregion //Private Methods

Here is the rest of the code. Scanner initialization was a little tricky without fully understanding the WIA API. So I had to resort to several hacks and empirical methods :).

Below are the other two classes you will need to scan images.

ImageScanner:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Bmec.ScanLibrary.Properties;
using System.Configuration;
using WIA;
using System.Drawing;
using System.IO;
using System.Security.Permissions;
using System.Security.Principal;

[assembly: System.Security.Permissions.FileIOPermission(SecurityAction.RequestMinimum, Unrestricted = true)]
namespace Bmec.ScanLibrary.Scan
{
public class ImageScanner
{
#region Fields
private Device _device=null;
private ScannerSettings _scannerSettings;
#endregion //Fields

#region Constructors

/// <summary>
///
/// </summary>
/// <exception cref="ImageScannerException">This exception can be thrown,
/// if any errors are present while initializing scanner.</exception>
public ImageScanner(ScannerSettings scannerSettings)
{
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);

if (scannerSettings == null)
throw new ImageScannerException("Scanner Settings are not specified!");

_scannerSettings = scannerSettings;

Settings settings = new Settings();
try
{
// try automatically select scanner.
if (!String.IsNullOrEmpty(settings.DeviceId))
{
_device = GetDevice(settings.DeviceId);
}

// if didn't succeed try manually select scanner.
if (_device == null)
{
_device = GetDevice(settings);
}

}
catch (Exception ex)
{
throw new ImageScannerException("Scanner was not selected or is not available!\n"+ex.Message);
}

// if device is still null, then throw an error.
if (_device == null)
throw new ImageScannerException("Scanner was not selected or is not available!");

}

#endregion //Constructors

#region Public Methods

public void SetDevice()
{
Settings settings = new Settings();
_device = GetDevice(settings);
}

/// <summary>
/// This will scan images, but everything must be ready before doing this.
/// If device (scanner) is not ready an ImageScannerException will be thrown.
/// </summary>
/// <returns>Collection of scanned images.</returns>
public IEnumerable<Bitmap> Scan()
{
List<Bitmap> images = new List<Bitmap>();

if (_device == null)
throw new ImageScannerException("Scanner is not available! Please select a scanner and try again.");

WIA.Item item = null;
try
{
foreach (DeviceCommand command in _device.Commands)
{
item = _device.ExecuteCommand(command.CommandID);
}
}
catch (Exception ex)
{
// skip this
}

try
{
// if item is still not initialized, we'll try a different approach
if (item == null)
{
foreach (Item i in _device.Items)
{
foreach (DeviceCommand command in i.Commands)
{
item = _device.ExecuteCommand(command.CommandID);
}
}
}
}
catch (Exception ex)
{
// skip this
}

try
{
// if item is still null, we'll pick the first available
foreach (WIA.Item i in _device.Items)
{
item = i;
break;
}
}
catch (Exception ex)
{
//skip this
}

if(item == null)
throw new ImageScannerException("Scanner is not ready!\nPlease turn scanner on, feed paper into the scanner and try again.");
try
{
// setting properties (dimensions and resolution of the scanning.)
setItem(item, "6146", _scannerSettings.Color); // color setting (default is gray scale)
setItem(item, "6147", _scannerSettings.Resolution); //horizontal resolution
setItem(item, "6148", _scannerSettings.Resolution); // vertical resolution
setItem(item, "6149", _scannerSettings.HorizontalCrop); // horizontal starting position
setItem(item, "6150", _scannerSettings.VerticalCrop); // vertical starting position
setItem(item, "6151", (int)((double)_scannerSettings.Resolution * (_scannerSettings.Width - _scannerSettings.HorizontalCrop))); // width
setItem(item, "6152", (int)((double)_scannerSettings.Resolution * (_scannerSettings.Height - _scannerSettings.VerticalCrop))); // height
}
catch (Exception ex)
{
throw new ImageScannerException("Was not able to set scanning parameters.\n" + ex.Message);
}

// if we reached this point, then scanner is probably initialized.
bool isTransferring = true;
foreach (string format in item.Formats)
{
while (isTransferring)
{
try
{
WIA.ImageFile file = (item.Transfer(format)) as WIA.ImageFile;
if (file != null)
{
Stream stream = new MemoryStream();
stream.Write(file.FileData.get_BinaryData() as Byte[], 0, (file.FileData.get_BinaryData() as Byte[]).Length);

// resetting stream position to beginning after data was written into it.
stream.Position = 0;
Bitmap bitmap = new Bitmap(stream);
images.Add(bitmap);
}
else
isTransferring = false; // something happend and we didn't get image
}
catch (Exception ex)
{
// most likely done transferring
// I was not able to find a way to pole scanner for paper feed status.
isTransferring = false;

// scanner's paper feeder was not loaded with paper.
if (images.Count() == 0)
throw new ImageScannerException("Scanner is not loaded with paper or not ready.");

}
}
}

return images;
}

#endregion //Public Methods

#region Private Methods

private void setItem(IItem item, object property, object value)
{
WIA.Property aProperty = item.Properties.get_Item(ref property);
aProperty.set_Value(ref value);
}

private Device GetDevice(Settings settings)
{
Device device=null;
CommonDialogClass dialog = new CommonDialogClass();
if (String.IsNullOrEmpty(settings.DeviceId))
{
device = dialog.ShowSelectDevice(WiaDeviceType.ScannerDeviceType, true, false);
if (device != null)
{
settings.DeviceId = device.DeviceID;
settings.Save();
}
}
return device;
}

private Device GetDevice(string deviceId)
{
WIA.DeviceManager manager = new DeviceManager();
Device device=null;
foreach( DeviceInfo info in manager.DeviceInfos)
{
if(info.DeviceID == deviceId)
{
device = info.Connect();
break;
}
}
return device;
}
#endregion //Private Methods
}
}

ImageScannerException:

using System;
using System.Runtime.Serialization;
using System.Security.Permissions;

namespace Bmec.ScanLibrary.Scan
{
public class ImageScannerException : Exception, ISerializable
{
#region Properties

public string UserFriendlyMessage { get; private set; }

#endregion //Properties

#region Constructors

/// <summary>
/// All messages are set to String.Empty.
/// </summary>
public ImageScannerException()
: base(String.Empty)
{
UserFriendlyMessage = String.Empty;
}

/// <summary>
/// UserFriendlyMessage is set to message.
/// </summary>
/// <param name="message">Error message.</param>
public ImageScannerException(string message)
: base(message)
{
UserFriendlyMessage = message;
}

/// <summary>
/// UserFriendlyMessage is set to message.
/// </summary>
/// <param name="message">Error Message</param>
/// <param name="innerException">Inner Exception if any, null otherwise. </param>
public ImageScannerException(string message, Exception innerException)
: base(message, innerException)
{
UserFriendlyMessage = message;
}

/// <summary>
/// Custom exception for ImageScanner.
/// </summary>
/// <param name="message">Detailed error message.</param>
/// <param name="userFriendlyMessage">User friendly error message.</param>
public ImageScannerException(string message, string userFriendlyMessage)
: base(message)
{
UserFriendlyMessage = userFriendlyMessage;
}

/// <summary>
/// Custom exception for ImageScanner.
/// </summary>
/// <param name="message">Detailed error message.</param>
/// <param name="innerException">Inner Exception if any, null otherwise.
/// (If inner exception is null use different overloaded constructor)</param>
/// <param name="userFriendlyMessage">User friendly error message.</param>
public ImageScannerException(string message, Exception innerException, string userFriendlyMessage)
: base(message, innerException)
{
UserFriendlyMessage = userFriendlyMessage;
}

/// <summary>
/// Custom exception for ImageScanner.
/// </summary>
/// <param name="info">Serialization data.</param>
/// <param name="context">Serialization streaming context.</param>
public ImageScannerException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
UserFriendlyMessage = info.GetString("UserFriendlyMessage");
}

#endregion //Constructors

#region Methods

/// <summary>
/// Implementation for ISerializable.
/// </summary>
/// <param name="info">Serialization data.</param>
/// <param name="context">Serialization streaming context.</param>
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("UserFriendlyMessage", UserFriendlyMessage);
}

#endregion //Methods
}
}

Now to be able to easily use these classes I created a helper class which combines scanning functionality with PDF conversion:

ScannerHelper:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Bmec.ScanLibrary.Converters;
using System.IO;

namespace Bmec.ScanLibrary.Scan
{
public class ScannerHelper
{
public ScannerSettings ScannerSettings { get; set; }
public PdfSettings PdfSettings { get; set; }

/// <summary>
/// Constructor with default settings.
/// </summary>
public ScannerHelper()
{
ScannerSettings = new ScannerSettings(ScanningSize.Letter,120, Color.BlackWhite);
PdfSettings = new PdfSettings(PdfOrientation.Portrait, PdfSize.Letter);
}

/// <summary>
/// Customizable constructor
/// </summary>
/// <param name="scannerSettings">Settings for the scanner.</param>
/// <param name="pdfSettings">Settings for PDF page(s).</param>
public ScannerHelper(ScannerSettings scannerSettings, PdfSettings pdfSettings)
{
ScannerSettings = scannerSettings;
PdfSettings = pdfSettings;
}

public void ScanToPdf(string newPdfFileName)
{
ImageScanner scanner = new ImageScanner(ScannerSettings);
PdfConverter converter = new PdfConverter(PdfSettings);

converter.SaveFrom(scanner.Scan(), newPdfFileName);
}

/// <summary>
/// This method scans to Pdf Stream.
/// </summary>
/// <returns>Return open stream with PDF binary data in it. The stream needs to be flushed and closed after use.</returns>
public Stream ScanToPdf()
{
ImageScanner scanner = new ImageScanner(ScannerSettings);
PdfConverter converter = new PdfConverter(PdfSettings);

return converter.ConvertFrom(scanner.Scan());
}
}
}

Here are couple of overloads. One lets you scan into a file, the other one returns stream of PDF data, which you may use in your program.

That’s it for scanning.

Tuesday, December 21, 2010

Converting Images to PDF

There is myriad of third party components which allow .NET developers to create PDF files. Most of them are not free. While this is all good what I noticed is that most of the developers overlook the fact that Visual Studio already has something that creates PDF files and it is Microsoft Report Viewer, which can be distributed for free.

In this post I will try to explain how to use Report Viewer to generate PDF files. For my organization I created a reusable library which can generate PDF files from images. These images are usually scanned documents and may come directly from the scanner or loaded from the file system. I will write about scanning component in my next post.

Following the framework naming “traditions” (conventions), my library is called Bmec.ScanLibrary which includes PDF converter and scanning functionality.

So how do we use Microsoft Report Viewer. The main idea behind this library is to generate a report with embedded images and export this report as a PDF file, pretty simple.

Reports for Microsoft Report Viewer are stored as XML files. I created couple of reports using Report Designer to study the structure of the report. Then based on my findings I created a LINQ query, which iterates over supplied collection of images and outputs XML. Here is the main piece:

private Stream ConvertTo(IEnumerable<Bitmap> images, Stream resultStream)
{
if (images == null images.Count() == 0)
throw new PdfConverterException("No images to convert!");

// creating in-memory report
XDocument reportXML = CreateRDLC(images);
Stream stream = new MemoryStream();
XmlWriter writer = XmlWriter.Create(stream);
reportXML.Save(writer);

//disposing
writer.Flush();
writer.Close();

LocalReport report = new LocalReport();
stream.Position = 0;
report.LoadReportDefinition(stream);

//disposing
stream.Flush();
stream.Close();

String mimeType = "";
String encoding = "";
String[] streams;
Warning[] warnings;
Byte[] renderedBytes;
StringBuilder deviceInfo = new StringBuilder();
String fileExtension;
deviceInfo.Append("<DeviceInfo>");
deviceInfo.Append(" <OutputFormat>PDF</OutputFormat>");
deviceInfo.Append("</DeviceInfo>");

report.Refresh();
renderedBytes = report.Render("PDF", deviceInfo.ToString(), out mimeType, out encoding, out fileExtension, out streams, out warnings);

resultStream.Write(renderedBytes, 0, renderedBytes.Length);
return resultStream;
}

private XDocument CreateRDLC(IEnumerable<Bitmap> images)
{
int i = 0;
XNamespace rd = "http://schemas.microsoft.com/SQLServer/reporting/reportdesigner";
XNamespace xmlns = "http://schemas.microsoft.com/sqlserver/reporting/2005/01/reportdefinition";
XDocument doc = new XDocument(
new XDeclaration("1.0", "utf-8", "no"),
new XElement(xmlns + "Report",
new XAttribute(XNamespace.Xmlns + "rd", "http://schemas.microsoft.com/SQLServer/reporting/reportdesigner"),
new XElement(xmlns + "InteractiveHeight", _settings.HeightLabel),
new XElement(xmlns + "InteractiveWidth", _settings.WidthLabel),
new XElement(xmlns + "RightMargin", _settings.RightMarginLabel),
new XElement(xmlns + "LeftMargin", _settings.LeftMarginLabel),
new XElement(xmlns + "BottomMargin", _settings.BottomMarginLabel),
new XElement(xmlns + "TopMargin", _settings.TopMarginLabel),
new XElement(xmlns + "Width", _settings.BodyWidthLabel),
new XElement(rd + "SnapToGrid", "true"),
new XElement(rd + "DrawGrid", "true"),
new XElement(rd + "ReportId", (new Guid()).ToString()),
new XElement(xmlns + "EmbeddedImages",
from Bitmap image in images
select new XElement(xmlns + "EmbeddedImage",
new XAttribute("Name", "i" + image.GetHashCode().ToString()),
new XElement(xmlns + "MIMEType", "image/jpeg"),
new XElement(xmlns + "ImageData", BitmapToByte(image)))

),
new XElement(xmlns + "Body",
new XElement(xmlns + "ReportItems",
from Bitmap image in images
select
new XElement(xmlns + "Image",
new XAttribute("Name", "ImageName" + image.GetHashCode().ToString()),
new XElement(xmlns + "Sizing", "FitProportional"),
new XElement(xmlns + "Height", _settings.BodyHeightLabel),
new XElement(xmlns + "Top", ((i++) * _settings.BodyHeight).ToString() + "in"),
new XElement(xmlns + "MIMEType", "image/jpeg"),
new XElement(xmlns + "Source", "Embedded"),
new XElement(xmlns + "Style"),
new XElement(xmlns + "ZIndex", 1),
new XElement(xmlns + "Value", "i" + image.GetHashCode().ToString()))
),
new XElement(xmlns + "Height", _settings.BodyHeightLabel)
),
new XElement(xmlns + "Language", "en-US")
)
);
return doc;
}


Since images need to be embedded into report we need to convert them into binary data. Below is the function to do just that.



private String BitmapToByte(Bitmap image)
{
String result=String.Empty;

// the file size is limited here to .NET 128 MB per array per ApplicationDomain.
// so if image file is close or greater than 128 MB this will fail.
try
{
Stream str = new MemoryStream();
image.Save(str, ImageFormat.Jpeg);
Byte[] output = new Byte[str.Length];
str.Position = 0;
str.Read(output, 0, (int)str.Length);
result = Convert.ToBase64String(output, Base64FormattingOptions.None);
str.Flush();
str.Close();
}
catch (OutOfMemoryException ex)
{
throw new PdfConverterException(ex.Message, ex, "The file size is too large. It is limited to 128 MB. Try smaller image.");
}
catch (Exception ex)
{
throw new PdfConverterException(ex.Message, ex);
}
return result;
}

Once this is done we can return to the caller either binary stream of PDF data like so:


/// <summary>
/// Convert to a Stream containing Pdf binary data from a list of images.
/// </summary>
/// <param name="images">List of images.</param>
/// <exception cref="PdfConverterException">This exception could be thrown if conversion fails.
/// Usually due to file size is too large.</exception>
/// <returns>Open stream with position set to 0 (beginning).
/// Stream can be read, but needs to be flushed and closed when done.</returns>
public Stream ConvertFrom(IEnumerable<Bitmap> images)
{
Stream resultStream = new MemoryStream();
Stream outStream = ConvertTo(images, resultStream);
outStream.Position = 0;
return outStream;
}

or save it directly to file:


/// <summary>
/// Saves list of images into specified pdf file.
/// If file already exists, it will be deleted.
/// </summary>
/// <param name="images">List of images.</param>
/// <param name="pdfFilePath">Pdf file path where images will be saved. If file exists it will be deleted.</param>
public void SaveFrom(IEnumerable<Bitmap> images, string pdfFilePath)
{
if (File.Exists(pdfFilePath))
File.Delete(pdfFilePath);

Stream resultStream = new FileStream(pdfFilePath, FileMode.OpenOrCreate);
Stream stream = ConvertTo(images, resultStream);
stream.Flush();
stream.Close();
}

When building class libraries or frameworks it is important to use XML comments like these:


/// <summary>
/// Saves list of images into specified pdf file.
/// If file already exists, it will be deleted.
/// </summary>
/// <param name="images">List of images.</param>
/// <param name="pdfFilePath">Pdf file path where images will be saved. If file exists it will be deleted.</param>

Why? Because they will show up in Intellisense and also if building documentation these comments are automatically extracted into a very neat Help file. Sandcastle along with GUI for it are free tools for just that!

Below is the full source code for the 3 classes related to PDF:

PdfConverter

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Reporting.WinForms;

namespace Bmec.ScanLibrary.Converters
{
public class PdfConverter
{
PdfSettings _settings;

/// <summary>
/// Default constructor.
/// </summary>
public PdfConverter()
{
_settings = new PdfSettings(PdfOrientation.Portrait, PdfSize.Letter);
}

/// <summary>
/// Creates instance for PdfConverter for various convert options.
/// </summary>
/// <param name="settings">Pdf export settings, related to pages sizes.
/// If settings is null, we use default settings.</param>
public PdfConverter(PdfSettings settings)
{
if (settings == null)
settings = new PdfSettings(PdfOrientation.Portrait, PdfSize.Letter);

_settings = settings;
}

/// <summary>
/// Convert to a Stream containing Pdf binary data from a list of images.
/// </summary>
/// <param name="images">List of images.</param>
/// <exception cref="PdfConverterException">This exception could be thrown if conversion fails.
/// Usually due to file size is too large.</exception>
/// <returns>Open stream with position set to 0 (beginning).
/// Stream can be read, but needs to be flushed and closed when done.</returns>
public Stream ConvertFrom(IEnumerable<Bitmap> images)
{
Stream resultStream = new MemoryStream();
Stream outStream = ConvertTo(images, resultStream);
outStream.Position = 0;
return outStream;
}

/// <summary>
/// Convert to a Stream containing Pdf binary data from a single image.
/// </summary>
/// <param name="image">Instance of Bitmap image.</param>
/// <exception cref="PdfConverterException">This exception could be thrown if conversion fails.
/// Usually due to file size is too large.</exception>
/// <returns>Open stream with position set to 0 (beginning).
/// Stream can be read, but needs to be flushed and closed when done.</returns>
public Stream ConvertFrom(Bitmap image)
{
Stream resultStream = new MemoryStream();
List<Bitmap> images = new List<Bitmap>();
images.Add(image);
Stream outStream = ConvertTo(images, resultStream);
outStream.Position = 0;
return outStream;
}

/// <summary>
/// Saves list of images into specified pdf file.
/// If file already exists, it will be deleted.
/// </summary>
/// <param name="images">List of images.</param>
/// <param name="pdfFilePath">Pdf file path where images will be saved. If file exists it will be deleted.</param>
public void SaveFrom(IEnumerable<Bitmap> images, string pdfFilePath)
{
if (File.Exists(pdfFilePath))
File.Delete(pdfFilePath);

Stream resultStream = new FileStream(pdfFilePath, FileMode.OpenOrCreate);
Stream stream = ConvertTo(images, resultStream);
stream.Flush();
stream.Close();
}

/// <summary>
/// Saves image into specified pdf file.
/// If file already exists, it will be deleted.
/// </summary>
/// <param name="image">Instance of Bitmap image.</param>
/// <param name="pdfFilePath">Pdf file path where image will be saved. If file exists it will be deleted.</param>
public void SaveFrom(Bitmap image, string pdfFilePath)
{
if (File.Exists(pdfFilePath))
File.Delete(pdfFilePath);

if (image == null)
throw new PdfConverterException("No images to convert!");

Stream resultStream = new FileStream(pdfFilePath, FileMode.OpenOrCreate);
List<Bitmap> images = new List<Bitmap>();
images.Add(image);
Stream stream = ConvertTo(images, resultStream);
stream.Flush();
stream.Close();
}

private Stream ConvertTo(IEnumerable<Bitmap> images, Stream resultStream)
{
if (images == null images.Count() == 0)
throw new PdfConverterException("No images to convert!");

// creating in-memory report
XDocument reportXML = CreateRDLC(images);
Stream stream = new MemoryStream();
XmlWriter writer = XmlWriter.Create(stream);
reportXML.Save(writer);

//disposing
writer.Flush();
writer.Close();

LocalReport report = new LocalReport();
stream.Position = 0;
report.LoadReportDefinition(stream);

//disposing
stream.Flush();
stream.Close();

String mimeType = "";
String encoding = "";
String[] streams;
Warning[] warnings;
Byte[] renderedBytes;
StringBuilder deviceInfo = new StringBuilder();
String fileExtension;
deviceInfo.Append("<DeviceInfo>");
deviceInfo.Append(" <OutputFormat>PDF</OutputFormat>");
deviceInfo.Append("</DeviceInfo>");

report.Refresh();
renderedBytes = report.Render("PDF", deviceInfo.ToString(), out mimeType, out encoding, out fileExtension, out streams, out warnings);

resultStream.Write(renderedBytes, 0, renderedBytes.Length);
return resultStream;
}

private XDocument CreateRDLC(IEnumerable<Bitmap> images)
{
int i = 0;
XNamespace rd = "http://schemas.microsoft.com/SQLServer/reporting/reportdesigner";
XNamespace xmlns = "http://schemas.microsoft.com/sqlserver/reporting/2005/01/reportdefinition";
XDocument doc = new XDocument(
new XDeclaration("1.0", "utf-8", "no"),
new XElement(xmlns + "Report",
new XAttribute(XNamespace.Xmlns + "rd", "http://schemas.microsoft.com/SQLServer/reporting/reportdesigner"),
new XElement(xmlns + "InteractiveHeight", _settings.HeightLabel),
new XElement(xmlns + "InteractiveWidth", _settings.WidthLabel),
new XElement(xmlns + "RightMargin", _settings.RightMarginLabel),
new XElement(xmlns + "LeftMargin", _settings.LeftMarginLabel),
new XElement(xmlns + "BottomMargin", _settings.BottomMarginLabel),
new XElement(xmlns + "TopMargin", _settings.TopMarginLabel),
new XElement(xmlns + "Width", _settings.BodyWidthLabel),
new XElement(rd + "SnapToGrid", "true"),
new XElement(rd + "DrawGrid", "true"),
new XElement(rd + "ReportId", (new Guid()).ToString()),
new XElement(xmlns + "EmbeddedImages",
from Bitmap image in images
select new XElement(xmlns + "EmbeddedImage",
new XAttribute("Name", "i" + image.GetHashCode().ToString()),
new XElement(xmlns + "MIMEType", "image/jpeg"),
new XElement(xmlns + "ImageData", BitmapToByte(image)))

),
new XElement(xmlns + "Body",
new XElement(xmlns + "ReportItems",
from Bitmap image in images
select
new XElement(xmlns + "Image",
new XAttribute("Name", "ImageName" + image.GetHashCode().ToString()),
new XElement(xmlns + "Sizing", "FitProportional"),
new XElement(xmlns + "Height", _settings.BodyHeightLabel),
new XElement(xmlns + "Top", ((i++) * _settings.BodyHeight).ToString() + "in"),
new XElement(xmlns + "MIMEType", "image/jpeg"),
new XElement(xmlns + "Source", "Embedded"),
new XElement(xmlns + "Style"),
new XElement(xmlns + "ZIndex", 1),
new XElement(xmlns + "Value", "i" + image.GetHashCode().ToString()))
),
new XElement(xmlns + "Height", _settings.BodyHeightLabel)
),
new XElement(xmlns + "Language", "en-US")
)
);
return doc;
}

private String BitmapToByte(Bitmap image)
{
String result=String.Empty;

// the file size is limited here to .NET 128 MB per array per ApplicationDomain.
// so if image file is close or greater than 128 MB this will fail.
try
{
Stream str = new MemoryStream();
image.Save(str, ImageFormat.Jpeg);
Byte[] output = new Byte[str.Length];
str.Position = 0;
str.Read(output, 0, (int)str.Length);
result = Convert.ToBase64String(output, Base64FormattingOptions.None);
str.Flush();
str.Close();
}
catch (OutOfMemoryException ex)
{
throw new PdfConverterException(ex.Message, ex, "The file size is too large. It is limited to 128 MB. Try smaller image.");
}
catch (Exception ex)
{
throw new PdfConverterException(ex.Message, ex);
}
return result;
}
}

PdfConverterException


using System;
using System.Runtime.Serialization;
using System.Security.Permissions;


namespace Bmec.ScanLibrary.Converters
{
public class PdfConverterException: Exception, ISerializable
{
#region Properties

public string UserFriendlyMessage { get; private set; }

#endregion //Properties

#region Constructors

/// <summary>
/// All messages are set to String.Empty.
/// </summary>
public PdfConverterException()
: base(String.Empty)
{
UserFriendlyMessage = String.Empty;
}

/// <summary>
/// UserFriendlyMessage is set to message.
/// </summary>
/// <param name="message">Error message.</param>
public PdfConverterException(string message)
: base(message)
{
UserFriendlyMessage = message;
}

/// <summary>
/// UserFriendlyMessage is set to message.
/// </summary>
/// <param name="message">Error Message</param>
/// <param name="innerException">Inner Exception if any, null otherwise. </param>
public PdfConverterException(string message, Exception innerException)
:base(message, innerException)
{
UserFriendlyMessage = message;
}

/// <summary>
/// Custom exception for PdfConverter.
/// </summary>
/// <param name="message">Detailed error message.</param>
/// <param name="userFriendlyMessage">User friendly error message.</param>
public PdfConverterException(string message, string userFriendlyMessage)
: base(message)
{
UserFriendlyMessage = userFriendlyMessage;
}

/// <summary>
/// Custom exception for PdfConverter.
/// </summary>
/// <param name="message">Detailed error message.</param>
/// <param name="innerException">Inner Exception if any, null otherwise.
/// (If inner exception is null use different overloaded constructor)</param>
/// <param name="userFriendlyMessage">User friendly error message.</param>
public PdfConverterException(string message, Exception innerException, string userFriendlyMessage)
: base(message, innerException)
{
UserFriendlyMessage = userFriendlyMessage;
}

/// <summary>
/// Custom exception for PdfConverter.
/// </summary>
/// <param name="info">Serialization data.</param>
/// <param name="context">Serialization streaming context.</param>
public PdfConverterException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
UserFriendlyMessage = info.GetString("UserFriendlyMessage");
}

#endregion //Constructors

#region Methods

/// <summary>
/// Implementation for ISerializable.
/// </summary>
/// <param name="info">Serialization data.</param>
/// <param name="context">Serialization streaming context.</param>
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("UserFriendlyMessage", UserFriendlyMessage);
}

#endregion //Methods

}
}

PdfSettings

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Bmec.ScanLibrary.Converters
{
public enum PdfOrientation
{
Portrait,
Landscape
}

public enum PdfSize
{
Letter,
Legal
}

public class PdfSettings
{
#region Fields

/// <summary>
/// width for pdf export in inches
/// </summary>
private double _width=8.5;

/// <summary>
/// height for pdf export in inches
/// </summary>
private double _height=11;

/// <summary>
/// Left margin.
/// by default we set it to 0.5 inches
/// </summary>
private double _leftMargin=0.5;

/// <summary>
/// Right margin.
/// by default we set it to 0.5 inches
/// </summary>
private double _rightMargin=0.5;

/// <summary>
/// Top margin.
/// by default we set it to 0.5 inches
/// </summary>
private double _topMargin=0.5;

/// <summary>
/// Bottom margin.
/// by default we set it to 0.5 inches
/// </summary>
private double _bottomMargin=0.5;

/// <summary>
/// Size of PDF output pages.
/// </summary>
private PdfSize _size;

/// <summary>
/// Orientation of PDF output pages.
/// </summary>
private PdfOrientation _orientation;

#endregion //Fields

#region Constructors

/// <summary>
/// Constructor which creates custom settings.
/// Margins can be set separate in Properties.
/// Default margins are 0.5 in.
/// </summary>
/// <param name="width">Width in inches.</param>
/// <param name="height">Height in inches.</param>
public PdfSettings(double width, double height)
{
Width = width;
Height = height;
}

/// <summary>
/// Constructor accepts standard settings.
/// </summary>
/// <param name="orientation">Page orientation.</param>
/// <param name="size">Page size. </param>
public PdfSettings(PdfOrientation orientation, PdfSize size)
{
Orientation = orientation;
Size = size;
}

/// <summary>
/// Default Constructor
/// </summary>
public PdfSettings()
{
}

#endregion //Constructors

#region Properties


/// <summary>
/// Bottom margin.
/// by default it is set to 0.5 inches.
/// If bottom and top margins are greater then height, then they are set to default.
/// </summary>
public double BottomMargin
{
get
{
return _bottomMargin;
}
set
{
if (value + _topMargin > Height)
{
_bottomMargin = 0.5;
_topMargin = 0.5;
}
else
_bottomMargin = value;
}
}

/// <summary>
/// Formatted label for Margin value;
/// </summary>
public string BottomMarginLabel
{
get { return BottomMargin.ToString("00.00") + "in"; }
}

/// <summary>
/// height for pdf export in inches
/// </summary>
public double Height
{
get
{
return _height;
}
set
{
_height = value;
}
}

/// <summary>
/// Left margin.
/// by default it is set to 0.5 inches.
/// If Right and Left margins are greater then width, then they are set to default.
/// </summary>
public double LeftMargin
{
get
{
return _leftMargin;
}
set
{
if (value + _rightMargin > Width)
{
_leftMargin = 0.5;
_rightMargin = 0.5;
}
else
_leftMargin = value;
}
}

/// <summary>
/// Formatted label for Margin value;
/// </summary>
public string LeftMarginLabel
{
get { return LeftMargin.ToString("00.00") + "in"; }
}

/// <summary>
/// Orientation of PDF output pages.
/// </summary>
public PdfOrientation Orientation
{
get
{
return _orientation;
}
set
{
_orientation = value;

if (_orientation == PdfOrientation.Landscape)
{
if (Size == PdfSize.Legal)
{
Height = 8.5;
Width = 14;
}
else if (Size == PdfSize.Letter)
{
Height = 8.5;
Width = 11;
}
}
else
{
if (Size == PdfSize.Legal)
{
Height = 14;
Width = 8.5;
}
else if (Size == PdfSize.Letter)
{
Height = 11;
Width = 8.5;
}
}
}
}

/// <summary>
/// Right margin.
/// by default it is set to 0.5 inches.
/// If Right and Left margins are greater then width, then they are set to default.
/// </summary>
public double RightMargin
{
get
{
return _rightMargin;
}
set
{
if (value + _leftMargin > Width)
{
_leftMargin = 0.5;
_rightMargin = 0.5;
}
else
_rightMargin = value;
}
}

/// <summary>
/// Formatted label for Margin value;
/// </summary>
public string RightMarginLabel
{
get { return RightMargin.ToString("00.00") + "in"; }
}

/// <summary>
/// Size of PDF output pages.
/// </summary>
public PdfSize Size
{
get
{
return _size;
}
set
{
_size = value;

if (_orientation == PdfOrientation.Landscape)
{
if (Size == PdfSize.Legal)
{
Height = 8.5;
Width = 14;
}
else if (Size == PdfSize.Letter)
{
Height = 8.5;
Width = 11;
}
}
else
{
if (Size == PdfSize.Legal)
{
Height = 14;
Width = 8.5;
}
else if (Size == PdfSize.Letter)
{
Height = 11;
Width = 8.5;
}
}
}
}

/// <summary>
/// Top margin.
/// by default it is set to 0.5 inches.
/// If bottom and top margins are greater then height, then they are set to default.
/// </summary>
public double TopMargin
{
get
{
return _topMargin;
}
set
{
if (value + _bottomMargin > Height)
{
_bottomMargin = 0.5;
_topMargin = 0.5;
}
else
_topMargin = value;
}
}

/// <summary>
/// Formatted label for Margin value;
/// </summary>
public string TopMarginLabel
{
get { return TopMargin.ToString("00.00") + "in"; }
}

/// <summary>
/// width for pdf export in inches
/// </summary>
public double Width
{
get
{
return _width;
}
set
{
_width = value;
}
}

/// <summary>
/// width for pdf export in inches
/// </summary>
public string WidthLabel
{
get
{
return Width.ToString("00.00") + "in";
}
}

/// <summary>
/// height for pdf export in inches
/// </summary>
public string HeightLabel
{
get
{
return Height.ToString("00.00") + "in";
}
}

/// <summary>
/// Returns formatted height of the pdf body = (height - top and bottom margins).
/// </summary>
public string BodyHeightLabel
{
get { return (Height - _topMargin - _bottomMargin).ToString("00.00") + "in"; }
}

/// <summary>
/// Returns height of the pdf body = (height - top and bottom margins).
/// </summary>
public double BodyHeight
{
get { return (Height - _topMargin - _bottomMargin); }
}

/// <summary>
/// Returns formatted width of the pdf body = (width - left and right margins).
/// </summary>
public string BodyWidthLabel
{
get { return (Width - _leftMargin - _rightMargin).ToString("00.00") + "in"; }
}

/// <summary>
/// Returns width of the pdf body = (width - left and right margins).
/// </summary>
public double BodyWidth
{
get { return (Width - _leftMargin - _rightMargin); }
}


#endregion //Properties
}
}

Here is an example of what came out of the library when I supplied two images:

image

Let me know how it goes for you.

In my next post I will write about accessing scanners from .NET using Windows Image Acquisition Interface or WIA for short and how to scan images into PDF.

Monday, December 20, 2010

Code Metrics

This is continuation of my previous post on MVVM project I recently completed.

I decided to analyze some of the code metrics. I really like NDepend add-in for Visual Studio, but it is not a free tool. So I had to resort to .NET Reflector and multiple community developed add-ins.

Here is a screen shot from CodeMetrics add-in for .NET Reflector.

image

This tool also provides nice statistics for classes, methods  or modules from IL assemblies.
Some people like to talk a lot about Cyclomatic Complexity, while it is useful only on a per method basis. Cyclomatic Complexity of the whole project grows dramatically as your project grows and becomes inadequate in such cases. The rule of thumb is if Cyclomatic Complexity greater than 15 the project will have increased difficulty in knowledge transfer, testing, debugging and maintenance. Additionally such code tends to be more rigid which means changes are more difficult to implement.

Below is a recommendation given by NDepend:

Recommendations: Methods where CC is higher than 15 are hard to understand and maintain. Methods where CC is higher than 30 are extremely complex and should be split in smaller methods (except if they are automatically generated by a tool).

In my project there are 14 193 methods, most of the ones with higher cyclomatic complexity were generated and the rest 13 923 have cyclomatic complexity of 9 or less, which is about 98% of all code base.

Another useful metrics for types (classes) is “Depth of Inheritance Tree”. Here is a recommendation given by NDepend about depth of inheritance tree:

Depth of Inheritance Tree (DIT): The Depth of Inheritance Tree for a class or a structure is its number of base classes (including the System.Object class thus DIT >= 1).
Recommendations: Types where DepthOfInheritance is higher or equal than 6 might be hard to maintain. However it is not a rule since sometime your classes might inherit from tier classes which have a high value for depth of inheritance. For example, the average depth of inheritance for framework classes which derive from System.Windows.Forms.Control is 5.3.

In SHMMP Manager Average Depth of Inheritance Tree is 4.08, which is very good.

There are other numbers CodeMetrics add-in provides, some you may use to impress some people, usually those unfamiliar with programming :). Such as number of lines of code. In SHMMP Manager there are about 200 000 lines of code. This number is even greater if taken straight from IL disassembler and is 1 295 965, which, if printed, translates to roughly 43 000 pages. Of course 90% of these lines were generated by the compiler :)

NDepend can also analyze code for testability and compositionality and provides nice dependency graphs and matrices. Based on Dependency Matrix, for an example, one may start refactoring process. Keeping track of dependencies especially important when building common class libraries or frameworks.

So try these tools and see if they are helpful.

One of My Recent MVVM Projects

I decided to start a small series of post related to one of my recent MVVM projects and the issues I faced and the solutions I found while working on this project.

First let me give a little bit of history and an overview.

I work at Biomedical Engineering Center and about year and a half ago our company put a freeze on spending as a result multiple project were shelved. This project, let’s call it SHMMP Manager, was supposed to be developed by an outside vendor, which we already picked and provided with functional specifications. After the freeze we had to scramble and bail out of the contract. Likely the vendor was very accommodative even though they were loosing a big chunk of money.

The decision was made to develop application internally, but only after all the issues with the current application were resolved. Current (now previous) application was developed 10 years ago in Access VBA with SQL Server back end. So it took several month to close outstanding issues and in October of 2009 we began planning and requirements gathering. Even though we had most of the specs developed we felt it would be wise to revisit the document once again and make any changes if necessary.

A month after talking to subject matter experts I decided the document was sufficiently complete and started working on designing the application’s architecture. Several month into the project I realized that my interrogation of subject matter experts was not thorough enough and had to make multiple adjustments to the application’s design.

Here is a high level overview of application features:

- Automatic PDF web reports generation
- Flexible permissions manager for report users
- Document imaging, context search and retrieval
- Flexible error validations
- Robust billing
- Full auditing
- Automation of business processes which were not automated in the current application.
- Better data mining.

After requirements, functional specs and database design the next step was to develop data migration module. This module was suppose to convert old data to new structure. While this sounds simple it didn’t turn out to be. Old data was dirty, not normalized and had completely different structure. So this step turned into a major cleaning project with multiple corrections to database design. Most of the changes were related to improving enforcement of data integrity. Additional steps were taking to split database into 3 parts:

- online processing (tuned for making changes INSERTs and UPDATEs)
- analytical processing (tuned for fast retrieval, for reports)
- auditing (tracking changes made to the first database)

Such separation allowed us to maintain high performance for data entry and reporting, and as a positive side effect this design also helped us to improved our disaster recovery procedures. For example if one database crashes the other two are not affected and may continue operating. On top of this we also have multiple hardware and software redundancies, such as back ups, transaction log shipping, extra server and HDD mirroring. At times it is easier to restore a single incorrect user operation from one of the two extra databases.

Constant change requests early on in the project forced me to use code generation techniques.

For SQL Server it was pretty straight forward as T-SQL allows to walk the structure of the database. So all the triggers, many stored procedures and two extra databases were automatically generated by a piece of code which was much smaller. This allowed me to rapidly introduce structural changes and generate about 50K lines of code in couple of minutes. It also improved robustness as code was tested and evaluated very frequently while it’s foot print was very small.

For C# generation I had to use Text Templating Transformation Toolkit (T4) provided by Microsoft as part of Visual Studio IDE. Microsoft was already using a script for it’s LINQ to SQL classes and I decided to use it as a base example to generate view model (business layer) for the application. It turned out to be very helpful. Now it generates about 80K lines of code for all the tables, views and functions in data model.

Here is a list of several screen shots from the application.

Release Notes 
Application Release Notes

 

image  
Users have the ability to change Welcome message, which appears on the reports web site.

 

image
Web reports web site is auto generated as well. Once report is designed in SQL Server Report Designed it is dropped into Report Server, then using SHMMP Manager authorized users decide who may see what reports and what data on the reports. An external user who received permissions to view particular datasets and reports will see it appear in available reports section.
All report parameters are automatically generated based on the XML structure of the selected report.

image  
An example of generated report.

 

image
An example of data entry form with error validations. There are two validation pipeline in the application: Errors and warnings. Errors prevent changes to the database, while warnings notify users of a potential mistake.

image
Users may change multiple settings in the application, such as scanner settings, or size of the font and forms.

 

image 
Or a background picture.

image
But the most complicated piece of the application is data mining. Users may see and navigated to associated records within database thus uncovering problems or learning data easily.

image
Plus some fun animations throughout the app to keep users’ attention  :)

In the next post I’ll dive deeper to cover some of the trouble points in developing MVVM apps and my personal goals for the project.

Tuesday, March 30, 2010

WPF DataGrid ScrollIntoView

Technorati Tags: ,,,

In my previous post I talked about using Attached Behaviors to scroll selected item into view. It turns out there is a bug in WPF DataGrid and ScrollIntoView could sometimes throw NullReferenceException when VirtualizingStackPanel.IsVirtualizing="True" .

To avoid this exception there was a solution suggested on this forum http://wpf.codeplex.com/Thread/View.aspx?ThreadId=39458 which basically executes ScrollIntoView on a thread with a very low priority.

Here is my previous solution with suggested work around.

public class DataGridBehavior
{
#region AutoScrollIntoView

public static bool GetAutoScrollIntoView(DataGrid dataGrid)
{
return (bool)dataGrid.GetValue(AutoScrollIntoViewProperty);
}

public static void SetAutoScrollIntoView(
DataGrid dataGrid, bool value)
{
dataGrid.SetValue(AutoScrollIntoViewProperty, value);
}

public static readonly DependencyProperty AutoScrollIntoViewProperty =
DependencyProperty.RegisterAttached(
"AutoScrollIntoView",
typeof(bool),
typeof(DataGridBehavior),
new UIPropertyMetadata(false, OnAutoScrollIntoViewWhenSelectionChanged));

static void OnAutoScrollIntoViewWhenSelectionChanged(
DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
DataGrid dataGrid = depObj as DataGrid;
if (dataGrid == null)
return;

if (!(e.NewValue is bool))
return;

if ((bool)e.NewValue)
dataGrid.SelectionChanged += OnDataGridSelectionChanged;
else
dataGrid.SelectionChanged -= OnDataGridSelectionChanged;
}

static void OnDataGridSelectionChanged(object sender, RoutedEventArgs e)
{
// Only react to the SelectionChanged event raised by the DataGrid
// Ignore all ancestors.
if (!Object.ReferenceEquals(sender, e.OriginalSource))
return;

DataGrid dataGrid = e.OriginalSource as DataGrid;
if (dataGrid != null && dataGrid.SelectedItem != null)
{
// this is a workaround to fix the layout issue.
// otherwise ScrollIntoView should work directly.
dataGrid.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new DispatcherOperationCallback(ScrollItemIntoView),dataGrid);
}
}

static object ScrollItemIntoView(object sender)
{
DataGrid dataGrid = sender as DataGrid;
if (dataGrid != null && dataGrid.SelectedItem != null)
{
dataGrid.ScrollIntoView(dataGrid.SelectedItem);
}
return null;
}

#endregion // AutoScrollIntoView





Happy Coding!