Friday, December 31, 2010

INotifyPropertyChanged - an anti-pattern? UpdateControls – a solution?

In this post I will write about some problems I faced with INotifyPropertyChanged while using MVVM and some solutions available.

Let’s first examine the problem.

Is INotifyPropertyChanged an anti-pattern or not?

The short answer is yes? But it really depends on how big is the project. It is very common to use PropertyChanged events inside ViewModels of an MVVM application to propagate changes occurring in your classes back to presentation layer via WPF data binding mechanism.

So why the size of the project matters. Well, it all comes down to maintainability of the code base.  PropertyChange events are usually raised by passing property names as string constants (that’s a bummer). If the number of view models and their properties is large, the project will have too many hard coded strings. Yes, it is possible to move these strings at the beginning of the class file, and there are very nice tools which can do that for you, such as Refactor! from DevExpress or even Visual Studio itself. But still if you are changing schema and your data model is regenerated then most likely corresponding view models will require changes as well and that is where maintenance becomes a nightmare on projects with more than 10 view models. Add to it all the business complexities and tracking control flow will be almost impossible. So the question basically is do you want to pollute your code with hardcoded string? If code is large – then probably NO, if code is small then I guess it’s OK.

Now let’s see what are the options (this list is not extensive).

1. Why not use reflection to get property names, when property names change we can simply recompile? So such solution was proposed by Karl Shifflett – a Program Manager on Patterns and Practices Prism Team, Microsoft Corporation. But later Sacha Barber found a flaw in such approach with Stack Frames. Karl since updated his post and commented on this problem. Basically if you compile your code for release and remove pdb (debug database with initial method and property names) files, due to compiler optimization the property names will not be properly resolved and some calls will end up in the wrong place inside the stack :(. Sacha since proposed his own framework for MVVM called Cinch. While it sounds like a solid piece of work it doesn’t really address PropertyChanged problem.

2. Josh Smith blogged about this problem as well and proposed using lambda expressions to validate property names at compile time, thus avoiding StackFrame problem. He used a property observer pattern and created a generic class (PropertyObserver<TPropertySource>) to handle it along with weak event listeners. Still this approach requires changes to be propagates when properties change. While again there are tools which can do this in semi-automated fashion it could be error prone when there are too many dependencies between properties within and outside of the class.

3. Another approach is to use weak event referencing for all independent properties. Such solution called UpdateControls was proposed by Michael Perry. He has a great set of videos on his web site explaining this approach. And this is wonderful, but it because an overkill when working with large collections. It takes up too much memory to register for every single independent property in let’s say a list of 100 000 records.

4. My approach. I decided to use a mix of things from the above.
   - All my Data Model classes are generated and use Property Changed events. Since there is no business logic which resides in these classes there is no complexity associated in maintaining custom event handlers or raising of events. Everything is generated and regenerated automatically when database schema changes.
   - For my view models I used a similar approach. But use UpdateControl for the properties. Partial view model classes are generated based off of a database schema (dbml XML model file) using slightly modified T4 template provided by Damien. Here is my template below.

<# // L2ST4 - LINQ to SQL templates for T4 v0.85 - http://www.codeplex.com/l2st4
// Copyright (c) Microsoft Corporation.  All rights reserved.
// This source code is made available under the terms of the Microsoft Public License (MS-PL)
#><#@ template language="C#v3.5" hostspecific="True"
#><#@ include file="..\\DataModels\\L2ST4.ttinclude"
#><#@ assembly name="..\My Documents\Visual Studio 2008\Projects\SHMMP\ShmmpManager\ShmmpManager\bin\Debug\ShmmpManager.exe" 
#><#@ import namespace="ShmmpManager.ViewModels"
#><#@ output extension=".generated.cs"
#><# // Set options here
var options = new {
DbmlFileName = Host.TemplateFile.Replace("ViewModel.tt","Data.dbml").Replace("ViewModels","DataModels"), // Which DBML file to operate on (same filename as template)
SerializeDataContractSP1 = false, // Emit SP1 DataContract serializer attributes
FilePerEntity = true, // Put each class into a separate file
StoredProcedureConcurrency = false, // Table updates via an SP require @@rowcount to be returned to enable concurrency
};
var code = new CSharpCodeLanguage();
var data = new Data(options.DbmlFileName);
var manager = Manager.Create(Host, GenerationEnvironment);
data.ContextNamespace = (new string[] { manager.DefaultProjectNamespace }).FirstOrDefault(s => !String.IsNullOrEmpty(s));
data.EntityNamespace = (new string[] { manager.DefaultProjectNamespace }).FirstOrDefault(s => !String.IsNullOrEmpty(s));
string baseClassName = "EntityViewModelBase";
string entityBase = "ViewModelBase";
manager.StartHeader();
#>#pragma warning disable 1591
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by LINQ to SQL template for T4 C#
//     Generated at <#=DateTime.Now#>
//       The original template was modified by Ivan to meet certain 
//        project related requirements. 
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Text;
using System.Linq;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Collections.Generic;
using ShmmpManager.DataModels;
<#if (data.Functions.Count > 0) {#>
using System.Reflection;
<#}
string dataContractAttributes = (options.SerializeDataContractSP1) ? "IsReference=true" : "";
if (data.Serialization) {#>
using System.Runtime.Serialization;
<#}#>
using UpdateControls;

<# manager.EndBlock();
foreach(Table table in data.Tables) {
foreach(TableClass class1 in table.Classes) {
manager.StartNewFile(Path.ChangeExtension(class1.Name + "ViewModel",".generated.cs"));
if (!String.IsNullOrEmpty(data.EntityNamespace)) {#>

namespace <#=data.EntityNamespace#>.ViewModels
{    
<#        }
if (data.Serialization && class1.IsSerializable) {
#>    [DataContract(<#=dataContractAttributes#>)]
<#        }
if (class1 == table.BaseClass) {#>
<#            foreach(TableClass subclass in data.TableClasses.Where(c => c.Table == table)) {
if (!String.IsNullOrEmpty(subclass.InheritanceCode)) {#>
[InheritanceMapping(Code=@"<#=subclass.InheritanceCode#>", Type=typeof(<#=subclass.Name#>)<# if (subclass.IsInheritanceDefault) {#>, IsDefault=true<#}#>)]
<#                }
if (data.Serialization && subclass.IsSerializable) {#>[KnownType(typeof(<#=subclass.Name#>))]<#}
}
#>    <#=code.Format(class1.TypeAttributes)#>partial class <#=class1.Name#>ViewModel : <#
if (class1.Name == "EquipmentPremiumRisk" 
|| class1.Name == "EquipmentPremiumManaged"
|| class1.Name == "EquipmentPremiumShared"
|| class1.Name == "EquipmentPremiumCapped"
|| class1.Name == "EquipmentPremiumDirectedService"){ #> EquipmentPremiumViewModelBase <#}
else if (class1.Name == "EquipmentPremiumAdjustmentRisk" 
|| class1.Name == "EquipmentPremiumAdjustmentManaged"
|| class1.Name == "EquipmentPremiumAdjustmentShared"
|| class1.Name == "EquipmentPremiumAdjustmentCapped"
|| class1.Name == "EquipmentPremiumAdjustmentDirectedService"){ #> EquipmentPremiumAdjustmentViewModelBase <#}                        
else if (!String.IsNullOrEmpty(entityBase)) {#> <#=baseClassName#> <# }#>
<#         else { #><#=class1.SuperClass.Name#> <#}}#>
{        
#region Fields
// list to support error validations
protected new static List<PropertyRuleBase<ViewModelBase>> _propertiesRules = new List<PropertyRuleBase<ViewModelBase>>();

// list to support warnings validations. 
protected new static List<PropertyRuleBase<ViewModelBase>> _propertiesWarnings = new List<PropertyRuleBase<ViewModelBase>>();

// static list to support singleton pattern. 
private static List<<#=class1.Name#>ViewModel> _instances = new List<<#=class1.Name#>ViewModel>();

#endregion

#region Data Members
private <#=class1.Name#> _<#= class1.Name #>;
#endregion

#region Properties

public override bool IsNew
{
get
{
<#              if (class1.Name == "Agent"
|| class1.Name == "Message"
|| class1.Name == "TypeCoverageDetail"
|| class1.Name == "ErrorLog" 
|| class1.Name.StartsWith("vw")
){#>
return _<#= class1.Name #>.<#= class1.PrimaryKey[0].Name #> == default(<#= class1.PrimaryKey[0].Type #>);
<#                }else{#>                
return String.IsNullOrEmpty(_<#= class1.Name #>.AddedByStaffCode);
<#                }#>                
}
}

<#         if (class1.Name != "ErrorLog" 
&& !class1.Name.StartsWith("vw")
) 
{
#>    

private Independent _indAudit = new Independent(); 
public IQueryable<<#= class1.Name #>Audit> Audit
{
get 
{
_indAudit.OnGet();
return from <#=class1.Name#>Audit c in ShmmpAudit.<#=class1.Name#>Audits
where c.<#= class1.PrimaryKey[0].Name #> == _<#= class1.Name #>.<#= class1.PrimaryKey[0].Name #>
orderby c.DateStamp descending
select c;
}
}

private Independent _indHistory = new Independent();
public IQueryable<<#= class1.Name #>Audit> History
{
get
{
_indHistory.OnGet();

return (new List<<#= class1.Name #>Audit>()).AsQueryable();

// TODO: write history query, needs to return IEnumerable<ENTITYHistory>
}
}

public override string HistoryMessage
{
get
{
string historyMessage = String.Empty; 

// getting  <#=class1.Name#> Audit 
<#=class1.QualifiedName#>Audit  auditRecord = (AuditRecord as <#=class1.QualifiedName#>Audit);

if (auditRecord != null)
{
historyMessage = String.Format("Historical reference for: \nDate - ({0:d}), Time - ({1:T}), Operation - ({2}), Staff - ({3}).",
auditRecord.DateStamp.Date,
auditRecord.DateStamp,
auditRecord.TableOperation.Description,
auditRecord.StaffCode);
}

//<#=class1.Name#>History historyRecord = (HistoryRecord as <#=class1.Name#>History);

//if (historyRecord != null)
//{
//    historyMessage = String.Format("Historical reference for: \nBill Period - ({0}).",
//        historyRecord.BillPeriod.Date);                        
//}

return historyMessage;
}
}
<#         } #>
#endregion //Properties

#region Construction

/// <summary>
/// This is static constructor to emulate singleton pattern. 
/// </summary>
/// <param name="<#=class1.Name.ToLower()#>">Entity record.</param>
/// <param name="baseTable">Base Table which stores current entity.</param>
/// <returns></returns>
public static <#=class1.Name#>ViewModel Create(<#=class1.Name#> <#=class1.Name.ToLower()#>,ITable baseTable)
{
<#=class1.Name#>ViewModel instance =null; //= _instances.FirstOrDefault(i=> i.DataEntity == <#=class1.Name.ToLower()#>);

//if (instance == null) 
//{
if (<#=class1.Name.ToLower()#> != null)
{
if (_propertiesRules == null)
{
_propertiesRules = new List<PropertyRuleBase<ViewModelBase>>();
}

if (_propertiesWarnings == null)
{
_propertiesWarnings = new List<PropertyRuleBase<ViewModelBase>>();
}

instance = new <#=class1.Name#>ViewModel(<#=class1.Name.ToLower()#>, baseTable);
//_instances.Add(instance); 
}
//}
return instance;
}


private <#=class1.Name#>ViewModel(<#=class1.Name#> <#=class1.Name.ToLower()#>,ITable baseTable)
{
BaseTable = baseTable; 
DataEntity = <#=class1.Name.ToLower()#>;
_<#= class1.Name#> = <#=class1.Name.ToLower()#>;

<#            if (class1.HasPrimaryKey){#>
// initializing primary and foreign keys
<#                foreach(Column column in class1.Columns) {
#>
<#=column.Storage #> = _<#= class1.Name#>.<#= column.Member #>;
<#                    
}
}#>

// initializing data context
DataContext = baseTable.Context as <#=data.ContextName#>; 

_propertyChangedHandler = new System.ComponentModel.PropertyChangedEventHandler(DataEntity_PropertyChanged);

_<#= class1.Name#>.PropertyChanged += _propertyChangedHandler; 
Initialize();

<#            if(class1.Name != "ErrorLog" 
&& !class1.Name.StartsWith("vw")
)
{#>
_saveHandler = OnSaveEvent; 
this.SavedEvent += _saveHandler;
<#            }#>            
}

partial void Initialize();
#endregion

<#        int dataMemberIndex = 1;
if (class1.Columns.Count > 0) {
#>        #region Column Mappings
<#            foreach(Column column in class1.Columns) { #>
//data member
private <#=code.Format(column.StorageType)#> <#=column.Storage#><# if (column.IsReadOnly ) {#> = default(<#=code.Format(column.StorageType)#>)<#}#>;
private static PropertyRuleBase<ViewModelBase> <#=column.Member#>PropertyRules;
private static PropertyRuleBase<ViewModelBase> <#=column.Member#>PropertyWarnings;
<#        if (!column.IsPrimaryKey && !column.IsReadOnly){#>        
private Independent _ind<#=column.Member#> = new Independent();
<#        }#>
<#
#>        <#=code.Format(column.MemberAttributes)#><# 
if (((class1.Name == "EquipmentPremiumRisk" 
|| class1.Name == "EquipmentPremiumManaged"
|| class1.Name == "EquipmentPremiumShared"
|| class1.Name == "EquipmentPremiumCapped"
|| class1.Name == "EquipmentPremiumDirectedService")
&&(typeof(EquipmentPremiumViewModelBase)
.GetMember(column.Member).FirstOrDefault() != null))
||
((class1.Name == "EquipmentPremiumAdjustmentRisk" 
|| class1.Name == "EquipmentPremiumAdjustmentManaged"
|| class1.Name == "EquipmentPremiumAdjustmentShared"
|| class1.Name == "EquipmentPremiumAdjustmentCapped"
|| class1.Name == "EquipmentPremiumAdjustmentDirectedService")
&&(typeof(EquipmentPremiumAdjustmentViewModelBase)
.GetMember(column.Member).FirstOrDefault() != null)))
{#>override <#}#><#=code.Format(column.Type)#> <#=column.Member#>
{
get 
{ 
<#                if (!column.IsPrimaryKey && !column.IsReadOnly){#>
_ind<#=column.Member#>.OnGet(); 
<#                }
if (column.CanBeNull){#>
if (this.IsDisposed 
|| this.DataContext == null 
|| this.DataContext.IsDisposed)
return null;
<#                 } 
if(class1.Name == "ErrorLog" 
|| class1.Name.StartsWith("vw")
)
{#>
return _<#= class1.Name#>.<#=column.Member#>;
<#                }
else
{#>
return <#=column.StorageValue#>;
<#                }#>                
}
<#                if (!column.IsReadOnly && !column.IsPrimaryKey) { #>
set {
if (<#=column.StorageValue#> != value) {
_ind<#=column.Member#>.OnSet();
<#=column.StorageValue#> = value;
OnPropertyChanged("<#=column.Member#>");
}
}
<#                }#>
}

<#            }#>
#endregion

#region Column Validation Warnings' Registration

public override List<PropertyRuleBase<ViewModelBase>> PropertiesWarnings 
{
get {return _propertiesWarnings;}
protected set {_propertiesWarnings = value;}
}

protected override void AddAllPropertyWarningsDefinitions(<#=entityBase#> obj)
{
if (_propertiesWarnings == null)
{
_propertiesWarnings = new List<PropertyRuleBase<ViewModelBase>>();
}

if (_propertiesWarnings.Count == 0) // checking this condition twice to prevent dead locks. 
{
lock (obj)
{
if (_propertiesWarnings.Count == 0)
{
// adding properties' warnings collections
<#                        foreach(Column column in class1.Columns) {#>
<#=column.Member#>PropertyWarnings = new PropertyRuleBase<ViewModelBase> { PropertyName = "<#=column.Member#>" };
_propertiesWarnings.Add(<#=column.Member#>PropertyWarnings);

<#                        } #>
AddPropertyWarnings();
}
}
}
}
#endregion

#region Column Validation Rules' Registration

public override List<PropertyRuleBase<ViewModelBase>> PropertiesRules 
{
get {return _propertiesRules;}
protected set {_propertiesRules = value;}
}

protected override void AddAllPropertyRulesDefinitions(<#=entityBase#> obj)
{
if (_propertiesRules == null)
{
_propertiesRules = new List<PropertyRuleBase<ViewModelBase>>();
}

if (_propertiesRules.Count == 0) // checking this condition twice to prevent dead locks. 
{
lock (obj)
{
if (_propertiesRules.Count == 0)
{
// adding properties' rules collections
<#                        foreach(Column column in class1.Columns) {#>
<#=column.Member#>PropertyRules = new PropertyRuleBase<ViewModelBase> { PropertyName = "<#=column.Member#>" };
_propertiesRules.Add(<#=column.Member#>PropertyRules);

<#                        } #>
AddPropertyRules();
}
}
}
}
#endregion

#region Methods 

protected override bool HasChanged()
{
return (
<#                 int columnIndex = 0;
foreach(Column column in class1.Columns){
if(columnIndex!=0){#>             || <# }#>                
<#= column.Member #> != _<#= class1.Name #>.<#= column.Member #>
<#                    columnIndex++;
}#>    
|| base.HasChanged()
);
}
<#        if(class1.Name != "ErrorLog" 
&& !class1.Name.StartsWith("vw")
)
{#>        
private void OnSaveEvent(object sender, EventArgs e)
{
_indAudit.OnSet();
}
<#        }#>        
#endregion

#region Commands 

#region ReviewAuditCommand

<#        if(class1.Name != "ErrorLog" 
&& !class1.Name.StartsWith("vw"))
{#>        
protected override void DoReviewAuditRefresh() 
{
<#            foreach(Column column in class1.Columns) {
if (!column.IsPrimaryKey && !column.IsReadOnly)
{#>            
<#=column.Member#> = (AuditRecord as <#=class1.Name#>Audit).<#=column.Member#><#
if(column.Type.FullName == "System.Int32" 
|| column.Type.FullName == "System.Boolean"
|| column.Type.FullName == "System.DateTime" 
|| column.Type.FullName == "System.Decimal" 
|| column.Type.FullName == "System.Byte" ){#>.Value<#}#>;
<#              }
}#>        
}
<#        }#>
#endregion

#region CurrentRecordCommand 

<#        if(class1.Name != "ErrorLog" 
&& !class1.Name.StartsWith("vw"))
{#>            
protected override void DoCurrentRecordRefresh()
{
base.DoCurrentRecordRefresh(); 

<#            foreach(Column column in class1.Columns) {
if (!column.IsPrimaryKey && !column.IsReadOnly)
{#>
<#=column.Member#> = _<#= class1.Name #>.<#= column.Member #>;
<#              }
}#>
}        
<#        }#>
#endregion 

#region CancelCommand

protected override void DoCancel()
{
base.DoCancel();
// restore values for all properties
<#            foreach(Column column in class1.Columns){
if (!column.IsReadOnly && !column.IsPrimaryKey){#>
<#= column.Member #> = _<#= class1.Name #>.<#= column.Member #>; 
<#                }
}#>
}

#endregion

#region SaveCommand

protected override void DoSave()
{
IsUpdating = true; 
// set values for all properties
<#            foreach(Column column in class1.Columns){
if (!column.IsReadOnly 
&& !column.IsPrimaryKey 
&& class1.Associations.Where(association => association.ThisKey.FirstOrDefault().Name == column.Name).Count()==0){#>
_<#= class1.Name #>.<#= column.Member #> = <#if(column.Type.FullName == "System.String") {#> ((<#=column.Member#> == null)?String.Empty : <#= column.Member #>);<#} else {#> <#=column.Member #>;<# } #> 
<#                }
}#>
bool isNew = this.IsNew;
<#            foreach(Association association in class1.Associations){
if(!association.IsMany && association.IsForeignKey){#>    
if(<#=association.ThisKey.FirstOrDefault().Member#>!= _<#= class1.Name #>.<#= association.ThisKey.FirstOrDefault().Member #>)
{
if(isNew)
{
_<#= class1.Name #>.<#= association.ThisKey.FirstOrDefault().Member #> = _<#=association.ThisKey.FirstOrDefault().Member#>;
}
else
{
_<#= class1.Name #>.<#= association.Member#> = DataContext.<#= association.Type.Table.Member#>.Where(entity => entity.<#= association.OtherKey.FirstOrDefault().Member#> == <#=association.ThisKey.FirstOrDefault().Member#>).FirstOrDefault();
}
}
<#                }
}#>
base.DoSave(); 
IsUpdating = false; 
}

protected override void DoErase()
{
IsUpdating = true; 
// set values for all properties
<#            foreach(Column column in class1.Columns){
if (!column.IsReadOnly && !column.IsPrimaryKey){#>
_<#= class1.Name #>.<#= column.Member #> = default(<#=code.Format(column.Type)#>); 
<#                }
}#>    
IsUpdating = false; 
}

#endregion

#region RefreshCommand 

protected override void DoRefresh(bool fromDatabase)
{
if (fromDatabase)
{
//refresh values from database.
DataContext.Refresh(RefreshMode.OverwriteCurrentValues, DataEntity);
}
<#            foreach(Column column in class1.Columns){
if(!column.IsReadOnly && !column.IsPrimaryKey){#>
<#=column.Member#> = _<#= class1.Name#>.<#=column.Member#>;
<#                }
if(column.IsReadOnly && !column.IsPrimaryKey)
{#>
<#=column.Storage#> = _<#= class1.Name #>.<#= column.Member #>;
<#                 }
}#>        

<#            foreach(Association association in class1.Associations){
if(!association.IsMany && association.IsForeignKey){#>    
_<#= class1.Name #>.<#= association.Member#> = DataContext.<#= association.Type.Table.Member#>.Where(entity => entity.<#= association.OtherKey.FirstOrDefault().Member#> == _<#=association.ThisKey.FirstOrDefault().Member#>).FirstOrDefault();
<#                }
}#>
}

#endregion 

#endregion    

#region Cleanup 
protected override void Cleanup()
{
if (_saveHandler != null)
{
this.SavedEvent -= _saveHandler; 
_saveHandler = null; 
}
if (_propertyChangedHandler != null && _<#= class1.Name#> != null) 
{
this._<#= class1.Name  #>.PropertyChanged -= _propertyChangedHandler;
_propertyChangedHandler = null; 
}
//_<#= class1.Name #> = null; 
base.Cleanup();
}
#endregion //Cleanup 
<#        }

if (class1.Associations.Count > 0) {
#>        
#region Associations
<#            foreach(Association association in class1.Associations) {#>
private <# if (association.IsMany) { #>IEnumerable< <#= association.Type.Name#>ViewModel><# } else {#> <#= association.Type.Name#>ViewModel<#}#> _<#=association.Member#>;
//private Independent _ind<#=association.Member#> = new Independent();

<#=code.Format(association.MemberAttributes)#><#if (association.IsMany) { #>IEnumerable<<#=association.Type.Name#>ViewModel><# } else {#> <#= association.Type.Name#>ViewModel<#} #> <#=association.Member#>
{
get 
{
if (this.IsDisposed
|| this.DataContext == null 
|| this.DataContext.IsDisposed)
return null;
<#
if (class1.Name.StartsWith("vw") || class1.Name == "ErrorLog")
{#>
if (_<#=association.Member#>==null 
|| this.<#=association.ThisKey.FirstOrDefault().Name#> != this._<#=class1.Name#>.<#=association.ThisKey.FirstOrDefault().Name#>
<#                    if (!association.IsMany){#>                || _<#=association.Member#>.<#=association.OtherKey.FirstOrDefault().Name#> != this._<#=class1.Name#>.<#=association.ThisKey.FirstOrDefault().Name#><#}#>)
{
<#                    if(association.IsMany)
{#>
List<<#= association.Type.Name#>ViewModel> list = new List<<#= association.Type.Name#>ViewModel>(); 
foreach(<#=association.Type.Name#> entity in DataContext.<#=association.Type.Table.Member#>.Where(t=> t.<#=association.OtherKey.FirstOrDefault().Name#>==this.<#=association.ThisKey.FirstOrDefault().Name#>)<#if(!association.IsMany){#>.FirstOrDefault()<#}else{#><#}#>)
{
list.Add(<#=association.Type.Name#>ViewModel.Create(entity, DataContext.<#=association.Type.Table.Member#>));
}
_<#=association.Member#> = list.AsEnumerable();
<#                    }
else
{#>                    
_<#=association.Member#> = <#=association.Type.Name#>ViewModel.Create(DataContext.<#=association.Type.Table.Member#>.Where(t=> t.<#=association.OtherKey.FirstOrDefault().Name#>==this.<#=association.ThisKey.FirstOrDefault().Name#>)<#if(!association.IsMany){#>.FirstOrDefault()<#}else{#><#}#>,DataContext.<#=association.Type.Table.Member#>);
<#                     } #>                                        
}
return _<#=association.Member#>;
<#                }        
else 
{#>
//_ind<#=association.Member#>.OnGet();
//*(HistoryRecord as <#=class1.Name#>History).<#=association.Member#>*/
if (_<#=association.Member#>==null 
|| this.<#=association.ThisKey.FirstOrDefault().Name#> != this._<#=class1.Name#>.<#=association.ThisKey.FirstOrDefault().Name#>
<#                    if (!association.IsMany){#>                    || _<#=association.Member#>.<#=association.OtherKey.FirstOrDefault().Name#> != this._<#=class1.Name#>.<#=association.ThisKey.FirstOrDefault().Name#><#}#>)
{
<#                    if(association.IsMany)
{#>
List<<#= association.Type.Name#>ViewModel> list = new List<<#= association.Type.Name#>ViewModel>(); 
foreach(<#=association.Type.Name#> entity in DataContext.<#=association.Type.Table.Member#>.Where(t=> t.<#=association.OtherKey.FirstOrDefault().Name#>==this.<#=association.ThisKey.FirstOrDefault().Name#>)<#if(!association.IsMany){#>.FirstOrDefault()<#}else{#><#}#>)
{
list.Add(<#=association.Type.Name#>ViewModel.Create(entity, DataContext.<#=association.Type.Table.Member#>));
}
_<#=association.Member#> = list.AsEnumerable();
<#                    }
else
{#>                    
_<#=association.Member#> = <#=association.Member#>ViewModel.Create(DataContext.<#=association.Type.Table.Member#>.Where(t=> t.<#=association.OtherKey.FirstOrDefault().Name#>==this.<#=association.ThisKey.FirstOrDefault().Name#>)<#if(!association.IsMany){#>.FirstOrDefault()<#}else{#><#}#>,DataContext.<#=association.Type.Table.Member#>);
<#                     } #>                                        
}
<#                if(!association.Member.StartsWith("vw") && association.Member != "ErrorLog")
{#>
<#=class1.Name#>Audit auditRecord = (AuditRecord as <#=class1.Name#>Audit);
if (auditRecord != null)
{
var key =  
auditRecord.<#=association.ThisKey.FirstOrDefault().Name#><#
if(association.ThisKey.FirstOrDefault().Type.FullName == "System.Int32" 
|| association.ThisKey.FirstOrDefault().Type.FullName == "System.Boolean"
|| association.ThisKey.FirstOrDefault().Type.FullName == "System.DateTime" 
|| association.ThisKey.FirstOrDefault().Type.FullName == "System.Decimal" 
|| association.ThisKey.FirstOrDefault().Type.FullName == "System.Byte" ){#>.Value<#}#>;        
<#                    if (association.IsMany)
{#>
//foreach(<#=association.Type.Name#>ViewModel vm in _<#=association.Member#>)
//{
//    DataModelAuditBase ar = ShmmpAudit.<#=association.Type.Name#>Audits
//                        .Where(t=>t.<#=association.OtherKey.FirstOrDefault().Name#> == key
//                            && t.DateStamp <= auditRecord.DateStamp).FirstOrDefault();
//    if (ar != null)
//        vm.AuditRecord = ar; 
//}
List<<#= association.Type.Name#>ViewModel> list = new List<<#= association.Type.Name#>ViewModel>(); 
foreach(<#=association.Type.Name#> entity in DataContext.<#=association.Type.Table.Member#>.Where(t=> t.<#=association.OtherKey.FirstOrDefault().Name#>==key)<#if(!association.IsMany){#>.FirstOrDefault()<#}else{#><#}#>)
{
list.Add(<#=association.Type.Name#>ViewModel.Create(entity, DataContext.<#=association.Type.Table.Member#>));
}
_<#=association.Member#> = list.AsEnumerable();
<#                    }
else
{#>
DataModelAuditBase ar = ShmmpAudit.<#=association.Type.Name#>Audits
.Where(t=>t.<#=association.OtherKey.FirstOrDefault().Name#> == key
&& ((t.TransactionId == auditRecord.TransactionId && (t.DateStamp - auditRecord.DateStamp).Minutes < 60 )
|| (t.DateStamp >= auditRecord.DateStamp))
).FirstOrDefault();
if (ar != null)
{
if(_<#=association.Member#>.AuditRecord != ar)
{
_<#=association.Member#>.AuditRecord = ar; 
}
}
else
_<#=association.Member#> = <#=association.Member#>ViewModel.Create(
DataContext.<#=association.Type.Table.Member#>.
Where(t=> t.<#=association.OtherKey.FirstOrDefault().Name#>==key)<#if(!association.IsMany){#>.FirstOrDefault()<#}else{#><#}#>,DataContext.<#=association.Type.Table.Member#>);
<#                    }#>                        
}
<#                }#>                
return _<#=association.Member#>;
<#                }#>    
}
}

<#            }#>
#endregion
<#        }


#>    }
<#        if (!String.IsNullOrEmpty(data.EntityNamespace)) {#>
}
<#        }
manager.EndBlock();
}
}
manager.StartFooter();#>
#pragma warning restore 1591
<#manager.EndBlock(); 
manager.Process(options.FilePerEntity);#>

These templates are very similar to ASP, but reading such templates is very hard. So to read them easily there is a special and FREE markup tool developed by Tangible Engineering and a nice intro article written by Oleg Sych.

- For collections I use ObservableCollections thus disabling UpdateControls for them and preventing creation of millions of event handlers and events for large collections.

- For every object which gets into the view or somehow selected by a user a view model is generated. This dramatically improves performance of UpdateControls while still gives me a no worry approach for my PropertyChange events. Each view model may have custom validation logic or anything else since all classes are generated as partial.

- Having business logic in model views also helps working around some limitations of LINQ to SQL 3.5. Such as inability to detach entities, undo operations or working in concurrent scenarios with multiple threads. 

Having being able to find solution to all these problems I realized that such approach brings many additional benefits to the table. In simple words in my application:

- all lists are ObservableCollections of data models. Since data models are also partial classes, they can be easily extended with additional functionality.
- users make changes only to view models. Thus having the ability to roll back changes if database errors occur or having the ability to change multiple objects at the same time. Both of these things are impossible with current LINQ to SQL, since all the changes are made directly to the model and there is no way to detach any objects. The whole model needs to be disposed. While disposing the whole model means all change tracking that is built into LINQ to SQL classes is useless. Now in my model I get to enjoy the benefits of change tracking, and the ability to roll back changes. When objects need to be saved (persisted) to a data store, it is done in a separate instance of database DataContext. If errors occur I throw away such temporary data context, since I can’t detach entity from the model. And notify users about the error. If everything is fine, the new entity gets attached to the current model, or changed entity is refreshed from the data store.
- another benefit is data mining. Since I am using only one data context internally I am able to do joins between multiple lists and run queries of different complexities. Current LINQ to SQL doesn’t allow queries to overlap collections from different instances of the same data context.

So this was another not so easy, but still ISolvable<TProblem>.

Happy Coding and have a wonderful New Year!

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.