XSLT in a loop keep getting only first file transformation in the output. How to Stop caching XSLT output - c#

When I am looping thru list of files and doing transformation on each file in a folder the effect of the transformation is as if it keeps transforming first file again and again. The transformation looked to be caching.
Here is the Code for transformation:
I am doing this inside WebAPI call.. in .NET framework 4.5.2 version . The ASP.NET page is calling the WebAPI.
I tried all different overloads and moving instancing transformation new out of loop on the top. I did not find any easy way of disabling the caching.
var d = new DirectoryInfo(strXMLFolder);
foreach (FileInfo file in d.GetFiles())
{
String strXSLTFile = strParamArr[2];
String xmlFile = file.Name;
//File.WriteAllBytes(strFile, transferObj.XSLTTemplateFileData);
XslCompiledTransform proc = new XslCompiledTransform();
proc.Load(strXSLTFolder + strXSLTFile);
proc.Transform(strXMLFolder + xmlFile, strXMLFolder + xmlFile + "-IntermediateXML.xml");
..................................
The XSLT file has following Code:
<data>
<xsl:for-each select="//QuantitativeResponseAssay/Setup/Elements/*/Name">
<row>
<PLA_file_name>
<xsl:value-of select="$vFileName"/>
</PLA_file_name>
<Analyst>
<xsl:value-of select="$vAnalyst"/>
</Analyst>
<xsl:choose>
<xsl:when test="position() = 1">
<Sample>Reference</Sample>
</xsl:when>
<xsl:when test="position() = 2">
<Sample>Sample 1</Sample>
</xsl:when>
<xsl:when test="position() = 3">
<Sample>Sample 2</Sample>
</xsl:when>
<xsl:when test="position() = 4">
<Sample>QC</Sample>
</xsl:when>
<xsl:otherwise>
<Sample>Error</Sample>
</xsl:otherwise>
</xsl:choose>
<Sample_Name>
<xsl:value-of select="."/>
</Sample_Name>
<Upper_Asymptote>
<xsl:value-of select="//StatisticTestResults/StatisticTestResult[TestName='AdditionalTestParameterEstimateUpperAsymptote'][./InvolvedAssayElements[Name=current()]]/Value"/>
</Upper_Asymptote>
<Dynamic_Range>
<xsl:value-of select="//StatisticTestResult[TestName='AdditionalTestParameterEstimateDynamicRange'][./InvolvedAssayElements[Name=current()]]/Value" />
</Dynamic_Range>
<Slope_Factor>
<xsl:value-of select="//StatisticTestResult[TestName='AdditionalTestParameterEstimateSlope'][./InvolvedAssayElements[Name=current()]]/Value"/>
</Slope_Factor>
<Regression_pvalue>
<xsl:if test="current() != 'Reference Standard'">
<xsl:value-of select="//QuantitativeResponseAssay/AssayResults/AssayResult/StatisticTestResults/StatisticTestResult[TestName='FTestRegression'][./InvolvedAssayElements[Name=current()]]/PValue" />
</xsl:if>
</Regression_pvalue>
<nonparallelism_pvalue>
<xsl:if test="current() != 'Reference Standard'">
<xsl:value-of select="//QuantitativeResponseAssay/AssayResults/AssayResult/StatisticTestResults/StatisticTestResult[TestName='FTestNonParallelism'][./InvolvedAssayElements[Name=current()]]/PValue" />
</xsl:if>
</nonparallelism_pvalue>
<LOF_pvalue>
<xsl:if test="current() != 'Reference Standard'">
<xsl:value-of select="//QuantitativeResponseAssay/AssayResults/AssayResult/StatisticTestResults/StatisticTestResult[TestName='FTestNonLinearity'][./InvolvedAssayElements[Name=current()]]/PValue" />
</xsl:if>
</LOF_pvalue>
<Potency>
<xsl:if test="string-length(//QuantitativeResponseAssay/AssayResults/AssayResult/PotencyResults/PotencyResult[TestControlSampleName=current()]/RelativePotencyValue) > 1">
<xsl:value-of select="//QuantitativeResponseAssay/AssayResults/AssayResult/PotencyResults/PotencyResult[TestControlSampleName=current()]/RelativePotencyValue * 100 " />
</xsl:if>
</Potency>
<!-- Column L Width_CI v -->
<Width_CI>
<xsl:if test="string-length(//QuantitativeResponseAssay/AssayResults/AssayResult/PotencyResults/PotencyResult[TestControlSampleName=current()]/PercentualRelativePotencyRange) > 1">
<xsl:value-of select="//QuantitativeResponseAssay/AssayResults/AssayResult/PotencyResults/PotencyResult[TestControlSampleName=current()]/PercentualRelativePotencyRange * 100 " />
</xsl:if>
</Width_CI>
<!-- Column M A_unrestricted v -->
<A_unrestricted>
<xsl:value-of select="//FullModel/FitResult/ParameterEstimate[ParameterName='A'][AssayElementName=current()]/Value" />
</A_unrestricted>
<!-- Column N B_unrestricted v -->
<B_unrestricted>
<xsl:value-of select="//FullModel/FitResult/ParameterEstimate[ParameterName='B'][AssayElementName=current()]/Value" />
</B_unrestricted>
<!-- Column O C_log2_unrestricted v -->
<C_unrestricted>
<xsl:value-of select="//FullModel/FitResult/ParameterEstimate[ParameterName='C'][AssayElementName=current()]/Value"/>
</C_unrestricted>
<!-- Column P D_unrestricted v -->
<D_unrestricted>
<xsl:value-of select="//FullModel/FitResult/ParameterEstimate[ParameterName='D'][AssayElementName=current()]/Value" />
</D_unrestricted>
<!-- Column Q G_unrestricted v -->
<G_unrestricted>
<xsl:value-of select="//FullModel/FitResult/ParameterEstimate[ParameterName='G'][AssayElementName=current()]/Value" />
</G_unrestricted>
<Template_ID>
<xsl:value-of select="$vTemplate"/>
</Template_ID>
<Template_Revision>
<xsl:value-of select="$vTemplateRevision"/>
</Template_Revision>
</row>
</xsl:for-each>
</data>
I expect different output in each intermediate.XML file but I keep getting the fist file result in the output in intermediate file.

Related

Renaming XSLT attribute value from C# code

I have XSLT as below :-
<ServiceRequest ExternalSystemName="ServiceNow" Company="{ServiceRequest-Company}">
<xsl:if test="{ServiceRequest-LastResolvedDate} and {ServiceRequest-LastResolvedDate} != ''" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:attribute name="LastResolvedDate">
<xsl:value-of select="{ServiceRequest-LastResolvedDate}" />
</xsl:attribute>
</xsl:if>
<xsl:if test="{ServiceRequest-ServiceType} and {ServiceRequest-ServiceType} != ''" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:attribute name="ServiceType">
<xsl:value-of select="'SR'" />
</xsl:attribute>
</xsl:if>
...
...
I want to rename particular attribute name via C# code.
For that I had written below code :-
var property = root.Descendants(elementName).Descendants().Attributes("name").Where(x => x.Value == "LastResolvedDate");
foreach (var item in property)
{
item.Name = "renamed_new_name";
}
This code gives me error that atribute name cannot be assigned and is readonly.
What can be the possible solution?
EDIT 1 :
It is changing attribute name :-
<xsl:if test="LastResolvedOn/value and LastResolvedOn/value != ''">
<xsl:attribute renamed_new_name="LastResolvedOn">
<xsl:value-of select="LastResolvedOn/value" />
</xsl:attribute>
</xsl:if>
Where I needed :-
<xsl:if test="LastResolvedOn/value and LastResolvedOn/value != ''">
<xsl:attribute name="renamed_new_name">
<xsl:value-of select="LastResolvedOn/value" />
</xsl:attribute>
</xsl:if>
You need to remove the existing attribute and add a new one, e.g.:
var elements = root.Descendants(elementName).Descendants()
.Where(x => (string)x.Attribute("name") == "LastResolvedDate");
foreach (var item in elements)
{
item.Attribute("name").Remove();
item.Add(new XAttribute("renamed_new_name", "LastResolvedDate"));
}

At least one of minimum, optimum, or maximum IPD must be specified on table

I'm getting this error (Title) when trying to execute a Render using Fo.Net when creating a PDF.
[MethodImpl(MethodImplOptions.Synchronized)]
public static void MakePdf(XmlDocument xslFoDocument, Stream outputStream)
{
FonetDriver driver = PdfPrinterDriver.InitFonetDriver();
driver.Render(xslFoDocument, outputStream);
}
I found another post on this subject (for Java rather than for C# as I'm using but i assume it is the same error with the same cause): At least one of minimum, optimum, or maximum IPD must be specified on table - XSL-FO Apache FOP. The problem is that I can't find anywhere in the xsl file below where size hasn't been set. Anyone know what might be wrong? Below is the full XSL stylesheet file
EDIT: After further Troubleshooting I managed to localize the table causing the exception to be thrown. I still can't see where I've missed declaring the size though.
<fo:table-column column-width="7cm"/>
<xsl:for-each select="$units">
<xsl:variable name="bgcolor">
<xsl:choose>
<xsl:when test="position() mod 2 = 0">white</xsl:when>
<xsl:otherwise>#F4F2F0</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<fo:table-column background-color="{$bgcolor}"/>
</xsl:for-each>
<fo:table-body>
<fo:table-row>
<fo:table-cell>
<fo:block>
</fo:block>
</fo:table-cell>
<xsl:for-each select="$units">
<fo:table-cell text-align="center">
<xsl:choose>
<xsl:when test="not(contains($hiddenGroups, 'image'))">
<fo:block margin-top="3mm" margin-left="4mm" margin-right="4mm" margin-bottom="-0.8mm">
<fo:external-graphic content-width="500mm" width="100%" src="{$apiurl}ImageFiles/{Attributes/Attribute[#id='Image']/FileInfo/#id}/Data?apikey={$apikey}"/>
</fo:block>
<fo:block background-color="white" font-weight="600" padding-top="2mm" padding-bottom="2mm" margin-left="4mm" margin-right="4mm" margin-bottom="3mm">
<xsl:value-of select="#name"/>
</fo:block>
</xsl:when>
<xsl:otherwise>
<fo:block background-color="white" font-weight="600" padding-top="2mm" padding-bottom="2mm" margin-top="3mm" margin-left="4mm" margin-right="4mm" margin-bottom="3mm">
<xsl:value-of select="#name"/>
</fo:block>
</xsl:otherwise>
</xsl:choose>
</fo:table-cell>
</xsl:for-each>
</fo:table-row>
<xsl:for-each select="$general_attributes">
<xsl:variable name="attribute_id" select="#id"/>
<xsl:variable name="attribute_type" select="#attributeDefinitionType"/>
<xsl:if test="not(contains(#id, 'HIDE_COMP'))">
<fo:table-row keep-with-previous="always" border-bottom-style="solid" border-bottom-width="thin" border-bottom-color="#D0D0D0">
<fo:table-cell display-align="center" padding-top="2mm" padding-bottom="2mm">
<fo:block margin-left="2mm" margin-right="2mm">
<xsl:value-of select="#name"/>:
</fo:block>
</fo:table-cell>
<xsl:for-each select="$units">
<fo:table-cell padding-top="2mm" padding-bottom="2mm" padding-left="4mm" padding-right="4mm" display-align="center" border-left-style="solid" border-right-style="solid" border-width="0.1mm" border-color="#D0D0D0">
<xsl:call-template name="show-attribute">
<xsl:with-param name="type" select="$attribute_type"/>
<xsl:with-param name="attribute" select="Attributes/Attribute[#id=$attribute_id]"/>
<xsl:with-param name="count" select="count($units)"/>
</xsl:call-template>
</fo:table-cell>
</xsl:for-each>
</fo:table-row>
</xsl:if>
</xsl:for-each>
<xsl:if test="not(contains($hiddenGroups, 'Enkät'))">
<xsl:for-each select="$specific_attributes">
<xsl:variable name="attribute_id" select="#id"/>
<xsl:variable name="attribute_type" select="#attributeDefinitionType"/>
<xsl:if test="not(#group = preceding-sibling::*/#group)">
<fo:table-row>
<fo:table-cell padding-top="10mm" padding-bottom="5mm" number-columns-spanned="{1 + count($units)}">
<fo:block font-size="13" font-weight="bold" color="white" background-color="#0191ac" padding-top="2mm" padding-bottom="1.5mm" margin-bottom="2mm">
<fo:inline padding-left="4mm"><xsl:value-of select="#group"/></fo:inline>
</fo:block>
</fo:table-cell>
</fo:table-row>
</xsl:if>
<fo:table-row keep-with-previous="always" border-bottom-style="solid" border-bottom-width="thin" border-bottom-color="#D0D0D0">
<fo:table-cell display-align="center" padding-top="2mm" padding-bottom="2mm">
<fo:block margin-left="2mm" margin-right="2mm">
<xsl:value-of select="#name"/>
</fo:block>
</fo:table-cell>
<xsl:for-each select="$units">
<fo:table-cell padding-top="2mm" padding-bottom="2mm" padding-left="4mm" padding-right="4mm" display-align="center" border-left-style="solid" border-right-style="solid" border-width="0.1mm" border-color="#D0D0D0">
<xsl:call-template name="show-attribute">
<xsl:with-param name="type" select="$attribute_type"/>
<xsl:with-param name="attribute" select="Attributes/Attribute[#id=$attribute_id]"/>
<xsl:with-param name="count" select="count($units)"/>
</xsl:call-template>
</fo:table-cell>
</xsl:for-each>
</fo:table-row>
</xsl:for-each>
</xsl:if>
</fo:table-body>
</fo:table>
I'm marking this as answered and reference to the first comment from #lfurini. As for the last question #lfurini, I was looking at the wrong table when I thought I'd set the table width already. So to sum up for anyone else also wondering about this; If the width is set in fo:table, it's enough to set the width on one of the columns and the rest will adjust.

Converting XML to formatted text vial XSLT

I'm completely new to the game. The project I'm working on needs to be able to alternately render to either html or text. I decided to try XSLT's so I could modify the output without really changing the underlying code structure. HTML output is fine. Getting a little lost in trying to write a transform to text. Here's why:
The xml I'm transforming is within this type of structure:
<Data>
<Text x="0" y="1">First Line</Text>
<Text x="12" y="1">Continued on Same Line</Text>
<Text x="36" y="1">Still Going</Text>
<Text x="5" y="2">Slightly Indented New Line</Text>
</Data>
The basic template I'm using for html is working fine. I am trying now to create a template for a text output, ie
<xsl:output method="text"/>
but am as of yet unable to devise a way to build strings from Text elements based on "x" and "y" values (or coordinates), which is what I need to do for the text output such that what writes to the text file from the sample xml above is:
First Line Continued on Same Line Still Going
Slightly Indented New Line
So, if I were to do the equivalent in code, it might look like:
private string SomeMethod(XPathNavigator TestNav)
{
int iRow = 0;
int iColumn = 0;
XPathNodeIterator lineIterator = TestNav.SelectChildren("Data", "");
StringBuilder text = new StringBuilder();
while (lineIterator.MoveNext())
{
XPathNavigator curNav= lineIterator.Current;
XPathExpression Exp = curNav.Compile("*[#x]|*/*[#x]");
Exp.AddSort("#y", XmlSortOrder.Ascending, XmlCaseOrder.None, "", XmlDataType.Number);
Exp.AddSort("#x", XmlSortOrder.Ascending, XmlCaseOrder.None, "", XmlDataType.Number);
XPathNodeIterator positionIterator = curNav.Select(Exp);
while (positionIterator.MoveNext())
{
String elValue = positionIterator.Current.InnerXml;
int xTxt = int.Parse(positionIterator.Current.GetAttribute("x", ""));
int yTxt = int.Parse(positionIterator.Current.GetAttribute("y", ""));
if (iRow < yTxt)
{
string newLines = new string('\n', yTxt - iRow);
text = text.Append(newLines);
iColumn = 0;
}
if (iColumn < xTxt)
{
string newLines = new string(' ', xTxt - iColumn);
text = text.Append(newLines);
}
text = text.Append(elValue);
iRow = yTxt;
iColumn = xTxt + elValue.Length;
}
if (lineIterator.Count != 0)
{
text = text.Append("\n\f\n");
iRow = 0;
iColumn = 0;
}
}
return text.ToString();
}
Given the structure of the above xml, any ideas how I could go about doing the same thing in an XSLT, once again with the output method set to text so it persists to file so that complete lines are built from the x and y coordinates of the individual words. So, if for two words, "Hello World" they are represented as
<Text x="0" y="1">Hello</Text>
<Text x="6" y="1">World</Text>
Then "Hello" instantiates a string, and consumes 5 characters of space, 0-4. "World" starts at 6, so character index "5" is filled with white space (and would continue to do so until the next highest x attribute is reached). The next highest x attribute in a 'Text' element with the same y attribute is "6". So it's value is then appended to the existing string. Same logic for y values.
Please let me know if this isn't clear enough, I'll happily explain more, or differently.
This is a bit more complicated than it seems.
You must group <Text> items by #y. All with the same #y value are on the same line.
You must decide whether you want to represent empty/missing lines (i.e. non-consecutive #y values) or not. Not representing them is simpler.
You must decide whether you just want to concatenate everything that shares the same line or arrange it by the respective #x values individually. The latter is considerably more complicated.
As a minimum you can prefix the first item on every line by the amount of #x that was defined.
This XSLT transformation
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:key name="kLine" match="Text" use="#y" />
<xsl:variable name="padding" select="' '"/>
<xsl:template match="Data">
<xsl:for-each select="Text[generate-id() = generate-id(key('kLine', #y))]">
<xsl:apply-templates select="key('kLine', #y)">
<xsl:sort select="#x" data-type="number" />
</xsl:apply-templates>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
<xsl:template match="Text">
<xsl:if test="generate-id() = generate-id(key('kLine', #y))">
<xsl:value-of select="substring($padding, 1, #x)" />
</xsl:if>
<xsl:value-of select="." />
<xsl:if test="position() < last()">
<xsl:text> </xsl:text>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
gives with your input
First Line Continued on Same Line Still Going
Slightly Indented New Line
The notable technique used here is called Muenchian grouping.
FWIW, here is a solution that does what you request. Decide for yourself if it's worth the trouble.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:key name="kLine" match="Text" use="#y" />
<xsl:variable name="padX" select="' '"/>
<xsl:variable name="padY" select="'
'"/>
<xsl:template match="Data">
<xsl:for-each select="Text[generate-id() = generate-id(key('kLine', #y))]">
<xsl:sort select="#y" data-type="number" />
<xsl:apply-templates select="key('kLine', #y)">
<xsl:sort select="#x" data-type="number" />
</xsl:apply-templates>
<!-- find the vertical position of the logically following text -->
<xsl:variable name="nextY">
<xsl:for-each select="../Text[#y > current()/#y]">
<xsl:sort select="#y" data-type="number" />
<xsl:if test="position() = 1">
<xsl:value-of select="#y" />
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:value-of select="substring($padY, 1, $nextY - #y)" />
</xsl:for-each>
</xsl:template>
<xsl:template match="Text">
<!-- indent first item on line -->
<xsl:if test="generate-id() = generate-id(key('kLine', #y))">
<xsl:value-of select="substring($padX, 1, #x)" />
</xsl:if>
<xsl:variable name="text" select="normalize-space()" />
<!-- calculate the available text block width -->
<xsl:variable name="width">
<!-- find the horizontal position of the logically following text -->
<xsl:variable name="nextX">
<xsl:for-each select="key('kLine', #y)[#x > current()/#x]">
<xsl:sort select="#x" data-type="number" />
<xsl:if test="position() = 1">
<xsl:value-of select="#x" />
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:choose>
<xsl:when test="$nextX > 0">
<xsl:value-of select="$nextX - #x - 1" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="string-length($text)" />
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<!-- current text + necessary right padding for next item -->
<xsl:value-of select="substring(concat($text, $padX), 1, $width)" />
<xsl:if test="position() < last()">
<xsl:text> </xsl:text>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
used on this:
<Data>
<Text x="0" y="1">First Line</Text>
<Text x="20" y="1">Continued on Same Line</Text>
<Text x="42" y="1">Still Going</Text>
<Text x="4" y="5">Slightly Indented New Line</Text>
<Text x="30" y="3">30 spaces before this</Text>
</Data>
gives (line an grid numbers added for convenience):
| 1 2 3 4 5
| 5 0 5 0 5 0 5 0 5 0 5
-+-------------------------------------------------------
1|First Line Continued on Same Lin Still Going
2|
3| 30 spaces before this
4|
5| Slightly Indented New Line
Note how it cuts out text that would not fit. Also, this is not thoroughly tested, it's just a proof-of concept. It may behave all kinds of funny on your input.
Also note that you have to decide whether you want to use a 1-based (like with #y) or a 0-based (like with #x) co-ordindate system. Currently it's not consistent in your sample input.

Iterating in XSLT

I have the following XSL:
<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform' version='1.0'>
<xsl:param name='width' select ="270"/>
<xsl:param name='height' select="180"/>
<xsl:variable name="counter" select="0" />
<xsl:template name="while">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<line x1="{$counter}" y1="0.5" x2="{$counter}" y2="10.5" stroke="black" stroke-width="1" />
<xsl:variable name="counter" select="$counter + 10" />
<xsl:if test="$counter < $width">
<xsl:call-template name="while"/>
</xsl:if>
</svg>
</xsl:template>
</xsl:stylesheet>
I'm trying to get a line drawn every 10pixels across the width, like ruler markings.
When I run this code, it gets stuck in a loop. I can't debug, I just get a stack overflow exception. I presume either my counter value isn't increasing by 10, or that my evaluation of checking if the counter < width is incorrect.
Can someone please point me in the right direction?
I think you need to pass the params when calling your template.
Something like:
<xsl:template name="loop">
<xsl:param name="count" select="1"/>
<xsl:if test="$count > 0">
<xsl:call-template name="loop">
<xsl:with-param name="count" select="$count - 1"/>
</xsl:call-template>
<xsl:value-of select="$count"/>
<xsl:text> </xsl:text>
</xsl:if>
</xsl:template>
You need to pass the count to your template by using xsl:with-param.
Example:
<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform' version='1.0'>
<xsl:output indent="yes"/>
<xsl:param name='width' select ="270"/>
<xsl:param name='height' select="180"/>
<xsl:template match="/">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<xsl:call-template name="while"/>
</svg>
</xsl:template>
<xsl:template name="while">
<xsl:param name="currentCount" select="0"/>
<line x1="{$currentCount}" y1="0.5" x2="{$currentCount}" y2="10.5" stroke="black" stroke-width="1" />
<xsl:variable name="counter" select="$currentCount + 10" />
<xsl:if test="$counter < $width">
<xsl:call-template name="while">
<xsl:with-param name="currentCount" select="$counter"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>

C# FlowDocument to HTML conversion

Basically, I have a RichTextBox and I want to convert the formatted contents of it to HTML so it can be sent as an email.
The method I am currently using does not give any formatting at all:
string message = new TextRange(messageTextBox.Document.ContentStart,
messageTextBox.Document.ContentEnd).Text;
So I searched around and found this, however, it is over 5 years old and in the comments an MSFT user has commented saying that it is no longer supported - "This sample has been removed from our sample set and is no longer supported", and the HTML it generates is in an older format than modern HTML or XHTML which would be better to have.
Can anybody show me how I can convert the formatted contents of a RichTextBox to HTML?
(So when the email is sent it the recipient sees the email with formatting)
The general technique is to use a XamlWriter to convert the FlowDocument content to a stream of XML, and then to use an XSLT transform to convert the XML to HTML. That's not much of an answer, but that's because there's a huge range of possible HTML representations of any given FlowDocument.
This transform, for instance, converts every top-level Section to a div, every Paragraph to a p, and every Run to a span whose class tells you whether or not it's italicized, bold-faced, or underlined, or any combination of the above. It was useful for the purpose I wrote it for, but to call it a lossy transformation is an understatement:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="msxsl x">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
<xsl:template match="x:Section[not(parent::x:Section)]">
<div>
<xsl:apply-templates select="node()"/>
</div>
</xsl:template>
<xsl:template match="x:Section">
<xsl:apply-templates select="node()"/>
</xsl:template>
<xsl:template match="x:Paragraph">
<p>
<xsl:apply-templates select="node()"/>
</p>
</xsl:template>
<xsl:template match="x:Run">
<xsl:variable name="class">
<xsl:if test="#FontStyle='Italic'">
<xsl:text>i </xsl:text>
</xsl:if>
<xsl:if test="#FontWeight='Bold'">
<xsl:text>b </xsl:text>
</xsl:if>
<xsl:if test="contains(#TextDecorations, 'Underline')">
<xsl:text>u </xsl:text>
</xsl:if>
</xsl:variable>
<span>
<xsl:if test="normalize-space($class) != ''">
<xsl:attribute name="class">
<xsl:value-of select="normalize-space($class)"/>
</xsl:attribute>
</xsl:if>
<xsl:value-of select="text()"/>
</span>
</xsl:template>
</xsl:stylesheet>
Here's a value converter I wrote to do the conversion - note that in order to use the value converter, you also have to hack around and implement a version of RichTextBox that exposes the content as a dependency property. Really this whole project was a pain.
public class FlowDocumentToHtmlConverter : IValueConverter
{
private static XslCompiledTransform ToHtmlTransform;
private static XslCompiledTransform ToXamlTransform;
public FlowDocumentToHtmlConverter()
{
if (ToHtmlTransform == null)
{
ToHtmlTransform = LoadTransformResource("Converters/FlowDocumentToXhtml.xslt");
}
if (ToXamlTransform == null)
{
ToXamlTransform = LoadTransformResource("Converters/XhtmlToFlowDocument.xslt");
}
}
private static XslCompiledTransform LoadTransformResource(string path)
{
Uri uri = new Uri(path, UriKind.Relative);
XmlReader xr = XmlReader.Create(Application.GetResourceStream(uri).Stream);
XslCompiledTransform xslt = new XslCompiledTransform();
xslt.Load(xr);
return xslt;
}
#region IValueConverter Members
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (!(value is FlowDocument))
{
return null;
}
if (targetType == typeof(FlowDocument))
{
return value;
}
if (targetType != typeof(string))
{
throw new InvalidOperationException(
"FlowDocumentToHtmlConverter can only convert back from a FlowDocument to a string.");
}
FlowDocument d = (FlowDocument)value;
using (MemoryStream ms = new MemoryStream())
{
// write XAML out to a MemoryStream
TextRange tr = new TextRange(
d.ContentStart,
d.ContentEnd);
tr.Save(ms, DataFormats.Xaml);
ms.Seek(0, SeekOrigin.Begin);
// transform the contents of the MemoryStream to HTML
StringBuilder sb = new StringBuilder();
using (StringWriter sw = new StringWriter(sb))
{
XmlWriterSettings xws = new XmlWriterSettings();
xws.OmitXmlDeclaration = true;
XmlReader xr = XmlReader.Create(ms);
XmlWriter xw = XmlWriter.Create(sw, xws);
ToHtmlTransform.Transform(xr, xw);
}
return sb.ToString();
}
}
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null)
{
return new FlowDocument();
}
if (value is FlowDocument)
{
return value;
}
if (targetType != typeof(FlowDocument))
{
throw new InvalidOperationException(
"FlowDocumentToHtmlConverter can only convert to a FlowDocument.");
}
if (!(value is string))
{
throw new InvalidOperationException(
"FlowDocumentToHtmlConverter can only convert from a string or FlowDocument.");
}
string s = (string)value;
FlowDocument d;
using (MemoryStream ms = new MemoryStream())
using (StringReader sr = new StringReader(s))
{
XmlWriterSettings xws = new XmlWriterSettings();
xws.OmitXmlDeclaration = true;
using (XmlReader xr = XmlReader.Create(sr))
using (XmlWriter xw = XmlWriter.Create(ms, xws))
{
ToXamlTransform.Transform(xr, xw);
}
ms.Seek(0, SeekOrigin.Begin);
d = XamlReader.Load(ms) as FlowDocument;
}
XamlWriter.Save(d, Console.Out);
return d;
}
#endregion
}
Extended version of XSLT sheet above I thought might help. Not perfect but slightly more comprehensive. Extension of an extension of another answer referencing this one.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="msxsl x">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
<!--<xsl:template match="x:Section[not(parent::x:Section)]">
<div>
<xsl:apply-templates select="node()"/>
</div>
</xsl:template>-->
<xsl:template match="x:Section[not(parent::x:Section)]">
<xsl:variable name="style">
<xsl:if test="#FontStyle='Italic'">
<xsl:text>font-style:italic;</xsl:text>
</xsl:if>
<xsl:if test="#FontWeight='Bold'">
<xsl:text>font-weight:bold;</xsl:text>
</xsl:if>
<xsl:if test="contains(#TextDecorations, 'Underline')">
<xsl:text>text-decoration:underline;</xsl:text>
</xsl:if>
<xsl:if test="#FontSize != ''">
<xsl:text>font-size:</xsl:text>
<xsl:value-of select="#FontSize" />
<xsl:text>pt;</xsl:text>
</xsl:if>
<xsl:if test="#FontFamily != ''">
<xsl:text>font-family:</xsl:text>
<xsl:value-of select="#FontFamily" />
<xsl:text>;</xsl:text>
</xsl:if>
<xsl:if test="#Foreground != ''">
<xsl:text>color:</xsl:text>
<xsl:value-of select="concat(substring(#Foreground, 1, 1), substring(#Foreground, 4))" />
<xsl:text>;</xsl:text>
</xsl:if>
<xsl:if test="#Foreground-Color != ''">
<xsl:text>color:</xsl:text>
<xsl:value-of select="#Foreground-Color"/>
<xsl:text>;</xsl:text>
</xsl:if>
</xsl:variable>
<div>
<xsl:if test="normalize-space($style) != ''">
<xsl:attribute name="style">
<xsl:value-of select="normalize-space($style)"/>
</xsl:attribute>
</xsl:if>
<xsl:value-of select="text()"/>
<xsl:apply-templates select="node()"/>
</div>
</xsl:template>
<xsl:template match="x:Section">
<xsl:apply-templates select="node()"/>
</xsl:template>
<xsl:template match="x:Paragraph">
<xsl:variable name="style">
<xsl:if test="#FontStyle='Italic'">
<xsl:text>font-style:italic;</xsl:text>
</xsl:if>
<xsl:if test="#FontWeight='Bold'">
<xsl:text>font-weight:bold;</xsl:text>
</xsl:if>
<xsl:if test="contains(#TextDecorations, 'Underline')">
<xsl:text>text-decoration:underline;</xsl:text>
</xsl:if>
<xsl:if test="#FontSize != ''">
<xsl:text>font-size:</xsl:text>
<xsl:value-of select="#FontSize" />
<xsl:text>pt;</xsl:text>
</xsl:if>
<xsl:if test="#FontFamily != ''">
<xsl:text>font-family:</xsl:text>
<xsl:value-of select="#FontFamily" />
<xsl:text>;</xsl:text>
</xsl:if>
<xsl:if test="#Foreground != ''">
<xsl:text>color:</xsl:text>
<xsl:value-of select="concat(substring(#Foreground, 1, 1), substring(#Foreground, 4))" />
<xsl:text>;</xsl:text>
</xsl:if>
<xsl:if test="#Foreground-Color != ''">
<xsl:text>color:</xsl:text>
<xsl:value-of select="#Foreground-Color"/>
<xsl:text>;</xsl:text>
</xsl:if>
</xsl:variable>
<p>
<xsl:if test="normalize-space($style) != ''">
<xsl:attribute name="style">
<xsl:value-of select="normalize-space($style)"/>
</xsl:attribute>
</xsl:if>
<xsl:value-of select="text()"/>
<xsl:apply-templates select="node()"/>
</p>
</xsl:template>
<xsl:template match="x:Span">
<xsl:variable name="style">
<xsl:if test="#FontStyle='Italic'">
<xsl:text>font-style:italic;</xsl:text>
</xsl:if>
<xsl:if test="#FontWeight='Bold'">
<xsl:text>font-weight:bold;</xsl:text>
</xsl:if>
<xsl:if test="contains(#TextDecorations, 'Underline')">
<xsl:text>text-decoration:underline;</xsl:text>
</xsl:if>
<xsl:if test="#FontSize != ''">
<xsl:text>font-size:</xsl:text>
<xsl:value-of select="#FontSize" />
<xsl:text>pt;</xsl:text>
</xsl:if>
<xsl:if test="#FontFamily != ''">
<xsl:text>font-family:</xsl:text>
<xsl:value-of select="#FontFamily" />
<xsl:text>;</xsl:text>
</xsl:if>
<xsl:if test="#Foreground != ''">
<xsl:text>color:</xsl:text>
<xsl:value-of select="concat(substring(#Foreground, 1, 1), substring(#Foreground, 4))" />
<xsl:text>;</xsl:text>
</xsl:if>
<xsl:if test="#Foreground-Color != ''">
<xsl:text>color:</xsl:text>
<xsl:value-of select="#Foreground-Color"/>
<xsl:text>;</xsl:text>
</xsl:if>
</xsl:variable>
<span>
<xsl:if test="normalize-space($style) != ''">
<xsl:attribute name="style">
<xsl:value-of select="normalize-space($style)"/>
</xsl:attribute>
</xsl:if>
<xsl:value-of select="text()"/>
<xsl:apply-templates select="node()"/>
</span>
</xsl:template>
<xsl:template match="x:Run">
<xsl:variable name="style">
<xsl:if test="#FontStyle='Italic'">
<xsl:text>font-style:italic;</xsl:text>
</xsl:if>
<xsl:if test="#FontWeight='Bold'">
<xsl:text>font-weight:bold;</xsl:text>
</xsl:if>
<xsl:if test="contains(#TextDecorations, 'Underline')">
<xsl:text>text-decoration:underline;</xsl:text>
</xsl:if>
<xsl:if test="#FontSize != ''">
<xsl:text>font-size:</xsl:text>
<xsl:value-of select="#FontSize" />
<xsl:text>pt;</xsl:text>
</xsl:if>
<xsl:if test="#FontFamily != ''">
<xsl:text>font-family:</xsl:text>
<xsl:value-of select="#FontFamily" />
<xsl:text>;</xsl:text>
</xsl:if>
<xsl:if test="#Foreground != ''">
<xsl:text>color:</xsl:text>
<xsl:value-of select="concat(substring(#Foreground, 1, 1), substring(#Foreground, 4))" />
<xsl:text>;</xsl:text>
</xsl:if>
<xsl:if test="#Foreground-Color != ''">
<xsl:text>color:</xsl:text>
<xsl:value-of select="#Foreground-Color"/>
<xsl:text>;</xsl:text>
</xsl:if>
</xsl:variable>
<span>
<xsl:if test="normalize-space($style) != ''">
<xsl:attribute name="style">
<xsl:value-of select="normalize-space($style)"/>
</xsl:attribute>
</xsl:if>
<xsl:value-of select="text()"/>
<xsl:apply-templates select="node()"/>
</span>
</xsl:template>
</xsl:stylesheet>
For those who is looking for solution for .Net Core (APS.Net Core) - nuget MarkupConverter did the trick for me. Add dependency reference
<PackageReference Include="MarkupConverter" Version="1.0.6" />
Then use it
MarkupConverter.MarkupConverter markupConverter = new MarkupConverter.MarkupConverter();
try
{
message = markupConverter.ConvertXamlToHtml(message);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Failed to parse flowdocument");
}

Categories