Hmmm, it seems I have managed to solve my own problem. It's weird. If you hide the column at design time the product ID will always be blank. If you hide the column one cell at a time using the RowDataBound event everything is peachy:
Protected Sub gv_RowDataBound(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs) Handles GridView1.RowDataBound
e.Row.Cells(0).Visible = False
End Sub
BUT, if you hide the entire column at once in the DataBound event, it breaks again:
Protected Sub gv_DataBound(ByVal sender As Object, ByVal e As System.EventArgs) Handles GridView1.DataBound
GridView1.Columns(0).Visible = False
End Sub
...I don't like that.