Creating new key in HKCR works, but doesn't - c#

I have the following code, which returns "Success", but searching the registry with two different tools with the search string "{3BDAAC43-E734-11D5-93AF-00105A990292}" yields no results.
RegistryKey RK = Registry.ClassesRoot.CreateSubKey("CLSID\\{3BDAAC43-E734-11D5-93AF-00105A990292}");
if ( RK != null )
{
MessageBox.Show("Success", "Success");
}
else
{
MessageBox.Show("Failure", "Failure");
}
[Edit] I hate problems that I solve without knowing how, but I'm past this issue.
I'm now able to create a subkey, but I can't set any value pairs. I've found several examples how it should be done, but can't see how my code is different than them.
This is my new code. The subkey appears, but never has anything in it.
Registry.ClassesRoot.DeleteSubKey(clsid.EntryGuid, false);
RK.CreateSubKey(clsid.EntryGuid);
RK.OpenSubKey(clsid.EntryGuid, true);
RK.SetValue("(Default", clsid.RootObjectName);
RK.CreateSubKey("InprocServer32", true);
RK.SetValue("(Default", clsid.InprocServer32);
RK.SetValue("(Default", clsid.ThreadingModel);
RK.CreateSubKey("ProgID", true);
RK.SetValue("(Default", clsid.ProgID);
RK.CreateSubKey("TypeLib", true);
RK.SetValue("(Default", clsid.TypeLib);
RK.CreateSubKey("Version", true);
RK.SetValue("(Default", clsid.Version);
RK.Close();
if ( RK != null )
{
MessageBox.Show("Success", "Success");
}
else
{
MessageBox.Show("Failure", "Failure");
}
In the above code, clsid is an object of class, clsRegistryCLSID, which is a part of my project:
public clsRegistryCLSID(int ID, string hive, string rootObjectName, string ThreadingModel, string InprocServer32, string ProgID, string TypeLib, string Version, string EntryGuid)
{
public int ID { get; set; }
public string hive { get; set; }
public string RootObjectName { get; set; }
public string ThreadingModel { get; set; }
public string InprocServer32 { get; set; }
public string ProgID { get; set; }
public string TypeLib { get; set; }
public string Version { get; set; }
public string EntryGuid { get; set; }

The following shows how to add subkeys to HKEY_CLASSES_ROOT\CLSID, as well as, values and data. I searched the (Win10) registry, found subkeys with similar names, and followed the structure of existing subkeys.
The structure of the subkey that is created in the code below is:
Create a new Windows Forms App (.NET Framework) project (name: RegistryCreateSubkeyAndValues)
Add an Application Manifest to your project
Note: This is used to prompt the user to execute the program as Administrator.
In VS menu, click Project
Select Add New Item...
Select Application Manifest File (Windows Only) (name: app.manifest)
Click Add
In app.manifest, replace
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
with
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
As can be seen in the image above, the subkeys are nested. To be in harmony with this concept, I'll create a variety of nested classes to hold the necessary information.
I'll name the top-level class RegClsid which will be created last to avoid errors showing in Visual Studio. To keep the class names organized, I've chosen to prepend each class name with its parent's name.
Create a class (name: RegClsidInprocServer32.cs) - name is RegClsid + InprocServer32 => RegClsidInprocServer32
public class RegClsidInprocServer32
{
public string Default { get; set; }
public string ThreadingModel { get; set; }
}
Create a class (name: RegClsidProgId.cs) - name is RegClsid + ProgId => RegClsidProgId
public class RegClsidProgId
{
public string Default { get; set; }
}
Create a class (name: RegClsIdTypeLib.cs) - name is RegClsid + TypeLib => RegClsidTypeLib
public class RegClsIdTypeLib
{
public string Default { get; set; }
}
Create a class (name: RegClsIdVersion.cs) - name is RegClsid + Version => RegClsidVersion
public class RegClsIdVersion
{
public string Default { get; set; }
}
Create a class (name: RegClsid.cs)
public class RegClsid
{
public string EntryGuid { get; set; }
public RegClsidInprocServer32 InprocServer32 { get; set; } = new RegClsidInprocServer32();
public RegClsidProgId ProgId { get; set; } = new RegClsidProgId();
public RegClsIdTypeLib TypeLib { get; set; } = new RegClsIdTypeLib();
public RegClsIdVersion Version { get; set; } = new RegClsIdVersion();
}
Add the following using directives:
using Microsoft.Win32;
The following shows how to add/update a registry subkey in HKEY_CLASSES_ROOT\CLSID. using statements are used to ensure that each RegistryKey is properly disposed.
Here's an illustration of the code below. Notice the nesting of the using statements and how the return value is used in each of the nested using statements.
AddUpdateRegClsid:
private bool AddUpdateRegClsid(RegClsid clsid, RegistryView regView = RegistryView.Registry64)
{
//open HKCR hive using the specified view
using (RegistryKey localKey = RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, regView))
{
if (localKey != null)
{
//open subkey with write permissions
using (RegistryKey clsidKey = localKey.OpenSubKey(#"CLSID", true))
{
if (clsidKey != null)
{
//create or open subkey with write permissions
using (RegistryKey guidKey = clsidKey.CreateSubKey(clsid.EntryGuid, true))
{
if (guidKey != null)
{
//create or open subkey 'InprocServer32' with write permissions
//HKCR\Clsid\<entryGuid>\InprocServer32
using (RegistryKey inprocServer32Key = guidKey.CreateSubKey("InprocServer32", true))
{
if (inprocServer32Key != null)
{
//default
inprocServer32Key.SetValue("", clsid.InprocServer32.Default, RegistryValueKind.String);
//ThreadingModel
inprocServer32Key.SetValue("ThreadingModel", clsid.InprocServer32.ThreadingModel, RegistryValueKind.String);
}
}
//create or open subkey 'ProgID' with write permissions
//HKCR\Clsid\<entryGuid>\ProgID
using (RegistryKey progidKey = guidKey.CreateSubKey("ProgID", true))
{
if (progidKey != null)
{
//default
progidKey.SetValue("", clsid.ProgId.Default, RegistryValueKind.String);
}
}
//create or open subkey 'TypeLib' with write permissions
//HKCR\Clsid\<entryGuid>\TypeLib
using (RegistryKey typeLibKey = guidKey.CreateSubKey("TypeLib", true))
{
if (typeLibKey != null)
{
//default
typeLibKey.SetValue("", clsid.TypeLib.Default, RegistryValueKind.String);
}
}
//create or open subkey 'Version' with write permissions
//HKCR\Clsid\<entryGuid>\Version
using (RegistryKey versionKey = guidKey.CreateSubKey("Version", true))
{
if (versionKey != null)
{
//default
versionKey.SetValue("", clsid.Version.Default, RegistryValueKind.String);
}
}
return true;
}
}
}
}
}
}
return false;
}
Usage 1:
//ToDo: replace values below with desired values
//create new instance and set values
RegClsid clsid = new RegClsid()
{
EntryGuid = "{3BDAAC43-E734-11D5-93AF-00105A990292}",
InprocServer32 = new RegClsidInprocServer32()
{
Default = "My InprocServer32 default",
ThreadingModel = "Both"
},
ProgId = new RegClsidProgId()
{
Default = "My ProgID default"
},
TypeLib = new RegClsIdTypeLib()
{
Default = "My TypeLib default"
},
Version = new RegClsIdVersion()
{
Default = "My Version default"
}
};
//add CLSID entry to registry
bool result = AddUpdateRegClsid(clsid);
if (result)
MessageBox.Show($#"'HKEY_Classes_Root\CLSID\{clsid.EntryGuid}' successfully added/updated.");
else
MessageBox.Show($#"Error: Failed to add/update 'HKEY_Classes_Root\CLSID\{clsid.EntryGuid}'.");
Usage 2:
//ToDo: replace values below with desired values
//InprocServer32 - create new instance and set value(s)
RegClsidInprocServer32 inprocServer32 = new RegClsidInprocServer32()
{
Default = "My InprocServer32 default",
ThreadingModel = "Both"
};
//ProgID - create new instance and set values
RegClsidProgId progId = new RegClsidProgId()
{
Default = "My ProgID default"
};
//TypeLib - create new instance and set value(s)
RegClsIdTypeLib typeLib = new RegClsIdTypeLib()
{
Default = "My TypeLib default"
};
//Version - create new instance and set value(s)
RegClsIdVersion version = new RegClsIdVersion()
{
Default = "My Version default"
};
//Clsid - create new instance and set values
RegClsid clsid = new RegClsid()
{
EntryGuid = "{3BDAAC43-E734-11D5-93AF-00105A990292}",
InprocServer32 = inprocServer32,
ProgId = progId,
TypeLib = typeLib,
Version = version
};
//add CLSID entry to registry
bool result = AddUpdateRegClsid(clsid);
if (result)
MessageBox.Show($#"'HKEY_Classes_Root\CLSID\{clsid.EntryGuid}' successfully added/updated.");
else
MessageBox.Show($#"Error: Failed to add/update 'HKEY_Classes_Root\CLSID\{clsid.EntryGuid}'.");
Usage 3:
//ToDo: replace values below with desired values
//InprocServer32 - create new instance and set value(s)
RegClsidInprocServer32 inprocServer32 = new RegClsidInprocServer32();
inprocServer32.Default = "My InprocServer32 default";
inprocServer32.ThreadingModel = "Both";
//ProgID - create new instance and set value(s)
RegClsidProgId progId = new RegClsidProgId();
progId.Default = "My ProgID default";
//TypeLib - create new instance and set value(s)
RegClsIdTypeLib typeLib = new RegClsIdTypeLib();
typeLib.Default = "My TypeLib default";
//Version - create new instance and set value(s)
RegClsIdVersion version = new RegClsIdVersion();
version.Default = "My Version default";
//Clsid - create new instance and set values
RegClsid clsid = new RegClsid();
clsid.EntryGuid = "{3BDAAC43-E734-11D5-93AF-00105A990292}";
clsid.InprocServer32 = inprocServer32;
clsid.ProgId= progId;
clsid.TypeLib = typeLib;
clsid.Version = version;
//add CLSID entry to registry
bool result = AddUpdateRegClsid(clsid);
if (result)
MessageBox.Show($#"'HKEY_Classes_Root\CLSID\{clsid.EntryGuid}' successfully added/updated.");
else
MessageBox.Show($#"Error: Failed to add/update 'HKEY_Classes_Root\CLSID\{clsid.EntryGuid}'.");
Here's the result of executing the code above:
The following shows how to delete the registry subkey and child subkeys from HKEY_CLASSES_ROOT\CLSID.
DeleteRegClsid:
private bool DeleteRegClsid(string entryClsid, RegistryView regView = RegistryView.Registry64)
{
//open HKCR hive using the specified view
using (RegistryKey localKey = RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, regView))
{
if (localKey != null)
{
//open subkey with write permissions
using (RegistryKey clsidKey = localKey.OpenSubKey(#"CLSID", true))
{
if (clsidKey != null)
{
//delete subkey and all child subkeys
clsidKey.DeleteSubKeyTree(entryClsid);
return true;
}
}
}
}
return false;
}
Usage:
string entryGuid = "{3BDAAC43-E734-11D5-93AF-00105A990292}";
bool result = DeleteRegClsid(entryGuid);
if (result)
MessageBox.Show($#"'HKEY_Classes_Root\CLSID\{entryGuid}' successfully deleted.");
else
MessageBox.Show($#"Error: Failed to delete 'HKEY_Classes_Root\CLSID\{entryGuid}'.");
Resources:
RegistryKey Class
RegistryKey.OpenBaseKey
RegistryKey.OpenSubKey
RegistryKey.CreateSubKey
C# - How to set value of (Default) in the registry?
Delete a Registry key using C#
Nested Types (C# Programming Guide)
String interpolation using $
Verbatim text - # in variables, attributes, and string literals
Object and Collection Initializers (C# Programming Guide)
How to initialize objects by using an object initializer (C# Programming Guide)

Related

C# Parameters class with app.config

I have a configuration class with all the parameters of my application, to acquire images from a scanner.
I have parameters like color/BW, resolution...
The parameters are changed often, so I'm searching a solution to write automatically when I save the parameters the changed parameters in the app.config file. And to do the reverted thing, write my class from the app.config at the init of the software.
Here are my two classes :
private void GetParameters() {
try
{
var appSettings = ConfigurationManager.AppSettings;
Console.WriteLine( ConfigurationManager.AppSettings["MyKey"]);
if (appSettings.Count == 0)
{
Console.WriteLine("AppSettings is empty.");
}
else
{
foreach (var key in appSettings.AllKeys)
{
Console.WriteLine("Key: {0} Value: {1}", key, appSettings[key]);
}
}
}
catch (ConfigurationErrorsException)
{
MessageBox.Show("Error reading app settings");
}
}
private void SetParameters(string key, string value)
{
try
{
Configuration configManager = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
KeyValueConfigurationCollection confCollection = configManager.AppSettings.Settings;
if (confCollection[key] == null)
{
confCollection.Add(key, value);
}
else
{
confCollection[key].Value = value;
}
configManager.Save(ConfigurationSaveMode.Modified);
ConfigurationManager.RefreshSection(configManager.AppSettings.SectionInformation.Name);
}
catch (ConfigurationErrorsException)
{
MessageBox.Show("Error writing app settings");
}
}
I don't want to call the method for every parameter...
And there is my parameters class :
class ScannerParameters
{
public bool Color { get; set; }
public int Resolution{ get; set; }
public string FilePath { get; set; }
public TypeScan TypeScan { get; set; }
public string TextTest{ get; set; }
}
The question can be translated into how do I save an object into some kind of persistence?
Either use a database (seems like an overkill) or serialize it using a serializer or simply write it all down into a text file yourself. Using json serialization, serializing your ScannerParameters and then writing that into a file would seem most appropriate.
Using newtonsoft json, which is defacto standard for .net there's nice examples # http://www.newtonsoft.com/json/help/html/SerializingJSON.htm
In your case you would do:
// our dummy scannerParameters objects
var parameters = new ScannerParameters();
// let's serialize it all into one string
string output = JsonConvert.SerializeObject(paramaters);
// let's write all that into a settings text file
System.IO.File.WriteAllText("parameters.txt", output);
// let's read the file next time we need it
string parametersJson = System.IO.File.ReadAllText("parameters.txt);
// let's deserialize the parametersJson
ScannerParameters scannerParameters = JsonConvert.DeserializeObject<ScannerParameters>(parametersJson);

Change due date for production order in SAP B1 with the DI SDK

I am trying to change/update the due date of a production order in SAP B1 with the code below:
public static void ChangeDueDateForProductionOrder(SAPB1Credentials credentials, int poAbsEntry, DateTime dueDate)
{
DebugLogger.Log(credentials.CompanyDB, $"Changing due date for production order '{poAbsEntry}' to '{dueDate.ToString(C_DATE_FORMAT_NL)}'.");
using (var sap = new SAPB1Connection(credentials))
{
ProductionOrders productionOrder = sap.Company.GetBusinessObject(BoObjectTypes.oProductionOrders);
if(productionOrder.GetByKey(poAbsEntry))
{
productionOrder.DueDate = dueDate;
if (productionOrder.Update() != 0)
{
var message = $"Error while changing due date for '{poAbsEntry}' to '{dueDate.ToString(C_DATE_FORMAT_NL)}', the following error is given '{sap.Company.GetLastErrorDescription()}'.";
DebugLogger.Log(credentials.CompanyDB, message);
throw new Exception(message);
}
}
else
throw new Exception($"PoId '{poAbsEntry}' does not exists.");
}
}
However, I am getting the following error:
Error while changing due date for '145' to '11-09-2016', the following error is given 'Field cannot be updated (ODBC -1029)'.
And the error SAP gives is:
Field cannot be updated (ODBC -1029)
Additional information:
It is a new production order that has the status Planned.
Creation date is Today.
Due date I am trying to change it to is Today + 5 days.
The Id (AbsEntry) for the production order is 145.
SAP Business One 9.1
Yes, I can alter the due date in the SAP B1 GUI without problems.
Short, Self Contained, Correct (Compilable), Example
The code below gives me the exact same error. Replaced connection settings with ??.
using System;
using SAPbobsCOM; // Reference to SAP B1 DI SDK (32-bit)
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
var credentials = new SAPB1Credentials
{
Server = "??",
CompanyDB = "??",
Username = "??",
Password = "??"
};
SAPbobsCOM.Company sap = new SAPbobsCOM.Company();
sap.Server = credentials.Server;
sap.DbServerType = credentials.ServerType;
sap.CompanyDB = credentials.CompanyDB;
sap.UserName = credentials.Username;
sap.Password = credentials.Password;
sap.language = credentials.Language;
sap.UseTrusted = credentials.UseTrusted;
var returnCode = sap.Connect();
if (returnCode != 0)
{
var error = sap.GetLastErrorDescription();
throw new Exception(error);
}
ProductionOrders productionOrder = sap.GetBusinessObject(BoObjectTypes.oProductionOrders);
if (productionOrder.GetByKey(145))
{
productionOrder.DueDate = DateTime.Now.AddDays(5);
if (productionOrder.Update() != 0)
{
var error = sap.GetLastErrorDescription();
throw new Exception(error);
}
}
else
throw new Exception($"PoId does not exists.");
}
public class SAPB1Credentials
{
public string Server { get; set; }
public string CompanyDB { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public BoSuppLangs Language { get; set; } = BoSuppLangs.ln_English;
public BoDataServerTypes ServerType { get; set; } = BoDataServerTypes.dst_MSSQL2008;
public bool UseTrusted { get; set; } = false;
}
}
}
What am I missing, and why do I get this error?
From the linked SAP thread in the comment.
Edy Simon said:
Hi Lars,
Must be a Patch Level bug.
I can use your code to update it on SBO 881PL6.
You might want to test it on another version.
Regards
Edy
Which resolved the issue.

How can I save the settings in the IsolatedStorage while BackgroundAudioPlayer's instance is active?

I have Two projects in my Solution. Let's say Project A and Project B.
Project A
It's the main project and has settings. I have a check box to give user an option to "Repeat" track(s). This project can also access PROJECT B's public instances.
Project B
It's the BackgroundAudioAgent and has it's own settings. This project doesn't have access to PROJECT A settings. Therefore, in PROJECT A , I need to access the settings of PROJECT B and save it there. So that, when the "Repeat" is enabled, the agent restarts playing.
PROBLEM
I am unable to save the settings (in other words, the settings are saved, but it does not take any affect) when the BackgroundAudioPlayer's instance is running. I always have to close the instance, and when I do that, the settings can be changed.
QUESTION
What is the most efficient way to do what I am trying to do?
How can I save the settings in the IsolatedStorage without closing the BackgroundAudioPlayer's instance? (as I don't want to interrupt any track being played).
CODE: What I have to do to save settings.
public bool SettingAudioRepeat
{
get
{
return GetValueOrDefault<bool>(SettingAudioRepeatKeyName, SettingAudioRepeatDefault);
}
set
{
if (AddOrUpdateValue(SettingAudioRepeatKeyName, value))
{
bool resumePlay = false;
try
{
if (BackgroundAudioPlayer.Instance.PlayerState != PlayState.Shutdown)
{
BackgroundAudioPlayer.Instance.Close();
resumePlay = true;
}
}
catch { }
TaskEx.Delay(300);
IQR_Settings iqrSet = new IQR_Settings();
iqrSet.SettingAudioRepeat = value;
iqrSet.Save(); //Saving the settings for Project B
Save(); //Saving the settings for Project A
try
{
if (resumePlay)
BackgroundAudioPlayer.Instance.Play(); //It starts all from scracth
}
catch { }
}
}
public T GetValueOrDefault<T>(string Key, T defaultValue)
{
T value;
// If the key exists, retrieve the value.
if (settings.Contains(Key))
{
value = (T)settings[Key];
}
// Otherwise, use the default value.
else
{
value = defaultValue;
}
return value;
}
CODE: What I simply want to do.
public bool SettingAudioRepeat
{
get
{
return GetValueOrDefault<bool>(SettingAudioRepeatKeyName, SettingAudioRepeatDefault);
}
set
{
if (AddOrUpdateValue(SettingAudioRepeatKeyName, value))
{
IQR_Settings iqrSet = new IQR_Settings();
iqrSet.SettingAudioRepeat = value;
iqrSet.Save(); //Saving the settings for Project B
Save(); //Saving the settings for Project A
}
}
I agree that Background Audio is a breast. Whenever using any background agent you cannot rely on the ApplicationSettings to be synced. If you want to have settings saved and accessed from the UI (app) and background (audio agent) you should save a file. You can serialize the settings using Json.Net and save a file to a known location. Here is sample of what is might look like
// From background agent
var settings = Settings.Load();
if(settings.Foo)
{
// do something
}
And here is a sample Settings File. The settings would need to be saved on a regular basis.
public class Settings
{
private const string FileName = "shared/settings.json";
private Settings() { }
public bool Foo { get; set; }
public int Bar { get; set; }
public static Settings Load()
{
var storage = IsolatedStorageFile.GetUserStoreForApplication();
if (storage.FileExists(FileName) == false) return new Settings();
using (var stream = storage.OpenFile(FileName, FileMode.Open, FileAccess.Read))
{
using (var reader = new StreamReader(stream))
{
string json = reader.ReadToEnd();
if (string.IsNullOrEmpty(json) == false)
{
return JsonConvert.DeserializeObject<Settings>(json);
}
}
}
return new Settings();
}
public void Save()
{
var storage = IsolatedStorageFile.GetUserStoreForApplication();
if(storage.FileExists(FileName)) storage.DeleteFile(FileName);
using (var fileStream = storage.CreateFile(FileName))
{
//Write the data
using (var isoFileWriter = new StreamWriter(fileStream))
{
var json = JsonConvert.SerializeObject(this);
isoFileWriter.WriteLine(json);
}
}
}
}
I personally have a FileStorage class that I use for saving/loading data. I use it everywhere. Here it is (and it does use the Mutex to prevent access to the file from both background agent and app). You can find the FileStorage class here.

listing virtual directories in IIS 6.0 - C#

I have written a Windows Application.My question is:I have been listing virtual directories in IIS 6.0 with through code as below.I have to find pyhsical path of the virtual directory that are selected.
Also,DirectoryEntry class has a property called properties. But,I can't use it. Lastly,I getting the following the error.
The directory cannot report the number of properties
Code:
try
{
string serverName = "localhost";
string VirDirSchemaName = "IIsWebVirtualDir";
iisServer = new DirectoryEntry("IIS://" + serverName + "/W3SVC/1");
DirectoryEntry folderRoot = iisServer.Children.Find("Root",VirDirSchemaName);
return folderRoot.Children;
}
catch (Exception e)
{
throw new Exception("Error while retrieving virtual directories.",e);
}
why don't you use WMI
using System.DirectoryServices;
private DirectoryEntry _iisServer = null;
private DirectoryEntry iisServer
{
get
{
if (_iisServer == null)
{
string path = string.Format("IIS://{0}/W3SVC/1", serverName);
_iisServer = new DirectoryEntry(path);
}
return _iisServer;
}
}
private IDictionary<string, DirectoryEntry> _virtualDirectories = null;
private IDictionary<string, DirectoryEntry> virtualDirectories
{
get
{
if (_virtualDirectories == null)
{
_virtualDirectories = new Dictionary<string, DirectoryEntry>();
DirectoryEntry folderRoot = iisServer.Children.Find("Root", VirDirSchemaName);
foreach (DirectoryEntry virtualDirectory in folderRoot.Children)
{
_virtualDirectories.Add(virtualDirectory.Name, virtualDirectory);
}
}
return _virtualDirectories;
}
}
List all virtual directories in IIS 5,6 and 7

How do you upgrade Settings.settings when the stored data type changes?

I have an application that stores a collection of objects in the user settings, and is deployed via ClickOnce. The next version of the applications has a modified type for the objects stored. For example, the previous version's type was:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
And the new version's type is:
public class Person
{
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
}
Obviously, ApplicationSettingsBase.Upgrade wouldn't know how to perform an upgrade, since Age needs to be converted using (age) => DateTime.Now.AddYears(-age), so only the Name property would be upgraded, and DateOfBirth would just have the value of Default(DateTime).
So I'd like to provide an upgrade routine, by overriding ApplicationSettingsBase.Upgrade, that would convert the values as needed. But I've ran into three problems:
When trying to access the previous version's value using ApplicationSettingsBase.GetPreviousVersion, the returned value would be an object of the current version, which doesn't have the Age property and has an empty DateOfBirth property (since it can't deserialize Age into DateOfBirth).
I couldn't find a way to find out from which version of the application I'm upgrading. If there is an upgrade procedure from v1 to v2 and a procedure from v2 to v3, if a user is upgrading from v1 to v3, I need to run both upgrade procedures in order, but if the user is upgrading from v2, I only need to run the second upgrade procedure.
Even if I knew what the previous version of the application is, and I could access the user settings in their former structure (say by just getting a raw XML node), if I wanted to chain upgrade procedures (as described in issue 2), where would I store the intermediate values? If upgrading from v2 to v3, the upgrade procedure would read the old values from v2 and write them directly to the strongly-typed settings wrapper class in v3. But if upgrading from v1, where would I put the results of the v1 to v2 upgrade procedure, since the application only has a wrapper class for v3?
I thought I could avoid all these issues if the upgrade code would perform the conversion directly on the user.config file, but I found no easy way to get the location of the user.config of the previous version, since LocalFileSettingsProvider.GetPreviousConfigFileName(bool) is a private method.
Does anyone have a ClickOnce-compatible solution for upgrading user settings that change type between application versions, preferably a solution that can support skipping versions (e.g. upgrading from v1 to v3 without requiring the user to in install v2)?
I ended up using a more complex way to do upgrades, by reading the raw XML from the user settings file, then run a series of upgrade routines that refactor the data to the way it's supposed to be in the new next version. Also, due to a bug I found in ClickOnce's ApplicationDeployment.CurrentDeployment.IsFirstRun property (you can see the Microsoft Connect feedback here), I had to use my own IsFirstRun setting to know when to perform the upgrade. The whole system works very well for me (but it was made with blood and sweat due to a few very stubborn snags). Ignore comments mark what is specific to my application and is not part of the upgrade system.
using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Xml;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using System.Reflection;
using System.Text;
using MyApp.Forms;
using MyApp.Entities;
namespace MyApp.Properties
{
public sealed partial class Settings
{
private static readonly Version CurrentVersion = Assembly.GetExecutingAssembly().GetName().Version;
private Settings()
{
InitCollections(); // ignore
}
public override void Upgrade()
{
UpgradeFromPreviousVersion();
BadDataFiles = new StringCollection(); // ignore
UpgradePerformed = true; // this is a boolean value in the settings file that is initialized to false to indicate that settings file is brand new and requires upgrading
InitCollections(); // ignore
Save();
}
// ignore
private void InitCollections()
{
if (BadDataFiles == null)
BadDataFiles = new StringCollection();
if (UploadedGames == null)
UploadedGames = new StringDictionary();
if (SavedSearches == null)
SavedSearches = SavedSearchesCollection.Default;
}
private void UpgradeFromPreviousVersion()
{
try
{
// This works for both ClickOnce and non-ClickOnce applications, whereas
// ApplicationDeployment.CurrentDeployment.DataDirectory only works for ClickOnce applications
DirectoryInfo currentSettingsDir = new FileInfo(ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath).Directory;
if (currentSettingsDir == null)
throw new Exception("Failed to determine the location of the settings file.");
if (!currentSettingsDir.Exists)
currentSettingsDir.Create();
// LINQ to Objects for .NET 2.0 courtesy of LINQBridge (linqbridge.googlecode.com)
var previousSettings = (from dir in currentSettingsDir.Parent.GetDirectories()
let dirVer = new { Dir = dir, Ver = new Version(dir.Name) }
where dirVer.Ver < CurrentVersion
orderby dirVer.Ver descending
select dirVer).FirstOrDefault();
if (previousSettings == null)
return;
XmlElement userSettings = ReadUserSettings(previousSettings.Dir.GetFiles("user.config").Single().FullName);
userSettings = SettingsUpgrader.Upgrade(userSettings, previousSettings.Ver);
WriteUserSettings(userSettings, currentSettingsDir.FullName + #"\user.config", true);
Reload();
}
catch (Exception ex)
{
MessageBoxes.Alert(MessageBoxIcon.Error, "There was an error upgrading the the user settings from the previous version. The user settings will be reset.\n\n" + ex.Message);
Default.Reset();
}
}
private static XmlElement ReadUserSettings(string configFile)
{
// PreserveWhitespace required for unencrypted files due to https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=352591
var doc = new XmlDocument { PreserveWhitespace = true };
doc.Load(configFile);
XmlNode settingsNode = doc.SelectSingleNode("configuration/userSettings/MyApp.Properties.Settings");
XmlNode encryptedDataNode = settingsNode["EncryptedData"];
if (encryptedDataNode != null)
{
var provider = new RsaProtectedConfigurationProvider();
provider.Initialize("userSettings", new NameValueCollection());
return (XmlElement)provider.Decrypt(encryptedDataNode);
}
else
{
return (XmlElement)settingsNode;
}
}
private static void WriteUserSettings(XmlElement settingsNode, string configFile, bool encrypt)
{
XmlDocument doc;
XmlNode MyAppSettings;
if (encrypt)
{
var provider = new RsaProtectedConfigurationProvider();
provider.Initialize("userSettings", new NameValueCollection());
XmlNode encryptedSettings = provider.Encrypt(settingsNode);
doc = encryptedSettings.OwnerDocument;
MyAppSettings = doc.CreateElement("MyApp.Properties.Settings").AppendNewAttribute("configProtectionProvider", provider.GetType().Name);
MyAppSettings.AppendChild(encryptedSettings);
}
else
{
doc = settingsNode.OwnerDocument;
MyAppSettings = settingsNode;
}
doc.RemoveAll();
doc.AppendNewElement("configuration")
.AppendNewElement("userSettings")
.AppendChild(MyAppSettings);
using (var writer = new XmlTextWriter(configFile, Encoding.UTF8) { Formatting = Formatting.Indented, Indentation = 4 })
doc.Save(writer);
}
private static class SettingsUpgrader
{
private static readonly Version MinimumVersion = new Version(0, 2, 1, 0);
public static XmlElement Upgrade(XmlElement userSettings, Version oldSettingsVersion)
{
if (oldSettingsVersion < MinimumVersion)
throw new Exception("The minimum required version for upgrade is " + MinimumVersion);
var upgradeMethods = from method in typeof(SettingsUpgrader).GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
where method.Name.StartsWith("UpgradeFrom_")
let methodVer = new { Version = new Version(method.Name.Substring(12).Replace('_', '.')), Method = method }
where methodVer.Version >= oldSettingsVersion && methodVer.Version < CurrentVersion
orderby methodVer.Version ascending
select methodVer;
foreach (var methodVer in upgradeMethods)
{
try
{
methodVer.Method.Invoke(null, new object[] { userSettings });
}
catch (TargetInvocationException ex)
{
throw new Exception(string.Format("Failed to upgrade user setting from version {0}: {1}",
methodVer.Version, ex.InnerException.Message), ex.InnerException);
}
}
return userSettings;
}
private static void UpgradeFrom_0_2_1_0(XmlElement userSettings)
{
// ignore method body - put your own upgrade code here
var savedSearches = userSettings.SelectNodes("//SavedSearch");
foreach (XmlElement savedSearch in savedSearches)
{
string xml = savedSearch.InnerXml;
xml = xml.Replace("IRuleOfGame", "RuleOfGame");
xml = xml.Replace("Field>", "FieldName>");
xml = xml.Replace("Type>", "Comparison>");
savedSearch.InnerXml = xml;
if (savedSearch["Name"].GetTextValue() == "Tournament")
savedSearch.AppendNewElement("ShowTournamentColumn", "true");
else
savedSearch.AppendNewElement("ShowTournamentColumn", "false");
}
}
}
}
}
The following custom extention methods and helper classes were used:
using System;
using System.Windows.Forms;
using System.Collections.Generic;
using System.Xml;
namespace MyApp
{
public static class ExtensionMethods
{
public static XmlNode AppendNewElement(this XmlNode element, string name)
{
return AppendNewElement(element, name, null);
}
public static XmlNode AppendNewElement(this XmlNode element, string name, string value)
{
return AppendNewElement(element, name, value, null);
}
public static XmlNode AppendNewElement(this XmlNode element, string name, string value, params KeyValuePair<string, string>[] attributes)
{
XmlDocument doc = element.OwnerDocument ?? (XmlDocument)element;
XmlElement addedElement = doc.CreateElement(name);
if (value != null)
addedElement.SetTextValue(value);
if (attributes != null)
foreach (var attribute in attributes)
addedElement.AppendNewAttribute(attribute.Key, attribute.Value);
element.AppendChild(addedElement);
return addedElement;
}
public static XmlNode AppendNewAttribute(this XmlNode element, string name, string value)
{
XmlAttribute attr = element.OwnerDocument.CreateAttribute(name);
attr.Value = value;
element.Attributes.Append(attr);
return element;
}
}
}
namespace MyApp.Forms
{
public static class MessageBoxes
{
private static readonly string Caption = "MyApp v" + Application.ProductVersion;
public static void Alert(MessageBoxIcon icon, params object[] args)
{
MessageBox.Show(GetMessage(args), Caption, MessageBoxButtons.OK, icon);
}
public static bool YesNo(MessageBoxIcon icon, params object[] args)
{
return MessageBox.Show(GetMessage(args), Caption, MessageBoxButtons.YesNo, icon) == DialogResult.Yes;
}
private static string GetMessage(object[] args)
{
if (args.Length == 1)
{
return args[0].ToString();
}
else
{
var messegeArgs = new object[args.Length - 1];
Array.Copy(args, 1, messegeArgs, 0, messegeArgs.Length);
return string.Format(args[0] as string, messegeArgs);
}
}
}
}
The following Main method was used to allow the system to work:
[STAThread]
static void Main()
{
// Ensures that the user setting's configuration system starts in an encrypted mode, otherwise an application restart is required to change modes.
Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal);
SectionInformation sectionInfo = config.SectionGroups["userSettings"].Sections["MyApp.Properties.Settings"].SectionInformation;
if (!sectionInfo.IsProtected)
{
sectionInfo.ProtectSection(null);
config.Save();
}
if (Settings.Default.UpgradePerformed == false)
Settings.Default.Upgrade();
Application.Run(new frmMain());
}
I welcome any input, critique, suggestions or improvements. I hope this helps someone somewhere.
This may not really be the answer you are looking for but it sounds like you are overcomplicating the problem by trying to manage this as an upgrade where you aren't going to continue to support the old version.
The problem isn't simply that the data type of a field is changing, the problem is that you are totally changing the business logic behind the object and need to support objects that have data relating to both old and new business logic.
Why not just continue to have a person class which has all 3 properties on it.
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public DateTime DateOfBirth { get; set; }
}
When the user upgrades to the new version, the age is still stored, so when you access the DateOfBirth field you just check if a DateOfBirth exists, and if it doesn't you calculate it from the age and save it so when you next access it, it already has a date of birth and the age field can be ignored.
You could mark the age field as obsolete so you remember not to use it in future.
If necessary you could add some kind of private version field to the person class so internally it knows how to handle itself depending on what version it considers itself to be.
Sometimes you do have to have objects that aren't perfect in design because you still have to support data from old versions.
I know this has already been answered but I have been toying with this and wanted to add a way I handled a similar (not the same) situation with Custom Types:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
private DateTime _dob;
public DateTime DateOfBirth
{
get
{
if (_dob is null)
{ _dob = DateTime.Today.AddYears(Age * -1); }
else { return _dob; }
}
set { _dob = value; }
}
}
If both the private _dob and public Age is null or 0, you have another issue all together. You could always set DateofBirth to DateTime.Today by default in that case. Also, if all you have is an individual's age, how will you tell their DateOfBirth down to the day?

Categories