From e273eaeb7dedaad730f78d447922ea8808bbeffc Mon Sep 17 00:00:00 2001 From: Jay Beavers Date: Wed, 14 Sep 2022 08:02:11 -0700 Subject: [PATCH 1/9] Conversion over to .NET 4.8 for VS 2022 --- .gitignore | 2 +- Paraffin/Constants.Designer.cs | 2 +- Paraffin/Paraffin.csproj | 3 ++- Paraffin/Paraffin.sln | 12 ++++++++++-- Paraffin/app.config | 3 +++ 5 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 Paraffin/app.config diff --git a/.gitignore b/.gitignore index 38d4953..1935609 100644 --- a/.gitignore +++ b/.gitignore @@ -116,7 +116,7 @@ _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML - +.vs/ ############ ## Windows diff --git a/Paraffin/Constants.Designer.cs b/Paraffin/Constants.Designer.cs index cc4783e..a4b4567 100644 --- a/Paraffin/Constants.Designer.cs +++ b/Paraffin/Constants.Designer.cs @@ -19,7 +19,7 @@ namespace Wintellect.Paraffin { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Constants { diff --git a/Paraffin/Paraffin.csproj b/Paraffin/Paraffin.csproj index e6dd058..587b996 100644 --- a/Paraffin/Paraffin.csproj +++ b/Paraffin/Paraffin.csproj @@ -10,7 +10,7 @@ Properties Wintellect.Paraffin Paraffin - v4.6.2 + v4.8 512 false @@ -108,6 +108,7 @@ + diff --git a/Paraffin/Paraffin.sln b/Paraffin/Paraffin.sln index aa7864a..0b5a9f0 100644 --- a/Paraffin/Paraffin.sln +++ b/Paraffin/Paraffin.sln @@ -1,10 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26228.4 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.32804.182 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paraffin", "Paraffin.csproj", "{69CEBB42-4C4B-4546-9DEE-A5183E989214}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7D9EBD25-EB18-4647-AF98-C4526D711C55}" + ProjectSection(SolutionItems) = preProject + ..\.gitignore = ..\.gitignore + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -19,4 +24,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F7F8258A-8B29-4FA2-BDA0-5E219B463EC2} + EndGlobalSection EndGlobal diff --git a/Paraffin/app.config b/Paraffin/app.config new file mode 100644 index 0000000..3e0e37c --- /dev/null +++ b/Paraffin/app.config @@ -0,0 +1,3 @@ + + + From 51f55957f3cb9e3ce717fbe8adbb71fefbbe1719 Mon Sep 17 00:00:00 2001 From: Jay Beavers Date: Wed, 14 Sep 2022 08:09:31 -0700 Subject: [PATCH 2/9] Fix all static analyzer errors and warnings --- Paraffin/ArgParser.cs | 33 +++++++++++---------------------- Paraffin/Constants.Designer.cs | 8 ++++---- Paraffin/Main.Create.cs | 17 ++++++++--------- 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/Paraffin/ArgParser.cs b/Paraffin/ArgParser.cs index accb177..0c4495e 100644 --- a/Paraffin/ArgParser.cs +++ b/Paraffin/ArgParser.cs @@ -7,12 +7,11 @@ // //------------------------------------------------------------------------------ +using System; +using System.Diagnostics; + namespace Wintellect.Paraffin { - using System; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - /// /// A command line argument parsing class. /// @@ -22,9 +21,9 @@ namespace Wintellect.Paraffin /// /// There are two arrays of flags you'll pass to the constructors. The /// flagSymbols are supposed to be standalone switches that toggle an option - /// on. The dataSymbols are for switches that take data values. For - /// example, if your application needs a switch, -c, to set the count, - /// you'd put "c" in the dataSymbols. This code will allow both "-c100" and + /// on. The dataSymbols are for switches that take data values. For + /// example, if your application needs a switch, -c, to set the count, + /// you'd put "c" in the dataSymbols. This code will allow both "-c100" and /// the usual "-c" "100" both to be passed on the command line. Note that /// you can pass null/Nothing for dataSymbols if you don't need them. /// @@ -45,11 +44,11 @@ internal abstract class ArgParser private readonly Boolean caseSensitiveSwitches; /// - /// Initializes a new instance of the ArgParser class and defaults to + /// Initializes a new instance of the ArgParser class and defaults to /// "/" and "-" as the only valid switch characters /// /// - /// The array of simple flags to toggle options on or off. + /// The array of simple flags to toggle options on or off. /// /// /// The array of options that need data either in the next parameter or @@ -58,11 +57,6 @@ internal abstract class ArgParser /// /// True if case sensitive switches are supposed to be used. /// - [SuppressMessage("Microsoft.Naming", - "CA1726:UsePreferredTerms", - MessageId = "flag", - Justification = "Flag is appropriate term when " + - "dealing with command line arguments.")] protected ArgParser(String[] flagSymbols, String[] dataSymbols, Boolean caseSensitiveSwitches) @@ -94,11 +88,6 @@ protected ArgParser(String[] flagSymbols, /// Thrown if or /// are invalid. /// - [SuppressMessage("Microsoft.Naming", - "CA1726:UsePreferredTerms", - MessageId = "flag", - Justification = "Flag is appropriate term when " + - "dealing with command line arguments.")] protected ArgParser(String[] flagSymbols, String[] dataSymbols, Boolean caseSensitiveSwitches, @@ -117,7 +106,7 @@ protected ArgParser(String[] flagSymbols, if ((null == flagSymbols) || (0 == flagSymbols.Length)) { throw new ArgumentException(Constants.ArrayMustBeValid, - "flagSymbols"); + nameof(flagSymbols)); } Debug.Assert(null != switchChars, "null != switchChars"); @@ -133,7 +122,7 @@ protected ArgParser(String[] flagSymbols, if ((null == switchChars) || (0 == switchChars.Length)) { throw new ArgumentException(Constants.ArrayMustBeValid, - "switchChars"); + nameof(switchChars)); } this.flagSymbols = flagSymbols; @@ -178,7 +167,7 @@ protected enum SwitchStatus /// The string array to parse through. /// /// - /// True if parsing was correct. + /// True if parsing was correct. /// /// /// Thrown if is null. diff --git a/Paraffin/Constants.Designer.cs b/Paraffin/Constants.Designer.cs index a4b4567..fe95e1e 100644 --- a/Paraffin/Constants.Designer.cs +++ b/Paraffin/Constants.Designer.cs @@ -8,10 +8,10 @@ // //------------------------------------------------------------------------------ -namespace Wintellect.Paraffin { - using System; - - +namespace Wintellect.Paraffin +{ + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// diff --git a/Paraffin/Main.Create.cs b/Paraffin/Main.Create.cs index 5e6e468..505bcbd 100644 --- a/Paraffin/Main.Create.cs +++ b/Paraffin/Main.Create.cs @@ -10,6 +10,7 @@ namespace Wintellect.Paraffin { using System; + using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; @@ -86,7 +87,7 @@ private static void RecurseDirectoriesForNewFile(XElement currElement, // That means I'll be adding files to the node, which // has been passed in the currElement. XElement addToNode = currElement; - if (argValues.NoRootDirectoryState == false) + if (!argValues.NoRootDirectoryState) { // It's new so create a Directory element. XElement directoryNode = CreateDirectoryElement(directory); @@ -109,10 +110,10 @@ private static void RecurseDirectoriesForNewFile(XElement currElement, AddNewFilesToDirectoryNode(directory, addToNode); // Recurse the directories if I'm supposed to do so. - if (false == argValues.NoRecursion) + if (!argValues.NoRecursion) { String[] dirs = Directory.GetDirectories(directory); - foreach (var item in dirs) + foreach (String item in dirs) { Boolean skipDirectory = IsDirectoryExcluded(item); if (false == skipDirectory) @@ -137,11 +138,9 @@ private static void RecurseDirectoriesForNewFile(XElement currElement, private static void AddNewFilesToDirectoryNode(String directory, XElement directoryElem) { - var filesQuery = ProcessedDirectoryFiles(directory); - - // Only do the work if there are some files in the directory. - var files = filesQuery as String[] ?? filesQuery.ToArray(); - if (!files.Any()) + IEnumerable filesQuery = ProcessedDirectoryFiles(directory); + String[] files = filesQuery as String[] ?? filesQuery.ToArray(); + if (files.Length == 0) { return; } @@ -150,7 +149,7 @@ private static void AddNewFilesToDirectoryNode(String directory, XElement currentComponent = CreateComponentElement(); // For each file on disk. - foreach (var file in files) + foreach (String file in files) { // Create the File element and add it to the current // Component element. From c755750203cd3921520e3da0f706c21f84440204 Mon Sep 17 00:00:00 2001 From: Jay Beavers Date: Wed, 14 Sep 2022 08:16:31 -0700 Subject: [PATCH 3/9] More migration work --- .gitignore | 1 + Paraffin/Paraffin.csproj | 5 +++-- Paraffin/Paraffin.ruleset | 20 ++++++++++++++++++++ ParaffinInstall/ParaffinInstall.wixproj | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 Paraffin/Paraffin.ruleset diff --git a/.gitignore b/.gitignore index 1935609..498050a 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ local.properties ## TODO: If you have NuGet Package Restore enabled, uncomment this packages/ Debug/ +Release/ # Visual C++ cache files ipch/ diff --git a/Paraffin/Paraffin.csproj b/Paraffin/Paraffin.csproj index 587b996..65b4128 100644 --- a/Paraffin/Paraffin.csproj +++ b/Paraffin/Paraffin.csproj @@ -52,7 +52,7 @@ true true - ParaffinRuleSet.ruleset + Paraffin.ruleset false @@ -68,7 +68,7 @@ true true - Migrated rules for Paraffin.ruleset + Paraffin.ruleset false @@ -110,6 +110,7 @@ + diff --git a/Paraffin/Paraffin.ruleset b/Paraffin/Paraffin.ruleset new file mode 100644 index 0000000..2a8beb1 --- /dev/null +++ b/Paraffin/Paraffin.ruleset @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ParaffinInstall/ParaffinInstall.wixproj b/ParaffinInstall/ParaffinInstall.wixproj index e18ed38..aae1937 100644 --- a/ParaffinInstall/ParaffinInstall.wixproj +++ b/ParaffinInstall/ParaffinInstall.wixproj @@ -8,7 +8,7 @@ 2.0 ParaffinInstall Package - $(MSBuildExtensionsPath)\Microsoft\WiX\v3.x\Wix2010.targets + $(MSBuildExtensionsPath)\Microsoft\WiX\v3.x\Wix.targets bin\$(Configuration)\ From f458c51344e61a66b8344d34d89b4efc59cb90fe Mon Sep 17 00:00:00 2001 From: Jay Beavers Date: Wed, 14 Sep 2022 08:27:13 -0700 Subject: [PATCH 4/9] Add Command Line parameter support and documentation --- Paraffin/Constants.Designer.cs | 8 ++++---- Paraffin/Constants.resx | 2 ++ Paraffin/Main.Update.cs | 13 +++++++++++++ Paraffin/Main.cs | 4 +++- Paraffin/ParaffinArgParser.cs | 6 ++++++ 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Paraffin/Constants.Designer.cs b/Paraffin/Constants.Designer.cs index fe95e1e..a4b4567 100644 --- a/Paraffin/Constants.Designer.cs +++ b/Paraffin/Constants.Designer.cs @@ -8,10 +8,10 @@ // //------------------------------------------------------------------------------ -namespace Wintellect.Paraffin -{ - - +namespace Wintellect.Paraffin { + using System; + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// diff --git a/Paraffin/Constants.resx b/Paraffin/Constants.resx index 3afe901..4df25e0 100644 --- a/Paraffin/Constants.resx +++ b/Paraffin/Constants.resx @@ -255,6 +255,8 @@ Optional parameters when creating a new fragment: -Permanent - Sets Permanent="Yes" on all file elements. -WiX4 - Create fragments compatible with WiX 4, defaults to WiX 3. + -PerUser - Supports Limited Privileges / Per User MSI by adding + a Registry Value Key Path to each File's Component. Required parameters to update a previously created file for major upgrades. If you delete an input file, using -update will delete the Component and File diff --git a/Paraffin/Main.Update.cs b/Paraffin/Main.Update.cs index 8e45e7f..99df808 100644 --- a/Paraffin/Main.Update.cs +++ b/Paraffin/Main.Update.cs @@ -784,6 +784,19 @@ private static Boolean InitializeArgumentsFromFile(String inputXml) RegexOptions.IgnoreCase)); } + var perUserUsage = options.Descendants(PERUSER); + if (perUserUsage.Count() == 1) + { + String rawValue = perUserUsage.First().Value; + if (0 == String.Compare(rawValue, + "true", + StringComparison.OrdinalIgnoreCase)) + { + argValues.PerUser = true; + } + } + + // Now that everything is read out of the original options block, // add in any additional -ext, -dirExclude, and -regExExclude // options specified on the command line. diff --git a/Paraffin/Main.cs b/Paraffin/Main.cs index 8d6d7c2..e3e0492 100644 --- a/Paraffin/Main.cs +++ b/Paraffin/Main.cs @@ -236,6 +236,7 @@ internal static partial class Program private const String REGEXEXITEMELEM = "RegEx"; private const String PERMANENT = "Permanent"; private const String WIX4 = "WiX4"; + private const String PERUSER = "PerUser"; #endregion // The PE file extensions. @@ -465,7 +466,8 @@ private static void AddCommandLineOptionsComment(XElement wixElement) new XElement(NODIRECTORYELEM, argValues.NoRootDirectory), new XElement(DISKIDELEM, argValues.DiskId), new XElement(PERMANENT, argValues.Permanent), - new XElement(WIX4, argValues.WiX4) + new XElement(WIX4, argValues.WiX4), + new XElement(PERUSER, argValues.PerUser) ); // Add the file extension exclusions. diff --git a/Paraffin/ParaffinArgParser.cs b/Paraffin/ParaffinArgParser.cs index 4e8fdaf..5a93889 100644 --- a/Paraffin/ParaffinArgParser.cs +++ b/Paraffin/ParaffinArgParser.cs @@ -270,6 +270,12 @@ public Boolean WiX4 } } + /// + /// Gets whether the generated file components should include a registry key + /// to support perUser limitedPrivileges MSIs + /// + public Boolean PerUser { get; set; } + /// /// Gets the namespace as this is different between WiX 3 and WiX 4. /// Defaults to WiX3. From a9123b0afb85cc3d1420b4f98a26c1620cc2abde Mon Sep 17 00:00:00 2001 From: Jay Beavers Date: Wed, 14 Sep 2022 08:54:25 -0700 Subject: [PATCH 5/9] Add support for PerUser ... --- Paraffin/Main.Create.cs | 7 +++++++ Paraffin/Main.Update.cs | 8 +++++-- Paraffin/Main.cs | 46 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/Paraffin/Main.Create.cs b/Paraffin/Main.Create.cs index 505bcbd..a0e78ad 100644 --- a/Paraffin/Main.Create.cs +++ b/Paraffin/Main.Create.cs @@ -155,6 +155,13 @@ private static void AddNewFilesToDirectoryNode(String directory, // Component element. XElement fileElement = CreateFileElement(file); currentComponent.Add(fileElement); + + if (argValues.PerUser) + { + XElement registryValueElement = CreateRegistryValueElement(file); + currentComponent.Add(registryValueElement); + } + directoryElem.Add(currentComponent); currentComponent = CreateComponentElement(); } diff --git a/Paraffin/Main.Update.cs b/Paraffin/Main.Update.cs index 99df808..1541f38 100644 --- a/Paraffin/Main.Update.cs +++ b/Paraffin/Main.Update.cs @@ -363,10 +363,14 @@ private static void UpdateFilesInDirectoryNode(String directory, // file so just add it. First create a new File // element. XElement fileElement = CreateFileElement(file); - - // Add the file to this component. compElement.Add(fileElement); + if (argValues.PerUser) + { + XElement registryValueElement = CreateRegistryValueElement(file); + compElement.Add(registryValueElement); + } + // Add this element to the directory. addToElement.Add(compElement); } diff --git a/Paraffin/Main.cs b/Paraffin/Main.cs index e3e0492..91a56d3 100644 --- a/Paraffin/Main.cs +++ b/Paraffin/Main.cs @@ -821,13 +821,57 @@ private static XElement CreateFileElement(String fileName) file.Add(new XAttribute("Checksum", "yes")); } - file.Add(new XAttribute("KeyPath", "yes")); + if (!argValues.PerUser) + { + file.Add(new XAttribute("KeyPath", "yes")); + } fileName = AliasedFilename(fileName); file.Add(new XAttribute("Source", fileName)); return file; } + /// + /// Creates a RegistryValue element for the file in . + /// This is to facilitate LimitedPrivilige/PerUser MSI creation + /// + /// + /// The full filename to process. + /// + /// + /// A valid for the RegistryValue element. + /// + private static XElement CreateRegistryValueElement(String fileName) + { + String fileId; + if (argValues.Version == Version1File) + { + // Create a unique filename. In a one file per component run, + // this will mean that the file and it's parent component will + // have the same number. + fileId = CreateSeventyCharIdString("file", + argValues.GroupName, + componentNumber - 1); + } + else + { + Guid g = Guid.NewGuid(); + fileId = String.Format(CultureInfo.InvariantCulture, + "file_{0}", + g.ToString("N").ToUpper(CultureInfo.InvariantCulture)); + } + + XElement registryValue = new XElement(argValues.WixNamespace + "RegistryValue"); + registryValue.Add(new XAttribute("Root", "HKCU")); + registryValue.Add(new XAttribute("Key", "Software\\Tolt Technologies\\Ability Drive\\InstalledFiles")); + registryValue.Add(new XAttribute("Name", fileId)); +// registryValue.Add(new XAttribute("Value", fileId)); + registryValue.Add(new XAttribute("Type", "string")); + registryValue.Add(new XAttribute("KeyPath", "yes")); + + return registryValue; + } + /// /// Creates a standard Component element. /// From 681fb7016bb397d3287d2a94b279926913eff223 Mon Sep 17 00:00:00 2001 From: Jay Beavers Date: Wed, 14 Sep 2022 08:56:37 -0700 Subject: [PATCH 6/9] Nit fix whitespace --- Paraffin/Constants.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Paraffin/Constants.resx b/Paraffin/Constants.resx index 4df25e0..a28e912 100644 --- a/Paraffin/Constants.resx +++ b/Paraffin/Constants.resx @@ -256,7 +256,7 @@ Optional parameters when creating a new fragment: -WiX4 - Create fragments compatible with WiX 4, defaults to WiX 3. -PerUser - Supports Limited Privileges / Per User MSI by adding - a Registry Value Key Path to each File's Component. + a Registry Value Key Path to each File's Component. Required parameters to update a previously created file for major upgrades. If you delete an input file, using -update will delete the Component and File From a7ccf8a6b8b191a1201af48ea72ff08a998f43bf Mon Sep 17 00:00:00 2001 From: Jay Beavers Date: Wed, 14 Sep 2022 09:04:29 -0700 Subject: [PATCH 7/9] Add PerUser to ParaffinArgParser --- Paraffin/ParaffinArgParser.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Paraffin/ParaffinArgParser.cs b/Paraffin/ParaffinArgParser.cs index 5a93889..b9d0012 100644 --- a/Paraffin/ParaffinArgParser.cs +++ b/Paraffin/ParaffinArgParser.cs @@ -61,6 +61,7 @@ internal class ParaffinArgParser : ArgParser private const String WIN64VAR = "win64var"; private const String PERMANENT = "permanent"; private const String WIX4 = "WiX4"; + private const String PERUSER = "perUser"; #endregion private const String DEFAULTDIRREF = "INSTALLDIR"; @@ -104,7 +105,8 @@ public ParaffinArgParser() VERBOSE, VERBOSESHORT, PERMANENT, - WIX4 + WIX4, + PERUSER }, new[] { @@ -147,6 +149,7 @@ public ParaffinArgParser() this.Win64 = String.Empty; this.Permanent = false; this.WiX4 = false; + this.PerUser = false; this.Version = Program.CurrentFileVersion; @@ -566,6 +569,10 @@ protected override SwitchStatus OnSwitch(String switchSymbol, this.WiX4 = true; break; + case PERUSER: + this.PerUser = true; + break; + default: { this.errorMessage = Constants.UnknownCommandLineOption; From fbb286a9dfd01a0fe312e6fb6eb07706c3cf5d75 Mon Sep 17 00:00:00 2001 From: Jay Beavers Date: Sat, 8 Oct 2022 10:57:09 -0700 Subject: [PATCH 8/9] Add PerUser support to Update --- Paraffin/Main.Update.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Paraffin/Main.Update.cs b/Paraffin/Main.Update.cs index 1541f38..440da38 100644 --- a/Paraffin/Main.Update.cs +++ b/Paraffin/Main.Update.cs @@ -589,7 +589,7 @@ private static void FixKeyPathAttribute(String file, XElement compElement) { var attrib = fileElement.Attribute("KeyPath"); - if (attrib == null) + if (attrib == null && !argValues.PerUser) // When PerUser, KeyPath is stored on a RegistryValue { fileElement.Add( new XAttribute("KeyPath", "yes")); From e5f23532aa2efef8347b50d7fb987c90d9a38bb2 Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 9 Oct 2022 09:31:17 -0700 Subject: [PATCH 9/9] Add in required Value to RegistryValue Element --- Paraffin/Main.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Paraffin/Main.cs b/Paraffin/Main.cs index 91a56d3..06f6098 100644 --- a/Paraffin/Main.cs +++ b/Paraffin/Main.cs @@ -865,7 +865,7 @@ private static XElement CreateRegistryValueElement(String fileName) registryValue.Add(new XAttribute("Root", "HKCU")); registryValue.Add(new XAttribute("Key", "Software\\Tolt Technologies\\Ability Drive\\InstalledFiles")); registryValue.Add(new XAttribute("Name", fileId)); -// registryValue.Add(new XAttribute("Value", fileId)); + registryValue.Add(new XAttribute("Value", "")); registryValue.Add(new XAttribute("Type", "string")); registryValue.Add(new XAttribute("KeyPath", "yes"));