Wednesday, March 29, 2006

How to get the active Team Foundation Server and Project from an Add-In

Ed Hintz posted "How to Write a Team Foundation Version Control Add-in for Visual Studio" which is a great guide to writing an add-in to integrate with Team Foundation version control. I won't repeat that info her. If you want to do something that is related to the active Team Server and/or Project from within a VS Add-In or VSIP package then read on.

First make sure you have a reference to Microsoft.VisualStudio.TeamFoundation - either follow Ed's steps to add them via the REG.EXE commands for convenience or load them using the Browse tab on the Add Reference dialog - in a default installation the assembly will be in "%programfiles%\Microsoft Visual Studio 8\Common7\IDE\PrivateAssemblies".

Next add a using clause:

using Microsoft.VisualStudio.TeamFoundation;

Create a member variable in your Connect class for the TeamFoundationServerExt object:

private TeamFoundationServerExt _tfsExt;

In your OnConnection method, initialize the _tfsExt object:

_tfsExt = _applicationObject.GetObject("Microsoft.VisualStudio.TeamFoundation.
TeamFoundationServerExt") as TeamFoundationServerExt;

The TeamFoundationServerExt object exposes two things:

  1. ActiveProjectContext - a property of type ProjectContextExt which contains the active server and project
  2. ProjectContextChanged - an event which is fired when the current project and/or server is changed

public sealed class ProjectContextExt
{
public string DomainName { get; }
public string DomainUri { get; }
public string ProjectName { get; }
public string ProjectUri { get; }
}

DomainName is the display name of the current server and DomainUri is the actual URI of the current server. And ProjectName is the display name of the current server and ProjectUri is the Team Project URI. Note that these values can be null if there is no active server or project.

Your ProjectContextChanged handler should look like this:

void _tfsExt_ProjectContextChanged(object sender, EventArgs e)
{
}

Within your handler you requery the ActiveProjectContext property of the TeamFoundationServerExt object.

Will Code For Food

This article originally appeared in the Wall Street Journal several years ago.  It's come up from time to time at work and most folks find it to be an amusing tale so I figured I'd post this before I lose track of it.



In these dark days of dot-com despair, Web developer Brian Yangas never thought a practical joke would yield three job leads in an hour.
 
Recently Patrick Husting, eHome Inc.'s chief technology officer and co-founder, told Mr. Yangas and 30 of his co-workers at the company's Bellevue, Wash., tech office that the online real-estate brokerage would shut all of its 11 regional sales offices and lay off the employees there.


Only the tech office would remain open while eHome tried to sell its technology. But the company might run out of money, so Mr. Husting suggested the employees be prepared and start looking for other work.


Mr. Yangas, a self-described "quirky guy," didn't fix up his resume; he fixed up a cardboard sign. It read "Web Developer...dot-com went bust...will code for food (or options)...hungry -- sober."


He arrived at work at 8:30 the next morning, bundled against the November chill in a hardy brown twill jacket and baseball cap. Sign in hand, he strolled out to the nearby highway-exit ramp and waited to make his co-workers laugh as they arrived. Mr. Yangas, 31, hoped his joke would "relieve the anxiety," he says. "You know, lighten the mood."


For the first 10 minutes, passing drivers essentially ignored the avid foosball player. Then some of the commuters, most of whom he didn't recognize, "started to notice and laugh, so I thought I'd stay a bit longer," he says.


A man driving "maybe a Toyota" shouted "Cogenix is hiring!" Mr. Yangas recalls. Cogenix is a database-application and Web site maker based in nearby Redmond, Wash. About five minutes later, Cami Cole, an associate with information-technology recruiter Maxim Group, stopped and asked "Are you really a Web developer?" Mr. Yangas nodded, and Ms. Cole handed him a business card with the parting words, "I can get you a job."


After about 45 minutes, Mr. Yangas went inside to warm up. He came back out at 10am because his boss wanted a picture of the prank. While Mr. Yangas was waiting for Mr. Husting to find a camera, "a long-haired guy in a blue truck" yelled, "Go to Microsoft campus building D. Ask for Tim."


"I wasn't sure what to think about it at first," Mr. Yangas remembers, "but later that day, no one was doing any work," so he headed down to the Microsoft campus. "I thought, it's been a weird day so far; let's see what fate brings."


He found the D building on Microsoft's Red West campus. It was three or four stories high and Mr. Yangas, who had caught only a fleeting glimpse of his contact, worried he wouldn't find the right Tim.


But he did. With the help of a new receptionist who didn't know a soul in the building but did have access to a photo database of employees, Mr. Yangas located what he thought might be the right extension.


The receptionist called: "There's this guy down here who said you yelled to him on the street." Tim Noonan, a development manager for Microsoft's MSN Explorer group, came down to meet Mr. Yangas, took him back to his office and talked to him for 15 to 20 minutes before passing him on to other members of Mr. Noonan's team.


"We're always looking for good developers, so why not" talk with Mr. Yangas, Mr. Noonan says. "And if he's as good a developer as he is at coming up with this scheme then woo-hoo! We value creativity."


Mr. Yangas "was pretty excited" about the outcome: Mr. Noonan called him back for a second, formal interview a few weeks later. And yet another rolled around a week later. Among other things, he was asked to write code on a dry-erase board and was interviewed during lunch to see if he could answer tough questions while eating. The interview lasted nine hours.


Thursday brought good news. A human-resources representative called to say the company would be making him a job offer early in the new year, once "the numbers guy comes back from vacation."


Meanwhile, eHome hasn't shut down. Since the pre-Thanksgiving meeting, Mr. Yangas's current employer has decided to reposition itself as a technology company. EHome's Web site is up and running, the company is referring leads to former agents and receiving referral fees.


So Mr. Yangas, who has two kids and is paying off a home mortgage and a minivan, says he hasn't "gone back out" with his sign. He was encouraged by the job leads, he says, but, in fact, "I was actually kind of hoping for money."

Tuesday, March 28, 2006

The best thing you can get for your iPod

The Bose SoundDock. Really. It is way cool. It is way expensive too but given the simplicity, elegance, and sound quality it is well worth it. Too many iPod docking units have subwoofers, separate speakers, lots of switches, look clunky, etc.

When I told my wife I was going to put a stereo in our living room that she had meticulously decorated she gave me a very skeptical look. Then I brought the SoundDock home and she liked it pretty much from the get go - "you mean I just plug my iPod in and that's it?" Yep. And it charges the iPod as well so I/she can just grab it and go. Since we got it we've had a lot of guests and pretty much everyone is amazed at the sound produced by that little gizmo. I'm sure we've inspired a few iPod and Bose purchases. The downside is that the SoundDock isn't very versatile - it just works with the iPod and that's it.

HippieSound Studios Construction Week 6

Last week was a slow week for progress. My contractor was called away on an emergency job for the better part of the week. But he did get the bulk of the grid ceiling up - I know this is a bear because the ceiling is filled with regular insulation, then mineral fiber insulation, then really heavy duty suspended ceiling wire with 4x the number of supports because this is a HEAVY ceiling being 5/8" thick drywall tiles with mass load vinyl overlayment.



This he helped me level the floor. I started to do it myself. This shit was supposed to be self leveling concrete. Um. Yeah. You level it yourSELF. Fuckers. I've done a bunch of construction work but have never used a trowel in my life. I didn't screw it up though because when he came over and helped me out we knocked it out pretty quick.

Flashing Dialogboxes

This example demonstrates how to flash a dialog in the taskbar after performing a long running operation from the command line.  A good example of how this is useful is in the VS Team Foundation version control toolset.  Naturally, it includes a command line utility (TF.EXE) to perform source control operations.  Sometimes these operations can be very lengthy and in the end result in a dialog box popping up - performing a lengthy GET operation that results in conflicts to resolve is a good example of when this would happen.


Our own UI framework does quite a bit more than this - this is just a scaled down example of what happens when a dialog is invoked from the command line.  Many of our dialogs end up getting displayed in Visual Studio, standalone Windows apps, Excel, the command line, etc.  So in our framework the method to show a modal dialog is actually virtual so the appropriate thing happens in each environment.


 using System;
 using System.Diagnostics;
 using System.Text;
 using System.Windows.Forms;
 using System.Runtime.InteropServices;
 using System.Threading;
  
 namespace ConsoleFlasher
 {
     class Program
     {
         static void Main(string[] args)
         {
             Thread.Sleep(5000); //  Lengthy operation...
             using (MyForm form = new MyForm())
             {
                 UIHost.ShowModalDialog(form);
             }
         }
     }
  
     static class UIHost
     {
         static public DialogResult ShowModalDialog(Form form)
         {
             return ShowModalDialog(form, null);
         }
  
         //********************************************************************************************
         /// <summary>
         /// Display a modal dialog a windows application.
         /// </summary>
         /// <param name="form">The form to display.</param>
         /// <param name="parent">The parent window for window stack ordering.</param>
         //********************************************************************************************
         static public DialogResult ShowModalDialog(Form form, IWin32Window parent)
         {
             bool isTopLevel = parent == null;
  
             form.ShowInTaskbar = isTopLevel;
             form.MinimizeBox = isTopLevel;
             form.ShowIcon = isTopLevel;
  
             if (isTopLevel)
             {
                 //  Attach an event handler so maybe we'll flash the title in the taskbar
                 form.Activated += new EventHandler(OnModalDialogFormActivate);
  
                 //  This is where you would also set your app icon on the dialog as well.
             }
        
             DialogResult result = form.ShowDialog(parent);
        
             if (isTopLevel)
             {
                 form.Activated -= new EventHandler(OnModalDialogFormActivate);
             }
        
             return result;
         }
  
         //********************************************************************************************
         /// <summary>
         /// On form activate for a modal dialog for the console.  We check to see if we are the
         /// foreground window and flash the taskbar if we aren't.
         /// </summary>
         /// <param name="sender">Form to activate</param>
         /// <param name="e">Standard event args</param>
         //********************************************************************************************
         static private void OnModalDialogFormActivate(object sender, EventArgs e)
         {
             Form form = sender as Form;
  
             if (form != null)
             {
                 Debug.Assert(form.Visible, "Form should already be visible for FormActivate");
  
                 IntPtr foregroundHwnd = NativeMethods.GetForegroundWindow();
  
                 //  First see if we're in the foreground
  
                 if (foregroundHwnd != form.Handle)
                 {
                     IntPtr consoleHwnd = NativeMethods.GetConsoleWindow();
  
                     //  We're not in the foreground so let's see if our console is.
  
                     if (foregroundHwnd != consoleHwnd)
                     {
                         //  We're not in the foreground and neither is our console window so let's
                         //  flash the user.
                         int flashCount = 1;
  
                         //  make sure we read the count from the system and respect this for accessibility
                         NativeMethods.SystemParametersInfo(NativeMethods.SPI_GETFOREGROUNDFLASHCOUNT, 0, ref flashCount, 0);
  
                         NativeMethods.FLASHWINFO fwi = new NativeMethods.FLASHWINFO();
  
                         fwi.cbSize = (UInt32)Marshal.SizeOf(typeof(NativeMethods.FLASHWINFO));
                         fwi.dwFlags = NativeMethods.FLASHW_ALL;
                         fwi.dwTimeout = 0;      //  This will get the system default which is the caret blink rate
                         fwi.uCount = (UInt32)flashCount;
                         fwi.hwnd = form.Handle;
  
                         //  blinky, blinky, blinky
                         NativeMethods.FlashWindowEx(ref fwi);
                     }
                     else
                     {
                         //  Our console has the input focus so let's be a bit more agressive...
                         NativeMethods.SetForegroundWindow(new HandleRef(form, form.Handle));
                     }
                 }
  
                 //  Don't call us again...
                 form.Activated -= new EventHandler(OnModalDialogFormActivate);
             }
             else
             {
                 Debug.Fail("Invalid sender in OnModalDialogFormLoad");
             }
         }
  
     }
  
     static class NativeMethods
     {
         public const int SPI_GETFOREGROUNDFLASHCOUNT = 0x2004;
  
         public const int FLASHW_STOP = 0;
         public const int FLASHW_CAPTION = 0x00000001;
         public const int FLASHW_TRAY = 0x00000002;
         public const int FLASHW_ALL = (FLASHW_CAPTION | FLASHW_TRAY);
         public const int FLASHW_TIMER = 0x00000004;
         public const int FLASHW_TIMERNOFG = 0x0000000C;
  
         [StructLayout(LayoutKind.Sequential)]
         internal struct FLASHWINFO
         {
             public UInt32 cbSize;
             public IntPtr hwnd;
             public UInt32 dwFlags;
             public UInt32 uCount;
             public UInt32 dwTimeout;
         }
  
         [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
         public extern static int FlashWindowEx(ref FLASHWINFO fwi);
  
         [DllImport("user32", CharSet = CharSet.Auto)]
         public static extern bool SystemParametersInfo(int nAction, int nParam, ref int value, int ignore);
  
         [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
         internal static extern IntPtr SetForegroundWindow(HandleRef hwnd);
  
         [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
         internal static extern IntPtr GetForegroundWindow();
  
         [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
         internal static extern IntPtr GetConsoleWindow(); 
     }
 }

Monday, March 27, 2006

Robin Trower is touring the US again!!!

I'm a huge Robin Trower fan but haven't seen him live in many years mostly because he hasn't toured the US in many years and when he was touring he didn't come near where I was living.  I was about to book a flight to England just to see him again so I went to check the tour schedule this weekend and saw that he's added US tour dates this year.  I am beyond excited.  I will try and catch at least 2 or 3 shows.

Overcoming a .NET ListView CheckBoxes quirk

In the Visual Studio Team Foundation version control UI, we display your pending changes in a ListView control.  That ListView has CheckeBoxes set to true as we allow you to perform a variety of operations on the checked items. 


At the same time we want you to be able to double click on an item in the list and have the file open in the editor. 


Unfortunately, the .NET ListView component automatically toggles the checked state of items when you double click on them.  I know that this is not the behavior of the underlying Win32 ListView control so it has to be something in the WinForms code.


At this point it's worth examing how this all works.  In a traditional C/C++ application, the ListView control sends WM_NOTIFY messages to the window that is the parent of the ListView.  This is typically a dialog box window.  In WinForms, events are exposed directly from the controls themselves.  So internally WinForms will take the WM_NOTIFY message and reflect it back to the child control and then the child control handles the message by firing events that you add your event handlers too.  This happens for other messages besides WM_NOTIFY - such as WM_COMMAND.


A few minutes with a program such as Spy++ will show you the message traffic.  When you double click a ListView item the underlying Win32 ListView sends a WM_NOTIFY message to the parent window (typically your Form).  The WinForms message handler for the parent window then reroutes the message back to the ListView by sending it a new message - WM_REFLECT + WM_NOTIFY.  The WinForms ListView message handler then dispatches it.  When the WinForms ListView sees a NM_DBLCLK notification it then sends a message (LVM_HITTEST) to the Win32 ListView control asking where the click occurred.  If it was on an item, the WinForms ListView code will then toggle the checked state of the item.


Since none of this behavior is exposed via the properties of the ListView control we'll have to work around it using less convenient means.  The solution I came up with for Team Foundation was to set a flag during the NM_DBLCLK notification that we're in the midst of a double click notification and then we intercept the LVM_HITTEST call and return that no item was found.

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Windows.Forms;
 
namespace ListViewCheckBoxes
{
    class MyListView : ListView
    {     
        private bool m_doubleClickDoesCheck = true//  maintain the default behavior
        private bool m_inDoubleClickCheckHack = false;
 
        //****************************************************************************************
        // This function helps us overcome the problem with the managed listview wrapper wanting
        // to turn double-clicks on checklist items into checkbox clicks.  We count on the fact
        // that the base handler for NM_DBLCLK will send a hit test request back at us right away.
        // So we set a special flag to return a bogus hit test result in that case.
        //****************************************************************************************
        private unsafe void OnWmReflectNotify(ref Message m)
        {
            if (!DoubleClickDoesCheck && CheckBoxes)
            {
                NativeMethods.NMHDR* nmhdr = (NativeMethods.NMHDR *)m.LParam;
 
                if (nmhdr->code == NativeMethods.NM_DBLCLK)
                {
                    m_inDoubleClickCheckHack = true;
                }
            }
        }
 
        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        protected override void WndProc(ref Message m)
        {
            switch (m.Msg)
            {
                //  This code is to hack around the fact that the managed listview
                //  wrapper translates double clicks into checks without giving the
                //  host to participate.
                //  See OnWmReflectNotify() for more details.
                case NativeMethods.WM_REFLECT + NativeMethods.WM_NOTIFY:
                    OnWmReflectNotify(ref m);
                    break;
 
                //  This code checks to see if we have entered our hack check for
                //  double clicking items in check lists.  During the NM_DBLCLK
                //  processing, the managed handler will send a hit test message
                //  to see which item to check.  Returning -1 will convince that
                //  code not to proceed.
                case NativeMethods.LVM_HITTEST:
                    if (m_inDoubleClickCheckHack)
                    {
                        m_inDoubleClickCheckHack = false;
                        m.Result = (System.IntPtr)(-1);
                        return;
                    }
                    break;
            }
 
            base.WndProc(ref m);
        }
 
        [Browsable(true),
         Description("When CheckBoxes is true, this controls whether or not double clicking will toggle the check."),
         Category("My Controls"),
         DefaultValue(true)]
        public bool DoubleClickDoesCheck
        {
            get
            {
                return m_doubleClickDoesCheck;
            }
 
            set
            {
                m_doubleClickDoesCheck = value;
            }
        }
    }
 
    //****************************************************************************************
    //  This is stuff you would normally put in a separate file with all the other interop
    //  you have to work with.
    //****************************************************************************************
    public class NativeMethods
    {
        public const int WM_USER = 0x0400;
        public const int WM_REFLECT = WM_USER + 0x1C00;
        public const int WM_NOTIFY = 0x004E;
        public const int LVM_HITTEST = (0x1000 + 18);
        public const int NM_DBLCLK = (-3);
 
        [StructLayout(LayoutKind.Sequential)]
        public struct NMHDR
        {
            public IntPtr hwndFrom;
            public UIntPtr idFrom;
            public int code;
        }
    }
 
}
 

Saturday, March 25, 2006

Carvin HF2 is DONE!

The Carvin HF2 "Holdsworth Fatboy" is finished and is supposed to ship Monday!

Friday, March 24, 2006

Give the .NET loader a hand - how to load assemblies from VS PrivateAssemblies

This program demonstrates how to use the AppDomain.AssemblyResolve event to help the .NET loader get assemblies loaded that it can't find.


The VS Team Foundation edition ships with a number of assemblies you may want to reference in your own apps.  Most of the important ones are installed in the GAC so you should have no problem loading them at runtime.  However, some of them are installed under the VS PrivateAssemblies directory (by default this is C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\PrivateAssemblies).  Your options are you can copy these DLLs around, put that directory in your path, put your tool in the same directory, or have .exe.config file for your app that tells the loader where to look.  Quite frankly, none of these options is particularly appealing since they all make distributing your tool more cumbersome.  So let's help the loader out!

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using Microsoft.Win32;
 
namespace TeamFoundationAssemblyLoadTest
{
    class Program
    {
        static void Main(string[] args)
        {
            AppDomain domain = AppDomain.CurrentDomain;
 
            domain.AssemblyResolve += new ResolveEventHandler(domain_AssemblyResolve);
 
            //  The following line triggers the AssemblyResolve event to fire because
            //  it uses a class in Microsoft.TeamFoundation.WorkItemTracking.Controls.dll
            //  which is not installed in the GAC or anywhere else convenient.
            ClassThatUsesTeamFoundation.SomeFunction();
        }
 
        static Assembly domain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            String[] assemblyParams = args.Name.Split(',');
           
            Debug.Assert(assemblyParams.Length > 0 && !String.IsNullOrEmpty(assemblyParams[0]), "Invalid assembly name arguments passed to domain_AssemblyResolve");
 
            //  Note that there are additional fields passed that indicate the
            //  version, public key token, etc.  For this demonstration, we           
            //  are just looking at the assembly name.

 
            String assemblyName = assemblyParams[0];
            Assembly loadedAssembly = null;
 
            switch (assemblyName)
            {
                case "Microsoft.TeamFoundation.WorkItemTracking.Controls":
                case "Microsoft.VisualStudio.TeamFoundation":
                    // etc.
                    loadedAssembly = LoadVSPrivateAssembly(assemblyName);
                    break;
 
                default:
                    Debug.Fail(assemblyName + " is not supported by TeamFoundationAssemblyLoadTest.domain_AssemblyResolve");
                    break;
            }
 
            return loadedAssembly;
        }
 
        //  This function will load the named assembly from the Visual Studio PrivateAssemblies
        //  directory.  This is where a number of Team Foundation assemblies are located that are
        //  not easily accessible to an app.  Fortunately, the .NET loader gives us a shot at
        //  finding them and we just happen to know where to look.
        static Assembly LoadVSPrivateAssembly(String assemblyName)
        {
            Assembly loadedAssembly = null;
 
            using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"Software\Microsoft\VisualStudio\8.0"))
            {
                if (key != null)
                {
                    Object obj = key.GetValue("InstallDir");
 
                    if ((obj != null) && (obj is String))
                    {
                        String vsInstallDir = obj as String;
                        String privateAssembliesDir = Path.Combine(vsInstallDir, "PrivateAssemblies");
                        String assemblyFile = Path.Combine(privateAssembliesDir, assemblyName + ".dll");
 
                        loadedAssembly = Assembly.LoadFile(assemblyFile);
                    }
                    else
                    {
                        Debug.Fail("VS 8.0 InstallDir value is missing or invalid");
                    }
                }
                else
                {
                    Debug.Fail("Could not open VS 8.0 registry key");
                }
            }
 
            return loadedAssembly;
        }
    }
}
 

Finally fixed my Blogger template

If anyone was actually paying attention, they'd see that my Links have varied and the styles haven't been right from time to time. Well, it's cuz I update my blog from a couple different machines and had outdated blog templates on each machine.

I think it's all fixed again. I also added some quick links to the musical journey posts.

Carvin HF2 now entering "String Setup and Quality Assurance"

The Carvin HF2 "Holdsworth Fatboy" has transitioned from the "Buffing and Fret Detailing" stage to "String Setup and Quality Assurance". Sooooo close!

Thursday, March 23, 2006

Wednesday, March 22, 2006

Musical Journey Part 6

Ok, it's been a while since I posted on this...

So we're about to the 11th grade I believe. This is when I started playing with Eric Leifert (guitar) and Steve Keister (drums). Steve's brother Mike played guitar very well too but he was a bit older and already in college.

This was another big turning point for me musically. Eric and Steve exposed me to Robin Trower, Black Sabbath, Iron Maiden, Judas Priest, etc. - basically more of the heavy and moody stuff. They were into Zeppelin and Rush as well. We hit it off pretty good. We had a great circle of friends too. Much partying :)

At some point in the 10th grade Phil Hurd and I started to form a band with Donald Davis on drums. I was playing bass. I don't even remember where the bass came from to be honest. We didn't last long. But I figured I ought to mention it.

Another reason I remember this time period is because it was around this time that I traded away my Antigua Telecaster for a piece of crap Ibanez Iceman. Yucko.

When I first heard Robin Trower's Bridge of Sighs album I about lost my mind. This music was AMAZING. Really amazing. James DeWar was a fantastic vocalist. And Trower! Wow! Trower played the most incredible stuff. He still does. When you first listen to him it seems simple but as you dig in his music is very rich with subtleties and complexities that are just brilliant. I've listened to Bridge of Sighs more than any other album ever. I've owned the LP, 2 cassettes, and 3 copies of it on CD!

So Mike D. played bass with us and I'm sure a few other guys did along the way as well. Who knows? We played stuff like The Ocean, Freewill, War Pigs, Iron Man, Livin' After Midnight, etc.

We didn't have a singer really. Bob Stanley would come over and sing once and a while. Bob was fun and really wanted to be a singer. Mostly he wanted to get stoned and cruise around Lake Barcroft.

We usually practiced at Steve's house early on. His parents didn't seem to mind the racket and it was handy for Eric since they were next door neighbors.