Wednesday, April 04, 2012

2012 FoxPro Lifetime Achievement Award Committee

The FoxPro Lifetime Achievement Award honors those individuals who have contributed a great deal to the FoxPro community over the years. See http://fox.wikis.com/wc.dll?Wiki~FoxProCommunityLifetimeAchievementAward~VFP for previous award recipients. These recipients wish to continue the award and have created a committee to select a recipient for 2012. The committee consists of all 11 living previous recipients, Alan Griver (yag) of Microsoft, and one person from the FoxPro community.

If you wish to be considered for inclusion on the committee, please email me (dhennig@stonefield.com) by April 30, 2012. The existing committee will select the community member and announce their selection in early May. The committee will then issue a call for nominations for the 2012 recipient and will make their selection from the nominees.

2012 Ceil Silver Ambassador Nominees

The Ceil Silver Ambassador Fund brings a developer to the Southwest Fox conference in the United States as an ambassador for the developers in his or her country. This gives the recipient the opportunity to meet and share experiences with developers attending Southwest Fox and gives other attendees the opportunity to learn about VFP development in the recipient's country. For more information on the Ambassador Fund or to learn how to contribute to it, please see http://www.swfox.net/ambassador.aspx.

The Ambassador selection committee consists of Southwest Fox organizers Rick Schummer, Tamar Granor, and Doug Hennig, former Ambassadors Emerson Santon Reed, Cesar Chalom, Bernard Bout, and Borislav Borissov, and VFP community members Christof Wollenhaupt and Rick Bean.
We are looking for nominations from the VFP community for the 2012 recipient. To nominate someone you think is deserving to be selected, please email their name and a brief list of their contributions to the VFP community to ambassadorfund@swfox.net. Names must be submitted no later than April 30, 2012.

Thursday, March 29, 2012

The Internet (and my wife) Literally Saved my Life

A couple of weeks ago, I slipped on an icy sidewalk taking out the garbage and thought I’d sprained my ankle. (Nice timing: by the next day, all the ice and snow was gone!) Although I felt something go “pop” inside, I was sure it wasn’t broken because within five minutes I could put a little weight on it (I limped back into the house) and within an hour was able to put almost my full weight on it. I spent the morning elevating and icing it. At lunch, my wife came home from work and insisted she take me for an x-ray. The doctor assured us it wasn’t broken but took an x-ray just in case. Was he surprised to see a double break in the fibula just above where it meets the ankle. Also, the entire ankle was dislocated a few millimeters to the left. (And of course it was sprained too, but that goes with the territory.) I had surgery that night to screw a plate into place to hold things together and reset the ankle displacement.

Life is a hassle dealing with an immobilized leg and crutches: you can’t carry anything so someone has to fetch stuff for you, taking a shower is a major production, stairs are scary, and I have to keep my leg elevated a lot to keep my foot from swelling too much. So, that means only a couple of hours a day at the computer and sleeping in a spare bedroom on the main floor of our house. My poor wife Peggy: her work load about tripled: doing all of my household chores in addition to hers plus taking care of me. I’m going to owe her big time once I get back on my feet!

Fast forward almost two weeks. Feeling like my life is on hold for six weeks but getting through it OK. Virtually no pain in my ankle. We drove to Winnipeg for the Canada West Short Track Speed Skating Championships, which my son qualified for. After a day of watching skating on Saturday, I developed a low-grade fever and some back pain right after dinner. It cleared up Sunday, but then I awoke about midnight Sunday night to the most excruciating pain I’d ever felt. Every breath felt like someone was stabbing me with a knife in my left-side ribs. I took some muscle relaxants and after they kicked in, I was able to fall asleep sitting up (laying down was still too painful).

Monday was much better. Still had a low-grade fever but the rib pain was manageable. We drove back home from Winnipeg and Peggy and I discussed what could have caused the pain and fever. I was sure it was pulled muscles in my ribs: I’d been sitting in a cold arena for a couple of days, getting up and down the bleachers, probably straining muscles in the process. I had something similar happen about six months earlier: I sneezed while in an awkward position and pulled the muscles in my right ribcage, and this felt just like that. However, I couldn’t explain the fever, so Peggy had me Google “fever after surgery” on our iPad while I rested on the couch.

The most common cause is infection, but since I didn’t have any pain in my leg, I doubted that was it. A few others didn’t make sense in my case, but one thing did seem slightly reasonable: pulmonary embolism. Circulation in an immobilized leg is reduced and as a result, a clot can form. If part of the clot breaks off, it can travel up and get stuck in the lung, causing chest pain, especially when taking deep breaths, and low-grade fever. However, it’s also often accompanied by altered heart rhythm, shortness of breath, and other symptoms I didn’t have. Plus the pain really felt like it was in my ribs rather than in my lungs and I didn’t think muscle relaxants would relieve lung pain. I was about to write that one off too when a sentence caught my eye: pulmonary embolism is fatal if untreated. I mentioned that to Peggy and she insisted we go to the ER the next morning. I agreed, thinking we’d eliminate that possibility and find the real issue.

After some preliminary tests, the doctor figured it was either a clot or pneumonia. I told him “I hope I have pneumonia. Hmm, I doubt that’s a sentence that’s been said before”. He laughed but agreed with me. After more tests, x-rays, and even a CT scan, the diagnosis came back: pulmonary embolism. Crap.

Fortunately, the treatment is straight-forward: blood thinners for six months to prevent further clotting and eventually the existing clots will erode. Blood thinners have their own hassles—I have to inject my belly twice a day for the next four days with a fast-acting blood thinner; I need daily blood tests for at least a week to make sure the dosage of blood thinner pills is correct; certain foods, such as spinach, are off limits; bruises form easily; I have to wear a compression stocking after the cast comes off (like that won’t make me feel like I’m an 80 year-old); and alcohol is discouraged (nooooooooo!). However, the doctor and several nurses told me while I was unlucky to have this happen, I was lucky it was a small clot (a larger one could’ve caused sudden death) and that we came into the ER early (lots of people wait for days, hoping the symptoms will go away on their own).

So, there are two morals to this story (well, three: stuff happens, so deal with it):

  • Don’t be afraid to get an unlikely, even exotic-sounding, potential problem checked out. No one will laugh at you. Google is definitely your friend.
  • Always listen to your wife. Always. She is smarter than you.

Monday, March 19, 2012

Southwest Fox 2012 Session Proposals Due This Week

Just a reminder that session proposals for Southwest Fox 2012 are due by 8 AM EDT this Friday, March 23. If you’re interested in speaking, please download the Call for Speakers and the proposal application from http://www.swfox.net/CallForSpeakers.aspx. We look forward to hearing from you.

Tuesday, March 06, 2012

Southwest Fox 2012

Save the dates for Southwest Fox and Southwest Xbase++ 2012! The conferences take place October 18-21, 2012.

This year we have two conferences as one great event at the same location. Southwest Fox has always served Visual FoxPro developers an opportunity to learn and extend their skills, and network with fellow developers. Alaska Software is working on PolarFox, a product that keeps the Visual FoxPro language alive in their next generation tool. You get two conferences for the price of one!

The conferences take place at the San Tan Elegante Conference and Reception Center, the same great location as last year.

If you’re interested in presenting at Southwest Fox 2012, please visit http://www.swfox.net/callforspeakers.aspx, read the complete Call for Speakers document (linked from that page), and download the proposal submission application. Session proposals are due by March 23.

Finally, if there are any topics you hope will be covered this year, please send them to info@swfox.net, right away.

Friday, February 10, 2012

Automatic Property Testing

Suppose you have a class that looks like this:

public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName));
}

private string _Phone;

public string Phone
{
get
{
return _Phone;
}
set
{
if (_Phone != value)
{
_Name = value;
OnPropertyChanged("Phone");
}
}
}

private string _Name;

public string Name
{
get
{
return _Name;
}
set
{
if (_Name != value)
{
_Name = value;
}
}
}
}


It’s a simple person class with Name and Phone properties. Because it implements INotifyPropertyChanged, the setter for every property should raise the PropertyChanged event. However, there are two bugs in this class:




  • The setter for Phone sets _Name rather than _Phone.


  • The developer forgot to raise PropertyChanged in the setter for Name.



We want to write some unit tests for this class. We need at least two tests for every property: one ensuring the value stored to the property is the one read from it and one ensuring that storing a value raises PropertyChanged. Here’s an example of the tests for Phone:



private string changedProperty;

public void PropertyChanged(object sender,
System.ComponentModel.PropertyChangedEventArgs e)
{
changedProperty = e.PropertyName;
}

[TestMethod()]
public void Phone_ValueTest()
{
Person target = new Person();
string expected = "555-555-5555";
target.Phone = expected;
string actual = target.Phone;
Assert.AreEqual(expected, actual);
}

[TestMethod()]
public void Phone_RaisesPropertyChanged()
{
Person target = new Person();
target.PropertyChanged += new
PropertyChangedEventHandler(PropertyChanged);
target.Phone = "555-555-5555";
string actual = changedProperty;
string expected = "Phone";
Assert.AreEqual(expected, actual);
}


As you can guess, it’s quickly going to get tedious writing these tests, especially if you have a lot of properties. Let’s automate this.



ClassTester is a project on CodePlex (http://classtester.codeplex.com) that automatically tests every property to ensure the value written is the value read and, if the class implements INotifyPropertyChanged, writing to the property raises PropertyChanged. Using ClassTester couldn’t be easier: replace all of the property test methods with this one simple method:



[TestMethod()]
public void AutomaticPropertyChangedTest()
{
PropertyTester tester = new PropertyTester(new Person());
tester.TestProperties();
}


Nice and (almost) generic: we can use this test forevery class; the only change we have to make is the name of the class to test.



When run, this test fails with the error “The get value of the 'Phone' property on the type 'AutomaticPropertyTesting.Person' did not equal the set value”. That points out the first bug, so we fix that by changing “_Name” to “_Phone” in the setter for Phone. (If this seems far-fetched, I actually ran into that today; the hazards of copy and paste coding!) Running the test again still gives a failure: “The property 'Name' on the type 'AutomaticPropertyTesting.Person' did not throw a PropertyChangedEvent”. Adding OnPropertyChanged(“Phone”) to the setter for Name doesn’t fix the problem; only the correct OnPropertyChanged(“Name”) allows the test to pass.



Let’s make the class a little more complex by adding an ID property that’s automatically set to a new Guid value when the class is instantiated. Note that the setter is private so only this class can change the value, such as when a person is loaded from a database, and that it doesn’t raise PropertyChanged because of this.



private Guid _id = Guid.NewGuid();

public
Guid ID
{
get
{
return _id;
}

private set
{
_id = value;
}
}

Now the test fails again: “The property 'ID' on the type 'AutomaticPropertyTesting.Person' did not throw a PropertyChangedEvent”. Fortunately, ClassTester has a mechanism to ignore certain properties: you can add this to the test method:

tester.IgnoredProperties.Add("ID");


Of course, you’d have to do that for every property you don’t want automatically tested (not only for this reason, but some others described on the Codeplex site, such as when a property’s type is an interface or a class with a non-default constructor). That can get a little tedious too. So, I created a couple of classes to further automate this.



The first class is a new attribute:



public class DontAutomaticallyTestAttribute : Attribute {}


The second class is a façade for PropertyTester. What it brings to the table is automatically adding properties marked with the DontAutomaticallyTest attribute to the IgnoredProperties list.



public class PropertyTest
{
private object _toBeTested;

public PropertyTest(object toBeTested)
{
_toBeTested = toBeTested;
}

public void TestProperties()
{
PropertyTester tester = new PropertyTester(_toBeTested);
PropertyInfo[] props = _toBeTested.GetType().GetProperties(
BindingFlags.Public | BindingFlags.Instance);
foreach (PropertyInfo prop in props)
{
if (DontTest(prop))
{
tester.IgnoredProperties.Add(prop.Name);
}
}
tester.TestProperties();
}

private bool DontTest(PropertyInfo property)
{
return property.GetCustomAttributes(
typeof(DontAutomaticallyTestAttribute), false).Length > 0;
}
}


Now you change the line of code in AutomaticPropertyChangedTest to use the new class:



PropertyTest tester = new PropertyTest(new Person());



and add the DontAutomaticallyTest attribute to those properties you don’t want automatically tested (you will write manual tests, won’t you?):



[DontAutomaticallyTest]
public Guid ID


Of course, you also have to add a reference to the project holding these two classes to both the project for your classes and your test project. However, for a couple of minutes work, you can now save tons of time creating tests for all the properties in your classes.

Sunday, February 05, 2012

Updating the VFPX ReportBuilder.APP

There are a couple of bugs in the VFP 9 Service Pack 2 version of ReportBuilder.APP, which provides the Report Designer dialogs and event handlers. I discussed the first bug, which causes the Printer Environment setting for a report to be turned on when you click the font button in the Field or Label Properties dialogs, in an earlier blog post. The other is a small one: using code like:

do (_reportbuilder) with 3, "TablePath.DBF"

to specify a custom registry table causes DELETED to be set off. The fix for this is simple: save the current setting of DELETED in FRXBuilder.PRG before setting it off and restore the setting near the end.

Several people suggested I actually implement these fixes in the copy of ReportBuilder.APP (and the corresponding source code) available on VFPX, so I did so today.

Thursday, December 08, 2011

Using Google Translate from VFP

Stonefield Query is a localizable application: all strings displayed to the user are stored in a resource file and we provide a resource editor that allows a developer to translate the strings into other languages. This means that someone has to do the translation into a particular language and keep it update to date when we release a new version, which as you can guess is a lot of work.

Inspired by Christof Wollenhaupt’s Googlefy Your Apps session at Southwest Fox 2011, I looked at using the Google Translate API to automate the translation process and allow us to translate into more languages than have currently been done. It actually turned out to be pretty easy.

First, you have to sign up for a Google account. The Translate API isn’t free but it isn’t very expensive: $20 for 1 million characters. After you’ve enabled the API, you’re assigned a key that has to be passed to the API on every call.

The API uses REST, which is a fancy of way of saying that the parameters are passed as part of a URL. Here’s an example:

https://www.googleapis.com/language/translate/v2?key=INSERT-YOUR-KEY&q=hello%20world&source=en&target=de

This tells the API to translate “hello world” (the encoded text in the “q” parameter) from English (“en” in the source parameter) to German (“de” in the target parameter). It returns the result as JSON:

{ "data": { "translations": [ { "translatedText": "Hallo Welt" } ] } }

To access the API from VFP code, use Craig Boyd’s VFPConnection library. Below is a function that does all the work. Pass it the text to translate and the source and target languages (spelled out, such as “English” and “German”) and it returns either the translated text if it succeeded, null if the language is invalid, or blank if the translation failed. This function supports all of the languages the Translate API supports. Note: replace the assignment to lcKey with your Google API key.

Automatic translation may not be quite as good as manual translation because it doesn’t necessarily use the same colloquialisms a native speaker would. However, it’s an excellent starting point; someone can use the Resource Editor to tweak any strings to the proper translation.

lparameters tcPhrase, ;
    tcFromLanguage, ;
    tcToLanguage
local lcKey, ;
    lcPhrase, ;
    lcFromLanguage, ;
    lcToLanguage, ;
    lcURL, ;
    lcResult, ;
    lcTranslate

* Specify the Google API key.

lcKey = 'PUT YOUR KEY HERE'

* HTML encode the phrase to translate.

lcPhrase = Encode(tcPhrase)

* Get the language codes.

lcFromLanguage = GetLanguage(tcFromLanguage)
if empty(lcFromLanguage)
    return .NULL.
endif empty(lcFromLanguage)
lcToLanguage = GetLanguage(tcToLanguage)
if empty(lcToLanguage)
    return .NULL.
endif empty(lcToLanguage)

* Set up VFPConnection.

set library to VFPConnection.FLL

* Call Google Translate and return the result.

lcURL       = 'https://www.googleapis.com/language/translate/v2' + ;
    '?key=' + lcKey + ;
    '&q=' + lcPhrase + ;
    '&source=' + lcFromLanguage + ;
    '&target=' + lcToLanguage
lcResult    = HTTPSToStr(lcURL)
lcTranslate = ''
if not empty(lcResult)
    lcTranslate = strconv(strextract(lcResult, '"translatedText": "', '"'), 11)
endif not empty(lcResult)
return lcTranslate


function Encode(tcString)
local lcString
lcString = strtran(tcString, '<', '&lt;')
lcString = strtran(lcString, '>', '&gt;')
lcString = strtran(lcString, '"', '&quot;')
lcString = strtran(lcString, '&', '&amp;')
lcString = strtran(lcString, ' ', '%20')
lcString = strtran(lcString, '?', '%3F')
return lcString


procedure GetLanguage(tcLanguage)
local laLanguages[52, 2], ;
    lnLanguage, ;
    lcLanguage
laLanguages[ 1, 1] = 'Afrikaans'
laLanguages[ 1, 2] = 'af'

laLanguages[ 2, 1] = 'Albanian'
laLanguages[ 2, 2] = 'sq'

laLanguages[ 3, 1] = 'Arabic'
laLanguages[ 3, 2] = 'ar'

laLanguages[ 4, 1] = 'Belarusian'
laLanguages[ 4, 2] = 'be'

laLanguages[ 5, 1] = 'Bulgarian'
laLanguages[ 5, 2] = 'bg'

laLanguages[ 6, 1] = 'Catalan'
laLanguages[ 6, 2] = 'ca'

laLanguages[ 7, 1] = 'Chinese Simplified'
laLanguages[ 7, 2] = 'zh-CN'

laLanguages[ 8, 1] = 'Chinese Traditional'
laLanguages[ 8, 2] = 'zh-TW'

laLanguages[ 9, 1] = 'Croatian'
laLanguages[ 9, 2] = 'hr'

laLanguages[10, 1] = 'Czech'
laLanguages[10, 2] = 'cs'

laLanguages[11, 1] = 'Danish'
laLanguages[11, 2] = 'da'

laLanguages[12, 1] = 'Dutch'
laLanguages[12, 2] = 'nl'

laLanguages[13, 1] = 'English'
laLanguages[13, 2] = 'en'

laLanguages[14, 1] = 'Estonian'
laLanguages[14, 2] = 'et'

laLanguages[15, 1] = 'Filipino'
laLanguages[15, 2] = 'tl'

laLanguages[16, 1] = 'Finnish'
laLanguages[16, 2] = 'fi'

laLanguages[17, 1] = 'French'
laLanguages[17, 2] = 'fr'

laLanguages[18, 1] = 'Galician'
laLanguages[18, 2] = 'gl'

laLanguages[19, 1] = 'German'
laLanguages[19, 2] = 'de'

laLanguages[20, 1] = 'Greek'
laLanguages[20, 2] = 'el'

laLanguages[21, 1] = 'Hebrew'
laLanguages[21, 2] = 'iw'

laLanguages[22, 1] = 'Hindi'
laLanguages[22, 2] = 'hi'

laLanguages[23, 1] = 'Hungarian'
laLanguages[23, 2] = 'hu'

laLanguages[24, 1] = 'Icelandic'
laLanguages[24, 2] = 'is'

laLanguages[25, 1] = 'Indonesian'
laLanguages[25, 2] = 'id'

laLanguages[26, 1] = 'Irish'
laLanguages[26, 2] = 'ga'

laLanguages[27, 1] = 'Italian'
laLanguages[27, 2] = 'it'

laLanguages[28, 1] = 'Japanese'
laLanguages[28, 2] = 'ja'

laLanguages[29, 1] = 'Korean'
laLanguages[29, 2] = 'ko'

laLanguages[30, 1] = 'Latvian'
laLanguages[30, 2] = 'lv'

laLanguages[31, 1] = 'Lithuanian'
laLanguages[31, 2] = 'lt'

laLanguages[32, 1] = 'Macedonian'
laLanguages[32, 2] = 'mk'

laLanguages[33, 1] = 'Malay'
laLanguages[33, 2] = 'ms'

laLanguages[34, 1] = 'Maltese'
laLanguages[34, 2] = 'mt'

laLanguages[35, 1] = 'Norwegian'
laLanguages[35, 2] = 'no'

laLanguages[36, 1] = 'Persian'
laLanguages[36, 2] = 'fa'

laLanguages[37, 1] = 'Polish'
laLanguages[37, 2] = 'pl'

laLanguages[38, 1] = 'Portuguese'
laLanguages[38, 2] = 'pt'

laLanguages[39, 1] = 'Romanian'
laLanguages[39, 2] = 'ro'

laLanguages[40, 1] = 'Russian'
laLanguages[40, 2] = 'ru'

laLanguages[41, 1] = 'Serbian'
laLanguages[41, 2] = 'sr'

laLanguages[42, 1] = 'Slovak'
laLanguages[42, 2] = 'sk'

laLanguages[43, 1] = 'Slovenian'
laLanguages[43, 2] = 'sl'

laLanguages[44, 1] = 'Spanish'
laLanguages[44, 2] = 'es'

laLanguages[45, 1] = 'Swahili'
laLanguages[45, 2] = 'sw'

laLanguages[46, 1] = 'Swedish'
laLanguages[46, 2] = 'sv'

laLanguages[47, 1] = 'Thai'
laLanguages[47, 2] = 'th'

laLanguages[48, 1] = 'Turkish'
laLanguages[48, 2] = 'tr'

laLanguages[49, 1] = 'Ukrainian'
laLanguages[49, 2] = 'uk'

laLanguages[50, 1] = 'Vietnamese'
laLanguages[50, 2] = 'vi'

laLanguages[51, 1] = 'Welsh'
laLanguages[51, 2] = 'cy'

laLanguages[52, 1] = 'Yiddish'
laLanguages[52, 2] = 'yi'

lnLanguage = ascan(laLanguages, tcLanguage, -1, -1, 1, 15)
if lnLanguage > 0
    lcLanguage = laLanguages[lnLanguage, 2]
else
    lcLanguage = ''
endif lnLanguage > 0
return lcLanguage

Monday, December 05, 2011

Southwest Fox 2012

Southwest Fox 2011 was a great success. Now we’re trying to figure out what form next year’s event will take. You can help us by answering a few questions about your preferences. Please take the survey at https://www.surveymonkey.com/s/VCCKNDG, so we can include your input.

Whether you attended Southwest Fox this year, or it’s been a few years, or you never attended Southwest Fox, your input will really help us figure out what direction to take next year and in the years to come.

Tuesday, November 08, 2011

Southwest Fox 2011, Day 4

I planned on attending a couple of sessions Saturday morning, but ended up having a long chat with Toni Feltman and Steve Bodnar about .Net development, unit testing, version control, and other stuff. Again, this is one of the special things about conferences in general—discussions with other developers to help get a fresh perspective on things, solve problems, and spark new ideas—but especially SWFox: since we require all speakers to provide detailed white papers for their sessions, you still get the full benefit of all sessions whether you attend them or not. That makes the networking opportunities at SWFox even more valuable.

The one session I couldn’t miss, of course, was my own: the repeat of my ActiveX controls session. This time, I took special care to not miss any steps in my demo, so the attendees were cheated out of the opportunity to see a speaker squirm and learn from their mistakes. Sorry, guys!

The closing session was short but sweet. One of the things we discussed was 2012.

image

As many people suspected, this was a tough, scary year for us. By mid-August, we were in full panic mode, as we were so far below breakeven that it looked like a five-figure loss for us. Fortunately, a larger-than-usual number of people signed up after September 1; that combined with an increase in sponsorship this year took us over the top. However, we’re really concerned about next year: if the trend of decreasing attendance continues, 2012 could be a disaster.

So, one of the questions on our conference evaluation form was what type of format would attendees like for a future event: the traditional SWFox format, a less expensive “Code Camp” style (volunteer speakers, no food provided or perhaps sponsored food), an even less expensive one-track format (all sessions in a single room), etc. The reason for asking this question is to try to find out what we can do to minimize our risk. If we get the same number of people registered for 2012 as 2011 but they all register in June or July, there’s no panic and we can have the same format we’ve always had. But if the attendance goes down or people defer registration until fall like happened this year, we have to make adjustments so we don’t face an enormous loss. We don’t know what the answer to this is yet, but the takeaway from the closing session is that we did make a commitment to host an event in 2012.

Of course, asking people who paid a premium price to attend a premium conference doesn’t necessarily mean you’ll get the same answers as you do from those who didn’t attend for whatever reason. So, in the next month or so, we’re going to ask the community what kind of conference they’d like us to put on. When it comes time, please respond to the survey so we can get an accurate feel for what folks want us to do.

One other thing I’d ask: if you love SWFox like we do, and want to see it continue, do us a favor and register early next year. That would take out the panic for us and allow us to plan for the type of conference we really want to host.

I really felt that this was the best SWFox ever. The mood was upbeat, the sessions were amazing, the food was great, the conference center was wonderful … It wasn’t just me: I heard from lots of other people that they felt the same way.

The closing session ended with us giving away thousands of dollars in prizes provided by our sponsors, including some goodies like T-shirts that we threw into the audience (Tamar wisely counseled us against throwing the pins!).

After cleaning up the session rooms, we had lunch with Sharon, our main contact at the hotel and conference center, to do a post-mortem on the conference. We discussed the few things that didn’t work (Internet access and temperature control in the rooms) and the great number of things that did (dedication and hard work of the staff, the food, and the other things that make this a great venue). We also discussed some potential dates for next year.

After that meeting, Rick, Therese, Tamar, Marshal and I had our own meeting to talk a little about 2012, then met a few others (Bo Burban and his son and Steve Bodnar) for dinner. We went to Kona Grill, where Rick and I had some of the best sushi I’ve ever had.

Then it was time for the main event: the long-standing tradition of indoor go-kart racing. About 20 of us showed up at Octane Racing (formerly F1 Race Factory) to compete for the title of fastest racer. With Rick Strahl not driving this year, first place was up for grabs. I thought I did really well, but both Paul Mrozowski and my former employee Rob Eisler (just kidding, Rob!) beat me. In the second race, I finished behind Bo but ahead of Paul. See the Geeks & Gurus Facebook page for photos.

We got back to the hotel by about 11:30, too jazzed up to call it a night yet. Besides, there was still two jumbo bottles of champagne to drink. Which we, of course, did, sitting around the pool. I finally called it a night at about 1:30.

Sunday was kind of anti-climatic, as usual: a nice breakfast with the Schummers and Granors followed by a long day of travel home. I slept almost the entire flight from Phoenix to Minneapolis, something I rarely do.

Thanks to everyone who came to SWFox and made it the best one ever. We’ll see you next year for sure!