AveryPierce
AveryPierce

Reputation: 68

XML, LINQ and XDocument.Save problems

I'm having trouble saving xml files after making changes to them. I've spent all day today trying to figure this out and I've gotten nowhere.

I have this xml doc:

<?xml version=1.0" encoding="utf-8"?>
<content>
      <weapon id="1234" name="blahblah">
         <note info="blah blah" />
      </weapon>
      <weapon id="5678" name="blahblah2">
         <note info="blah blah" />
      </weapon>
</content>

This is what I've come up with so far that doesn't exactly work (Edited to show how I read file):

FileOpenPicker openPicker = new FileOpenPicker();
openPicker.SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.DocumentsLibrary;
openPicker.FileTypeFilter.Add(".xml");

StorageFile gfile = await openPicker.PickSingleFileAsync()

fileContent = await FileIO.ReadTextAsync(gfile, Windows.Storage.Streams.UnicodeEncoding.Utf8);

Xdocument  xml = XDocument.Parse(fileContent);

xml.Descendants("weapon").Where(c => c.Attribute("id").Value.Equals("5678")).FirstorDefault().Remove();

IRandomAccessStream writeStream = await gfile.OpenAsync(FileAccessMode.ReadWrite);
Stream stream = writeStream.AsStreamForWrite();

xml.Save(stream);

The resulting xml doc would be something like this:

<?xml version=1.0" encoding="utf-8"?>
<content>
      <weapon id="1234" name="blahblah">
         <note info="blah blah" />
</content>apon>
      <weapon id="5678" name="blahblah2">
         <note info="blah blah" />
      </weapon>
</content>

If I try to use FileAccessMode.ReadWriteNoCopyOnWrite for OpenAsync the file ends up being 0 bytes.

Anybody know how I can write this file correctly while still using XDocument.Save?

Upvotes: 3

Views: 8882

Answers (5)

Slate
Slate

Reputation: 3704

I'm writing to a FileStream, which flushes to disk.

public static async Task SaveXMLAsync(XDocument linqXml, StorageFolder localFolder, string filename)
{
  // Create a storage file
  StorageFile file = await localFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);

  // Write the XML to a File Stream.
  using (FileStream sourceStream = new FileStream(file.Path, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
  {
    linqXml.Save(sourceStream);

    // Async flush to disk.
    await sourceStream.FlushAsync();
  };
}

Upvotes: 0

Lyon
Lyon

Reputation: 606

Why not just use System.IO.File.WriteAllText?

XDocument xml = XDocument.Load(xmlFilePath);

System.IO.File.WriteAllText(xmlFilePath, string.Format(@"<?xml version=""1.0""?>{0}{1}", Environment.NewLine, xml));

Upvotes: 1

hmadrigal
hmadrigal

Reputation: 1050

Hi today I have to write an XML file, and basically it worked well if I XDocument.Save works well if I provide a valid stream.

WinRT can be tricky because it has limited access to the file system.

/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{

    public List<DataStructure> DataList { get; set; }

    public MainPage()
    {
        this.InitializeComponent();
        DataList = Enumerable.Range(0, 25).Select(i => new DataStructure() { Id = i, Data = string.Format("Number : {0}", i) }).ToList();
        this.Loaded += MainPage_Loaded;
    }

    async void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        this.Loaded -= MainPage_Loaded;
        //var xmlDocument =
        //    new XDocument(
        //        new XElement("DataList",
        //            DataList.Select(dataItem =>
        //                new XElement("DataItem",
        //                    new XAttribute("id", dataItem.Id), new XAttribute("data", dataItem.Data)))));

        var rootNode = new XElement("DataList");
        var xmlDocument = new XDocument(rootNode);
        foreach (var dataItem in DataList)
        {
            rootNode.Add(new XElement("DataItem",
                            new XAttribute("id", dataItem.Id), new XAttribute("data", dataItem.Data)));
        }

        FileSavePicker savePicker = new FileSavePicker();
        savePicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
        // Dropdown of file types the user can save the file as
        savePicker.FileTypeChoices.Add("XML Document", new List<string>() { ".xml" });
        // Default file name if the user does not type one in or select a file to replace
        savePicker.SuggestedFileName = "New Xml Document";

        StorageFile file = await savePicker.PickSaveFileAsync();
        // Process picked file
        if (file != null)
        {
            // Store file for future access
            var fileToken = Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.Add(file);
            var writterStream = await file.OpenStreamForWriteAsync();
            xmlDocument.Save(writterStream);
        }

    }

I create a WinRT default project on VS2012 (RC). Then I modified the MainPage.cs file to make it look like the previous one. The file contains a hardcoded list of DataStructures, the we create a XDocument based on the List. Finally we ask the user to provide a writable stream to save our XDocument. (We can also get a writable stream from Isolated Storage). Finally we just call write method using the proper Stream.

Upvotes: 0

Kris Vandermotten
Kris Vandermotten

Reputation: 10201

It turns out that this problem is more complex than it may seem at first sight.

The problems we need to solve include

  • write to the file asynchronously
  • write efficiently, i.e. with buffered IO
  • overwrite the entire file, trimming the existing file if necessary
  • make our write operation awaitable

After a lot of experimentation, the solution that I chose is to write the XML to a System.IO.MemoryStream, and then copy that memory stream to the storagefile. I do understand that this requires memory for a temporary copy of the data. But it works, is fast (buffered IO, small number of native Windows calls, only three awaits), trims correctly, and awaiting the operation actually works correctly. Note that this was not the case with some other methods I tried. Here's the solution:

MemoryStream ms = new MemoryStream()

xml.Save(ms, SaveOptions.DisableFormatting);

await ms.CopyToAsync(gfile);

The CopyToAsync extention method:

using System.IO;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Storage;
using Windows.Storage.Streams;

internal static class Extensions
{
    public static async Task CopyToAsync(this MemoryStream source, StorageFile file)
    {
        using (IRandomAccessStream raStream = await file.OpenAsync(FileAccessMode.ReadWrite))
        {
            using (IOutputStream stream = raStream.GetOutputStreamAt(0))
            {
                await stream.WriteAsync(source.GetWindowsRuntimeBuffer());
                await stream.FlushAsync();
            }

            raStream.Size = raStream.Position;
        }
    }
}

Upvotes: 1

JP Alioto
JP Alioto

Reputation: 45127

I got it to work, but I need to do more research on the behavior of your code. Try this ...

var local = Windows.Storage.ApplicationData.Current.LocalFolder;
var file = await local.GetFileAsync("test.xml");
var data = await FileIO.ReadTextAsync(file);
var xml = XDocument.Parse(data);

xml.Descendants("weapon").Where(c => c.Attribute("id").Value.Equals("5678")).FirstOrDefault().Remove();

file = await local.CreateFileAsync("test.xml", CreationCollisionOption.ReplaceExisting);
var writeStream = await file.OpenStreamForWriteAsync() as Stream;

xml.Save(writeStream);

writeStream.Flush();

Where test.xml is a file in your local folder with your original XML.

Upvotes: 0

Related Questions