J'étais sur le point d'implémenter une substitution de ToString () sur une classe métier particulière afin de produire un format compatible avec Excel pour l'écriture dans un fichier de sortie, qui sera repris plus tard et traité. Voici à quoi ressemblent les données:
5555555 "LASTN SR, FIRSTN" 5555555555 13956 STREET RD TOWNSVILLE MI 48890 25.88 01-003-06-0934
Ce n'est pas grave pour moi de créer une chaîne de format et de remplacer ToString()
, mais cela modifiera le comportement de ToString()
pour tous les objets que je décide de sérialiser de cette façon, ce qui rend l'implémentation de ToString()
complètement désordonnée.
Maintenant, je lisais sur IFormatProvider , et une classe qui implémente cela semble être une bonne idée, mais je suis encore un peu confus quant à l’emplacement de toute cette logique et à la manière de construire la classe de formatage.
Que faites-vous lorsque vous devez créer un fichier CSV, délimité par des tabulations ou une autre chaîne arbitraire non XML à partir d'un objet?
Voici un mode générique pour créer un fichier CSV à partir d'une liste d'objets, en utilisant la réflexion:
public static string ToCsv<T>(string separator, IEnumerable<T> objectlist)
{
Type t = typeof(T);
FieldInfo[] fields = t.GetFields();
string header = String.Join(separator, fields.Select(f => f.Name).ToArray());
StringBuilder csvdata = new StringBuilder();
csvdata.AppendLine(header);
foreach (var o in objectlist)
csvdata.AppendLine(ToCsvFields(separator, fields, o));
return csvdata.ToString();
}
public static string ToCsvFields(string separator, FieldInfo[] fields, object o)
{
StringBuilder linie = new StringBuilder();
foreach (var f in fields)
{
if (linie.Length > 0)
linie.Append(separator);
var x = f.GetValue(o);
if (x != null)
linie.Append(x.ToString());
}
return linie.ToString();
}
De nombreuses variantes peuvent être apportées, telles que l'écriture directe dans un fichier dans ToCsv () ou le remplacement de StringBuilder par un IEnumerable et des instructions de rendement.
Voici une version simplifiée de l'idée de CSV de Per Hejndorf (sans la surcharge de mémoire car elle génère chaque ligne à tour de rôle). En raison de la demande générale, il prend également en charge les champs et les propriétés simples en utilisant Concat
.
Cet exemple n'a jamais été conçu pour être une solution complète, mais simplement pour faire avancer l'idée originale publiée par Per Hejndorf. Pour générer un fichier CSV valide, vous devez remplacer les caractères de délimitation de texte contenus dans le texte par une séquence de 2 caractères de délimitation. par exemple. une simple .Replace("\"", "\"\"")
.
Après avoir utilisé à nouveau mon propre code dans un projet aujourd'hui, j'ai réalisé que je n'aurais rien dû prendre pour acquis lorsque j'ai repris l'exemple de @Per Hejndorf
. Il est plus logique de supposer un délimiteur par défaut de "," (virgule) et de définir le délimiteur comme second paramètre, facultatif. Ma propre version de bibliothèque fournit également un 3ème paramètre header
qui contrôle si une ligne d'en-tête doit être renvoyée car vous ne souhaitez parfois que les données.
public static IEnumerable<string> ToCsv<T>(IEnumerable<T> objectlist, string separator = ",", bool header = true)
{
FieldInfo[] fields = typeof(T).GetFields();
PropertyInfo[] properties = typeof(T).GetProperties();
if (header)
{
yield return String.Join(separator, fields.Select(f => f.Name).Concat(properties.Select(p=>p.Name)).ToArray());
}
foreach (var o in objectlist)
{
yield return string.Join(separator, fields.Select(f=>(f.GetValue(o) ?? "").ToString())
.Concat(properties.Select(p=>(p.GetValue(o,null) ?? "").ToString())).ToArray());
}
}
vous l'utilisez alors comme ceci pour les virgules:
foreach (var line in ToCsv(objects))
{
Console.WriteLine(line);
}
ou comme ceci pour un autre délimiteur (par exemple, TAB):
foreach (var line in ToCsv(objects, "\t"))
{
Console.WriteLine(line);
}
écrit la liste dans un fichier CSV délimité par des virgules
using (TextWriter tw = File.CreateText("C:\testoutput.csv"))
{
foreach (var line in ToCsv(objects))
{
tw.WriteLine(line);
}
}
ou écrivez-le délimité par des tabulations
using (TextWriter tw = File.CreateText("C:\testoutput.txt"))
{
foreach (var line in ToCsv(objects, "\t"))
{
tw.WriteLine(line);
}
}
_ {Si vous avez des champs/propriétés complexes, vous devrez les filtrer hors des clauses de sélection.} _
Voici une version simplifiée de l'idée CSV de Per Hejndorf (sans la surcharge de mémoire car elle génère chaque ligne à tour de rôle) et ne comporte que 4 lignes de code :)
public static IEnumerable<string> ToCsv<T>(string separator, IEnumerable<T> objectlist)
{
FieldInfo[] fields = typeof(T).GetFields();
yield return String.Join(separator, fields.Select(f => f.Name).ToArray());
foreach (var o in objectlist)
{
yield return string.Join(separator, fields.Select(f=>(f.GetValue(o) ?? "").ToString()).ToArray());
}
}
Vous pouvez le itérer comme ceci:
foreach (var line in ToCsv(",", objects))
{
Console.WriteLine(line);
}
où objects
est une liste d'objets fortement typée.
public static IEnumerable<string> ToCsv<T>(string separator, IEnumerable<T> objectlist)
{
FieldInfo[] fields = typeof(T).GetFields();
PropertyInfo[] properties = typeof(T).GetProperties();
yield return String.Join(separator, fields.Select(f => f.Name).Concat(properties.Select(p=>p.Name)).ToArray());
foreach (var o in objectlist)
{
yield return string.Join(separator, fields.Select(f=>(f.GetValue(o) ?? "").ToString())
.Concat(properties.Select(p=>(p.GetValue(o,null) ?? "").ToString())).ToArray());
}
}
En règle générale, je ne préconise que le remplacement de toString en tant qu'outil de débogage. Si c'est pour la logique métier, ce doit être une méthode explicite sur la classe/interface.
Pour une sérialisation simple comme celle-ci, je vous conseillerais de créer une classe distincte connaissant votre bibliothèque de sortie CSV et vos objets métier effectuant la sérialisation, au lieu de l'insérer dans les objets métier eux-mêmes.
De cette façon, vous vous retrouvez avec une classe par format de sortie qui produit une vue de votre modèle.
Pour une sérialisation plus complexe dans laquelle vous essayez d'écrire un graphe d'objets pour la persistance, envisagez de le placer dans les classes métier, mais uniquement si cela permet un code plus propre.
Le problème avec les solutions que j'ai trouvées jusqu'à présent est qu'elles ne vous permettent pas d'exporter un sous-ensemble de propriétés, mais uniquement l'objet entier. La plupart du temps, lorsque nous devons exporter des données au format CSV, nous devons "adapter" son format de manière précise. J'ai donc créé cette méthode d'extension simple qui me permet de le faire en transmettant un tableau de paramètres de type Func<T, string>
à spécifiez le mappage.
public static string ToCsv<T>(this IEnumerable<T> list, params Func<T, string>[] properties)
{
var columns = properties.Select(func => list.Select(func).ToList()).ToList();
var stringBuilder = new StringBuilder();
var rowsCount = columns.First().Count;
for (var i = 0; i < rowsCount; i++)
{
var rowCells = columns.Select(column => column[i]);
stringBuilder.AppendLine(string.Join(",", rowCells));
}
return stringBuilder.ToString();
}
Utilisation:
philosophers.ToCsv(x => x.LastName, x => x.FirstName)
Génère:
Hayek,Friedrich
Rothbard,Murray
Brent,David
J'ai eu un problème avec la variation de HiTech Magic: deux propriétés avec la même valeur, une seule serait peuplée. Cela semble l'avoir corrigé:
public static IEnumerable<string> ToCsv<T>(string separator, IEnumerable<T> objectlist)
{
FieldInfo[] fields = typeof(T).GetFields();
PropertyInfo[] properties = typeof(T).GetProperties();
yield return String.Join(separator, fields.Select(f => f.Name).Union(properties.Select(p => p.Name)).ToArray());
foreach (var o in objectlist)
{
yield return string.Join(separator, (properties.Select(p => (p.GetValue(o, null) ?? "").ToString())).ToArray());
}
}
La réponse de Gone Coding a été très utile. J'ai apporté quelques modifications à celui-ci afin de gérer les gremlins de texte qui ralentiraient la sortie.
/******************************************************/
public static IEnumerable<string> ToCsv<T>(IEnumerable<T> objectlist, string separator = ",", bool header = true)
{
FieldInfo[] fields = typeof(T).GetFields();
PropertyInfo[] properties = typeof(T).GetProperties();
string str1;
string str2;
if(header)
{
str1 = String.Join(separator, fields.Select(f => f.Name).Concat(properties.Select(p => p.Name)).ToArray());
str1 = str1 + Environment.NewLine;
yield return str1;
}
foreach(var o in objectlist)
{
//regex is to remove any misplaced returns or tabs that would
//really mess up a csv conversion.
str2 = string.Join(separator, fields.Select(f => (Regex.Replace(Convert.ToString(f.GetValue(o)), @"\t|\n|\r", "") ?? "").Trim())
.Concat(properties.Select(p => (Regex.Replace(Convert.ToString(p.GetValue(o, null)), @"\t|\n|\r", "") ?? "").Trim())).ToArray());
str2 = str2 + Environment.NewLine;
yield return str2;
}
}