Monday, January 25, 2010

Developing applications that target multiple version of Office on a single computer can be difficult, daunting, disturbing, and down-right dirty.

The Microsoft Office team has performed some herculean tasks to ease our burdens, but there are still some hurdles to overcome.  Having multiple versions of Office installed on a single development machine is not possible and it appears that you cannot install the Office PIAs on a machine that doesn't have that version of Office installed (e.g., you cannot install the Office 2003 PIAs on a box with Office 2007 on it).  Additionally, having multiple development environments each targeting a specific version of Office is not always practical.

It is fortunate that add-ins created targeting the Office 2003 PIAs will work seamlessly in Office 2007 (they will not, however, be able to take advantage of the new features of the 2007 system such as the Ribbon, etc).

If you have a VS project that was built on a box with Office 2003 + VSTO 2005 SE + Office 2003 PIA and you open it on a machine with Office 2007 + VSTO 2005 SE + Visual Studio Tools for the Office system 3.0 Runtime, you will be prompted to upgrade your project to the new office platform.  Upgrading your project will have the unfortunate side-effect of rendering the project useless on your other dev environment that doesn't have Office 2007, etc on it.

Fortunately, there is a handy VS setting neatly tucked away in Tools -> Options -> Office Tools -> Project Upgrade called Always upgrade to installed version of Office.  Unchecking this option will allow you to open the project in Visual Studio without being compelled to upgrade.

I find it somewhat comical that using VS2008 on a machine with Office 2007 + the two VSTO editions you can create a project targeting Office 2003 but when you reopen it you'll be prompted to upgrade (unless you have the setting disabled) it.

If you don't have them there, don't forget to add the Office2003PIA and VSTOSERuntime bootstrappers to %ProgramFiles%\Microsoft SDKs\Windows\v6.0A\Bootstrapper\Packages in order to compile your projects.

 

Monday, January 25, 2010 4:29:35 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Thursday, October 22, 2009

I had the need today to create an application must run with administrative privileges.  Fortunately, this is a pretty straightforward endeavor.

On Windows Vista, Windows 7, or Windows Server 2008 (and beyond) I wanted to take advange of the Admin Approval Mode (AAM) or Over The Shoulder (OST) UAC elevation prompt automatically (and as a side-effect have the little shield icon accompanying my app's icon).  This wouldn't be possible on a previous OS (such as Windows Server 2003 or XP), so I needed to add an explicit permission check.

To get the UAC prompt all you need to do is embed an application manifest.  Vista+, when displaying the application icon, will probe the app and, upon finding the manifest requiring admin rights, will display the icon properly.

Create a manifest file (e.g., app.exe.manifest) in your project.  This is what mine looked like:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity version="1.0.0.0" name="APPLICATION_NAME"/>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>

By default (if you don't create a manifest), a .NET application will have the <requestedExecutionLevel /> set to 'asInvoker' and run the application as the user invoking it.  You can set this to one of three values: asInvoker, highestAvailable, and requireAdministrator.  Also, don't forget to set the name to match your application's name.

Then, to embed the application you can do a few things.

One technique is to use the mt.exe (ManifestTool) from the Visual Studio Command Prompt:

mt.exe -manifest app.exe.manifest -updateresource:app.exe,#1

But I don't believe this would work if your application is signed with a strong name unless you were to delay-sign it.

The easier (and better) approach is to use the Project Properties --> Application tab.  Simply select your app.exe.manifest file in the Manifest dropdown.  It will automatically be embedded.

NOTE: In order to debug your application that requires admin rights you'll need to run Visual Studio from an elevated token.  Otherwise you won't have privileges enough to attach to the process.

Next, I wanted to ensure that the user runs the application with administrative rights.  This check was placed particularly for the pre-Vista operating systems which didn't have UAC.  To do this I added the following code:

[STAThread()]
private static void Main() {
   Application.EnableVisualStyles();
   Application.SetCompatibleTextRenderingDefault(false);

   // check to make sure that the user is running with administrative rights.
   // in Vista/Win7/Svr2008 this is automatically taken care of via the
   // embedded manifest (which ensures administrative rights) via UAC.

   try {
      if ( !userHasAdminRights() ) {
         MessageBox.Show("This application requires administrative privileges.",
                         "Unauthorized Access",
                         MessageBoxButtons.OK, MessageBoxIcon.Information);
         return;
      }
   }
   catch ( Exception er ) {
      MessageBox.Show(er.ToString());  // Of course you'd handle this differently
   }

   Application.Run(new MainForm());
}


private static bool userHasAdminRights() {
   WindowsIdentity identity = WindowsIdentity.GetCurrent();
   return ( null != identity && new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator) );
}

Pretty simple, really.

C# | Security
Thursday, October 22, 2009 7:28:57 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Monday, August 24, 2009

Here's something I threw together the other day to help me organize my ripped audio books.  I have several audio books (Harry Potter, LOTR, Dresden Files, etc) that I've been ripping to file so that I could gather an entire 10-20 CD set onto a single disc for easy listening while driving without needing to switch discs.  I like to use WMA files for easy ripping (via Media Player) and the small footprint.

I've found, much to my chagrin, that the information retrieved online for these books (via CDDB, FreeDB, or All Music Guide (which WMP uses)) is unreliable, frequently contains typos and misspellings, and is extremely inconsistent with respect to formatting and convention.

I found that when I inserted a CD I would spend a good amount of time entering the book name, the genre, the year it was recorded, the author, and the performer, and I'd take the time to rename the tracks.  After a handful of CDs this was almost unbearable and extremely tedious.

I therefore set out to write a script that would do this, updating all of the ID3 tags after the fact.  The only thing I would need to concern myself with was that the book name (I like the form Book Name - Disc XX) was correct.  This script belows makes a few assumptions about the folder and file names:

  • each folder contains a single disk and that they are named according to the pattern "Book Name - Disc XX"
  • all books will be collected into a single folder with the name pattern "Book Set - Book Name"

For example, I ripped a book by Jim Butcher named Small Favor.  Each disc went into a folder named "Small Favor - Disc 01", "Small Favor - Disc 02", etc.  I would then collect all the ripped files into a single directory named "Dresden Files - Book 10 - Small Favor".  This way, when all was said and done, I'd have all the books in order and grouped.

I decided to use PowerShell as the language of preference for automating this procedure.  Not only does it provide a very powerful scripting environment, but I can also leverage the .NET Framework (which I love dearly).  As such, I was able to take advantage of TagLib#'s ability to edit the tags within the files.  This script assumes that the taglib-sharp.dll is in the same directory as the .ps1 script file.

The following PowerShell scripts performs all the collecting, enumerating, and tagging of my files.  Feel free to adapt it and tweak it according to your own preferences.  If you have suggestions, I'd welcome them.  Like I said, I threw it together in a very short time so it's probably weak in many regards but seems to get the job done.

Taking my example above, my command line would be:

.\collectaudiobook "Small Favor" "Dresden Files - Book 10" "Jim Butcher" "James Marsters" 2008

Now I don't need to worry about what information was loaded from the CDDB-esque service, the script updates it all after-the-fact. :)

CollectAudioBook.ps1

$a = $args.length

if ( $a -lt 2 ) {
  write @"

USAGE:

`t.\CollectAudioBook.ps1 titlePrefix targetFolderPrefix [author] [artist] [year]

REQUIRED PARAMETERS:

titlePrefix
`tSpecifies the subfolders to process as a single book.
`t(e.g., 'Small Favor' would locate 'Small Favor - Disc 01', 'Small Favor - Disc 02', etc)
`tThe folders are processed in order by name, assuming that to be the proper sequencing of the resulting files.

targetFolderPrefix
`tSpecifies the target folder to which all tracks are collected.  If this
`tfolder doesn't exist, it is created.
`tNOTE: The folder's full name will be comprised of the targetFolderPrefix, a hyphen, and the titlePrefix.
`t(e.g., a titlePrefix of 'Small Favor' and a targetFolderPrefix of 'Dresden Files - Book 10' becomes
`tDresden Files - Book 10 - Small Favor)

author
`tIdentifies the author of the book.

artist
`tIdentifies the name of the performer.

year
`tIdentifies the year of the recording.

"@
  exit
}

$bookName = $args[0]
$bookSet = $args[1] + ' - ' + $bookName
$author = $args[2]
$artist = $args[3]
$year = $args[4]

$target = '.\' + $bookSet
$partNum = 0

# load taglib-sharp.dll which allows for the manipulation on the media tags in the files
$asm = [Reflection.Assembly]::LoadFrom((Resolve-Path ".\taglib-sharp.dll"))

# create the target directory
$dir = New-Item -path "$target" -type directory -force

# enumerate all files in each folder that start with the prefix, copying and renaming each file
foreach ( $folder in ( Get-ChildItem .\$bookName* | Where-Object { $_.Mode.StartsWith('d') } | Sort Name ) ) {
  foreach ( $file in ( Get-ChildItem $folder | Sort Name ) ) {
    ++$partNum

    $targetPrefix = 'Part ' + $partNum.ToString("00")
    $targetFileName = $targetPrefix + '.wma'
    write "Copying $file`t-->`t$targetFileName"
    Copy-Item $folder\$file $target\$targetFileName

    # update the media information in the file
    $media = [TagLib.File]::Create((Resolve-Path "$target\$targetFileName"))
    $media.Tag.Title = $bookName + ' - ' + $targetPrefix
    if ( $author -ne $null ) { $media.Tag.AlbumArtists = $author }
    if ( $artist -ne $null ) { $media.Tag.Performers = $artist }
    $media.Tag.Genres = { Audio Book }
    if ( $year -ne $null ) { $media.Tag.Year = $year }
    $media.Save()
  }
}

Monday, August 24, 2009 10:20:53 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, July 22, 2009

http://windowsteamblog.com/blogs/windows7/archive/2009/07/21/when-will-you-get-windows-7-rtm.aspx

Can't wait! I've been using Windows 7 in the Beta and RC (Release Candidate) builds for some time now and I LOVE it! It's been rock solid (particularly for it's pre-release form) and very fast. It's been my primary OS now on 3 separate computers so I'm really excited about being able to get my hands on the RTM version in the next few weeks.

Wednesday, July 22, 2009 12:11:47 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, June 17, 2009
Well, I've always loved classical music...too bad I'm not on Windows 95 anymore.

Wednesday, June 17, 2009 12:20:03 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Friday, June 12, 2009
This is definitely not news, but I wanted to blog it anyway.

Having recently just reset a development server to use Windows Server 2008, I needed to open the firewall to allow ICMP packets through.  Essentially, I wanted to have the server respond to PING requests.  On a fresh Windows Vista or Windows Server 2008 install (and I imagine Windows 7, though I've not checked), the ICMP protocol is blocked by default; the server won't respond to PINGs.  When you ping an IP address and don't get a reply that doesn't mean that there isn't a computer on the other end, but it sure is a handy way to check.

In Windows Vista/2008 enabling ICMP through the firewall isn't difficult, but if you want to use a GUI, you have to do it through an inbound rule in the Windows Firewall with Advanced Security option.

Personally, I prefer to do it via the command-line through this simple operation: netsh firewall set icmpsetting 8

Friday, June 12, 2009 3:41:21 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Sunday, May 03, 2009

As you may or may not be aware IE 7+ (and other browsers) support using CSS 2.1 attribute selectors.  Attribute selectors allow you to specify a style on an element whose element matches a particular pattern.  For instance:

td[x] { font-weight:bold; }

This selector will bold the text of any TD element on the page that has an attribute "x" (regardless of value).  Other attribute selector styles include:

...[x="value"] matches where x is exactly 'value'
...[x~="value"] matches where x contains a space-separated list of values, one of which is exactly 'value'
...[x^="value"] matches where the attribute x begins with 'value'
...[x$="value"] matches where the attribute x ends with 'value'
...[x*="value"] matches where the attribute x contains 'value'
...[x|="value"] matches where the attribute x begins with either 'value' or 'value-'

In and of themselves, these are pretty darn cool.  You can do some neat things with CSS and HTML.

I ran into an interested scenario this past week that I'd like to share.  I have some JavaScript that alters the value of attributes at runtime.  I found that the page doesn't automatically update according to the stylesheet specification.  Curiously, it would update when I moused-over the element in question.

I found, however, that I could force the issue by assigning the CSS classname to the element that it already has.  This is enough to trigger the change and have the element update according to its attribute values.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
   <style type="text/css">
      td[req] { font-weight:bold; }
      td[err="1"] { color:red !important; }
   </style>
   <script type="text/javascript">
      function fn() {
         var td = document.getElementById("t");
         td.err = "0";
         td.className = td.className;  // trigger the change by reassigning the CSS class
      }
   </script>
</head>
<body>
   <table>
      <tr><td class="test" err="1" req="1" id="t">Cell</td></tr>
   </table>
   <button onclick="javascript:fn();">Remove Error</button>
</body>
</html>

Interestingly, for this to work properly in a non-IE browser (such as Firefox), I found I could not use the object.property=value syntax.  Instead, I had to use the SetAttribute() function.  Also, with Firefox, reassigning the className was unnecessary.

td.setAttribute("err", "0");

That's probably the best way to handle it then, for cross-browser compliance.

CSS | Web
Sunday, May 03, 2009 9:28:44 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Thursday, April 23, 2009

My buddy Rob Bagby has announced this exciting event called XamlFest which will be in Salt Lake City on May 1st.  If you have the chance, I'd encourage you to sign up and attend.

Here's the info: http://blogs.msdn.com/bags/archive/2009/03/31/xamlfest-in-salt-lake-city-on-may-1.aspx

Thursday, April 23, 2009 12:35:44 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback