Wednesday, May 09, 2012

Html.DemoPartial - Side-by-Side Render and Code for Partial Views

I was working on an ASP.NET MVC site for demonstrating some internal concepts recently. One of the things I wanted to show was how to use some HtmlHelper extension methods. I wanted the viewer to be able to see the rendered output and then flip to see the code, sort of like how you see in snazzy demos like the DevExpress MVC Extensions.

Here's a little screencast/demo of what I'm talking about.

Unable to display content. Adobe Flash is required.

So... how do you do that? Here's how it plays out:

  • The tabs are jQuery UI.
  • The syntax highlighting is SyntaxHighlighter.
  • The HTML is rendered with a special HtmlHelper extension method DemoPartial that I'll show you how to write.

The usage is really nice – you put your demo code into a partial view and then render it like this:

@Html.DemoPartial("MyDemoPartialView")

First, I'll warn you... I do a little bit of crazy and totally unsupported reflection work in here because there's some stuff in ASP.NET MVC that I really wish was public. But since it's not... there's some hackery.

I wrote this using MVC 3. I won't guarantee that I'll be keeping this article up to date with future versions of MVC, and future versions may make the reflection work obsolete. You're on your own for adapting it to the future.

On with the show!

First, let's set up the view that will be housing our demos. You'll need to include...

  • jQuery (JS)
  • jQuery UI (JS and CSS)
  • SyntaxHighlighter (JS and CSS)
  • SyntaxHighlighter "brush" scripts for XML/HTML and Razor (JS)

You'll also need a little bit of startup script to get the tabs running. The base view will look like this:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>My Demo Page</title>
  <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
  <link href="@Url.Content("~/Content/themes/smoothness/jquery-ui.css")" rel="stylesheet" type="text/css" />
  <link href="@Url.Content("~/Content/shCore.css")" rel="stylesheet" type="text/css" />
  <link href="@Url.Content("~/Content/shThemeDefault.css")" rel="stylesheet" type="text/css" />
  <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
  <script src="@Url.Content("~/Scripts/jquery-ui-1.8.19.min.js")" type="text/javascript"></script>
  <script src="@Url.Content("~/Scripts/shCore.js")" type="text/javascript"></script>
  <script src="@Url.Content("~/Scripts/shBrushRazor.js")" type="text/javascript"></script>
  <script src="@Url.Content("~/Scripts/shBrushXml.js")" type="text/javascript"></script>
  <script type="text/javascript">
    $(function () {
      // Initialize the demo/code tabs on the page, if any.
      $(".demo-tabs").tabs();

      // Execute syntax highlighting.
      SyntaxHighlighter.all();
    });
  </script>
</head>

<body>
  <h1>My Demo Page</h1>
  <!-- Here's where the DemoPartial calls go. -->
</body>
</html>

One thing you may get stuck on is the SyntaxHighlighter "brush" script for Razor. "Brushes" are the way that the SyntaxHighlighter script knows what is a keyword, what's a constant, and so on, so it knows what to highlight. There isn't one by default for Razor. I cobbled one together on my own based on a copy/paste/modify on the shBrushXml.js brush that deals with XML and HTML. You can learn how to create a custom brush on the SyntaxHighlighter site. There's a download link at the bottom of this entry so you can grab my totally unsupported, not entirely complete, you're-on-your-own-if-you-use-this brush for Razor.

Now that you have your page, you need the DemoPartial extension method.

First, create your static class for extension methods as well as your DemoPartial extension. For mine, I used the same overrides as the standard Html.Partial extension so I'd have feature-parity.

public static class DemoHtmlExtensions
{
  public static MvcHtmlString DemoPartial(
    this HtmlHelper htmlHelper,
    string partialViewName)
  {
    return htmlHelper.DemoPartial(partialViewName, null, htmlHelper.ViewData);
  }

  public static MvcHtmlString DemoPartial(
    this HtmlHelper htmlHelper,
    string partialViewName,
    object model)
  {
    return htmlHelper.DemoPartial(partialViewName, model, htmlHelper.ViewData);
  }

  public static MvcHtmlString DemoPartial(
    this HtmlHelper htmlHelper,
    string partialViewName,
    ViewDataDictionary viewData)
  {
    return htmlHelper.DemoPartial(partialViewName, null, viewData);
  }

  public static MvcHtmlString DemoPartial(
    this HtmlHelper htmlHelper,
    string partialViewName,
    object model,
    ViewDataDictionary viewData,
    bool codeOnly = false)
  {
    // Here's the important bit.
  }
}

I marked the overload that's interesting - the one with the most parameters. You'll also notice the optional codeOnly parameter – I'll explain that in a minute.

The first thing you have to do is generate a unique ID for your set of demo/code tabs. It needs to be unique so you can use the extension method multiple times on the page. The unique ID is used in generating HTML for the jQuery UI tabs, which need some unique link targets.

var tabSetName = HtmlHelper.GenerateIdFromName(
  String.Format(
    CultureInfo.InvariantCulture,
    "{0}-{1}",
    partialViewName,
    Guid.NewGuid()));

I realize that's sort of reaching back into our ASP.NET web forms past, but I couldn't think of a much better way without forcing the consumer to do that unique ID on their own. In the end, I wasn't too worried about it.

Let's create a StringWriter where we can generate our output. We'll write the demo tab headers out first:

using (var writer = new StringWriter(CultureInfo.InvariantCulture))
{
  writer.WriteLine(
    "<div class=\"demo-tabs\"><ul><li><a href=\"#{0}-1\">Demo</a></li><li><a href=\"#{0}-2\">Code</a></li></ul><div id=\"{0}-1\">",
    tabSetName);
}

What that does is:

  • Open a container with a special class of "demo-tabs" – <div class="demo-tabs">
  • Create an unordered list of two items. jQuery UI will turn these into the tab headers. This is where that unique ID comes into play – each tab contains a link to a unique anchor in the page.
    • The first tab links to the demo part ("live render").
    • The second tab links to the code part ("highlighted syntax").

Next we need to write out the demo tab contents. That's the part with the live render of the demo. It looks like this:

if (codeOnly)
{
  writer.WriteLine("[View included for code only; switch to the code tab.]");
}
else
{
  writer.WriteLine(htmlHelper.Partial(partialViewName, model, viewData));
}

Remember that codeOnly optional parameter? That's so we can insert demos of things that may not have UI to them. It's an easy way to, say, show how a helper function works – include it via a partial demo view.

If we don't want a "live render" (we want "code only") we'll render a placeholder text block in the demo tab; otherwise we'll include the live render of the partial view in our output.

Next we need to close the demo tab contents and start the code tab contents:

writer.WriteLine("</div><div id=\"{0}-2\">", tabSetName);

Here's where we almost begin to touch on that tricky reflection I was talking about.

The next step is obviously to render the actual code into the view. That means getting the partial view from the registered set of view engines.

I made a second extension method FindPartialViewFile that does exactly that. I will show you that in a minute. For now, let's just pretend it exists and works.

We need to:

  • Get the view file.
  • Determine which syntax highlighter to use (Razor or HTML/ASPX).
  • HTML-encode the view file contents and dump it to the code tab.

That looks like this:

var partialViewFile = ViewEngines.Engines.FindPartialViewFile(
  htmlHelper.ViewContext.Controller.ControllerContext,
  partialViewName);
if (partialViewFile != null)
{
  string brush = null;
  switch (Path.GetExtension(partialViewFile.Name).ToUpperInvariant())
  {
    case ".CSHTML":
      brush = "razor";
      break;
    default:
      brush = "html";
      break;
  }
  writer.Write("<pre class=\"brush: {0}\">", brush);
  using (var reader = new StreamReader(partialViewFile.Open()))
  {
    writer.WriteLine(htmlHelper.Encode(reader.ReadToEnd()));
  }
  writer.WriteLine("</pre>");
}
else
{
  writer.WriteLine("<p>[Demo code file not available. Please see original source.]</p>");
}

First we use the FindPartialViewFile extension (which I'll show you in a minute) to locate the virtual file containing the view code. If the file comes back null, it means we can't locate the file so we'll render a little placeholder text. If it can be found, we figure out which syntax it is by using the file extension – if it isn't a .cshtml file, we fall back to use the standard HTML highlighting. Finally, we read the contents of the partial view, HTML-encode it, and dump it out to the code tab contents.

The last thing to do here is close the code tab contents and the overall tab container, then return the whole mess.

writer.WriteLine("</div></div>");
return MvcHtmlString.Create(writer.ToString());

The whole thing all put together nicely is available below in the download.

OK, so now for the tricky bit – locating the virtual file containing the view code.

You'll notice I say "the virtual file" and not "the physical file." That's because ASP.NET uses a mechanism called VirtualPathProvider to abstract itself away from the filesystem. You can serve files from the disk... or embedded resources... or a zip file... or a database... or wherever else.

The default ASP.NET MVC view engines (web forms, Razor) both interface with the VirtualPathProvider to do their file location. They do this by deriving from a common VirtualPathProviderViewEngine class... and this is what I count on to do my crazy reflection-based view location magic.

Deep in the bowels of VirtualPathProviderViewEngine is a private method called GetPath that actually gets the virtual path to a view or partial view if the view engine is able to locate it. There are a bajillion undocumented parameters on this sucker (because it's private) but it's the only way to actually tie a view name to a virtual file path. So... we need access.

First, create a new extension method class. We're going to extend ViewEngineCollection so we can do that call you saw earlier. I don't like jamming a bunch of extension methods for different things into the same class, so I didn't.

We can get a reference to that GetPath method like so...

private static readonly MethodInfo _getPathMethod =
  typeof(VirtualPathProviderViewEngine)
    .GetMethod("GetPath", BindingFlags.Instance | BindingFlags.NonPublic);

You're now in ridiculously unsupported territory.

To make it easier on ourselves to call this thing, let's create a delegate with the same signature as GetPath. It's long, it's ugly... it is what it is.

private delegate string VirtualPathProviderViewEngineGetPath(
  ControllerContext controllerContext,
  string[] locations,
  string[] areaLocations,
  string locationsPropertyName,
  string name,
  string controllerName,
  string cacheKeyPrefix,
  bool useCache,
  out string[] searchedLocations);

Yeah, that's the signature. You can double-check in Reflector if you want. I'll wait.

Now that we have our private method reference and our convenience delegate, we need to...

  • Iterate through all of the view engines in the collection.
  • For each engine that's a VirtualPathProviderViewEngine, call the GetPath method on the engine.
    • If it returns null, move to the next engine – the view wasn't found.
    • If it returns a string, use the VirtualPathProvider to get a reference to the virtual file and return that.

In code, it looks like this:

foreach (var viewEngine in viewEngines)
{
  var vppEngine = viewEngine as VirtualPathProviderViewEngine;
  if (vppEngine == null)
  {
    continue;
  }

  var getPathDelegate = (VirtualPathProviderViewEngineGetPath)Delegate.CreateDelegate(
    typeof(VirtualPathProviderViewEngineGetPath),
    vppEngine,
    _getPathMethod);
  string[] searchedLocations = null;
  var path = getPathDelegate(
    controllerContext,
    vppEngine.PartialViewLocationFormats,
    vppEngine.AreaPartialViewLocationFormats,
    "PartialViewLocationFormats",
    partialViewName,
    controllerContext.RouteData.GetRequiredString("controller"),
    "Partial",
    false,
    out searchedLocations);

  if (!String.IsNullOrEmpty(path))
  {
    return HostingEnvironment.VirtualPathProvider.GetFile(path);
  }
}

return null;

I got the list of parameters by checking out the MVC code for this thing. There really are hardcoded strings like that going in. They seem to be used as cache keys and such. In the end, you get the path to the partial view or you don't, and that's what matters.

Whew! Now put that together with the DemoPartial extension and you're in business!

  • Put your demo code into a standalone partial view.
  • In your main view, call Html.DemoPartial() and pass in the name of your partial view (and any other data). Just like Html.Partial only tabified.
  • Revel in how awesome your demo site is and how easy it is to maintain.

Below is the download for the complete source and my cobbled-up Razor syntax highlighter brush. Standard disclaimers apply:

  • It's free, and you get what you pay for.
  • Totally unsupported.
  • Works on my box.
  • Performance not guaranteed.
  • No warranty expressed or implied.
  • Don't say I didn't warn you.
  • Good luck and have fun.

[Download Source – DemoHtmlExtensions, ViewEngineCollectionExtensions, Razor Syntax Highlighter Brush, Stub Main View – 5KB]

Wednesday, May 02, 2012

ASP.NET MVC, aria-required, and jQuery Unobtrusive Validation

I saw this interesting article about using model metadata in ASP.NET MVC to add the aria-required attribute to required input fields. The approaches there are all totally valid and work great if you have the requirement to add the attribute on the server-side.

However, if you're using jQuery unobtrusive validation, you've already got some metadata you can use from the client side. Required fields all get the data-val-required attribute associated with them:

<input
  data-val="true"
  data-val-required="This is a required field."
  id="RequiredField"
  name="RequiredField"
  type="text"
  value="" />

The attribute only exists on required fields. With that, it's easy enough to jQuery your way to freedom and leisure:

$(function () {
  $('[data-val-required]').attr('aria-required', true);
});

...and now all of your required fields have the appropriate ARIA attribute.

Sunday, April 22, 2012

Little People Hall of Justice

Phoenix is really into the Fisher Price "Little People" figures, particularly the DC Super Friends line. She doesn't know a lot of words, but she knows "Bapmo!" (Batman) and loves flying them around the room.

A while ago we got her the Batcave playset so she'd have some additional vehicles and stuff, not just figures. This, of course, led to also getting Wonder Woman's invisible jet and the Batmobile. (OK, maybe it doesn't hurt that we both like the Super Friends. Come on, when your 18-month-old runs around yelling about Batman, that's pretty sweet.)

The proliferation of Super-Friends-related items started lending itself to a bit of a storage dilemma, and on top of that, I started thinking back to the cartoons... How can you have the Super Friends without the Hall of Justice?

The cartoon Hall of Justice

So I took a cardboard box, went to the craft store and picked up some supplies... and I present to you: The Little People Hall of Justice!

From 20120422 Hall of Justice

The main form is just a cardboard box. The outside is covered in a thin white foam that makes it soft. I lined the inside with tagboard and added a fold-down flap that Velcros shut so you can play inside. The windows on the front are blue tagboard behind some cutouts in the foam. The edges of the cardboard are lined with twill tape to avoid any cardboard cuts. I reinforced the top with additional cardboard so it's not quite climb-proof, but it should stand up to some reasonable amount of abuse.

I posted several views of it in the photo album if you're interested. The pictures have captions that explain how things are put together... in case you want to make your own.

Wonder Twin powers ACTIVATE!

Wednesday, April 11, 2012

Using CodeRush Duplicate Code Detection in a Real Project

I'm working on a fairly major refactoring project, merging a couple of fairly large libraries together that have some overlapping/similar functionality. I figured this would be the perfect time to try out the reasonably new CodeRush Duplicate Code Detection feature.

The solution I'm working on has eight projects - four unit test projects, four shipping class libraries, and a total of 1248 C# code files.

I didn't have duplicate code detection running in the background during the initial pass at the merge. Instead, I ran the detection after the initial pass to indicate some pain points where I need to start looking at combining code. (Let me apologize in advance - I wanted to show you the analysis results in a real life project, but because it's a real project, I can't actually show you the code in which we're detecting duplicates. I can only show you the duplicate reports. With that...)

First, I started with the default settings (analysis level 4):

Default Duplicate Code analysis settings - level 4

Running that pass took about eight seconds. I found a few duplicate blocks in unit tests, which weren't unexpected. Things having to do with similar tests (specifically validating some operator overload functionality):

Level 4 analysis results

I wanted to do a little more, so I turned it up a notch to analysis level 3 and ran another pass. This time the pass took about 12 seconds and I found a lot more duplicates. This time the duplicates got into the actual shipping code and started pointing out some great places to start refactoring and merging code. This pass also grabbed more duplicates across classes (whereas the previous pass only caught duplicates within a single class/file).

Level 3 analysis results

Well, since one notch on the analysis level settings was good, two must be better, right? Let's crank it up to analysis level 2!

Duplicate Code settings at analysis level 2

Once you get to level 2 and below, you get a warning: Analysis might require significant CPU power and memory. I'm not too concerned with this right now since I'm running the analysis manually, but if you turn on the background analysis mechanism you'd probably want to verify that's really what you want.

Anyway, the third pass using analysis level 2 still only took about 12 seconds... and re-running it, to verify that time, only took around 5 seconds so I'm guessing there is some sort of caching in place. (But don't hold me to that.)

Analysis level 2 results

Now we're talking! Tons of duplicates found on the level 2 run. However, while the code is very similar, there aren't as many "automatic fixes" that the system can suggest.

Message - no automatic fix for duplicate code.

I don't fault CodeRush for this - the duplicates will require a bit of non-automated thought to combine, which is why they pay us (programmers). (If they could replace us with scripts, they'd do it, right?) I'm sure the geniuses at DevExpress will shortly replace me with a keystroke in CodeRush so they can hire my cat in my place, but until then, I can use this as a checklist of things to look at.

Out of curiosity, I decided to do another run, this time at level 0 - the tightest analysis level available. This run took ~50 seconds and found so many duplicates it's insane. Some are actionable, some are what I'd call "false positives" (like argument validation blocks - I want the validation to take place in the method being called so the stack trace is correct if there's an exception thrown). Still, this is good stuff.

Analysis results - level 0

Given the balance between the too-granular detection and the not-granular-enough, I think I'm going to go with the level 2 pass, address those things, and then maybe turn it up from there.

Overall, I was really impressed with the speed of this thing. I mean, 1248 files, thousands of classes... and it takes less than a minute to do super-detailed analysis? That's akin to magic.

Big props to DevExpress for a super-awesome tool that helps me improve my code. And, hey, some of the automatic fixes that are built in don't hurt, either. :)

If you want to use this on your project, head over and pick up CodeRush. Honestly, it's the first thing I install right after Visual Studio, and this sort of feature is exactly why.

Wednesday, March 28, 2012

Falling Apart

I think I'm getting old and starting to fall apart.

For the last year I've had this Eustachian tube dysfunction that causes me to hear myself really loud in my left ear. If I'm talking, all I hear is me. When it's really bad, I can hear my heartbeat or my breathing. Walking around would cause a thump-thump-thump sound with every step.

I ran through five days of prednisone and cipro, then 10 days, with no luck. After a 25-day-run of prednisone, I ended up going in on Monday and getting a tympanostomy tube in my ear.

When they do it, you lay on your side and they drop some anesthetic in your ear. You lay there for a while letting that soak in, then they pop your eardrum and put the tube in. The tube itself is about the size of a pencil lead. It didn't hurt going in, but there is still some occasional soreness a couple of days later if I yawn really big or bend over such that the blood rushes to my head.

I still hear myself a little, but in general that whole side of my head feels clearer. I'm waiting for a few days before I test to see if I can clear it.

That same day I noticed the left half of my tongue had gone numb. I let it go for a day, but it was still numb, and that freaked me out a bit, so I called the doctor's office.

When they puncture your eardrum, some of the anesthetic runs through your Eustachian tube into other areas of your head. Apparently some hit my tongue just right (though I didn't taste anything) and numbed me up. Today, two days later, I'm better... but that lasts a really long time. What was most freaky was that I could feel things on my tongue, I just couldn't taste, like half of my taste buds had just died. I'm guessing some nerve got hit or something.

I also got some new glasses because Phoenix got ahold of my old ones and decided to rip the arm off. Since my prescription had expired, I had to get a new one. Upon getting my new glasses, I noticed everything looked like I was looking out a slight fish-eye lens – straight lines weren't straight anymore. Looking at, say, a brick wall was really trippy because the whole thing looked curved. I tried them for a week and took them back.

The optician said I wasn't adapting to the lens material, so I went from polycarbonate lenses to some other higher-density material. Same thing – weird curvy vision. Again, I tried for a week and never adapted, so I fell back to plastic lenses. (I did inquire as to whether something in my new prescription might be causing this but I was assured it was the lens material.)

I'm wearing the plastic lenses right now and still seeing curvy. Keeping in mind it takes about a week to get my glasses fixed up every time I take them back, we're going on well over a month now since I first tried to get new glasses. I'll give these two weeks before taking them back, but right now I hate these stupid glasses with every fiber of my being. I hate that nothing looks right. I really just want my old glasses with my old prescription even if I wasn't seeing 20/20.

So that's three of my five senses that have gone out on me recently and I have to say, it's stressing me out.

I won't even get into the pressure cooker work has become, or my lack of time to do anything at home, or the bathroom caulk I've replaced twice now because I can't get it to set.

Basically I'm just tired all the time now. I'm hoping things pick up soon because I'm really just falling apart.

Wednesday, March 21, 2012

Call an MSBuild Target Like a Function

I'm working on building a bunch of projects that all follow a specific convention for naming, NuGet packaging, and so on. As part of that, I want to run the build for each component – from clean to package – all at once rather than clean everything, then build everything, then package everything. (For the sake of the article, let's ignore whether that's a good idea or not and just stick with me.)

MSBuild has batching, which sort of works like "for-each," but in examples you see you can really only "batch" on tasks. Targets (groups of tasks) allow you to specify inputs and outputs, but the "outputs" list is assumed to be files, so if it finds the outputs are up to date, it won't run that input.

Anyway, I found this article that explains how to sort of abuse the inputs and outputs on targets so you can effectively do for-each over a target.

First, create an item list with your inputs and metadata. It doesn't have to be files. For your inputs, pass the list of parameters. For the outputs, put a dummy value that always evaluates to empty/null – that way it's never seen as up to date and will always run.

Here's a sample script:

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Start"
  xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
  ToolsVersion="4.0">
  <ItemGroup>
    <SomeValues Include="First">
      <Meta>true</Meta>
    </SomeValues>
    <SomeValues Include="Second">
      <Meta>false</Meta>
    </SomeValues>
  </ItemGroup>
  <Target Name="Start">
    <CallTarget Targets="Parameterized" />
  </Target>
  <Target Name="Parameterized" Inputs="@(SomeValues)" Outputs="%(Identity).Dummy">
    <Message Text="Item = %(SomeValues.Identity)" />
    <Message Text="Meta = %(SomeValues.Meta)" />
    <Message Text="---" />
  </Target>
</Project>

The output, as you'll see, is that the "Parameterized" target gets called once for each item in the group.

Thursday, March 15, 2012

A Typical Play Session with my Toddler

My 15-month-old daughter, Phoenix, is a kick in the pants. This kid has got more energy in her than I can keep up with. Of course, when she gets home from day care, she runs rampant and really wants to play, so we do.

I envision a typical play session with a different little girl going something like, "Oh, look, Cookie Monster is going into Hooper's Store. See how he's in there? He's looking for cookies. Cookie starts with 'C.' Here comes Elmo, he's bringing Cookie Monster some cookies. Yum!"

That's not quite how it goes at our house. Phoenix is into Sesame Street, so we do have the Sesame Street Neighborhood Playset and figures, but she's also into Little People, so Daddy ended up getting some figures that he could better relate to... like Batman and Joker.

Little People - Batman and Joker

We've also got some Little People vehicles, like the school bus. That means a play session goes more like this for us:

Travis: Hey, Phoenix, can you find Batman? Where's Batman?

Phoenix: <holding up the Batman figure> BAPMO!

Travis: Yes, that's Batman! Great! I'll play Joker, because Daddy is always the bad guy anyway. Here's Joker, he's getting into the bus with this little kid. Do you see how Joker is in the bus? Now Joker has a hostage. Hostage starts with 'H' – can you say 'hostage?'"

Phoenix: <dancing the Batman figure around on the floor> BAPMO! BAPMO!

Travis: Right, Batman has to come stop the Joker. Joker is going to drive the bus away from Batman. Vroom! Vroom!

Phoenix: <grabbing the school bus> pbbbbbbbbbtttttt! <making raspberry/spitting noises and driving the bus around>

Travis: Perfect! Now Batman's going to try to stop Joker... but the Joker has a secret weapon. The shark! Watch out, the shark is going to get you!

At this point, I grab this giant stuffed shark we bought at Ikea for $15. It's the best $15 I've ever spent. Phoenix wrestles this shark and screams and runs and then comes back to wrestle it some more. I attack her with the shark, and she screams and giggles.

Phoenix attacking the giant shark

Travis: Hey, Jenn, where's the Wonder Woman figure?

Jenn: I don't know, have you looked in the toy box?

Travis: Yeah, but I don't see her. Phoe, where's Wonder Woman?

Phoenix: <ignoring me, still wrestling the shark>

Travis: Well, crap. How am I supposed to set up the Hall of Justice in Hooper's Store if I can't find Wonder Woman?

...and so on. I'm not sure if that's typical of everyone or if it's just me. I'm thinking it's just me. (Phoenix also accompanies me on occasion to the local Things From Another World comic store. Gotta start her early.)

Tuesday, March 13, 2012

Convert Any Set of Headphones to Bluetooth/Wireless

When I'm at work I like listening to music... but I hate being tethered to my computer (or iPod dock, or whatever) by the headphone cord. If I want to slide my chair over to the whiteboard to write something up or reach over and get something out of my bag, I have one of two choices: make the cord so long that it gets tangled up on my chair and in my desk stuff; or take the headphones off to go do whatever it is and put them back on.

Also, I like my headphones. I have some ear buds, I have some over-the-ear headphones, and while they're not bajillion-dollar models, I like them. I don't want Yet Another Pair of Headphones that are wireless. I want to use my headphones. The ones I already own.

Before I explain this, let the audiophiles be warned: This doesn't yield super-awesome quality. But then again, I'm not sure I'd call music-over-Bluetooth on any level "super-awesome." Just be aware.

First, get a Bluetooth audio dongle receiver like the TaoTronics BTI-005. That's the one I got and it seems to work pretty well. This runs between $20 and $30 at the time of this writing. There are a few of these dongles out there, but on Amazon right now this one is the most popular and is one of the few that doesn't also require you to plug it in.

The audio receiver allows you to plug your existing headphones in and it will receive any audio played over Bluetooth through those headphones. You just converted your headphones to Bluetooth.

TaoTronics BTI-005 Bluetooth audio dongle receiver

The thing is, you may or may not be done.

If your audio source (iPod, etc.) supports Bluetooth, you're probably done. Pair it up and listen to a song. See how the quality is.

If the quality is pretty good on your Bluetooth audio source, you're done.

If your audio source doesn't support Bluetooth or if the quality sucks, you're not done. I found, for example, that there's some real weirdness with connecting a Bluetooth audio headset to a Windows 7 computer. <techspeak>Windows 7 requires a very specific A2DP audio profile that not every Bluetooth headset supports. This dongle doesn't support it. If you search for "Bluetooth audio quality in Windows 7" you'll see a ton of people with the same problem on different headsets/adapters.</techspeak>

In this case, you also need a Bluetooth audio transmitter dongle. I bought the JayBird uSport Bluetooth adapter for 3.5mm devices. What this does is plug into the headset jack on your audio source (computer, iPod, phone, etc.) and sends the audio signal via Bluetooth. Pair that up with your receiver dongle and you have both pieces to the puzzle covered – the transmitter and the receiver.

JayBird uSport Bluetooth adapter for 3.5mm devices

Again, there are different transmitters out there, but this one seems pretty popular and supports this new protocol called "apt-X" that allows you to potentially get a better audio quality if you also use a receiver with "apt-X." You can also try the TaoTronics TT-BA01 adapter for 3.5mm devices, which will probably work. I wanted the upgrade-ability, so I went with the JayBird.

I'm listening to Pandora through my computer using the BTI-005/JayBird uSport combo right now and it's decent. Is it audiophile quality? No, but it's decent. Given that it's compressed music streaming over the internet and into a mediocre pair of headphones... I don't think the Bluetooth portion of things is that noticeable. When there's a gap between songs or a really quiet section of a song, I do hear a low-level "buzzing" background noise. But for what I'm doing... it's good enough.