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.
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.
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:
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.
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.
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.
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:
(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:
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.
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:
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):
null reference exception. This is a quick way to exercise test case 32. That's all. It's not very much, but enough to exercise all the applicable test cases. Now let's go to the details.
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.
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:
<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:
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.
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:
<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:
[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.
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 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.
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.
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.
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:
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:
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:
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:
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:
static string variable named DataDirectory in the Program class. DataDirectory to the path of this directory. Otherwise, set DataDirectory to %ALLUSERSPROFILE%\Killer Application\Data. 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.
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.
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:
You may get surprised when you see that the Mmethod is as follows:ain
<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.
Before doing anything else, the following piece of code is required:
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException)
Without this, you will receive this ugly error window instead of the fancy WER window in case of unhandled exception:
And, you guessed it, test case 32 will fail if this happens.
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:
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.
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:
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.
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:
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:
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) DataDirectory to the path of this directory. 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:
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:
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:
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.
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):
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)
Nothing unusual here, we just pass control to the application main window:
Application.EnableVisualStyles()
Application.SetCompatibleTextRenderingDefault(False)
Application.Run(New FormMain())
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:
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:
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:
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.
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:
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:
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.
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:
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); }
}
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:
The piece of code that does this is as follows:
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.
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:
public override void Rollback(System.Collections.IDictionary savedState)
{
if(EventLog.SourceExists(APPNAME))
{
EventLog.DeleteEventSource(APPNAME);
}
base.Rollback(savedState);
}
Three actions are needed when our application is being uninstalled:
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.
No mystery here; note that even if we remove the event source, the generated events will remain.
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:
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.
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.
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:
With this, you have created an empty installer project for our application. Now we need to make some initial adjustments.
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:
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.
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:
KillerApplication.Support. KillerApplication.Install. 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:
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.
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:
KillerApplication.Install. 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:
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:
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.
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:
InstallLocation key is missing. 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:
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.
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.
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:
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 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).
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.
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:
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.
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.
And now, let's see the test cases that we really care about.
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.
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.
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.
Same as test case 2, anyway a well designed application should not attempt to write to the Windows directory.
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.
The .NET Framework will do it for us, we don't have to do anything special.
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.
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.
Yes, the installer created by Visual Studio relies on the Windows Installer technology.
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.
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).
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.
The MSI file generated by Visual Studio indeed contains these properties.
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.
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.
We use custom actions but these are not of the forbidden types.
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.
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.
No reboot will be forced after install, no special action is required here.
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.
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.
There are no null values in the specified table on the installer generated by Visual Studio.
Again, our installer will pass this test case with no required action in our side.
Same as above.
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.
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.
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.
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||