Click here to Skip to main content
15,867,594 members
Articles / Programming Languages / Visual Basic
Article

Certification by Example

Rate me:
Please Sign up or sign in to vote.
4.93/5 (95 votes)
8 Jan 2008CPOL55 min read 184.8K   714   233   31
How to prepare a .NET application to obtain the Certified for Windows Vista logo, including the source code (Visual Studio 2005 solution) of a simple but complete application
Killer Application about box

Introduction

If you plan to obtain the Certified for Windows Vista logo for a .NET application, this article will be useful for you. Here you will find a simple but complete (certifiable) VB.NET + C# desktop application, including the full source code of both the application and the installer, all packaged in a Visual Studio 2005 solution.

Background

Some time ago, I was assigned with a task that seemed daunting to me: to obtain the Certified for Windows Vista logo for one of our applications. All I had to achieve this was Visual Studio, the source code of the application, and an Internet connection. Well, and the necessary funding when required.

The work has been finished and the application is now certified. While "daunting" is not actually the right word to describe the process, it was not at all easy. The problem was the lack of information provided by Microsoft; I'm not referring to all the administratrivia (registering at Winqual, obtaining a digital certificate for your organization, asking for waivers, and the like; all of this is pretty well explained in the Innovate on Windows Vista and Winqual pages), but to the pure technical part. Microsoft throws at your face the test cases document and... good luck, you are alone.

For example, take test case 25: Verify the application properly handles files in use during install. You open Orca, browse the installer you have created for your application and... ooops, no trace of the MsiRMFilesInUse dialog. The installer created with Visual Studio does not create it. What to do now?

Luckily, we have the Internet, we have search engines, and we have quite a few people who already fought the certification process. For example, the solution for the MsiRMFilesInUse issue is in this forum post. Ok ok, I said that Microsoft does not provide any help and now I post a link to an MSDN blog. I meant that Microsoft should have given this information in the first place, in a more complete test case document or in a separate FAQ.

Well, enough Microsoft Good/Evil discussion. The fact is that after I finished the certification work, I thought that it would be a good idea to share the knowledge I have obtained during the whole process in order to make life a bit easier for other people having the same task assigned. Who knows, maybe "you" could save "my" life tomorrow, so I want you to be happy with me by then.

Before You Go Ahead

First of all, I assume that you have already gotten your feet wet with the certification process. I mean, I assume that you are aware that in order to be certified, an application must pass a number of test cases as described in the test cases document provided by Microsoft (download it from the Innovate on Windows Vista page), that you need to purchase an organizational digital certificate, that you need to register your application for error reporting at Winqual, and that once your application is ready, you need to submit it to a testing authority. Except for the test cases, I'll not explain the details of the whole process here.

Second, I assume that your application is structurally similar to the one I certified. "Structurally similar" is a nice buzzword that I have invented right now to say that your application should be as follows:

  • Fully managed application (.NET assemblies only, no unmanaged code).
  • 32-bit application.
  • Only a desktop application is installed. Neither drivers nor services are installed.
  • Standalone application. Network connectivity is not required.
  • Installation is always per machine. Per user install is not supported.
  • Application data is common to all users. Per user data is not generated.
  • The application does not launch automatically after installation.
  • The application does not restart after a shutdown.
  • Concurrent user sessions are not supported.
  • Remote desktop (Terminal Server) execution is not supported.

What I will explain here applies to applications that have the above features. This is not to say that if your application differs somewhere from mine, you must stop reading right now. It is only to say that you must then be extra careful, since some of the things I will say here might not be true for you and/or you would need to search for extra information somewhere else. For example, if your application supports concurrent user sessions, you must ensure that sounds from one session are not heard in another one. I'll not cover this issue here, as I didn't need to address it.

The Big Flashing Red Letters Disclaimer

This is common sense, but anyway, here goes: whatever you read here, you MUST test your application against all applicable test cases before submitting it to the testing authority. I'll not accept any complaint of the type, "I wasted $1000 on a failed certification because you said XXX, but in my application it turned to be ZZZ!" I'm human and therefore I commit mistakes, not to mention that I don't know absolutely everything about Visual Studio and .NET applications. I want to help you, but I'm not God.

So, What Will We Do?

We will dissect an application that I have created especially for this article. The application name is Killer Application and is developed by the fictitious company Capsule Corporation. The source code, in the form of a Visual Studio 2005 solution, is available for download at the top of this page.

What Killer Application does is, essentially, nothing useful (it consists of a parent MDI form with a couple of menus that show some data and allow you to create and read a text file). However, it will pass all the test cases and could obtain the Certified for Windows Vista logo; that is, if you are Bill Gates and have a $1000 note in your pocket ready to spend on anything. I have created it so that it mimics the basic structure of the real application that I prepared for certification. For this reason, there are VB.NET and C# projects mixed.

After you have read this article, you can use the Killer Application solution as a skeleton for creating your own application, or you can just grab some source code snippets, or you can simply get some ideas about how to do things and do all your code from scratch; whatever best fits your needs. It could even happen that you find a better way to do things (really!). In this case, it would be nice if you could drop a comment explaining your discoveries.

Setup Your Environment

Let's start to move by preparing the environment needed to compile Killer Application (NOT the testing environment: you do not need Windows Vista for the development process; in fact, I use Windows XP). First you need, obviously, Visual Studio 2005. I guess that Visual Studio 2008 would also work via the appropriate solution conversion.

Second, I'll assume that you have obtained your organizational certificate. If not, you will not be able to compile the solution unless you modify the solution post-build events in Visual Studio (more details on this later). I'll assume that the credentials and private key file names are, respectively, capsulecred.spc and capsulekey.pvk, but of course you can use whatever names you like.

Third, create a directory named vistatools at the root of your home drive (that is, C:\vistatools). This is where we will put some extra files needed when compiling the application.

Fourth, copy the following files in the vistatools directory:

  • mt.exe, the Microsoft Manifest Tool. It is part of the Visual Studio 2005 SDK and you should have it at the C:\Program files\Microsoft Visual Studio 8\SDK\v2.0\Bin directory. Otherwise, download and install the SDK.
  • MsiTran.exe, the MSI Transform Tool. It is part of the Windows Vista SDK.
  • signtool.exe, the code signing utility. It is part of both the Visual Studio 2005 SDK and the Windows Vista SDK.

(For sure you can find these files alone for download by searching on Internet, but I prefer to point to the "official" source.)

Fifth, you need to create a digital certificate file for your organization from the credentials file and the private key file. You need to do this only once for all your applications. Here are the steps:

  1. Download, uncompress and install the pvkimprt utility.
  2. Copy the capsulecred.spc and capsulekey.pvk files to the vistatools directory.
  3. Open a command prompt, go to the vistatools directory and execute the following: pvkimprt -PFX capsulecred.spc capsulekey.pvk

A GUI will then appear asking for the private key password. Later, it will ask you if you want to export the private key. Say yes. Select default parameters on the next screen (PKCS #12 format, allow secure protection) and enter a new password for the generated certificate file (I'll assume that you enter kaitokun here). Finally, when asked for the name of the certificate file, browse to the vistatools directory and select an appropriate name (I'll assume capsulekey.pfx).

When you are done, you can delete the capsulecred.spc and capsulekey.pvk keys from the vistatools directory if you want (but ensure that you have stored them somewhere else, of course!). The contents of the directory should be: mt.exe, MsiTran.exe, signtool.exe and capsulekey.pfx. With all of this, you are ready to compile Killer Application.

Application Structure

Here we will take an overview to Killer Application: what it is composed of and what it does. Later, we'll dive into the details of the source code and the project settings. The Killer Application solution consists of four projects:

  • Killer Application: the main application assembly, it is a VB.NET executable containing all the GUI and the (very simple) application logic .
  • KillerApplication.Support: a C# DLL containing just a custom control. I have included it to mimic the usual structure of most real world applications (one main executable file plus one or more supporting DLL files).
  • KillerApplication.Install: another C# DLL, this one contains a class with custom actions for the installer. We'll see later why this code needs to be in its own assembly.
  • Killer Application installer: the installer project that will generate the installer (MSI) file for Killer Application.

When you run the application, you will see an MDI form with a menu containing three main entries. This menu allows you to perform some simple actions that will help you in exercising the test cases (or at least that was the intent):

  • The File menu contains three entries:
    • Open will open a form with controls that allow you to read and display the contents of a text file placed anywhere in the disk. Any exception will be caught and its information displayed. There are buttons to populate the file path text box with three "interesting" locations: the Windows directory and the logouser1 and logouser2 personal directories. Use this form to exercise test case 2.
    • Save will open a form with controls that allow you to create a text file anywhere in the disk. Again, exceptions will be caught and there are buttons to populate the file path text box with the Windows, logouser1 and logouser2 directories. Use this form to exercise test cases 2 and 3.
    • Exit will -- you guessed it -- terminate the application execution.
  • The Do stuff menu contains five entries:
    • Show cool control will open a form that simply contains the custom control defined in the support DLL.
    • View photo will open a form that simply shows photography. The photography file is included in the project as a contained file. This form, as well as the following two forms, indirectly exercise test case 15.
    • View text is similar to the view photo form, but this time the contents of a text file are displayed instead of photography.
    • View data will work only if you have SQL Server 2005 Express installed on the machine that runs the application. It will open a form that shows the data contained on a small SQL Server database, included in the project as a contained file. There is a nice trick with the connection string that we'll see later.
    • Crash generates a null reference exception. This is a quick way to exercise test case 32.
  • The Help menu contains only one menu entry:
    • About will display a simple About box.

That's all. It's not very much, but enough to exercise all the applicable test cases. Now let's go to the details.

Setting Up the Solution

There are three main focus areas to look at when performing the application dissection: the solution/project settings, the source code and the installer project. In this section, we'll see the details of the first one. I'll do this by enumerating the steps that should be followed if the solution were to be created from scratch. All of this was, of course, done by me when creating the Killer Application solution. I believe that this information will be useful for you even if you are preparing for certification of an already existing solution.

Note that, of course, you can use whatever solution and project names you like, and create more or less projects depending on your needs.

1. Create the Project

Open Visual Studio and create a new project of type Other Project Types -> Visual Studio Solutions -> Blank Solution. Name it Killer Application. Add three new projects to the solution: a VB Windows Forms Application named Killer Application, a C# Class Library Project named Capsule.KillerApplication.Support, and another C# Class Library Project named Capsule.KillerApplication.Install. Remove the default Form1 and Class1 items that Visual Studio adds to the projects by default. Don't create the installer project at this moment. Add a reference to the support class library in the main VB.NET project. Add a new class to the main VB.NET project and name it Program. Add the following placeholder code to the class:

VB.NET
<STAThread()> _
Public Shared Sub Main(ByVal args() As String)
End Sub

Open the properties dialog of the main VB.NET project and perform the following changes in the Application tab:

  • Change the root namespace to Capsule.KillerApplication.
  • Uncheck the Enable application framework box.
  • Change the startup object to Program.

Note that if your main application project is a C# project, the Program class with the placeholder Main method is created automatically. You still need to change the root namespace anyway. You may ask, "Why do we bother with a Main method instead of placing the startup code in the main form load event?" As we'll see when we take a look to the application source code, we need to check a number of conditions before our application starts, and some of them may even prevent the application from running. So, we need to be able to execute code before any form is created.

2. Setup Metadata

Now we'll fill in some information about the application in the AssemblyInfo files of each project. In C# projects, it is in the Properties folder. In the VB.NET project, it is in the My Project folder, but in this case you first need to activate the Show All Files icon in the solution explorer window. For the main application assembly, that's the data we will set up:

XML
<Assembly: AssemblyTitle("Killer Application")>
<Assembly: AssemblyDescription( _
   "Simple example of an application that could obtain the
        ""Certified for Windows Vista"" logo.")>
<Assembly: AssemblyCompany("Capsule Corporation")>
<Assembly: AssemblyProduct("Killer Application")>
<Assembly: AssemblyCopyright("© Capsule Corporation 2007, 2008")> 

For the support DLL, the data will be:

C#
[assembly: AssemblyTitle("Capsule.KillerApplication.Support")]
[assembly: AssemblyDescription("Support DLL for Killer Application")]
[assembly: AssemblyCompany("Capsule Corporation")]
[assembly: AssemblyProduct("Capsule.KillerApplication.Support")]
[assembly: AssemblyCopyright("© Capsule Corporation 2007, 2008")]

...and very similar for the install custom actions DLL; just change the assembly title and description. Setting up metadata is not strictly necessary, but it is good practice. Killer Application uses this data in the about box.

3. Add the Manifest File(s)

Test case 1 states that all application executable files must "contain an embedded manifest that define its execution level." With Visual Studio 2005, there is no direct way for including manifest files in assemblies. Visual Studio 2008 makes this task easier, by the way, but we'll assume we all are poor Visual Studio 2005 users here. Luckily, there is an indirect way to do this. In the application main assembly (the VB.NET executable), add a new text file and name it Killer Application.exe.manifest. The file name must be the same as the assembly name, plus .exe.manifest. Ensure that in the properties page for the file, the Build Action is set to None. Then open the file and paste the following inside:

XML
<?xml version="1.0" encoding="utf-8" ?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level="asInvoker" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>

Note that if our application had more executable (*.exe) projects, we would have to repeat this step for each one. The contents of the manifest file is always the same; only the file name itself changes. This manifest file will be processed by the project post-build events, as we'll see right now.

4. Setup the Post-build Events

Remember the vistatools directory we created once upon a time? Well, it's now time to use it. We will set the post-build events of the projects so that some nice things are done with the generated assemblies. In the properties window of the VB.NET executable project, select the Compile tab, click the Build events button, and in the Post-build event command line box paste the following:

%HOMEDRIVE%\vistatools\Mt.exe -manifest "$(ProjectDir)$(TargetFileName).manifest"
     -outputresource:"$(ProjectDir)obj\$(ConfigurationName)\$(TargetFileName);1"
%HOMEDRIVE%\vistatools\signtool.exe sign
    /f %HOMEDRIVE%\vistatools\capsulekey.pfx /p kaitokun /v
         /t http://timestamp.verisign.com/scripts/timstamp.dll
    "$(ProjectDir)obj\$(ConfigurationName)\$(TargetFileName)"

Note: actually, there are only two commands to execute. I have divided them in separate lines each to improve readability. In the post-build event window of Visual Studio, the commands must be in one single line each.

In the first command, we use the manifest tool to embed the manifest file in the resulting assembly. The second command will sign the assembly, thus fulfilling test case 5 ("verify application installed executables and files are signed"). Note that you will have to change the capsulekey.pfx file name and the kaitokun password for your real values. If you don't have an organizational certificate yet, but still want to compile the application, just remove this line or, better, disable it by prepending a rem command to it.

Note that we target the file that is created in the $(ProjectDir)obj\$(ConfigurationName) directory, instead of the file created in the target generation directory (usually bin\ConfigName). That's because, when generating the installer, the executable file to be packed will actually be taken from the obj directory. That's the file we want to be "manifested" and signed, rather than the one used for debugging and testing in the development machine.

Now let's go with the support assemblies. For both KillerApplication.Support and KillerApplication.Install, open the properties window, select the Build Events tab and, in the Post-build event command line box, paste the following (note again that it is a single command divided into four lines):

%HOMEDRIVE%\vistatools\signtool.exe sign /f
    %HOMEDRIVE%\vistatools\capsulekey.pfx /p kaitokun /v
    /t http://timestamp.verisign.com/scripts/timstamp.dll
    "$(ProjectDir)obj\$(ConfigurationName)\$(TargetFileName)"

Yes, it is exactly the same as in the case of the executable file, but without the manifest stuff. Again, if you don't have an organizational certificate yet, remove or comment the command temporarily. The first version of post-build events (two commands) must be set on all projects that generate an EXE file. The second version (one command) must be set on all projects that generate a DLL file.

5. Add your Code and Data

That's all you need regarding project setup (except for the installer project, which we will dissect later). Now you need to add the application code and the auxiliary data (images, texts, datasets, whatever) to the projects. You can use whatever code and data you need... except, of course, that you need to do a few special things, as we will see right now.

Where is My Data?

Before we plunge into the intricacies of the source code, we'll take a look to another subject that also needs attention: the application data. That is, all application files that are part of the project but are not code.

Visual Studio lets you to add these kinds of files to your projects. Just right-click on the project and select either Add new item or Add existing item. Then in the properties page for the item, make sure that Build Action is set to Content. Killer Application has three files of this kind inside the data folder of the main project: one photography, one text file and one database file, as shown in the following image:

Killer Application content files

The question is: where do these files go when the application is generated? The answer is that it depends on how you generate the application:

  • If you directly build or run the application from within Visual Studio, the content files will be copied in the same directory as the executable file is generated (that is, the bin\ConfigName directory in the project directory).
  • When creating an installer, as we'll see later, you can instruct the compiler to pack all the content files altogether and, on install time, to copy them on any directory of your choice in the destination machine.

In both cases, the original directory structure is preserved. This means that, in the case of Killer Application, when generating the solution there will be a bin\Release\Killer Application.exe file together with a bin\Release\Data\Texts\Agreement.txt, as well as two more files (the photo and the database) within their original relative paths:

Killer Application folder generated by VS

It is now the moment to take a look at test case 15: "verify application installs to the correct folders by default." For application-wide data, the correct folder is the one pointed by the %ALLUSERSPROFILE% variable (also called CommonApplicationData and CommonAppDataFolder in .NETesque). This is usually C:\Documents and Settings\All Users in Windows XP and c:\ProgramData in Windows Vista. If you cheat a little and scroll down (or better, look into the Killer Application solution), you will see that the Killer Application installer creates a directory named, of course, Killer Application, and puts all the project content files here:

Killer Application folder in installer project

So with all of this in mind, you probably think that it would be nice to use a single code base to access the application data on all cases, wherever the data is placed. And you are right. Here is how I have achieved it in Killer Application:

  1. Create a static string variable named DataDirectory in the Program class.
  2. At boot time, check if there is a directory named Data in the application executable directory. If so, set DataDirectory to the path of this directory. Otherwise, set DataDirectory to %ALLUSERSPROFILE%\Killer Application\Data.
  3. When you need to access application data from within the code, obtain the proper path by combining the value of Program.DataDirectory with the relative path of the file, without the data part. For example: Path.Combine(Program.DataDirectory, "Texts\Agreement.txt").

There are a couple of extra tricks here, but we'll see them when looking at the source code.

The Source (Finally)

We are now really ready to look at the source code of Killer Application. We'll look at the boot sequence, then we'll see what the menu entries on the Killer Application main window do, and finally we'll examine the custom actions contained in the installer support project.

Let's Boot

We'll start the source code dissection at the boot sequence, that is, the code that Killer Application executes at startup. Open the Program.cs file in Visual Studio, look for the Main method, and here is what you will find:

1. Wrap Main in a Try-Catch-log Block

You may get surprised when you see that the M<code>ain method is as follows:

VB.NET
<STAThread()> _
Public Shared Sub Main(ByVal args() As String)
    Try
        _Main(args)
    Catch ex As Exception
        Dim text As String = _
            String.Format("Unexpected exception in Killer Application:{0}({1}){0}{2}", _
            Environment.NewLine, ex.GetType().Name, ex.Message)
        EventLog.WriteEntry("Application Error", text, EventLogEntryType.Error, 1000)
        Throw
    End Try
End Sub

Test case 32 says: "verify that the application only handles exceptions that are known and expected." Then, why are we doing just the opposite here? Shouldn't we just let alone unexpected exceptions here?

The problem is what you can read in the "verification" part of the test case: "There must be both an Error message with 'Source' listed as Application Error and an Information message with 'Source' listed as Windows Error Reporting for each executable above in order to pass this test case." It happens that the information message is generated, but there is no trace of the error message in the event log. Hence, we must generate it by hand, and that's exactly what this weird piece of code does. After the logging is done, the Throw statement rethrows the exception unmodified, so everything is OK and we pass the test case.

Note that at the end of the catch block, we must use Throw and not Throw ex. The former will rethrow the original exception with the original call stack preserved, while the later will generate a new exception that will cause the call stack to be lost (and will also cause test case 32 to fail, I admit I have no idea why).

The rest of the initialization code is inside the _Main method.

2. Set the Unhandled Exception Mode

Before doing anything else, the following piece of code is required:

C#
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException)

Without this, you will receive this ugly error window instead of the fancy WER window in case of unhandled exception:

Killer Application not valid crash window

And, you guessed it, test case 32 will fail if this happens.

3. Check Remote Desktop Execution

Test case 9 says: "verify application launches and executes properly using Remote Desktop." But as explained earlier, Killer Application does not support remote desktop execution. How can the test case pass then? The answer is in the small print. There is a note in the test case description that says: "if application does not support Remote Desktop, it must pop-up a message indicating this to the User and write a message to the Windows NT Event Log in order to pass this test case." And here is where our life gets saved:

VB.NET
If SystemInformation.TerminalServerSession OrElse _
        Command.ToLower().Contains("failremote") Then
    MessageBox.Show("Terminal Server execution is not allowed.", _
        "Killer Application", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
    Dim evlog As New EventLog_
        ("Application", Environment.MachineName, "Killer Application")
    evlog.WriteEntry("Terminal Server execution was attempted. _
            User was notified and application terminated.", _
         EventLogEntryType.Information)
    Return
End If

The failremote command line switch is a trick I use to exercise this functionality without actually having to set up a Terminal Server connection. It can be removed in real applications. By the way, credit for this piece of code goes to mister Amitava.

4. Check for Existing Application Instances

Test case 8 says: "verify application launches and executes properly using Fast User Switching." Again, this is a feature not supported by Killer Application. And again, the small print saves us: "if application does not support concurrent user sessions, it must pop-up a message indicating this to the User and write a message to the Windows NT Event Log in order to pass this test case."

So we'll do something similar to the case of the remote desktop execution, but a little more complex. We will first check if Killer Application is already being run by another user; if so, we show an error message, we create the appropriate event log, and we terminate. If not, we check if Killer Application is already being run by ourselves; if so, we activate the main window of the already running instance.

We will need a little of "advanced" code to achieve this. In the support project you can find the AlreadyRunningChecker class (it should be in the main project, but I had the code in C# and I was too lazy to convert it to VB). This class has two static methods: ActivateProcessMainWindow, that will activate the main window of a process given is process ID (by using unmanaged APIs); and GetSameNameProcess, that will return the process ID of an already existing instance of Killer Application (by using instrumentation). With the help of this class, we can properly check the presence of other application instances in the following way:

VB.NET
Dim pid As Long = AlreadyRunningChecker.GetSameNameProcess(False)
If pid <> 0 OrElse Command.ToLower().Contains("failmultiuser") Then
    MessageBox.Show("This application is already being run by another user.", _
        "Killer Application", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
    Dim evlog As New EventLog_
        ("Application", Environment.MachineName, "Killer Application")
    evlog.WriteEntry( _
        "Multiple user execution was attempted. _
        User was notified and application terminated.", _
         EventLogEntryType.Information)
    Return
End If

pid = AlreadyRunningChecker.GetSameNameProcess(True)
If pid <> 0 Then
    AlreadyRunningChecker.ActivateProcessMainWindow(pid)
    Return
End If

I got most of the code of the AlreadyRunningChecker class from somewhere on Internet but I can't remember where. Sorry.

5. Setup the Data Directory Path

We have spoken about this before: the application data files (all application files that are not code) are placed in different locations depending on whether the application is run from within Visual Studio or installed using the generated MSI file. We will use the global variable Program.DataDirectory to store the actual path of the data files. The following code will set up the contents of this variable:

VB.NET
DataDirectory = ConfigurationManager.AppSettings("DataDirectory")
If String.IsNullOrEmpty(DataDirectory) Then
    DataDirectory = Path.Combine(Application.StartupPath, "Data")
    If Not Directory.Exists(DataDirectory) Then
        DataDirectory = Path.Combine(Environment.GetFolderPath( _
        Environment.SpecialFolder.CommonApplicationData), "Killer Application\Data\")
    End If
Else
    DataDirectory = Path.Combine(Application.StartupPath, DataDirectory)
End If
If Not DataDirectory.EndsWith("\") Then DataDirectory += "\"

What this code does is the following:

  1. Check if the key DataDirectory exists in the appSettings section in the configuration file and its value is not empty. If so, set DataDirectory to its value. (A relative path refers to the application executable path)
  2. Otherwise, check if a Data directory exists in the same directory of the application executable. If so, set DataDirectory to the path of this directory.
  3. Otherwise, set DataDirectory to the common application data folder plus Killer Application\Data.

2 will apply when the application is run or built from within Visual Studio, 3 will apply when the application is installed, and 1 is reserved for special purposes where you want to use a different set of data with an already installed application (for example, for debugging on a production machine). There is an extra thing to do after the DataDirectory field has been set. One of the data files of Killer Application is a database file, accessed via a typed dataset. If you look at the settings file, you will see that the connection string used is as follows:

C#
Data Source=.\SQLEXPRESS;AttachDbFilename=
    "|DataDirectory|Database\KillerApplicationDatabase.mdf";
Integrated Security=True;User Instance=True

This connection string assumes SQL Server 2005 Express is installed locally. But look at the AttachDbFilename key: it points to the directory where application data files are placed. Can this value be changed? Yes, and that's what we do right now:

C#
AppDomain.CurrentDomain.SetData("DataDirectory", DataDirectory)

The DataDirectory value that the SQL Server engine uses is an application domain wide setting that can be set with the SetData method of the AppDomain class. By default, this setting is not set (i.e. its value is null), so the SQL Server engine assumes the application executable directory as its value. Since we can have the database file in a number of different places, we need to appropriately set this setting so that SQL Server can find the database file.

Moreover, if you hate global variables, you could directly use this application domain setting instead of the Program.DataDirectory variable to obtain the data files path. Simply use this code to obtain its value: AppDomain.CurrentDomain.GetData("DataDirectory").

There is however a problem with this approach. With this connection string, whenever you try to edit a typed dataset (to add a new TableAdapter, for example) Visual Studio will complain that it can't find the database file. That's because Visual Studio always assumes the default value for the DataDirectory setting, therefore, while editing the dataset you need to do the following modification in the connection string:

C#
AttachDbFilename="|DataDirectory|Data\Database\KillerApplicationDatabase.mdf";

I'm sure there must be a better solution, but I got used to this one so that I kept this schema.

6. Log the Application Execution

This is actually not necessary but I do it as an example of modifying a file in the data directory (which theoretically is an issue when uninstalling the application, we'll see later why and how it can be solved). Simply, a line of text containing the current time and user name is appended to a file named log.txt in the root of the data directory (the file is not part of the solution nor the install package, it is created the first time it is written to):

VB.NET
Dim log As String = String.Format("Application run by {0} on {1}{2}", _
    Environment.UserName, DateTime.Now, Environment.NewLine)
File.AppendAllText(Path.Combine(DataDirectory, "log.txt"), log)

7. Run the application

Nothing unusual here, we just pass control to the application main window:

C#
Application.EnableVisualStyles()
Application.SetCompatibleTextRenderingDefault(False)
Application.Run(New FormMain())

Opening and Saving Files

Test case 2 says "verify Least-Privilege Users cannot modify other users documents or files", and test case 3 says "verify Least-Privilege user is not able to save files to Windows System directory". The good news is that you don't need to do anything special to fulfill these test cases, as the operating system will grant or deny access to files and folders as appropriate. You must simply be sure to properly control the exceptions that will be thrown when trying to read or write where you (the user, actually) are not authorized to.

The File menu in Killer Application will help you with these test cases. It contains two entries, Open and Save, that allow to create and open a text file. Any generated exception will be caught and its associated information displayed.

The save dialog window looks like this:

Killer Application save file dialog

There is a text box where you must enter the path where a text file will be created. Three buttons allow you to populate this text box with three "interesting" locations: the Windows system directory, and the home directories for the logouser1 and logouser2 users (these users must be created for testing the application, as explained in the test cases specification document). You can also select any directory by using the directory tree that appears when clicking the "..." button. Finally, the Create file button will create a file named KILLERAPP.TXT in the selected directory, containing a fixed text.

Note that we don't use a SaveFileDialog control, which would make life easier for us. This is on purpose, since the file save and open dialog controls do not let the user to even browse any unauthorized directory, thus making these test cases trivial to fulfill. The real challenge (well, not quite a challenge actually) is to pass the test cases when dealing with files by code, and that's why this custom save dialog is so ugly.

This is the code attached to the Create file button click event:

VB.NET
Try
    File.WriteAllText(Path.Combine(txtPath.Text, "KILLERAPP.TXT"), _
        "Congratulations! You have successfully created a text file _
            with Killer Application.")
    MessageBox.Show(Me.MdiParent, "Text file created successfully.", _
        "Killer Application", MessageBoxButtons.OK, MessageBoxIcon.Information)

Catch ex As UnauthorizedAccessException
    MessageBox.Show(Me.MdiParent, "Sorry, you don't have the necessary _
            permissions for creating a file here.", _
        "Killer Application", MessageBoxButtons.OK, MessageBoxIcon.Warning)

Catch ex As Exception
    Dim text As String = String.Format("Ooops. Unexpected error:{0}{0}({1}){0}{2}", _
        Environment.NewLine, ex.GetType().Name, ex.Message)
    MessageBox.Show(Me.MdiParent, text, "Killer Application", _
        MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try

When trying to write a file in an unauthorized place, we will get a UnauthorizedAccessException that we must handle appropriately (in this case, we just translate it to a human readable error message). In this simple application, we blindly catch any other possible exception and simply show it, in real applications we would probably do a more accurate exception handling.

The brother of the save dialog is the open dialog, whose details I'll not show here because it is very similar to the save dialog. It just adds one more textbox to enter the name of the file to open (which can be populated with KILLERAPP.TXT) and another one to show the file contents; as for the code, the File.WriteAllText invocation is changed to File.ReadAllText.

With all of this, these are the steps you can follow to exercise test cases 2 and 3:

  1. Log in Windows as logouser1, run Killer Application and select File -> Save.
  2. Try to create a file in the Windows directory and ensure that you can't.
  3. Same as above for the logouser2 directory.
  4. Try to create a file in the logouser1 directory, you should be able to do it without errors.
  5. Close the Save dialog, select File -> Open and ensure that you can open the file you have created (named KILLERAPP.TXT).
  6. Quit Killer Application, log in Windows as logouser2, run Killer Application and select File -> Open.
  7. Ensure that you can't open the KILLERAPP.TXT file from the logouser1 directory.
  8. Try to create a file in the logouser1 directory, you should get an error.

A note of caution here. As explained here, you may encounter that you actually can write to the Windows directory. If this happens, it means that your executable file does not have a valid manifest. Check if you forgot to include the manifest file in the project and/or to appropriately set the project post-build event to include the manifest file in the assembly with mt.exe.

Showing Data (and Crashing)

The Do stuff menu on Killer Application main window contains three entries that are related to handling data files: View photo, View text and View data.

All what was worth saying about the application data files has been said already: we have seen that test case 15 forces us to put all the application data files in a given directory when the application is installed, we have seen how to do this while at the same time having these data files at hand at develop+debug time, and we saw what to do when SQL Server 2005 Express database files are involved. These menu entries allow us to see these concepts working.

For example, look at FormViewPhoto. It loads the photography file with the following code:

VB.NET
Dim photoPath As String = Path.Combine(Program.DataDirectory, "Photos\KaitoCute.jpg")
pictureBox.Load(photoPath)

As for FormViewText, it loads the text file in this way:

VB.NET
Dim textPath As String = Path.Combine(Program.DataDirectory, "Texts\Agreement.txt")
textBox.Text = File.ReadAllText(textPath)

...and so on. If you had more data files, that's how to access them: take the file path in the project removing the initial "Data", combine it with the root data directory whose path is at Program.DataDirectory, and you are done. In the case of the SQL Server database, this is handled via the connection string and the DataDirectory application domain setting.

There is an extra menu entry, Crash, that will just force a null reference exception. This will allow you to easily exercise test case 32 ("verify that the application only handles exceptions that are known and expected"), but this is only for convenience and it does not exonerate you from actually using threadhijacker to make your application crash.

Custom Actions for the Installer

Last but not least within the source code dissection, we will take a look at the KillerApplication.Install project. This project contains one single class (plus a couple of classes with auxiliary code), Installer, that contains custom actions for the installer.

Custom actions are pieces of code that execute when the application is installed and/or uninstalled. They are useful to perform tasks outside of the standard functionality provided by the Windows Installer technology (which is basically to copy files, to create registry entries, and to create program menu and desktop shortcuts). Custom actions are defined inside a .NET class which must have the RunInstaller attribute, and must be added to the custom action editor on the installer project. We'll see more on that later.

Before going ahead, let's answer one question we made to ourselves some time ago: why must these custom actions be in a separate assembly? Why can't they be part of the main assembly or the support assembly? The answer is because this would cause the application to not uninstall cleanly.

More precisely, what will happen if you put custom actions inside one of the assemblies of the application itself? Something very ugly: after the application is uninstalled, you will see that the folder where the application was installed still exists, and that it contains one single file: yes, the assembly containing the custom actions. And while no test case says explicitly that the application must perform a clean uninstall (although one could infer it from test case 23), it is common sense; no one wants it's application to leave garbage in the user hard disk after being uninstalled.

What is the solution for this problem? Easy: put the code for the custom actions in a separate assembly, and install this assembly anywhere but in the application folder. In my case, I chose the common files folder (we'll see later how to do this) and it worked just fine: custom actions are properly executed and the application is uninstalled cleanly.

I admit that I don't know very well why this happens this way and that I found this solution by trial and error. Suggestions about alternative ways will be welcome.

Having said that, let's see what custom actions we are using in our project and what they are for. Note that for convenience, the Installer class defines two text constants to store the application name, as well as one property that will tell us where the application is installed:

C#
private const string APPNAME="Killer Application";
private const string APPFILE="KILLER APPLICATION.EXE";

private string ApplicationDataPath
{
    get { return Path.Combine(Environment.GetFolderPath
            (Environment.SpecialFolder.CommonApplicationData), APPNAME); }
}

1. Install

First of all, we need a custom action that will be executed when the application is installed. At install time, we need to perform two tasks:

  1. Create an event source for our application in the Windows event log.
  2. Give normal users access to the application data files directory. This is necessary because the installer is executed with administrator privileges, and thus the created data files directory belongs to the administrator.

The piece of code that does this is as follows:

C#
public override void Install(System.Collections.IDictionary stateSaver)
{
    if(!EventLog.SourceExists(APPNAME))
    {
        EventLog.CreateEventSource(APPNAME, "Application");
    }

    string userGroupName=FindUserForSid.GetNormalUsersGroupName();
    AclManager manager=new AclManager(ApplicationDataPath, userGroupName, "F");
    manager.SetAcl();

    base.Install(stateSaver);
}

The FindUserForSid.GetNormalUsersGroupName invocation will obtain for us the name of the standard users groups, which is different depending on the language of Windows (for example, it is Users in English and Usuarios in Spanish). I took the code for this class from pinvoke.net (changing the administrators group SID for the users group SID of course). The AclManager class changes the directory permissions for the given user's group. I took it from Rick Strahl's blog.

2. Rollback

Test case 23 says: "verify the application rolls back the install and restores machine back to previous state." To achieve this, we need a rollback custom action that will undo any actions performed during installation. In this case, we only need to remove the event log source, since any installed files will be automatically removed by the standard installer code:

C#
public override void Rollback(System.Collections.IDictionary savedState)
{
    if(EventLog.SourceExists(APPNAME))
    {
        EventLog.DeleteEventSource(APPNAME);
    }
    base.Rollback(savedState);
}

3. Uninstall

Three actions are needed when our application is being uninstalled:

  1. Delete the data files directory.

    The data files directory is created at install time, therefore it should be automatically removed by the installer so we should not worry about it. Well, it happens that this is true only for the contained files that were originally created by the installer. If you create new files in this directory, these will remain after the application has been uninstalled. Killer Application creates indeed one new file in the data directory, to log the application execution (see the boot sequence), therefore we remove this directory by hand.

  2. Delete the event source.

    No mystery here; note that even if we remove the event source, the generated events will remain.

  3. Delete the application file in the prefetch directory.

    Windows XP and Vista have a directory named Prefetch that contains shortcuts to the most recently used applications, to shorten application startup time. These shortcuts can safely be removed by hand, and that's what we do with the prefetched file for Killer Application, thus achieving a completely clean uninstall.

    Here is the code that does all of this. Note that if for some reason the removal of the data directory or the prefetch file fails (this can happen if the involved folders are open in the explorer while the uninstallation takes place), the user is warned so that at least he can manually delete these files:

    C#
    protected override void OnAfterUninstall(System.Collections.IDictionary savedState)
    {
        //* Delete data files
    
        string path=ApplicationDataPath;
        if(Directory.Exists(path))
        {
            try
            {
                Directory.Delete(path, true);
            }
            catch(Exception ex)
            {
                MessageBox.Show(string.Format(
    @"Error when trying to delete the program data folder:
    ({0}) {1}
    
    Uninstall process will continue, but the folder will not be deleted.
    The folder path is:
    {2}", ex.GetType().Name, ex.Message, path), APPNAME + " uninstaller",
    MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }
        }
    
        //* Delete the event source
    
        if(EventLog.SourceExists(APPNAME))
        {
            EventLog.DeleteEventSource(APPNAME);
        }
    
        //* Delete file from Prefetch folder
    
        string prefetchPath=Path.Combine
            (Environment.ExpandEnvironmentVariables("%windir%"), "Prefetch");
    
        try
        {
            string[] files=Directory.GetFiles(prefetchPath, APPFILE+"*.*");
            foreach(string file in files)
            {
                File.Delete(file);
            }
        }
        catch
        {
            string prefetchDir=Path.Combine
                (Environment.ExpandEnvironmentVariables("%windir%"), "Prefetch");
            MessageBox.Show(@"Some "+APPNAME+" files may remain in the
                "+prefetchPath+" directory. " +
                "You can delete these files manually.",
                APPNAME + " uninstaller", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        }
    
        base.OnAfterUninstall(savedState);
    }

And with this, we have finished dissecting the Killer Application source code. Let's go to the installer project now.

The Installer

Preparing the application source code and tuning the application projects settings was only one part of the work we had to do on our way to the certification. There is a last important thing to do before our application is completely logo-able: to create and adjust an installer for our application.

In order to be certifiable, our application must use the Windows Installer technology to install (Click Once deployment is also accepted but we'll not discuss this possibility here). This means that we must generate an installer file in MSI format that will contain our application appropriately packed, including all the install logic (the GUI to show at install time, the custom actions, and the necessary metadata that will be generated in the Windows registry so that Windows will know how to properly uninstall the application).

The good news is that we can create such an installer with Visual Studio, but of course, we need to cheat a little if we want a completely test cases-proof installer. And how to achieve this is what we will see right now.

Step One: Create the Installer Project

The installer project is part of the application solution, but when explaining how to create the solution I didn't mention that on purpose, because I wanted the installer to have a section on its own in this article. So, this is what I did to create an installer project for Killer Application:

  1. With the Killer Application solution loaded, in the Visual Studio File menu, select Add -> New project.
  2. In the Add new project dialog, select Other Project Types -> Setup and Deployment -> Setup Project. In the project name text box, write Killer Application installer.
  3. Right click the new installer project in the solution explorer, select Properties. In the Configuration dropdown list select All Configurations, then in the Output file name text box, write bin\Killer Application 1.0.msi.

With this, you have created an empty installer project for our application. Now we need to make some initial adjustments.

Step Two: Adjust Project Properties

When the installer project is selected in the solution explorer, the properties window of the project will show us some configuration values that we can modify. Some of these have already appropriate values by default, others can be filled but can also be left blank, and lastly there are some values that must be properly set if we want things to work the right way. These values are the following ones:

  • InstallAllUsers must be set to True, since our application is always installed per machine, not per user.
  • Localization must be set to the appropriate value, that must match the Windows language in which your application is intended to be certified. For Killer Application, it is Spanish (Spain) as in the application that I helped to certify, in your application you must set it appropriately.
  • Product name: Change it to the application name, in this case, Killer Application.
  • Same for Title, in this case it is Killer Application 1.0.
  • Make sure that Version matches the application version number, in this case the default value 1.0.0 is OK.

There is an extra setting to change but it is not in the project properties window. You will need to open the user interface editor (it is an icon in the solution explorer folder), select the Installation Folder window, and in the properties window, set the InstallAllUsersVisible to False. That's the minimum to set up to make things work, but probably you would want also to set the value of other useful properties like Description, Manufacturer, ManufacturerUrl and SupportUrl. Once the application is installed, this information can be displayed in the Windows control panel, more precisely in the Add/Remove Programs window.

Step Three: Add the Files to Install

Our installer application is by now quite useless, since it does not install anything. We need to tell Visual Studio which files must be packed in the MSI files, and this is done via the File System Editor accessible from the solution explorer. Let's go then, hold your breath and:

  1. Remove the User's Desktop folder (unless you want to create an application icon in the user's desktop when the application is installed, I don't do it in Killer Application).
  2. Right click the Application Folder, select Add -> Project Output. Select primary output for Killer Application.
  3. Repeat the above step for the primary output for KillerApplication.Support.
  4. Right click the User's Program Menu, select Add -> Folder, name the new folder Killer Application.
  5. Return to the Application Folder, right click Primary output for Killer Application , select Create Shortcut. Name the shortcut Killer Application.
  6. Cut the shortcut you have created, and paste it into the User's Program Menu - Killer Application folder.
  7. Right click the file system root (labelled File System on Target Machine), select Add Special Folder - Common Files Folder.
  8. Right click the Common Files Folder, select Add -> Project Output. Select primary output for KillerApplication.Install.
  9. Right click the file system root (labelled File System on Target Machine), select Add Special Folder - Custom Folder. Name the new folder Killer Application.
  10. Select the custom folder you have created, open the properties window, set DefaultLocation to [CommonAppDataFolder]\Killer Application and Property to COMMONAPPDATAFOLDER.
  11. Right click the custom folder, select Add -> Project Output. Select content files for Killer Application.

Phew. Well, you can see the results of this piece of work in the Killer Application installer project itself. Just remember that we have seen already one part of how the file system editor should look like:

Killer Application folder in installer project

A final note on this point. The first time you compile the installer project, you will see the following in the results window:

WARNING: Two or more objects have the same target location ('[targetdir]\capsule.killerapplication.support.dll')

Now look at the Detected Dependencies folder in the installer project and you will see that a reference to the KillerApplication.Support.dll has appeared. What has happened is that Visual Studio has detected the support DLL as a dependency of the main project, and thus it has added it to the list of files to be packaged. But we had done this already when we added the primary output of the support project to the application folder in the file system editor; hence we have it twice. The solution: right click the file reference in the dependencies folder, and select Exclude.

Step Four: Add the Custom Actions

We have created code for installer custom actions in the KillerApplication.Install assembly, but the installer will not actually treat this code as install custom actions unless we explicitly instruct it to do so. To achieve this, we need to do the following:

  1. In the solution explorer, select the custom actions editor.
  2. Right click the Install folder, select Add Custom Action.
  3. In the Select Item in Project window, select the Common Files folder, then select the primary output from KillerApplication.Install.
  4. A name will be requested for the custom action, write "Create event source and give users permissions on data directory".
  5. Repeat the above steps for the Rollback folder, this time give the name "Delete event source on install error" to the custom action.
  6. Repeat for the Uninstall folder, this time give the name "Delete event source and data files" to the custom action.

Actually, you can give whatever names you like to custom actions, but it is a good idea to use meaningful names to keep things clear for yourself. Here's how the custom actions editor should look like when you're done:

Killer Application installer custom actions editor

Step Five: Make Your Application XP2+ Only or Vista Only (Optional)

You may want to make your application require a minimum version of the Windows operating system to work. We can instruct the installer to refuse to work if a lower version is detected, here are the required steps:

  1. In the solution explorer, select the launch conditions editor.
  2. Right click the Launch Conditions folder, select Add Launch Condition.
  3. A name will be requested, write "Windows XP SP2 or later."
  4. Select the condition you just created, open the properties window and set the following value for the Condition property: (VersionNT=501 AND ServicePackLevel>=2) OR VersionNT>501. Set the following text for the Message property: Killer Application requires Windows XP SP2 or later. Windows Vista is recommended.

This will make your application installable only on Windows XP SP2 or later (Windows 2003, Windows Vista, Windows 2008, and the new ones that will appear). If you want to be more restrictive and make your application only Vista or later compatible, set the condition as follows: VersionNT>=600. More details about the operating system version conditions here.

Step Six: Add the Transform Files

The MSI file that will be generated by our install project will lack some important data that is needed to obtain the certification. More precisely:

  • Test case 18 says: "verify application creates uninstall registry key and values." However, as you will see if you open the MSI with Orca, the InstallLocation key is missing.
  • Test case 25 says: "verify the application properly handles files in use during install." But again, open the MSI with Orca and you will discover that the MsiRMFilesInUse is not here.

To solve these issues, we need to create two transform files by using Orca, and to apply them to the MSI file. More precisely:

  1. Create a transform file for the test case 25 issue, as explained in this MSDN forum entry (search the accepted answer by Derek Sanderson). Save it with the name AddMsiRmFilesInUse.mst in the Killer Application installer project folder.
  2. Create a transform file for the test case 18 issue, again with Orca, this time using the data provided in this article by Simon Williams (search the Install/Uninstall section). Save it with the name VistaPatch2.mst in the Killer Application installer project folder.
  3. Right click the installer project in the solution explorer, select Add -> File, browse to the installer project folder, and select the AddMsiRmFilesInUse.mst and VistaPatch2.mst files.
  4. Once these files have been added to the project, select them in the solution explorer, and in the properties page set Exclude to True (we don't want these files to be packaged in the MSI file).

These transform files will patch the resulting MSI file when it is generated. To achieve this, of course we need a post-build event, and that's what we will create now. Select the installer project in the solution explorer, open the properties page, and in the post-build event box paste the following:

%HOMEDRIVE%\vistatools\MsiTran.exe -a "$(ProjectDir)VistaPatch2.mst" "$(BuiltOuputPath)"
%HOMEDRIVE%\vistatools\MsiTran.exe -a "$(ProjectDir)AddMsiRMFilesInUse.mst"
    "$(BuiltOuputPath)"

Note that in order to be able to create the transform files, you will need to open the MSI file in Orca... which we have not yet generated. To solve this fish-that-eats-its-own-tail problem, compile the installer project once without the transforms (remove or comment the post-build event commands then) so that you obtain an initial MSI file to work with. Warning: the AddMsiRMFilesInUse.mst included in the Killer Application solution that you can download on this page has all the UI messages in Spanish. You should create your own transform file in your language.

Step Seven: Add the NoImpersonate Patch

There is a subtle problem with the generated MSI file. If you try to install your application by directly running it (instead of running setup.exe), you will get a nasty and strange unexpected error 2869 window, and the whole install process will stop. This has to do with the interaction of the custom actions with the wonderful, superb, amazing Vista's User Access Control.

The solution to this problem is on this blog entry by Hunter555. You need to create a file named NoImpersonate.js in the installer project directory. Paste in the file the script code that you will find in this blog entry, and then proceed as with the manifest file: add it to the installer project and set its Exclude property to True. The script will appropriately modify the MSI file after it is generated, so that it does not produce any error when executed directly. To achieve this we need to add the following command to the installer project post-build event:

cscript.exe "$(ProjectDir)NoImpersonate.js" "$(BuiltOuputPath)"

cscript.exe is the Windows Script Host and is already included in the operating system.

Step Eight: Adjust setup.exe (Optional)

The adjustments that will be explained in this section are needed only if you plan to distribute the setup.exe file that Visual Studio generates together with the MSI file. Theoretically, it is enough to distribute the MSI file alone, but just in case I submitted setup.exe along with the MSI file to the testing authority.

Whatever. If you plan to distribute the setup.exe file, these are the additional steps you will need to perform in the installer project:

  1. Create the Manifest File

    Test case 13 says: "verify application’s installer contains an embedded manifest". So let's go for it: open your favourite text editor and create a file named setup.exe.manifest in the installer project directory. The contents of this file must be as follows:

    XML
    <?xml version="1.0" encoding="utf-8" ?>
    <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
      <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
        <security>
          <requestedPrivileges>
            <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
          </requestedPrivileges>
        </security>
      </trustInfo>
    </assembly>

    (Well, my text editor supports UTF-8 encoding. If yours does not, modify the encoding attribute on the XML declaration appropriately).

    Now right click the installer project in the solution explorer, select Add -> File, browse to the installer project folder, and select the setup.exe.manifest file you just created. Select the file in the solution explorer, and in the properties page set Exclude to True. Note that this file is different from the one we embed in the application main executable file in that the requested execution level is requireAdministrator. Indeed, we will need administrator privileges to install our application (this is required for the stuff that the custom actions do).

  2. Expand the Post-build Event

    We have created a nice manifest file and now we need to create the command that will process it. So add the following two commands to the installer's post-build event box (note that as usual, the commands are split across two/three lines each for ease of reading):

    %HOMEDRIVE%\vistatools\Mt.exe -manifest "$(ProjectDir)setup.exe.manifest"
        -outputresource:"$(ProjectDir)$(Configuration)\setup.exe;1"
    %HOMEDRIVE%\vistatools\signtool sign /f  %HOMEDRIVE%\vistatools\capsulekey.pfx
        /p kaitokun /v /t http://timestamp.verisign.com/scripts/timstamp.dll
        "$(ProjectDir)$(Configuration)\setup.exe"

    Note that I have included code to sign the setup.exe file. I believe that this is not necessary (it is not mentioned in the test cases document), but anyway signing this file does not hurt and it can be even useful if you plan to do advanced things like this one with your installer.

Step Nine: Party!

Believe it or not, but we are done. If you followed all the steps explained in this rather long text, you have a nice and 100% Vista certifiable application with a not less nice installer. You can have a beer or two (just remember: if you drink, don't code). Just to summarize, here is how the solution explorer looks for the Killer Application solution after having done all the work:

Killer Application complete solution explorer
KillerApp_contentfiles.png

Test Cases Revisited

To finish this article (yes, I swear this is the last section), we'll take the reverse approach to test cases. We have dissected a sample application, mentioning the involved test cases as appropriate. Now we will list the test cases and we'll mention what we did for fulfilling them. Remember: don't trust me, I'm bad, so test your application against all applicable test cases before sending it to the testing authority.

The Easy Part

There are a few cases that do not apply, that is, you don't even need to bother about them. Remember that this is true for applications that are structurally similar (nice word, I have to use it more) to Killer Application; be sure to double check if you actually need to check any of these cases.

  • TEST CASE 4. Verify application installer does not have a 16-Bit installer, does not use or rely on 16-Bit code or components and does not attempt to install any non 64-Bit drivers on x64 versions of Windows regardless if application is a Win32 application or is native to 64-bit.

    (there is no 16-bit stuff in a .NET application and we don't install drivers)
  • TEST CASE 6. Verify all kernel-mode drivers installed by the application are signed.

    (we don't install drivers)
  • TEST CASE 10. Verify drivers and services start in Safe-Mode.

    (we don't install drivers or services)
  • TEST CASE 14. Verify application launches with installed user token.

    (our application does not launch after it is installed)
  • TEST CASE 16. Verify ClickOnce application is signed with a valid Authenticode Certificate.

    (our application does not use ClickOnce for deployment)
  • TEST CASE 17. Verify ClickOnce application only stores data in installed user’s folders and does not write to WRP registry keys during install.

    (our application does not use ClickOnce for deployment)
  • TEST CASE 31. Verify application does not break into a debugger with the specified AppVerifier checks.

    (our application is fully managed)

And now, let's see the test cases that we really care about.

The Test Cases that Matter

Note that there is only one line or two about each test case, since we are just referring to concepts that we have seen in detail in the rest of the article.

  • TEST CASE 1. Verify all of the application’s executables contain an embedded manifest that defines its execution level.

    We have manually included a manifest file on the project that generates an EXE file, and we have set up the project post-build event to embed the manifest in the executable file by using mt.exe after the file is generated.

  • TEST CASE 2. Verify Least-Privilege Users cannot modify other users documents or files.

    Whenever we write to a file, we catch a possible UnauthorizedAccessException and perform the appropriate corrective action, for example telling the user about the lack of appropriate permissions.

  • TEST CASE 3. Verify Least-Privilege user is not able to save files to Windows System directory.

    Same as test case 2, anyway a well designed application should not attempt to write to the Windows directory.

  • TEST CASE 5. Verify application installed executables and files are signed.

    We use a post-build event to sign all the generated EXE and DLL files by using signtool.exe and our organizational certificate after the files are generated.

  • TEST CASE 7. Verify application properly checks for Operating System version.

    The .NET Framework will do it for us, we don't have to do anything special.

  • TEST CASE 8. Verify application launches and executes properly using Fast User Switching.

    In the startup code we check if the application is already being run by another user, if so we show an error message, we write a message in the Windows event log and we terminate the execution.

  • TEST CASE 9. Verify application launches and executes properly using Remote Desktop.

    In the startup code we check if the application is being run by using Remote Desktop, if so we show an error message, we write a message in the Windows event log and we terminate the execution.

  • TEST CASE 11. Verify application installer uses Windows Installer.

    Yes, the installer created by Visual Studio relies on the Windows Installer technology.

  • TEST CASE 12. Verify application’s MSI installer does not receive any errors from the Internal Consistency Evaluators.

    Do the test case steps and you will see that you don't receive any errors. Note however that you may receive warnings (NOT errors) within the range of ICEs specified in the test case specification, but this is not an issue for certification.

  • TEST CASE 13. Verify application’s installer contains an embedded manifest.

    This one applies if you distribute the setup.exe file generated by Visual Studio together with the MSI file. We manually include a manifest in this file the same way as in the application main executable file (see test case 1).

  • TEST CASE 15. Verify application installs to the correct folders by default.

    We achieve this by installing the application data files in the common application data folder. At startup time we obtain and store the correct path for these data files.

  • TEST CASE 18. Verify Windows Installer package contains Manufacturer, ProductCode, ProductLanguage, ProductName, ProductVersion (major and minor), and UpgradeCode property tags and that they are not null.

    The MSI file generated by Visual Studio indeed contains these properties.

  • TEST CASE 19. Verify application creates uninstall registry key and values.

    The MSI file generated by Visual Studio contains these properties except for InstallLocation. To solve this, we use a post-build event to apply a transform to the MSI file after it is generated, by using msitran.exe.

  • TEST CASE 20. Verify application does not try to write to or replace any WRP registry keys or files.

    The installer generated by Visual Studio will not try to do such ugly things. Anyway here is an application that will parse the AppVerifier logs for you and will tell you if there is something that will make this test case fail.

  • TEST CASE 21. Verify the application does not use nested install custom actions.

    We use custom actions but these are not of the forbidden types.

  • TEST CASE 22. Verify the application does not add custom columns to the Windows Installer's standard tables and that any custom tables or properties are not prefixed with 'msi'.

    This one is rather tedious to check. Anyway, no, the installer generated by Visual Studio does not add these ugly custom columns, tables or properties.

  • TEST CASE 23. Verify the application rolls back the install and restores machine back to previous state.

    We have added a rollback custom action so that the Windows event log source we create at install time is deleted if the install process fails. Note that when performing the test case steps, you must use the FailInstallFromDeferredCustomAction.msm module instead of FailInstallFromCommitCustomAction.msm.

  • TEST CASE 24. Verify the application does not force a reboot during install.

    No reboot will be forced after install, no special action is required here.

  • TEST CASE 25. Verify the application properly handles files in use during install.

    There is no trace of the required MsiRMFilesInUse dialog in the MSI file generated by Visual Studio. To solve this, again we use a post-build event to apply a transform to the MSI file after it is generated, by using msitran.exe.

  • TEST CASE 26. Verify the application can be installed quietly from the command line.

    Our application will pass this test case because we force a per-machine install; if we do a per-user install, the test case will fail. If you really need it, for sure there must be a workaround to pass this test when doing per-user installs, but you will have to find it by yourself, my friend.

  • TEST CASE 27. Verify the application’s Windows Installer ComponentID table does not contain null values.

    There are no null values in the specified table on the installer generated by Visual Studio.

  • TEST CASE 28. Verify the application’s Windows Installer Package does not contain more than one COM Server for each Component.

    Again, our installer will pass this test case with no required action in our side.

  • TEST CASE 29. Verify the application’s Windows Installer Package does not contain more than one shortcut for each component.

    Same as above.

  • TEST CASE 30. Verify the application is Restart Manager Aware

    Amitava and Simon Williams explain what to do to make a .NET application Restart Manager aware. Basically, you capture the WM_QUERYENDSESSION and WM_ENDSESSION Windows messages, and do the appropriate actions, such as saving the user data for later recovery.

    You may have noticed that there is nothing about this in the Killer Application code. And the fact is that, while capturing the end session messages and properly preparing for shutdown is of course good practice, it is not actually necessary to do it to certificate an application. In short, actually no special action is required to pass this test case.

  • TEST CASE 32. Verify that the application only handles exceptions that are known and expected.

    We have enclosed the execution of the application's Main method in a try-catch block so that we can generate the required entry in the Windows event log in case of unexpected exception. After we perform the log, the original exception is rethrown via a Throw command (NOT Throw ex or Throw new Exception).

    Also, at application startup, we set the unhandled exception mode to UnhandledExceptionMode.ThrowException so that the WER window will actually appear when an exception arises.

Final Note

I hope that all of this blah blah would have been somewhat useful for you. And remember that what I have explained is a way to obtain the certification, not the way. If you think there are better ways to do this or that, you have the comments section at your disposal.

History

  • 9 January, 2008: first version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer SunHotels
Spain Spain
Under the secret identity of a C# programmer, a freaky guy who loves MSX computers and japanese culture is hidden. Beware!

Comments and Discussions

 
GeneralGoog job Pin
Dr.Luiji16-Jan-08 9:42
professionalDr.Luiji16-Jan-08 9:42 
GeneralWow Pin
Sacha Barber12-Jan-08 0:00
Sacha Barber12-Jan-08 0:00 
What an excellent and highly useful and well researched/explained article.

Top job. 5 from me

Sacha Barber
  • Microsoft Visual C# MVP 2008
  • Codeproject MVP 2008
Your best friend is you.
I'm my best friend too. We share the same view, and never argue

My Blog : sachabarber.net

GeneralAwesome, thanks Pin
Ennis Ray Lynch, Jr.10-Jan-08 16:18
Ennis Ray Lynch, Jr.10-Jan-08 16:18 
GeneralRe: Awesome, thanks Pin
konamiman.MSX14-Jan-08 21:06
konamiman.MSX14-Jan-08 21:06 
GeneralPurpose of Test Case 32 Pin
Patrick Etc.10-Jan-08 10:19
Patrick Etc.10-Jan-08 10:19 
GeneralRe: Purpose of Test Case 32 Pin
konamiman.MSX10-Jan-08 21:07
konamiman.MSX10-Jan-08 21:07 
GeneralGreat Pin
geo_m9-Jan-08 19:57
geo_m9-Jan-08 19:57 
GeneralGreat resource Pin
Daniel Vaughan9-Jan-08 17:53
Daniel Vaughan9-Jan-08 17:53 
GeneralThank you very much Pin
BGaddis9-Jan-08 4:43
BGaddis9-Jan-08 4:43 
GeneralRe: Thank you very much Pin
AllenR9-Jan-08 20:55
professionalAllenR9-Jan-08 20:55 
GeneralGreat, useful, clear Pin
Arialdo Martini9-Jan-08 3:58
Arialdo Martini9-Jan-08 3:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.