Catch DllNotFoundException doesn't work - c#

I am trying to log the connected scanner on a pc.
I am using NTwain.dll from https://bitbucket.org/soukoku/ntwain.
If I run my app on a server some dependency dlls from ntwain fail to load so I will load the dll at runtime and if it will fail I just want to return an empty list. There is no reference to NTwain in the project references anymore.
Problem:
If I have the NTwain.dll in the folder with the exe and I run it on a server the app crashes. It doesn't return an empty list. If I delete the dll and run the app the empty list gets returned.
Code:
public class Scanner : IDB
{
private enum DataGroups : uint
{
None = 0,
Control = 0x1,
Image = 0x2,
Audio = 0x4,
Mask = 0xffff,
}
public string Name { get; private set; }
public string ProductFamily { get; private set; }
public string Version { get; private set; }
public Scanner()
{
Name = String.Empty;
}
public static List<Scanner> getScanners()
{
List<Scanner> scanners = new List<Scanner>();
try
{
Assembly assembly = Assembly.LoadFrom(Environment.CurrentDirectory + "\\NTwain.dll");
Type tident = assembly.GetType("NTwain.Data.TWIdentity");
Type tsession = assembly.GetType("NTwain.TwainSession");
object appId = tident.GetMethod("CreateFromAssembly").Invoke(null, new object[] { DataGroups.Image, System.Reflection.Assembly.GetExecutingAssembly() });
object session = Activator.CreateInstance(tsession, appId);
tsession.GetMethod("Open", new Type[0]).Invoke(session, null);
object sources = session.GetType().GetMethod("GetSources").Invoke(session, null);
foreach (var item in (IEnumerable)sources)
{
Scanner scanner = new Scanner();
scanner.Name = (string)item.GetType().GetProperty("Name").GetValue(item, null);
scanner.ProductFamily = (string)item.GetType().GetProperty("ProductFamily").GetValue(item, null);
object version = item.GetType().GetProperty("Version").GetValue(item, null);
scanner.Version = (string)version.GetType().GetProperty("Info").GetValue(version, null);
scanners.Add(scanner);
}
return scanners;
}
catch (Exception e)
{
return new List<Scanner>();
}
}
}

I would guess to catch a DllNotFoundException works in your code:
If u delete the dll then
Assembly assembly = Assembly.LoadFrom(Environment.CurrentDirectory + "\\NTwain.dll");
throws an DllNotFoundException which is catched by your catch block which says to return an empty list (which works as u say).
If u don't delete the dll then the code passes above line successfully. In case by the following lines a different thread is started and an error occurs that is not catched within that thread, then your catch block will not catch that error (whatever it might be) and the application crashes.

Related

Obtain Assembly attributes from unloaded executable

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 2010 EnvDTE.EditPoint Insert function returning 'Exception from HRESULT: 0x80041001'

Overview:
Microsoft Visual Studio is a tool in and of itself. Pun intended. I am trying to use EnvDTE from a System.Windows.Window that is being used as a plugin for Visual Studio. I do not want to access the project from which the plugin was created, rather the currently open project at the time the window is in use (whole different question). Anyway, I am running into an issue with the EnvDTE.EditPoint.Insert() method where I am getting the exception 'COM Exception was caught 'Exception from HRESULT: 0x80041001'' whenever I attempt to use it.
Here is my code:
public class DTEHelper
{
static private DTE _dte = null;
static private AddIn _addIn = null;
static private ProjectItem _pItem = null;
static private TextDocument _tDoc = null;
static private EditPoint _ePoint = null;
static private Project _projP = null;
static private SolutionBuild _sBuild = null;
public DTE dte { get { return _dte; } set { _dte = value; } }
public AddIn addIn { get { return _addIn; } set { _addIn = value; } }
public ProjectItem pItem { get { return _pItem; } set { _pItem = value; } }
public TextDocument tDoc { get { return _tDoc; } set { _tDoc = value; } }
public EditPoint ePoint { get { return _ePoint; } set { _ePoint = value; } }
public Project projP { get { return _projP; } set { _projP = value; } }
public SolutionBuild sBuild { get { return _sBuild; } set { _sBuild = value; } }
}
Problem Method:
private void addMethod(string file, DTEHelper dteHelper)
{
try
{
//Open existing file
System.Array projectFiles = (System.Array)dteHelper.dte.ActiveSolutionProjects;
EnvDTE.Project project = null;
if (projectFiles.Length > 0)
{
project = (EnvDTE.Project)(projectFiles.GetValue(0));
dteHelper.pItem = (from ProjectItem csFile in project.ProjectItems where csFile.Name.Contains(file) select csFile).First();
if (dteHelper.pItem.Name.Contains(".cs"))
{
EnvDTE.Window window = dteHelper.pItem.Open(Constants.vsViewKindCode);
TextDocument document = (TextDocument)window.Document.Object("TextDocument");
EditPoint edit = (EditPoint)document.CreateEditPoint();
edit.EndOfDocument();
edit.LineUp();
edit.Insert(""); //Problem Right Here
string fileName = dteHelper.pItem.get_FileNames(0);
dteHelper.pItem.SaveAs(fileName);
}
}
}
catch (Exception e)
{
MessageBox.Show(string.Format(System.Globalization.CultureInfo.CurrentUICulture, "Failed to add code to: " + file, this.ToString()),
"ERROR");
}
}
dteHelper.dte initialized when button is pressed in Window
dteHelper.dte = (EnvDTE.DTE)System.Runtime.InteropServices.Marshal.GetActiveObject("VisualStudio.DTE.10.0");
Turns out that everything is working fine, the only issue was that the file I was searching for ('foo.cs') was not 'in' the project (not recognized by the compiler) so I had to add it to the solution.

How to use ErrorListener for IronRuby

I have a C# program to execute an IronRuby script. But before doing that, I'd like to compile the file first to see if there is any errors. But it seems the ErrorListener does not work well. Is there anything wrong with my code?
class Program
{
static void Main(string[] args)
{
try
{
ScriptEngine engine = null;
engine = Ruby.CreateEngine();
ScriptSource sc = engine.CreateScriptSourceFromFile("MainForm.rb");
ErrorListener errLis = new MyErrorListener();
sc.Compile(errLis);
}
}
class MyErrorListener : ErrorListener
{
public override void ErrorReported(ScriptSource source, string message, Microsoft.Scripting.SourceSpan span, int errorCode, Microsoft.Scripting.Severity severity)
{
Console.WriteLine(message);
}
}
Ruby file:
require "mscorlib"
require "System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
require "System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
include System::Windows::Forms
dfasdf error here
class MainForm < Form
def initialize()
self.InitializeComponent()
end
def InitializeComponent()
#
# MainForm
#
self.Name = "MainForm"
self.Text = "HelloRubyWin"
end
end
What you're trying to do seems to not actually work. Not sure if it's a bug or not, though.
To workaround it, just execute the code inside a try/catch block and look for MissingMethodExecption. Pay attention that this also will not help if the syntax error is inside a method since IronRuby (or any other dynamic language) doesn't do anything with "nested" code until it actually executes it.
So in general, I think you won't get a lot of value from what you're trying to do.
The try/catch code sample:
ScriptEngine engine = null;
engine = Ruby.CreateEngine(x => { x.ExceptionDetail = true; });
ScriptSource sc = engine.CreateScriptSourceFromFile("MainForm.rb");
ErrorListener errLis = new MyErrorListener();
sc.Compile(errLis);
try
{
dynamic d = sc.Execute();
}
catch (MissingMethodException e)
{
Console.WriteLine("Syntax error!");
}
I researched on this problem finally found the solution that is first write your ErrorListener Class in following Manger.
public class IronRubyErrors : ErrorListener
{
public string Message { get; set; }
public int ErrorCode { get; set; }
public Severity sev { get; set; }
public SourceSpan Span { get; set; }
public override void ErrorReported(ScriptSource source, string message, Microsoft.Scripting.SourceSpan span, int errorCode, Microsoft.Scripting.Severity severity)
{
Message = message;
ErrorCode = errorCode;
sev = severity;
Span = span;
}
}
then
var rubyEngine = Ruby.CreateEngine();
ScriptSource src = rubyEngine.CreateScriptSourceFromFile("test.rb");
IronRubyErrors error = new IronRubyErrors();
src.Compile(error);
if (error.ErrorCode != 0)
{
MessageBox.Show(string.Format("Discription {0} \r\nError at Line No.{1} and Column No{2}", error.Message, error.span.Start.Line, error.span.Start.Column));
}
try
{
if (error.ErrorCode == 0)
{
var res = src.Execute();
}
}
catch (MissingMethodException ex)
{
MessageBox.Show(ex.Message);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
Because Ironruby is DLR therefore it compiles On RunTime if characters which are not the keyword of IronRuby like (~ ,` , ^ and so On) OR you have created any Syntax Error other then Error will capture on Compile Method of ScriptSource and the object of ErrorListener Class will be filled and we will find ErrorCode etc. if you have used a Method which is not defined in Ruby Library for example you have typed the method for Number Conversion like this
#abc.to_
which is not correct then it will be caught in MissingMethodException Block
Correct method is
#abc.to_f
and if you got Error Other then these (like Divide by Zero Exception)then that will be caught in Exception Block

How to detect the status of msbuild from command line or C# Application

I am writing up a checkout, build and deployment application in C#, and need to know the best way to detect whether my call to msbuild.exe has succeeded or not. I have tried to use the error code from the process, but I am not sure whether this is always accurate.
Is there a way (through the code below) that I can tell whether msbuild.exe completed successfully?
try
{
Process msbProcess = new Process();
msbProcess.StartInfo.FileName = this.MSBuildPath;
msbProcess.StartInfo.Arguments = msbArguments;
msbProcess.Start();
msbProcess.WaitForExit();
if (msbProcess.ExitCode != 0)
{
//
}
else
{
//
}
msbProcess.Close();
}
catch (Exception ex)
{
//
}
As far as I've been able to determine, MSBuild returns an exit code greater then zero when it encounters an error. If it doesn't encounter any errors, it returns exit code 0. I've never seen it exit with code lower than 0.
I use it in a batch file:
msbuild <args>
if errorlevel 1 goto errorDone
In four years of using it this way, I've never had reason to question the correctness of this approach.
Several questions on the MSDN forums ask the same thing.
The standard response is, in effect, "if errorlevel is 0, then there was no error".
Sorry if I'm a little bit too late for the party... but nearly 7 years after the question was posted I wanted to see a complete answer for it. I did some tests using the code below, and here are the conclusions:
Analysis
msbuild.exe returns 1 when at least one build error occurs, and returns 0 when the build is successfully completed. At present, the program does not take warnings into account, which means a successful build with warnings causes msbuild.exe to still return 0.
Other errors like: trying to build a project that does not exist, or providing an incorrect argument (like /myInvalidArgument), will also cause msbuild.exe to return 1.
Source Code
The following C# code is a complete implementation for building your favorite projects by firing msbuild.exe from a command line. Don't forget to setup any necessary environment settings before compiling your projects.
Your BuildControl class:
using System;
namespace Example
{
public sealed class BuildControl
{
// ...
public bool BuildStuff()
{
MsBuilder builder = new MsBuilder(#"C:\...\project.csproj", "Release", "x86")
{
Target = "Rebuild", // for rebuilding instead of just building
};
bool success = builder.Build(out string buildOutput);
Console.WriteLine(buildOutput);
return success;
}
// ...
}
}
MsBuilder class: Builds stuff by calling MsBuild.exe from command line:
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Example
{
public sealed class MsBuilder
{
public string ProjectPath { get; }
public string LogPath { get; set; }
public string Configuration { get; }
public string Platform { get; }
public int MaxCpuCount { get; set; } = 1;
public string Target { get; set; } = "Build";
public string MsBuildPath { get; set; } =
#"C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MsBuild.exe";
public string BuildOutput { get; private set; }
public MsBuilder(string projectPath, string configuration, string platform)
{
ProjectPath = !string.IsNullOrWhiteSpace(projectPath) ? projectPath : throw new ArgumentNullException(nameof(projectPath));
if (!File.Exists(ProjectPath)) throw new FileNotFoundException(projectPath);
Configuration = !string.IsNullOrWhiteSpace(configuration) ? configuration : throw new ArgumentNullException(nameof(configuration));
Platform = !string.IsNullOrWhiteSpace(platform) ? platform : throw new ArgumentNullException(nameof(platform));
LogPath = Path.Combine(Path.GetDirectoryName(ProjectPath), $"{Path.GetFileName(ProjectPath)}.{Configuration}-{Platform}.msbuild.log");
}
public bool Build(out string buildOutput)
{
List<string> arguments = new List<string>()
{
$"/nologo",
$"\"{ProjectPath}\"",
$"/p:Configuration={Configuration}",
$"/p:Platform={Platform}",
$"/t:{Target}",
$"/maxcpucount:{(MaxCpuCount > 0 ? MaxCpuCount : 1)}",
$"/fileLoggerParameters:LogFile=\"{LogPath}\";Append;Verbosity=diagnostic;Encoding=UTF-8",
};
using (CommandLineProcess cmd = new CommandLineProcess(MsBuildPath, string.Join(" ", arguments)))
{
StringBuilder sb = new StringBuilder();
sb.AppendLine($"Build started: Project: '{ProjectPath}', Configuration: {Configuration}, Platform: {Platform}");
// Call MsBuild:
int exitCode = cmd.Run(out string processOutput, out string processError);
// Check result:
sb.AppendLine(processOutput);
if (exitCode == 0)
{
sb.AppendLine("Build completed successfully!");
buildOutput = sb.ToString();
return true;
}
else
{
if (!string.IsNullOrWhiteSpace(processError))
sb.AppendLine($"MSBUILD PROCESS ERROR: {processError}");
sb.AppendLine("Build failed!");
buildOutput = sb.ToString();
return false;
}
}
}
}
}
CommandLineProcess class - Starts a command line process and waits until it finishes. All standard output/error is captured, and no separate window is started for the process:
using System;
using System.Diagnostics;
using System.IO;
namespace Example
{
public sealed class CommandLineProcess : IDisposable
{
public string Path { get; }
public string Arguments { get; }
public bool IsRunning { get; private set; }
public int? ExitCode { get; private set; }
private Process Process;
private readonly object Locker = new object();
public CommandLineProcess(string path, string arguments)
{
Path = path ?? throw new ArgumentNullException(nameof(path));
if (!File.Exists(path)) throw new ArgumentException($"Executable not found: {path}");
Arguments = arguments;
}
public int Run(out string output, out string err)
{
lock (Locker)
{
if (IsRunning) throw new Exception("The process is already running");
Process = new Process()
{
EnableRaisingEvents = true,
StartInfo = new ProcessStartInfo()
{
FileName = Path,
Arguments = Arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
},
};
if (!Process.Start()) throw new Exception("Process could not be started");
output = Process.StandardOutput.ReadToEnd();
err = Process.StandardError.ReadToEnd();
Process.WaitForExit();
try { Process.Refresh(); } catch { }
return (ExitCode = Process.ExitCode).Value;
}
}
public void Kill()
{
lock (Locker)
{
try { Process?.Kill(); }
catch { }
IsRunning = false;
Process = null;
}
}
public void Dispose()
{
try { Process?.Dispose(); }
catch { }
}
}
}
PS: I'm using Visual Studio 2017 / .NET 4.7.2

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