Most recent post

Friday, July 9, 2010

VSTO, ClickOnce, MSBuild equals happiness

After spending several frustrating days working on VSTO and ClickOnce deployment I thought I'd share the experiences in the below guidance and instructions for newbie’s to follow (like I was a few days ago). There are some great articles out there, but a lot of misinformation because of differences between VS2008 and VS2005.

The objective :-From an intranet hosting a VSTO package enable a use to click on a URL of the document (http://mysebserver/VSTO/vstogluegood.docx) that is opened on their PC and the required VSTO file is installed and ran. The VSTO ClickOnce package I wanted to be created using MSBuild project file.

Target development environment and Office version
- VSTO developed using Visual Studio 2008
- Office 2007 (specifically targeting Word 2007)

Client side components required
The below are a list of client side components required to run VSTO packages using ClickOnce.
- .NET Framework 3.5
- Microsoft Office 2007 System Update Redistributable PIA (o2007pia.msi)
- Microsoft Visual Studio Tools for the Microsoft Office system (vstor30.exe)

Client side component installs note
- I opted for a pre-deployment of the packages through ActiveDirectory group policy. The o2007pia.msi is easy to place in the OU, however the vstor30.exe isn't because it is an .exe. The solution we found was to extract the vstor30.exe using the below command :-

vstor30.exe /x:Extract

Using the extracted contents in <current directory>\Extract\, we simply placed the trin_trir.msi in the OU. I suspect this is less than ideal, but worked for us.

Certificates
You need a certificate to deploy and run VSTO packages. There are 3 choices :-
a) Purchase a certificate (mandatory if you are deploying beyond your internal network)
b) Use the Temporary Visual Studio certificate created (downside - expires in 1 year)
c) Create a Certificate using makecert.

I'm using the 3rd choice as it provides you with a more robust certificate which can be deployed using ActiveDirectory. Below are the steps :-

1. Open the Visual Studio Command Prompt
2. Type makecert -r -pe -n "CN=<yourCertificateName>" -b 01/01/2000 -e 01/01/2099 -ss My
3. We have made a .cer certificate, but for Visual Studio we need a .pfx certificate. Within Internet Explorer under Options->Content->Certificates find your certificate and export it as a .pfx, (Choose export private key). Now keep this certificate for the VSTO packages.
4. To create a .cer which isn't exportable (and can be pushed out onto client PCs), simple delete the existing certificate created in step 3 and double click on the .pfx file created in step 4. Install the certificate (mark as not exportable) and then export it as a .cer.

You will need to install the non-exportable .cer on the users' PCs, while using the .pfx file in Visual Studio when you create your packages.

Certificate installation
The certificate created above needs to be installed in
a) Trusted Root Certificate Authorities
b) Trusted Publishers

Manual
- On each PC double click on the .cer file created above as administrator and install into the 2 locations.

OU
- In the Group Policy Object Editor add the certificates to Computer Configuration > Windows Settings > Security Settings > Public Key Policies. For a full walkthrough see this (don't forget to place in both certificate locations).:-https://support.smoothwall.net/index.php?_m=knowledgebase&_a=viewarticle&kbarticleid=180

Word configuration
To automatically allow the Word document to run the ClickOnce deployment it must have the location of the website in its Trusted locations.

Manual
- Within Word goto Office menu->Word Options->Trust Center->Trust Center Settings->Trusted Locations.
a. Allow Trusted Locations on my network – checked
b. The following list of locations :- e.g.
http://website-Prod/VSTO/
http://website-UAT/VSTO/
http://website-TEST/VSTO/
http://website-DEV/VSTO/
* Ensure Subfolders of this location are also trusted is checked when added.

Note: Word has a quirk where the VSTOs must be installed in the subfolder of the site (for Trusted Locations), so you can't use http://MyWebSite/Gluegood.docx, while you can use http://MyWebSite/VSTO/Gluegood.docx

OU
- Follow instructions here on how to set the above in an OU (http://technet.microsoft.com/en-us/library/cc178948(office.12).aspx )


Web site configuration
So that the VSTO runs correctly you need to add the MIME type of extension .vsto and application/x-ms-vsto. See this article for instructions (http://msdn.microsoft.com/en-us/library/bb608629.aspx )


MSBuild project file (aka the Magic happens here)
VS2008 comes with a nice 'Publish' UI, however it lacks features in the following ways :-
1. You are unable to differentiate builds between the environments.
2. It can't be part of a scheduled build

There seem lots of other ways of doing the below, but I like the simplicity of building and creating the VSTO in once step using MSBuild without needing to fiddle with other tools.

Below I have provided VB.NET code to generate the MSBuild file and kick off an install. The below however can easily be converted to .msbuild file and .cmd file with hard coded values.

There are 2 parts to the MSBuild file :-
a) The properties created in the Property Group
b) The Actual MSBuild settings

Below are parameters which are passed to the code :-
SolutionFile - This is the location of the .sln file to be compiled. e.g. C:\Projects\VSTOGluegood.sln

Version - This is the version of the VSTO project. e.g. 3.1.2.29

Environment - We use this to determine the SolutionId (see below)

Destination - This is the network address in which you want to create the VSTO. e.g. \\MyWebServer\e$\Site\VSTO\

DestinationURL - This is the public address in which the VSTO will be installed, when new versions are checked by the clients this is the location they will use. e.g. http://MySite/VSTO

SolutionId - To make the VSTOs unique for each environment they must have different AssemblyName and different SolutionId – (http://briannoyes.net/2006/11/03/ClickOnceDeploymentApplicationIdentity.aspx) . The function CompileVSTOReports_GetSolutionId in the below code looks for the .vbproj and extracts the SolutionId, however it replaces the last character so each environment is unique, e.g. Our ProjId is dd0d7e0e-7913-4e9a-8541-d3ef27387d16, for VSTO deployments the 6 becomes an 'a' in DEV, 'b' in TEST, 'c' in UAT and 'd' in PROD.

ManifestCertificateThumbprint - This value you extract from the .vbproj file and is the password you created for the .pfx file encrypted by Visual Studio. To get this value in Visual Studio sign the project with the .pfx file and then open up the .vbproj file and find the value and paste it below.

  Private Sub CompileVSTOReports_CompileSolutionFile(ByVal SolutionFile As System.IO.FileInfo, _
                            ByVal Version As String, _
                            ByVal Environment As String, _
                            ByVal Destination As String, _
                            ByVal DestinationURL As String)

    Dim oStreamWriter As System.IO.StreamWriter = Nothing
    Dim sBuildFile As String = My.Computer.FileSystem.GetTempFileName


      Dim sMSBuild As System.Text.StringBuilder = New System.Text.StringBuilder

      sMSBuild.Append("<Project DefaultTargets=""Main"" xmlns=""http://schemas.microsoft.com/developer/msbuild/2003"">" & vbCrLf)
      sMSBuild.Append("<PropertyGroup>" & vbCrLf)
      sMSBuild.Append("  <SourceSolution>" & SolutionFile.FullName & "</SourceSolution>" & vbCrLf)
      ' If you change the PFX file you must change the Thumbprint below. This comes from the .vbproj file if you manually
      ' use the PFX file - when seen in a text editor.
      sMSBuild.Append("  <KeyFileLocation>" & My.Application.Info.DirectoryPath & "\gluegood.pfx</KeyFileLocation>" & vbCrLf)
      sMSBuild.Append("  <VersionNumber>" & Version & "</VersionNumber>" & vbCrLf)
      sMSBuild.Append("  <AssemblyName>" & "VSTO_" & Replace(SolutionFile.Name, ".sln", "") & "_" & Environment & "</AssemblyName>" & vbCrLf)
      sMSBuild.Append("  <PublishDir>" & Destination & "</PublishDir>" & vbCrLf)
      sMSBuild.Append("  <InstallUrl>" & DestinationURL & "</InstallUrl>" & vbCrLf)
      sMSBuild.Append("  <SolutionID>" & CompileVSTOReports_GetSolutionId(Environment, SolutionFile) & "</SolutionID>" & vbCrLf)
      sMSBuild.Append("</PropertyGroup>" & vbCrLf)

      sMSBuild.Append("<Target Name=""Main"">" & vbCrLf)
      sMSBuild.Append("  <MSBuild Projects=""$(SourceSolution)""" & vbCrLf)
      sMSBuild.Append("    Properties = ""Configuration=Release;" & vbCrLf)
      sMSBuild.Append("      InstallUrl=$(InstallUrl);" & vbCrLf)
      sMSBuild.Append("      PublishDir=$(PublishDir);" & vbCrLf)
      sMSBuild.Append("      PublishUrl=$(InstallUrl);" & vbCrLf)
      sMSBuild.Append("      SignAssembly=true;" & vbCrLf)
      sMSBuild.Append("      DelaySign=false;" & vbCrLf)
      sMSBuild.Append("      AssemblyOriginatorKeyFile=$(KeyFileLocation);" & vbCrLf)
      sMSBuild.Append("      ManifestKeyFile=($KeyFileLocation);" & vbCrLf)
      ' If you change the PFX file you must change the Thumbprint below. This comes from the .vbproj file if you manually
      ' use the PFX file - when seen in a text editor.
      sMSBuild.Append("      ManifestCertificateThumbprint=84489FD07E578BBA71CC67E9EB4F49E1B4EE740A;" & vbCrLf)
      sMSBuild.Append("      SignManifests=true;" & vbCrLf)
      sMSBuild.Append("      ApplicationVersion=$(VersionNumber);" & vbCrLf)
      sMSBuild.Append("      BootstrapperEnabled=true;" & vbCrLf)
      sMSBuild.Append("      UpdateEnabled=true;" & vbCrLf)
      sMSBuild.Append("      UpdateInterval=0;" & vbCrLf)
      sMSBuild.Append("      AssemblyName=$(AssemblyName);" & vbCrLf)
      sMSBuild.Append("      SolutionID=$(SolutionId);" & vbCrLf)
      sMSBuild.Append("      IsWebBootstrapper=True""" & vbCrLf)
      sMSBuild.Append("    ContinueOnError = ""false""" & vbCrLf)
      sMSBuild.Append("    Targets=""Publish"" />" & vbCrLf)
      sMSBuild.Append("</Target>" & vbCrLf)
      sMSBuild.Append("</Project>" & vbCrLf)

      oStreamWriter = New System.IO.StreamWriter(sBuildFile, False)
      oStreamWriter.Write(sMSBuild.ToString)
      oStreamWriter.Close()
      oStreamWriter = Nothing

Running the compile in code :-

      Dim sBuildLogFileName As String = "MSBuild_VSTO_" & Now.ToString("yyyyMMdd_hhmmss") & ".log"

      ' Run the build
      Dim oProcessInfo As System.Diagnostics.ProcessStartInfo = New ProcessStartInfo("C:\WINDOWS\Microsoft.NET\Framework\v3.5\MSBuild.exe", """" & sBuildFile & """ /clp:errorsonly /l:FileLogger,Microsoft.Build.Engine;logfile=" & sBuildLogFileName & " /filelogger")
      oProcessInfo.WindowStyle = ProcessWindowStyle.Hidden
      Dim oProcess = Process.Start(oProcessInfo)
      oProcess.WaitForExit()

      If oProcess.ExitCode <> 0 Then
        'Error
        Throw New Exception("Build failed for solution " & SolutionFile.FullName & ". See log file " & sBuildLogFileName)
      End If


Notes:
Debugging - VSTO and ClickOnce is 'fun' to debug. Below are some suggestions :-
1. Run the .vsto deployment file to install the package first. Do this directly by copying the file and then clicking on the link. If you get a security warning you know that Office is going to silently discard the VSTO, so troubleshoot this first.
2. In add/remove programs you can see the VSTO, so check it exists. Remove it manually between tests.
3. Initially run the .docx or .dotx locally from the C:\ of the test PC. This gets around any security issues.

Passing query string / parameters via URL - We required to deliver some parameters to the VSTO. A really simple way of doing this is by passing Me.Fullname to the below function e.g.

  Private Sub ThisDocument_Startup(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Startup
    ...
GetParameterIntFromQueryString("UserId", Me.Fullname)
   ...
  End Sub


  Public Shared Function GetParameterIntFromQueryString(Byval ParameterName as String, ByVal Location As String) As Integer
    ' Retrieve the ParameterName from the QueryString used to launch the document. Pass to this function me.Fullname
    Dim oURI As UriBuilder = New UriBuilder(Location)
    Dim sQueryString = oURI.Query.ToString

    If sQueryString = vbNullString Then
      Return 0
    Else
      Dim NameValueTable As New Collections.Specialized.NameValueCollection()
      NameValueTable = System.Web.HttpUtility.ParseQueryString(sQueryString)
      If IsNumeric(NameValueTable(ParameterName)) Then
        Return CInt(NameValueTable(ParameterName))
      Else
        Return 0
      End If
    End If
  End Function


e.g. Opening the VSTO from a website with http://myvstoserver/VSTO/UserReport.docx?UserId=123 would return 123 from the above function.

I would strongly suggest to prompt for the parameters if not supplied (use Inputbox) so it can be run in debug mode, or your users can run off website.

VSTO run once from .docx - We required our VSTO to run once and then not again. Unfortunately the original developer created the VSTO as a Word Document (.docx) which meant once the code the Word Document that was saved continue to embed in it the VSTO relationship, so when the user opened it again it would re-run wiping any manual changes they made.. The work around is either to change the project to a Document Template (.dotx), or in the ThisDocument_Startup at the end of the Finally block place the command Me.RemoveCustomization() - which strips the relationship between the document and VSTO.

Enjoy!

*************************************************
** Legal **
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

0 Comments:

Post a Comment

<< Home