I've been trying to create some custom check-in policies in TFS 2010 recently, I created a DLL and added a registry key as outlined in the following link:
https://msdn.microsoft.com/en-us/library/bb668980.aspx
(Note: I already know this link is retired, but I'm unable to do it in the updated fashion indicated here: http://blogs.msdn.com/b/jimlamb/archive/2010/03/31/how-to-implement-package-and-deploy-custom-check-in-policy-for-tfs-2010.aspx)
I didn't necessarily expect my own DLL to work correctly from the start in a functional sense, but Step 4 only shows the out of the box policies, mine is not available.
What could be causing mine to not appear? Below is the code I'm using (I've removed what it actually does as I don't think that's important)
[Serializable]
public class CheckInPolicy : PolicyBase
{
private string _status = string.Empty;
public override string Description
{
get { return "My description"; }
}
public override string Type
{
get { return "My type"; }
}
public override string TypeDescription
{
get { return "My type description"; }
}
public override bool Edit(IPolicyEditArgs policyEditArgs)
{
return true;
}
public override PolicyFailure[] Evaluate()
{
// Check if the check in meets the policy rules
// ...
// ...
// ...
}
public override void Activate(PolicyFailure failure)
{
base.Activate(failure);
}
public override void DisplayHelp(PolicyFailure failure)
{
MessageBox.Show(
"My policy help notification", "Policy Help");
}
}
This link mentioned that the policy needs to be placed in a different registry path if the OS is x64, but the server I'm using is a 32-bit Windows Server 2003 machine. The key is placed in:
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\TeamFoundation\SourceControl\Checkin Policies]
I guess you have a 64-bit machine, in that case the correct registry entry would be the 32-bit path:
[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\11.0\TeamFoundation\SourceControl\Checkin Policies
In addition to this, you usually need to build the Policy for each version of Visual Studio, as it also has references to the appropriate TeamExplorer libraries.
EDIT:
You can prohibit developers from overriding policies, if you extend your server side with a plugin, that declines all checkins with overridden policies.
You need to place the DLL under this TFS path (of course depending on your version):
Program Files\Microsoft Team Foundation Server 11.0\Application Tier\Web Services\bin\Plugins\
Here the code of this plugin class (some of the referenced DLLs can only be found on TFS servers). We allow to override if all files are in published folder, but you can of course define your own exclusions:
using System;
using Microsoft.TeamFoundation.Common;
using Microsoft.TeamFoundation.Framework.Server;
using Microsoft.TeamFoundation.VersionControl.Server;
using System.IO;
using System.Text.RegularExpressions;
using Microsoft.TeamFoundation.Framework.Server.Alm;
namespace TFS.CheckinPolicyOverrideEventHandler
{
public sealed class CheckinPolicyOverrideEventHandler : ISubscriber
{
public Type[] SubscribedTypes()
{
return new Type[1] { typeof(CheckinNotification) };
}
public EventNotificationStatus ProcessEvent(TeamFoundationRequestContext requestContext, NotificationType notificationType, object notificationEventArgs,
out int statusCode, out string statusMessage, out ExceptionPropertyCollection properties)
{
statusCode = 0;
properties = null;
statusMessage = String.Empty;
try
{
if (notificationType == NotificationType.DecisionPoint && notificationEventArgs is CheckinNotification)
{
CheckinNotification ev = notificationEventArgs as CheckinNotification;
if (ev != null && ev.PolicyOverrideInfo != null)
{
if (ev.PolicyOverrideInfo.PolicyFailures != null)
{
// One or more of the checkin policies have been overridden
// If all the files being checked in are in the published folder, then allow overridding the policies since those are installation packages
foreach (string file in ev.GetSubmittedItems(null))
{
if (!Regex.IsMatch(file, #"/published", RegexOptions.IgnoreCase) &&
!Regex.IsMatch(Path.GetDirectoryName(file), #"/published", RegexOptions.IgnoreCase))
{
statusCode = -1;
break;
}
}
if (statusCode != 0)
{
// One or more of the checkin policies have been overridden and not all files are installation files (in the published folder)
statusMessage = Resource.CheckinCancelledStatusMessage;
foreach (PolicyFailureInfo policy in ev.PolicyOverrideInfo.PolicyFailures)
{
statusMessage = String.Concat(statusMessage, "\n > ", policy.PolicyName, ": ", policy.Message);
}
return EventNotificationStatus.ActionDenied;
}
}
}
return EventNotificationStatus.ActionPermitted;
}
}
catch (Exception exception)
{
// decide what you want to do, if exception occurs
}
return EventNotificationStatus.ActionPermitted;
}
public string Name
{
get { return "TFS.CheckinPolicyOverrideEventHandler"; }
}
public SubscriberPriority Priority
{
get { return SubscriberPriority.Normal; }
}
}
}
Related
I am attempting to retrieve the typical AssemblyInfo attributes from an executable file, but not from the currently executing assembly. I wish to 'look into' a program file (.exe) elsewhere on the drive that I have written in C#.NET and check the AssemblyProduct string.
This is fairly easy and straightforward when you're looking for this information from the currently executing assembly. However, apparently not so much when you attempt to pull it from an unloaded assembly.
When I use the following code, it returns "Microsoft® .NET Framework" instead of the Product name that I put in my AssemblyInfo.cs file.
Note: I use the System.Reflection.AssemblyName object to pull the version info e.g:AssemblyName.GetAssemblyName(pathToAssembly) and this works correctly, but I'm unable to pull my assembly's attributes using that class or by any means I've tried thus far. Is there some other special class, or what am I missing or doing incorrectly here?
public static string GetAppProdIDFromPath(string pathToForeignAssembly)
{
var atts = GetForeignAssemblyAttributes(pathToForeignAssembly);
var id = string.Empty;
foreach (var att in atts)
{
if (att.GetType() == typeof(AssemblyProductAttribute))
{
id = ((AssemblyProductAttribute)att).Product;
}
}
return id;
}
private static object[] GetForeignAssemblyAttributes(string pathToAssembly)
{
if(File.Exists(pathToAssembly))
{
try
{
var assm = System.Reflection.Assembly.LoadFrom(pathToAssembly);
return assm.GetType().Assembly.GetCustomAttributes(false);
}
catch(Exception ex)
{
// logger etc
}
}
else
{
throw...
}
return null;
}
As Duncanp mentioned, there is a bug in my code. Posting it for clarity and for anyone down the road who looks for the same solution:
public static string GetAppProdIDFromPath(string pathToForeignAssembly)
{
var atts = GetForeignAssemblyAttributes(pathToForeignAssembly);
var id = string.Empty;
foreach (var att in atts)
{
if (att.GetType() == typeof(AssemblyProductAttribute))
{
id = ((AssemblyProductAttribute)att).Product;
}
}
return id;
}
private static object[] GetForeignAssemblyAttributes(string pathToAssembly)
{
if(File.Exists(pathToAssembly))
{
try
{
var assm = System.Reflection.Assembly.LoadFrom(pathToAssembly);
return assm.GetCustomAttributes(false); // fixed line
}
catch(Exception ex)
{
// logger etc
}
}
else
{
throw...
}
return null;
}
Visual Studio has the option to apply a Build Action for the App.config file as "Embedded Resource", which means including in the same final exe the content of the App.config.
Fine.
The problem is: how to read the data inside the embedded App.config? For example an appSetting value from a given key?
The code I used before to read from the App.config (the one phisically written on the disk which usually is nameoftheprogram.exe.config), seems to be not working anymore.
string s = System.Configuration.ConfigurationManager.AppSettings["mykey"];
Probably it must be re-adapted with other C# classes designed for this job.
Any ideas?
You can have interface IConfigUtility with method :
IConfigUtility.cs:
public interface IConfigUtility
{
string LogFilePath
{
get;
}
string GetAppSetting(string key);
}
ConfigUtility.cs
using System;
using System.Configuration;
public class ConfigUtility : IConfigUtility
{
Configuration config = null;
public string LogFilePath
{
get
{
return GetAppSetting(#"Code to read the log file path");
}
}
public ConfigUtility()
{
var exeConfigPath = this.GetType().Assembly.Location;
try
{
config = ConfigurationManager.OpenExeConfiguration(exeConfigPath);
}
catch (Exception)
{
//handle error here.. means DLL has no satellite configuration file.
}
}
public virtual string GetAppSetting(string key)
{
if (config != null)
{
KeyValueConfigurationElement element = config.AppSettings.Settings[key];
if (element != null)
{
string value = element.Value;
if (!string.IsNullOrEmpty(value))
return value;
}
}
return string.Empty;
}
}
Now you can use the above ConfigUtility.cs and read your appsettings key from the App.config file
How can I iterate through file names in the 'Default Document' IIS feature for an application in C#?
I am using Microsoft.Web.Administration.ServerManager.
Thanks
You can do it using its "loosely" typed model, basically for reading it for Default Web Site would look like:
using (ServerManager serverManager = new ServerManager())
{
Configuration webConfig = serverManager.GetWebConfiguration("Default Web Site");
ConfigurationSection section = webConfig.GetSection("system.webServer/defaultDocument");
foreach (ConfigurationElement item in section.GetCollection("files"))
{
Console.WriteLine(item["value"]);
}
}
You can also generate strongly-typed wrappers for collections and entries you want to use frequently which makes it a lot cleaner and prevents mistakes, that would make it look like:
using (ServerManager serverManager = new ServerManager())
{
Configuration webConfig = serverManager.GetWebConfiguration("Default Web Site");
DefaultDocumentSection section = (DefaultDocumentSection)webConfig.GetSection("system.webServer/defaultDocument", typeof(DefaultDocumentSection));
foreach (FileElement item in section.Files)
{
Console.WriteLine(item.Value);
}
}
And for that to work you need the following "strongly-typed-wrappers":
public class DefaultDocumentSection : ConfigurationSection
{
private FilesCollection _files;
public FilesCollection Files
{
get
{
if (_files == null)
{
_files = (FilesCollection)base.GetCollection("files", typeof(FilesCollection));
}
return _files;
}
}
}
public class FilesCollection : ConfigurationElementCollectionBase<FileElement>
{
protected override FileElement CreateNewElement(string elementTagName)
{
return new FileElement();
}
}
public class FileElement : ConfigurationElement
{
public string Value { get { return (string)base["value"]; } }
}
I found plenty of partial answers, but nothing really sufficient.
The case:
App is a working command line app, with no user interaction, except for the ability to receive an key press on enter to stop, this is already written in a way that disables even that when not run as Environment.UserInteractive == true.
I'm using Visual Studio 2010.
The problem is I need to convert this app to a windows service. Is it "just" making a new class file as a service, and have it call my start and stop methods on the existing application?
How about the installer (VS' default msi installer), can the existing installer project be "upgraded" to handle the Service installation as well?
I messed with this before, and ended up with an installer that kept refusing to install, as it kept detecting the service as already being installed, stopping the install process by then promptly rolling back everything. The service it detected were the one it had just installed.
To run a console app as either a Windows Service or a Console application, write a single console application and use command line arguments to determine if you should run directly or start up the service. Include an installer/uninstaller to install as a windows service with the right command line arguments.
Here's a base class we use that provides this functionality.
using System;
using System.Collections;
using System.Configuration.Install;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.ServiceProcess;
using System.Windows.Forms;
using Microsoft.Win32;
namespace Console40
{
public abstract class AbstractService : ServiceBase
{
public static AbstractService Current { get; private set; }
protected virtual string HelpTextPattern
{
get
{
#region Help Text
return
#"
USAGE
{0} [command]
WHERE [command] is one of
/console - run as a console application, for debugging
/service - run as a windows service
/install - install as a windows service
/uninstall - uninstall windows service
";
#endregion
}
}
public abstract string DisplayName { get; }
public ServiceExecutionMode ServiceExecutionMode { get; private set; }
protected abstract Guid UninstallGuid { get; }
protected virtual string UninstallRegKeyPath
{
get
{
return #"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
}
}
protected AbstractService(string serviceName)
{
ServiceName = serviceName;
if (Current != null)
{
throw new InvalidOperationException(String.Format(
"Service {0} is instantiating but service {1} is already instantiated as current. References to AbstractService.Current will only point to the first service.",
GetType().FullName,
Current.GetType().FullName));
}
Current = this;
}
public void Run(string[] args)
{
Environment.CurrentDirectory = AppDomain.CurrentDomain.BaseDirectory;
if (args.Length == 0 && Debugger.IsAttached)
{
args = new[] { "/console" };
}
if (args.Length == 0)
{
Console.WriteLine(HelpTextPattern, Path.GetFileName(GetType().Assembly.CodeBase));
}
else
{
switch (args[0].ToLower())
{
case "/service":
ServiceExecutionMode = ServiceExecutionMode.Service;
Run(new[] { this });
break;
case "/console":
ServiceExecutionMode = ServiceExecutionMode.Console;
Console.WriteLine("Starting Service...");
OnStart(new string[0]);
OnStartCommandLine();
OnStop();
break;
case "/install":
ServiceExecutionMode = ServiceExecutionMode.Install;
InstallService();
break;
case "/uninstall":
ServiceExecutionMode = ServiceExecutionMode.Uninstall;
UninstallService();
break;
case "/uninstallprompt":
ServiceExecutionMode = ServiceExecutionMode.Uninstall;
if (ConfirmUninstall())
{
UninstallService();
InformUninstalled();
}
break;
default:
if (!OnCustomCommandLine(args))
{
Console.WriteLine(HelpTextPattern, Path.GetFileName(GetType().Assembly.CodeBase));
}
break;
}
}
}
protected override void OnStart(string[] args)
{
OnStartImpl(args);
AppDomain.CurrentDomain.UnhandledException += OnCurrentDomainUnhandledException;
}
protected virtual void OnStartCommandLine()
{
Console.WriteLine("Service is running... Hit ENTER to break.");
Console.ReadLine();
}
protected abstract void OnStartImpl(string[] args);
void OnCurrentDomainUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
// do something useful here, log it..
}
protected override void OnShutdown()
{
Stop();
}
protected override void OnStop()
{
OnStopImpl();
}
protected abstract void OnStopImpl();
protected virtual bool OnCustomCommandLine(string[] args)
{
// for extension
return false;
}
private void InstallService()
{
GetInstaller(".InstallLog").Install(new Hashtable());
InstallServiceCommandLine();
CreateUninstaller();
}
private void InstallServiceCommandLine()
{
string keyParent = #"SYSTEM\CurrentControlSet\Services\" + ServiceName;
const string VALUE_NAME = "ImagePath";
try
{
using (RegistryKey key = Registry.LocalMachine.OpenSubKey(keyParent, true))
{
if (key == null)
{
throw new InvalidOperationException("Service not found in registry.");
}
var origPath = key.GetValue(VALUE_NAME) as string;
if (origPath == null)
{
throw new Exception("HKLM\\" + keyParent + "\\" + VALUE_NAME + " does not exist but was expected.");
}
key.SetValue(VALUE_NAME, origPath.Replace("\"\"", "\"") + " /service");
}
}
catch (Exception ex)
{
throw new Exception(
"Error updating service command line after installation. Unable to write to HKLM\\" + keyParent, ex);
}
}
private void CreateUninstaller()
{
using (RegistryKey parent = Registry.LocalMachine.OpenSubKey(UninstallRegKeyPath, true))
{
if (parent == null)
{
throw new Exception(String.Format("Uninstall registry key '{0}' not found.", UninstallRegKeyPath));
}
try
{
RegistryKey key = null;
try
{
string guidText = UninstallGuid.ToString("B");
key = parent.OpenSubKey(guidText, true) ??
parent.CreateSubKey(guidText);
if (key == null)
{
throw new Exception(String.Format("Unable to create uninstaller '{0}\\{1}'", UninstallRegKeyPath, guidText));
}
Assembly asm = GetType().Assembly;
Version v = asm.GetName().Version;
string exe = "\"" + asm.CodeBase.Substring(8).Replace("/", "\\\\") + "\"";
key.SetValue("DisplayName", DisplayName);
key.SetValue("ApplicationVersion", v.ToString());
key.SetValue("Publisher", "B-Line Medical");
key.SetValue("DisplayIcon", exe);
key.SetValue("DisplayVersion", v.ToString(2));
key.SetValue("URLInfoAbout", "http://www.blinemedical.com");
key.SetValue("Contact", "support#blinemedical.com");
key.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd"));
key.SetValue("UninstallString", exe + " /uninstallprompt");
}
finally
{
if (key != null)
{
key.Close();
}
}
}
catch (Exception ex)
{
throw new Exception(
"An error occurred writing uninstall information to the registry. The service is fully installed but can only be uninstalled manually through the command line.",
ex);
}
}
}
private bool ConfirmUninstall()
{
string title = "Uninstall " + DisplayName;
string text = "Are you sure you want to remove " + DisplayName + " from your computer?";
return DialogResult.Yes ==
MessageBox.Show(text, title, MessageBoxButtons.YesNo, MessageBoxIcon.Question,
MessageBoxDefaultButton.Button2);
}
private void InformUninstalled()
{
string title = "Uninstall " + DisplayName;
string text = DisplayName + " has been uninstalled.";
MessageBox.Show(text, title, MessageBoxButtons.OK, MessageBoxIcon.Information);
}
private void UninstallService()
{
GetInstaller(".UninstallLog").Uninstall(null);
RemoveUninstaller();
}
private TransactedInstaller GetInstaller(string logExtension)
{
var ti = new TransactedInstaller();
ti.Installers.Add(new ServiceProcessInstaller
{
Account = ServiceAccount.LocalSystem
});
ti.Installers.Add(new ServiceInstaller
{
DisplayName = DisplayName,
ServiceName = ServiceName,
StartType = ServiceStartMode.Automatic
});
string basePath = Assembly.GetEntryAssembly().Location;
String path = String.Format("/assemblypath=\"{0}\"", basePath);
ti.Context = new InstallContext(Path.ChangeExtension(basePath, logExtension), new[] { path });
return ti;
}
private void RemoveUninstaller()
{
using (RegistryKey key = Registry.LocalMachine.OpenSubKey(UninstallRegKeyPath, true))
{
if (key == null)
{
return;
}
try
{
string guidText = UninstallGuid.ToString("B");
RegistryKey child = key.OpenSubKey(guidText);
if (child != null)
{
child.Close();
key.DeleteSubKey(guidText);
}
}
catch (Exception ex)
{
throw new Exception(
"An error occurred removing uninstall information from the registry. The service was uninstalled will still show up in the add/remove program list. To remove it manually delete the entry HKLM\\" +
UninstallRegKeyPath + "\\" + UninstallGuid, ex);
}
}
}
}
public enum ServiceExecutionMode
{
Unknown,
Service,
Console,
Install,
Uninstall,
Custom
}
}
Best thing for you to do is to start a new Project as a windows service.
In this new project you will find Service1.cs and this is the file that will be run from start. the following code will be int the file:
namespace WindowsService1
{
public partial class Service1 : ServiceBase
{
public Service1()
{
InitializeComponent();
}
protected override void OnStart(string[] args)
{
}
protected override void OnStop()
{
}
}
}
It isnt that hard to figure out what to do from here. Simply add your classes to the project and make sure that you copy your main code in the the OnStart() function. Of course you might have to slightly edit the code to make sure it has no readlines in it.
Now you must create and installer. How you can do this can be found here:
http://msdn.microsoft.com/en-us/library/zt39148a%28v=vs.100%29.aspx
I hope this helped :D
Kind Regards
RoXaS
Keep C# application running
public partial class DemoService : ServiceBase
{
static void Main(string[] args)
{
DemoService service = new DemoService();
if (Environment.UserInteractive)
{
service.OnStart(args);
Console.WriteLine("Press any key to stop program");
Console.Read();
service.OnStop();
}
else
{
ServiceBase.Run(service);
}
}
Check the link above. I provide some code as well as a link describing using a console for double-duty as Console and Service. I will use a console project and check for UserInteractive before running as a service. This way you can debug as if it's a console, but install it as a service on a production server.
With regards to installing, I don't have experience with installing from .msi, but we use a batch script to install the service (using sc.exe) and then its just a matter of replacing files if you update the code.
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?