Friday, April 27, 2007

If it can go wrong, it will!

If you've been reading my blog, you know that I am developing an application using Visual Studio 2005 and Castle's ActiveRecord framework. Recently, I bought Visual Studio 2005 Pro so I could use Crystal Reports. Believe it or not, The user was able to install my application without a working version of .Net 2.0 around. As far as I can tell, they did a Windows Update, and somehow it corrupted their video drivers. Then they did a system restore back to before .Net 2.0 was installed. After this, they installed my application, and it crashed (of course!). They tried to repair .Net 2.o Framework, but that didn't quite do it. So now I will probably be connecting via Remote Desktop to survey the damages.

I learned something from this, and I will probably have two tiny executables in my bin folder ... a "hello world" application that maybe says "If you can see this, then .Net 2.0 is installed." and maybe does some sniffing around to see if an important font (Bar Code 3 of 9) is installed. When someone calls you up and says that an application crashed before it has shown the login form, that is never good. My first guess was that there was a security problem, so I asked him to go to Administrative Tools, but he could not find the .Net 2 Configuration Wizard. Aha! I guess I need to include the re-distributable components on CD, just for these cases. And I need to add a logging library, so I have some idea what the user is doing if and when the program crashes.

Why am I having so many problems? Can you say 3 week delivery time for a 5 month project? So I gave them "what I have", even though it is not fully tested. I hate doing this, but I did not have a choice. So I will be updating the application every few weeks as I enable menu items and enable or add forms. The good side of this is that the users will be testing my code, and because there is a real need, I was forced to focus my efforts on the core feature set. Since the most critical feature is creating an XML report, I started with that, hard-coding as I went. Then I dropped each value into the schema where it made the most sense, until I had no more hard-coded values. Then I refactored the code two or three times, separating user interface, business logic and entity / data management. I probably have more work to do here, but I have NUnit tests for some of the application, and since the UI (user interface) is in an EXE assembly, and the business logic and data classes are in a DLL assembly, all I have to do is write tests, and anything that I can't do from the NUnit tests needs refactoring.

Since I have about 100 things to do before I finish the application, I have resorted to two types of comment tags, the standard "//TODO: Implement method bar.MarkWidgetAsAssembled" and "//FUTURE: Do this later, since it is not a critical deliverable element". After my first official deliverable, I will do a file find and replace of //FUTURE: with //TODO: and finish up.

Another defensive tactic that I commonly use is try / catch blocks in my event methods. For example if I have a "Create ..." button, I will wrap my processing in a try / catch block, and throw a Debug.WriteLine(ex.ToString()) or log the exception then inform the user that the request failed. The business code that I called will be self-cleaning, so I do not need a finally block. The business code will have a try / finally if there is a file that needs to be closed, etc. .Net 2.0 has a BackgroundWorker that allows me to send progress percent and messages, without any System.Windows.Forms (SWF) references. This is important, since one of my rules is that business / data library code should NEVER refer to UI components aka SWF. This allows me to test my library code more easily, and who knows, some parts of the application might be used in a batch environment. Perhaps my file import code could be hooked to a directory watcher that imports new files and then moves them to /loaded or /error subdirectories. If my import code popped up a MessageBox under any circumstance, I could never use it in an automated fashion. Here is some pseudo-code:

// Button Event
private void BtnCreate_Click(sender, event)
{
try
{
DateTime selectedMonth = GetDateFromUI();
CreateWidgetSet(selectedMonth);
}
catch (Exception ex)
{
LogError(ex);
MessageBox("An error occurred while trying to create a widget set.\n\n" + ex.Message,
Title, MessageBoxButtons.OK);
}
}

// business action method
private void CreateWidgetSet(DateTime selectedMonth)
{
// actual business method
Widget.CreateASet(selectedMonth);
}

Labels: ,

Thursday, April 26, 2007

I won 2 fandango tickets

I know this entry is off-topic, but yesterday was pretty amazing. A co-worker told me about Blingo, and I signed up last month. Blingo is a search engine that uses Google's database for results, but as you search, you have a chance to win something. Blingo has "Thousand Dollar Thursdays" and sometimes they even give away an automobile! So yesterday I was trying to figure out why my ABit A8N SLI Fatality motherboard was crashing Windows XP since I added a SATA hard drive and a DVD burner. While I was searching, I won a Fandango movie ticket! That was cool, but I kept searching, and found that I needed to turn off the "[x] Let BIOS decide" option, and then turn off Command Queuing. So 30 minutes later, I did a different search and won again! That is 2 wins in a row! On the side panel, it lists who won what, and it showed me twice. The co-worker who referred me was ecstatic, as he has never won anything on Blingo so far. I use Firefox, and I like a clean look to my browser, so I was glad they have a search engine plug-in. Try it, you'll like it.

Labels: ,

Wednesday, April 11, 2007

Defensive Programming with ActiveRecords

[Update 1: finished the entry, and added some compilable code.]
[Update 2: Updated the compilable code - improved the Property setter code.]

Recently, I have been working on a project with Castle's ActiveRecord support. Since I am changing my database schema as I discover new requirements, and adapt to new data sources, I occasionally run into query / model / table mismatches. Now, gross mismatches cause NHibernate to choke, and refuse to accept my models, but if I have merely added a field, that will be ignored until insert time. Even then, it might not cause an exception if it is a nullable field.

Now when I first started on this project, I wrote a simple SQL procedure that selected a list of fields from the database schema, walked through a cursor for each field and PRINTed code, then I would create a class file in Visual Studio 2005 and paste that code in.

-- Get a list of tables
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME != 'sysdiagrams';

SELECT c.Column_Name, c.Column_Default, c.Is_Nullable,
c.Data_Type, c.Character_Maximum_Length,
c.Numeric_Precision, c.Numeric_Scale
FROM Information_Schema.Columns c
WHERE Table_Name = @Table
ORDER BY Ordinal_Position ;


As development has progressed, I have learned to take advantage of the partial class support in .Net 2.0. My current model consists of a partial class with several sections: static fieldname strings, private variables, public constructors - empty and full field list, properties, and optionally a property changed event. Then, I have another (classname)_biz.cs file containing any other methods that I need to solve a problem. The one method that I always have is a ToString over-ride, to make it easy to pop an array of my objects into a list box or combo box. This week, I wrote a simple c# 2.0 windows form application to generate both the model and biz partial classes. Since I have no idea what ToString() should return, I concatenate all non-key fields with braces around them and add a TODO: comment. I use Subversion to protect my code, or more accurately, I use TortoiseSVN - a windows explorer extension that makes commits very fast, and remembers check-in messages. I don't care what YOU use, just use something! And do not put your repository on the same physical disk drive as your development code. Ideally, the repository would be on a remote server, but at the very least put it on another disk drive.



And just to make sure I don't miss something, I wrote a Unit test that walks through my list of ActiveRecord entities and verifies that private variables, properties and static strings Prop_FieldName = "FieldName" all meet expectations. Unmapped fields are listed in the debug output, but not considered errors. Field name mismatches cause an error.

Now, you might ask why I would bother with the "static string Prop_FieldName" section. This centralizes my field name references when using NHibernate's Criteria or in my case either an ICriterion array or a DetachedCriteria. Below is an example SQL table, model class and (finally) some business logic to load a record by a service code. In this line "Expression.Eq(ServiceType.Prop_TypeCode, p_TypeCode)", Prop_TypeCode is guaranteed by my unit tester to match the like-named field and corresponding database property. This protects me from spellings errors and from inadvertently referencing a field that is not part of my table. Now, I did not go so far as to decompile the Code DOM and ensure that each property references the correct private variable. I have to start trusting myself somewhere! As a point of interest, If I were to significantly re-structure my database, to the point that NHibernate would not register my model classes (entities), all I have to do is generate the model classes into another directory and do a "diff". Once the models compile and pass their tests again, the business code should also work as expected. (As a side note, This is not an example of how I format my code, but Blogger's layouts work better if I have shorter lines.)


// SQL table definition
CREATE TABLE ServiceTypes (
ID smallint identity(1,1) primary key not null,
TypeName varchar(30) not null,
TypeCode char(3) not null
)

// ServiceType.cs
namespace Demo.Model
{
// Generate class for table ServiceType
// value class ServiceType generated from ServiceTypes
// RTate [04/12/2007] Created

using System;
using System.ComponentModel;
using Castle.ActiveRecord;

[ActiveRecord("ServiceTypes")]
public partial class ServiceType
: ActiveRecordValidationBase, INotifyPropertyChanged
{

#region Property_Names

public static string Prop_ID = "ID";
public static string Prop_TypeName = "TypeName";
public static string Prop_TypeCode = "TypeCode";

#endregion

#region Private_Variables

private short _id;
private string _TypeName;
private string _TypeCode;

#endregion

#region Constructors

public ServiceType()
{
}

public ServiceType(
short p_id,
string p_TypeName,
string p_TypeCode)
{
_id = p_id;
_TypeName = p_TypeName;
_TypeCode = p_TypeCode;
}

#endregion

#region Properties

[PrimaryKey("ID", Access=PropertyAccess.NosetterLowercaseUnderscore)]
public short ID
{
get { return _id; }
}

[Property("TypeName", NotNull=true, Length=30), ValidateLength(1,30)]
public string TypeName
{
get { return _TypeName; }
set
{
if ((_TypeName == null) || (!value.Equals(_TypeName)))
{
_TypeName = value;
NotifyPropertyChanged("TypeName");
}
}
}

[Property("TypeCode", NotNull=true, Length=3), ValidateLength(1,3)]
public string TypeCode
{
get { return _TypeCode; }
set
{
if ((_TypeCode == null) || (!value.Equals(_TypeCode)))
{
_TypeCode = value;
NotifyPropertyChanged("TypeCode");
}
}
}

#endregion

#region INotifyPropertyChanged Members

public event PropertyChangedEventHandler PropertyChanged;

private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}

#endregion

} // ServiceType
}

// ServiceType_biz.cs
namespace Demo.Model
{
// business partial class ServiceType
// generated from table ServiceTypes
// RTate [04/12/2007] Created

using System;
using NHibernate.Expression;

public partial class ServiceType
{

#region Business Methods

public ServiceType FindByTypeCode(string p_TypeCode)
{
ICriterion[] crit = new ICriterion[1];
crit[0] = Expression.Eq(ServiceType.Prop_TypeCode, p_TypeCode);
return ServiceType.FindFirst(crit, null);
}

public override string ToString()
{
//TODO: Choose a suitable string representation
return "[" + TypeName + "]" + " [" + TypeCode + "]";
}

#endregion

} // ServiceType
}

Labels: , , ,