2008-09-15

Best Buy Store 694 Customer Service

I've had to deal with the customer service desk at Best Buy Store #694 in SE Aurora, CO on four occasions since it opened (about 3 years ago). I have to say, I'm very happy with them.

The first time was when I had to exchange an Xbox 360 that was giving me the infamous "red ring of death". Since this was before Microsoft had owned up to their hardware failures and started fixing problems after 3 months, I decided to invoke the Best Buy product replacement plan. Took the box to the desk, they brought me a new one. I asked if I could keep my old hard drive, and they said it would be no problem. (It was a common request by this point.) They helped me unpack the new console and swap hard drives. Aside from time spent in line, I was in and out in probably 10 minutes.

The second time, I had ordered a game from BestBuy.com, and when it was finally delivered (UPS mis-routed it first, and then severe snowstorms prevented its final delivery another week), it ended up being the wrong game. I brought it to the customer service desk, and the gal working there explained that Best Buy stores and BestBuy.com were separate entities, so they were limited to what they could do. They checked to see if they had the game I wanted in stock, but they didn't (which was why I ordered online in the first place), so she said I'd have to deal with BestBuy.com's customer support. She then picked up the phone and called BestBuy.com's customer support, explained the situation, and turned the phone over to me so I could finish the details. She could've very easily just told me to go home and call them, but the fact that she took an interest in getting me in touch with whom I needed to talk to, to get my issue resolved, really went a long way to winning my respect.

The third issue was when we had just picked up a business points membership or some such promotion. We bought a couple items, and we were supposed to get a certain amount discounted. The register refused to give us the appropriate discount, and the cashier wasn't able to do much about it. (Not surprising. I've been a cashier, albeit in a grocery store, and for better or worse, you're given very little control.) She directed us to customer service. We took our receipt over, and the gal there worked with the register for a bit. Then fought with it. She wasn't able to get it to give us the precise amount of credit back on our credit card, but she figured out a way to coerce the register to give us a discount that resulted in a slightly higher amount (like a buck and a half), so she went with that and called it good. All the while, she had a very positive attitude about helping, even when the system was obviously frustrating.

And then comes the most recent experience. I figured this one might be the most... interesting. Once again, I had an Xbox 360 that needed replacing. Because the hard drive sizes had been increased, I figured this time, I didn't want to just keep my old hard drive; I'd want to transfer my data from the old drive to the new. I'd done the research to see what it would take and invoked the GeezerGamers.com network to obtain a hard drive transfer kit that Microsoft provides for this purpose. I entered the store, armed with the transfer kit and some fresh Krispy Kreme donuts for bribery.

Once I had selected my replacement system (a 360 Elite, which, since the Best Buy replacement plan is based on original purchase price and the console prices had dropped quite a bit, only cost me the price of a new replacement plan if I wanted it -- "You better believe it" was my reply to that), I explained what I wanted to do. I explained that I had the transfer kit with me, and donuts. She politely declined the donuts (saying that, oddly enough, I was the third person to offer her donuts that day) and asked what I would need. Just a TV, and I made a point of saying it would probably take an hour. (I figured it was only fair that she knew exactly what I was asking.) As it so happened, the Geek Squad desk around the corner had a TV monitor that they rarely use, and they were happy to let me use it. They couldn't assist with the actual transfer (a policy that comes from wanting to avoid getting into legal tangles transferring songs between MP3 players), which I understood completely. About an hour later, I handed her the old hard drive, thanked her again, again offered her donuts (which she again politely declined), and walked out with my shiny new Elite.

So I just have to give "mad props" to this store's customer service team. They've really helped me out. I'm not saying I'd expect them to break rules for me, though — before helping me, the gal (who is likely a customer service manager, which is probably why she was able to get me set up on a TV for an hour) had to be called over to explain to someone else that they can't price-match another store's bundle deal. But as far as helping me out with issues, they've been nice, friendly, willing to help, and willing to do what it takes to resolve whatever problem I've had.

2008-09-04

I may be slow, but I'm FIRST!

I've been meaning to add this to my driving posts for a while. I just read an interesting article on Ars Technica, titled Selfish driving causes everyone to pay the Price of Anarchy, which is a pretty interesting discussion about an upcoming research article on traffic patterns and how, when each person is driving according to their own personal best interests, the entire group (including themselves) suffer.

It doesn't really have anything to do with this particular post, but it did remind me of one behavior I've observed and thought worth mentioning.

Many times, I've approached a stop light on a multi-lane road, where my lane is clear, but adjacent lanes have multiple people waiting at the light. Since I tend to accelerate fairly quickly "off the block", I like seeing this, because it means I'll get to reach cruising speed much faster. (One theory is, for better gas mileage, one should accelerate more slowly; however, in a hybrid car such as I drive, the theory is reversed, as faster acceleration is supposed to induce more assistance from the electric motor and actually decrease gas consumption.)

However, quite often, as I'm approaching the stop light, someone from an adjacent lane will pull out in front of me and take the "pole position". (If I'm lucky, it'll happen far enough in advance that I won't have to slam on my brakes to avoid a collision.) There are two possible, logical reasons for this behavior: (1) this person likes to accelerate quickly, and therefore wants to be where no one is in front of him, or (2) they need to make a turn soon and are changing lanes while they have a chance. However, way, way too often, this person will end up accelerating more slowly than the person they just got out from behind, and keep going straight for quite some time (i.e. several miles), even moving back into the lane they just left.

So what was the point of changing lanes? A better view of cross traffic before the light changed? A desire to witness the changing of the light for one's self? Mistrust that the car in front of them would actually be able to go once the light changed? Or that irrational fear of someone passing them, that same one that causes people in the right lane on the interstate to suddenly accelerate as I approach alongside in the left lane (note that I habitually drive with cruise control, so I'm reasonably certain my speed is constant)?

Or maybe this pathological desire to be "FIRST!" extends beyond internet comment boards?

2008-08-30

Bandwidth cap - essential or anti-competitive?

Comcast has recently announced that, in order to preserve their network service, they are going to implement a bandwidth cap of 250GB per month. This, they say, won't affect but a single percent of their users. The first time you go over this limit in a given month, you get a warning. The second time, your service is cut.

So how necessary is this? If only a percent of their customers are using that much bandwidth, and if the vast majority of users are way under this limit (which they loudly claim repeatedly, in an attempt to allay most people's fears that they might be among those cut), how clogged can their series of tubes really be?

If that's not the real reason, what could it be? They aren't currently offering a way to get a bigger limit, even for more money (although if they were to do so, we could simply lump them in with cell phone providers who keep raising rates for text messaging packages that include fewer messages). If the network is not truly clogged, what could be the motivation?

A telling clue is in their response about "Comcast Digital Voice". This is their Voice-Over-IP telephone service. We subscribe to this because it is an alternative to Qwest, and we believe it is the lesser of two evils. (Actually the service was somewhat different, up until just a few months ago when everything was switched over to VOIP. Now a single cable modem provides our internet and phone lines.) Naturally, if internet usage is going to be capped, people were understandably concerned about their phone service, which is coming over the internet, using up valuable bytes. Their answer? "Comcast Digital Voice is a completely separate service and is not a factor." (source: CNET Q&A with Comcast)

Oh, ho. So, Comcast is able to route its VOIP traffic specially, whether on a slightly different signal or just somehow flagged. So, what about other VOIP services, like Vonage? Well, they are just using standard internet protocols, so naturally, they will be subject to the cap.

What about movies? Comcast has also said that movies & TV, including streaming on demand, will not be affected. Whether this is because it's not using IP or because its traffic is also "specially-marked", I'm not sure, but it's not particularly relevant. The fact is, to stream a Comcast movie with your cable box, you don't have to think about bandwidth restrictions or any crap like that. But if you want to stream videos over the internet using Netflix or even YouTube (let alone bittorrent or other ways of downloading full movies), well, the meter is running, buddy.

This fall, the Xbox 360 will be receiving a dashboard update that will enable streaming-on-demand via a Netflix account. I was very close to convincing myself that it might be time to subscribe to Netflix. Now, that draw is gone. How can I want to stream movies to my 360 when I know that it'll possibly end up threatening to cut off my internet?

250GB may be more than enough for casual use of even third-party videos and VOIP. But it's hard to say for sure. Comcast has no intentions of making a meter available so you can see where you are on the bandwidth use for the month, and that's just the way they like it. Because of the fear of a bandwidth cap, even if it is set high, people are going to fear going to their competition for movies and VOIP.

The way my network is set up, everything runs through one Linux box before hitting the internet. Fortunately, I was able to set up the extremely easy to use program vnstat, which monitors network throughput on the interface of your choice. I intend to monitor my usage for September, trying not to vary my usage from typical habits, to get some indication of how much I'm actually using. Only then will I really know if there is a problem.

I'm sure other people won't be so lucky.

2008-08-26

The Smoke Detector Paradox

There are 24 hours in a day. The number of minutes in the day is equally divided among the 24 hours. However, despite this equal division of time, the battery in any given smoke detector will fail to the point where said detector begins emitting its warning chirp only during the nighttime hours, with a higher probability of occurrence during the "wee hours" between 2am and 4:30am — late enough to be deep asleep, too late to get back to sleep for any refreshing length of time, yet way too early to even consider just getting up for the day.

It happened again this weekend. At 4am Sunday morning, I hear my wife scolding my dog for climbing up on the comfy chair in the bedroom. What got him all riled up?

*chirp*

Oh, the smoke alarm. I get up and find the stepstool while my wife goes to the basement to retrieve a 9V battery. Replace the battery, put the smoke detector back up.

*chirp*

Wrong one. It wasn't the one that was installed with the house, but the one installed with the alarm system. The two are side-by-side, so it was an easy mistake — although if I had been more awake, I would've remembered that the alarm system had in fact been warning me that the fire alarm was reporting a low battery for a couple days. Oops.

Climb back up, take that smoke detector down. But it doesn't take a 9V battery; it takes two camera-style "123" batteries — not the kind you'd have sitting on-hand for an emergency change in the middle of the night.

So, I remove the batteries to at least silence the chirping — which immediately sets off the house alarm. My wife runs downstairs to deactivate the panel before the siren starts. Moments later (fortunately, before we are able to get back to sleep), we get a call from the alarm company to confirm the situation.

It took me 20 minutes to get back to sleep. I was lucky. My wife was up for over an hour.

I've complained about this paradox before, and the response I usually get is unsympathetic. "Well, you're supposed to change the batteries in all your smoke detectors twice a year anyway."

Except that would be overkill. Our smoke detectors don't run on batteries; they're connected to the house wiring, and the battery is used as a backup. As a result, the batteries often last a couple years or more. This one is a different case, as it was installed later and does run on batteries, but not your typical 9V, die-in-half-a-year variety. And at $10 a battery (and this thing takes two of them), you can bet I want to use them as long as they last rather than replace them on a premature schedule.

If I ever make a smoke detector, I will ensure that it obeys the following rules for when the battery is low:

  • Do not begin chirping if it is dark.
  • Wait until there has been light for at least two hours before chirping. (Sun comes up a lot earlier than I do in the summer.)
  • Once chirping has started, do not stop until battery is changed. (Annoying, but I'll concede for safety reasons not allowing someone to just deal with it during the day until they can sleep at night; what if they were out all day?)
  • If there has been no light for 10-12 hours, begin chirping. (In case it's not in range of visible light. Yes, there's a chance this could still go off at night.)

At most, this would delay the warning for half a day, which, considering for how long I've heard these things chirp, isn't that long. A little extra cost for the light sensor. Battery drain shouldn't be too bad, as it only has to sample light every half hour or hour or so, and only when it's about to reach low battery stage, and then only for the first half day at most. (And if, like mine, it's on house wiring anyway, that point's fairly moot.)

Perhaps I should patent this idea...

2008-08-20

Not in MY lane!

Driving the kids to school yesterday, we were in the middle lane of a three-lane road (in our direction), behind what looked like one of those rented moving vans, except it lacked any markings whatsoever. Anyway, that was the approximate size and shape of this vehicle. Traffic was a little heavy, as the right lane was blocked due to some road work (morning rush hour being the ideal time to tear up a piece of road, naturally). Up ahead was an intersection with a double left turn. Since traffic in the left lane splits up into people going straight and two lanes of people turning left, the left lane has a tendency to empty a little more quickly. And, since the right lane was blocked off and people needed to merge, the middle and right lanes were moving much more slowly.

I'm usually one who just stays in his lane, because if I try to switch to a faster lane, invariably I end up reliving the opening scene to Office Space, where as soon as I switch lanes, I come to a complete stop as the lane I just left speeds up. And on the rare occasions when I do seem to gain ground, I notice that a car I passed up manages to catch up to me two lights later anyway. So why stress, right?

In this case, this truck in front of me decided he wanted to merge left. Possibly because that lane was moving faster, possibly just to make room for the people in the right lane to merge. Either way, he waited until the left lane traffic lightened a bit. As soon as there was an opening, he'd turn on his signal.

The instant that signal came on, the people in that left lane would hit the accelerator and close the gap, making sure they got across that two or three car lengths of empty space before that truck dared to get in front of them, and then slam on their brakes to narrowly avoid hitting the stopped car in front — but at least they prevented the unthinkable, someone merging into their lane.

I watched this happen two or three times. Different people each time, because it would actually put them in front of that truck, so the next time it was someone else coming from behind to claim the empty space as theirs.

And what did they gain? Despite being two or three cars ahead, traffic as a whole was still moving rather slowly. Whether you are behind a big truck or not, you're still not moving more than 5mph at best, and it's still three light cycles before you get through the intersection.

2008-08-17

IM Convenience, USPS Prices

My cell phone carrier, T-Mobile, just raised their text messaging rates yet again. Once upon a time, when I first had their service, it was 10¢ per message sent or received, but the first 15 incoming messages were free. An unlimited plan was available for $5/month, but I would've needed to have 50 messages for that to be worth it. At some point, when I wasn't paying attention, they dropped the "first 15 incoming free", which thoroughly annoyed me, but again, I still was way below the threshold where the unlimited package would be worth it. They also raised their rates to 15¢ per message (again, sent or received), but my text volume still fell below the threshold to make any package deal worth it.

One charming feature that T-Mobile provides is the ability to send text messages to any phone by using an email address. Send an email to mobilenumber@sometmobiledomain.com, and presto, they get a text message. Well, of course, spammers eventually caught on to this, and we started getting spam. Since T-Mobile charges per message sent or received, that meant we were paying for spam. Words cannot express how angry this concept makes me. Fortunately, T-Mobile's web site provides a very easy way to turn off the option to receive text messages by email. Once I found it (which of course wasn't exactly straightforward, although I don't know it's fair to say it was "buried", either), the spam stopped.

Just this month, though, T-Mobile raised their rates yet again. Now, it's 20¢ to send or receive every text message. Since they charge on both sides of this equation, that means for every text message one T-Mobile customer sends to another, they rake in 40¢. For an instant message. Nearly the same price as a first-class US Postage stamp (currently 42¢).

Oh, and to make matters worse, their $5 plan is now no longer unlimited. I think it is something on the order of 500 messages, and unlimited plans are $10 or $15 per month.

The part that bugs me the most about this is how they charge to receive messages. If someone calls me, I can see who is calling (or maybe Caller ID is blocked and I can't, but that's not the point), and I can choose to accept the call or not, thereby choosing whether or not I am going to use air time to take the call. But if someone sends me a text message, I have no choice. That text message is automatically delivered and accepted, and I am charged, whether I wanted it or not. It means my cell phone bill can be (and is) charged based on actions completely out of my control.

T-Mobile is not alone in this practice. That doesn't absolve them of the fact that they chose to engage in this rip-off.

I imagine some day it might be convenient to just have a portable internet device. Something that just has a constant internet connection, on which I could run an instant messenger client with voice capabilities. (Perhaps like Skype, which can call phones as well with a purchased plan, but even MSN Messenger can do voice chat, which might suffice.) That way, I could have my data pipe, and I could choose whatever services I wanted at whatever cost I decided best fit my needs. (For instance, just having MSN Messenger would mean someone couldn't call me from a land line; could I live with that?) That way I wouldn't be locked into a cell phone provider who tries to find new ways to make me pay for things, such as charging me for services I can't refuse.

2008-08-06

If you don't like my walking, stay off the crosswalk

Idiot driver of the day.

I was out walking today, as I'm trying to get into the habit so I get at least some exercise during the day. I come to a busy intersection, press the button for the crosswalk, and wait. The cross traffic gets a red light, and the left turn lights for the traffic running parallel to me light. The crosswalk light is of course still red, so I still wait. The left turn lights cycle off, and the straight lights go green, and the crosswalk also turns green. I start walking.

Because I'm on the left side of the intersection, the first half of my walk is in front of stopped cross traffic. I get past the halfway point, and this old white pickup truck has come from behind me to make a left turn. Note that I am still in the crosswalk, whose light had turned green (although started blinking red while I was crossing).

He pulls out and starts to make his turn, fortunately stopping just before he hits me. The guy leans out his window and starts yelling something at me. Since I'm listening to an audiobook, I don't know what he says, but I refuse to so much as acknoledge his presence. He's turning left, he must yield to oncoming traffic and most certainly pedestrians in the crosswalk.

Apparently my failure to beg his forgiveness for my existence made him mad, because then he laid on his horn until I had cleared out of his way. I didn't look at him or even alter my pace, but I was sorely tempted to stop and stand still.

2008-07-20

Guido is coming to break your kneecaps and offer a credit card

Debt collectors are either getting really lazy, or someone is using my address or phone number in a very weak attempt at identity theft — weak, I claim, because although we've been getting random calls from debt collectors for a matching last name (but a first name not even close to anyone in this household), my credit report is still clean.

Anyway, I just got an interesting letter in the mail from a debt collector, another matching last name but a random first name on a debt that of course I never incurred. (Seriously, how hard would it be to look up in a phone book and see if a name and address matched?)

Dear <random first name> <my last name>:

company name has been engaged to pursue collections on your above referenced account.

company name is pleased to provide you with an opportunity to satisfy this debt and allow you to qualify to receive a new Emblem® MasterCard® credit card.

The letter goes on to explain how this phantom member of my family can conveniently have his debt charged to this brand new credit card. The borrower must make payments that equate to 36% of the original debt amount within the first year before the credit card becomes active, at least, and there's no interest on that original debt amount; but apparently, defaulting on a debt is now a criterion for obtaining the means to incurring more debt. Genius.

Is it any wonder this country is in a crisis of sorts with people incurring vast amounts of debt, when companies are all-too-willing to offer newer, shinier shovels with which people can dig their own pits deeper?

2008-07-09

The passing paradox

I could probably fill a whole blog by itself with annoyances in relation to being on the road.

As I was driving across some of the western states with my family, I experienced a phenomenon that one would think would be statistically unlikely, yet somehow happens all too often. I can be on the interstate, cruise control locked at the speed limit in the right-hand lane for dozens of miles without encountering a single other car. And yet, it's just as I start to come up on a truck or RV that I need to pass, that suddenly, another car going slightly faster will be passing me on the left, timing it just right that he's right beside me as I'm right behind the truck.

It's almost as if somehow it was orchestrated, that the vehicles were positioned and timed just so, such that all three of us would converge on this single, seemingly random point on the interstate in the middle of nowhere.

Somewhat related, it seems that no matter how many unoccupied miles I pass through, if there is a "construction area" where one lane is blocked off for a few miles (which is another rant; why do they block off 10 miles of road when they are only working on 50 feet at a time?), I will invariably have to go through it stuck behind someone who feels the need to go 30MPH under the posted speed limit.

And somehow, this improbable convergence seems to occur consistently at the very start of the lane closure.

It's almost like I'm stuck inside a video game, the way these events that I would think statistically unlikely to occur as frequently as they do, happen with almost scripted regularity. Except that I can't drop the pedal down and reach unreasonable speeds, crash and respawn, and outrun cops until an indicator on my heads-up display disappears.

Although that would stand a greater chance of keeping me awake driving through the Nevada desert.

2008-07-01

Fresh or frozen?

I have a corn snake. She's about 12 years old now, something over six feet long. When she was little, I used to feed her on frozen "pinkies", baby mice that had not yet grown fur, pre-frozen for convenience. I tried to keep a small supply in the freezer, so that I wouldn't have to rush out and get some for feeding time, and have to figure out when the stores got their supplies in. I would thaw them by microwaving a cup of water, then taking a frozen pinkie in a plastic bag and submerging it until it thawed. She'd eat them right out of my hand.

When I moved to Colorado (smuggling my tiny serpent on board the airplane in a folded handkerchief, a move that I don't doubt would probably get me arrested post-9/11 — and yes, I suppose one could accurately say there was a bleepy-bleepin' snake on that bleepy-bleepin' plane), I was unable to find a store that carried frozen pinkies. In fact, I had a hard time finding pinkies at all, live or dead. But when I did finally track some down, they were live; and, for the next 11 years, my snake ate live mice. (Except for one time when the mouse died on the way home; but that's still a far cry from frozen.)

Tonight, though, I had a problem. I needed to feed my snake, and the local Petco was not going to get any live mice for a while. But, to my surprise, they did have frozen mice in stock. Individually wrapped and costing a little less than live mice, I was able to bring four home. The clerk suggested I try one, and if she didn't eat it, then I could always bring the remaining three back, still frozen, as everything in the store is guaranteed for 30 days.

I thawed out the first one in a sink of warm water. Using a pair of metal tongs, I picked it up by the tail, opened the feeding tank — and the mouse promptly slid out of the tongs and dropped on the floor. My snake started to climb out of the tank, but I managed to prod her back in. She saw the mouse on the floor, sniffed it, and then proceeded to eat. So dedicated to this task was she, that I was able to push the rest of her body back into the tank and close the lid.

The mouse still felt like it had a cold spot on it, so I heated some water on the stove and dumped that in the sink to thaw the remaining three mice. After some time, when I opened the next mouse package, a wave of warm mouse scent hit me. And I heard a *whump* from the feeding tank. I guess she smelled it, too. The remaining three mice, she ate without a problem. I even dropped the last one, and she found it quickly and started eating.

Part of the reason I was so desperate for mice that I tried frozen, was because she was overdue for her feeding. That may be a contributing factor for her eating anything she could find that smelled like a mouse, alive or dead. But I hope this means she's open to the idea on a regular basis, because it'll be so much more convenient to keep a supply of mice in the freezer for her.

2008-06-16

My M4A2MP3 script

I have a Sandisk Sansa MP3 player that I picked up on Woot.com for a song. It's a refurbished unit that has a couple little quirks, but for the most part, I love it. My primary praise for the thing is how easy it is to transfer files to. It identifies itself as a simple mass storage device, so all I have to do is drag and drop my music files to it. And I'm done. Contrast this with the iPod my mother got for Christmas, who after hours of frustration had to call her sister's son-in-law over for help in installing the iTunes software (which has been known to install other software behind your back; but we won't get into that here). Yes, the Sansa can be used in an alternate mode that does an auto-sync thing with Windows Media Player, but I chose not to go with that more confusing route. The Sansa does have a converter that you must use in order to do video, but when it comes down to it, I just don't find video on a 2" screen worth the hassle anyway.

One thing I do wish it had was a bookmarking function, as I like to listen to audiobooks; and if you lose your place in a 40-hour audio file, trying to find it again is frustrating. But since I've taken to using Goldwave to splitting my audiobooks into 1-hour files before transfer (and since playlists are in a standard, text-based format and very easy to create to "bind" the parts together), it's much less of an issue.

Anyway, the purpose of this post isn't so much to praise the Sansa, but to describe one workaround for a common trend. I've noticed a start of a shift from MP3s to M4As in audio, especially in podcasts. Xbox Live's Major Nelson did it for one episode (although he went back to MP3s afterward), and only the first GeezerGamers.com podcast was available in MP3. The Sansa, unfortunately, does not natively support M4A, just MP3 and WMA. Since I'm not yet ready to install Rockbox on it, I had to find another way.

I did some searching on converting M4A to MP3, and I found a pretty simple script here that does the conversion. It does, however, presuppose install paths for your programs (it launches two command-line utilities, FAAD and LAME, to convert to WAV and MP3, respectively). I decided I wanted to put it in a folder on my Sansa (since it's just a USB storage device, it can hold anything) and make it available no matter how it was assigned when it got plugged into a machine.

I have a folder, called M4A2MP3, that contains faad.exe, lame.exe, lame_enc.dll, and m4a2mp3.bat. The contents of the batch file are as follows:

echo off
REM Simple script to convert m4a to mp3, got it from http://pieter.wigleven.com/it/archives/3
REM With edits by Yakko Warner, http://yakkowarner.blogspot.com/2008/06/my-m4a2mp3-script.html
if /I "%~x1" NEQ ".m4a" (
    echo Warning, file doesn't look like an m4a: %~nx1
    pause
)
cls
echo.
echo Converting %1 to MP3
"%~dp0\faad.exe" -o "%TEMP%\%~nx1.wav" %1
"%~dp0\lame.exe" --preset standard "%TEMP%\%~nx1.wav" "%~dpn1.mp3"
del "%TEMP%\%~nx1.wav"

Being a simple script, it does require you have enough space wherever %TEMP% lives, but it does have several advantages over the original script. It uses %TEMP% as the location of the .wav file, which is typically faster, local storage; it replaces the .m4a extension with .mp3 instead of appending it; it allows the executables and script to travel together in a single folder; and no paths are hard-coded. It also, if you give it a file that doesn't have an .m4a extension, alerts you of this fact and presents you an opportunity to ^C and cancel the script.

To execute, it's as easy as dragging an .m4a file onto the batch file and watching it work. Note that it is possible to install this to your hard drive and add to Explorer's right-click menu for M4A files to run this script. I've set that up once, but considering how infrequently I have to convert M4As in general (usually just once a week), it hasn't quite been worth doing that on a regular basis.

Cleaning Temp on logoff

I've noticed that the Temp directory in Windows tends to get a little full, and it doesn't clean itself out. I find this rather odd, as it can eat up a lot of space. While I can't prove that it degrades performance or anything like that, it is a little housekeeping that keeps the more obsessive/compulsive side of myself happy.

There are a couple problems, though. One is just remembering to do it. The other is coming up with a convenient way to do it. If you've ever tried to delete these files by hand, you'll notice that Explorer will spend a lot of unnecessary time enumerating the files it's about to delete, and the first file that's actually in use will cause the entire operation to fail. The easiest way to accomplish the goals and get around the problem of locked files is to use a logoff script that is coded to skip or ignore those files.

Once upon a time, I wrote a VBScript file that would systematically delete each file, one by one. With the dreaded On Error Resume Next statement, I could skip the files that were locked without crashing the script. The added bonus was that I could write code very easily to skip certain files. There is an XP PowerToy that manages 4 virtual desktops, each with its own wallpaper; but it stores the wallpaper for each desktop in Temp, and expects that file to be there even across reboots. The huge drawback to this, of course, is that deleting files one at a time is extremely slow.

I've since decided that I don't use the virtual desktops, so there's no sense in installing that app. And, what I was doing in VBScript could be done with two lines of shell script that runs much faster. Since I always have to look up the reference for batch files and variables and what-not every time I try to create this file, I decided it was time to put it in "extended memory" (i.e. a blog post, where I could find it later).

del %TEMP%\*.* /s /f /q
for /d %%x in (%TEMP%\*) DO rmdir /s /q "%%x"

The first line deletes all files in all subdirectories at and below %TEMP%, without prompting. The second line then iterates all the directories and attempts to remove them, again without prompting.

Getting this to run on logoff isn't something that I'll forget, but for others' benefit, here you go. Note that this is only in Windows XP and 2000; I have not yet figured out the Vista equivalent.

  • Run the Group Policy Management Console (Start, Run, "gpedit.msc")
  • Drill down to "User Configuration\Windows Settings\Scripts (Logon/Logoff)"
  • Double-click Logoff
  • Click "Show Files" to open an Explorer window to where the files go, and copy the batch file there
  • Click "Add" and add the batch file to the script list

I have not yet had an issue with this, but it's always possible something could go wrong, so use at your own risk. :P

2008-06-03

DataGridView style inheritance

Interesting "feature" in .Net Windows Forms 2.0 DataGridView. We have a DGV that is initialized in code. At the top of the initialization code is this statement:

dgv.RowsDefaultCellStyle.ForeColor = Color.Black;
dgv.DefaultCellStyle.SelectionBackColor = Color.MediumBlue;
dgv.DefaultCellStyle.SelectionForeColor = Color.White;

Then, columns are created and added:

DataGridViewTextBoxColumn c = new DataGridViewTextBoxColumn();
c.Name = "Foo";
...
c.DefaultCellStyle.Format = "0.00";
c.DefaultCellStyle.ForeColor = Color.Red;
c.DefaultCellStyle.SelectionForeColor = Color.Red;
dgv.Columns.Add(c);

Guess what? The text is not red. Unless it's selected, then it is red. In order to force the text to turn red, I have to do this, after the DataSource is set:

foreach (DataGridViewRow r in dgv.Rows) {
   r.Cells["Foo"].Style.ForeColor = Color.Red;
}

Apparently, the column's DefaultCellStyle is ignored. Actually, it's overridden by the grid's RowsDefaultCellStyle, which is exactly backwards of what I would expect (RowsDefaultCellStyle and AlternatingRowsDefaultCellStyle should be higher up the override hierarchy, because they're more generic, describing an unbound range of rows, than a specific column's DefaultCellStyle).

What's even more frustrating, is that sorting the grid causes the individual styles to be conveniently forgotten. So, I have to add that same loop (or, to be more proper, move the loop to a function, call it from the DataBind event, and add a call...) to the DataGridView's Sorted event.

This was one of those problems that, once I figured out what was going on, only then was I able to google and find the articles that described it. And apparently it's not new, just news to me. I still find it backwards, requiring a whole lot of extra work to make it behave the right way; and thus I reserve the right to complain.

2008-05-29

Missing SQL Feature: Select Top By Group

There's a common problem in SQL, where you want to take records in one table, and join each of them to a matching record in some other table that has the maximum value in some column, but you need data from several columns in that second table.

Let's see if I can come up with an example. Say you have a table of Sales Reps, and you have a table of the Sales they've closed, including the dates and amounts of sales. You want to write a query that shows the Reps' names, and the amount of their largest sale. Easy:

Select r.Name, Max(s.Amount)
From Rep r Inner Join Sale s On r.Id = s.RepId
Group By r.Name

But what if you want to include the date of the sale? Well, now it gets much more complicated. One common solution is to use a subquery that just gets two columns:

Select RepId, Max(Amount) MaxAmount
From Sale
Group By RepId

...and then join against that, and join the Sales table to that on the Amount. Where it falls down, though, is when the maximum is duplicated. What happens, for instance, if a certain rep happened to close two million-dollar deals as his top sellers?

You have to create an additional subquery that finds the maximum Id that matches each RepId and Amount, join on that, and then join on the Sales table to get the rest of the record (the Date field, in this example). The mess looks like this:

Select r.Name, s.Date, s.Amount
From Rep r
Inner Join (
   Select RepId, Max(Amount) MaxAmount
   From Sale
   Group By RepId
) MaxSales On r.Id = MaxSales.RepId
Inner Join (
   Select Max(Id) MaxId, RepId, Amount
   From Sale
   Group By RepId, Amount 
) MaxIds On MaxSales.RepId = MaxIds.RepId And MaxSales.MaxAmount = MaxIds.Amount
Inner Join Sale s On MaxIds.MaxId = s.Id

Wouldn't it be far more intuitive and easier if you could do something like this:

Select r.Name, s.Date, s.Amount
From Rep r
Inner Join Top 1 Sale s Order By s.Amount On r.Id = s.RepId

I found this suggestion on a blog by Raymond Lewallen here. It does leave a bit of assumptions on the SQL compiler, like having to do an order by subgroup on the items in the join clause; perhaps those fields would need to be specified (kind of like how you have to specify everything in "GROUP BY" whenever you use an aggregate, instead of it assuming that it should just by nature of being in the SELECT clause).

2008-05-26

Dreamworks DVDs suck

Time to rant about Dreamworks DVDs. We just picked up Shrek the Third, and although not as bad as Shrek 2, it is still annoying.

My biggest complaint about Shrek 2 is that, when it starts, it launches this really long preview bit with Ben Stiller about Madagascar. But the DVD is authored such that you cannot press the skip-forward button or the menu button to skip it! Fortunately, fast-forward is not disabled. But it still annoys me that I can't instantly skip over it. I've seen this stupid documentary enough. I even have the movie on DVD; I don't need or want to see a promo to learn about a movie I've already seen.

Shrek the Third isn't as bad, although you do have to sit through the Dreamworks logo twice before getting to the menu, and the three previews can be skipped with the skip-forward feature (but not the menu button — a common annoyance with many DVDs).

If possible, I'd love to get a DVD player that does not respect the flags that say you cannot use features of the DVD player, like fast-forward, chapter skip, and menu. Nothing irritates me more than being forced to sit through logos and previews and copyright warnings. In fact, nothing makes me want to break copyright more than being forced to stare at the notice for several seconds every time I want to watch the movie, with every button press greeted with a "feature disabled" alert until the words are burned into my screen.

I wish I knew what this fascination was with the motion picture industry in taking away basic features from people, like DVD specs that include "do not allow" flags and TiVos that can't fast-forward through commercials. It's like they feel they have to balance their entertainment product with annoyance.

2008-05-20

VB RULES!!!

Well, in some cases. I do believe in using the right tool for the job, and, believe it or not, VB was the right tool for this job.

I had a need to convert an NT username to a full name. I found a lot of different, complicated methods of querying Active Directory, but the simplest answer was in VBScript:

v = GetObject("WinNT://domain/username")
v.FullName

As I looked and looked for an easy way to do this in .Net, I discovered that the GetObject function exists in VB.Net. Even better, after a quick trial, the same two lines of VBScript code worked in VB.Net!

Now, as it so happens, I was doing this in a Reporting Services report, which, for editing code, follows in the footsteps of SQL 2000's DTS ActiveX Task code editor — namely, VB in a textbox. What I discovered is, although it works great in the Visual Studio IDE, when I deployed it to the Reporting Services server (even on my local box, where every account is running as Administrator), it failed with a security exception.

The reasoning for this makes a whole lot of sense. Since you can access Reporting Services with a web browser and upload any .rdl files you want, if it didn't apply security constraints to that code, then anyone could upload any code they wanted to run as the Reporting Services account. Great, but how do you get around it when you need to?

The answer to that, I found (although not without a lot of digging), can be as simple as registering an assembly in the GAC, marking it with AllowPartiallyTrustedCallers, having it assert FullTrust permissions, and letting it do the restricted call. The report can then reference that assembly, and all is right with the world again.

Since I had to move this out of Reporting Services's code textbox, I thought I'd try rewriting it in C#. I still had a reference to the VisualBasic namespace to use the GetObject function, but the object i got back was of type System.__ComObject, and from there, I admit, I was stuck. Reflection couldn't get me to the properties, and C# doesn't allow for late-binding.

Could I have solved it in C# eventually? No doubt. But how long would it have taken me? This is my last day on this project, and I'd like to get things done; so if I can get it done by adding an assembly consisting of 15 lines of VB code in the GAC and call it a day, I have to wonder: why bother with anything else?

Imports System
Imports System.Security
Imports System.Security.Permissions

<Assembly: AllowPartiallyTrustedCallers()>

<PermissionSet(SecurityAction.Assert, Name:="FullTrust")> _
Public Class ADLookup
 Public Shared Function GetFullName(ByVal NTName As String) As String
  If String.IsNullOrEmpty(NTName) Then Return String.Empty
  Try
   Dim s As String = String.Format("WinNT://{0}", NTName.Replace("\"c, "/"c))
   Dim x As Object = GetObject(s)
   Return Convert.ToString(x.FullName)
  Catch
   Return String.Empty
  End Try

 End Function
End Class

Reminds me of doing .Net 1.1 code, importing the VB namespace to get at simple functions like IsNumeric and IsDate (tryParse didn't exist for many classes then, and at the time I wasn't experienced with try/catch, certainly not enough to rely on it for intentional testing of data).

2008-05-19

One page, a half-dozen files

Just had to continue on my SharePoint rant. Today, I wanted to add a new page to our SharePoint project. The goal of this page is to get a PDF file from a web service, and download it to a browser. Originally, I had this built in to the postback event of the web part that let you request this file, where the event set a property such that, in the Render method, it would then grab the HttpResponse object, clear it, set the Content-Type and Content-Disposition headers, write it, and end. This worked great, causing a download box to pop up to download the file, except there was a rather peculiar side-effect: after this, none of the events on the control would respond anymore. You could click on any link or button, and nothing would happen.

So, I figured this viewing page had to be separate from the existing page. Instead of a postback, it'd be a hyperlink with a _blank target and an appropriate pair of querystring parameters to trigger the correct report to open in a new window. So I created a brand new web part. Then I created the .webpart file, and the elements.xml and feature.xml file. Then update the manifest.xml file and the .ddf file. Finally, (after wondering where the heck my web part was and calling another developer over to give me another pair of eyes), I updated the deploy.bat file. The web part, plus six other files — and that doesn't include creating the .aspx page in the document library to host the web part (a process that I still have to do manually, since I don't yet know how to add it to a site definition or template or whatever the heck it is).

Let's compare this process to standard ASP.Net. Create the page, which creates the code-behind and designer files automatically. And then... oh wait, no "and then"!

2008-05-06

Mentadon't

I've finally had my fill of Mentadent toothpaste.

If you're not familiar with the product, it capitalizes on the idea that there's a dental benefit to brushing with a combination of baking soda and peroxide. The toothpaste is packaged in these rigid, two-chambered packages. You insert one into a dispenser, and as you push down on the top, two plungers push up on the two chambers, dispensing equal amounts of a blue and a white gel, presumably a baking soda and a peroxide formula.

That's the theory, anyway. In practice, it doesn't always work as smoothly as all that. Typically, because of an air bubble or something, at the start of a new package, one side will be short relative to the other, resulting in a couple days of all blue or all white. That, in and of itself, isn't too bad (except that it's usually the white side that's short, resulting in all blue, and the blue doesn't have the fresh taste that the white does).

The past two packages have been worse than normal, though. The former, the blue side had liquefied. Instead of a blue gel, out came a blue watery substance. I don't know if this was good or bad by itself — we did brush with it anyway, and for all I know probably ended up rotting our teeth with it — but when blue liquid started to show through the seams of the package (obviously designed to contain a gel, not water), it had to go, barely used.

The latter has seemed to defy laws of reality. For the entire life of the package, when dispensing the product, a good 3-4 inches of blue gel will come out for every half inch of white. I can understand this happening if they were filled unevenly, at the start of the package, but I would think at some point, eventually, it would even out. Unless I've been applying uneven pressure while pushing down on the dispenser — but since the thing is almost empty at this point, that kind of uneven pressure would eventually result in something like a 30° tilt.

The hassle just isn't worth it anymore. Sorry, Mentadent, but I just want a tube of toothpaste. This is just too complicated.

2008-05-04

You have two weeks to correct our mistake

I got a letter from the Colorado Department of Motor Vehicles. It seems that when they issued the title for my Toyota Prius, they gave it a fuel type of "Gas", whereas they should've given it "Electric/Gas". I wouldn't think it matters much; technically, the thing does run on gas. The only energy I put into the thing is unleaded gasoline. The fact that it uses that gas to power an electric generator and, in the end, transport me about 50 miles per gallon of gas used is nice and all, but it does all run on gas. If I didn't give it gas, it would be dead. (Barring any conversion process that may allow me to plug it in, but technically the same could be said for any car; just that the "conversion process" may be much more involved.)

But if the Colorado DMV wants to make the distinction, I have no problem with that. What I do find a little interesting is that, along with the convenient "no postage necessary" envelope to return my title, they gave me a deadline: "Please return the title to this office in the enclosed envelope by May 13, 2008." I've owned the car for four years, and now it's imperative that I get this done within two weeks? What would have happened if I were on vacation?

Not that I'm too bothered by it. The two week deadline might just be an arbitrary date they put on there just to get me to return it. And, that's probably not such a bad thing. If there were no deadline, just "return it someday", "someday" could easily turn into "never" as it gets put off indefinitely.

Still, the implied "Hey, we screwed up, now hurry up and act to fix our mistake" almost makes me want to wait until May 14th to send it off.

Global Warming - Lastest Excuse for the War on the Family

Pretty interesting article on how the pseudoscience of global warming is being used in a renewed attack against families and religion. I'll have to keep my comments to a minimum, because not only does the author already make plenty of points clear, I don't think I could comment further without wanting to tear into the lefty environmentalist whackos who actually believe that tripe.

I love the author's last paragraph, though. Reminds me of the Mystery Science Theater 3000 short "Mr. B Natural", when Tom Servo quips, "Meanwhile, the Midvale police visit his locker to find out why they call him, 'Buzz'."

I always found it interesting (and sad) how words get twisted. Instead of perversion, it's "progressive". Obscene is "adult". And conservative faith and belief is "backwards", "Puritanical", and even "dangerous".

I've said it before. Some days, I wish the Second Coming would hurry and get here to put an end to this madness.

2008-04-16

Web Part Madness

In my earlier post, I commented on how, from what I heard from another developer's rantings, web parts involve developing your own web page. As I'm about to dive into my own web part, I realize I didn't fully grasp what he meant.

It appears that some large things that one would normally take for granted in an ASP.Net web page — in particular, the creation of child controls and the rendering of the page and said controls as HTML — must now be coded manually. Yes, imagine my surprise when I opened up some web part code and saw the CreateChildControls and Render methods and all their code.

What. The. Crap.

It's like SharePoint development takes all the ease and safety of .Net coding and rips it away like a scab that hasn't fully healed, leaving a bloody festering wound in its place. But at least they give you a needle, and if you're lucky, you can go out on the web and find enough thread to sew the wound closed.

Yes, the geezer in me is already telling me to sit down and shut up. I've drawn out web pages by hand before, where the closest thing we had to a "server control" was an include file that wrote an expected structure of HTML code to the response stream. But for heaven's sake, that was over five years ago. We're supposed to be well beyond that. We're even coding with the very tools that take us beyond that, and we're being told that we have to do our jobs with the important tools tied behind our backs.

Maybe this is what Microsoft meant by "do more with less"?

2008-04-10

SharePoint, addendum

Just a quick little addendum on SharePoint. As part of my document library woes from before, I discovered my problem. It seems that there are differences between sites and webs, and in some cases, SharePoint blurs the lines between them, and in others, the lines are very clear. For instance, a Document Library site (or web — see, I'm already confused) that exists under the main site at http://localhost/Docs is not the same site as http://localhost/. Although some features are accessible through the web services located at http://localhost/_vti_bin/*.asmx (such as finding a document by URL), some features are not unless you use the services belonging to the specific site (e.g. http://localhost/Docs/_vti_bin/*.asmx, such as creating a folder).

It doesn't help that, when you add a web reference in Visual Studio to a subsite's services by directly entering http://localhost/Docs/_vti_bin/Dws.asmx, it adds the value to the app.config/web.config file at the root http://localhost/_vti_bin/Dws.asmx level, and if you're not expecting this, you're using the wrong services.

Not that all my problems are solved. Apparently, the web services aren't letting me get information on an empty folder, which sucks, so I dropped that possibility and moved on. (Compared to the object model, the web services are pathetic.)

Recently, I created a custom view for a document list. To create this view, I had to download and install the newest version of FrontPage of all things, called the SharePoint Designer (because Visual Studio refuses connect to a SharePoint web site with an explicit message saying so — more evidence at the lack of support in SharePoint for developer tools). I did this because I needed to remove all the "chrome" from the list view (it's being embedded in an IFrame from a CRM site), and the only way to do that is to customize the .aspx code itself, and the only tool to do that is FrontPage (by whatever name they call it), apparently.

With it all created and working great on my local development machine, all that's left is to migrate it to the servers. I have yet to figure this part out. Although we do have manifests and such set up for web part and event listener deployments, we do not have anything for a custom view template page, and so far Googling has failed to reveal anything that might help.

Again, I iterate how frustrating and ridiculous this all is as a developer. I should not have to waste hours of time trying to find out how to move my work from one server to another. It's possible to point my local copy of SharePoint Designer to the server (with some hosts file editing, since the URLs are only created local to the machines), but this kind of manual copying, essentially duplication of work, is also pretty dang frustrating.

I did come across tools that promise to migrate whole lists, but that's not what I want to do at all. That would overwrite content, which would be bad. I just want to migrate changes, update a view (actually the template for a view), and preferably without installing or pointing a development tool to each server in turn for each change. (And I wasn't able to determine if the solutions I came across would migrate views anyway...)

I'll try to keep these posts updated with any solutions that I find to this and other problems as they come up... >:(

2008-04-04

I'm a developer, and I hate SharePoint

When you develop a regular ol' web site in Visual Studio, you just say "create a new web site". You create your pages and web controls, classes, whatever you find necessary. To debug it, you set your breakpoints anywhere you want and hit F5. Studio these days even launches its own built-in web server, so you don't have to set up IIS. You can even change your code on the fly in some cases. When it comes time to deploy, you grab the code, copy it to the server, point IIS to it, and you're done. (Ok, it's rarely that simple, but it can be. Throw in a few web.config changes or other optional steps like pre-compiling if you wish...)

How easy is it to develop a SharePoint site? First, you have to install SharePoint. While I've never actually done this step, I can't really say that it's hard or easy; however, since IIS comes pre-installed, it's a pretty simple bet that installing anything is more difficult than not having to install anything. Considering how big SharePoint seems once you get it installed, I imagine installing and configuring it can be extremely complicated. Or perhaps it's fairly easy if you just accept all the defaults.

I'm working on a SharePoint project that, so far, we've tried building an event listener, a web part, and code that uses the web services. The event listener works fine, except that to debug, you have to:

  1. manually create the list in the SharePoint site on every developer's instance of SharePoint
  2. register the listener class library in the GAC
  3. use a command-line utility to register the listener with the event
  4. run IISRESET to restart SharePoint
  5. attach the Visual Studio debugger to the SharePoint process

Fortunately, a batch file takes care of the middle steps, and the first step only has to be done once. Still, it's a lot of extra overhead, and it has to be done for every listener that gets created.

The web part has been the responsibility of another developer. Based on his comments, I gather that designing a web part involves practically designing a whole web page. What I remember from them in a previous job, setting them up and deploying them was a royal pain. Modifying them was considered something you just Didn't Do. Their solution was that they had a web part that had, as a parameter, the path to a web control. If you wanted to create a new element on a web page, you created a regular .ascx web control, dropped the "container" web part on the page, and set the path property to the location of the .ascx control. Probably the only easy thing about that job.

The web services are supposed to be easy to use. They have been decidedly not so. My tasks have revolved around trying to interface with the default document library. I can access documents by using the URL http://localhost:ip/Docs/Documents/filename.doc just fine. And yet, if I try to get any information about the document library itself, it tells me the very document library from which it is serving documents doesn't exist. This is most evident when I use the Dws.GetDwsData method on an existing document. It returns a load of information (including a "LastUpdate" parameter which, instead of being a piece of useful information like when the document was last updated, is a key for future calls to see if it was last updated since a given key). One of these pieces of information is a reference to the Document library, which it returns as an error code, "ListNotFound". Because this document library that these documents are stored in doesn't exist, I can't use other methods I need, such as Versions.GetVersions, Dws.CreateFolder, or Lists.GetListItems.

And we haven't even gotten to the point where we try and deploy SharePoint content yet. As I already alluded to before, there's no real way to transfer content pieces from one machine to another in any easy or automated way. A simple list had to be manually created on every developer's machine, and it'll have to be done on Development and Test. In that previous job, they had an established procedure: back up the entire SharePoint database from development, and restore it in the test environment, then QA and then Production in turn. (Which, yes, meant you can't store any development or test information in SharePoint.) While I was there, for the first time, a SharePoint List was used to store data instead of content, and they had to bring in a Microsoft consultant to write a special utility to export the data and re-import it, so it wouldn't get lost in the complete-backup-and-restore deployment procedure they had for years. Because, of course, no procedure currently exists.

Supposedly, SharePoint is what companies want, because it means they can manage content without having to go through development to do it. Case in point, we have a static web site that we're moving into SharePoint for our client, because every change they want to make, they have to call us to make it. The problem is, it's taken our one consultant, the one who's supposed to be experienced in SharePoint, all week to get the existing site into SharePoint, and he still hasn't even gotten the framework in place, let alone the content — something even he agrees would take maybe two days, start to finish, to completely rebuild in a standard ASP.Net site.

Where it seems to make sense to set up a single-environment easy-to-manage web site and collaboration area, it is a nightmare for multi-environment (i.e. Dev, Test, Production) migration and custom development. And yet, it's what the people at this consulting firm keep selling like it's the Holy Grail. I have my own theories as to why this is, but the fact remains, we're stuck with a tool that just refuses to do the job, and tasks that should take half a week are becoming impossible to do in two.

2008-03-24

Whatever happened to Microsoft Phone?

Many years ago, I had access to a piece of software called Microsoft Phone. It was distributed with some versions of the Creative Labs Phone Blaster, a rather massive expansion card that combined a modem with a sound card. What made this combination really cool was that the card and the software turned your computer into a telephony device. It wasn't the first or only device on the block that could turn your computer into an answering machine — I had something in my computer in college that did the job as well (although I'm sure my roommate often times wished we just had a simple tape-based machine, especially when I would reboot my computer and it would knock him off the phone). It also wasn't the only software that could use your computer's microphone and speakers as a speakerphone — some Phone Blasters were bundled with completely different software that did that as well.

What made Microsoft's product so interesting is that it was integrated with its fledgeling text-to-speech and voice-recognition programs, and it used the common MAPI message storage system. Back in the day, when Windows 95 was still new, mail storage could be set up as more of a common database. At least, the mail storage location was a simple control panel icon, and the built-in Windows Mail client could connect to the MSN service without an issue, without having to use any custom email software. (It seemed simpler then; maybe it could still be done that way today, since I'm back to using Windows Mail in Vista instead of Outlook or Thunderbird or Outlook Express or even Live Mail.)

In any case, installing the Microsoft Phone software included installing Microsoft Voice, which allowed for some voice recognition. Trying to control the computer with it was more of a gimmick than being really useful at all. (Looking back on it now, it's hard to expect more out of 1995 technology, although I'll get into that in a minute.) The command set was fairly limited, although what commands it did know, it did recognize fairly well, so although there wasn't a lot you could do with it, you could at least do those things consistently.

Now, here's where things got interesting, as far as Microsoft Phone was concerned. One of Phone's features was that you could dial in to your phone and enter a code to access the program and start issuing commands. Fairly standard fare for answering machines. And, just like any ordinary answering machine, you could issue those commands by using a touchtone keypad. Where it started to set things apart was, Phone would prompt you for commands, and not by using pre-recorded prompts. It would read you instructions using text-to-speech reading from a help file. While this might not sound like such a big deal, especially in an age where sound compression and disk space are cheap, back then, this was huge. Phone was able to provide a rich, vocal interface without having to save megabytes of prerecorded files.

The next interesting thing it could do, because of text-to-speech, was it could read extended information about your message. It could not only announce the time and date of a message, and read the phone number, but it could also read the name of the caller. If that caller was in your address book (remember, this was all coming from your MAPI store), it could read that personalized name. Granted, the pronunciation wasn't always perfect, but it was still a very cool feature to hear your computer read the name of your caller to you over the phone along with the message.

Now, this was back in the days of dialup, and you could set your computer to call into your ISP and check and download email during the day. This brings me to the next very cool feature. Remember, all messages were stored in the single MAPI store. So when you play new messages, included in that was your computer reading your new email messages to you. The first time I showed this feature off to a friend, her jaw hit the floor as soon as she realized what was happening.

And finally, because this was running with the Microsoft Voice speech recognition engine, not only could you give the commands using your touchtone phone buttons, you could speak your commands and have the machine respond.

So where is this technology today? As I was writing this, I was thinking about how cool this was back in 1995, but I was questioning how it would work a decade later. In 1995, voice was still a preferred method of communication. Being able to call my computer and have it read my email to me would be a great feature. Today? Not so much. In 1995, 99% of email in my inbox was interesting and valid, came from human beings who sat down and thought about what they wanted to say, and wrote it as if they were writing a letter, with well-thought-out sentences and grammar. Today, most email is hastily dashed off, with lots of abbreviations, written with very little context. And that's only the ones I want to read, which is a minority. The majority of email I get now is mass mailings from corporations, or spam about prescription drugs I don't need or software that's not licensed or legal, or newsletters or chain letters or group mailings or any number of other impersonal communications — basically, nothing I'd need to phone home about.

However, the concept of having voice messages delivered to email is desirable. So I guess, a decade later, I'd be looking at the reverse of what was cool in 1995 — being able to connect a text-type device (i.e. an email browser) to my home message storage and get voice messages. It might be cool to have dictation transcribe that to text for very low bandwidth applications, but with the way email clients work these days, it'd almost be unnecessary. (Almost. I still use Pine over an SSH connection to read email from work, so text-only email and browsing isn't dead yet. At least not for me.)

I'm still intrigued by the concept of a software-controlled answering machine, one that could take messages, convert them to email (making the "from address" look like a phone number would be a nice touch), especially one that could be programmed to automatically answer and annoy calls from certain phone numbers (such as those identified charities that continue to call soliciting donations despite the request for no solicitations). In the end, we've got to go with something that "just works"; so although I tried to recapture those old glory days by buying the Microsoft Cordless Phone product around the turn of the century (that failed to live up to expectations, especially when it wasn't supported past Windows 98), we ended up buying a plain old telephone answering machine that isn't a piece of software installed on a computer — and my current roommates (i.e. my family) are much happier for it.

As far as voice control, I finally decided to play around with it in Vista the other day. I will say that I am impressed with how far it has come. For general navigation, there's this "say what you see" concept, where you can just say the name of what you see — the name of a link on a web page, for instance — and the computer will attempt to discern what you mean. You say the name of a command button or link, and it clicks it. However, it's still rather clumsy. I feel like I'm talking to my 1-year-old, having to repeat things over and over again, sometimes louder, sometimes trying to say things a different way. Dictation was especially frustrating, as you're supposed to be able to say "correct" and the words it just got wrong in order to fix it — and yet over and over again, as I was trying to dictate and correct, it started typing "correct this" and "correct that", like an old sitcom routine where the dullard keeps reciting stage directions.

And in that sense, it doesn't feel much different than 1995. Sure, you can actually dictate text now. But giving commands? That's been around for over 10 years, and we're still pointing and clicking with a mouse. At the end of the day, I couldn't see how it could possibly be any more convenient or useful than just using the freaking mouse or keyboard to directly input the location or text desired. Even if it does entertain my wife to listen to me try to use it....

2008-03-17

It's so hard to send email programmatically

One look at the subject line, and I'm sure the first word that comes to the minds of the four people who might see this blog is, "Huh?" Sending email isn't hard. Programs do it all the time. Why can't a genius like you figure it out, Yakko?

Well, here's the problem. See, I'm writing a custom Windows application for a client. This application is essentially a database for storing the deposits and withdrawals of a handful of investment companies and their investors. In essence, it's copying what he tracked in spreadsheets. Since I'm moving this from spreadsheet to database, I can implement all new features for him like tracking performance over time, graph them, compare them to market indices, and the big new feature, generate PDF reports and email them instead of printing paper copies one by one.

He runs all of this on a PC in his home office. As such, I made sure I wrote the program to be as small and simple as possible. Sure I considered using SQL Server Express with Reporting Services to generate the reports, but how easy would that be for him to install, or back up the program, or send to his part-time clerk for easy data entry? Small and lightweight were part of my requirements here. And thanks to the .Net framework, I managed to write a pretty decent app, with a SQL Server CE database engine, that would fit on a floppy disk. Not that I could find a floppy disk to put it on anymore, but you get the idea.

The one part of the program that really disappointed me, though, was the email tool. Here I managed to generate these nice-looking PDF reports in real-time (using the PDFSharp library -- they say "the same drawing routines can be used to create PDF documents, draw on the screen, or send output to any printer", and in my experience it really was almost that easy to convert my paper printing code to make a PDF). But how to email them?

While it is true that the .Net framework includes some very simple routines for sending SMTP mail, which I have used before in various client-server, intranet, and web applications, the trick is they work really well if you're using them on a server. Ever send email from a personal computer on a dial-up connection? (Yes, he's still on dial-up; don't ask.) Even if you're on broadband on a static IP, chances are your address is identifiable as an end-user, and there's a very good chance your email will be rejected with prejudice.

Now, he can send out email using his email program of choice. Why? Naturally, because it's configured to use his ISP's email server. Because I want to keep this program as simple as possible, adding email configuration was not in the design; and, I could find no way to query Windows to discover (a) what email program he's using, and (b) what settings that program uses to send email. (I couldn't even be certain I could use the same settings -- he's on MSN; what if it requires SPA?)

When you think about it, it all makes sense. How easy it would be for spammers to write programs to do just that and send email out via a user's ISP's email server, instead of trying to send out from the easily-blockable local address.  If I recall correctly, viruses used to do just that, before email programs had to get better at locking external program access out.

So what do I do? I create a "mailto" link with the appropriate subject and body parameter for every PDF that must get generated (but no attach parameter; although that used to work once upon a time, apparently that got to be too big of a security risk as well, and while some email programs simply ignore it, Outlook will refuse to start and throw an error instead), then delay about 5 seconds (because in some of my test scenarios, if a second "mailto" link was getting processed at just the wrong point of processing of the first one, the second would trigger an error instead of opening a second window). Once all the mail windows were open, an Explorer window would be opened to the folder containing all the PDFs generated, so they could be manually dragged and dropped on the appropriate mail windows.

I ask you: how much does that suck?

Probably the only redeeming point in this is that, at this early stage in the game, manual intervention is desired, so he can see the reports and the messages before they go anywhere. It's going to get old very fast, though...

2008-03-12

It's not a toy, it's for business

My wife and I got Tablet PCs for our business. We've been wanting them for a while. We decided to pick up a couple HP Pavilion tx1420us models. These are considered "home entertainment" models, not business models, but they had decent specs for the price.

The screen isn't as big or as fine as I would like. This replaces a Thinkpad T40p, which had a nice large screen and a 1400x1080 resolution. This tablet has a relatively small screen with a lot less real estate at 1200x800. Also, I've found that the pen takes a lot more pressure to use than I would like. It's quite tiring to write with for long periods of time, and it has a tendency to "stutter" when drawing over any distance.

But I definitely like the tablet. I've wanted one for a long time, and I'm glad I have one. They are fun to use, not to mention pretty easy when "point and click" is literally pointing with a pen and physically tapping it. There's something rather satisfying about being able to move things around screen by grabbing it with a pen and pulling it around. It makes it feel a little more "hands-on" than like I'm using a remote control (i.e. the mouse or touchpad).

It comes with Vista, which at the moment I have this sort of love/hate relationship with. I know the UI enhancements are all eye candy, but honestly it makes the computer "feel" next-gen, like we've finally progressed. However, it is very different than older versions of Windows. When I first sat down in front of XP, it didn't take long to find my way around, because it was so similar to Windows 2000, which was similar enough to 98... In Vista, I'm still trying to figure out how to do tasks that were second-nature to me before. So far, though, it hasn't been "too bad". I haven't run into any driver or compatibility problems, and I found the dialog box that pops up every time you launch a program easy to turn off (convenience trumps security, for now). I'm sure a big part of this is I'm running on all-new hardware that came with Vista — I still have no intention of ever trying to upgrade my older machines to this OS, even if they're still running long after XP is dead and buried.

2008-02-29

How fast is a parsec?

Possibly the most plausible explanation I've heard for the blatant error in unit in the cantina scene in A New Hope is, according to a copy of the script, Skywalker and Kenobi were supposed to react as if they knew they were being fed a load of hogwash — an emotion that was, unfortunately, never really conveyed.

The Wookieepedia article on the Kessel Run mentions this, plus another explanation offered by George Lucas in DVD commentary (that he meant his navigation computer could find shorter — and therefore faster — routes), but only as "behind the scenes". The more prominent explanation is given thus:

Solo was not referring directly to his ship's speed when he made this claim. Instead, he was referring to the shorter route he was able to travel by skirting the nearby Maw black hole cluster, thus making the run in under the standard distance. However, parsec relates to time in that a shorter distance equals a shorter time at the same speed. By moving closer to the black holes, Solo managed to cut the distance down to about 11.5 parsecs.

Essentially, his ship was so fast, he was able to take a more dangerous shortcut.

Why do I think this is a load of bunk, in the context of the cantina scene? The whole conversation at that point was about speed. Why mention distance when your client is asking about speed? Expecting a farmboy and a nomad to know the details about a particular space race as a condition for winning a fare seems like pretty bad business sense. Solo may have made some bad decisions, but I don't see him as being entirely stupid.

If you do take the Wookieepedia explanation as fact, though, it does explain that awkward silence with the following deleted scene:

Han: "It's the ship that made the Kessel Run in less than 12 parsecs."
Luke: "So, that would mean he'd have to take the Maw shortcut? How fast do you have to go to get around there?"
Ben: "Um, let's see, take a left at Geonosis Prime, you'd have to circle Kamino..."
Luke: "No, Kamino's too far; that's at least half a parsec out of the way; he'd have to go past Denobula."
Ben: "Denobula? Oh, yeah, at the Circle-K. Used to love their donuts. I haven't had their donuts since, oh, before you were born..."
Luke: "So, to get down to 12 parsecs, you'd have to cut pretty close to Naboo in order to make that turn."
Ben: "That would put you within... ten million kilometers of Maw?"
Luke: *scribbling* "No, no, carry the 4."
Ben: "Oh, right, right, half a million kilometers. Now, with the mass of Maw, the velocity needed for a Corellian freighter to escape would be... um..."
Han, annoyed, impatient: *ahem* "She's fast enough for you, old man."

Assuming Han was full of it, though, and Ben and Luke eventually caught on, I imagine the scene in docking bay 94 might've played out a little differently...

Luke: "What a piece of junk!"
Han: "She'll make .5 past light speed."
Luke: "What, .5 parsecs?"
Han: "She may not look like much, but she's got it where it counts."
Luke: "Really? Does it count all the way to blue?"
Han: "I've made a lot of special modifications myself."
Ben: "Yeah, like the ability to tell time with a ruler."
Han: "But, we're a little rushed..."
Luke: "Yeah, we've got to go in just a few inches."
Han: *thinking* I should've let Greedo shoot first...

2008-02-21

There's always a bigger fish

I've been on the job market for a little while now. I got laid off from my previous job, thanks to recent changes in the economy. Fortunately, the job market is fairly active at the moment. Inside of a week of finding out I was being laid off, I had two phone screens and an in-person interview.

One company with which I interviewed recently, I felt was going to be a sure thing. The phone interviews went very well, and when I met them in-person, everything seemed to go well. I had a great conversation with the manager, and the interview with the development team was great. I felt like it was a great team, like it was a great group of guys to work with, and that we would get along wonderfully. They seemed reasonably impressed with what I knew and what I could bring to the company as well.

I did have another company that was wanting to bring me on. They had worked with me before, they knew my capabilities, and I think they were nervous that I might take another offer (I had done it before — no hard feelings to them, but at the time, they didn't have anything for me, and I had another offer that was really good). I wasn't going to tell them no until I knew I was going to take another offer, so of course I was eager to see if I was going to get one from the interview. (I was pretty sure I would, but I did want to be sure the offer was in the ballpark of my desired salary/benefits.)

This is where it helps to be working through a recruiter. I can call him and pester him all I want, without fear of turning off the potential employer for calling them directly too much. ;)

I finally got their decision. They said I was a great fit and fully qualified, but they were going to go with someone who had just a little more expertise or experience or something. Now, fortunately, I did have interest from the other company, so I am able to switch over, accept, and start working on this other project. Even if I'm not sure it's ideal, the fact is, it's work, and as such, it's income. (There are also other benefits, such as a decent health care plan, not far from my kids' school, more of a "known quantity" as I've worked with them before...)

The annoying thing is, this isn't the first time I've heard that explanation. "Yeah, you'd be perfect, but someone else is better." It just leaves me wondering. Is it the truth? What is it that I could've improved on so that I could be the one that's just "a little better"? Or was there a flaw with me that they're not telling me? Or was this other guy's advantage more of an inside — a friend or relative that as good as had the job from the beginning? (Although usually it's the bigger companies that bother with interviewing just to say they did; this was a smaller company.)

Then again, I had been praying that I would be guided to the right job for me and my family. Although I think I would've enjoyed the work, it was a bit of a commute, which would've taken me away from my family. Perhaps that, or other reasons that I couldn't see, would've made this job not as good as something else.

In any case, it's hard to be too upset. Instead of having to make a difficult decision between two opportunities, the options narrowed themselves down for me. It's almost more of a relief, actually. And, I now know where my next few paychecks, health insurance, and so forth are coming from beyond the next two weeks. I know my wife can relax a bit, too, knowing everything's pretty much taken care of.

2008-01-16

ASP.Net, Dynamic Controls, and ViewState (oh my)

Let's say you have an ASP.Net page. On this page is a PlaceHolder. In that page, controls may get dynamically loaded into the PlaceHolder. This may occur as a result of a postback event (e.g. Button or LinkButton click) or simply as a result of some logic that occurs when the page loads. However, even with EnableViewState set on the PlaceHolder, controls that you load are not automatically persisted in ViewState. What to do?

This is a problem I've run into at least a half dozen times in my .Net coding career. It's also a problem I've solved that many times. However, every time I come across it, I forget one small detail, or I do something a little differently that makes it all fall apart. So, at least for my own benefit and perhaps for the benefit of others that may stumble upon this post, I'm putting the solution here. I haven't yet seen anything that pulls all these techniques together; this might not be the first, but at least it's one I know. Links to other pages contained here may not be from where I first got my information, but I'm including them for further reference and information.

First: When you create your controls, make sure you add them to the page FIRST, and THEN set any properties on them. This is because, in order for the ViewState manager to consider the control for management, it has to detect a change, and it can only detect changes after the control has been added to the Controls collection. (Thanks to Jeffrey Palermo's blog for pointing this out, and explaining the reason for it.) But what if you don't have any properties to set? That is remedied by the next consideration.

Second: Set an explicit ID. When ViewState is saved and restored, the control IDs must match. (I've seen a reference that says you have to use a custom class and explicitly tag it "ViewStateModeById", but my experience has shown me that ID matters whether you do this or not. Sequence may or may not be as important.) If you don't specify an ID, ASP.Net will automatically generate one, and there is no guarantee that the one generated when you create it on the fly will be the same when you recreate it on PostBack. The result is, you may get the control back, but its state will not persist on the first PostBack (although it might on the second and subsequent PostBacks, since it will be consistently reloading during the PostBack from the same spot -- this behavior may be confusing).

The tricky part comes in play when you have to reload the controls on PostBack. You have to do it before ViewState gets loaded, otherwise the controls won't be there to get their ViewState restored to them. The event before LoadViewState is Init. But you have to somehow have the page remember what controls were loaded. Keeping that value in ViewState is the most convenient, but you can't retrieve that value from Init because ViewState hasn't loaded yet. What to do?

I've seen one solution to this as to use the Session. I'm not a big fan of this myself for a few reasons. First, it requires Session. I consider it a goal to write web applications that don't require Session at all, because they generally perform faster and are more scalable. Granted, I rarely succeed, and Session does have its place, so writing it off isn't a valid enough reason. Second, it is very easy to get the page state out of sync with the session state. Hit the "Back" button a couple times, and then submit. The page submits a ViewState with two controls, but the server checks the Session variable and loads four controls, and then tries to apply the ViewState to it. Third, that variable has the lifetime of the Session. At worst, if it's not cleared out or checked appropriately, its presence may cause confusion on another page. At best, it's just taking up a couple bytes of memory long after its purpose has been served.

So what's the solution then? Third: My trick is to override the SaveViewState and LoadViewState methods on the page. SaveViewState returns a serializable object that gets dumped to the page. It's fairly trivial to override this method to return an object[2] (which is still an object and still serializable), putting your own data in one element and the results of base.SaveViewState() in the other. What you need to store in that element is something you can use to recreate the controls in the order they exist on the page. Maybe it's just a number -- if you create the controls as a series and give them IDs of ID{x}, where {x} is the sequential number, then all you need is the number of controls to recreate. Or maybe it's a string array that contains the list of control IDs. Or perhaps, if the controls are of different types, it's an array of the control types that have to be loaded, or the paths to the .ascx files. Whatever it takes.

The LoadViewState method is overridden to check to see if the ViewState object is an object array, and if the first element is of the type we expect (the one we create in SaveViewState). If so, it recreates the controls based on that first value, and then calls base.LoadViewState on the second. (If it turns out not to be an object array or something is not of the expected type, I am in the habit of just calling base.LoadViewState directly, just as a safety catch in case the overridden SaveViewState got missed. It's never happened yet, though.)

The code, therefore, looks a little like this:

/// <summary>
/// Saves the view state, including the IDs of placeholder controls so they can be reloaded on postback
/// </summary>
/// <returns>new ViewState object</returns>
protected override object SaveViewState() {
    object[] newViewState = new object[2];

    List<string> docTypeSelectionControlIDs = new List<string>();
    foreach (Control control in placeHolder1.Controls) {
        if (control is DocTypeSelectionControl) docTypeSelectionControlIDs.Add(control.ID);
    }
    string[] arrayOfIDs = docTypeSelectionControlIDs.ToArray();

    newViewState[0] = arrayOfIDs;
    newViewState[1] = base.SaveViewState();

    return newViewState;
}

/// <summary>
/// Loads the view state, including custom state information from the SaveViewState override
/// </summary>
/// <param name="savedState"></param>
protected override void LoadViewState(object savedState) {
    //if we can identify the custom view state as defined in the override for SaveViewState
    if (savedState is object[] && ((object[])savedState).Length == 2 && ((object[])savedState)[0] is string[]) {
        //re-load the DocTypeSelectionControls into the placeholder and restore their IDs
        object[] newViewState = (object[])savedState;
        string[] arrayOfDocTypeSelectionControlIDs = (string[])(newViewState[0]);
        foreach (string docTypeSelectionControlID in arrayOfDocTypeSelectionControlIDs) {
            DocTypeSelectionControl dtsc = (DocTypeSelectionControl) LoadControl("DocTypeSelectionControl.ascx");
            placeHolder1.Controls.Add(dtsc);
            dtsc.ID = docTypeSelectionControlID;
        }
        //load the ViewState normally
        base.LoadViewState(newViewState[1]);
    } else {
        base.LoadViewState(savedState);
    }
}

/// <summary>
/// Loads the page, including some dynamic controls
/// </summary>
protected override void OnLoad(EventArgs e) {
    if (!IsPostBack) {
        Document[] documents = (Document[])(Session["Documents"]); //The size of this array varies
        foreach (Document doc in documents) {
            DocTypeSelectionControl dtsc = (DocTypeSelectionControl) LoadControl("DocTypeSelectionControl.ascx");
            placeHolder1.Controls.Add(dtsc); //Add control FIRST
            dtsc.ID = String.Format("DTSC_{0}", doc.DocumentID); //THEN set ID
            dtsc.InitializeControl(doc); //control initializes itself based on this document, and saves info in its ViewState
        }
    }
}

Note that this is just a quick sample. The controls in the placeholder could be modified as a result of some control event, but it really doesn't matter. The part that controls saving and reloading the ViewState of the controls doesn't change.

UPDATE 4 Dec 2009 — I created a sample project, using Visual Studio 2005 and .Net 2.0, that probably does a better job at describing things than I'm trying to do here. The project is very simple, with a single page and a single control. The page starts by loading a single instance of the control into a placeholder, and a button lets you add as many more instances of the control as you want. Every time you add a control, there is a PostBack, and all existing controls get their state and values loaded and re-saved to ViewState. You can see that the controls themselves do nothing to save their values from PostBack to PostBack; it all happens automatically thanks to the standard ViewState manager. When you click the "Submit" button, the main page loops through all the controls, collects the selected values, and builds a table to display the results. There is no use of Session, and at any time you can use the Back (or even Forward) buttons to navigate to a previous state of the page and pick up from that point.

DynamicControls.zip

2008-01-07

Come join the Social!

I don't have a Zune, but I thought it might be kind of fun, since your Zune Card and your Gamercard are automatically one, to set up my Zune Card account and join "the Social", as it were.

For a company that usually has "integration" down to a science, they're really dropping the ball on this, aren't they?

I have Windows Media Player and Windows Live Messenger. With a simple flick of an option on both sides, I can have my status message on Live Messenger automatically update to show what I'm listening to in Windows Media Player. It should be pretty much the same thing for Zune, no?

No. I have to download the Zune player. While it has some nice little features of its own (I like the collage of album art you can have in the background of the "now playing" mode), it lacks a ton of other features that are in WMP11, like visualizations, minimizing to a toolbar, updating the Live Messenger status message, responding to the media keys on my keyboard...

I did some searching on the 'net, and all I could find were echos of the questions going through my mind: Why didn't they just make a plug-in for WMP11 and integrate Zune functionality into their already-very-capable and useful media player?

2008-01-06

Water heater want a blankie?

Our hot water has always been pretty hit-or-miss. Some mornings, I would get up, turn the water on full hot until it warms up, and have to back it off to a comfortable temperature; others, I would leave it on full and have a lukewarm shower. I don't know if lately it had been getting worse, or if we just decided to do something about it, but we finally got an insulation blanket for it.

The difference was immediate. The very next morning, I noticed that the time I had to wait before I got hot water was dramatically shorter. And, the water was a lot hotter. It was very nice. Since it had been known to happen on occasion before, though, I was hesitant to directly attribute it to the new blanket; but we installed it on New Year's Day, and for the past five days since, the results have continued to be positive. I've also noticed that water out of other taps (like the kitchen sink, for instance) also get hotter water faster.

So how much money have we thrown away over the years? Honestly, probably not a lot. It's said one of the problems with a water heater is that it has to constantly re-heat the same water in the tank as it sits and cools when it's not used, and it would seem that our water heater didn't do very much re-heating once the water cooled. It's a gas-fueled appliance, and our gas bill during the summer months has always been pretty low. Although add on the amount of water dumped down the drain waiting for hot water to flow, and even if it's still not a lot, it's still waste. Plus, we've been throwing away comfort. Even if it's not a big thing, it's nice to know that we managed to do something to improve our lives. Not only is it an improvement, it helps to take the edge off knowing there are so many bigger improvements that we just haven't gotten around to, or can't afford, yet.