May 2008 - Posts

Customizing the PathEdit and DirectoryCombo controls to validate path entries

My last post Common Tasks: Validating a Path received this comment from HookEm:

 Two of your 5 conditions can be handled without a custom action using the native MSI controls:

"Must be less than 200 characters in length": see the Text property of the PathEdit control (msdn.microsoft.com/.../aa370749(VS.85).aspx)

"must be on a fixed (local, non-removable) drive": see the available hexadecimal bit flags assignable to the Attributes property of the DirectoryCombo control (msdn.microsoft.com/.../aa368290(VS.85).aspx)

 -----------------------------------------

This was great information! Thanks, HookEm! Please keep the comments coming Smile

I checked out the PathEdit control.

You can put {200} (for example) in the Text field and that will prevent the user from manually typing in more than 200 characters. For most purposes this will work perfectly fine.

However, there are some aspects about how the dialog works that you may want to be aware of. These are mostly corner case situations, but your QA team may run across them in their testing, so just wanted you to be aware of them.

  • When you click OK, the InstallShield SetTargetPath function will then append a backslash to the path - giving a length of 201. 
  • If you then click the Change.. button to go back into the dialog, you will get an error that the path is too long, because now it is 201, not 200.
  • Also, from the dialog itself, if you use the new folder button it makes a folder called "New Folder". If that puts your path over the limit, you get an error, BUT it goes ahead and creates the folder anyway. The path showing in the PathEdit field is the truncated path, but if you click OK from the dialog it actually sets the path to the long path including the New Folder. SO.. it's possible to leave the dialog having set the path to a length longer that your desired limit.
  • It's possible to get into a state on the dialog where the INSTALLDIR value is out of synch with the path in the PathEdit field.

 

I also checked out the Directory Combo control.

Very good to know! I hadn't looked at this control's extra settings before. From the Dialog editor UI in InstallShield you can set each type of drive, Fixed, Removeable, RAMDisk, Remote, etc. to TRUE or FALSE in that combo box. Making that change will immediately reduce the likelihood of a person selecting a disalowed drive type.

Note, however, that the user is still free to type into the PathEdit box a path that uses a drive of that type. So this does help, but doesn't completely cover the situation.

 

One thing to note is that using just these settings you can't validate the path unless the user is using the UI. If you use a custom action and table to validate the path length, then you can validate the path during the Execution phase, even if the installation is being done without UI. So, I'd recommend using these settings in conjunction with a path validation custom action. I think they complement each other. 

 

Posted by SusanGorman with no comments

Common Task: Validate a Path

The built-in dialog for selecting the Destination Folder will automatically do a certain amount of validation on the selected path. For example, if the user types in a path using a character that isn't allowed by Windows (such as a question mark ? ), then the dialog won't allow it. How do you handle it, though, when a user enters a technically valid, but undesirable path, such as C:\Windows. That's "valid" but definitely undesirable. Another similar problem for us is path length.

 So what is a good method of validating the path?

Here is the basic setup:

In the Custom Actions I make a custom action of some sort (InstallScript, VB Script, DLL, doesn't matter what...) called MyCustomAction. This custom actions performs my path validation using the specific criteria for MY product. If the path is a valid path, then it sets the property PATH_VALID to TRUE. If the path is not valid, it sets PATH_VALID to FALSE. The custom action also displays an error message alerting the user as to why the path isn't valid and tells them to try again.

On the InstallChangeFolder dialog, OK button I have these events:

  1. SetTargetPath -> [_BrowseProperty]. Always. (This sets INSTALLDIR to the entered path and does some brief validation on it.)
  2. DoAction ->MyCustomAction. Always. 
  3. EndDialog -> Return. If PATH_VALID (This returns to the Destination Folder dialog when the path is valid).
  4. Reset. If Not PATH_VALID. (This resets the InstallChangeFolder dialog back to its original values and redisplays it when the path is not valid)

 

Cool! This works great.

Up until now, I used a VB script custom action and my own custom DLL to do this. Doing it this way puts part of the 'definition' of the installation rules into a script or DLL and actually obfuscates (hides) the functionality so it isn't easy to see. The intended overall design of an MSI installation is to use a data-driven architecture, where the definition of the installer is in tables and an engine operates on the data in those tables. Today's goal is to switch my path validation to an InstallScript custom action that reads data from a table.

Step One:
Evaluate what types of path validation rules may be needed and work out a custom table format that will allow these types of rules to be entered.

In my current installation program here is the set of path validation rules that I have to apply:

  • must be less than 200 characters in length
  • must be on a fixed (local, non-removable) drive
  • cannot be on the root of the drive
  • cannot be in the Windows directory OR any of its subdirectories
  • cannot contain any of the following characters $ # ; %

At first I tried to have a column for Drive Type, and a column for Length, etc. I realized though, that these columns were too specific. They'd only be valid for certain conditions/rules. The table needs to be more flexible than that. So after several stops and starts on different types of columns to use. I decided that I would need to define my own condition syntax and use some sort of parser to evaluate the syntax. This simplified the table format so that it could look like this:

Table Name - "ValidPathConditions"

  • Column 1 - "ConditionID" Type: String, String Length: 31, Primary Key.
  • Column 2 - "Condition"    Type: String, String Length: 255
  • Column 3 - "ErrorMessage"  Type: String, String Length: 255, Localizable

So each of your path validation rules will have its own descriptive name (ID) and its own error message to be displayed if this rule isn't followed. Because the Error Message column is marked as Localizable, when you type text into this field it will automatically create a String Table entry for that string (such as ID_STRING1). However, you won't be able to modify WHICH string table entry is used in that column from within the IDE. You'd have to edit the .ism file in a text editor in order to do that. Minor issue.

The condition syntax that I decided on goes like this [operator][expression]. Note there is no space between the operator and the expression.

Each condition must begin with one (and only one) of these operators.

  • ! - meaning "not". Whatever expression follows this operator describes a condition that a valid path does NOT meet.
  • = - meaning "equals". Whatever expression follows this operator describes a condition that a valid path DOES meet.
  • > or < - meaning greater than or less than. The expression that follows must be a number indicating path length.
  • % - meaning "does not contain". The expression that follows must be a single character.

The expressions following a ! or = operator can be:

  • A string literal path surrounded by double quotes. Such as "c:\program files\bad path"
  • A Directory table ID surrounded by brackets. Such as [WindowsFolder].
  • A string literal or Directory table ID followed by the special token CHILD. Such as [WindowsFolder]CHILD. This token indicates that the operator applies to ANY child directory of the given path. For example, =[ProgramFiles]CHILD specifies that a valid path must be a child directory of the Program Files folder.
  • A special token for drive type, such as FIXED.
  • The special token ROOT. For example, !ROOT specifies that a valid path must not be on the root of the path's drive.

Other than a few special tokens, I think that the syntax is pretty self-explanatory and is flexible enough that it could easily be expanded. A person reading the condition column in the table would probably be able to divine what the condition was intended to do without having to look at the InstallScript code. That's my goal.

Step Two:

Create the actual table and fill it with data. So.. I've created the above table using the Direct Editor. Then I added 8 rows to this table representing each of the path validation rules that I have to support. The Condition rows are:

ConditionID01

<400 The path must be less than %s characters long.
ConditionID02 =FIXED The path must be on a fixed (local) drive.
ConditionID03 !ROOT The path must not be on the root of the drive.
ConditionID04 ![WindowsFolder]CHILD The path may not be in the Windows directory nor any of its subdirectories.
ConditionID05 %$ The path may not contain the characters $ # ; or %.
ConditionID06 %# The path may not contain the characters $ # ; or %.
ConditionID07 %; The path may not contain the characters $ # ; or %.
ConditionID08 %% The path may not contain the characters $ # ; or %.

I gave the Condition ID's the way that I did because they will be processed in alphabetical order according to ConditionID. This allows me to control the order pretty effectively.

This is a good start for now. Next time I'll provide the InstallScript code that I'm using to evaluate this table. Let me know if you have comments or suggestions. This is all a learning process for me!  

Til next time .. Hope you are living the lifestyle of the MSIdle!

NOTE ABOUT PATH LENGTH: The InstallChangeFolder dialog automatically truncates any given path to a length of 240 (241 if you include the last slash that it automatically adds). (Actually it may be the SetTargetPath action that does this truncation, but regardless..) This truncation is all well and good, but sometimes 240 is still too long. The reason is that if you install any files to subdirectories of the given path, that's going to increase the path length. Your user could enter a valid 238 character long path, and all will appear to be well until it actually tries to copy the file to the subdirectory (the too long path), and at that point the installer generates an ugly error message and aborts. Yuk!

 

What are the common install tasks?

Out-of-the-box (meaning the default Basic MSI project) your Basic MSI installer project is already capable of handling most of the standard installation actions. But what are some of the common features that you might want to add to the installer project -- and how might you do it?

Here are some of the things I've run into. If you have suggestions of common tasks like this, please leave a comment about it.

  • Path validation. The user enters a path in the destination dialog and the built-in dialog will automatically prevent them from entering an invalid path. That's good, but what if a user choose a path like C:\, or C:\Windows? These probably are not smart places to install the product.Wink Another problem is path length. If your have subdirectories that get created under the install directory, then what looks like a valid path length may actually turn out to be too long once the full subdirectory and filenames get put together. Bottom line - I find it useful to be able to validate the path that the user enters on the Destination Dialog and then prompt them for a better path if it fails validation.
  • Cleanup user data on uninstall. The uninstall is automatically capable of removing any items that it originally installed. What if your product creates some sort of user data over time (history, configuration data, etc.)? In that case, the uninstall will not know about those items and won't remove them. Best practice would be to totally clean up at uninstall any items that your product put on the machine. (I like to refer to leftover items after uninstall as 'hanging chads'). So you will need a method to cleanup those hanging chads at uninstall time.
  • Advanced Cleanup user data on uninstall. Once you've added the cleanup feature, I guarantee that somebody is going to come up with a reason why under some conditions the user will NOT want to cleanup all the user data. So the very next request that will come your way is... Make the cleanup of user data an option for the user to decide at uninstall time. NOTE: You will want to make the default be that it leaves the custom data so that you can support the next feature in the list.
  • Port user data on upgrade. This is a common scenario and it is related to the above two items. When doing a major upgrade it will uninstall the old version and it does so silently (with no UI). Often the user may have customized some portion of the product and it is desirable for that customized information to get carried forward to the new version. Because uninstall may very well remove some of those customized items, you will need a method of backing up that data before uninstall, then restoring it after installation of the new version.
  • Perform different upgrade tasks depending on which version you are upgrading from. Sometimes it seems that users tend to hold on to old versions of the software as long as they can. This can create a situation where you end up having to support upgrading from several different versions of the product, perhaps going back a couple years. Oftentimes you will need to run a certain upgrade task if you are upgrading from version 1 and a different task if upgrading from version 2.
  • Require user to install to same directory on upgrade. This is fairly common. Even though a major upgrade uninstalls the old version, there may be good reasons why it is wise to reinstall to the same directory. One good reason: customized user data gets left behind in the old install directory and it saves having to copy it to a new location. So.. you want a reliable method of detecting the old install path and reusing it.
  • Use the same install project to produce differently 'branded' versions of the same product. This happens often with my product line. We have a product that is almost identical as far as installation needs go. The only real differences are in the graphics, the product names, and perhaps a few components. It's valuable to work out a consistent and reliable method for being able to design the installer in such a way that you can fairly easily 'swap out' one brand for another without having to maintain two separate install projects.

If you have any other suggestions for common install tasks let me know. I hope to write up quick tutorials on each of these tasks.  

Over time, your company may come up with a few other tweaks and minor adjustments to the default installer project that you will want to routinely add to your project. This includes things like "Make all the dialog titles say the product name only and remove "InstallShield Wizard" from the title." or "Always create an MSI log when the install runs." As features like this become known -- do yourself a favor and write a quick little document that lists the feature, why it is wanted, and a quick list of what items to change in the installer in order to provide that feature. This gives you a quick template to follow whenever you start a new install project. And... should you ever win the lottery and run off to Bimini with your significant other Big Smile, then the new install developer will thank you in his prayers for leaving that kind of documentation behind.

That's all for now!

Basic Introduction to InstallShield and Windows Installer

The scenario here is that the previous install developer won the lottery and ran off to Bimini with his girlfriend, leaving YOU the job of taking over a dozen different InstallShield 12 install projects. Let's assume you know very little about installer technology other than the fact that you've installed software a jillion times and watched that little progress bar for longer than you care to admit. Let's also assume you are fairly computer-savvy. You understand software and computers, in general, but you need not be a hard-core code writer. Ready? Action!

The Players 

First issue... who's who? InstallShield? Acresso? Windows Installer? MSI? Who are all these people?

Windows Installer (WI) is the name of the technology (from Microsoft) that has become the new standard for installation and deployment. 

MSI is a the default file extension for Windows Installer databases. People have gotten used to using "Windows Installer" and "MSI" pretty interchangeably. 

InstallShield (IS) is the name of an installation software that's been around for years (before Microsoft Windows Installer ever existed). Before MSI's existed, InstallShield used a script-based technology (based on their own proprietary scripting language called InstallScript) to define installation and deployment packages. There's also a Wiki article on InstallShield, though it isn't much... http://en.wikipedia.org/wiki/InstallShield

Acresso is the name of the company that makes InstallShield (as of 2008). InstallShield used to be produced by a company called InstallShield. In 2004 the InstallShield product line (company?) was sold to MacroVision. I wasn't real excited about that and didn't find that it improved the product or service much. But hey.. that's my opinion. In 2008 the InstallShield product line was sold again to a company called Acresso. It remains to be seen if this helps or hinders the product and its quality and reputation.

The Technology

Nowadays it seems like the only smart thing to do is to use an MSI-based installer. It integrates so completely and consistently with the Windows operating system that it only makes sense to use it if you are installing on Windows. So... what is the overall concept for WI (Windows Installer)?

The key here is to realize that the installation instructions are essentially stored in a database, made up of tables with columns and rows. The The Wikipedia article on it gives a pretty good overview. Check it out... http://en.wikipedia.org/wiki/Windows_installer

When you run the program, you call the WI engine -- msiexec.exe. The engine then acts on the instructions found in a database (an MSI) -- myapp.msi.

The information in the various tables of the database tell WI what UI to display, what to do with the information the user enters into any dialogs, what files to install, what registry keys to create, any extra actions to take and what order to do this all in, etc.

There are several different applications that you can use to create an MSI. InstallShield is only one of them, but it does seem to be one of the most common ones. The benefit of using some sort of graphical IDE for creating MSI's is that it takes care of all the cross-table interactions for you. For example, if searching for a registry value actually requires entry in 3 different tables - you get to define your search in one window and InstallShield takes care of creating the entries in all 3 tables for you and you don't even have to know about it. This is great for a new user! I couldn't have started using WI without the InstallShield IDE.

As you become more familiar with the product, and as your needs become more complex, you will eventually get familiar with what is actually going on in the tables 'behind-the-scenes' when you design something in the InstallShield UI. At some point you will find that you need to do something that you can't actually do from one of the InstallShield wizards and you have to edit a table and it's rows directly. That's ok. InstallShield gives you a Direct Editor so you can do just that.  No problem.

Three flavors to choose from 

So what is the difference between a Basic MSI, an InstallScript MSI and an InstallScript installer? Simple...

An InstallScript installation is old-school. It doesn't use the Windows Installer engine at all. Instead you define the installation package's actions using the InstallScript scripting language.  You will find that the end result quite similar. The major benefit in my mind is flexibility.

A Basic MSI package uses the built-in functionality of the Windows Installer, which is quite robust. You get the benefit of the InstallShield UI with which to create the MSI. You are, however, limited to the database structure and if you need to do something that exceeds the base functionality, now you have a choice (or a challenge?) in choosing a method to extend the MSI functionality. Options include vbscript, javascript, windows API dlls, write your own (C++) dll. The major benefit in my mind is reliability. If you are *only* using WI functionality then you know what what you are using has been well-tested and extensively exercised by a huge programmer and user base. The more you limit yourself to the basic WI functionality, the less opportunity for programmer error and the more reliable your installer is likely to be.

An InstallScript MSI is a half-breed. Essentially it is an MSI, with an added layer of execution on top driven by InstallScript. This allows the UI to be driven by the InstallScript engine, which then calls the MSI engine to make the actual changes to the system. Theoretically this should be the best of both worlds. You get the flexible and robust extensability available from InstallScript functions, but the reliability and safe structure of an MSI.

I use a Basic MSI wherever possible because I don't want the added layer of the InstallScript engine to create potential problems. But I'm also overly cautious ;-) I will probably start using InstallScript MSI's over the course of the next year because for security reasons (read: Vista) I need to move away from vbscript in my custom actions and InstallScript is a good alternative.