Is there any class in the .NET framework that can read/write standard .ini files:
[Section]
<keyname>=<value>
...
Delphi has the TIniFile component and I want to know if there is anything similar for C#?
Preface
Firstly, read this MSDN blog post on the limitations of INI files. If it suits your needs, read on.
This is a concise implementation I wrote, utilising the original Windows P/Invoke, so it is supported by all versions of Windows with .NET installed, (i.e. Windows 98 - Windows 11). I hereby release it into the public domain - you're free to use it commercially without attribution.
The tiny class
Add a new class called IniFile.cs to your project:
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
// Change this to match your program's normal namespace
namespace MyProg
{
class IniFile // revision 11
{
string Path;
string EXE = Assembly.GetExecutingAssembly().GetName().Name;
[DllImport("kernel32", CharSet = CharSet.Unicode)]
static extern long WritePrivateProfileString(string Section, string Key, string Value, string FilePath);
[DllImport("kernel32", CharSet = CharSet.Unicode)]
static extern int GetPrivateProfileString(string Section, string Key, string Default, StringBuilder RetVal, int Size, string FilePath);
public IniFile(string IniPath = null)
{
Path = new FileInfo(IniPath ?? EXE + ".ini").FullName;
}
public string Read(string Key, string Section = null)
{
var RetVal = new StringBuilder(255);
GetPrivateProfileString(Section ?? EXE, Key, "", RetVal, 255, Path);
return RetVal.ToString();
}
public void Write(string Key, string Value, string Section = null)
{
WritePrivateProfileString(Section ?? EXE, Key, Value, Path);
}
public void DeleteKey(string Key, string Section = null)
{
Write(Key, null, Section ?? EXE);
}
public void DeleteSection(string Section = null)
{
Write(null, null, Section ?? EXE);
}
public bool KeyExists(string Key, string Section = null)
{
return Read(Key, Section).Length > 0;
}
}
}
How to use it
Open the INI file in one of the 3 following ways:
// Creates or loads an INI file in the same directory as your executable
// named EXE.ini (where EXE is the name of your executable)
var MyIni = new IniFile();
// Or specify a specific name in the current dir
var MyIni = new IniFile("Settings.ini");
// Or specify a specific name in a specific dir
var MyIni = new IniFile(#"C:\Settings.ini");
You can write some values like so:
MyIni.Write("DefaultVolume", "100");
MyIni.Write("HomePage", "http://www.google.com");
To create a file like this:
[MyProg]
DefaultVolume=100
HomePage=http://www.google.com
To read the values out of the INI file:
var DefaultVolume = MyIni.Read("DefaultVolume");
var HomePage = MyIni.Read("HomePage");
Optionally, you can set [Section]'s:
MyIni.Write("DefaultVolume", "100", "Audio");
MyIni.Write("HomePage", "http://www.google.com", "Web");
To create a file like this:
[Audio]
DefaultVolume=100
[Web]
HomePage=http://www.google.com
You can also check for the existence of a key like so:
if(!MyIni.KeyExists("DefaultVolume", "Audio"))
{
MyIni.Write("DefaultVolume", "100", "Audio");
}
You can delete a key like so:
MyIni.DeleteKey("DefaultVolume", "Audio");
You can also delete a whole section (including all keys) like so:
MyIni.DeleteSection("Web");
Please feel free to comment with any improvements!
The creators of the .NET framework want you to use XML-based config files, rather than INI files. So no, there is no built-in mechanism for reading them.
There are third party solutions available, though.
INI handlers can be obtained as NuGet packages, such as INI Parser.
You can write your own INI handler, which is the old-school, laborious way. It gives you more control over the implementation, which you can use for bad or good. See e.g. an INI file handling class using C#, P/Invoke and Win32.
This article on CodeProject "An INI file handling class using C#" should help.
The author created a C# class "Ini" which exposes two functions from KERNEL32.dll. These functions are: WritePrivateProfileString and GetPrivateProfileString. You will need two namespaces: System.Runtime.InteropServices and System.Text.
Steps to use the Ini class
In your project namespace definition add
using INI;
Create a INIFile like this
INIFile ini = new INIFile("C:\\test.ini");
Use IniWriteValue to write a new value to a specific key in a section or use IniReadValue to read a value FROM a key in a specific Section.
Note: if you're beginning from scratch, you could read this MSDN article: How to: Add Application Configuration Files to C# Projects. It's a better way for configuring your application.
I found this simple implementation:
http://bytes.com/topic/net/insights/797169-reading-parsing-ini-file-c
Works well for what I need.
Here is how you use it:
public class TestParser
{
public static void Main()
{
IniParser parser = new IniParser(#"C:\test.ini");
String newMessage;
newMessage = parser.GetSetting("appsettings", "msgpart1");
newMessage += parser.GetSetting("appsettings", "msgpart2");
newMessage += parser.GetSetting("punctuation", "ex");
//Returns "Hello World!"
Console.WriteLine(newMessage);
Console.ReadLine();
}
}
Here is the code:
using System;
using System.IO;
using System.Collections;
public class IniParser
{
private Hashtable keyPairs = new Hashtable();
private String iniFilePath;
private struct SectionPair
{
public String Section;
public String Key;
}
/// <summary>
/// Opens the INI file at the given path and enumerates the values in the IniParser.
/// </summary>
/// <param name="iniPath">Full path to INI file.</param>
public IniParser(String iniPath)
{
TextReader iniFile = null;
String strLine = null;
String currentRoot = null;
String[] keyPair = null;
iniFilePath = iniPath;
if (File.Exists(iniPath))
{
try
{
iniFile = new StreamReader(iniPath);
strLine = iniFile.ReadLine();
while (strLine != null)
{
strLine = strLine.Trim().ToUpper();
if (strLine != "")
{
if (strLine.StartsWith("[") && strLine.EndsWith("]"))
{
currentRoot = strLine.Substring(1, strLine.Length - 2);
}
else
{
keyPair = strLine.Split(new char[] { '=' }, 2);
SectionPair sectionPair;
String value = null;
if (currentRoot == null)
currentRoot = "ROOT";
sectionPair.Section = currentRoot;
sectionPair.Key = keyPair[0];
if (keyPair.Length > 1)
value = keyPair[1];
keyPairs.Add(sectionPair, value);
}
}
strLine = iniFile.ReadLine();
}
}
catch (Exception ex)
{
throw ex;
}
finally
{
if (iniFile != null)
iniFile.Close();
}
}
else
throw new FileNotFoundException("Unable to locate " + iniPath);
}
/// <summary>
/// Returns the value for the given section, key pair.
/// </summary>
/// <param name="sectionName">Section name.</param>
/// <param name="settingName">Key name.</param>
public String GetSetting(String sectionName, String settingName)
{
SectionPair sectionPair;
sectionPair.Section = sectionName.ToUpper();
sectionPair.Key = settingName.ToUpper();
return (String)keyPairs[sectionPair];
}
/// <summary>
/// Enumerates all lines for given section.
/// </summary>
/// <param name="sectionName">Section to enum.</param>
public String[] EnumSection(String sectionName)
{
ArrayList tmpArray = new ArrayList();
foreach (SectionPair pair in keyPairs.Keys)
{
if (pair.Section == sectionName.ToUpper())
tmpArray.Add(pair.Key);
}
return (String[])tmpArray.ToArray(typeof(String));
}
/// <summary>
/// Adds or replaces a setting to the table to be saved.
/// </summary>
/// <param name="sectionName">Section to add under.</param>
/// <param name="settingName">Key name to add.</param>
/// <param name="settingValue">Value of key.</param>
public void AddSetting(String sectionName, String settingName, String settingValue)
{
SectionPair sectionPair;
sectionPair.Section = sectionName.ToUpper();
sectionPair.Key = settingName.ToUpper();
if (keyPairs.ContainsKey(sectionPair))
keyPairs.Remove(sectionPair);
keyPairs.Add(sectionPair, settingValue);
}
/// <summary>
/// Adds or replaces a setting to the table to be saved with a null value.
/// </summary>
/// <param name="sectionName">Section to add under.</param>
/// <param name="settingName">Key name to add.</param>
public void AddSetting(String sectionName, String settingName)
{
AddSetting(sectionName, settingName, null);
}
/// <summary>
/// Remove a setting.
/// </summary>
/// <param name="sectionName">Section to add under.</param>
/// <param name="settingName">Key name to add.</param>
public void DeleteSetting(String sectionName, String settingName)
{
SectionPair sectionPair;
sectionPair.Section = sectionName.ToUpper();
sectionPair.Key = settingName.ToUpper();
if (keyPairs.ContainsKey(sectionPair))
keyPairs.Remove(sectionPair);
}
/// <summary>
/// Save settings to new file.
/// </summary>
/// <param name="newFilePath">New file path.</param>
public void SaveSettings(String newFilePath)
{
ArrayList sections = new ArrayList();
String tmpValue = "";
String strToSave = "";
foreach (SectionPair sectionPair in keyPairs.Keys)
{
if (!sections.Contains(sectionPair.Section))
sections.Add(sectionPair.Section);
}
foreach (String section in sections)
{
strToSave += ("[" + section + "]\r\n");
foreach (SectionPair sectionPair in keyPairs.Keys)
{
if (sectionPair.Section == section)
{
tmpValue = (String)keyPairs[sectionPair];
if (tmpValue != null)
tmpValue = "=" + tmpValue;
strToSave += (sectionPair.Key + tmpValue + "\r\n");
}
}
strToSave += "\r\n";
}
try
{
TextWriter tw = new StreamWriter(newFilePath);
tw.Write(strToSave);
tw.Close();
}
catch (Exception ex)
{
throw ex;
}
}
/// <summary>
/// Save settings back to ini file.
/// </summary>
public void SaveSettings()
{
SaveSettings(iniFilePath);
}
}
The code in joerage's answer is inspiring.
Unfortunately, it changes the character casing of the keys and does not handle comments. So I wrote something that should be robust enough to read (only) very dirty INI files and allows to retrieve keys as they are.
It uses some LINQ, a nested case insensitive string dictionary to store sections, keys and values, and read the file in one go.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
class IniReader
{
Dictionary<string, Dictionary<string, string>> ini = new Dictionary<string, Dictionary<string, string>>(StringComparer.InvariantCultureIgnoreCase);
public IniReader(string file)
{
var txt = File.ReadAllText(file);
Dictionary<string, string> currentSection = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
ini[""] = currentSection;
foreach(var line in txt.Split(new[]{"\n"}, StringSplitOptions.RemoveEmptyEntries)
.Where(t => !string.IsNullOrWhiteSpace(t))
.Select(t => t.Trim()))
{
if (line.StartsWith(";"))
continue;
if (line.StartsWith("[") && line.EndsWith("]"))
{
currentSection = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
ini[line.Substring(1, line.LastIndexOf("]") - 1)] = currentSection;
continue;
}
var idx = line.IndexOf("=");
if (idx == -1)
currentSection[line] = "";
else
currentSection[line.Substring(0, idx)] = line.Substring(idx + 1);
}
}
public string GetValue(string key)
{
return GetValue(key, "", "");
}
public string GetValue(string key, string section)
{
return GetValue(key, section, "");
}
public string GetValue(string key, string section, string #default)
{
if (!ini.ContainsKey(section))
return #default;
if (!ini[section].ContainsKey(key))
return #default;
return ini[section][key];
}
public string[] GetKeys(string section)
{
if (!ini.ContainsKey(section))
return new string[0];
return ini[section].Keys.ToArray();
}
public string[] GetSections()
{
return ini.Keys.Where(t => t != "").ToArray();
}
}
I want to introduce an IniParser library I've created completely in c#, so it contains no dependencies in any OS, which makes it Mono compatible. Open Source with MIT license -so it can be used in any code.
You can check out the source in GitHub, and it is also available as a NuGet package
It's heavily configurable, and really simple to use.
Sorry for the shameless plug but I hope it can be of help of anyone revisiting this answer.
If you only need read access and not write access and you are using the Microsoft.Extensions.Confiuration (comes bundled in by default with ASP.NET Core but works with regular programs too) you can use the NuGet package Microsoft.Extensions.Configuration.Ini to import ini files in to your configuration settings.
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddIniFile("SomeConfig.ini", optional: false);
Configuration = builder.Build();
}
PeanutButter.INI is a Nuget-packaged class for INI files manipulation. It supports read/write, including comments – your comments are preserved on write. It appears to be reasonably popular, is tested and easy to use. It's also totally free and open-source.
Disclaimer: I am the author of PeanutButter.INI.
Usually, when you create applications using C# and the .NET framework, you will not use INI files. It is more common to store settings in an XML-based configuration file or in the registry.
However, if your software shares settings with a legacy application it may be easier to use its configuration file, rather than duplicating the information elsewhere.
The .NET framework does not support the use of INI files directly. However, you can use Windows API functions with Platform Invocation Services (P/Invoke) to write to and read from the files. In this link we create a class that represents INI files and uses Windows API functions to manipulate them.
Please go through the following link.
Reading and Writing INI Files
If you want just a simple reader without sections and any other dlls here is simple solution:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Tool
{
public class Config
{
Dictionary <string, string> values;
public Config (string path)
{
values = File.ReadLines(path)
.Where(line => (!String.IsNullOrWhiteSpace(line) && !line.StartsWith("#")))
.Select(line => line.Split(new char[] { '=' }, 2, 0))
.ToDictionary(parts => parts[0].Trim(), parts => parts.Length>1?parts[1].Trim():null);
}
public string Value (string name, string value=null)
{
if (values!=null && values.ContainsKey(name))
{
return values[name];
}
return value;
}
}
}
Usage sample:
file = new Tool.Config (Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) + "\\config.ini");
command = file.Value ("command");
action = file.Value ("action");
string value;
//second parameter is default value if no key found with this name
value = file.Value("debug","true");
this.debug = (value.ToLower()=="true" || value== "1");
value = file.Value("plain", "false");
this.plain = (value.ToLower() == "true" || value == "1");
Config file content meanwhile (as you see supports # symbol for line comment):
#command to run
command = php
#default script
action = index.php
#debug mode
#debug = true
#plain text mode
#plain = false
#icon = favico.ico
Try this method:
public static Dictionary<string, string> ParseIniDataWithSections(string[] iniData)
{
var dict = new Dictionary<string, string>();
var rows = iniData.Where(t =>
!String.IsNullOrEmpty(t.Trim()) && !t.StartsWith(";") && (t.Contains('[') || t.Contains('=')));
if (rows == null || rows.Count() == 0) return dict;
string section = "";
foreach (string row in rows)
{
string rw = row.TrimStart();
if (rw.StartsWith("["))
section = rw.TrimStart('[').TrimEnd(']');
else
{
int index = rw.IndexOf('=');
dict[section + "-" + rw.Substring(0, index).Trim()] = rw.Substring(index+1).Trim().Trim('"');
}
}
return dict;
}
It creates the dictionary where the key is "-". You can load it like this:
var dict = ParseIniDataWithSections(File.ReadAllLines(fileName));
I'm late to join the party, but I had the same issue today and I've written the following implementation:
using System.Text.RegularExpressions;
static bool match(this string str, string pat, out Match m) =>
(m = Regex.Match(str, pat, RegexOptions.IgnoreCase)).Success;
static void Main()
{
Dictionary<string, Dictionary<string, string>> ini = new Dictionary<string, Dictionary<string, string>>();
string section = "";
foreach (string line in File.ReadAllLines(.........)) // read from file
{
string ln = (line.Contains('#') ? line.Remove(line.IndexOf('#')) : line).Trim();
if (ln.match(#"^[ \t]*\[(?<sec>[\w\-]+)\]", out Match m))
section = m.Groups["sec"].ToString();
else if (ln.match(#"^[ \t]*(?<prop>[\w\-]+)\=(?<val>.*)", out m))
{
if (!ini.ContainsKey(section))
ini[section] = new Dictionary<string, string>();
ini[section][m.Groups["prop"].ToString()] = m.Groups["val"].ToString();
}
}
// access the ini file as follows:
string content = ini["section"]["property"];
}
It must be noted, that this implementation does not handle sections or properties which are not found.
To achieve this, you should extend the Dictionary<,>-class to handle unfound keys.
To serialize an instance of Dictionary<string, Dictionary<string, string>> to an .ini-file, I use the following code:
string targetpath = .........;
Dictionary<string, Dictionary<string, string>> ini = ........;
StringBuilder sb = new StringBuilder();
foreach (string section in ini.Keys)
{
sb.AppendLine($"[{section}]");
foreach (string property in ini[section].Keys)
sb.AppendLine($"{property}={ini[section][property]");
}
File.WriteAllText(targetpath, sb.ToString());
There is an Ini Parser available in CommonLibrary.NET
This has various very convenient overloads for getting sections/values and is very light weight.
Here is my own version, using regular expressions. This code assumes that each section name is unique - if however this is not true - it makes sense to replace Dictionary with List. This function supports .ini file commenting, starting from ';' character. Section starts normally [section], and key value pairs also comes normally "key = value". Same assumption as for sections - key name is unique.
/// <summary>
/// Loads .ini file into dictionary.
/// </summary>
public static Dictionary<String, Dictionary<String, String>> loadIni(String file)
{
Dictionary<String, Dictionary<String, String>> d = new Dictionary<string, Dictionary<string, string>>();
String ini = File.ReadAllText(file);
// Remove comments, preserve linefeeds, if end-user needs to count line number.
ini = Regex.Replace(ini, #"^\s*;.*$", "", RegexOptions.Multiline);
// Pick up all lines from first section to another section
foreach (Match m in Regex.Matches(ini, "(^|[\r\n])\\[([^\r\n]*)\\][\r\n]+(.*?)(\\[([^\r\n]*)\\][\r\n]+|$)", RegexOptions.Singleline))
{
String sectionName = m.Groups[2].Value;
Dictionary<String, String> lines = new Dictionary<String, String>();
// Pick up "key = value" kind of syntax.
foreach (Match l in Regex.Matches(ini, #"^\s*(.*?)\s*=\s*(.*?)\s*$", RegexOptions.Multiline))
{
String key = l.Groups[1].Value;
String value = l.Groups[2].Value;
// Open up quotation if any.
value = Regex.Replace(value, "^\"(.*)\"$", "$1");
if (!lines.ContainsKey(key))
lines[key] = value;
}
if (!d.ContainsKey(sectionName))
d[sectionName] = lines;
}
return d;
}
If you don't need bells and whistles (ie sections) here's a one liner:
List<(string, string)> ini = File.ReadLines(filename)
.Select(s => {
var spl = s.Split('=', 2);
return spl.Length == 2 ? (spl[0], spl[1]) : (s, "");
})
.Select(vt => (vt.Item1.Trim(), vt.Item2.Trim()))
.Where(vt => vt.Item1 != "")
.ToList();
To write:
File.WriteAllLines(filename, ini.Select(vt => $"{vt.Item1}={vt.Item2}"));
(if you don't care about duplicates use .ToDictionary() instead of .ToList() for easier access)
Here is my class, works like a charm :
public static class IniFileManager
{
[DllImport("kernel32")]
private static extern long WritePrivateProfileString(string section,
string key, string val, string filePath);
[DllImport("kernel32")]
private static extern int GetPrivateProfileString(string section,
string key, string def, StringBuilder retVal,
int size, string filePath);
[DllImport("kernel32.dll")]
private static extern int GetPrivateProfileSection(string lpAppName,
byte[] lpszReturnBuffer, int nSize, string lpFileName);
/// <summary>
/// Write Data to the INI File
/// </summary>
/// <PARAM name="Section"></PARAM>
/// Section name
/// <PARAM name="Key"></PARAM>
/// Key Name
/// <PARAM name="Value"></PARAM>
/// Value Name
public static void IniWriteValue(string sPath,string Section, string Key, string Value)
{
WritePrivateProfileString(Section, Key, Value, sPath);
}
/// <summary>
/// Read Data Value From the Ini File
/// </summary>
/// <PARAM name="Section"></PARAM>
/// <PARAM name="Key"></PARAM>
/// <PARAM name="Path"></PARAM>
/// <returns></returns>
public static string IniReadValue(string sPath,string Section, string Key)
{
StringBuilder temp = new StringBuilder(255);
int i = GetPrivateProfileString(Section, Key, "", temp,
255, sPath);
return temp.ToString();
}
}
The use is obviouse since its a static class, just call IniFileManager.IniWriteValue for readsing a section or IniFileManager.IniReadValue for reading a section.
You should read and write data from xml files since you can save a whole object to xml and also you can populate a object from a saved xml. It is better an easy to manipulate objects.
Here is how to do it:
Write Object Data to an XML File: https://msdn.microsoft.com/en-us/library/ms172873.aspx
Read Object Data from an XML File: https://msdn.microsoft.com/en-us/library/ms172872.aspx
Related
I have a server side logs folder that contains many hundreds of logs most of which are in subdirectories according to the machine the logs have come from. The task is to extract the name of the latest file in each directory containing a particular string (not all files have this string) so that analysis can be done per machine. I have included my attempt below but it seems rather clunky and long-winded and I wonder if there is an easier/better/faster/more efficient way of doing this maybe with linq?
void Main()
{
string SourcePath = #"L:\machinelogs";
string filemask = "*.log";
string searchitem = #"cannot access server data";
List<string> fileswithsearchitem = new List<string>();
DirectoryInfo directory = new DirectoryInfo(SourcePath);
IEnumerable<DirectoryInfo> dirs = directory.EnumerateDirectories("*",new EnumerationOptions() { RecurseSubdirectories = true, IgnoreInaccessible = true });
dirs.Append(directory);
foreach (var dir in dirs)
{
var found = false;
var files = dir.EnumerateFiles(filemask);
foreach(var file in files.OrderByDescending(f => f.CreationTime).ToList())
{
foreach (var line in File.ReadLines(file.FullName))
{
if(line.Contains(searchitem))
{
fileswithsearchitem.Add(file.FullName + " : " + line);
found = true;
break;
}
}
if(found)
{
break;
}
}
}
foreach (string item in fileswithsearchitem)
{
Console.WriteLine(item);
}
}
I wonder if there is an easier/better/faster/more efficient way of doing this
I second the suggestion to post your question on https://codereview.stackexchange.com/. I'm not being snarky or hostile. Asking for "an easier/better/faster/more efficient way" is asking for code review. You'll get better answers over there. With that out of the way...
...maybe with linq?
I have never seen Linq make anything execute faster. In fact the only times I've noticed a performance difference it was for the worse. On the other hand it is nice for making code more expressive. So I view Linq as a tradeoff. In this case yes it might be worth it to you.
The task is to extract the name of the latest file in each directory containing a particular string (not all files have this string) so that analysis can be done per machine.
I have included my attempt below but it seems rather clunky and long-winded
The code you've written isn't reusable; instead it requires:
A drive letter called "L" (not common)
A drive letter and paths separated with \ (which only happens on Windows)
Log file names to have the extension ".log"
The search text to be "cannot access server data"
Text to appear in STDOUT
A real file system
BUT are these really issues to you? Is it worth the time and effort to make abstractions? If what you have is working then why fix it? I can't answer these questions for you, but instead you'll have to do some soul searching.
Abstractions
Here are some possible abstractions, and some possible ways to use them.
An abstract file system
Having an abstract file system makes it easier to write automated tests so that you can be sure your code will continue working through changes over the coming years.
These are the methods I see you using:
EnumerateDirectories
EnumerateFiles
With sufficient little hand-waving, your code could look like this:
interface IDirectory
{
/// <summary>
/// Recursively yields all accessible nested directories
/// </summary>
IEnumerable<IDirectory> EnumerateDirectories();
/// <summary>
/// Yields all file paths that match the given mask. Yields them in order of
/// newest first.
/// </summary>
IEnumerable<string> EnumerateFiles(string mask);
}
interface IFileSystem
{
IDirectory OpenDirectory(string path);
Stream OpenFile(string path);
}
class DirectoryInfoAdapter : IDirectory
{
readonly DirectoryInfo _info;
public IEnumerable<IDirectory> EnumerateDirectories() => _info
.EnumerateDirectories("*", new EnumerationOptions() { RecurseSubdirectories = true, IgnoreInaccessible = true })
.Select(x => new DirectoryInfoAdapter(x));
public IEnumerable<string> EnumerateFiles(string mask) => _info
.EnumerateFiles(mask)
.Select(x => x.FullName);
}
class RealFileSystem : IFileSystem
{
public IDirectory OpenDirectory(string path) => new DirectoryInfoAdapter(new DirectoryInfo(path));
public Stream OpenFile(string path) => File.Open(path);
}
void DoStuff(IFileSystem fileSystem)
{
string SourcePath = #"L:\machinelogs";
string filemask = "*.log";
string searchitem = #"cannot access server data";
List<string> fileswithsearchitem = new List<string>();
IDirectory directory = fileSystem.OpenDirectory(SourcePath);
IEnumerable<IDirectory> dirs = directory.EnumerateDirectories();
dirs.Append(directory);
foreach (var dir in dirs)
{
var found = false;
var files = dir.EnumerateFiles(filemask);
foreach(var file in files)
{
using var stream = fileSystem.OpenFile(file);
using var reader = new StreamReader(stream);
while (reader.ReadLine() is {} line)
{
if(line.Contains(searchitem))
{
fileswithsearchitem.Add(file + " : " + line);
found = true;
break;
}
}
if(found)
{
break;
}
}
}
foreach (string item in fileswithsearchitem)
{
Console.WriteLine(item);
}
}
void Main()
{
IFileSystem fileSystem = new RealFileSystem();
DoStuff(fileSystem);
}
Then you could write an automated test like this:
class DictionaryBackedDirectory : IDirectory
{
readonly IReadOnlyCollection<IDirectory> _directories;
readonly IReadOnlyCollection<string> _files;
public DictionaryBackedDirectory(
IReadOnlyCollection<IDirectory> directories,
IReadOnlyCollection<string> files)
{
_directories = directories;
_files = files;
}
public IEnumerable<IDirectory> EnumerateDirectories() => _directories;
public IEnumerable<string> EnumerateFiles(string mask) => _files; // TODO: implement masking
}
class DictionaryBackedFileSystem : IFileSystem
{
readonly IReadOnlyDictionary<string, IDirectory> _directories;
readonly IReadOnlyDictionary<string, Func<Stream>> _files;
public DictionaryBackedFileSystem(
IReadOnlyDictionary<string, IDirectory> directories,
IReadOnlyDictionary<string, Func<Stream>> files)
{
_directories = directories;
_files = files
}
public IDirectory OpenDirectory(string path) => _directories[path];
public Stream OpenFile(string path) => _files[path]();
}
void AutomatedTest()
{
var mockFileSystem = new DictionaryBackedFileSystem(
new Dictionary<string, IDirectory>()
{
[#"L:\machinelogs"] = new DictionaryBackedDirectory(
new Dictionary<string, IDirectory>(),
new string[]
{
#"L:\machinelogs\log1.log"
}
)
},
new Dictionary<string, Func<Stream>>()
{
[#"L:\machinelogs\log1.log"] = () => new MemoryStream() // TODO: populate the memory stream with data for the test
}
)
DoStuff(mockFileSystem);
}
Advantages of doing this:
Increase reusability
You could implement a remote file system if you wanted
Make your code more testable
There are a lot of advantages to having "unit-testable code", and having
abstractions that can be "mocked" gets you closer to that golden city
Output results more abstractly
Your code doesn't have to be tied to Console.WriteLine() or to a particular output encoding.
For example:
readonly struct Result
{
public readonly string Path;
public readonly string Line;
public Result(string path, string line)
{
Path = path;
Line = line;
}
}
IEnumerable<Result> DoStuff(IFileSystem fileSystem)
{
string SourcePath = #"L:\machinelogs";
string filemask = "*.log";
string searchitem = #"cannot access server data";
IDirectory directory = fileSystem.OpenDirectory(SourcePath);
IEnumerable<IDirectory> dirs = directory.EnumerateDirectories();
dirs.Append(directory);
foreach (var dir in dirs)
{
var files = dir.EnumerateFiles(filemask);
foreach(var file in files)
{
using var stream = fileSystem.OpenFile(file);
using var reader = new StreamReader(stream);
while (reader.ReadLine() is {} line)
{
if(line.Contains(searchitem))
{
yield return new Result(file, line)
}
}
}
}
}
void Main()
{
IFileSystem fileSystem = new RealFileSystem();
foreach (var result in DoStuff(fileSystem))
{
Console.WriteLine(result.File + " : " + result.Line);
break; // Could easily change this to continue searching
}
}
See how this moves the console interaction out of your code, makes the output format someone else's problem, and also lets the consumer of your code decide if they want to continue searching once you have a search hit?
This will also get your code one step closer to be unit-testable. Feel free to ask if it's not clear why.
Inject parameters
The source path, file mask, and search item don't have to be hardcoded constants.
For example:
IEnumerable<Result> DoStuff(
IFileSystem fileSystem,
string sourcePath,
string fileMask,
string searchItem)
{
IDirectory directory = fileSystem.OpenDirectory(sourcePath);
IEnumerable<IDirectory> dirs = directory.EnumerateDirectories();
dirs.Append(directory);
foreach (var dir in dirs)
{
var files = dir.EnumerateFiles(fileMask);
foreach(var file in files)
{
using var stream = fileSystem.OpenFile(file);
using var reader = new StreamReader(stream);
while (reader.ReadLine() is {} line)
{
if(line.Contains(searchItem))
{
yield return new Result(file, line)
}
}
}
}
}
void Main()
{
IFileSystem fileSystem = new RealFileSystem();
foreach (var result in DoStuff(
fileSystem,
#"L:\machinelogs",
"*.log",
#"cannot access server data"
))
{
Console.WriteLine(result.File + " : " + result.Line);
break;
}
}
See how this makes it possible to search for other things?
Use Path.Combine
This will remove one dependency on Windows--namely the backslash path separator.
void Main()
{
IFileSystem fileSystem = new RealFileSystem();
foreach (var result in DoStuff(
fileSystem,
Path.Combine("L:", "machinelogs"),
"*.log",
#"cannot access server data"
))
{
Console.WriteLine(result.File + " : " + result.Line);
break;
}
}
I wouldn't be surprised if none of my code above compiles. This was written off the cuff
I'm trying to create a folder structure under a list in Sharepoint online.
I created a list called Shared1 according to this article:
http://blogs.technet.com/b/catastrophic_failure_joannav/archive/2013/10/23/how-to-create-a-custom-list-in-sharepoint-online-quot-w15-quot.aspx
And I'm using this code for trying to create an underlying structure:
using (var clientContext = new ClientContext("https://myEnv.sharepoint.com"))
{
var passWord = new SecureString();
foreach (char c in "myPSW".ToCharArray()) passWord.AppendChar(c);
clientContext.Credentials = new SharePointOnlineCredentials("myAcc#myenv.onmicrosoft.com", passWord);
Web web = clientContext.Web;
clientContext.Load(web);
clientContext.ExecuteQuery();
var folder = CreateFolder(clientContext.Web, "Shared1", "FolderA/SubFolderA/SubSubFolderA");
}
/// <summary>
/// Create Folder client object
/// </summary>
/// <param name="web"></param>
/// <param name="listTitle"></param>
/// <param name="fullFolderUrl"></param>
/// <returns></returns>
public static Folder CreateFolder(Web web, string listTitle, string fullFolderUrl)
{
if (string.IsNullOrEmpty(fullFolderUrl))
throw new ArgumentNullException("fullFolderUrl");
var list = web.Lists.GetByTitle(listTitle);
return CreateFolderInternal(web, list.RootFolder, fullFolderUrl);
}
private static Folder CreateFolderInternal(Web web, Folder parentFolder, string fullFolderUrl)
{
var folderUrls = fullFolderUrl.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
string folderUrl = folderUrls[0];
var curFolder = parentFolder.Folders.Add(folderUrl);
web.Context.Load(curFolder);
web.Context.ExecuteQuery();
if (folderUrls.Length > 1)
{
var subFolderUrl = string.Join("/", folderUrls, 1, folderUrls.Length - 1);
return CreateFolderInternal(web, curFolder, subFolderUrl);
}
return curFolder;
}
public static Folder GetFolder(Web web, string fullFolderUrl)
{
if (string.IsNullOrEmpty(fullFolderUrl))
throw new ArgumentNullException("fullFolderUrl");
if (!web.IsPropertyAvailable("ServerRelativeUrl"))
{
web.Context.Load(web, w => w.ServerRelativeUrl);
web.Context.ExecuteQuery();
}
var folder = web.GetFolderByServerRelativeUrl(web.ServerRelativeUrl + fullFolderUrl);
web.Context.Load(folder);
web.Context.ExecuteQuery();
return folder;
}
And upon the execution I get this error:
Unhandled Exception: Microsoft.SharePoint.Client.ServerException: List 'Shared1'
does not exist at site with URL 'https://myEnv.sharepoint.com'.
at Microsoft.SharePoint.Client.ClientRequest.ProcessResponseStream(Stream res
ponseStream)
at Microsoft.SharePoint.Client.ClientRequest.ProcessResponse()
at SharepointFolderRename.Program.CreateFolderInternal(Web web, Folder parent
Folder, String fullFolderUrl) in \\vmware-host\shared folders\Documents\Visual S
tudio 2012\Projects\Trunk2015\SharepointFolderRename\Program.cs:line 84
at SharepointFolderRename.Program.Main(String[] args) in \\vmware-host\shared
folders\Documents\Visual Studio 2012\Projects\Trunk2015\SharepointFolderRename\
Program.cs:line 49
This is a print of the list in SP.
What am I missing?
The reason why you are getting those errors is related with incorrect web url specified. While your list exists on sub site Shared1, you are trying to access it on root site,the below picture illustrates it
So, the solution would be to initialize client context for the proper web site, so replace:
using (var clientContext = new ClientContext("https://myEnv.sharepoint.com/"))
{
//...
}
with
using (var clientContext = new ClientContext("https://myEnv.sharepoint.com/Shared1"))
{
//...
}
It is assumed that you want to ctreate folders for list located on
Shared1 sub site
Example
The example below demonstrates how to create the folder hierarchy in Documents library under News sub site: (https://contoso.sharepoint.com/news)
Archive
|
2009
|
09
Usage
using (var ctx = GetContext("https://contoso.sharepoint.com/news", userName, password))
{
var list = ctx.Web.Lists.GetByTitle("Documents");
var folder = list.CreateFolder("Archive/2015/09");
Console.WriteLine(folder.ServerRelativeUrl);
}
where
public static ClientContext GetContext(Uri webUri, string userName, string password)
{
var securePassword = new SecureString();
foreach (var ch in password) securePassword.AppendChar(ch);
return new ClientContext(webUri) { Credentials = new SharePointOnlineCredentials(userName, securePassword) };
}
ListExtensions.cs file:
using System;
using Microsoft.SharePoint.Client;
namespace SharePoint.ClientExtensions
{
public static class ListExtensions
{
/// <summary>
/// Create Folder in List
/// </summary>
/// <param name="list"></param>
/// <param name="folderUrl"></param>
/// <returns></returns>
public static Folder CreateFolder(this List list, string folderUrl)
{
if (string.IsNullOrEmpty(folderUrl))
throw new ArgumentNullException("folderUrl");
if (!list.IsPropertyAvailable("RootFolder"))
{
list.Context.Load(list.RootFolder);
list.Context.ExecuteQuery();
}
return CreateFolderInternal(list.RootFolder,folderUrl);
}
private static Folder CreateFolderInternal(Folder parentFolder, string folderUrl)
{
var folderUrlParts = folderUrl.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
var curFolder = parentFolder.Folders.Add(folderUrlParts[0]);
parentFolder.Context.Load(curFolder);
parentFolder.Context.ExecuteQuery();
if (folderUrlParts.Length > 1)
{
var subFolderUrl = string.Join("/", folderUrlParts, 1, folderUrlParts.Length - 1);
return CreateFolderInternal(curFolder, subFolderUrl);
}
return curFolder;
}
}
}
You need to load the list in the CreateFolder method.
public static Folder CreateFolder(Web web, string listTitle, string fullFolderUrl)
{
if (string.IsNullOrEmpty(fullFolderUrl))
throw new ArgumentNullException("fullFolderUrl");
var list = web.Lists.GetByTitle(listTitle);
clientContext.Load(list);
clientCOntext.Execute();
return CreateFolderInternal(web, list.RootFolder, fullFolderUrl);
}
After search here in the forum i found solution to my question: i have root folder and i want to find the newest file from each directory under the root folder:
public static void FindNewestFile(string path)
{
List<string> list = getNewestFile(path);
foreach (string dir in list)
{
DirectoryInfo directory = new DirectoryInfo(dir);
try
{
FileInfo file = directory.GetFiles("*.*", SearchOption.AllDirectories).OrderByDescending(f => f.LastWriteTime).FirstOrDefault();
if (file != null)
{
// Do things with my file
}
}
catch (UnauthorizedAccessException)
{ }
}
}
private static List<string> getNewestFile(string path)
{
List<string> list = new List<string>();
foreach (string dir in EnumerateFoldersRecursively(path))
{
list.Add(dir);
}
return list;
}
private static IEnumerable<string> EnumerateFoldersRecursively(string root)
{
foreach (var folder in EnumerateFolders(root))
{
yield return folder;
foreach (var subFolder in EnumerateFoldersRecursively(folder))
{
yield return subFolder;
}
}
}
private static IEnumerable<string> EnumerateFolders(string root)
{
WIN32_FIND_DATA findData;
string spec = Path.Combine(root, "*");
using (SafeFindHandle findHandle = FindFirstFile(spec, out findData))
{
if (!findHandle.IsInvalid)
{
do
{
if ((findData.cFileName != ".") && (findData.cFileName != "..")) // Ignore special "." and ".." folders.
{
if ((findData.dwFileAttributes & FileAttributes.Directory) != 0)
{
yield return Path.Combine(root, findData.cFileName);
}
}
}
while (FindNextFile(findHandle, out findData));
}
}
}
My problem is that it bypass the root directory and not return the newest file from this directory
Just slightly alter your code to add this line:
List<string> list = getNewestFile(path);
list.Add(path); //Add current directory to list as well
foreach (string dir in list) //..etc
Should be the easiest fix I'd say.
If you want have the list of all your file you can do this:
string[] filePaths = Directory.GetFiles(#"c:\MyDir\", SearchOption.AllDirectories);
And then you can compare te array with an other older to see if there are new file or if files are delete
Wow, that is a lot of code for something where you can let .Net's BCL classes (and Linq) do the heavy lifting...
This should suffice:
public IEnumerable<FileInfo> GetNewestFilePerDirectory(
string root,
string pattern = "*",
SearchOption searchoption = SearchOption.TopDirectoryOnly
)
{
return new DirectoryInfo(root)
.EnumerateFiles(pattern, searchoption)
.GroupBy(g => g.Directory.FullName)
.Select(s => s.OrderBy(f => f.Name)
.First(f => f.CreationTimeUtc == s.Max(m => m.CreationTimeUtc))
);
}
A simple console-app demonstration / documentation to put the dots on the i:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
class Program
{
private static void Main(string[] args)
{
var newestfiles = GetNewestFilePerDirectory(
#"D:\foo\bar", "*", SearchOption.AllDirectories
);
Console.WriteLine(string.Join("\r\n", newestfiles.Select(f => f.FullName)));
}
/// <summary>
/// Scans a directory (and, optionally, subdirectories) and returns an
/// enumerable of <see cref="FileInfo"/> for the newest file in eacht
/// directory.
/// </summary>
/// <param name="root">
/// A string specifying the path to scan.
/// </param>
/// <param name="pattern">
/// The search string. The default pattern is "*", which returns all files.
/// </param>
/// <param name="searchoption">
/// One of the enumeration values that specifies whether the search operation should
/// include only the current directory or all subdirectories. The default value is
/// <see cref="SearchOption.TopDirectoryOnly"/>.
/// </param>
/// <returns>
/// Returns the newest file, per directory.
/// </returns>
/// <remarks>
/// For directories containing files of the same createtiondate, the first file when
/// sorted alphabetical will be returned.
/// </remarks>
private static IEnumerable<FileInfo> GetNewestFilePerDirectory(
string root,
string pattern = "*",
SearchOption searchoption = SearchOption.TopDirectoryOnly
)
{
return new DirectoryInfo(root)
.EnumerateFiles(pattern, searchoption)
.GroupBy(g => g.Directory.FullName)
.Select(s => s.OrderBy(f => f.Name)
.First(f => f.CreationTimeUtc == s.Max(m => m.CreationTimeUtc))
);
}
}
I need to create an application that parses a PST file and converts the mails into multiple EML files. Basically, I need to do the opposite of what's being asked in this question.
Is there any sample code or guidelines to achieve this feature?
You could use the Outlook Redemption library which is capable of opening PST and extracting messages as .EML (among other formats). Redemption is a COM Object (32 or 64 bit) that can be used in C# without any problem. Here is a Console Application sample code that demonstrates this:
using System;
using System.IO;
using System.Text;
using Redemption;
namespace DumpPst
{
class Program
{
static void Main(string[] args)
{
// extract 'test.pst' in the 'test' folder
ExtractPst("test.pst", Path.GetFullPath("test"));
}
public static void ExtractPst(string pstFilePath, string folderPath)
{
if (pstFilePath == null)
throw new ArgumentNullException("pstFilePath");
RDOSession session = new RDOSession();
RDOPstStore store = session.LogonPstStore(pstFilePath);
ExtractPstFolder(store.RootFolder, folderPath);
}
public static void ExtractPstFolder(RDOFolder folder, string folderPath)
{
if (folder == null)
throw new ArgumentNullException("folder");
if (folderPath == null)
throw new ArgumentNullException("folderPath");
if (folder.FolderKind == rdoFolderKind.fkSearch)
return;
if (!Directory.Exists(folderPath))
{
Directory.CreateDirectory(folderPath);
}
foreach(RDOFolder child in folder.Folders)
{
ExtractPstFolder(child, Path.Combine(folderPath, ToFileName(child.Name)));
}
foreach (var item in folder.Items)
{
RDOMail mail = item as RDOMail;
if (mail == null)
continue;
mail.SaveAs(Path.Combine(folderPath, ToFileName(mail.Subject)) + ".eml", rdoSaveAsType.olRFC822);
}
}
/// <summary>
/// Converts a text into a valid file name.
/// </summary>
/// <param name="fileName">The file name.</param>
/// <returns>
/// A valid file name.
/// </returns>
public static string ToFileName(string fileName)
{
return ToFileName(fileName, null, null);
}
/// <summary>
/// Converts a text into a valid file name.
/// </summary>
/// <param name="fileName">The file name.</param>
/// <param name="reservedNameFormat">The reserved format to use for reserved names. If null '_{0}_' will be used.</param>
/// <param name="reservedCharFormat">The reserved format to use for reserved characters. If null '_x{0}_' will be used.</param>
/// <returns>
/// A valid file name.
/// </returns>
public static string ToFileName(string fileName, string reservedNameFormat, string reservedCharFormat)
{
if (fileName == null)
throw new ArgumentNullException("fileName");
if (string.IsNullOrEmpty(reservedNameFormat))
{
reservedNameFormat = "_{0}_";
}
if (string.IsNullOrEmpty(reservedCharFormat))
{
reservedCharFormat = "_x{0}_";
}
if (Array.IndexOf(ReservedFileNames, fileName.ToLowerInvariant()) >= 0 ||
IsAllDots(fileName))
return string.Format(reservedNameFormat, fileName);
char[] invalid = Path.GetInvalidFileNameChars();
StringBuilder sb = new StringBuilder(fileName.Length);
foreach (char c in fileName)
{
if (Array.IndexOf(invalid, c) >= 0)
{
sb.AppendFormat(reservedCharFormat, (short)c);
}
else
{
sb.Append(c);
}
}
string s = sb.ToString();
// directory limit is 255
if (s.Length > 254)
{
s = s.Substring(0, 254);
}
if (string.Equals(s, fileName, StringComparison.Ordinal))
{
s = fileName;
}
return s;
}
private static bool IsAllDots(string fileName)
{
foreach (char c in fileName)
{
if (c != '.')
return false;
}
return true;
}
private static readonly string[] ReservedFileNames = new[]
{
"con", "prn", "aux", "nul",
"com0", "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9",
"lpt0", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9"
};
}
}
You need essentially to do the inverse of what is being asked in that question.
Load the PST file using Outlook interop (or redemption as above)
Enumerate all the files.
Use CDO, System.Mail or similar to compose an EML file for each file in the PST.
The thing to note is that a PST doesn't contain EML files, it contains MSG files. So you will have to do some form of conversion, and you will not get back exactly what was originally sent.
See also this question: Are there .NET Framework methods to parse an email (MIME)?
I spent a lot of time trying to figure out a good way to embed any file into Microsoft Word using OpenXml 2.0; Office documents are fairly easy but what about other file types such as PDF, TXT, GIF, JPG, HTML, etc....
What is a good way to get this to work for any file type, in C#?
Embedding Foreign Objects (PDF, TXT, GIF, etc…) into Microsoft Word using OpenXml 2.0
(Well, in collaboration with COM)
I got a lot from this site, so here I asked and answered my own question in order to give back a little on a topic in which I had difficulty finding answers on, hope it helps people.
There are several examples out there that show how to embed an Office Document into another Office Document using OpenXml 2.0, what’s not out there and easily understandable is how to embed just about any file into and Office Document.
I have learned a lot from other people’s code, so this is my attempt to contribute. Since I am already using OpenXml to generate documents, and I am in need of embedding other files into Word, I have decided use a collaboration of OpenXml and COM (Microsoft Office 2007 dll’s) to achieve my goal. If you are like me, “invoking the OLE server application to create an IStorage” doesn’t mean much to you.
In this example I’d like to show how I use COM to PROGRMATICALLY get the OLE-binary data information of the attached file, and then how I used that information within my OpenXml document. Basically, I am programmatically looking at the OpenXml 2.0 Document Reflector to get the information I need.
My code below is broken down into several classes, but here is an outline of what I am doing:
Create an OpenXml WordProcessingDocument, get the System.IO.FileInfo for the file you want to Embed
Create a custom OpenXmlEmbeddedObject object (this is what holds all the binary data)
Use the binary data from the above step to create Data and Image Streams
Use those Streams as the File Object and File Image for your OpenXml Document
I know there is a lot of code, and not much explanation… Hopefully it is easy to follow and will help people out
Requirements:
• DocumentFormat.OpenXml dll (OpenXml 2.0)
• WindowsBase dll
• Microsoft.Office.Interop.Word dll (Office 2007 – version 12)
• This the main class that starts everything, opens a WordProcessingDocument and class to have the file attached
using DocumentFormat.OpenXml.Packaging;
using System.IO;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Wordprocessing;
public class MyReport
{
private MainDocumentPart _mainDocumentPart;
public void CreateReport()
{
using (WordprocessingDocument wpDocument = WordprocessingDocument.Create(#"TempPath\MyReport.docx", WordprocessingDocumentType.Document))
{
_mainDocumentPart = wpDocument.AddMainDocumentPart();
_mainDocumentPart.Document = new Document(new Body());
AttachFile(#"MyFilePath\MyFile.pdf", true);
}
}
private void AttachFile(string filePathAndName, bool displayAsIcon)
{
FileInfo fileInfo = new FileInfo(filePathAndName);
OpenXmlHelper.AppendEmbeddedObject(_mainDocumentPart, fileInfo, displayAsIcon);
}
}
• This class in an OpenXml helper class, holds all the logic to embed an object into your OpenXml File
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Validation;
using DocumentFormat.OpenXml.Wordprocessing;
using OVML = DocumentFormat.OpenXml.Vml.Office;
using V = DocumentFormat.OpenXml.Vml;
public class OpenXmlHelper
{
/// <summary>
/// Appends an Embedded Object into the specified Main Document
/// </summary>
/// <param name="mainDocumentPart">The MainDocument Part of your OpenXml Word Doc</param>
/// <param name="fileInfo">The FileInfo object associated with the file being embedded</param>
/// <param name="displayAsIcon">Whether or not to display the embedded file as an Icon (Otherwise it will display a snapshot of the file)</param>
public static void AppendEmbeddedObject(MainDocumentPart mainDocumentPart, FileInfo fileInfo, bool displayAsIcon)
{
OpenXmlEmbeddedObject openXmlEmbeddedObject = new OpenXmlEmbeddedObject(fileInfo, displayAsIcon);
if (!String.IsNullOrEmpty(openXmlEmbeddedObject.OleObjectBinaryData))
{
using (Stream dataStream = new MemoryStream(Convert.FromBase64String(openXmlEmbeddedObject.OleObjectBinaryData)))
{
if (!String.IsNullOrEmpty(openXmlEmbeddedObject.OleImageBinaryData))
{
using (Stream emfStream = new MemoryStream(Convert.FromBase64String(openXmlEmbeddedObject.OleImageBinaryData)))
{
string imagePartId = GetUniqueXmlItemID();
ImagePart imagePart = mainDocumentPart.AddImagePart(ImagePartType.Emf, imagePartId);
if (emfStream != null)
{
imagePart.FeedData(emfStream);
}
string embeddedPackagePartId = GetUniqueXmlItemID();
if (dataStream != null)
{
if (openXmlEmbeddedObject.ObjectIsOfficeDocument)
{
EmbeddedPackagePart embeddedObjectPart = mainDocumentPart.AddNewPart<EmbeddedPackagePart>(
openXmlEmbeddedObject.FileContentType, embeddedPackagePartId);
embeddedObjectPart.FeedData(dataStream);
}
else
{
EmbeddedObjectPart embeddedObjectPart = mainDocumentPart.AddNewPart<EmbeddedObjectPart>(
openXmlEmbeddedObject.FileContentType, embeddedPackagePartId);
embeddedObjectPart.FeedData(dataStream);
}
}
if (!displayAsIcon && !openXmlEmbeddedObject.ObjectIsPicture)
{
Paragraph attachmentHeader = CreateParagraph(String.Format("Attachment: {0} (Double-Click to Open)", fileInfo.Name));
mainDocumentPart.Document.Body.Append(attachmentHeader);
}
Paragraph embeddedObjectParagraph = GetEmbeededObjectParagraph(openXmlEmbeddedObject.FileType,
imagePartId, openXmlEmbeddedObject.OleImageStyle, embeddedPackagePartId);
mainDocumentPart.Document.Body.Append(embeddedObjectParagraph);
}
}
}
}
}
/// <summary>
/// Gets Paragraph that includes the embedded object
/// </summary>
private static Paragraph GetEmbeededObjectParagraph(string fileType, string imageID, string imageStyle, string embeddedPackageID)
{
EmbeddedObject embeddedObject = new EmbeddedObject();
string shapeID = GetUniqueXmlItemID();
V.Shape shape = new V.Shape() { Id = shapeID, Style = imageStyle };
V.ImageData imageData = new V.ImageData() { Title = "", RelationshipId = imageID };
shape.Append(imageData);
OVML.OleObject oleObject = new OVML.OleObject()
{
Type = OVML.OleValues.Embed,
ProgId = fileType,
ShapeId = shapeID,
DrawAspect = OVML.OleDrawAspectValues.Icon,
ObjectId = GetUniqueXmlItemID(),
Id = embeddedPackageID
};
embeddedObject.Append(shape);
embeddedObject.Append(oleObject);
Paragraph paragraphImage = new Paragraph();
Run runImage = new Run(embeddedObject);
paragraphImage.Append(runImage);
return paragraphImage;
}
/// <summary>
/// Gets a Unique ID for an XML Item, for reference purposes
/// </summary>
/// <returns>A GUID string with removed dashes</returns>
public static string GetUniqueXmlItemID()
{
return "r" + System.Guid.NewGuid().ToString().Replace("-", "");
}
private static Paragraph CreateParagraph(string paragraphText)
{
Paragraph paragraph = new Paragraph();
ParagraphProperties paragraphProperties = new ParagraphProperties();
paragraphProperties.Append(new Justification()
{
Val = JustificationValues.Left
});
paragraphProperties.Append(new SpacingBetweenLines()
{
After = Convert.ToString(100),
Line = Convert.ToString(100),
LineRule = LineSpacingRuleValues.AtLeast
});
Run run = new Run();
RunProperties runProperties = new RunProperties();
Text text = new Text();
if (!String.IsNullOrEmpty(paragraphText))
{
text.Text = paragraphText;
}
run.Append(runProperties);
run.Append(text);
paragraph.Append(paragraphProperties);
paragraph.Append(run);
return paragraph;
}
}
• This is the most important part of this process, it is using Microsoft's internal OLE Server, creates the Binary DATA and Binary EMF information for a file. All you have to here is call the OpenXmlEmbeddedObject constructor and all get’s taken care of. It will mimic the process that goes on when you manually drag any file into Word; there is some kind of conversion that goes on when you do that, turning the file into an OLE object, so that Microsoft can recognize the file.
o The most imporant parts of this class are the OleObjectBinaryData and OleImageBinaryData properties; they contain the 64Bit string binary info for the file data and ‘.emf’ image.
o If you choose to not display the file as an icon, then the ‘.emf’ image data will create a snapshot of the file, like the first page of the pdf file for example, in which you can still double-click to open
o If you are embedding an image and choose not to display it as an Icon, then the OleObjectBinaryData and OleImageBinaryData properties will be the same
using System.Runtime.InteropServices;
using System.Xml;
using System.Diagnostics;
using System.IO;
using System.Drawing;
using Microsoft.Office.Interop.Word;
public class OpenXmlEmbeddedObject
{
#region Constants
private const string _defaultOleContentType = "application/vnd.openxmlformats-officedocument.oleObject";
private const string _oleObjectDataTag = "application/vnd";
private const string _oleImageDataTag = "image/x-emf";
#endregion Constants
#region Member Variables
private static FileInfo _fileInfo;
private static string _filePathAndName;
private static bool _displayAsIcon;
private static bool _objectIsPicture;
private object _objectMissing = System.Reflection.Missing.Value;
private object _objectFalse = false;
private object _objectTrue = true;
#endregion Member Variables
#region Properties
/// <summary>
/// The File Type, as stored in Registry (Ex: a GIF Image = 'giffile')
/// </summary>
public string FileType
{
get
{
if (String.IsNullOrEmpty(_fileType) && _fileInfo != null)
{
_fileType = GetFileType(_fileInfo, false);
}
return _fileType;
}
}
private string _fileType;
/// <summary>
/// The File Context Type, as storered in Registry (Ex: a GIF Image = 'image/gif')
/// * Is converted into the 'Default Office Context Type' for non-office files
/// </summary>
public string FileContentType
{
get
{
if (String.IsNullOrEmpty(_fileContentType) && _fileInfo != null)
{
_fileContentType = GetFileContentType(_fileInfo);
if (!_fileContentType.Contains("officedocument"))
{
_fileContentType = _defaultOleContentType;
}
}
return _fileContentType;
}
}
private string _fileContentType;
/// <summary>
/// Gets the ContentType Text for the file
/// </summary>
public static string GetFileContentType(FileInfo fileInfo)
{
if (fileInfo == null)
{
throw new ArgumentNullException("fileInfo");
}
string mime = "application/octetstream";
string ext = System.IO.Path.GetExtension(fileInfo.Name).ToLower();
Microsoft.Win32.RegistryKey rk = Microsoft.Win32.Registry.ClassesRoot.OpenSubKey(ext);
if (rk != null && rk.GetValue("Content Type") != null)
{
mime = rk.GetValue("Content Type").ToString();
}
return mime;
}
public bool ObjectIsOfficeDocument
{
get { return FileContentType != _defaultOleContentType; }
}
public bool ObjectIsPicture
{
get { return _objectIsPicture; }
}
public string OleObjectBinaryData
{
get { return _oleObjectBinaryData; }
set { _oleObjectBinaryData = value; }
}
private string _oleObjectBinaryData;
public string OleImageBinaryData
{
get { return _oleImageBinaryData; }
set { _oleImageBinaryData = value; }
}
private string _oleImageBinaryData;
/// <summary>
/// The OpenXml information for the Word Application that is created (Make-Shoft Code Reflector)
/// </summary>
public string WordOpenXml
{
get { return _wordOpenXml; }
set { _wordOpenXml = value; }
}
private String _wordOpenXml;
/// <summary>
/// The XmlDocument that is created based on the OpenXml Data from WordOpenXml
/// </summary>
public XmlDocument OpenXmlDocument
{
get
{
if (_openXmlDocument == null && !String.IsNullOrEmpty(WordOpenXml))
{
_openXmlDocument = new XmlDocument();
_openXmlDocument.LoadXml(WordOpenXml);
}
return _openXmlDocument;
}
}
private XmlDocument _openXmlDocument;
/// <summary>
/// The XmlNodeList, for all Nodes containing 'binaryData'
/// </summary>
public XmlNodeList BinaryDataXmlNodesList
{
get
{
if (_binaryDataXmlNodesList == null && OpenXmlDocument != null)
{
_binaryDataXmlNodesList = OpenXmlDocument.GetElementsByTagName("pkg:binaryData");
}
return _binaryDataXmlNodesList;
}
}
private XmlNodeList _binaryDataXmlNodesList;
/// <summary>
/// Icon Object for the file
/// </summary>
public Icon ObjectIcon
{
get
{
if (_objectIcon == null)
{
_objectIcon = Enterprise.Windows.Win32.Win32.GetLargeIcon(_filePathAndName);
}
return _objectIcon;
}
}
private Icon _objectIcon;
/// <summary>
/// File Name for the Icon being created
/// </summary>
public string ObjectIconFile
{
get
{
if (String.IsNullOrEmpty(_objectIconFile))
{
_objectIconFile = String.Format("{0}.ico", _filePathAndName.Replace(".", ""));
}
return _objectIconFile;
}
}
private string _objectIconFile;
/// <summary>
/// Gets the original height and width of the emf file being created
/// </summary>
public string OleImageStyle
{
get
{
if (String.IsNullOrEmpty(_oleImageStyle) && !String.IsNullOrEmpty(WordOpenXml))
{
XmlNodeList xmlNodeList = OpenXmlDocument.GetElementsByTagName("v:shape");
if (xmlNodeList != null && xmlNodeList.Count > 0)
{
foreach (XmlAttribute attribute in xmlNodeList[0].Attributes)
{
if (attribute.Name == "style")
{
_oleImageStyle = attribute.Value;
}
}
}
}
return _oleImageStyle;
}
set { _oleImageStyle = value; }
}
private string _oleImageStyle;
#endregion Properties
#region Constructor
/// <summary>
/// Generates binary information for the file being passed in
/// </summary>
/// <param name="fileInfo">The FileInfo object for the file to be embedded</param>
/// <param name="displayAsIcon">Whether or not to display the file as an Icon (Otherwise it will show a snapshot view of the file)</param>
public OpenXmlEmbeddedObject(FileInfo fileInfo, bool displayAsIcon)
{
_fileInfo = fileInfo;
_filePathAndName = fileInfo.ToString();
_displayAsIcon = displayAsIcon;
SetupOleFileInformation();
}
#endregion Constructor
#region Methods
/// <summary>
/// Creates a temporary Word App in order to add an OLE Object, get's the OpenXML data from the file (similar to the Code Reflector info)
/// </summary>
private void SetupOleFileInformation()
{
Microsoft.Office.Interop.Word.Application wordApplication = new Microsoft.Office.Interop.Word.Application();
Microsoft.Office.Interop.Word.Document wordDocument = wordApplication.Documents.Add(ref _objectMissing, ref _objectMissing,
ref _objectMissing, ref _objectMissing);
object iconObjectFileName = _objectMissing;
object objectClassType = FileType;
object objectFilename = _fileInfo.ToString();
Microsoft.Office.Interop.Word.InlineShape inlineShape = null;
if (_displayAsIcon)
{
if (ObjectIcon != null)
{
using (FileStream iconStream = new FileStream(ObjectIconFile, FileMode.Create))
{
ObjectIcon.Save(iconStream);
iconObjectFileName = ObjectIconFile;
}
}
object objectIconLabel = _fileInfo.Name;
inlineShape = wordDocument.InlineShapes.AddOLEObject(ref objectClassType,
ref objectFilename, ref _objectFalse, ref _objectTrue, ref iconObjectFileName,
ref _objectMissing, ref objectIconLabel, ref _objectMissing);
}
else
{
try
{
Image image = Image.FromFile(_fileInfo.ToString());
_objectIsPicture = true;
OleImageStyle = String.Format("height:{0}pt;width:{1}pt", image.Height, image.Width);
wordDocument.InlineShapes.AddPicture(_fileInfo.ToString(), ref _objectMissing, ref _objectTrue, ref _objectMissing);
}
catch
{
inlineShape = wordDocument.InlineShapes.AddOLEObject(ref objectClassType,
ref objectFilename, ref _objectFalse, ref _objectFalse, ref _objectMissing, ref _objectMissing,
ref _objectMissing, ref _objectMissing);
}
}
WordOpenXml = wordDocument.Range(ref _objectMissing, ref _objectMissing).WordOpenXML;
if (_objectIsPicture)
{
OleObjectBinaryData = GetPictureBinaryData();
OleImageBinaryData = GetPictureBinaryData();
}
else
{
OleObjectBinaryData = GetOleBinaryData(_oleObjectDataTag);
OleImageBinaryData = GetOleBinaryData(_oleImageDataTag);
}
// Not sure why, but Excel seems to hang in the processes if you attach an Excel file…
// This kills the excel process that has been started < 15 seconds ago (so not to kill the user's other Excel processes that may be open)
if (FileType.StartsWith("Excel"))
{
Process[] processes = Process.GetProcessesByName("EXCEL");
foreach (Process process in processes)
{
if (DateTime.Now.Subtract(process.StartTime).Seconds <= 15)
{
process.Kill();
break;
}
}
}
wordDocument.Close(ref _objectFalse, ref _objectMissing, ref _objectMissing);
wordApplication.Quit(ref _objectMissing, ref _objectMissing, ref _objectMissing);
}
/// <summary>
/// Gets the binary data from the Xml File that is associated with the Tag passed in
/// </summary>
/// <param name="binaryDataXmlTag">the Tag to look for in the OpenXml</param>
/// <returns></returns>
private string GetOleBinaryData(string binaryDataXmlTag)
{
string binaryData = null;
if (BinaryDataXmlNodesList != null)
{
foreach (XmlNode xmlNode in BinaryDataXmlNodesList)
{
if (xmlNode.ParentNode != null)
{
foreach (XmlAttribute attr in xmlNode.ParentNode.Attributes)
{
if (String.IsNullOrEmpty(binaryData) && attr.Value.Contains(binaryDataXmlTag))
{
binaryData = xmlNode.InnerText;
break;
}
}
}
}
}
return binaryData;
}
/// <summary>
/// Gets the image Binary data, if the file is an image
/// </summary>
/// <returns></returns>
private string GetPictureBinaryData()
{
string binaryData = null;
if (BinaryDataXmlNodesList != null)
{
foreach (XmlNode xmlNode in BinaryDataXmlNodesList)
{
binaryData = xmlNode.InnerText;
break;
}
}
return binaryData;
}
/// <summary>
/// Gets the file type description ("Application", "Text Document", etc.) for the file.
/// </summary>
/// <param name="fileInfo">FileInfo containing extention</param>
/// <returns>Type Description</returns>
public static string GetFileType(FileInfo fileInfo, bool returnDescription)
{
if (fileInfo == null)
{
throw new ArgumentNullException("fileInfo");
}
string description = "File";
if (string.IsNullOrEmpty(fileInfo.Extension))
{
return description;
}
description = string.Format("{0} File", fileInfo.Extension.Substring(1).ToUpper());
RegistryKey typeKey = Registry.ClassesRoot.OpenSubKey(fileInfo.Extension);
if (typeKey == null)
{
return description;
}
string type = Convert.ToString(typeKey.GetValue(string.Empty));
RegistryKey key = Registry.ClassesRoot.OpenSubKey(type);
if (key == null)
{
return description;
}
if (returnDescription)
{
description = Convert.ToString(key.GetValue(string.Empty));
return description;
}
else
{
return type;
}
}
#endregion Methods
}
_objectIcon = Enterprise.Windows.Win32.Win32.GetLargeIcon(_filePathAndName);
seems to be broken, but
_objectIcon = System.Drawing.Icon.ExtractAssociatedIcon(_filePathAndName);
should also work.
My answer here will tell you how to do this, but not show you with the SDK or a specific language.
This is a great answer and it helped me a lot, but the potential bug that user bic mentioned also exists
in
OpenXmlEmbeddedObject(FileInfo fileInfo, bool displayAsIcon)
at line 242,
_filePathAndName = fileInfo.ToString();
in
SetupOleFileInformation()
at line 264,
object objectFilename = _fileInfo.ToString();
line 289, and
Image image = Image.FromFile(_fileInfo.ToString());
line 293
wordDocument.InlineShapes.AddPicture(_fileInfo.toString(), ref _objectMissing, ref _objectTrue, ref _objectMissing);
All of these need to be "FullName" instead of "ToString()" if the code should work with relative paths as well. Hope this helps anyone who wants to use D Lyonnais's code!
Make a copy of the document how you wanted to get through code and then use Open XML SDK Tool 2.5 for Microsoft Office for writing code. This tool reflects code. which you can simply copy paste.