I'm working on a Window Forms application in Visual Studio, and I'm using a custom settings object to keep track of some application settings.
The user can change these settings through the PropertyGrid widget.
This works great for string and integer values, but now I also want to add a List<string> variable, so the user can enter a list of keywords.
I've added the List<string> variable to the settings object and I've added a TypeConverter to show it as a comma separated string representation in the PropertyGrid. Without the TypeConverter the value would display as just (Collection). It is displayed correctly and I can edit it, see screenshot below
this._MyProps = new PropsClass();
this._MyProps.ReadFromIniFile("mysettings.ini");
propertyGrid1.SelectedObject = this._MyProps;
Now I also want to write and read these setting to a settings.ini file, so I've added SaveToIniFile and ReadFromIniFile methods to the object. This works for string and integer values, except the List<string> is not saved and loaded to and from the .ini file correctly. When I call SaveToIniFile the content mysettings.ini is for example this, still using the "(Collection)" representation and not the values entered by the user:
[DataConvert]
KeyWordNull=NaN
ReplaceItemsList=(Collection)
YearMaximum=2030
So my question is, how can I save/load a List<string> setting to an ini file while also allowing the user to edit it in a PropertyGrid?
I know it'd have to convert from a string to a List somehow, maybe using quotes around the string to inclkude the line breaks, or maybe just comma-separated back to a list of values? But anyway I thought that is what the TypeConverter was for. So why is it showing correctly in he PropertyGrid but not in the ini file? See code below
The custom settings properties object:
// MyProps.cs
public class PropsClass
{
[Description("Maximum year value."), Category("DataConvert"), DefaultValue(2050)]
public int YearMaximum { get; set; }
[Description("Null keyword, for example NaN or NULL, case sensitive."), Category("DataConvert"), DefaultValue("NULL")]
public string KeyWordNull { get; set; }
private List<string> _replaceItems = new List<string>();
[Description("List of items to replace."), Category("DataConvert"), DefaultValue("enter keywords here")]
[Editor("System.Windows.Forms.Design.StringCollectionEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[TypeConverter(typeof(StringListConverter))]
public List<string> ReplaceItemsList
{
get
{
return _replaceItems;
}
set
{
_replaceItems = value;
}
}
and in the same PropsClass class, the write and read methods to save/load from a settings.ini file
[DllImport("kernel32.dll")]
public static extern int GetPrivateProfileSection(string lpAppName, byte[] lpszReturnBuffer, int nSize, string lpFileName);
public void SaveToIniFile(string filename)
{
// write to ini file
using (var fp = new StreamWriter(filename, false, Encoding.UTF8))
{
// for each different section
foreach (var section in GetType()
.GetProperties()
.GroupBy(x => ((CategoryAttribute)x.GetCustomAttributes(typeof(CategoryAttribute), false)
.FirstOrDefault())?.Category ?? "General"))
{
fp.WriteLine(Environment.NewLine + "[{0}]", section.Key);
foreach (var propertyInfo in section.OrderBy(x => x.Name))
{
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
fp.WriteLine("{0}={1}", propertyInfo.Name, converter.ConvertToInvariantString(propertyInfo.GetValue(this, null)));
}
}
}
}
public void ReadFromIniFile(string filename)
{
// Load all sections from file
var loaded = GetType().GetProperties()
.Select(x => ((CategoryAttribute)x.GetCustomAttributes(typeof(CategoryAttribute), false).FirstOrDefault())?.Category ?? "General")
.Distinct()
.ToDictionary(section => section, section => GetKeys(filename, section));
//var loaded = GetKeys(filename, "General");
foreach (var propertyInfo in GetType().GetProperties())
{
var category = ((CategoryAttribute)propertyInfo.GetCustomAttributes(typeof(CategoryAttribute), false).FirstOrDefault())?.Category ?? "General";
var name = propertyInfo.Name;
if (loaded.ContainsKey(category) && loaded[category].ContainsKey(name) && !string.IsNullOrEmpty(loaded[category][name]))
{
var rawString = loaded[category][name];
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
if (converter.IsValid(rawString))
{
propertyInfo.SetValue(this, converter.ConvertFromString(rawString), null);
}
}
}
}
// helper function
private Dictionary<string, string> GetKeys(string iniFile, string category)
{
var buffer = new byte[8 * 1024];
GetPrivateProfileSection(category, buffer, buffer.Length, iniFile);
var tmp = Encoding.UTF8.GetString(buffer).Trim('\0').Split('\0');
return tmp.Select(x => x.Split(new[] { '=' }, 2))
.Where(x => x.Length == 2)
.ToDictionary(x => x[0], x => x[1]);
}
}
and the TypeConverter class for the ReplaceItemsList property
public class StringListConverter : TypeConverter
{
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (value is List<string>)
{
return string.Join(",", ((List<string>)value).Select(x => x));
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
The reason your type converter is not used is because of this line:
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
You are getting the TypeConverter that is defined on the type of the property. So for ReplaceItemsList that would be the TypeConverter for List<T>. You need to get the TypeConverter for the property since that is where you added the TypeConverter attribute. So either you do something like you did for the category attribute in the read method where you use the PropertyInfo's GetCustomAttributes or you do what the PropertyGrid does which is use the PropertyDescriptors to get to the properties and their state. The latter would be better since if the object implemented ICustomTypeDescriptor or some other type augmentation like TypeDescriptionProvider then you would get that automatically.
So something like the following for the Save using PropertyDescriptors would be:
public void SaveToIniFile(string filename)
{
// write to ini file
using (var fp = new StreamWriter(filename, false, Encoding.UTF8))
{
// for each different section
foreach (var section in TypeDescriptor.GetProperties(this)
.Cast<PropertyDescriptor>()
.GroupBy(x => x.Attributes.Cast<Attribute>().OfType<CategoryAttribute>()
.FirstOrDefault()?.Category ?? "General"))
{
fp.WriteLine(Environment.NewLine + "[{0}]", section.Key);
foreach (var propertyInfo in section.OrderBy(x => x.Name))
{
var converter = propertyInfo.Converter;
fp.WriteLine("{0}={1}", propertyInfo.Name, converter.ConvertToInvariantString(propertyInfo.GetValue(this)));
}
}
}
}
If you are going to use a custom TypeConverter, you'll have to register it as a provider to TypeDescriptionProvider:
TypeDescriptor.AddProvider(new CustumTypeDescriptorProvider(), typeof(List<string>));
And in your implementation you could just do this in the constructor of PropsClass (instead of using the attribute). I created some custom code below that would do the split.
public class PropsClass
{
[DllImport("kernel32.dll")]
public static extern int GetPrivateProfileSection(string lpAppName, byte[] lpszReturnBuffer, int nSize, string lpFileName);
[Description("Maximum year value."), Category("DataConvert"), DefaultValue(2050)]
public int YearMaximum { get; set; }
[Description("Null keyword, for example NaN or NULL, case sensitive."), Category("DataConvert"), DefaultValue("NULL")]
public string KeyWordNull { get; set; }
private List<string> _replaceItems = new List<string>();
[Description("List of items to replace."), Category("DataConvert"), DefaultValue("enter keywords here")]
[Editor("System.Windows.Forms.Design.StringCollectionEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public List<string> ReplaceItemsList
{
get
{
return _replaceItems;
}
set
{
_replaceItems = value;
}
}
public PropsClass()
{
TypeDescriptor.AddProvider(new CustumTypeDescriptorProvider(), typeof(List<string>));
}
public void SaveToIniFile(string filename)
{
// write to ini file
using (var fp = new StreamWriter(filename, false, Encoding.UTF8))
{
// for each different section
foreach (var section in GetType()
.GetProperties()
.GroupBy(x => ((CategoryAttribute)x.GetCustomAttributes(typeof(CategoryAttribute), false)
.FirstOrDefault())?.Category ?? "General"))
{
fp.WriteLine(Environment.NewLine + "[{0}]", section.Key);
foreach (var propertyInfo in section.OrderBy(x => x.Name))
{
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
fp.WriteLine("{0}={1}", propertyInfo.Name, converter.ConvertToInvariantString(propertyInfo.GetValue(this, null)));
}
}
}
}
public void ReadFromIniFile(string filename)
{
// Load all sections from file
var loaded = GetType().GetProperties()
.Select(x => ((CategoryAttribute)x.GetCustomAttributes(typeof(CategoryAttribute), false).FirstOrDefault())?.Category ?? "General")
.Distinct()
.ToDictionary(section => section, section => GetKeys(filename, section));
//var loaded = GetKeys(filename, "General");
foreach (var propertyInfo in GetType().GetProperties())
{
var category = ((CategoryAttribute)propertyInfo.GetCustomAttributes(typeof(CategoryAttribute), false).FirstOrDefault())?.Category ?? "General";
var name = propertyInfo.Name;
if (loaded.ContainsKey(category) && loaded[category].ContainsKey(name) && !string.IsNullOrEmpty(loaded[category][name]))
{
var rawString = loaded[category][name];
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
if (converter.IsValid(rawString))
{
propertyInfo.SetValue(this, converter.ConvertFromString(rawString), null);
}
}
}
}
// helper function
private Dictionary<string, string> GetKeys(string iniFile, string category)
{
var buffer = new byte[8 * 1024];
GetPrivateProfileSection(category, buffer, buffer.Length, iniFile);
var tmp = Encoding.UTF8.GetString(buffer).Trim('\0').Split('\0');
return tmp.Select(x => x.Split(new[] { '=' }, 2))
.Where(x => x.Length == 2)
.ToDictionary(x => x[0], x => x[1]);
}
}
public class CustumTypeDescriptorProvider : TypeDescriptionProvider
{
public override ICustomTypeDescriptor GetTypeDescriptor(System.Type objectType, object instance)
{
if (objectType.Name == "List`1") return new StringListDescriptor();
return base.GetTypeDescriptor(objectType, instance);
}
}
public class StringListDescriptor : CustomTypeDescriptor
{
public override TypeConverter GetConverter()
{
return new StringListConverter();
}
}
public class StringListConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string s = value as string;
if (!string.IsNullOrEmpty(s))
{
return ((string)value).Split(',').ToList();
}
return base.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (destinationType == typeof(string))
{
return string.Join(",", (List<string>)value);
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
*NOTE: In my testing, the method ConvertFrom is called twice, once from converter.IsValid and once from propertyInfo.SetValue.
*NOTE2: You are using streamwriter to update the ini file. Since you are using GetPrivateProfileSection, it seems you should be using WritePrivateProfileSection to update the ini file.
*NOTE3: please consider the original comments in question about whether you should be using this method to read/write to an ini file. These methods have been around a long time. (https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getprivateprofilesection)
*NOTE4: A lot of this answer came from: Convert string to Array of string using TypeConverter
Leaving apart the PropertyGrid part which is just a way to put the info in a cartesian way on a Windows Form, the tricky part as I understand it, is to force a ini file (which is typically a 1:1 correlation between strings and strings) to house a collection of right hand side (rhs) values. Obviously, they cannot reside of a left hand side (lhs) of the ini line. You could, but then likely the choice of a ini file to persist your data is wrong at the roots.
So, why not avoiding to reinvent the wheel and go straight to custom parsing? This way you can also choose the separator to use (you may like to mimic windows, or apache setting files, etc).
Look at this implementation of one old code of mine. Grossly, it is an ini file telling my app which web exchange to contact for crypto trading. Before you get into a big headache with names: Monday is the name of the library holding the common code, and Friday is an app using Monday for trading online with a specific algo. Nothing important for the case being.
The ini file looks like this:
[Friday]
; if true the permutations will be only defined starting from the currency provided with index 0 in [Friday-Currencies]
MaxChainTransactions = 7
; if UseLastTradeValues is true, it will override UseConservativeBidAskValues
UseLastTradeValues = true
; if true the matrix values are (Ask+Bid)/2. This flag overrides UseConservativeBidAskValues but gets overridden by UseLastTradeValues
UseMidPriceValues = false
; if true the most conservative value between bid and ask will be used to calculate gains. Used only if UseLastTradeValues is false
UseConservativeBidAskValues = true
; if true, the solver will also solve with a matrix that will contain the best trade values from the available exchanges in parallel to other exchanges individually
MergeExchanges = false
; If true, Friday will issue one solution (if any) for each exchange, comprising one for the merged market if calculated.
DeliverOneSolutionPerExchange = true
; use values from the provided exchanges only
FridayExchanges = Bittrex| Binance | Kraken
; the minimum allowed gain remaining at the closure of the transaction chain to approve the full chain for ordering
MimimumAllowedGain = 1.05
; this is the target gain of a transaction. it is not used in case AllowPriceCorrectionBelowTargetGain is false
TargetGain = 1.09
; if true the prices in the transaction will be modified (increased towards the aggressive trade) of a factor equal to MaxPriceCorrectionFactor
AllowPriceCorrectionBelowTargetGain = true
; the frequency of each solution cycle (provided that the previous cycle has already completed and new data is arrived)
SolveEveryNSeconds = 15
Look at the line:
FridayExchanges = Bittrex| Binance | Kraken
it receives more than one exchange as a string separated by a pipe (|). You may choose other separators.
I suppose you parse the ini file with the support of some third party code. I used ini-parser (https://github.com/rickyah/ini-parser) that helped me a lot.
Parsing is done this way:
private static (int, string[]) ReadFridayExchanges()
{
string? configEntry = MondayConfiguration.MondaySettings["Friday"]["FridayExchanges"];
List<string> fridayExchanges = configEntry.ParseExchangeKey();
return (fridayExchanges.Count, fridayExchanges.ToArray());
}
Leave apart all the variables and names, the interesting one is MondaySettings. It is defined like this:
using IniParser;
using IniParser.Model;
/// <summary>
/// Holds the dictionary of Settings in IniData format.
/// </summary>
public static IniData MondaySettings { get; private set; }
and initialized like this:
[MemberNotNull(nameof(MondaySettings))]
private static void ReadSettingsFile(string settingsFile)
{
var iniParser = new FileIniDataParser();
MondaySettings = iniParser.ReadFile(settingsFile);
if (!DateTime.TryParse(MondaySettings["General"]["LastShutDown"], out _))
{
CorrectLastShutDownTime();
}
}
you find all nice calls in the ini-parser package to read and write the ini files automatically with almost one-liners.
When saving the file before closing and exiting:
private static bool SaveInternalSettings(string settingsFilename)
{
string settingsFile = Path.Combine(SettingsLocationFullPath, settingsFilename);
MondaySettings["General"]["LastShutDown"] = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture);
File.Delete(settingsFile);
try
{
var parser = new FileIniDataParser();
parser.WriteFile(settingsFile, MondaySettings);
return true;
}
catch
{
return false;
}
}
Now the interesting part: parsing multiple rhs values within ini. My solution was to do it manually, which is also one of the fastest ways.
/// <summary>
/// Reading Settings.ini for multi value lines, this routine parses the right hand side
/// of each records to define the list of values coupling it with the required
/// exchanges. Returns an array of exchanges that may be contained in a complex key to
/// resolve the All case and the presence of the pipe symbol (|).
/// NOTE: it makes use of the indexes for markets, therefore those structures should be
/// ready before this call to the method.
/// </summary>
/// <param name="rhrMarket">
/// The original key as written in the .ini file.
/// </param>
/// <returns>
/// A list of strings representing all the exchanges in a single record of
/// InterestingMarkets section the settings. If only one is contained in the original
/// key, a single element is contained.
/// </returns>
public static List<string> ParseExchangeKey(this string rhrMarket)
{
List<string> answer = new();
if (string.Equals(rhrMarket, "all", StringComparison.OrdinalIgnoreCase))
{
// producing all known exchanges as answer
foreach (KeyValuePair<string, int> ex in Tags.Exchanges.ActiveExchanges)
{
if (ex.Value != 0) // to exclude Monday which is id=0
{
answer.Add(ex.Key);
}
}
}
else if (rhrMarket.Contains('|'))
{
// sorting multiple cases
string[] split = rhrMarket.Split('|');
foreach (string? subElement in split)
{
answer.Add(subElement.ToLower().Trim());
}
}
else
{
answer.Add(rhrMarket.ToLower());
}
return answer;
}
There is also some XML to help you get to the point. As you can see, ParseExchangeKey returns a list of strings.
Just to make sure you have all the elements to get the Ienumerables, this is the definition of Tags.Exchanges.ActiveExchanges:
/// <summary>
/// Contains the set of active exchanges included into [ActiveExchanges] section in
/// settings.ini and having proper credentials to activate the web account. The
/// Values of the dictionary are the ID in _allExchanges. Monday is not included.
/// </summary>
[PublicAPI]
public static Dictionary<string, int> ActiveExchanges => _b_activeExchanges_;
/// <summary>
/// IMPL: This is internal because it needs to be set by <see cref="MondayConfiguration"/>.
/// it does not contain Monday's entry.
/// </summary>
private static readonly Dictionary<string, int> _b_activeExchanges_ = LoadExchangesDictionary();
private static Dictionary<string, int> LoadExchangesDictionary()
{
Dictionary<string, int> answer = new();
// set it internally without using the property which blocks the caller up the
// closure of the dictionary.
var counter = 0;
foreach (KeyValuePair<string, int> item in _allExchanges_)
{
if (item.Value != 0 && MondayConfiguration.Credentials.Any(c => c.Exchange == item.Key))
{
answer.Add(item.Key.ToLower(), item.Value);
counter++;
}
}
Saving is the same, however ini-parser leaves the original string with separators (pipes in this case) in its memory, if your used modified the set of multi-values, you just need to provide a simple ToString() version to concatenate it back before saving the file using the accustomed separator.
Then coupling with the controls of the Windows Forms is easy when you can get the List<string> object to move around.
This way you may skip all the custom classes for converting the type around which will likely slow down maintenance when you need to add or remove records from the ini file.
Using the below code, I'm trying to save app settings data in JSON format so it's easily readable and (in theory) easy to load back directly into data structures. Unfortunately it's not working out that way.
My general strategy is to ahve a series of lists representing different types of settings which I'll drop into one ListDictionary and then save as a single JSON object. Then, in theory, I load it back to a ListDictionary and recast the values into the lists they started as.
// Paths to pin to quick menu in Windows Explorer
public List<string> quickPaths = new List<string>();
public string diag = "";
public string settingsFile = System.AppDomain.CurrentDomain.BaseDirectory + "customizer_settings.json";
public Prefs()
{
ListDictionary prefs = LoadPrefs();
quickPaths = (List<string>)prefs["quickPaths"];
}
public ListDictionary LoadPrefs()
{
if (!File.Exists(settingsFile)) return new ListDictionary();
string json = File.ReadAllText(settingsFile);
return JsonSerializer.Deserialize<ListDictionary>(json);
}
public void SavePrefs()
{
ListDictionary toSave = new ListDictionary();
toSave["quickPaths"] = quickPaths;
File.WriteAllText(settingsFile, JsonSerializer.Serialize(toSave));
}
Instead, I'm getting the error in the title on the quickPaths assignment in the Prefs() constructor. I've looked it up and there's nothing else out there about this error specifically and no workarounds I've been able to find.
I've tried iterating over the prefs["quickPaths"] value and manually adding them one at a time to the List, but that's both inelegant and doesn't work anyway. Clearly I'm doing something wrong, but I don't know what. I thought I would get from deserialize exactly what I serialized, but it seems it doesn't work that way.
Here's what the output of the save function looks like:
{"quickPaths":["C:\\output","C:\\Users","C:\\Windows"]}
Try with the Newtonsoft like this
public class Prefs
{
public List<string> quickPaths = new List<string>();
public string diag = "";
public string settingsFile = System.AppDomain.CurrentDomain.BaseDirectory + "customizer_settings.json";
public Prefs()
{
ListDictionary prefs = LoadPrefs();
quickPaths = ((JArray)prefs["quickPaths"]).ToObject<List<string>>();
}
public ListDictionary LoadPrefs()
{
if (!File.Exists(settingsFile)) return new ListDictionary();
string json = File.ReadAllText(settingsFile);
return JsonConvert.DeserializeObject<ListDictionary>(json);
}
public void SavePrefs()
{
ListDictionary toSave = new ListDictionary();
toSave["quickPaths"] = quickPaths;
File.WriteAllText(settingsFile, JsonConvert.SerializeObject(toSave));
}
}
or you can deserialize one more time, like
quickPaths = JsonSerializer.Deserialize<List<string>>(((JsonElement)prefs["quickPaths"]).GetRawText());
I tried the Newtonsoft method including the deserializing of the sub-element and it didn't work (at least not how I implemented it).
Instead I re-evaluated my data structures and got rid of the ListDictionary in favor of Dictionary<string,List> since that's what I was doing anyway.
From there, I just needed to convert the comma-separated string to a list which can be done with built-in functions like so:
// Paths to pin to quick menu in Windows Explorer
public List<string> quickPaths = new List<string>();
public string diag = "";
public string settingsFile = System.AppDomain.CurrentDomain.BaseDirectory + "customizer_settings.json";
public Prefs()
{
Dictionary<string,List<string>> prefs;
prefs = LoadPrefs();
quickPaths = prefs["quickPaths"].ToList<string>();
}
public Dictionary<string,List<string>> LoadPrefs()
{
if (!File.Exists(settingsFile)) return new Dictionary<string,List<string>>();
string json = File.ReadAllText(settingsFile);
return JsonSerializer.Deserialize<Dictionary<string,List<string>>>(json);
}
public void SavePrefs()
{
Dictionary<string,List<string>> toSave = new Dictionary<string,List<string>>();
toSave["quickPaths"] = quickPaths;
File.WriteAllText(settingsFile, JsonSerializer.Serialize(toSave));
}
I have an input csv file that is read by C#. I want to write a piece of code that scans the file for all 'nulls' and replace with an empty string on the output.
so far I have wrote this. The public FileProcessor() doesnt seem to work
class FileProcessor
{
//File Objects
readonly Dictionary<string, FileInfo> _fileDict;
//DB Objects
private readonly EMIRDB _emirdb;
public FileProcessor()
{
_fileDict = new Dictionary<string, FileInfo>();
_emirdb = new EMIRDB();
}
public FileProcessor()
{
string replacenull = File.ReadAllText ("EMIR_VU_E_");
replacenull = replacenull.Replace("null", "");
File.WriteAllText("EMIR_VU_E_", replacenull);
}
You cannot have two constructors with the same set of parameters (in this case, zero parameters).
Thus:
public FileProcessor()
{
_fileDict = new Dictionary<string, FileInfo>();
_emirdb = new EMIRDB();
}
public FileProcessor()
{
string replacenull = File.ReadAllText ("EMIR_VU_E_");
replacenull = replacenull.Replace("null", "");
File.WriteAllText("EMIR_VU_E_", replacenull);
}
is not allowed.
You need to remove one of the constructors. Or move the code from one constructor into the other.
I'd like to parse a text file with a few dozen entries. Right now, I have a dumbed-down solution that reads line by line and compares against hard-coded strings:
while ((line = reader.ReadLine()) != null) //returns null if end of stream
{
cmpStr = "MODE";
try
{
if (line.Equals(cmpStr))
GlobalData.mode = Convert.ToInt32(line.Remove(0, cmpStr.Length));
}
catch { }
cmpStr = "TIME_YEAR";
try
{
if (line.Equals(cmpStr))
GlobalData.time_year = Convert.ToInt32(line.Remove(0, cmpStr.Length));
}
catch { }
// ... repeat to parse the remaining lines
}
GlobalData is a static class and looks like this:
public static class GlobalData
{
public static int mode;
public static int time_year;
public static int time_month;
public static int time_day;
public static int time_hour;
public static int time_minute;
// other entries omitted
public static string[] GlobalKeywords = new string[37]
{
"MODE",
"TIME_YEAR",
"TIME_MONTH",
"TIME_DAY",
"TIME_HOUR",
"TIME_MINUTE",
// other entries omitted
};
}
If it were possible to access my static fields by index, I'd do:
int i = 0;
while ((line = reader.ReadLine()) != null)
{
cmpStr = GlobalData.GlobalKeywords[i]; // when i == 0: cmpStr = "MODE"
if (line.Equals(cmpStr))
GlobalData[i] = Convert.ToInt32(line.Remove(0, cmpStr.Length));
// GlobalData[0] would be GlobalData.mode, and so on (but doesn't work)
i++;
}
catch { }
So, even though I can setup a loop to compare against a string array of keywords,
how do I assign a certain field of my static class ?
br
Chris
I'm not sure what your business constraints are, so it's hard to propose a fool-proof solution, though a few points:
cmpStr = "MODE";
try
{
if (line.Equals(cmpStr))
GlobalData.mode = Convert.ToInt32(line.Remove(0, cmpStr.Length));
}
This won't work as you (probably expect) - if line.Equals("MODE") then line.Remove(0, "MODE".Length) is an empty string. What you probably want is line.StartsWith(cmpStr) or line.Contains(cmpStr).
GlobalData is a static class
This doesn't seem a good approach for what you're doing. You may want to read up on static classes and when to use them (MSDN is a good starting point, though it obviously can't cover everything: http://msdn.microsoft.com/en-us/library/79b3xss3%28v=vs.80%29.aspx).
Other than that, you can probably simply replace all your int fields with a dictionary (though please rethink the static approach as described above):
public static Dictionary<String, int> Items = new Dictionary<String, int>();
Then your parsing code could look like this:
while ((line = reader.ReadLine()) != null) //returns null if end of stream
{
var matchingString
= GlobalData.GlobalKeywords.FirstOrDefault(s => line.StartsWith(s));
if (matchingString != null)
GlobalData[matchingString]
= Convert.ToInt32(line.Remove(0, matchingString.Length));
}
You will then be able to fetch that data using e.g. GlobalData.Items["MODE"].
One last bit: you may consider introducing constant values in your global data class, e.g.:
public const String MODE = "MODE";
Then you can use GlobalData.Items[GlobalData.MODE] and avoid typos: writing GlobalData.Items[GlobalData.MODe] would cause a compile error.
Replace this:
public static int mode;
public static int time_year;
public static int time_month;
public static int time_day;
public static int time_hour;
public static int time_minute;
With this:
public static Dictionary<string, int> my_values = new Dictionary<string, int>();
Then replace:
GlobalData[i] = Convert.ToInt32(line.Remove(0, cmpStr.Length));
with:
GlobalData.my_values[cmpStr] = Convert.ToInt32(line.Remove(0, cmpStr.Length));
That should do what you want even though I don't understand how you expect the Convert.ToInt32 to work. The way you are calling Remove will create an empty string (which might convert to 0, I can't remember) and even if it didn't, the line doesn't contain a number because you compared it successfully to a string like "MODE".
An elegant way to solve your problem is to prepare a different action for each of the acceptable strings. You use a Dictionary(Of String, <Action>) where Action is a common delegate type that receive a string in input and know how to process it accordingly to the keyword present at the beginning of the line.
// The common signature for every methods stored in the value part of the dictionary
public delegate void ParseLine(string line);
// Global dictionary where you store the strings as keyword
// and the ParseLine as the delegate to execute
Dictionary<String, ParseLine> m_Actions = new Dictionary<String, ParseLine>() ;
void Main()
{
// Initialize the dictionary with the delegate corresponding to the strings keys
m_Actions.Add("MODE", new ParseLine(Task1));
m_Actions.Add("TIME_YEAR", new ParseLine(Task2));
m_Actions.Add("TIME_MONTH", new ParseLine(Task3));
m_Actions.Add("TIME_DAY", new ParseLine(Task4));
.....
while ((line = reader.ReadLine()) != null)
{
// Search the space that divide the keyword from the value on the same line
string command = line.Substring(0, line.IndexOf(' ')).Trim();
// a bit of error checking here is required
if(m_Actions.ContainsKey(command))
m_Actions[command](line);
}
}
void Task1(string line)
{
// this will handle the MODE line
GlobalData.Mode = Convert.ToInt32(line.Substring(line.IndexOf(' ')+1).Trim());
}
void Task2(string line)
{
GlobalData.time_year = Convert.ToInt32(line.Substring(line.IndexOf(' ')+1).Trim());
}
void Task3(string line)
{
.....
}
void Task4(string line)
{
.....
}
A simple (and not really clean) approach is to add an indexer to your global data class and decide which field to set based on the index. But you have to extend the indexer every time you add a field (basically you move the if/switch from the while Loop into the indexer).
You could also use reflection, if you can match the keyword to the field name. This is not very performant but does not need to be extended as long as you can map the keyword to the new field name.
Another approach is to create a dictionary>. In this dictionary you register the keywords, e.g. (pseudo-code):
Class Level variable:
private keywordsDict = new Dictionary<string, Action<int>>();
In a constructor:
keywordsDict.Add("MODE", delegate(value) GlobalData.mode = value);
In while-loop:
var action = keywordsDict[line];
action(value);
In the later approach, you only need to extend the dictionary but not the algorithm as such if you have a new keyword/field.
May be i can tell you how to achieve it (GlobalData[i]) in C# thought its not the answer you are looking for.
class GlobalData
{
private string[] array = new string[10];
public GlobalData()
{
//now initialize array
array[0] = "SomeThingA";
array[1] = "SomeThingB";//continue initialization.
}
public string this[int index]
{
get {return array[index];}
}
}
Now the clients can use GlobalData like ,
GlobalData gd = new GlobalData();
gd[1] = "SomeOtherThing" ; //set the value.
string value = gd[1];//get the value
But this cant be done by making the class static as you see it works with 'this'
I am trying to figure the best way to map input files, such as XLS or CSV files to objects in the system. Let me expand on that a little. I have the following objects:
Company
Contact
Each have variables variables such as:
Addresses
Phone numbers
Emails
etc
The input file I can get varies, sometimes there will be column headings, sometimes not, the column arrangement and numbers will change at times. It could look as follows:
COMPANY - CONTACT - ADDRESS - PHONE
----------------------------------------------------------------------
company1 contact1 address1 phone1
company1 contact2 address2
company2 contact3 phone2
contact4 address3
The above shows that "company" will have the "addres1" and "phone1" attached to the company, the "contact1" is its own object but has "company1" as its parent object.
The same is true for "contact2", (except for the phone).
"contact4" has no parent object so "address3" belongs to the contact, rather than a Company.
My thinking so far would be to have the following objects:
Mappings - (this is where I am not sure how to implement it. It should say how columns should map to variables, the ownership / hierarchy - e.g. Company has Address)
IMappingLoader - (loads the mapping objects)
-- XmlMappingLoader
-- DbMappingLoader
IDataLoader - (loads the data into a dataset)
-- XLSLoader
-- CSVLoader
So the mappings would be loaded, the data loaded into a dataset and the correct objects returned.
The part I am not really sure the best way to approach is how to do the mappings part. How to be able to say which column should belong to which object.
Thanks for any and all advice.
Jon
Your parsers are going to have to know about your columns....otherwise it is unable to map the data to the specific object properties. Unless of course you introduce an indexed-properties class which you could store the information based on the order it is read.
You should create a parser factory and based on the extension of the file you would return the correct parser for the job e.g.
public class Record
{
private Dictionary<int, string> items = new Dictionary<int, string>();
private int propCount;
public Record(int size)
{
// populate array with empty strings
for(int i = 0; i <= size -1; i++)
items.Add(i, String.Empty);
propCount = size;
}
public string this[int index]
{
get { return items[index]; }
set { items[index] = value; }
}
public int PropertyCount { get { return propCount; } }
}
public interface IRecordParser
{
string FileName { get; set; }
string[] GetHeadings();
bool HasHeaders { get; set; }
void GoToStart();
Record ParseNextRecord();
}
public abstract class RecordParser
{
public string FileName { get; set; }
public bool HasHeaders { get; set; }
public abstract string[] GetHeadings();
public abstract void GoToStart();
public abstract Record ParseNextRecord();
}
public class ExcelRecordParser : RecordParser, IRecordParser
{
public ExcelRecordParser()
{
}
public override string[] GetHeadings()
{
if (HasHeaders)
// return column headings
else
// return default headings from settings file
}
public override void GoToStart()
{
// navigate to first row (or +1 if HasHeaders is true)
}
public override Record ParseNextRecord()
{
var headers = GetHeadings();
var r = new Record(headers.Length);
// enumerate rows, then for each row do...
for(int i = 0; i <= headers.Length - 1; i++)
r[i] = row[i];
return r;
}
}
public class CsvRecordParser : RecordParser, IRecordParser
{
public CsvRecordParser()
{
}
public override string[] GetHeadings()
{
if (HasHeaders)
// return first row split as headings
else
// return default headers from settings file
}
public override void GoToStart()
{
// navigate to start of file (or +1 if HasHeaders is true)
}
public override Record ParseNextRecord()
{
var headers = GetHeadings();
var r = new Record(headers.Length);
// enumerate lines, then for each line do...
for(int i = 0; i <= headers.Length - 1; i++)
r[i] = line[i];
return r;
}
}
public static class RecordParserFactory
{
public static IRecordParser Create(string ext)
{
switch (ext)
{
case ".xls":
return new ExcelRecordParser() as IRecordParser;
case ".csv":
return new CsvRecordParser() as IRecordParser;
default:
return null;
}
}
}
Usage
// would return an instance of CSV Parser
string file = #"C:\Data\MyRecords.csv";
IRecordParser parser = RecordParserFactory.Create(System.IO.Path.GetExtension(file));
// would return an instance of Excel Parser
string file = #"C:\Data\MyRecords.xls";
IRecordParser parser = RecordParserFactory.Create(System.IO.Path.GetExtension(file));
This would allow you to add other parsers if your file format changes in the future e.g. XML, Binary etc