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.
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.
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