Percipient Studios logo

Advanced XSLT with .NET Namespaces

21 September 2009
Filed under: xslt, advanced - by Douglas Robar

I prefer to use XSLT macros whenever possible, resorting to C# only when I absolutely have to.

Until recently I have created .net macros for even simple functionality that I would have preferred to put in an easily-maintained XSLT macro. Why? Because I couldn't access a needed namespace from within XSLT.

An example: file level operations

A recent site I worked on needed to display the size of a file. If the file were in umbraco's Media section this would be easy with the built-in umbracoBytes property. But in this case, the file was uploaded rather than placed in umbraco's Media section. Thus, the usual umbraco metadata was not available.

Various solutions presented themselves:

  • Add an 'umbracoBytes' property (of type label or static text) to my document type. Umbraco will auto-populate this property when a file is uploaded (this is VERY cool and a great tip to remember!). In my case, however, I had four upload fields in the docType and whatever file was uploaded last would have its filesize placed in the 'umbracoBytes' field. I needed a solution that would work for all the files uploaded.
  • Create an event handler setting a unique uploadFile1_umbracoBytes, uploadFile2_umbracoBytes, etc. property when uploading the file. This seemed like too much effort and I would be sure to forget to update the event handler should a fifth upload field be added to the docType later on.
  • Create a custom XSLT extension to get the filesize information. This is a good solution but a whole extension seemed rather over-kill for a few lines of C# code.

The C# function I wanted to use was very simple, taking the path to a file and returning the filesize in kilobytes. This could certainly be done as an XSLT extension:

public String uploadFileSize(String filePath) {

    if ((filePath != null) && (filePath.Length != 0)) {
        String localFile = HttpContext.Current.Server.MapPath(filePath);
        if (File.Exists(localFile)) {
            FileInfo fileinfo = new FileInfo(localFile);
            return String.Format("{0:#,###,###.##}", (fileinfo.Length / 1024));
        }
    }
    return null;
}

Using .NET namespaces in your XSLT macros

What I really wanted to do was augment my XSLT macro with that bit of C# code to get the file size from within the XSLT file itself, without needing to create a .NET control, some app_code, an event handler, or custom XSLT extension.

Unfortunately, file level access and many other fun capabilities are not allowed in your umbraco XSLT macros. Not by default, that is. But it's really simple to add that ability with in-line C# (a topic we'll cover in another post) in your XSLT macro.

<msxml:script language="CSharp" implements-prefix="ps">
    <msxml:using namespace="System.IO" />
    <msxml:assembly name="System.Web" />
    <msxml:using namespace="System.Web" />

    <![CDATA[
    public String uploadFileSize(String filePath) {

        if ((filePath != null) && (filePath.Length != 0)) {
            String localFile = HttpContext.Current.Server.MapPath(filePath);
            if (File.Exists(localFile)) {
                FileInfo fileinfo = new FileInfo(localFile);
                return String.Format("{0:#,###,###.##}", (fileinfo.Length / 1024));
            }
        }
        return null;
    }
    ]]>
</msxml:script>


The msxml:using and msxml:assembly statements are the important bits. The msxml:assembly loads the assembly like adding a reference in a .NET project. The msxml:using makes using the functions shorter with no prefix required, just as using System.IO; and using System.Web; in your C# code would do.

I found System.IO was already available so I didn't need to add the assembly statement for it. System.Web was not available so I added it with the msxml:assembly statement.

Completed XSLT macro with in-line .NET code

Here is a complete (and very simple, rather hard-coded) XSLT file. Your macros would probably have more logic in the main template, possibly set the uploadedFile variable with a macro parameter (using a propertyPicker data type), and include additional checks for errors. 

<!DOCTYPE xsl:stylesheet [
    <!ENTITY nbsp "&#x00A0;">
]>
<xsl:stylesheet
    version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxml="urn:schemas-microsoft-com:xslt"
    xmlns:ps="urn:schemas-percipient-studios"
    xmlns:umbraco.library="urn:umbraco.library"
    exclude-result-prefixes="msxml ps umbraco.library" >

    <xsl:output method="xml" omit-xml-declaration="yes" />

    <xsl:param name="currentPage"/>

    <!-- =========================================================== -->

    <xsl:template match="/">
        <xsl:variable name="uploadedFile" select="$currentPage/data[@alias='uploadedFile']" />
        <xsl:if test="string($uploadedFile) != ''">
            <p>
                The size of the uploaded file is:
                <xsl:value-of select="ps:uploadFileSize($uploadedFile)" /> kb
            </p>
        </xsl:if>
    </xsl:template>

    <!-- =========================================================== -->

    <msxml:script language="CSharp" implements-prefix="ps">
        <msxml:using namespace="System.IO" />
        <msxml:assembly name="System.Web" />
        <msxml:using namespace="System.Web" />

        <![CDATA[
        public String uploadFileSize(String filePath) {

            if ((filePath != null) && (filePath.Length != 0)) {
                String localFile = HttpContext.Current.Server.MapPath(filePath);
                if (File.Exists(localFile)) {
                    FileInfo fileinfo = new FileInfo(localFile);
                    return String.Format("{0:#,###,###.##}", (fileinfo.Length / 1024));
                }
            }
            return null;
        }
        ]]>
    </msxml:script>

    <!-- =========================================================== -->
                
</xsl:stylesheet>

Conclusion

As you can see, extending your XSLT macros with a bit of .NET functionality is very simple and will help you make the most of your macros. And because XSLT macros are compiled, performance is not a problem, though I also cached this macro heavily as well just to be on the safe side.

11 comments for “Advanced XSLT with .NET Namespaces”

  1. Gravatar of Casey Neehouse
    Casey Neehouse says:
    Great write-up. However...

    One thing to note is that using msxsl:script tags causes files to be generated in the temp directory on the server. These files tend to not be removed by the application, and can not be deleted while the application is in use. They also regenerate with each application start.

    This can be problematic to say the least. Thus, I don't recommend it anymore.
  2. Gravatar of Douglas Robar
    Douglas Robar says:
    Nice point, Casey.

    You're right that using in-line c# will cause a temporary file to be created in the %TEMP% folder. But it shouldn't stay around forever.

    According to MSDN...

    Temporary files are sometimes generated during XSLT processing. If a style sheet contains script blocks, or if it is compiled with the debug setting set to true, temporary files may be created in the %TEMP% folder. There may be instances when some temporary files are not deleted due to timing issues. For example, if the files are in use by the current AppDomain or by the debugger, the finalizer of the TempFileCollection object will not be able to remove them.

    The TemporaryFiles property can be used for additional cleanup to make sure that all temporary files are removed from the client.


    I haven't checked the source code, but perhaps umbraco needs to remove any remaining temporary files after running an xslt as noted at http://msdn.microsoft.com/en-us/library/system.xml.xsl.xslcompiledtransform.temporaryfiles%28VS.80%29.aspx?

    cheers,
    doug.
  3. Gravatar of Simon Justesen
    Simon Justesen says:
    Excellent article, like your writing style! I did something similar here: http://our.umbraco.org/wiki/reference/xslt/extend-your-xslt-with-custom-functions
  4. Gravatar of Douglas Robar
    Douglas Robar says:
    I found the solution to the temporary files issue that Casey reported. Simply be sure to set umbracoDebugMode to false in the web.config file.
    <add key="umbracoDebugMode" value="false" />

    In the umbraco source is:
    XslCompiledTransform macroXSLT = new XslCompiledTransform(debugMode);

    By disabling the debug mode in the web.config (which you should do for any production site) you'll ensure that XslCompiledTransform() doesn't create temporary files... at least in the majority of cases.

    Though there's some lingering discussion about temporary files being created by .net in some situations no matter the debug setting, the amount of files is drastically reduced if not eliminated, which should resolve the concern of temporary files and application pool memory growth with msxml:script blocks in your XSLT macros.

    cheers,
    doug.
  5. Gravatar of Jason
    Jason says:
    Is there any performance hit for this method over using a C# User Control?
  6. Gravatar of Jonny Heavey
    Jonny Heavey says:
    Hi Guys,

    Firstly, thanks for this post - I've found it very useful to date. However, I'm experiencing some problems when trying to use a spell checking library call 'NHunspell'. I appreciate this is a specific question, but is there any immediate and glaringly obvious reason why using this wouldn't work?
  7. Gravatar of Douglas Robar
    Douglas Robar says:
    @Jonny,

    I wouldn't think so, provided the library were built for use in web projects, rather than only in desktop apps.

    cheers,
    doug.
  8. Gravatar of Graham
    Graham says:
    In case anyone is interested I adapted the above example to find the width and height of images that aren't in the media section.

    public String imageHeight(String filePath) {
    if ((filePath != null) && (filePath.Length != 0)) {
    String localFile = HttpContext.Current.Server.MapPath(filePath);
    if (File.Exists(localFile)) {
    Image img = Image.FromFile(localFile);
    return String.Format("{0:#,###,###.##}",(img.Size.Height));
    }
    }
    return null;
    }


    public String imageWidth(String filePath) {
    if ((filePath != null) && (filePath.Length != 0)) {
    String localFile = HttpContext.Current.Server.MapPath(filePath);
    if (File.Exists(localFile)) {
    Image img = Image.FromFile(localFile);
    return String.Format("{0:#,###,###.##}",(img.Size.Width));
    }
    }
    return null;
    }
  9. Gravatar of Karthick
    Karthick says:
    Hi there,
    Is there way to call a method in a existing .NET Assembly ?

    I mean having the method uploadFileSize in a externla assembly and calling it from this XSLT ?

    Cheers,
    Karthick G.
  10. Gravatar of Douglas Robar
    Douglas Robar says:
    @Karthick G. - You certainly can!

    You can either add your dll's namespace and assembly information in the same way we did for System.Web.

    Or, you could create an XSLT extension with your logic in it (a topic for another post, but lots of information online already so google it to find some resources). Using an XSLT extension was the third option I considered but in this instance it was more effort than this simple task required. In many other cases an XSLT extension would be an excellent choice.

    cheers,
    doug.
  11. Gravatar of Karthick
    Karthick says:
    Hi Doug,

    I agree with your option of using XSLT Extensions. But i would prefer everthign to be in one XSLT.

    The reason behind this is to make the XSLT debuggable in Visual Studio IDE. I hope you understand my point.

    Please guide with any links that you have for my formar request.

    Cheers,
    Karthick G.
 
powered by <XSLTsearch>

Categories

Follow Us

RSS Feed
Follow us on Twitter
Follow us on Flickr