I wanted to make use of the ExcelReader class I created as a
source of data to create class instances.
Generally the classes in question inherit from a base class
that also knows how to save itself to a database. I could, of course maintain a
list of such classes within my Excel reading code and call an appropriate
routine as required. However I had in mind to create something that was generic
enough to handle a whole raft of classes – including some created and compiled “on
the fly” and thus unknowable to the source code.
The normal method for dealing with a range of classes
through a common function would be to specify an interface common to all and
then to interact through that interface.
Interfaces (if this is an unfamiliar term)
An interface is a list of public
properties and methods (also indexers and events) that should must be exposed
by a class that implements the interface. Interfaces themselves can be
inherited but contain no actual code – just the signatures (if you like) of the
components being specified.
One of the simplest ways of
creating an interface is to (VS2015) right click on the class name in the code
window and select “Extract Interface” from the “Quick Actions” context menu.
Earlier versions of Visual Studio offer a “Refactor” menu option that includes
extracting an interface.
Requiring a specific Interface is
an excellent way of ensuring that objects calling the functionality of a class you
provide supply full support. You can rely upon a method or event handler being
available – even if it just a “stub”. IComparable would be a good example of
just such an interface.
I have used interfaces in the
past for testing alternate algorithms. An interface allowed me to construct a
standardised software socket into which I could plug different classes at will.
I could thus test performance and function of alternate approaches without
having to change the code “consuming” the service/process.
If I come across a great in depth
web page on Interfaces I will post a link here but for a couple of helpful
examples you can do a lot worse than this C# Station post
Just about the only thing my target classes have in common is
an Update method. This limits the scope of any Interface a bit. The properties
could be addressed through reflection and it was a reasonable bet that a class
Clone() method could be used to create new class instances. So that was
beginning to sound like a workable strategy.
So my interface ended up being defined like this:
interface ObjectClone
{
object CloneAsObject();
void UpdateClass();
}
Each
class implementing the interface needs then to add a CloneAsObject method
alongside an UpdateClass method. The CloneAsObject() method is very simple:
public object CloneAsObject()
{
return (MyClassName)this.MemberwiseClone();
}
There is an assumption here that all such classes expose public properties for all of the key data elements.
I
defined an ExcelDataLoader class that will use an instance of the ExcelReaderclass to gather data from selected columns in a spreadsheet and use this data
to create instances of a class that implements that very simple ObjectClone
Interface.
The ExcelDataLoader
class needs a local definition and a public property:
private ObjectClone targetClass = null;
public ObjectClone TargetClass
{
set { targetClass = value; }
}
To pass in a new
instance of the required class.
Plus I also needed a way of passing the required
instructions to the ExcelDataLoader. A simple class to define the required
columns and the associated data name and type seemed the thing and that could
be as simple as:
public class ExcelColumnData : IComparable
{
public string ColumnName { get; set; } = "";
public string DataName { get; set; } = "";
public ExcelColumnData()
{
}
public ExcelColumnData(string ColumnName, string DataName)
{
this.ColumnName = ColumnName;
this.DataName = DataName;
}
public int CompareTo(object objx)
{
ExcelColumnData x = (ExcelColumnData)objx;
int res = this.ColumnName.Length.CompareTo(x.ColumnName.Length);
return (res == 0) ? this.ColumnName.CompareTo(x.ColumnName) : res; // AA after Z
}
}
Although you will note that this class implements an
interface – Icomparable so that List<T>Sort() can be used to ensure the
columns are presented in order.
These column definitions could then be passed as a List<ExcelColumndData>
to the ExcelDataLoader class.
The ExcelDataLoader class loadData() method uses an instance
of the ExcelReader class to extract some columns of data from a specified sheet
and then creates an instance of the class to be created. It then uses
reflection to loop through the class properties and if one of these matches a
specified data name associated with an Excel data column then the expected data
type is checked and an attempt made to cast (or parse) the data extracted from
Excel to the required type. If the parse works then the property is set to the
excel value.
private int loadData()
{
int recCount = 0;
excelReader = new ExcelReader(excelFilepath, sheetName);
List<string> colNames = new List<string>();
if(excelColumns.Count > 1) { excelColumns.Sort(); }
foreach (ExcelColumnData eColData in excelColumns)
{
colNames.Add(eColData.ColumnName);
}
List<List<string>> selCols = excelReader.GetSelectedColumns(colNames, skipFirstRow);
foreach(List<string> excelRow in selCols)
{
ObjectClone newObject = (ObjectClone)targetClass.CloneAsObject();
foreach (PropertyInfo propertyInfo in newObject.GetType().GetProperties())
{
if (propertyInfo.CanWrite)
{
string propertyName = propertyInfo.Name;
string dataType = propertyInfo.PropertyType.Name;
for(var eci=0; eci< excelColumns.Count; eci++)
{
if (excelColumns[eci].DataName.Equals(propertyName, StringComparison.OrdinalIgnoreCase))
{
// the strategy is to set the Property value if the incoming data is valid for the Property type
// otherwise the value is left as the class default setting
if (dataType.Equals("String", StringComparison.OrdinalIgnoreCase))
{
propertyInfo.SetValue(newObject, excelRow[eci], null);
} else if (dataType.Equals("Int64", StringComparison.OrdinalIgnoreCase))
{
long intVal = 0;
if (long.TryParse(excelRow[eci], out intVal))
{
propertyInfo.SetValue(newObject, intVal, null);
}
} else if (dataType.Equals("Int32", StringComparison.OrdinalIgnoreCase))
{
int intVal = 0;
if (int.TryParse(excelRow[eci], out intVal))
{
propertyInfo.SetValue(newObject, intVal, null);
}
} else if (dataType.Equals("Int16", StringComparison.OrdinalIgnoreCase))
{
Int16 intVal = 0;
if (Int16.TryParse(excelRow[eci], out intVal))
{
propertyInfo.SetValue(newObject, intVal, null);
}
} ... etc
'''
...
}
}
}
newObject.UpdateClass();
recCount++;
}
return recCount;
}
And you can pump data from Excel (or anywhere else) to create classes that are anonymous as far as the process is concerned and (in this instance) call a method to retain the data in a database for future use.
Usual caveats as this is prototype (although working) code and my production version will inevitably grow "horns" over time as I deal with edge cases and extra features.
No comments:
Post a Comment