Question
- How do you print a FlowDocument that have
BlockUIContainer
?
- How can I force a Measure/Update/Arrange on a FlowDocument?
Background
I have a generated FlowDocument
with paragraphs of text with a few Rectangle
elements filled DrawingBrushes
from a resource dictionary and BlockUIContainer
with custom controls.
The document renders correctly when viewed in any of the FlowDocument* controls HOWEVER when the document is converted to a FixedDocument/XpsDocument, none of the Rectangle
or BlockUIContainer
elements render.
I'm almost certain it is because the control has not been measured/arranged, however cannot figure out how to force that to happen before it is converted to the XpsDocument.
I have walked the LogicalTree
recursively and done the following,
UIElement element = (UIElement)d;
element.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
element.Arrange(new Rect(element.DesiredSize));
element.UpdateLayout();
where d
is a DependencyObject
. I can see that this sets the ActualWidth
and ActualHeight
properties when break-pointed in the debugger.
I have tried forcing the Dispatcher
to render as suggested by Will ♦.
Code used to print the XpsDocument
public class XpsDocumentConverter
{
public static XpsDocumentReference CreateXpsDocument(FlowDocument document)
{
// Need to clone the document so that the paginator can work
FlowDocument clonedDocument = DocumentHelper.Clone<FlowDocument>(document);
Uri uri = new Uri(String.Format("pack://temp_{0}.xps/", Guid.NewGuid().ToString("N")));
MemoryStream ms = new MemoryStream();
Package pkg = Package.Open(ms, FileMode.Create, FileAccess.ReadWrite);
PackageStore.AddPackage(uri, pkg);
XpsDocument xpsDocument = new XpsDocument(pkg, CompressionOption.Normal, uri.AbsoluteUri);
XpsSerializationManager rsm = new XpsSerializationManager(new XpsPackagingPolicy(xpsDocument), false);
DocumentPaginator paginator = new FixedDocumentPaginator(clonedDocument, A4PageDefinition.Default);
rsm.SaveAsXaml(paginator);
return new XpsDocumentReference(ms, xpsDocument);
}
}
As you can see I'm also using a custom DocumentPaginator
named 'FixedDocumentPaginator'; however I will not post that code as I doubt the issue is there as by the time it starts paginating the document in GetPage(int pageNumber)
everything has already been converted a Visual
and it is too late for layout.
Edit
Hmm. As I typed this, a thought just occurred to me that the cloned document may not have had a Measure/Arrange/UpdateLayout
done.
Question: How can I force a Measure/Update/Arrange on a FlowDocument?
A possible hack that I could work would be to show the cloned document in one of the FlowDocumentViewers (perhaps off-screen).
Another possible solution that I just learnt about and haven't tried would be to call: ContextLayoutManager.From(Dispatcher.CurrentDispatcher).UpdateLayout();
ContextLayoutManager
walks the logical tree for you and updates the layout.
Code used for cloning the document
public static FlowDocument Clone(FlowDocument originalDocument)
{
FlowDocument clonedDocument = new FlowDocument();
TextRange sourceDocument = new TextRange(originalDocument.ContentStart, originalDocument.ContentEnd);
TextRange clonedDocumentRange = new TextRange(clonedDocument.ContentStart, clonedDocument.ContentEnd);
try
{
using (MemoryStream ms = new MemoryStream())
{
sourceDocument.Save(ms, DataFormats.XamlPackage);
clonedDocumentRange.Load(ms, DataFormats.XamlPackage);
}
clonedDocument.ColumnWidth = originalDocument.ColumnWidth;
clonedDocument.PageWidth = originalDocument.PageWidth;
clonedDocument.PageHeight = originalDocument.PageHeight;
clonedDocument.PagePadding = originalDocument.PagePadding;
clonedDocument.LineStackingStrategy = clonedDocument.LineStackingStrategy;
return clonedDocument;
}
catch (Exception)
{
}
return null;
}
Posting this as future reference for others that are having similar rendering issues with FlowDocument/FixedDocument/XpsDocument.
A few things to note:
BlockUIContainers
are not cloned when you use the above method. This was not immediately obvious until I printed the logical tree out the debug window using some helper methods (these methods are posted below - they are incredibly useful).
- You need to display the document in a viewer and briefly show it on screen. Below is the helper method that I wrote to do this for me.
ForceRenderFlowDocument
private static string ForceRenderFlowDocumentXaml =
@"<Window xmlns=""http://schemas.microsoft.com/netfx/2007/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">
<FlowDocumentScrollViewer Name=""viewer""/>
</Window>";
public static void ForceRenderFlowDocument(FlowDocument document)
{
using (var reader = new XmlTextReader(new StringReader(ForceRenderFlowDocumentXaml)))
{
Window window = XamlReader.Load(reader) as Window;
FlowDocumentScrollViewer viewer = LogicalTreeHelper.FindLogicalNode(window, "viewer") as FlowDocumentScrollViewer;
viewer.Document = document;
// Show the window way off-screen
window.WindowStartupLocation = WindowStartupLocation.Manual;
window.Top = Int32.MaxValue;
window.Left = Int32.MaxValue;
window.ShowInTaskbar = false;
window.Show();
// Ensure that dispatcher has done the layout and render passes
Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Loaded, new Action(() => {}));
viewer.Document = null;
window.Close();
}
}
Edit: I just added window.ShowInTaskbar = false
to the method as if you were quick you could see the window appear in the taskbar.
The user will never "see" the window as it is positioned way off-screen at Int32.MaxValue
- a trick that was common back in the day with early multimedia authoring (e.g. Macromedia/Adobe Director).
For people searching and finding this question, I can tell you that there is no other way to force the document to render.
Visual and Logical Tree Helpers
public static string WriteVisualTree(DependencyObject parent)
{
if (parent == null)
return "No Visual Tree Available. DependencyObject is null.";
using (var stringWriter = new StringWriter())
using (var indentedTextWriter = new IndentedTextWriter(stringWriter, " "))
{
WriteVisualTreeRecursive(indentedTextWriter, parent, 0);
return stringWriter.ToString();
}
}
private static void WriteVisualTreeRecursive(IndentedTextWriter writer, DependencyObject parent, int indentLevel)
{
if (parent == null)
return;
int childCount = VisualTreeHelper.GetChildrenCount(parent);
string typeName = parent.GetType().Name;
string objName = parent.GetValue(FrameworkElement.NameProperty) as string;
writer.Indent = indentLevel;
writer.WriteLine(String.Format("[{0:000}] {1} ({2}) {3}", indentLevel,
String.IsNullOrEmpty(objName) ? typeName : objName,
typeName, childCount)
);
for (int childIndex = 0; childIndex < childCount; ++childIndex)
WriteVisualTreeRecursive(writer, VisualTreeHelper.GetChild(parent, childIndex), indentLevel + 1);
}
public static string WriteLogicalTree(DependencyObject parent)
{
if (parent == null)
return "No Logical Tree Available. DependencyObject is null.";
using (var stringWriter = new StringWriter())
using (var indentedTextWriter = new IndentedTextWriter(stringWriter, " "))
{
WriteLogicalTreeRecursive(indentedTextWriter, parent, 0);
return stringWriter.ToString();
}
}
private static void WriteLogicalTreeRecursive(IndentedTextWriter writer, DependencyObject parent, int indentLevel)
{
if (parent == null)
return;
var children = LogicalTreeHelper.GetChildren(parent).OfType<DependencyObject>();
int childCount = children.Count();
string typeName = parent.GetType().Name;
string objName = parent.GetValue(FrameworkElement.NameProperty) as string;
double actualWidth = (parent.GetValue(FrameworkElement.ActualWidthProperty) as double?).GetValueOrDefault();
double actualHeight = (parent.GetValue(FrameworkElement.ActualHeightProperty) as double?).GetValueOrDefault();
writer.Indent = indentLevel;
writer.WriteLine(String.Format("[{0:000}] {1} ({2}) {3}", indentLevel,
String.IsNullOrEmpty(objName) ? typeName : objName,
typeName,
childCount)
);
foreach (object child in LogicalTreeHelper.GetChildren(parent))
{
if (child is DependencyObject)
WriteLogicalTreeRecursive(writer, (DependencyObject)child, indentLevel + 1);
}
}
Usage
#if DEBUG
Debug.WriteLine("--- Start -------");
Debug.WriteLine(VisualAndLogicalTreeHelper.WriteLogicalTree(document));
Debug.WriteLine("--- End -------");
#endif
I found this solution here, and it helped me get the printing of the FlowDocment without having to render it off screen...So I hope it can help you!!
String copyString = XamlWriter.Save(flowDocViewer.Document);
FlowDocument copy = XamlReader.Parse(copyString) as FlowDocument;