PDA

View Full Version : Recursive Properties in formatted text



janakp
06-21-2006, 07:41 PM
I am using a Basic MSI Project, and creating a Set Property custom action to be used in a UI dialog based on a UI condition - PropertyA. The end result I want is that PropertyA gets its value from a localized String Table entry. This String Table entry - String1 looks like "blah [PropertyB] blah" (w/out the quotes)

I found that I cannot set the PropertyA value directly to {String1} in the custom action. It displays the literal string "{String1}" instead of the string table value.

As a workaround, I created another localized property - PropertyC which refers to {String1}. And my custom action sets PropertyA value to [PropertyC]. Now in the UI dialog I see the text: "blah [PropertyB] blah". It's not doing the property replace in String1.

When I look up formatted text syntax it is supposed to evaluate Properties inside out, i.e., [PropertyB] should have been replaced with its value.

Any help would be much appreciated. Thanks.

MichaelU
06-22-2006, 10:36 AM
I think your best bet is to make the text of the dialog element (presumably a label) be the translated string which includes the property reference. Then this string can be translated and will be merged in as part of build (which is when the Basic MSI string table substitutions are performed). I believe it will then work as you expect.

MSI will not recursively evaluate properties, as otherwise you would end up with an infinite loop trying to evaluate the simple PROP1=[PROP2], PROP2=[PROP1]. So if instead you need this to handle this like the prompt in a MessageBox call, you will need to format it yourself perhaps with MsiFormatRecord.

janakp
06-22-2006, 04:16 PM
I think your best bet is to make the text of the dialog element (presumably a label) be the translated string which includes the property reference. Then this string can be translated and will be merged in as part of build (which is when the Basic MSI string table substitutions are performed). I believe it will then work as you expect.

MSI will not recursively evaluate properties, as otherwise you would end up with an infinite loop trying to evaluate the simple PROP1=[PROP2], PROP2=[PROP1]. So if instead you need this to handle this like the prompt in a MessageBox call, you will need to format it yourself perhaps with MsiFormatRecord.

Hi Micahel, Thanks for your reply. Option #1 to make the text of the dialog element the translated string was my first approach. However, I'm trying to display multiple translated text strings within the same dialog. I'm trying to create a dialog with consolidated pre-requisite error messages, .e.g, "You need to have at least [MinimumRAM] MB of RAM on your computer", "You need to have .NET Framework [MinimumNetFramework] or higher" etc. So I created multiple text controls for each possible translated string message, and that substituted the property with its value like you mentioned. But there the issue is each text control is tied to a specific coordinate (top, left) in the dialog, and therefore could leave blank lines depending upon which pre-requisites were missing.

So seems like I would have to explore Option #2: MsiFormatRecord. I am new to InstallShield & Windows Installer SDK functions. Seems like this would mean I would have to include an InstallScript custom action to use MsiFormatRecord. How can I do that in a Basic MSI Project?

Thanks again.
Janak

MichaelU
06-23-2006, 10:16 AM
You can do it in InstallScript; if you trust Virus checkers not to disable it you can probably do it in VB; or you can write it in C++. For InstallScript, start with the InstallScript view by adding a Setup.rul, follow the notes in that new script file, and add a custom action to run it. For VB or C++ just add the custom action (VB or DLL). For ease and proof of concept I might suggest prototyping in VB, although the function names will be different.

Another option, if you only have a couple messages, would be to create multiple text fields and use ControlConditions to hide or show them appropriately. The real problem here is the limitations of what you can easily do in native MSI dialogs, but the custom action solution to this feels a bit heavyweight if it's your only such action.

janakp
06-23-2006, 10:31 AM
You can do it in InstallScript; if you trust Virus checkers not to disable it you can probably do it in VB; or you can write it in C++. For InstallScript, start with the InstallScript view by adding a Setup.rul, follow the notes in that new script file, and add a custom action to run it. For VB or C++ just add the custom action (VB or DLL). For ease and proof of concept I might suggest prototyping in VB, although the function names will be different.

Another option, if you only have a couple messages, would be to create multiple text fields and use ControlConditions to hide or show them appropriately. The real problem here is the limitations of what you can easily do in native MSI dialogs, but the custom action solution to this feels a bit heavyweight if it's your only such action.

Thanks Michael. Multiple text fields & ControlConditions is what I have right now, but like you're saying there are limitations, e.g., as i'd mentioned in my previous post, each text field control is tied to a specific top, left coordinate on the dialog. So there are blank regions on the dialog for the text controls that are hidden based on ControlConditions.

I'm going to try your suggestion to prototype the custom action in VB, and then convert to C++/InstallScript. I was hoping there is an easier way.

Thanks,
Janak

MichaelU
06-23-2006, 11:27 AM
I think the easy way (but not necessarily timely) to do something like this would be for us to add string table support for localizing the "right side" of a SetProperty custom action (or control event). Then you could do it like you originally described, but without the extra level of storing your localized string as a propery.

You could also theoretically split your strings into pieces and store each piece in a property, so in your SetProperty action instead of [DIALOG_PROP1]="blah [PROP1] blah" you had [DIALOG_PROP1]="[PROP1_PREFIX][PROP1][PROP1_SUFFIX]", but that would be a nightmare both logisitically, and for your localization team.

Given the current state of things, doing one additional level of property resolution in a custom action is probably as easy as it gets. Heath Stewart (http://blogs.msdn.com/heaths/archive/2006/03/20/556251.aspx) talks about this a little, but isn't covering it from a dialog localization standpoint.

janakp
06-26-2006, 12:29 PM
Thanks for all your help Michael. One thing I noticed when I included the InstallScript custom actions that it increased the size of the MSI by about 2.5MB, my guess is primarily due to the InstallScript engine dll? Would writing the custom actions in a C++ dll instead keep the MSI smaller? I didn't want to have yet another separate codebase for the MSI custom action dll's.

For anyone interested, I'm including the InstallScript custom action code I ended up adding to resolve property references using MsiFormatRecord:

There's one Installscript function for each prerequisite condition error, e.g., SetPreReqErrorTextMemory, SetPreReqErrorTextOS. Unfortunately, had to write a function for each condition instead of one generic function with an argument to indicate which condition to process. There is a restriction for InstallScript functions used in custom actions - you can only have one argument, a handle to the MSI database.

Each prerequisite condition has a corresponding property containing a localized string table entry (with embedded property references), e.g., PreReqErrorText_Memory="You need to have at least [MinimumRAM] on your computer", PreReqErrorText_OS="Your computer must be running at least [MinimumOS]".

The EvaluateProperty() private function uses MsiFormatRecord to resolve embedded property references in the string table entries. One thing to watch out for in MsiFormatRecord - the return string prefixes the record number: "1: blah blah blah". So I had to parse out the "1: " from the result (ParseRecordValue private function).

The AppendToProperty() private function stores the cumulative result of all the prerequisite condition properties. And I display the cumulative result property in the dialog. Phew!...for anyone who read this far.

Setup.rul:
////////////////////////////////////////////////////////////////////////////////
//
// IIIIIII SSSSSS
// II SS InstallShield (R)
// II SSSSSS (c) 1996-2002, InstallShield Software Corporation
// II SS All rights reserved.
// IIIIIII SSSSSS
//
//
// This template script provides the code necessary to build an entry-point
// function to be called in an InstallScript custom action.
//
//
// File Name: Setup.rul
//
// Description: InstallShield script
//
////////////////////////////////////////////////////////////////////////////////

// Include Ifx.h for built-in InstallScript function prototypes, for Windows
// Installer API function prototypes and constants, and to declare code for
// the OnBegin and OnEnd events.
#include "ifx.h"

// Evaluates a property value and returns string after resolving property
// references & other non-literal text references
// Argument 1: handle to Installer database
// Argument 2: Property name
// Returns resolved Property value
prototype STRING EvaluateProperty(HWND, STRING);

// Appends string to property value
// Argument 1: handle to Installer database
// Argument 2: Property name to which to append
// Argument 3: string to append
// Argument 4: delimiter to add between current property value and append string
prototype AppendToProperty(HWND, STRING, STRING, STRING);

// Parse the record value from formatted record string
// Argument 1: formatted string: <RecordFieldNumber>: <RecordFieldValue>
// Returns parsed string: <RecordFieldValue>
prototype STRING ParseRecordValue (STRING);

// The keyword export identifies MyFunction() as an entry-point function.
// The argument it accepts must be a handle to the Installer database.
export prototype SetPreReqErrorTextMemory(HWND);
export prototype SetPreReqErrorTextNetVersion(HWND);
export prototype SetPreReqErrorTextOS(HWND);
export prototype SetPreReqErrorTextAdminUser(HWND);

// To Do: Declare global variables, define constants, and prototype user-
// defined and DLL functions here.
#define PREREQERRORS_PROPNAME "PreReqErrorsProperty"


// To Do: Create a custom action for this entry-point function:
// 1. Right-click on "Custom Actions" in the Sequences/Actions view.
// 2. Select "Custom Action Wizard" from the context menu.
// 3. Proceed through the wizard and give the custom action a unique name.
// 4. Select "Run InstallScript code" for the custom action type, and in
// the next panel select "MyFunction" (or the new name of the entry-
// point function) for the source.
// 5. Click Next, accepting the default selections until the wizard
// creates the custom action.
//
// Once you have made a custom action, you must execute it in your setup by
// inserting it into a sequence or making it the result of a dialog's
// control event.

///////////////////////////////////////////////////////////////////////////////
//
// Function: MyFunction
//
// Purpose: This function will be called by the script engine when
// Windows(TM) Installer executes your custom action (see the "To
// Do," above).
//
///////////////////////////////////////////////////////////////////////////////
function SetPreReqErrorTextMemory(hMSI)
STRING sPropVal;
begin
sPropVal = EvaluateProperty(hMSI, "PreReqErrorText_Memory");
AppendToProperty(hMSI, PREREQERRORS_PROPNAME, sPropVal, "\n\n");

end;

function SetPreReqErrorTextNetVersion(hMSI)
STRING sPropVal;
begin
sPropVal = EvaluateProperty(hMSI, "PreReqErrorText_NetFramework");
AppendToProperty(hMSI, PREREQERRORS_PROPNAME, sPropVal, "\n\n");

end;

function SetPreReqErrorTextOS(hMSI)
STRING sPropVal;
begin
sPropVal = EvaluateProperty(hMSI, "PreReqErrorText_OS");
AppendToProperty(hMSI, PREREQERRORS_PROPNAME, sPropVal, "\n\n");

end;

function SetPreReqErrorTextAdminUser(hMSI)
STRING sPropVal;
begin
sPropVal = EvaluateProperty(hMSI, "PreReqErrorText_AdminUser");
AppendToProperty(hMSI, PREREQERRORS_PROPNAME, sPropVal, "\n\n");

end;


function STRING EvaluateProperty(hMSI, sPropertyName)
STRING sPropertyValue;
STRING sFormattedValue;
STRING sResolvedValue;
NUMBER nBufferLength;
HWND hRecord;
begin
// get raw property value
nBufferLength = 1000;
MsiGetProperty(hMSI, sPropertyName, sPropertyValue, nBufferLength);

// create empty record to temporarily store the unresolved string
hRecord = MsiCreateRecord(1);
MsiRecordSetString(hRecord, 1, sPropertyValue);

// resolve non-literal string references in record
nBufferLength = 1000;
MsiFormatRecord(hMSI, hRecord, sFormattedValue, nBufferLength);
// Parse the result string to extract the record field value only
// Result string format: <FieldNumber>: <FieldValue>
// We only want <FieldValue>
sResolvedValue = ParseRecordValue(sFormattedValue);

// deallocate resources
CloseHandle(hRecord);

return sResolvedValue;
end;


function AppendToProperty(hMSI, sPropertyName, sAppendString, sDelimiter)
STRING sPropertyValue;
STRING sResultValue;
NUMBER nBufferLength;
begin
// get raw property value
nBufferLength = 1000;
MsiGetProperty(hMSI, sPropertyName, sPropertyValue, nBufferLength);

sResultValue = sPropertyValue + sAppendString + sDelimiter;

MsiSetProperty(hMSI, sPropertyName, sResultValue);

end;

function STRING ParseRecordValue(sFormatRecordResult)
STRING sResult;
begin
sResult = sFormatRecordResult;
if (sFormatRecordResult % ": ") then
StrSub(sResult, sFormatRecordResult, 2, StrLengthChars(sFormatRecordResult));
endif;
return sResult;
end;

MichaelU
06-26-2006, 01:02 PM
Looks like you have the right idea, and if it's working you should be golden.

For something like this, a C++ DLL would probably have significantly less overhead. If you do it as a proper MSI DLL, you'll still be limited to one argument, but you can solve that as you did here with multiple entry points, or by hardcoding an interface property and using SetProperty actions to populate it before calling your action; yeah, it's ugly either way. If you use our Standard DLL wrapper, you can place all the information in the custom action view, including both input and output properties. I would lean towards the MSI DLL.