Saturday, July 12, 2008

Saving a WPF Xaml FlowDocument to XPS with working DataBinding

I needed to create an invoice system, where the look and feel of the invoice could differ and also of course the invoice data would be different from invoice to invoice. After talking to a collega, whom pointed me into the direction of WPF Xaml FlowDocuments for templating the invoices, I started playing with them. So I created a template FlowDocument on which the actual invoice data would be placed and then saved as an XPS document for later use. But when saving the FlowDocument to an XPS file the databinding did not get triggered, it was basically just saving the template FlowDocument. I have read many examples on how to use databinding in a FlowDocument, but these where all when eventually printing the document to a printer, than everything works like a charm. Then you can use; Xaml inline recources, references to an external xml file or bind it with an object. So I continued my search and finally found the Microsoft Example (http://msdn.microsoft.com/en-us/library/ms771375.aspx) where they where able to save my FlowDocuments to an XPS file and keep databinding working properly. So I went through the code to see how it was done, wrote this in a library and will go through it here in this post.

First below here is the complete library that you will need to save a Xaml FlowDocument to XPS.

Class: XamlToXps
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Windows.Threading;
using System.Windows.Documents;
using System.Windows.Documents.Serialization;
using System.Windows.Markup;

namespace Fohjin.XamlLibrary
{
    /// <summary>
    /// Class that will take care of converting a Xaml WPF FlowDocument to a XPS document
    /// </summary>
    public class XamlToXps
    {
        /// <summary>
        /// Holds an internal copy of the Loaded Xaml
        /// </summary>
        private IDocumentPaginatorSource _FlowDocument;
        /// <summary>
        /// This Dictionary is used to perform string replacements in the source Xaml, 
        /// I use this f.ex. to replace an external default reference to an actual XML 
        /// file location
        /// </summary>
        private Dictionary<string, string> _StringReplacement;

        /// <summary>
        /// Initializes a new instance of the <see cref="XamlToXps"/> class.
        /// </summary>
        public XamlToXps()
        {
            _StringReplacement = new Dictionary<string, string>();
        }

        /// <summary>
        /// Gets or sets the string replacement dictionary later to be used for string 
        /// replacements in the Xaml source.
        /// </summary>
        /// <value>The string replacement dictionary.</value>
        public Dictionary<string, string> StringReplacement
        {
            set
            {
                _StringReplacement = value;
            }
            get
            {
                return _StringReplacement;
            }
        }

        /// <summary>
        /// Loads the source Xaml using a file location.
        /// At this stage there will be automatic string replacement.
        /// </summary>
        /// <param name="xamlFileName">Name of the xaml file.</param>
        public void LoadXaml(string xamlFileName)
        {
            using (FileStream inputStream = File.OpenRead(xamlFileName))
            {
                LoadXaml(inputStream);
            }
        }
        /// <summary>
        /// Loads the source Xaml using a Stream.
        /// At this stage there will be automatic string replacement.
        /// </summary>
        /// <param name="xamlFileStream">The xaml file stream.</param>
        public void LoadXaml(Stream xamlFileStream)
        {
            ParserContext pc = new ParserContext
            {
               BaseUri = new Uri(Environment.CurrentDirectory + "/")
            };
            object newDocument = XamlReader.Load(ReplaceStringsInXaml(xamlFileStream), pc);

            if (newDocument == null)
                throw new Exception("Invalid Xaml, could not be parsed");

            if (newDocument is IDocumentPaginatorSource)
                LoadXaml(newDocument as IDocumentPaginatorSource);
        }
        /// <summary>
        /// Loads the source Xaml in the form of a complete FlowDocument. 
        /// At this stage there is no automatic string replacement.
        /// </summary>
        /// <param name="flowDocument">The flow document.</param>
        public void LoadXaml(IDocumentPaginatorSource flowDocument)
        {
            _FlowDocument = flowDocument;
            FlushDispatcher();
        }

        /// <summary>
        /// Saves the prepared FlowDocument to a XPS file format.
        /// In this library there is only the XPS serializer, but in the Microsoft 
        /// example there are several, I still have to investigate how to get the others.
        /// </summary>
        /// <param name="fileName">Name of the file.</param>
        public void Save(string fileName)
        {
            DeleteOldFile(fileName);
            FlowDocument flowDocument = _FlowDocument as FlowDocument;

            SerializerProvider serializerProvider = new SerializerProvider();
            SerializerDescriptor selectedPlugIn = null;
            foreach (SerializerDescriptor serializerDescriptor in serializerProvider.InstalledSerializers)
            {
                if (!serializerDescriptor.IsLoadable || !fileName.EndsWith(serializerDescriptor.DefaultFileExtension))
                    continue;
                selectedPlugIn = serializerDescriptor;
                break;
            }

            if (selectedPlugIn != null)
            {
                using (Stream package = File.Create(fileName))
                {
                    SerializerWriter serializerWriter = serializerProvider.CreateSerializerWriter(selectedPlugIn, package);
                    IDocumentPaginatorSource idoc = flowDocument;
                    if (idoc != null)
                        serializerWriter.Write(idoc.DocumentPaginator, null);
                    package.Close();
                }
            }
            else
                throw new Exception("No Serializer found for your requested output format");
        }

        /// <summary>
        /// Deletes the old output file if it exists.
        /// </summary>
        /// <param name="fileName">Name of the file.</param>
        private static void DeleteOldFile(string fileName)
        {
            if (File.Exists(fileName))
            {
                File.Delete(fileName);
            }
        }

        /// <summary>
        /// Replaces the Key Value Pairs in the Xaml source, this is an other way of data 
        /// binding, but I prefere the build-in way.
        /// </summary>
        /// <param name="xamlFileStream">The xaml file stream.</param>
        /// <returns></returns>
        private Stream ReplaceStringsInXaml(Stream xamlFileStream)
        {
            string rawXamlText;
            xamlFileStream.Seek(0, SeekOrigin.Begin);
            using (StreamReader streamReader = new StreamReader(xamlFileStream))
            {
                rawXamlText = streamReader.ReadToEnd();
            }
            foreach (KeyValuePair<string, string> keyValuePair in _StringReplacement)
            {
                rawXamlText = rawXamlText.Replace(keyValuePair.Key, keyValuePair.Value);
            }
            return new MemoryStream(new UTF8Encoding().GetBytes(rawXamlText));
        }

        /// <summary>
        /// Have to figure out what this does and why it works, but not including 
        /// this and calling it wil actually fail the databinding process
        /// </summary>
        private static void FlushDispatcher()
        {
            FlushDispatcher(Dispatcher.CurrentDispatcher);
        }

        /// <summary>
        /// Have to figure out what this does and why it works, but not including 
        /// this and calling it wil actually fail the databinding process
        /// </summary>
        /// <param name="ctx"></param>
        private static void FlushDispatcher(Dispatcher ctx)
        {
            FlushDispatcher(ctx, DispatcherPriority.SystemIdle);
        }

        /// <summary>
        /// Have to figure out what this does and why it works, but not including 
        /// this and calling it wil actually fail the databinding process
        /// </summary>
        /// <param name="ctx"></param>
        /// <param name="priority"></param>
        private static void FlushDispatcher(Dispatcher ctx, DispatcherPriority priority)
        {
            ctx.Invoke(priority, new DispatcherOperationCallback(delegate { return null; }), null);
        }
    }
}

Well go through it, I’ll tell you what I found out when playing with the code and trying to get it to work:
  • You need to call the FlushDispatcher() method because else it won’t trigger the databinding process. This is something I have to look into why this is, but during my tests I definitely needed this. It is doing something with threading.
  • And the other thing is if you try to load a Xaml FlowDocument document using XamlReader.Load() method you will need to use the Stream version, and include a ParseContext object with a BaseUrl parameter. If you try to load the document straight from a file location it will not trigger the databinding process.
Now how to use this little library:
Usage
1
2
3
4
XamlToXps _XamlToXps = new XamlToXps();
_XamlToXps.StringReplacement.Add("[replace_with_content_location]", System.IO.Path.Combine(Environment.CurrentDirectory, "dataSource.xml"));
_XamlToXps.LoadXaml(System.IO.Path.Combine(Environment.CurrentDirectory, "FlowDocument.xaml"));
_XamlToXps.Save(System.IO.Path.Combine(Environment.CurrentDirectory, "dataSource.xps"));

I'll have to do some testing to see if this is working with data binding objects, but the other two mentioned options do work. I hope you found these examples helpful. Also take a look at the following links:

http://msdn.microsoft.com/en-us/library/ms771375.aspx
http://msdn.microsoft.com/en-us/library/aa970268.aspx
http://weblogs.asp.net/scottgu/archive/2007/02/22/wpf-text-reading-and-flow-document-support-and-the-new-nytimes-daily-mail-and-seattle-post-intelligencer-reader-applications.aspx
http://roecode.wordpress.com/category/xps/
http://blogs.msdn.com/fyuan/archive/2007/03/10/convert-xaml-flow-document-to-xps-with-style-multiple-page-page-size-header-margin.aspx