Subscribe Now

ABC, 123, Ruby, C#, SAS, SQL, TDD, VB.NET, XYZ

Thursday, December 13, 2007

SQL Server 2005 Non-Adoption

The tide may have finally turned, but I have some opinions why SQL Server 2005 was not initially embraced by a lot of IT shops.

First of all, have you seen how well it integrates with Access 2003 for ADP development? It's phenomenal! (In the sense that it is non-functional.) Let's hope Access 2007 / SQL Server 2008 integration works.

Second of all, I have a sneaking suspicion that there really weren't a lot of devs out there dying to write C# or VB.NET stored procedures, and that this highly touted new feature was largely a gimmick imagined up by the big brains in Redmond and not really something that the practical developer would really use. T-SQL rocks after all! :)

You have opinions one way or the other?

Saturday, December 1, 2007

Futzing with FUTS - Part III

Let's dig a little deeper into using FUTS (Framework for Unit Testing SAS® programs).

We have 10,000 hospitalization records in a CSV file that looks like this. We want to do some basic analyses on this file: average age at admission, average length of stay and number of patient-days per doctor.

In order to unit test the SAS code that will perform the analysis, we learned in part 1 & part 2 in this series that testable code is written as macros. I've written three relatively simple SAS macros to do the three calculations. (I'm using Billy Kreuter's age calculation code.) This file that contains the three macros is named c:\hosp\macros.sas.
%macro Age(agevar,bdatevar,indexdt);
&agevar = floor((intck('month',&bdatevar,&indexdt) - (day(&indexdt) < losvar =" &dischargedtvar">

The Age and LOS macros calculate new variables and are to be called inside a data step, whereas the DocPatDays macro creates a new summary dataset, one record per DoctorID.

Before writing the production SAS program, let's test the macros with unit tests that utilize the FUTS macros. The test data for testing Age and LOS is called test_hosps.CSV and looks like this. It's basically a sample of the main hospitalizations CSV file plus EXPECTED_AGE and EXPECTED_LOS, calculated by hand.


The unit test code for Age looks like this. (We're importing the test_hosps.CSV data, calculated Age using the production macro and then comparing the EXPECTED_AGE with the age calculated by the macro.)

options mprint;
%include 'c:\hosp\macros.sas';
proc import datafile='test_hosps.csv' out=TestData dbms=csv replace;
getnames=yes;
run;

data actual;
set TestData (drop=EXPECTED_AGE);
%Age(AGE,BDATE,ADMITDATE);
run;

data expected;
set TestData (rename=(EXPECTED_AGE=AGE));
run;

%assert_compare_equal(base=expected,compare=actual);
On the first run of the unit test program (utAge.sas) we get an error on the assert.
68         %assert_compare_equal(base=expected,compare=actual);
MPRINT(ASSERT_COMPARE_EQUAL): proc compare base=expected compare=actual;
MPRINT(ASSERT_COMPARE_EQUAL): ;
MPRINT(ASSERT_COMPARE_EQUAL): run;

NOTE: There were 10 observations read from the data set WORK.EXPECTED.
NOTE: There were 10 observations read from the data set WORK.ACTUAL.
NOTE: The PROCEDURE COMPARE printed page 1.
NOTE: PROCEDURE COMPARE used (Total process time):
real time 0.45 seconds
cpu time 0.03 seconds


MPRINT(GENERATE_EVENT): options linesize=max;
ERROR: Data set actual not equal to expected
Looking at the .lst file reveals what is going on....proc import is attaching the BEST12 format and informat to the expected age variable (not present on the calculated age variable). They key thing is that all observations with all compared variables are equal, but there's still that annoying error in the log.

The COMPARE Procedure
Comparison of WORK.EXPECTED with WORK.ACTUAL
(Method=EXACT)

Data Set Summary

Dataset Created Modified NVar NObs

WORK.EXPECTED 26NOV07:19:37:43 26NOV07:19:37:43 7 10
WORK.ACTUAL 26NOV07:19:37:43 26NOV07:19:37:43 7 10


Variables Summary

Number of Variables in Common: 7.
Number of Variables with Differing Attributes: 1.


Listing of Common Variables with Differing Attributes

Variable Dataset Type Length Format Informat

AGE WORK.EXPECTED Num 8 BEST12. BEST32.
WORK.ACTUAL Num 8


Observation Summary

Observation Base Compare

First Obs 1 1
Last Obs 10 10

Number of Observations in Common: 10.
Total Number of Observations Read from WORK.EXPECTED: 10.
Total Number of Observations Read from WORK.ACTUAL: 10.

Number of Observations with Some Compared Variables Unequal: 0.
Number of Observations with All Compared Variables Equal: 10.

NOTE: No unequal values were found. All values compared are exactly equal.

To take care of that bothersome error caused by a simple difference is irrelevant variable attributes, the utAge.sas unit test SAS program gets updated to this.

options mprint;
%include 'c:\hosp\macros.sas';
proc import datafile='test_hosps.csv' out=TestData dbms=csv replace;
getnames=yes;
run;

data actual;
set TestData (drop=EXPECTED_AGE);
%Age(AGE,BDATE,ADMITDATE);
run;

data expected;
set TestData (rename=(EXPECTED_AGE=AGE));
format age; informat age; *Takes away formating/informating;
run;

%assert_compare_equal(base=expected,compare=actual);
The error goes away! Our unit test for the Age macro passes! :)

The unit test code for LOS looks very similar to the Age unit test code.
options mprint;
%include 'C:\hosp\macros.sas';
proc import datafile='test_hosps.csv' out=TestData dbms=csv replace;
getnames=yes;
run;

data actual;
set TestData (drop=EXPECTED_LOS);
%LOS(LOS,ADMITDATE,DISCHARGEDATE);
run;

data expected;
set TestData (rename=(EXPECTED_LOS=LOS));
format LOS; informat LOS; *Takes away formating/informating;
run;

%assert_compare_equal(base=expected,compare=actual);
We need another test data file for the PatDocDays unit testing. It is hand-calculated doctor-level summary data and will supply the expected values when processing the test_hosps.CSV with the PatDocDays macro.

The utDocPatDays.sas unit test code looks like this.
options mprint;
%include 'C:\hosp\macros.sas';
proc import datafile='test_hosps.csv' out=InputData dbms=csv replace;
getnames=yes;
run;

data InputData;
set InputData;
%LOS(LOS,AdmitDate,DischargeDate);
run;

proc import datafile='test_patient_days.csv' out=Expected dbms=csv replace;
getnames=yes;
run;

data Expected;
set Expected (rename=(EXPECTED_PT_DAYS=PT_DAYS));
format PT_DAYS; informat PT_DAYS; *Takes away formating/informating;
run;

%DocPatDays(actual,InputData,PT_DAYS,DOCTORID,LOS);

data actual;
set actual (keep=DOCTORID PT_DAYS);
format PT_DAYS; informat PT_DAYS;
run;

%assert_compare_equal(base=expected,compare=actual);
And finally, the production code looks like this.

options mprint;
%include 'C:\hosp\macros.sas';
proc import datafile='hospitalizations.csv' out=HospitalData dbms=csv replace;
getnames=yes;
run;

*Calculate age at hospital admission and length of stay;
data HospitalData2;
set HospitalData;
%Age(AgeAtAdmit,BDate,AdmitDate);
%LOS(LOS,AdmitDate,DischargeDate);
run;

proc means data=HospitalData2;
var AgeAtAdmit LOS;
run;

*Calculate patient-days per doctor;
%DocPatDays(PatDays,HospitalData2,PatDays,DoctorID,LOS);

proc means data=PatDays maxdec=2;
var PatDays;
run;

proc print data=PatDays;
var DoctorID PatDays;
run;
The output looks like this.


The MEANS Procedure

Variable N Mean Std Dev Minimum Maximum

AgeAtAdmit 10000 46.2669000 23.1170955 6.0000000 86.0000000
LOS 10000 8.0142000 3.7280810 2.0000000 14.0000000


The MEANS Procedure

Analysis Variable : PatDays

N Mean Std Dev Minimum Maximum

100 801.42 95.13 609.00 1049.00


Pat
Obs DOCTORID Days

1 100 744
2 101 732
3 102 840
4 103 1035
5 104 879
6 105 907
7 106 752
8 107 771
9 108 880
10 109 901
11 110 775
12 111 910
13 112 799
14 113 849
15 114 774
16 115 753
17 116 845
18 117 726
19 118 690
20 119 856
21 120 675
22 121 747
23 122 727
24 123 692
25 124 992
26 125 755
27 126 782
28 127 954
29 128 773
30 129 926
31 130 790
32 131 727
33 132 702
34 133 922
35 134 843
36 135 835
37 136 755
38 137 625
39 138 687
40 139 728
41 140 805
42 141 687
43 142 669
44 143 651
45 144 1010
46 145 909
47 146 950
48 147 805
49 148 920
50 149 679
51 150 777
52 151 756
53 152 837
54 153 798
55 154 832
56 155 893
57 156 684
58 157 754
59 158 808
60 159 788
61 160 793
62 161 816
63 162 752
64 163 1049
65 164 712
66 165 778
67 166 880
68 167 945
69 168 866
70 169 713
71 170 842
72 171 807
73 172 854
74 173 609
75 174 698
76 175 712
77 176 704
78 177 944
79 178 696
80 179 754
81 180 811
82 181 803
83 182 771
84 183 798
85 184 766
86 185 872
87 186 623
88 187 755
89 188 759
90 189 795
91 190 788
92 191 706
93 192 959
94 193 785
95 194 823
96 195 1027
97 196 903
98 197 751
99 198 765
100 199 891
FYI: I generated the hospitalizations.CSV dataset with this C# program.


C# data generation program

Thursday, November 22, 2007

The Value of Cross-Training

It was back when Java 1.1 was a hot ticket item that I decided to tinker with this cool, new language. I learned the basics and made a number of fun applets. One silly one I recall involved a cat chasing a mouse around on a rectangular surface. The hardware mouse moved the software mouse and the cat head followed the mouse wherever it went automatically. At the time I was in a primarily Microsoft shop, so I never got to use my Java knowledge at work, but when C# came out, it was very easy to pick up for me due to my Java experience.

A while back I also learned a fair bit about writing Flash applications using Macromedia Flash MX. I wonder how much this is going to help when I dig into Silverlight one of these days.

More recently, I spent a weekend studying Ruby on Rails and then BAM! - a few months later Microsoft's ASP.NET MVC Framework becomes a hot topic. Due to my experience with RoR, understanding the architecture and coding of MS MVC has been very easy. I may never code a production RoR app, but by studying it, I've given myself a valuable education.

Cynics may grumble about "M$ copying others" (I'm won't touch that one!), but there have definitely been other instances of helpful cross-training not involving Microsoft products. An obvious example is all that IBM Mainframe JCL I got to write a decade or so ago...that prepared me quite well for…for…for, well, um, Soap Headers! (And punch cards prepared me for voting in Palm Beach county.) LOL. Ok, maybe that's a bit of a stretch, but I am certain that my work with Perl helped with understanding some Ruby syntax and of course when SAS started supporting regular expressions, the experience with Perl regex paid off. And my explorations into the Matisse object-oriented database definitely helped grokking NNHibernate easier. Procedural programming in QBASIC certainly prepped me for classic VB. And let's not forget BASIC with its fun and frequent GOTO statements and the not infrequent use of same in VB/VBA error handling. On Error Goto Err_Handler. Good times.

My point is that when the urge strikes you to explore that language you've been curious about, go for it. It might just turn out to be the next cool thing or cross-train you for it.

Changing gears a bit, this reminds of the cross-training encouraged by the Integral Life Practice starter kit. By exercising your body, mind, spirit, and shadow (unconscious) in different ways, each realm of human experience is enhanced. For example, the benefits of meditation (spirit) extend well beyond the spiritual realm, into the physical (a more relaxed state), mental (more clarity), and shadow (unconscious material is more accessible to productive processing).

Happy Thanksgiving! :)

Wednesday, November 21, 2007

Visual Studio.NET 2008 Arrives

No more beta blues! Visual Studio.NET 2008 has been released to manufacturing!

90-day trial editions are available right now at this location on MSDN.

Here is the express products web install page for Visual Basic 2008 Express Edition, Visual C# 2008 Express Edition, Visual C++ 2008 Express Edition, & Visual Web Developer 2008 Express Edition. Free to download immediately. (You'll want a fast connection.)

Here's a nice blog post about new features courtesy of Doug Stewart.

Sunday, November 18, 2007

Futzing with FUTS - Part II

In this second look at FUTS (Framework for Unit Testing SAS® programs), I will walk through an example of how to convert a regular piece of SAS code into a testable piece of SAS code with a unit test for testing it. The example consists of proc sql code to calculate age group based on age at an index date.

proc sql;
create table Calc as
select *, (case
when int((IndexDt-BDate)/365.25) between 0 and 17 then 'A: 0-17'
when int((IndexDt-BDate)/365.25) between 18 and 34 then 'B: 18-34'
when int((IndexDt-BDate)/365.25) between 35 and 49 then 'C: 35-49'
when int((IndexDt-BDate)/365.25) between 50 and 64 then 'D: 50-64'
when int((IndexDt-BDate)/365.25) > 64 then 'E: 65+'
else '?' end) as AgeGroup
from sample;
quit;
As I mentioned in Part I, the trick to making this code testable is to place it in a macro. Editing this code to fit in a macro results in something like this. The macro creates the calculated age group variable in an output dataset (outfile) given an input dataset (infile) containing a birthdate variable (bdatevar) and index date variable (indexdtvar).
%macro CalcAgeGroup(outfile,infile,bdatevar,indexdtvar);
proc sql;
create table &outfile as
select *, (case
when int((&indexdtvar-&bdatevar)/365.25) between 0 and 17 then 'A: 0-17'
when int((&indexdtvar-&bdatevar)/365.25) between 18 and 34 then 'B: 18-34'
when int((&indexdtvar-&bdatevar)/365.25) between 35 and 49 then 'C: 35-49'
when int((&indexdtvar-&bdatevar)/365.25) between 50 and 64 then 'D: 50-64'
when int((&indexdtvar-&bdatevar)/365.25) > 64 then 'E: 65+'
else '?' end) as AgeGroup
from &infile;
quit;
%mend CalcAgeGroup;

The production code that calls this macro to achieve the same goal as the first bit of code looks like this. A simple one liner.

%CalcAgeGroup(Calc,sample,BDate,IndexDt);

To unit test the macro, we need at least a minimal set of test data like this CSV. In this sample data file, you can see there's an ID var, BDATE, INDEXDT, and EXPECTED_AGEGROUP based on a calculation done by hand.

Test Data CSV

Given the CSV, the unit test code looks like this. (The macro is assume to be saved in a file called CalcAgeGroup.sas.)


*Import the CSV file into a SAS dataset called TestData;
proc import datafile='test_data.csv' out=TestData dbms=csv replace; getnames=yes; run;

data input; *Prep input dataset;
set TestData (drop=Expected_AgeGroup);
run;

data expected; *Prep expected output dataset;
length AgeGroup $8.; *Make AgeGroup the right size;
set TestData (rename=(Expected_AgeGroup=AgeGroup));
format AgeGroup; informat AgeGroup; *Drop format/informat;
run;

%include 'CalcAgeGroup.sas';
%CalcAgeGroup(Calc,input,bdate,indexdt); *Run test data through macro;

%assert_compare_equal(base=expected,compare=Calc); *Compare macro output & expected output;

Here we see the use of the FUTS macro %assert_compare_equal which compares SAS two datasets (attributes and data values) and throws an error into the log if the base and compare files are at all different. The rest of the code is fairly self-explanatory and involves preparing an input dataset for the macro and an expected output file to compare with the actual output file.

Friday, November 16, 2007

The Other Mother of Invention: Impatience

On 11/8, I blogged about Unit Testing an Email Method. Things have happened since then...



Bad Sleep

You may have heard the saying about necessity or laziness being the mother of invention, but I say impatience is a good mother, too. I say this based on the fact that I quickly got sick of waiting for my email unit tests to run when I was running the whole batch of unit tests in NUnit. Pretty soon I had them [Ignore]'d out. I was distressed about the yellow instead of the green, but my impatience was stronger. One property of a good unit test is that it runs rapidly.



Eventually (doh!) it dawned on me that I could both have and eat my cake. How? I modified my email unit test to query my Outlook Inbox every two seconds to see if the target email had arrived and exit early if so (with success). Otherwise, it would sleep for two more seconds and check the inbox again -- up to a maximum of eight repetitions (i.e., 16 seconds).



This refactoring made me quite the happy camper. Imagine my joy as I ran all of my unit tests quickly without the manditory 10-second waits for the tests involving email. I, like many NUnit users I am sure, just love to see that green bar -- and as soon as possible!



I took all of the Outlook-related goo and put it in a separate class called OutlookInbox with a single public (static) method called EmailReceived. I just pass in the subject line of the email I am expecting to receive and the method works its magic and I get back a boolean value telling me if the email arrived in time or not. The responsibility for doing the 2-second chunks of waiting is delegated to this method and the work of actually interfacing with Outlook is delegated to a helper method called checkInbox.



using System;
using System.Runtime.InteropServices;
using Outlook = Microsoft.Office.Interop.Outlook;

namespace TestHarness
{
public class OutlookInbox
{
private OutlookInbox() { }

public static bool EmailReceived(string subjectline)
{
const int MAXTRIES = 8;
int tries = 0;
bool emailReceived = false;
do
{
tries++;
System.Threading.Thread.Sleep(2000);
emailReceived = checkInbox(subjectline);
} while (emailReceived == false && tries < MAXTRIES);
return emailReceived;
}

private static bool checkInbox(string subjectline)
{
//This webpage (http://www.developerfusion.co.uk/show/4667/) was helpful in
//developing this code.
Outlook.ApplicationClass outlookApp = null;
Outlook.NameSpace outlookNS = null;
bool emailArrived = false;
try
{
outlookApp = new Outlook.ApplicationClass();
outlookNS = outlookApp.GetNamespace("MAPI");
outlookNS.Session.Logon("outlook", "", false, true);
Outlook.MAPIFolder inbox = outlookNS.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
foreach (Outlook.MailItem email in inbox.Items)
{
if (email.Subject == subjectline)
{
emailArrived = true;
email.Delete();
}
}
}
finally
{
if (outlookNS != null)
outlookNS.Logoff();
}
return emailArrived;
}
}
}


Now my unit test code looks like this. This is testing a web service that sends email. The business about ServiceConnections.WebServiceBaseUrl() manages whether we are pointing at the dev or prod service.



using System;
using NUnit.Framework;
using IndependentContractorApp.BusinessLayer;

namespace TestHarness
{
[TestFixture]
public class EmailServiceTestSuite
{
[Test]
public void SendRegularText()
{
string asmx = "EmailService.asmx";
string url = ServiceConnections.WebServiceBaseUrl() + asmx;
EmailWebService.EmailService svc = new EmailWebService.EmailService();
svc.Url = url;
svc.Credentials = System.Net.CredentialCache.DefaultCredentials;
DateTime now = DateTime.Now;
svc.SendRegularText("me@mycompany.org", "SendRegularText Test Email - " + now, "Body Text");
Assert.IsTrue(OutlookInbox.EmailReceived("SendRegularText Test Email - " + now),
"Email failed to arrive");
}
}
}


NUnit green bar of success

Ahh, a thing of beauty. :)


Sunday, November 11, 2007

Futzing with FUTS - Part I

Unit testing is a well accepted practice in the software development community. There are many tools and articles devoted it. Google 'unit testing' if you have any doubts.



What about those of us working in the SAS realm? Given that SAS is basically a data-oriented scripting language, is it feasible to even think of unit test SAS code? I would say, "of course it is, it's code afterall!" If there is code, we can test it. There's CUnit for heaven's sake! I've even had the pleasure of using it (and then opted to roll my own C unit tests). :)



A typical SAS program moves data around or analyzes it and consists of data steps (cursor style data manipulation) and/or procedures ("procs"). Also thrown in are miscellaneous statements to make it all work how it should (e.g., libname statements). Here is SAS program that creates a text file with 100 random numbers between 1 and 10.



data _NULL_;
file "c:\my_folder\random.txt";
do i = 1 to 100;
r = 1 + int(10*ranuni(-1));
put r;
end;
run;


Naturally, SAS is capable of WAY more powerful things, but we must start simple.



What if I want to test my random number generator to make sure that it always and only generates numbers between 1 and 10? How would I do such a thing in SAS? Let's change gears for a minute and consider what we would do in C#. In C# we would have solution containing three projects: a class library called RandomNumberGenerator, a console application called RandomClient, and a class library called TestHarness.



RandomNumberGenerator



using System;

namespace UtilityLib
{
public class RandomNumberGenerator
{
private static Random generator = new Random();

public static int Ranuni()
{
return generator.Next(1, 11);
}
}
}


RandomClient



using System;
using System.IO;
using UtilityLib;

namespace RandomClient
{
class Program
{
static void Main(string[] args)
{
using (StreamWriter writer = new StreamWriter(@"c:\my_folder\random.txt"))
{
for(int i=0; i<100; ++i)
writer.WriteLine(RandomNumberGenerator.Ranuni());
writer.Flush();
writer.Close();
}
}
}
}


TestHarness



using System;
using UtilityLib;
using NUnit.Framework;

namespace TestHarness
{
[TestFixture]
public class RandomNumberGeneratorTestSuite
{
[Test]
public void Ranuni_TestBounds()
{
for (int i = 0; i < 100; ++i)
{
int r = RandomNumberGenerator.Ranuni();
Assert.IsTrue(r >= 1 && r <= 10);
}
}

[Test]
public void Ranuni_TestFullRange()
{
int[] counts = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
for (int i = 0; i < 100; ++i)
counts[RandomNumberGenerator.Ranuni() - 1] += 1;
for (int i = 0; i < 10; ++i)
Assert.IsTrue(counts[i] >= 1);
}
}
}


NUnit Success

Now let's get back to SAS. What can we similarly do to test random number generation in SAS? The trick to unit testing in SAS is to place the code you want to test (your black box, if you will), into a macro. So the random number generation code becomes a SAS macro like this.



RandomNumberGenerator



%macro RandomNumberGenerator;
1 + int(10*ranuni(-1))
%mend;


Now my client code looks like this. It just calls the RandomNumberGenerator macro 100 times to create the output file.



RandomClient



data _NULL_;
file "c:\my_folder\random.txt";
do i = 1 to 100;
r = %RandomNumberGenerator;
put r;
end;
run;


Now what about a test harness for RandomNumberGenerator? It is finally time for FUTS (Framework for Unit Testing SAS® programs) to make its appearance. FUTS, a free product from Thotwave, is a wonderful set of easy to use assert SAS macros that test for various conditions - similar to the set of NUnit asserts. Unlike NUnit, FUTS doesn't have a slick GUI, and instead FUTS throws errors into the SAS log when an assert fails and writes nothing to the log in case of success. To run your tests, run the test harness code and check the SAS log for errors.



To test the RandomNumberGenerator macro, I first create a temporary dataset called test1 that contains 100 random numbers. To do a lower and upper bounds check (i.e., all random numbers are between 1 and 10), I select the max(r) and min(r) into macro variables and use the FUTS macro %assert_sym_compare to test (a) minr is greater than or equal to (GE) 1 and (b) maxr is less than or equal to (LE) 10. This is equivalent to Assert.IsTrue(r >= 1 && r <= 10); in the C# test Ranuni_TestBounds() above. The second test, making sure that each number from 1 to 10 is generated at least once, is accomplished by first performing a proc freq (count how many times each value appears), then getting the count for each number (1, 2, ..., 10) into a macro variable and testing, using %assert_sym_compare again, that each count is GE 1.



TestHarness



data test1;
do i = 1 to 100;
r = %RandomNumberGenerator;
output;
end; drop i;
run;

proc sql noprint;
select min(r), max(r) into :minr, :maxr from test1;
quit;
%assert_sym_compare(&minr, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&maxr, 10, type=COMPARISON, operator=LE);

proc freq data=test1;
tables r / noprint out=freqout;
run;
proc sql noprint;
select count into :count1 from freqout where r=1;
select count into :count2 from freqout where r=2;
select count into :count3 from freqout where r=3;
select count into :count4 from freqout where r=4;
select count into :count5 from freqout where r=5;
select count into :count6 from freqout where r=6;
select count into :count7 from freqout where r=7;
select count into :count8 from freqout where r=8;
select count into :count9 from freqout where r=9;
select count into :count10 from freqout where r=10;
quit;
%assert_sym_compare(&count1, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count2, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count3, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count4, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count5, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count6, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count7, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count8, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count9, 1, type=COMPARISON, operator=GE);
%assert_sym_compare(&count10, 1, type=COMPARISON, operator=GE);


This last bit of code is repetitive and would ideally be "macro-ized".

Friday, November 9, 2007

Fun with XMethods.com

It's fun to occasionally take a stroll through xmethods.com see what kinds of web services folks are creating.



Here are some of the ones I found interesting in my look today.



Convert Text to Braille



Here is a neat and easy to use service for converting text to Braille. The BrailleText conversion method takes two parameters, the text you want to convert (string) and the font size for the output (float). It returns a byte array that is easily converted into a JPG and displayed on a WinForm.



private void button1_Click(object sender, EventArgs e)
{
net.webservicex.www.Braille svc = new TextToBraille1.net.webservicex.www.Braille();
byte[] response = svc.BrailleText(this.textBox1.Text, 20);
using (FileStream fs = new FileStream("braille.jpg", FileMode.Create, FileAccess.Write))
{
foreach(byte b in response)
{
fs.WriteByte(b);
}
}
pictureBox1.Load("braille.jpg");
}


Text to Braille winform

How to convert the image to actual Braille that the blind can read...hmmm...looks like there are many possibilities there.




Dates of U.S. Holidays



This web service offers methods for discovering the dates of U.S. holidays for the year of your choice. For example, the GetThanksgivingDay method requires that you pass it a year value (integer) and it returns a DateTime value. I wired this service up to a simple ASP.NET page containing a calendar control and show this year's and next year's holidays, disabling selection of those dates as well.



Imports System.Collections.Generic

Public Class Holiday
Public Sub New(ByVal d As DateTime, ByVal nm As String)
Me.Date = d
Me.Name = nm
End Sub
Public [Date] As DateTime
Public Name As String
End Class

Partial Class _Default
Inherits System.Web.UI.Page

Private holidays As New List(Of Holiday)

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Dim svc As New com.holidaywebservice.www.USHolidayDates()
For yr As Integer = DateTime.Now.Year To DateTime.Now.Year + 1
holidays.Add(New Holiday(svc.GetChristmasDay(yr), "Christmas"))
holidays.Add(New Holiday(svc.GetIndependenceDay(yr), "Independence Day"))
holidays.Add(New Holiday(svc.GetLaborDay(yr), "Labor Day"))
holidays.Add(New Holiday(svc.GetMartinLutherKingDay(yr), "MLK Day"))
holidays.Add(New Holiday(svc.GetMemorialDay(yr), "Memorial Day"))
holidays.Add(New Holiday(svc.GetNewYear(yr), "New Year's Day"))
holidays.Add(New Holiday(svc.GetPresidentsDay(yr), "President's Day"))
holidays.Add(New Holiday(svc.GetThanksgivingDay(yr), "Thanksgiving"))
Next
End Sub

Protected Sub Calendar1_DayRender(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.DayRenderEventArgs) Handles Calendar1.DayRender
For Each h As Holiday In holidays
If e.Day.Date.Year = h.Date.Year AndAlso e.Day.Date.DayOfYear = h.Date.DayOfYear Then
e.Cell.Text = h.Name
e.Cell.Enabled = False
End If
Next
End Sub
End Class


U.S. Holidays on a Calendar

While the intent of this web service is quite nice, sadly it provides incorrect results as you can see in the image above (Christmas on the 24th? New Year's Day on Dec 31st?). This just illustrates that there are dangers in using black box web services that live "out there in the wild."



Daily Dilbert Cartoon



And last, but not least, this delightful web service serves up the URL of the

Dilbert Cartoon of the day (or the bytes of the JPG). I used the GetDailyDilbertImagePath method (no arguments) to return the URL (string) of the cartoon JPG location and plug that into a picturebox on a WinForm. Very easy.



Public Class Form1
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim svc As New com.esynaps.www.DailyDilbert()
Dim url As String = svc.DailyDilbertImagePath()
Me.PictureBox1.Load(url)
Me.Text = url
End Sub
End Class


WinForm displaying Dilbert cartoon of the day

Thursday, November 8, 2007

Unit Testing an Email Method

Inbox

I recently ran into the problem of needing to unit test a web method in an app that sends email. Searching the web, I finally found this which helped me develop my test. Many thanks to Simon Soanes for his post. Here’s the C# 2.0 test harness code.



using System;
using NUnit.Framework;
using MyApp.WebLayer;
using System.Runtime.InteropServices;
using Outlook = Microsoft.Office.Interop.Outlook; //Refs Microsoft Outlook 11.0 Object Library

namespace TestHarness
{
[TestFixture]
public class EmailServiceTestSuite
{
[Test]
public void SendRegularText()
{
EmailWebService.EmailService svc = new EmailWebService.EmailService();
DateTime now = DateTime.Now;
svc.SendRegularText("me@myorg.org", "SendRegularText Test Email - " + now, "");

System.Threading.Thread.Sleep(10000); //Wait 10 seconds for email to arrive...sometimes not enough

Outlook.ApplicationClass outlookApp = null;
Outlook.NameSpace outlookNS = null;
bool emailArrived = false;
try
{
outlookApp = new Outlook.ApplicationClass();
outlookNS = outlookApp.GetNamespace("MAPI");
outlookNS.Session.Logon("outlook", "", false, true);
Outlook.MAPIFolder inbox = outlookNS.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
foreach (Outlook.MailItem email in inbox.Items)
{
if (email.Subject == "SendRegularText Test Email - " + now)
{
emailArrived = true;
email.Delete();
}
}
}
finally
{
if (outlookNS != null)
outlookNS.Logoff();
}
Assert.IsTrue(emailArrived, "Email failed to arrive");
}
}
}


For testing purposes, I just send a test email to myself with a guaranteed unique subject line, wait for 10 seconds which is usually enough time for the email to show up in my Outlook Inbox, then assert that the email is present. The SendRegularText web method accepts three parameters: email recipient, subject line, and body text (left blank in the test).

Wednesday, November 7, 2007

VB.NET / C# Language Conversion Links

I find it's really helpful to have at least a basic understanding of VB.NET and C#.



C#


using System;

public class MyClass
{
static void Main()
{
Console.WriteLine("Hi, from C#!");
}
}


VB.NET



Imports System

Public Class MyClass
Shared Sub Main()
Console.WriteLine("Howdy, from VB.NET!")
End Sub
End Class


Here are links to some miscellaneous online resources.



Tuesday, November 6, 2007

Test Driving TallPDF.NET 3.0

I am a big fan of PDF documents. The only thing about them that is a drag is creating them. I usually create them from a source document of some kind using CutePDF Writer. As the website says, "FREE for personal and commercial use! No watermarks! No Popup Web Ads!" What could be better!? It installs as a printer driver, so you can create a PDF out of just about any kind of file.



For example, I can take this carefully drawn self-portrait in mspaint.exe and make a PDF using CutePDF Writer in no time flat.



self portrait in mspaint

Hit the File | Print... menu...



CutePDF Writer

Select CutePDF Writer, press Print, et voila! It's a PDF...



PDF of self portrait

What could be better than that? Well, I'll tell you...programmatically creating PDFs!



This is where TallPDF.NET 3.0 comes in.



After downloading this .NET component (I really just need the DLL, although the documentation that comes with it is nice, too), I just reference it in any old .NET application. I will use a C# console application.



Here's all I need in the way of code and references.



Visual Studio C# console program to programmatically create a PDF

I actually don't need System.Data or System.Xml for this example, but System.Drawing, System.Web, and of course TallComponents.PDF.Layout are all required. As you can see from the using statements, TallPDF is organized into multiple namespaces. Here I only reference the three required by this example. The object model is pretty self-explanatory: we have a document object, with one or more section objects, each containing paragraph objects which may be text or shapes or images. At the end of the code, we're just pumping PDF bits through a file stream using the Document.Write method.



The PDF produced looks like this.



self portrait PDF

Here's a slightly more complex example.



using System;
using System.Drawing;
using System.IO;
using TallComponents.PDF.Layout;
using F = TallComponents.PDF.Layout.Fonts;
using TallComponents.PDF.Layout.Paragraphs;
using TallComponents.PDF.Layout.Navigation;
using TallComponents.PDF.Layout.Shapes;
using B = TallComponents.PDF.Layout.Brushes;
using P = TallComponents.PDF.Layout.Pens;
using FLD = TallComponents.PDF.Layout.Shapes.Fields;

namespace TestTallPdf2
{
class Program
{
static void Main(string[] args)
{
string lorem1 = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit...";
string lorem2 = "Cras suscipit. Aliquam hendrerit. Vivamus aliquam. Vestibulum...";
Document doc = new Document();
Section sec = new Section();
doc.Sections.Add(sec);

Header hdr = new Header();
TextParagraph phdr = new TextParagraph();
hdr.Paragraphs.Add(phdr);
phdr.Fragments.Add(new Fragment("My Lorem Ipsum Document", F.Font.HelveticaBoldOblique, 12));
hdr.TopMargin = new Unit(0.5, UnitType.Inch);
sec.Header = hdr;

Heading hd1 = new Heading(0);
hd1.SpacingBefore = 10;
hd1.SpacingAfter = 5;
hd1.Fragments.Add(new Fragment("Introduction", F.Font.Helvetica, 11));
sec.Paragraphs.Add(hd1);

TextParagraph psec = new TextParagraph();
psec.Fragments.Add(new Fragment(lorem1, F.Font.TimesRoman, 10));
psec.SpacingAfter = 10;
psec.LineSpacing = 3;
sec.Paragraphs.Add(psec);

Heading hd2 = new Heading(0);
hd2.SpacingBefore = 10;
hd2.SpacingAfter = 5;
hd2.Fragments.Add(new Fragment("About the Author", F.Font.Helvetica, 11));
sec.Paragraphs.Add(hd2);

TextParagraph psec2 = new TextParagraph();
psec2.Justified = true;
psec2.Fragments.Add(new Fragment(lorem2, F.Font.TimesRoman, 10));
psec2.LineSpacing = 3;
sec.Paragraphs.Add(psec2);

Heading hd3 = new Heading(0);
hd3.SpacingBefore = 10;
hd3.SpacingAfter = 5;
hd3.Fragments.Add(new Fragment("1/4 of a Pie", F.Font.Helvetica, 11));
sec.Paragraphs.Add(hd3);

Drawing drawing = new Drawing(60, 60);
PieShape pie = new PieShape();
pie.Start = 0;
pie.Sweep = 90;
pie.Pen = new P.Pen(System.Drawing.Color.Red, 2);
pie.Brush = new B.SolidBrush(System.Drawing.Color.Blue);
drawing.Shapes.Add(pie);
sec.Paragraphs.Add(drawing);

Heading hd4 = new Heading(0);
hd4.SpacingBefore = 10;
hd4.SpacingAfter = 5;
hd4.Fragments.Add(new Fragment("Lorem Ipsum Bracelet", F.Font.Helvetica, 11));
sec.Paragraphs.Add(hd4);

Drawing drawing2 = new Drawing(180, 180);
ImageShape img = new ImageShape(@"C:\my_folder\bracelet.jpg");
drawing2.Shapes.Add(img);
sec.Paragraphs.Add(drawing2);

Heading hd5 = new Heading(0);
hd5.SpacingBefore = 10;
hd5.SpacingAfter = 5;
hd5.Fragments.Add(new Fragment("Enter your name", F.Font.Helvetica, 11));
sec.Paragraphs.Add(hd5);

Drawing drawing3 = new Drawing(200, 30);
FLD.TextFieldShape field = new FLD.TextFieldShape(200, 30);
field.FullName = "txtName";
drawing3.Shapes.Add(field);
sec.Paragraphs.Add(drawing3);

ViewerPreferences vp = new ViewerPreferences();
vp.ZoomFactor = 0.75; //75%
doc.ViewerPreferences = vp;
using (FileStream fs = new FileStream(@"C:\my_folder\lorem.pdf",
FileMode.Create, FileAccess.Write))
{
doc.Write(fs);
}
}
}
}


This produces a PDF looking like this.



Lorem Ipsum PDF

Pretty nice, eh!



It works in ASP.NET, too.



using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using TallComponents.PDF.Layout;
using TallComponents.PDF.Layout.Paragraphs;

public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Document doc = new Document();
Section sec = new Section();
doc.Sections.Add(sec);
TextParagraph par = new TextParagraph();
par.Fragments.Add(new Fragment("Hello from ASP.NET!"));
sec.Paragraphs.Add(par);
doc.Write(Response);
}
}


ASP.NET example

Saturday, November 3, 2007

SubSonic for Database Versioning

If you're a regular SubSonic user, then you are probably well aware of its database versioning feature. Even if you're into other CRUD glue like data adapters, LINQ, or NHibernate, I think you will find this feature
a reason to check out SubSonic. This is not about source control (there are plenty of other tools for that). This is about taking a snapshot of your database objects (including the data in the tables) at a point in time and being able to recreate an exact copy of that snapshot.



With SubSonic installed, open Visual Studio and go into the Tools | External Tools... menu and configure a new tool as shown. In this case, I'm calling the new tool SubSonic DB Versioner.



SubSonic tool setup in Visual Studio

Click ok, then add an app.config/web.config to your project. I'll explain this using the app.config of a Winforms app.



To set up SubSonic for database versioning, here is the minimum you will need to specify in the config file. This is boilerplate code you can easily reuse by just changing the provider/connection string.



SubSonic app.config settings

I recommend creating a new folder in your project called something like DatabaseSnapshots. You are now ready to make a database snapshot using the versioning tool. All you have to do is go back into the Tools menu and select SubSonic DB Versioner. A window will pop up asking you if you want to accept the default command line arguments (because we checked Prompt for arguments). For the purposes of this demonstration, we need to change the Arguments from "version /out App_Code\DB" to "version /out DatabaseSnapshots". Then let 'er rip, keeping an eye on the Output window to see the progress messages.



When it finishes, you will end up with two new .sql scripts in your DatabaseSnapshots folder (e.g., SqlDataProvider_Schema_2007_10_30.sql and SqlDataProvider_Data_2007_10_30.sql). The first script, the one with Schema in the name, contains code to recreate all tables, views, stored procedures, etc.



Here's just a tiny portion of the schema script.



/****** Object:  Table [dbo].[CustomerDemographics]    Script Date: 10/30/2007 13:52:13 ******/
SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[CustomerDemographics]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)
BEGIN
CREATE TABLE [dbo].[CustomerDemographics](
[CustomerTypeID] [nchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[CustomerDesc] [ntext] COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
CONSTRAINT [PK_CustomerDemographics] PRIMARY KEY NONCLUSTERED
(
[CustomerTypeID] ASC
) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
END
/****** Object: Table [dbo].[Region] Script Date: 10/30/2007 13:52:14 ******/
SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[Region]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)
BEGIN
CREATE TABLE [dbo].[Region](
[RegionID] [int] NOT NULL,
[RegionDescription] [nchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
CONSTRAINT [PK_Region] PRIMARY KEY NONCLUSTERED
(
[RegionID] ASC
) ON [PRIMARY]
) ON [PRIMARY]
END
/****** Object: Table [dbo].[Employees] Script Date: 10/30/2007 13:52:14 ******/
SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[Employees]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)
BEGIN
CREATE TABLE [dbo].[Employees](
[EmployeeID] [int] IDENTITY(1,1) NOT NULL,
[LastName] [nvarchar](20) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[FirstName] [nvarchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[Title] [nvarchar](30) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[TitleOfCourtesy] [nvarchar](25) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[BirthDate] [datetime] NULL,
[HireDate] [datetime] NULL,
[Address] [nvarchar](60) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[City] [nvarchar](15) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[Region] [nvarchar](15) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[PostalCode] [nvarchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[Country] [nvarchar](15) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[HomePhone] [nvarchar](24) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[Extension] [nvarchar](4) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[Photo] [image] NULL,
[Notes] [ntext] COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[ReportsTo] [int] NULL,
[PhotoPath] [nvarchar](255) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
CONSTRAINT [PK_Employees] PRIMARY KEY CLUSTERED
(
[EmployeeID] ASC
) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
END


That's pretty neat, but the second script is the coolest. It contains code to populate the database tables with your data. Here's a snippet of the data script.



ALTER TABLE [Region] NOCHECK CONSTRAINT ALL
GO

PRINT 'Begin inserting data in Region'
INSERT INTO [Region] ([RegionID], [RegionDescription])
VALUES(1, 'Eastern ')
INSERT INTO [Region] ([RegionID], [RegionDescription])
VALUES(2, 'Western ')
INSERT INTO [Region] ([RegionID], [RegionDescription])
VALUES(3, 'Northern ')
INSERT INTO [Region] ([RegionID], [RegionDescription])
VALUES(4, 'Southern ')
ALTER TABLE [Region] CHECK CONSTRAINT ALL
GO



ALTER TABLE [Shippers] NOCHECK CONSTRAINT ALL
GO

SET IDENTITY_INSERT [Shippers] ON
PRINT 'Begin inserting data in Shippers'
INSERT INTO [Shippers] ([ShipperID], [CompanyName], [Phone])
VALUES(1, 'Speedy Express', '(503) 555-9831')
INSERT INTO [Shippers] ([ShipperID], [CompanyName], [Phone])
VALUES(2, 'United Package', '(503) 555-3199')
INSERT INTO [Shippers] ([ShipperID], [CompanyName], [Phone])
VALUES(3, 'Federal Shipping', '(503) 555-9931')
SET IDENTITY_INSERT [Shippers] OFF
ALTER TABLE [Shippers] CHECK CONSTRAINT ALL
GO



ALTER TABLE [Suppliers] NOCHECK CONSTRAINT ALL
GO

SET IDENTITY_INSERT [Suppliers] ON
PRINT 'Begin inserting data in Suppliers'
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(1, 'Exotic Liquids', 'Charlotte Cooper', 'Purchasing Manager', '49 Gilbert St.', 'London', NULL, 'EC1 4SD', 'UK', '(171) 555-2222', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(2, 'New Orleans Cajun Delights', 'Shelley Burke', 'Order Administrator', 'P.O. Box 78934', 'New Orleans', 'LA', '70117', 'USA', '(100) 555-4822', NULL, '#CAJUN.HTM#')
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(3, 'Grandma Kelly''s Homestead', 'Regina Murphy', 'Sales Representative', '707 Oxford Rd.', 'Ann Arbor', 'MI', '48104', 'USA', '(313) 555-5735', '(313) 555-3349', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(4, 'Tokyo Traders', 'Yoshi Nagase', 'Marketing Manager', '9-8 Sekimai Musashino-shi', 'Tokyo', NULL, '100', 'Japan', '(03) 3555-5011', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(5, 'Cooperativa de Quesos ''Las Cabras''', 'Antonio del Valle Saavedra', 'Export Administrator', 'Calle del Rosal 4', 'Oviedo', 'Asturias', '33007', 'Spain', '(98) 598 76 54', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(6, 'Mayumi''s', 'Mayumi Ohno', 'Marketing Representative', '92 Setsuko Chuo-ku', 'Osaka', NULL, '545', 'Japan', '(06) 431-7877', NULL, 'Mayumi''s (on the World Wide Web)#http://www.microsoft.com/accessdev/sampleapps/mayumi.htm#')
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(7, 'Pavlova, Ltd.', 'Ian Devling', 'Marketing Manager', '74 Rose St. Moonie Ponds', 'Melbourne', 'Victoria', '3058', 'Australia', '(03) 444-2343', '(03) 444-6588', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(8, 'Specialty Biscuits, Ltd.', 'Peter Wilson', 'Sales Representative', '29 King''s Way', 'Manchester', NULL, 'M14 GSD', 'UK', '(161) 555-4448', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(9, 'PB Knäckebröd AB', 'Lars Peterson', 'Sales Agent', 'Kaloadagatan 13', 'Göteborg', NULL, 'S-345 67', 'Sweden', '031-987 65 43', '031-987 65 91', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(10, 'Refrescos Americanas LTDA', 'Carlos Diaz', 'Marketing Manager', 'Av. das Americanas 12.890', 'Sao Paulo', NULL, '5442', 'Brazil', '(11) 555 4640', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(11, 'Heli Süßwaren GmbH & Co. KG', 'Petra Winkler', 'Sales Manager', 'Tiergartenstraße 5', 'Berlin', NULL, '10785', 'Germany', '(010) 9984510', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(12, 'Plutzer Lebensmittelgroßmärkte AG', 'Martin Bein', 'International Marketing Mgr.', 'Bogenallee 51', 'Frankfurt', NULL, '60439', 'Germany', '(069) 992755', NULL, 'Plutzer (on the World Wide Web)#http://www.microsoft.com/accessdev/sampleapps/plutzer.htm#')
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(13, 'Nord-Ost-Fisch Handelsgesellschaft mbH', 'Sven Petersen', 'Coordinator Foreign Markets', 'Frahmredder 112a', 'Cuxhaven', NULL, '27478', 'Germany', '(04721) 8713', '(04721) 8714', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(14, 'Formaggi Fortini s.r.l.', 'Elio Rossi', 'Sales Representative', 'Viale Dante, 75', 'Ravenna', NULL, '48100', 'Italy', '(0544) 60323', '(0544) 60603', '#FORMAGGI.HTM#')
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(15, 'Norske Meierier', 'Beate Vileid', 'Marketing Manager', 'Hatlevegen 5', 'Sandvika', NULL, '1320', 'Norway', '(0)2-953010', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(16, 'Bigfoot Breweries', 'Cheryl Saylor', 'Regional Account Rep.', '3400 - 8th Avenue Suite 210', 'Bend', 'OR', '97101', 'USA', '(503) 555-9931', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(17, 'Svensk Sjöföda AB', 'Michael Björn', 'Sales Representative', 'Brovallavägen 231', 'Stockholm', NULL, 'S-123 45', 'Sweden', '08-123 45 67', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(18, 'Aux joyeux ecclésiastiques', 'Guylène Nodier', 'Sales Manager', '203, Rue des Francs-Bourgeois', 'Paris', NULL, '75004', 'France', '(1) 03.83.00.68', '(1) 03.83.00.62', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(19, 'New England Seafood Cannery', 'Robb Merchant', 'Wholesale Account Agent', 'Order Processing Dept. 2100 Paul Revere Blvd.', 'Boston', 'MA', '02134', 'USA', '(617) 555-3267', '(617) 555-3389', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(20, 'Leka Trading', 'Chandra Leka', 'Owner', '471 Serangoon Loop, Suite #402', 'Singapore', NULL, '0512', 'Singapore', '555-8787', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(21, 'Lyngbysild', 'Niels Petersen', 'Sales Manager', 'Lyngbysild Fiskebakken 10', 'Lyngby', NULL, '2800', 'Denmark', '43844108', '43844115', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(22, 'Zaanse Snoepfabriek', 'Dirk Luchte', 'Accounting Manager', 'Verkoop Rijnweg 22', 'Zaandam', NULL, '9999 ZZ', 'Netherlands', '(12345) 1212', '(12345) 1210', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(23, 'Karkki Oy', 'Anne Heikkonen', 'Product Manager', 'Valtakatu 12', 'Lappeenranta', NULL, '53120', 'Finland', '(953) 10956', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(24, 'G''day, Mate', 'Wendy Mackenzie', 'Sales Representative', '170 Prince Edward Parade Hunter''s Hill', 'Sydney', 'NSW', '2042', 'Australia', '(02) 555-5914', '(02) 555-4873', 'G''day Mate (on the World Wide Web)#http://www.microsoft.com/accessdev/sampleapps/gdaymate.htm#')
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(25, 'Ma Maison', 'Jean-Guy Lauzon', 'Marketing Manager', '2960 Rue St. Laurent', 'Montréal', 'Québec', 'H1J 1C3', 'Canada', '(514) 555-9022', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(26, 'Pasta Buttini s.r.l.', 'Giovanni Giudici', 'Order Administrator', 'Via dei Gelsomini, 153', 'Salerno', NULL, '84100', 'Italy', '(089) 6547665', '(089) 6547667', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(27, 'Escargots Nouveaux', 'Marie Delamare', 'Sales Manager', '22, rue H. Voiron', 'Montceau', NULL, '71300', 'France', '85.57.00.07', NULL, NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(28, 'Gai pâturage', 'Eliane Noz', 'Sales Representative', 'Bat. B 3, rue des Alpes', 'Annecy', NULL, '74000', 'France', '38.76.98.06', '38.76.98.58', NULL)
INSERT INTO [Suppliers] ([SupplierID], [CompanyName], [ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], [Country], [Phone], [Fax], [HomePage])
VALUES(29, 'Forêts d''érables', 'Chantal Goulet', 'Accounting Manager', '148 rue Chasseur', 'Ste-Hyacinthe', 'Québec', 'J2S 7S8', 'Canada', '(514) 555-2955', '(514) 555-2921', NULL)
SET IDENTITY_INSERT [Suppliers] OFF
ALTER TABLE [Suppliers] CHECK CONSTRAINT ALL
GO


I was so excited the first time I saw this, I nearly choked on my Diet Coke.



There's a nice introduction to SubSonic here. The latest news (here and here) is that Rob Conery (a.k.a. Mr. SubSonic) has joined Microsoft and MS will be paying him to continue developing SS. Rock on.

Wednesday, October 31, 2007

Your Friends, The Visual Studio Code Snippets

There are some great and well-known third-party tools that enhance or complement Visual Studio .NET, such as NUnit and Roeder's Reflector. Visual Studio's Code Snippets, both the default (built-in) snippets, and the C# and VB.NET add on snippets are amazingly useful and seemingly under-advertised and under-utilized. I don't hear many people talking about them and I haven't seen much use of them.



What are code snippets? Code shortcuts for writing bigger chunks of code. A quick and powerful example is a good way to introduce them.



Here's the default code snippet "prop" in action in C# that codes up an entire read/write property complete with private storage variable. They way I use this is instead of writing everything out by hand to code out a property, I simply type "prop" followed by TAB, TAB. As you can see, prop even appears in Intellisense.



prop code snippet in Intellisense dropdown list

After I hit TAB the 2nd time, I see this.



prop code snippet fills in a lot of code for you

Then I just type a quick "string", TAB, TAB, "myName", TAB, "Name" and I'm done!



quick product of using the prop code snippet

Will I ever type out another private member/getter/setter property combo again? I doubt it.



The official documentation says, "The Code Snippet Inserter is invoked through the Insert Code Snippet or Surround With commands on the IntelliSense menu, or by using the keyboard shortcuts CTRL+K, then X and CTRL+K, then S respectively." I find it easiest to just type out the snippet's keyword and press TAB, TAB. Things are a little different in the VB.NET editor. Snippet keywords don't appear on Intellisense lists, but simply right-click and select Insert Snippet.



in VB.NET right-click and select Insert Snippet

After clicking Insert Snippet, a categorized menu of snippets is presented.



snippet menu

Selecting Common Code Patterns and then Exception Handling and shows the available snippets in that category.



Exception handling code snippets

Selecting Try...Catch...Finally...End Try Statement inserts this code.



Try...Catch...Finally...End Try code snippet

Now I just need to decide what class of Exception I want to catch and I'm off and running.



To add on more snippets from MSDN or make and add your own, use the Code Snippets Manager.



Code Snippets Manager

For example, adding the Database code snippets gives you nice shortcuts such as "adoCreateSqlConn" which generates the following code.



System.Data.SqlClient.SqlConnection conn = new System.Data.SqlClient.SqlConnection();
conn.ConnectionString = @"Data Source=ServerName;Initial Catalog=Northwind;Persist Security Info=True;User ID=;Password=";


I hope this was helpful. Enjoy!

Monday, October 29, 2007

Microsoft .NET (2.0) FTP Support Still Sucks

An FTP client is coaxed out of the .NET 1.1 framework by Liberty (& Enterprise Distributed Technologies –LOL) and Howard Richards shows how to get the job done with .NET 2.0. Whidbey (2.0) was billed as having FTP support, but when you have to write this code does that qualify as support?
string URI = "ftp://mirror.x10.com/gnuftp/gnu/README.DESCRIPTIONS";
System.IO.FileInfo fi = new System.IO.FileInfo(@"c:\temp\deleteme.txt");
System.Net.FtpWebRequest ftp =
(System.Net.FtpWebRequest)System.Net.FtpWebRequest.Create(URI);
ftp.Credentials = new System.Net.NetworkCredential("anonymous", "");
ftp.KeepAlive = false;
ftp.Method = System.Net.WebRequestMethods.Ftp.DownloadFile;
ftp.UseBinary = true;
using (System.Net.FtpWebResponse response = (System.Net.FtpWebResponse)ftp.GetResponse())
{
using (System.IO.Stream responseStream = response.GetResponseStream())
{
using (System.IO.FileStream fs = fi.OpenWrite())
{
try
{
byte[] buffer = new byte[2048];
int read = 0;
do
{
read = responseStream.Read(buffer, 0, buffer.Length);
fs.Write(buffer, 0, read);
} while (!(read == 0));
responseStream.Close();
fs.Flush();
fs.Close();
}
catch (Exception)
{
fs.Close();
fi.Delete();
throw;
}
}
responseStream.Close();
}
response.Close();
}

It almost makes more sense to shell out to c:\windows\system32\ftp.exe as in this VBA code.
Dim ff As Integer
ff = FreeFile
ff = FreeFile
Open "c:\temp\ftpcommands.scr" For Output As #ff
Print #ff, "open mirror.x10.com"
Print #ff, "anonymous"
Print #ff, "no_password"
Print #ff, "cd gnuftp/gnu"
Print #ff, "binary"
Print #ff, "get README.DESCRIPTIONS c:\temp\deleteme3.txt"
Print #ff, "bye"
Close #ff
ff = FreeFile
Open "c:\temp\ftpcommands.bat" For Output As #ff
Print #ff, "ftp -s:c:\temp\ftpcommands.scr"
Print #ff, "Echo ""Complete"" > c:\temp\ftpcommands.done"
Close #ff
Shell "c:\temp\ftpcommands.bat", vbHide
Do While Dir("c:\temp\ftpcommands.done") = ""
DoEvents
Loop
Dim dtmContinueAt As Date
dtmContinueAt = Now + TimeValue("0:00:03")
Do While Now < dtmContinueAt
Loop
Kill "c:\temp\ftpcommands.scr"
Kill "c:\temp\ftpcommands.bat"
Kill "c:\temp\ftpcommands.done"

Now contemplate this equivalent Ruby code.
require 'net/ftp'
Net::FTP.open('mirror.x10.com') do |ftp|
ftp.login
files = ftp.chdir('gnuftp/gnu')
ftp.getbinaryfile('README.DESCRIPTIONS', 'c:\temp\deleteme2.txt')
end

I feel prompted to ask, just wtf does Microsoft have against FTP? Aside from that, the obvious question is “will .NET 3.x ‘Orcas’ do better?” Or maybe the obvious question is, “is .NET a misnomer*?”

* Because last time I checked FTP was an Internet protocol: “a system for transferring computer files especially via the Internet.”

First Post

Dear readers, welcome to my blog. This is the first of what I hope will be many posts.

You may be wondering...what is “technoyoga”? I chose the name of this blog because I am interested in and practicing both computer technology and spirituality (among other things). A large bulk of my time is spent working in the computer programming arena as well as the spiritual practice domain. And who says they can't co-exist? What programmer hasn’t experienced the meditative bliss of an n-hour long coding binge in which the ego subsides and is replaced by classes, methods, unit tests, and compilations? I think a lot of programmers out there know what I’m talking about even if they haven’t put it in such terms.

I was introduced to the term “karma yoga” by Ram Dass many years ago and quite like the practice, hard as it is. As a blogger, I-I will strive to jab and throw haymakers on a regular basis without expectation and have some fun. I figure if Scott Hanselman can have his Computer Zen, then I can have my technoyoga. :)

Please note I am not affiliated in any way with Technoyoga.com or the Techno Yoga Mat. Hopefully they won’t ask me to change the name of my blog.