This is my first OpenXML project. I am trying to edit the CustomXML file of a docx file. I am trying to change this:
<?xml version="1.0" encoding="UTF-8"?>
<PERSON>
<NAMETAG>NAME</NAMETAG>
<DOBTAG>DOB</DOBTAG>
<SCORE1TAG>SCORE1</SCORE1TAG>
<SCORE2TAG>SCORE2</SCORE2TAG>
</PERSON>
To this:
<?xml version="1.0" encoding="UTF-8"?>
<PERSON>
<NAMETAG>John Doe</NAMETAG>
<DOBTAG>01/01/2020</DOBTAG>
<SCORE1TAG>90.5</SCORE1TAG>
<SCORE2TAG>100.0</SCORE2TAG>
</PERSON>
I would prefer to not use search and replace but instead navigate the WordprocessingDocument to find the correct properties to modify. I tried to do a whole delete/add but that corrupted the file and did not work. Here is that code:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
byte[] byteArray = File.ReadAllBytes(#"C:\Simple_Template.docx");
using (MemoryStream stream = new MemoryStream())
{
stream.Write(byteArray, 0, (int)byteArray.Length);
WordprocessingDocument doc = WordprocessingDocument.Open(stream, true);
doc.MainDocumentPart.DeleteParts<CustomXmlPart>(doc.MainDocumentPart.CustomXmlParts);
string newcustomXML = #"<?xml version=""1.0\"" encoding=""UTF-8\""?><Person><NAMETAG>John Doe</NAMETAG><DOBTAG>DOB</DOBTAG><SCORE1TAG>90.5</SCORE1TAG><SCORE2TAG>100.0</SCORE2TAG></PERSON>";
CustomXmlPart xmlPart = doc.MainDocumentPart.AddCustomXmlPart(CustomXmlPartType.CustomXml);
byte[] byteArrayXML = Encoding.UTF8.GetBytes(newcustomXML);
using (MemoryStream xml_strm = new MemoryStream(byteArrayXML))
{
xmlPart.FeedData(xml_strm);
}
doc.MainDocumentPart.Document.Save();
doc.Close();
File.WriteAllBytes(#"C:\Simple_Template_Replace.docx", stream.ToArray());
}
}
I have also tried to navigate through the structure but I am having a hard time figuring out where in the WordprocessingDocument object contains the actual values that I need to modify. Ideally, I would like something like this psuedo-code:
doc.MainDocumentPart.CustomXMLPart.Select("NAMETAG") = "John Doe"
--------Follow On----------
The answer below worked well without a Namespace. Now I would like to add one. This is the new XML:
<?xml version="1.0"?><myxml xmlns="www.mydomain.com">
<PERSON>
<NAMETAG>NAME</NAMETAG>
<DOBTAG>DOB</DOBTAG>
<SCORE1TAG>SCORE1</SCORE1TAG>
<SCORE2TAG>SCORE2</SCORE2TAG>
</PERSON>
</myxml>
I have adjusted the code to the following but the SelectSingleNode call is returning NULL. Here is the updated code:
XmlNamespaceManager mgr = new XmlNamespaceManager(xmlDocument.NameTable);
mgr.AddNamespace("ns", "www.mydomain.com");
string name_tag = xmlDocument.SelectSingleNode("/ns:myxml/ns:PERSON/ns:NAMETAG", mgr).InnerText;
I was able to fix this myself. I did not realize that you need to include "ns:" with every element. I still thought that I would be able to pass in String.Empty into my AddNamespace and then I would not have to do it. But this will work for now.
The problem is with the newcustomXML value, it has two '\' characters in the XML declaration and also the start tag of "PERSON" element has capital case instead of upper case.
So, try using the following instead:
string newcustomXML = #"<?xml version=""1.0"" encoding=""UTF-8""?>
<PERSON>
<NAMETAG>John Doe</NAMETAG>
<DOBTAG>01/01/2020</DOBTAG>
<SCORE1TAG>90.5</SCORE1TAG>
<SCORE2TAG>100.0</SCORE2TAG>
</PERSON>";
Also regarding your navigation attempt, try using this:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
byte[] byteArray = File.ReadAllBytes(#"C:\Simple_Template.docx");
using (MemoryStream stream = new MemoryStream())
{
stream.Write(byteArray, 0, (int)byteArray.Length);
WordprocessingDocument doc = WordprocessingDocument.Open(stream, true);
CustomXmlPart xmlPart = doc.MainDocumentPart.CustomXmlParts.First();
XmlDocument xmlDocument = new XmlDocument();
using (var inputStream = xmlPart.GetStream(FileMode.Open, FileAccess.Read))
using (var outputStream = new MemoryStream())
{
xmlDocument.Load(inputStream);
xmlDocument.SelectSingleNode("/PERSON/NAMETAG").InnerText = "John Doe";
xmlDocument.Save(outputStream);
outputStream.Seek(0, SeekOrigin.Begin);
xmlPart.FeedData(outputStream);
}
doc.MainDocumentPart.Document.Save();
doc.Close();
File.WriteAllBytes(#"C:\Simple_Template_Replace.docx", stream.ToArray());
}
}
When using the code below I get corrupted xlsx file which can be fixed by removing the residual data from the connections.xml file.
What is causing the issue and how to fix this?
using (SpreadsheetDocument excelDoc = SpreadsheetDocument.Open(file.FullName, true))
{
WorkbookPart workbookpart = excelDoc.WorkbookPart;
ConnectionsPart connPart = workbookpart.ConnectionsPart;
string spreadsheetmlNamespace = #"http://schemas.openxmlformats.org/spreadsheetml/2006/main";
NameTable nt = new NameTable();
XmlNamespaceManager nsManager = new XmlNamespaceManager(nt);
nsManager.AddNamespace("sh", spreadsheetmlNamespace);
XmlDocument xdoc = new XmlDocument(nt);
xdoc.Load(connPart.GetStream());
XmlNode oxmlNode = xdoc.SelectSingleNode("/sh:connections/sh:connection/sh:dbPr/#connection", nsManager);
oxmlNode.Value = oxmlNode.Value.Replace(oxmlNode.Value, "foo");
xdoc.Save(connPart.GetStream());
}
What comes out as connections.xml looks like this:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<connections xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<connection id="1" keepAlive="1" name="LCR" type="5" refreshedVersion="4" background="1" saveData="1">
<dbPr connection="foo" command="test" commandType="1" />
<olapPr sendLocale="1" rowDrillCount="1000" serverFill="0" serverFont="0" serverFontColor="0" />
</connection>
</connections>y Options=2;MDX Missing Member Mode=Error;Disable Prefetch Facts=True" command="test" commandType="1"/><olapPr sendLocale="1" rowDrillCount="1000" serverFill="0" serverFont="0" serverFontColor="0"/></connection></connections>
Please note the residual data at the end. If this is removed, xlsx can be open again.
Fixed this by adding extra line which clears the part before adding data to it.
...
oxmlNode.Value = oxmlNode.Value.Replace(oxmlNode.Value, newConnection);
connPart.FeedData(connPart.GetStream()); //Added
xdoc.Save(connPart.GetStream())
...
From MSDN:
Feed data into the part stream. The stream of the part will be
truncated at first.
Found the answer thanks to SO Related question section. Particularly, OpenXML replace specific customxml part of word document.
After I could create an xml, and adding data and element to it, I want to be able to remove a specific element from it as well. I tried to follow what it said from here Deleting XML element nodes, then I could be able to remove any element from it; however, it does not remove that element completely; therefore, it's producing an error to my xml file.
My sample xml is like this (before removing)
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<!--Favorite's xml-->
<favorites>
<favorite id="1" pro_id="1" pro_name="Boots Expert Anti-Blemish Cleansing Foam" cate_xml="ProductsOily.xml" pro_image="images/Oily-Dry/BO001.JPG" />
<favorite id="2" pro_id="2" pro_name="Clean & Clear Advantage Oil Absorbing Cream Cleanser" cate_xml="ProductsOily.xml" pro_image="images/Oily-Dry/BP251.jpg" />
</favorites>
From example, I tried to remove an element that has pro_id equals 1, but my xml file, after remove, became like this
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<!--Favorite's xml-->
<favorites>
<favorite id="2" pro_id="2" pro_name="Clean & Clear Advantage Oil Absorbing Cream Cleanser" cate_xml="ProductsOily.xml" pro_image="images/Oily-Dry/BP251.jpg" />
</favorites>" pro_name="Clean & Clear Advantage Oil Absorbing Cream Cleanser" cate_xml="ProductsOily
Here is my code to do this:
var storage = IsolatedStorageFile.GetUserStoreForApplication();
fileName = "Favorite\\Favorite.xml";
XDocument docx = null;
using (IsolatedStorageFileStream isoStreamx = new IsolatedStorageFileStream(fileName, FileMode.Open, storage))
{
// isoStreamx.Position = 0;
docx = XDocument.Load(isoStreamx);
isoStreamx.SetLength(docx.ToString().Length);
docx.Root.Elements().Where(x => x.Attribute("pro_id").Value == NavigationContext.QueryString["id"] as string).Remove();
isoStreamx.Position = 0;
docx.Save(isoStreamx);
}
How can I completely remove an element? Please help me, thanks.
You're currently reusing the same stream to save over the top. That will only overwrite data - it won't truncate the file at the end point of your document. What you really want to do is effectively create a new file. Something like:
var storage = IsolatedStorageFile.GetUserStoreForApplication();
fileName = "Favorite\\Favorite.xml";
XDocument docx = null;
using (var isoStreamx = new IsolatedStorageFileStream(fileName, FileMode.Open, storage))
{
docx = XDocument.Load(isoStreamx);
}
var target = (string) NavigationContext.QueryString["id"];
docx.Root
.Elements()
.Where(x => x.Attribute("pro_id").Value == target)
.Remove();
using (var isoStreamx = new IsolatedStorageFileStream(fileName, FileMode.Create, storage))
{
docx.Save(isoStreamx);
}
You could keep your current code, and just call isoStreamx.SetLength(isoStreamx.Position) at the end (removing the current pointless and broken SetLength call) - but I think it's cleaner to use the code above.
What I am doing is trying to change the value of a Microsoft Office Word documents XML and save it as a new file. I know that there are SDK's that I could use to make this easier but the project I am tasked with maintaining is doing things this way and I was told I had to as well.
I have a basic test document with two placeholders mapped to the following XML:
<root>
<element>
Fubar
</element>
<second>
This is the second placeholder
</second>
</root>
In my test project I have the following:
string strRelRoot = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
//the word template
byte[] buffer = File.ReadAllBytes("dev.docx");
MemoryStream stream = new MemoryStream(buffer, true);
Package package = Package.Open(stream, FileMode.Open, FileAccess.ReadWrite);
//get the document relationship
PackageRelationshipCollection pkgrcOfficeDocument = package.GetRelationshipsByType(strRelRoot);
//iterate through relationship
foreach (PackageRelationship pkgr in pkgrcOfficeDocument)
{
if (pkgr.SourceUri.OriginalString == "/")
{
//uri for the custom xml
Uri uriData = new Uri("/customXML/item1.xml", UriKind.Relative);
//delete the existing xml if it exists
if (package.PartExists(uriData))
{
// Delete template "/customXML/item1.xml" part
package.DeletePart(uriData);
}
PackagePart pkgprtData = package.CreatePart(uriData, "application/xml");
//hard coded test data
string xml = #"<root>
<element>
Changed
</element>
<second>
The second placeholder changed
</second>
</root>";
Stream fromStream = pkgprtData.GetStream();
//write the string
fromStream.Write(Encoding.UTF8.GetBytes(xml),0,xml.Length);
//destination file
Stream dest = File.Create("test.docx");
//write to the destination file
for (int a = fromStream.ReadByte(); a != -1; a = fromStream.ReadByte())
{
dest.WriteByte((byte)a);
}
}
}
What is happening right now is the file test.docx is being created but it is a blank document. I'm not sure why this is happening. Any suggestions anyone could offer on this approach and/or what I am doing incorrectly would be very much appreciated. Thanks much!
After your fromStream.Write call, the stream pointer is positioned after the data you've just written. So your first call to fromStream.ReadByte is already at the end of the stream, and you read (and write) nothing.
You need to either Seek to the beginning of the stream after writing (if the stream returned by the package supports seeking), or close fromStream (to ensure the data you've written is flushed) and reopen it for reading.
fromStream.Seek(0L, SeekOrigin.Begin);
I'm pretty sure this is not a duplicate so bear with me for just a minute.
How can I programatically (C#) ZIP a file (in Windows) without using any third party libraries? I need a native windows call or something like that; I really dislike the idea of starting a process, but I will if I absolutely have to. A PInovke call would be much better.
Failing that, let me tell you what I'm really trying to accomplish: I need the ability to let a user download a collection of documents in a single request. Any ideas on how to accomplish this?
How can I programatically (C#) ZIP a file (in Windows) without using
any third party libraries?
If using the 4.5+ Framework, there is now the ZipArchive and ZipFile classes.
using (ZipArchive zip = ZipFile.Open("test.zip", ZipArchiveMode.Create))
{
zip.CreateEntryFromFile(#"c:\something.txt", "data/path/something.txt");
}
You need to add references to:
System.IO.Compression
System.IO.Compression.FileSystem
For .NET Core targeting net46, you need to add dependencies for
System.IO.Compression
System.IO.Compression.ZipFile
Example project.json:
"dependencies": {
"System.IO.Compression": "4.1.0",
"System.IO.Compression.ZipFile": "4.0.1"
},
"frameworks": {
"net46": {}
}
For .NET Core 2.0, just adding a simple using statement is all that is needed:
using System.IO.Compression;
Are you using .NET 3.5? You could use the ZipPackage class and related classes. Its more than just zipping up a file list because it wants a MIME type for each file you add. It might do what you want.
I'm currently using these classes for a similar problem to archive several related files into a single file for download. We use a file extension to associate the download file with our desktop app. One small problem we ran into was that its not possible to just use a third-party tool like 7-zip to create the zip files because the client side code can't open it -- ZipPackage adds a hidden file describing the content type of each component file and cannot open a zip file if that content type file is missing.
private static string CompressFile(string sourceFileName)
{
using (ZipArchive archive = ZipFile.Open(Path.ChangeExtension(sourceFileName, ".zip"), ZipArchiveMode.Create))
{
archive.CreateEntryFromFile(sourceFileName, Path.GetFileName(sourceFileName));
}
return Path.ChangeExtension(sourceFileName, ".zip");
}
I was in the same situation, wanting to .NET instead of a third party library. As another poster mentioned above, simply using the ZipPackage class (introduced in .NET 3.5) is not quite enough. There is an additional file that MUST be included in the archive in order for the ZipPackage to work. If this file is added, then the resulting ZIP package can be opened directly from Windows Explorer - no problem.
All you have to do is add the [Content_Types].xml file to the root of the archive with a "Default" node for every file extension you wish to include. Once added, I could browse the package from Windows Explorer or programmatically decompress and read its contents.
More information on the [Content_Types].xml file can be found here: http://msdn.microsoft.com/en-us/magazine/cc163372.aspx
Here is a sample of the [Content_Types].xml (must be named exactly) file:
<?xml version="1.0" encoding="utf-8" ?>
<Types xmlns=
"http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="xml" ContentType="text/xml" />
<Default Extension="htm" ContentType="text/html" />
<Default Extension="html" ContentType="text/html" />
<Default Extension="rels" ContentType=
"application/vnd.openxmlformats-package.relationships+xml" />
<Default Extension="jpg" ContentType="image/jpeg" />
<Default Extension="png" ContentType="image/png" />
<Default Extension="css" ContentType="text/css" />
</Types>
And the C# for creating a ZIP file:
var zipFilePath = "c:\\myfile.zip";
var tempFolderPath = "c:\\unzipped";
using (Package package = ZipPackage.Open(zipFilePath, FileMode.Open, FileAccess.Read))
{
foreach (PackagePart part in package.GetParts())
{
var target = Path.GetFullPath(Path.Combine(tempFolderPath, part.Uri.OriginalString.TrimStart('/')));
var targetDir = target.Remove(target.LastIndexOf('\\'));
if (!Directory.Exists(targetDir))
Directory.CreateDirectory(targetDir);
using (Stream source = part.GetStream(FileMode.Open, FileAccess.Read))
{
source.CopyTo(File.OpenWrite(target));
}
}
}
Note:
This code uses the Stream.CopyTo method in .NET 4.0
This will become much simpler with the introduction of the ZipArchive class in .NET 4.5: http://msdn.microsoft.com/en-us/library/system.io.compression.ziparchive(v=vs.110).aspx
Based off Simon McKenzie's answer to this question, I'd suggest using a pair of methods like this:
public static void ZipFolder(string sourceFolder, string zipFile)
{
if (!System.IO.Directory.Exists(sourceFolder))
throw new ArgumentException("sourceDirectory");
byte[] zipHeader = new byte[] { 80, 75, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
using (System.IO.FileStream fs = System.IO.File.Create(zipFile))
{
fs.Write(zipHeader, 0, zipHeader.Length);
}
dynamic shellApplication = Activator.CreateInstance(Type.GetTypeFromProgID("Shell.Application"));
dynamic source = shellApplication.NameSpace(sourceFolder);
dynamic destination = shellApplication.NameSpace(zipFile);
destination.CopyHere(source.Items(), 20);
}
public static void UnzipFile(string zipFile, string targetFolder)
{
if (!System.IO.Directory.Exists(targetFolder))
System.IO.Directory.CreateDirectory(targetFolder);
dynamic shellApplication = Activator.CreateInstance(Type.GetTypeFromProgID("Shell.Application"));
dynamic compressedFolderContents = shellApplication.NameSpace(zipFile).Items;
dynamic destinationFolder = shellApplication.NameSpace(targetFolder);
destinationFolder.CopyHere(compressedFolderContents);
}
}
Add these 4 functions to your project:
public const long BUFFER_SIZE = 4096;
public static void AddFileToZip(string zipFilename, string fileToAdd)
{
using (Package zip = global::System.IO.Packaging.Package.Open(zipFilename, FileMode.OpenOrCreate))
{
string destFilename = ".\\" + Path.GetFileName(fileToAdd);
Uri uri = PackUriHelper.CreatePartUri(new Uri(destFilename, UriKind.Relative));
if (zip.PartExists(uri))
{
zip.DeletePart(uri);
}
PackagePart part = zip.CreatePart(uri, "", CompressionOption.Normal);
using (FileStream fileStream = new FileStream(fileToAdd, FileMode.Open, FileAccess.Read))
{
using (Stream dest = part.GetStream())
{
CopyStream(fileStream, dest);
}
}
}
}
public static void CopyStream(global::System.IO.FileStream inputStream, global::System.IO.Stream outputStream)
{
long bufferSize = inputStream.Length < BUFFER_SIZE ? inputStream.Length : BUFFER_SIZE;
byte[] buffer = new byte[bufferSize];
int bytesRead = 0;
long bytesWritten = 0;
while ((bytesRead = inputStream.Read(buffer, 0, buffer.Length)) != 0)
{
outputStream.Write(buffer, 0, bytesRead);
bytesWritten += bytesRead;
}
}
public static void RemoveFileFromZip(string zipFilename, string fileToRemove)
{
using (Package zip = global::System.IO.Packaging.Package.Open(zipFilename, FileMode.OpenOrCreate))
{
string destFilename = ".\\" + fileToRemove;
Uri uri = PackUriHelper.CreatePartUri(new Uri(destFilename, UriKind.Relative));
if (zip.PartExists(uri))
{
zip.DeletePart(uri);
}
}
}
public static void Remove_Content_Types_FromZip(string zipFileName)
{
string contents;
using (ZipFile zipFile = new ZipFile(File.Open(zipFileName, FileMode.Open)))
{
/*
ZipEntry startPartEntry = zipFile.GetEntry("[Content_Types].xml");
using (StreamReader reader = new StreamReader(zipFile.GetInputStream(startPartEntry)))
{
contents = reader.ReadToEnd();
}
XElement contentTypes = XElement.Parse(contents);
XNamespace xs = contentTypes.GetDefaultNamespace();
XElement newDefExt = new XElement(xs + "Default", new XAttribute("Extension", "sab"), new XAttribute("ContentType", #"application/binary; modeler=Acis; version=18.0.2application/binary; modeler=Acis; version=18.0.2"));
contentTypes.Add(newDefExt);
contentTypes.Save("[Content_Types].xml");
zipFile.BeginUpdate();
zipFile.Add("[Content_Types].xml");
zipFile.CommitUpdate();
File.Delete("[Content_Types].xml");
*/
zipFile.BeginUpdate();
try
{
zipFile.Delete("[Content_Types].xml");
zipFile.CommitUpdate();
}
catch{}
}
}
And use them like this:
foreach (string f in UnitZipList)
{
AddFileToZip(zipFile, f);
System.IO.File.Delete(f);
}
Remove_Content_Types_FromZip(zipFile);
There is now an officially documented section in the dotnet docs on how to compress and decompress without third-party libraries. It includes full code samples too!
https://learn.microsoft.com/en-us/dotnet/standard/io/how-to-compress-and-extract-files
Looks like Windows might just let you do this...
Unfortunately I don't think you're going to get around starting a separate process unless you go to a third party component.